本文以随机存取存储器(RAM)为核心,系统讲解其在FPGA开发中的设计方法与实践技巧。文章主要通过对Vivado Block Memory Generator IP的仿真对比,用verilog 实现真双端口RAM,包含时钟异步时的处理。

  以下有部分内容摘自Xilinx官方手册,如有理解差异,请参考原手册。
  Xilinx 简单双口RAM框图如下:
在这里插入图片描述

图1 真双口RAM框图

1 Xilinx 真双端口RAM的配置

1.1 Basic页

  实际配置如下,Basic页Memory Type选择True Dual Port RAM,其他配置属性与单端口RAM相同,要了解详细属性说明,可以参考使用verilog代码实现RAM—单端口RAM
在这里插入图片描述

图2 Basic 页

1.2 Port A Options页

  Port A Options页配置如下,配置属性与单端口相同,同样可以配置Operating Mode,后文进行仿真时会验证与单端口是否有区别。
在这里插入图片描述

图3 Port A Options

1.3 Port B Options页

  相比于单端口RAM,真双端口RAM多出一个PORTB口,如下为Port B Options页配置,B口可配置写和读位宽,相比与PORT A 多出一个Enable ECC PIPE选项(选择ECC时有效),用于ECC时PIPELINE,其他属性相同。
在这里插入图片描述

图4 Port B Options

1.4 Other Options页

  Other Options页配置与单端口RAM相同。
在这里插入图片描述

图5 Other Options

1.5 Summary页

  Summary页属性与单端口RAM相同。
在这里插入图片描述

图6 Summary

2 Xilinx 真双端口RAM的仿真

2.1 对读写冲突的仿真

  配置模式Port A:No Change,Port B:Write First;
  仿真结果如下,A端口首次写入时,输出端口为0,然后对1地址进行读操作,输出端口为0x3333,下一个周期在2地址写入0x7777,输出端口保持上一次读出的0x3333,与单端口RAM的No Change 一致;对B端口进行写操作时,输出端口立马更新为当前写入的值,与单端口RAM的Write First 一致。
在这里插入图片描述

图7 A端口No Change, B端口Write First

  配置模式Port A:Read First,Port B:No Change;
  A端口第1次进行读操作时,读出端口数据为0x3333,下一个周期对2地址写入0x7777,此时输出端口为0x5555,保持上一次写入的旧值,与单端口RAM的Read First 一致;;对B端口首次写入时,输出端口数据为0,其后对1地址进行读操作,输出端口为0x4444,再下一个周期对1地址写入0x8888,此时输出端口保持上一次读出的值0x4444,与单端口RAM的No Change 一致。
在这里插入图片描述

图8 A端口Read First, B端口No Change

  配置模式Port A:No Change,Port B:No Change;
  A、B端口同时进行写操作,写入不同值,再同时读,可以看出读出的数据分别为0x6666、0x4444、0x2222,为通过B端口写入的,因此A、B端口同时写入时,B端口优先,A、B端口同时读取时,同时输出;A端口写、B端口读时,A端口保持上一次读取的值,B端口输出当前A端口写入的值,A端口读、B端口写时,A端口输出上一次写入的值,B端口输出上一次读取的值。
在这里插入图片描述

图9 A端口No Change, B端口No Change

2.2 异步时钟操作的仿真

  A端口慢时钟,B端口快时钟,第一阶段A、B端口同时写入5、6、7地址时,由于A端口后采集,写入A端口的值;第二阶段通过同时读7、6、5地址可以看出B端口输出值分别为0x5555、0x3333、0x1111;第三阶段通过A端口写入5、6、7地址,A端口输出保持上一次读出的值0x1111,B端口读出A端口写入的值;第四阶段通过B端口在7、6、5地址写入0xAAAA、0xBBBB、0xCCCC,A端口输出B端口写入的数据。
在这里插入图片描述

图10 A端口慢时钟,B端口快时钟

  A端口快时钟,B端口慢时钟,第一阶段A、B端口同时写入5、6、7地址时,由于B端口后采集,写入B端口的值;第二阶段通过同时读7、6、5地址可以看出A端口输出值分别为0x6666、0x4444、0x2222;第三阶段通过A端口写入5、6、7地址,A端口输出保持上一次读出的值0x2222,B端口读出B端口之前写入的值;第四阶段通过B端口在7、6、5地址写入0xAAAA、0xBBBB、0xCCCC,A端口输出A端口之前写入的数据。
在这里插入图片描述

图11 A端口快时钟,B端口慢时钟

3 Verilog 实现真双端口RAM

3.1 同步时钟代码实现

  若读写时钟为同步时钟,如下为简单双端口RAM的 verilog 代码实现。

module tdpram #(
    parameter DATA_WIDTH = 16,      // 数据位宽
    parameter ADDR_WIDTH = 4        // 地址位宽
)(
    input  wire                         clk       ,  // 共用时钟
    // 端口A接口
    input  wire                         wr_ena    ,  // 端口A写使能
    input  wire        [ADDR_WIDTH-1:0] wr_addra  ,  // 端口A地址
    input  wire        [DATA_WIDTH-1:0] wr_dataa  ,  // 端口A输入数据
    output reg         [DATA_WIDTH-1:0] rd_dataa  ,  // 端口A输出数据
    
    // 端口B接口
    input  wire                         wr_enb    ,  // 端口B写使能
    input  wire        [ADDR_WIDTH-1:0] wr_addrb  ,  // 端口B地址
    input  wire        [DATA_WIDTH-1:0] wr_datab  ,  // 端口B输入数据
    output reg         [DATA_WIDTH-1:0] rd_datab     // 端口B输出数据
);

// 存储器定义
reg [DATA_WIDTH-1:0] mem [(1<<ADDR_WIDTH)-1:0];

// 端口A操作逻辑
always @(posedge clk) begin
    if (wr_ena) begin                               // 写操作优先级判断                                     
        if (wr_enb && (wr_addra == wr_addrb)) begin // 冲突检测:当两端口同时写同一地址时,端口B优先
            mem[wr_addra] <= wr_datab;              // 端口B数据写入
        end else begin
            mem[wr_addra] <= wr_dataa;              // 正常写入端口A数据
        end
    end                                
    rd_dataa <= mem[wr_addra];                      // 读操作(始终读取当前存储值)
end

// 端口B操作逻辑
always @(posedge clk) begin
    if (wr_enb) begin
        mem[wr_addrb] <= wr_datab;                  // 端口B始终可写
    end
    rd_datab <= mem[wr_addrb];                      // 读操作
end

endmodule

  testbench如下:

module tb_tdpram();

// 参数定义
localparam DATA_WIDTH = 16  ;
localparam ADDR_WIDTH = 4   ;  // 深度=16

// 信号声明
reg                                     clk                ;
reg                                     wr_ena             ;
reg                    [ADDR_WIDTH-1:0] wr_addra           ;
reg                    [DATA_WIDTH-1:0] wr_dataa           ;
wire                   [DATA_WIDTH-1:0] rd_dataa           ;
reg                                     wr_enb             ;
reg                    [ADDR_WIDTH-1:0] wr_addrb           ;
reg                    [DATA_WIDTH-1:0] wr_datab           ;
wire                   [DATA_WIDTH-1:0] rd_datab           ;

integer i;
initial begin
    #10
    for (i = 0; i <= (1<<ADDR_WIDTH); i = i + 1) begin
        u_ram.mem[i] = 16'h0;
    end
end

// 实例化RAM
tdpram #(
    .DATA_WIDTH                        (DATA_WIDTH                ),
    .ADDR_WIDTH                        (ADDR_WIDTH                ) 
) u_ram (
    .clk                               (clk                       ),

    .wr_ena                            (wr_ena                    ),
    .wr_addra                          (wr_addra                  ),
    .wr_dataa                          (wr_dataa                  ),
    .rd_dataa                          (rd_dataa                  ),

    .wr_enb                            (wr_enb                    ),
    .wr_addrb                          (wr_addrb                  ),
    .wr_datab                          (wr_datab                  ),
    .rd_datab                          (rd_datab                  ) 
);

// 时钟生成
initial begin
    clk = 0;
    forever #10 clk = ~clk;  
end

// initial begin
//     rd_clk = 0;
//     forever #10 rd_clk = ~rd_clk; 
// end

// 测试逻辑
initial begin
    wr_ena   =   1'b0                       ;
    wr_enb   =   1'b0                       ;
    wr_addra =   4'd0                       ;
    wr_addrb =   4'd0                       ;
    #40                                     ;
    wr_ena   =   1'b1                       ;
    wr_enb   =   1'b1                       ;
    WR(4'd6,4'h0,16'h1111,4'h0,16'h2222)    ;
    WR(4'd6,4'h1,16'h3333,4'h1,16'h4444)    ;
         
    WR(4'd7,4'h0,16'h7777,4'h0,16'h8888)    ;
    WR(4'd7,4'h1,16'h9999,4'h1,16'haaaa)    ;
    WR(4'd6,4'h0,16'hdddd,4'h1,16'heeee)    ; 

    WR(4'd4,4'h3,16'hdddd,4'h3,16'heeee)    ; 
    WR(4'd5,4'h3,16'h0000,4'h3,16'h0000)    ; 

end

task WR (
    input   [3:0]   wr_en       ,
    
    input   [3:0]   addr_a      ,
    input   [15:0]  data_a      ,

    input   [3:0]   addr_b      ,
    input   [15:0]  data_b       
);

begin
    if(wr_en==4'd0)begin            //A write
        wr_ena      =   1           ;
        wr_addra    =   addr_a      ;
        wr_dataa    =   data_a      ;
        #20                         ;
        wr_ena      =   0           ;
    end
    else if(wr_en==4'd1)begin       //A read
        wr_ena      =   0           ;
        wr_addra    =   addr_a      ;
        #20                         ;
        wr_ena      =   0           ;
    end
    else if(wr_en==4'd2)begin       //B write
        wr_enb      =   1           ;
        wr_addrb    =   addr_b      ;
        wr_datab    =   data_b      ;        
        #20                         ;
        wr_enb      =   0           ;
    end    
    else if(wr_en==4'd3)begin       //B read
        wr_enb      =   0           ;
        wr_addrb    =   addr_b      ;
        #20                         ;
        wr_enb      =   0           ;
    end     
    else if(wr_en==4'd4)begin       //A write B write
        wr_ena      =   1           ;
        wr_addra    =   addr_a      ;
        wr_dataa    =   data_a      ;    
        wr_enb      =   1           ;
        wr_addrb    =   addr_b      ;
        wr_datab    =   data_b      ;        
        #20                         ;
        wr_ena      =   0           ;
        wr_enb      =   0           ;
    end   
    else if(wr_en==4'd5)begin       //A read B read
        wr_ena      =   0           ;
        wr_enb      =   0           ;
        wr_addra    =   addr_a      ;
        wr_addrb    =   addr_b      ;
        #20                         ;
        wr_ena      =   0           ;
        wr_enb      =   0           ;
    end
    else if(wr_en==4'd6)begin       //A write B read
        wr_ena      =   1           ;
        wr_addra    =   addr_a      ;
        wr_dataa    =   data_a      ;  
        
        wr_enb      =   0           ;
        wr_addrb    =   addr_b      ;
        #20                         ;
        wr_ena      =   0           ;
        wr_enb      =   0           ;
    end 
    else if(wr_en==4'd7)begin       //A read B write
        wr_ena      =   0           ;
        wr_addra    =   addr_a      ;
        
        wr_enb      =   1           ;
        wr_addrb    =   addr_b      ;
        wr_datab    =   data_b      ;   
        #20                         ;
        wr_ena      =   0           ;
        wr_enb      =   0           ;
    end                  
end   
endtask


endmodule

  先通过A端口分别在0、1地址写入0x1111、0x3333,然后在A端口读0、1地址时,B端口同时写入0x8888、0xaaaa,A输出端口数据为0x1111、0x3333,为前一次在A端口写入的值,同时在A、B端口对3地址写入数据时,B端口的数据0xeeee优先,A端口无效。
在这里插入图片描述

图12 同步时钟仿真
  欢迎相互讨论交流!!!

参考文档:Block Memory Generator v8.4

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐