存储-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
posted @ 2026-01-09 18:37  李白的白  阅读(17)  评论(0)    收藏  举报