存储-BRAM控制
BRAM控制器
bram_controller.v
`timescale 1ns / 1ps
//////////////////////////////////////////////////////////////////////////////////
// 双端口BRAM控制器,支持并发读写操作
// 用于存储特征图和中间结果
// 支持突发传输和地址自动递增
// - 支持真双端口操作
// - 可配置的存储深度和宽度
// - 内置地址边界检查
//////////////////////////////////////////////////////////////////////////////////
module bram_controller #(
// 参数定义部分
parameter ADDR_WIDTH = 10, // 地址位宽,决定存储深度:2^10 = 1024
parameter DATA_WIDTH = 32, // 数据位宽
parameter DEPTH = 1024, // 存储深度
parameter INIT_FILE = "", // 初始化文件路径(可选)
parameter USE_OUTPUT_REG = 1, // 是否使用输出寄存器(提高时序)
parameter WRITE_MODE = "NO_CHANGE" // 写模式:"WRITE_FIRST", "READ_FIRST", "NO_CHANGE"
)(
// ========================================
// 端口A - 主要用于写操作
// ========================================
input wire clka, // 端口A时钟
input wire ena, // 端口A使能
input wire wea, // 端口A写使能
input wire [ADDR_WIDTH-1:0] addra, // 端口A地址
input wire [DATA_WIDTH-1:0] dina, // 端口A写数据
output reg [DATA_WIDTH-1:0] douta, // 端口A读数据
// ========================================
// 端口B - 主要用于读操作
// ========================================
input wire clkb, // 端口B时钟
input wire enb, // 端口B使能
input wire web, // 端口B写使能
input wire [ADDR_WIDTH-1:0] addrb, // 端口B地址
input wire [DATA_WIDTH-1:0] dinb, // 端口B写数据
output reg [DATA_WIDTH-1:0] doutb, // 端口B读数据
// ========================================
// 控制和状态信号
// ========================================
input wire rst_n, // 异步复位(低有效)
output reg [31:0] read_count, // 读操作计数
output reg [31:0] write_count, // 写操作计数
output wire conflict, // 地址冲突指示
output wire [ADDR_WIDTH-1:0] max_addr // 最大有效地址
);
// ========================================
// 内部信号声明
// ========================================
// BRAM存储阵列 - 这是实际的存储器
(* ram_style = "block" *) // 强制综合工具使用BRAM
reg [DATA_WIDTH-1:0] bram_array [0:DEPTH-1];
// 内部寄存器(用于不同的写模式)
reg [DATA_WIDTH-1:0] douta_reg;
reg [DATA_WIDTH-1:0] doutb_reg;
// 地址冲突检测
reg conflict_detected;
// 延迟一拍的地址(用于某些写模式)
reg [ADDR_WIDTH-1:0] addra_delayed;
reg [ADDR_WIDTH-1:0] addrb_delayed;
// ========================================
// 初始化逻辑
// ========================================
// 从文件初始化BRAM(如果提供了初始化文件)
initial begin
if (INIT_FILE != "") begin
$readmemh(INIT_FILE, bram_array);
$display("BRAM初始化完成,从文件: %s", INIT_FILE);
end else begin
// 如果没有初始化文件,初始化为0
integer i;
for (i = 0; i < DEPTH; i = i + 1) begin
bram_array[i] = {DATA_WIDTH{1'b0}};
end
$display("BRAM初始化为0");
end
// 初始化计数器
read_count = 32'd0;
write_count = 32'd0;
end
// ========================================
// 端口A操作逻辑
// ========================================
always @(posedge clka) begin
if (!rst_n) begin
// 复位时清零输出
douta_reg <= {DATA_WIDTH{1'b0}};
addra_delayed <= {ADDR_WIDTH{1'b0}};
end else if (ena) begin
// 端口A使能时的操作
// 保存当前地址(某些模式需要)
addra_delayed <= addra;
// 边界检查
if (addra < DEPTH) begin
if (wea) begin
// 写操作
bram_array[addra] <= dina;
write_count <= write_count + 1;
// 根据写模式决定输出
case (WRITE_MODE)
"WRITE_FIRST": douta_reg <= dina; // 输出新写入的数据
"READ_FIRST": douta_reg <= bram_array[addra]; // 输出原数据
"NO_CHANGE": douta_reg <= douta_reg; // 保持不变
default: douta_reg <= dina;
endcase
`ifdef SIMULATION
$display("BRAM写入: 地址=%h, 数据=%h @ %t", addra, dina, $time);
`endif
end else begin
// 读操作
douta_reg <= bram_array[addra];
read_count <= read_count + 1;
`ifdef SIMULATION
$display("BRAM读取: 地址=%h, 数据=%h @ %t", addra, bram_array[addra], $time);
`endif
end
end else begin
// 地址越界处理
douta_reg <= {DATA_WIDTH{1'bx}}; // 输出未知值表示错误
`ifdef SIMULATION
$display("ERROR: BRAM地址越界! 地址=%h, 最大=%h @ %t", addra, DEPTH-1, $time);
`endif
end
end
end
// ========================================
// 端口B操作逻辑(类似端口A)
// ========================================
always @(posedge clkb) begin
if (!rst_n) begin
doutb_reg <= {DATA_WIDTH{1'b0}};
addrb_delayed <= {ADDR_WIDTH{1'b0}};
end else if (enb) begin
addrb_delayed <= addrb;
if (addrb < DEPTH) begin
if (web) begin
// 写操作
bram_array[addrb] <= dinb;
write_count <= write_count + 1;
case (WRITE_MODE)
"WRITE_FIRST": doutb_reg <= dinb;
"READ_FIRST": doutb_reg <= bram_array[addrb];
"NO_CHANGE": doutb_reg <= doutb_reg;
default: doutb_reg <= dinb;
endcase
end else begin
// 读操作
doutb_reg <= bram_array[addrb];
read_count <= read_count + 1;
end
end else begin
doutb_reg <= {DATA_WIDTH{1'bx}};
end
end
end
// ========================================
// 输出寄存器选择
// ========================================
generate
if (USE_OUTPUT_REG) begin : gen_output_reg
// 使用额外的输出寄存器(改善时序,增加1拍延迟)
always @(posedge clka) begin
if (!rst_n)
douta <= {DATA_WIDTH{1'b0}};
else
douta <= douta_reg;
end
always @(posedge clkb) begin
if (!rst_n)
doutb <= {DATA_WIDTH{1'b0}};
else
doutb <= doutb_reg;
end
end else begin : gen_no_output_reg
// 直接输出(减少延迟)
always @(*) begin
douta = douta_reg;
doutb = doutb_reg;
end
end
endgenerate
// ========================================
// 地址冲突检测
// ========================================
always @(*) begin
// 检测两个端口是否同时访问相同地址
conflict_detected = ena && enb && (addra == addrb) && (wea || web);
end
assign conflict = conflict_detected;
// ========================================
// 辅助输出
// ========================================
assign max_addr = DEPTH - 1;
// ========================================
// 断言检查(仅用于仿真)
// ========================================
`ifdef SIMULATION
// 检查地址是否有效
always @(posedge clka) begin
if (ena && (addra >= DEPTH)) begin
$display("ASSERT ERROR: 端口A地址越界 %h >= %h", addra, DEPTH);
end
end
always @(posedge clkb) begin
if (enb && (addrb >= DEPTH)) begin
$display("ASSERT ERROR: 端口B地址越界 %h >= %h", addrb, DEPTH);
end
end
// 冲突警告
always @(posedge clka or posedge clkb) begin
if (conflict_detected) begin
$display("WARNING: BRAM地址冲突检测到! addra=%h, addrb=%h @ %t",
addra, addrb, $time);
end
end
`endif
endmodule
BRAM测试
tb_bram_controller.v
`timescale 1ns / 1ps
//////////////////////////////////////////////////////////////////////////////////
// 测试内容:
// 1. 单端口读写
// 2. 双端口并发操作
// 3. 地址冲突检测
// 4. 边界条件测试
//////////////////////////////////////////////////////////////////////////////////
module tb_bram_controller;
// ========================================
// 参数定义
// ========================================
parameter ADDR_WIDTH = 10;
parameter DATA_WIDTH = 32;
parameter DEPTH = 1024;
parameter CLK_PERIOD = 5; // 5ns = 200MHz
// ========================================
// 测试信号声明
// ========================================
// 时钟和复位
reg clka, clkb;
reg rst_n;
// 端口A信号
reg ena;
reg wea;
reg [ADDR_WIDTH-1:0] addra;
reg [DATA_WIDTH-1:0] dina;
wire [DATA_WIDTH-1:0] douta;
// 端口B信号
reg enb;
reg web;
reg [ADDR_WIDTH-1:0] addrb;
reg [DATA_WIDTH-1:0] dinb;
wire [DATA_WIDTH-1:0] doutb;
// 状态信号
wire [31:0] read_count;
wire [31:0] write_count;
wire conflict;
wire [ADDR_WIDTH-1:0] max_addr;
// 测试变量
integer i, j;
integer errors;
reg [DATA_WIDTH-1:0] expected_data;
// ========================================
// DUT实例化
// ========================================
bram_controller #(
.ADDR_WIDTH(ADDR_WIDTH),
.DATA_WIDTH(DATA_WIDTH),
.DEPTH(DEPTH),
.USE_OUTPUT_REG(1),
.WRITE_MODE("WRITE_FIRST")
) DUT (
// 端口A
.clka(clka),
.ena(ena),
.wea(wea),
.addra(addra),
.dina(dina),
.douta(douta),
// 端口B
.clkb(clkb),
.enb(enb),
.web(web),
.addrb(addrb),
.dinb(dinb),
.doutb(doutb),
// 控制
.rst_n(rst_n),
.read_count(read_count),
.write_count(write_count),
.conflict(conflict),
.max_addr(max_addr)
);
// ========================================
// 时钟生成
// ========================================
// 端口A时钟
initial begin
clka = 0;
forever #(CLK_PERIOD/2) clka = ~clka;
end
// 端口B时钟(可以是不同频率)
initial begin
clkb = 0;
forever #(CLK_PERIOD/2) clkb = ~clkb;
end
// ========================================
// 测试任务定义
// ========================================
// 任务:写入数据到端口A
task write_porta;
input [ADDR_WIDTH-1:0] addr;
input [DATA_WIDTH-1:0] data;
begin
@(posedge clka);
ena = 1'b1;
wea = 1'b1;
addra = addr;
dina = data;
@(posedge clka);
ena = 1'b0;
wea = 1'b0;
$display("[%t] 端口A写入: 地址=%0h, 数据=%0h", $time, addr, data);
end
endtask
// 任务:从端口A读取数据
task read_porta;
input [ADDR_WIDTH-1:0] addr;
output [DATA_WIDTH-1:0] data;
begin
@(posedge clka);
ena = 1'b1;
wea = 1'b0;
addra = addr;
@(posedge clka); // 等待一个周期
@(posedge clka); // 如果USE_OUTPUT_REG=1,需要额外一拍
data = douta;
ena = 1'b0;
$display("[%t] 端口A读取: 地址=%0h, 数据=%0h", $time, addr, data);
end
endtask
// 任务:写入数据到端口B
task write_portb;
input [ADDR_WIDTH-1:0] addr;
input [DATA_WIDTH-1:0] data;
begin
@(posedge clkb);
enb = 1'b1;
web = 1'b1;
addrb = addr;
dinb = data;
@(posedge clkb);
enb = 1'b0;
web = 1'b0;
$display("[%t] 端口B写入: 地址=%0h, 数据=%0h", $time, addr, data);
end
endtask
// 任务:从端口B读取数据
task read_portb;
input [ADDR_WIDTH-1:0] addr;
output [DATA_WIDTH-1:0] data;
begin
@(posedge clkb);
enb = 1'b1;
web = 1'b0;
addrb = addr;
@(posedge clkb);
@(posedge clkb); // USE_OUTPUT_REG延迟
data = doutb;
enb = 1'b0;
$display("[%t] 端口B读取: 地址=%0h, 数据=%0h", $time, addr, data);
end
endtask
// ========================================
// 主测试流程
// ========================================
initial begin
// 初始化波形记录
$dumpfile("bram_test.vcd");
$dumpvars(0, tb_bram_controller);
// 打印测试开始
$display("\n");
$display("========================================");
$display(" BRAM控制器测试开始");
$display("========================================");
$display("配置参数:");
$display(" 地址宽度: %0d", ADDR_WIDTH);
$display(" 数据宽度: %0d", DATA_WIDTH);
$display(" 存储深度: %0d", DEPTH);
$display("========================================\n");
// 初始化信号
rst_n = 1'b0;
ena = 1'b0;
wea = 1'b0;
addra = {ADDR_WIDTH{1'b0}};
dina = {DATA_WIDTH{1'b0}};
enb = 1'b0;
web = 1'b0;
addrb = {ADDR_WIDTH{1'b0}};
dinb = {DATA_WIDTH{1'b0}};
errors = 0;
// 复位
#(CLK_PERIOD*10);
rst_n = 1'b1;
#(CLK_PERIOD*5);
// ========================================
// 测试1:基本单端口读写
// ========================================
$display("\n--- 测试1:基本单端口读写 ---");
// 写入测试数据
for (i = 0; i < 10; i = i + 1) begin
write_porta(i, 32'hA000_0000 + i);
end
// 读取并验证
for (i = 0; i < 10; i = i + 1) begin
read_porta(i, expected_data);
if (expected_data != (32'hA000_0000 + i)) begin
$display("ERROR: 地址%0d读取错误!期望=%0h,实际=%0h",
i, 32'hA000_0000 + i, expected_data);
errors = errors + 1;
end
end
if (errors == 0) begin
$display("测试1通过!");
end else begin
$display("测试1失败!错误数:%0d", errors);
end
// ========================================
// 测试2:双端口并发读写
// ========================================
$display("\n--- 测试2:双端口并发读写 ---");
errors = 0;
// 并发写入
fork
begin
for (i = 100; i < 110; i = i + 1) begin
write_porta(i, 32'hB000_0000 + i);
#(CLK_PERIOD*2);
end
end
begin
for (j = 200; j < 210; j = j + 1) begin
write_portb(j, 32'hC000_0000 + j);
#(CLK_PERIOD*2);
end
end
join
// 交叉读取验证
for (i = 100; i < 110; i = i + 1) begin
read_portb(i, expected_data); // 用端口B读端口A写的数据
if (expected_data != (32'hB000_0000 + i)) begin
$display("ERROR: 交叉读取错误!");
errors = errors + 1;
end
end
if (errors == 0) begin
$display("测试2通过!");
end else begin
$display("测试2失败!错误数:%0d", errors);
end
// ========================================
// 测试3:地址冲突检测
// ========================================
$display("\n--- 测试3:地址冲突检测 ---");
// 同时访问相同地址
@(posedge clka);
ena = 1'b1;
wea = 1'b1;
addra = 10'h300;
dina = 32'hDEAD_BEEF;
@(posedge clkb);
enb = 1'b1;
web = 1'b0;
addrb = 10'h300; // 相同地址
#(CLK_PERIOD);
if (conflict) begin
$display("地址冲突正确检测到!");
end else begin
$display("ERROR: 地址冲突检测失败!");
errors = errors + 1;
end
ena = 1'b0;
enb = 1'b0;
// ========================================
// 测试4:边界条件测试
// ========================================
$display("\n--- 测试4:边界条件测试 ---");
// 写入最大地址
write_porta(max_addr, 32'hFFFF_FFFF);
read_porta(max_addr, expected_data);
if (expected_data == 32'hFFFF_FFFF) begin
$display("最大地址访问成功!");
end else begin
$display("ERROR: 最大地址访问失败!");
errors = errors + 1;
end
// ========================================
// 测试5:突发传输测试
// ========================================
$display("\n--- 测试5:突发传输测试 ---");
// 连续写入
@(posedge clka);
ena = 1'b1;
wea = 1'b1;
for (i = 500; i < 520; i = i + 1) begin
addra = i;
dina = i * 100;
@(posedge clka);
end
ena = 1'b0;
wea = 1'b0;
// 连续读取
@(posedge clka);
ena = 1'b1;
wea = 1'b0;
for (i = 500; i < 520; i = i + 1) begin
addra = i;
@(posedge clka);
@(posedge clka); // 等待数据
if (douta != i * 100) begin
$display("ERROR: 突发读取错误!地址=%0d", i);
errors = errors + 1;
end
end
ena = 1'b0;
if (errors == 0) begin
$display("突发传输测试通过!");
end
// ========================================
// 打印最终统计
// ========================================
#(CLK_PERIOD*10);
$display("\n========================================");
$display(" 测试完成统计");
$display("========================================");
$display("总读操作数: %0d", read_count);
$display("总写操作数: %0d", write_count);
$display("总错误数: %0d", errors);
if (errors == 0) begin
$display("\n所有测试通过!✓");
end else begin
$display("\n测试失败!共%0d个错误 ✗", errors);
end
$display("========================================\n");
#(CLK_PERIOD*10);
$finish;
end
// ========================================
// 监控输出(用于调试)
// ========================================
always @(posedge clka) begin
if (conflict) begin
$display("[%t] WARNING: 检测到地址冲突!", $time);
end
end
endmodule

浙公网安备 33010602011771号