前言

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

  以下有部分内容摘自Xilinx官方手册,如有理解差异,请参考原手册。

  Xilinx 简单双口RAM框图如下:

图1 简单双口RAM框图

1 Xilinx 简单双端口RAM的配置

1.1 Basic页

  实际配置如下,Basic页Memory Type选择Simple 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页配置,相比与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 对读写冲突的仿真

  配Operating Mode配置为写优先,仿真结果如下,在0地址和1地址同时读写时,在写入时,doutb马上输出为写入的数据,符合写优先。

图7 配置为Write First

  Operating Mode配置为读优先,仿真结果如下,同时对同地址读写时,doutb端口马上更新为写入的数据,与单端口RAM不同。

图8 配置为Read First

  Operating Mode配置为No Change,仿真结果如下,同时对同地址读写时,doutb端口马上更新为写入的数据,与单端口RAM不同。

图9 配置为No Change

  不使能B端口时,doutb始终为0。

图10 不使能B端口

  从上述仿真结果可以看出,简单双口RAM读写冲突时均更新为写入的数据。

2.2 对数据位宽转换的仿真

  如下为大位宽转换为小位宽的操作,在0地址写入0x1234,然后对0地址和1地址进行读操作,输出为0x34,0x12,可见数据为小端模式存放(低字节存低地址)。

图11 大位宽写,小位宽读

  如下为小位宽转换为大位宽的操作,在0地址写入0x1234,在1地址写入0x5678,然后读0地址,读出数据为0x56781234,仍是小端模式(低字节存低地址)

图12 小位宽写,大位宽读

2.3 异步时钟操作的仿真

  如下为快时钟写,慢时钟读,写操作在写时钟上升沿采集到写操作时,将数据写入,读操作时,在读时钟上升沿采集到读操作时,读出数据。

图13 快时钟写,慢时钟读

  如下为慢时钟写,快时钟读,写操作在写时钟上升沿采集到写操作时,将数据写入,读操作时,在读时钟上升沿采集到读操作时,读出数据,互相不影响。

图14 慢时钟写,快时钟读

3 Verilog 实现简单双端口RAM

3.1 同步时钟代码实现

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

module sdpram #(
    parameter DATA_WIDTH = 16       ,   // 数据位宽
    parameter ADDR_WIDTH = 4            // 地址位宽(深度 = 2^ADDR_WIDTH)
) (
    input  wire                         wr_clk                     ,// 写时钟
    input  wire                         wr_en                      ,// 写使能
    input  wire        [ADDR_WIDTH-1:0] wr_addr                    ,// 写地址
    input  wire        [DATA_WIDTH-1:0] wr_data                    ,// 写数据

    input  wire                         rd_clk                     ,// 读时钟
    input  wire                         rd_en                      ,// 读使能
    input  wire        [ADDR_WIDTH-1:0] rd_addr                    ,// 读地址
    output wire        [DATA_WIDTH-1:0] rd_data                     // 读数据
);

// 定义存储器数组(深度=2^ADDR_WIDTH)
reg [DATA_WIDTH-1:0] mem [(1<<ADDR_WIDTH)-1:0];
reg [DATA_WIDTH-1:0] data_b_reg;

// ----------------------------
// 写逻辑(同步写)
// ----------------------------
always @(posedge wr_clk) begin
    if (wr_en) begin
        mem[wr_addr] <= wr_data;  // 写入数据
    end
end

// ----------------------------
// 读逻辑(同步读)
// ----------------------------
always @(posedge rd_clk) begin
    if (rd_en) begin
        if(wr_en && wr_addr==rd_addr)begin
            data_b_reg  <= wr_data ;
        end
        else begin
            data_b_reg <= mem[rd_addr];
        end
    end
end   

assign rd_data = data_b_reg ; 

endmodule

  testbench如下:

module tb_sdpram();

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

// 信号声明
reg                                     wr_clk, rd_clk             ;
reg                                     wr_en, rd_en               ;
reg                    [ADDR_WIDTH-1:0] wr_addr, rd_addr           ;
reg                    [DATA_WIDTH-1:0] wr_data                    ;
wire                   [DATA_WIDTH-1:0] rd_data                    ;



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
sdpram #(
    .DATA_WIDTH                        (DATA_WIDTH                ),
    .ADDR_WIDTH                        (ADDR_WIDTH                )
) u_ram (
    .wr_clk                            (wr_clk                    ),
    .wr_en                             (wr_en                     ),
    .wr_addr                           (wr_addr                   ),
    .wr_data                           (wr_data                   ),
    .rd_clk                            (rd_clk                    ),
    .rd_en                             (rd_en                     ),
    .rd_addr                           (rd_addr                   ),
    .rd_data                           (rd_data                   ) 
);

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

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


// 测试逻辑
initial begin
  wr_clk  =   0                 ;  
  rd_clk  =   0                 ;  
  #15                           ;  
  WR(2'd1,4'hA,16'hAABB,4'h0)   ;
  WR(2'd1,4'h0,16'h1234,4'h0)   ;
  WR(2'd1,4'h1,16'h5678,4'h0)   ;
   
  #10
  WR(2'd0,4'hA,16'hAABB,4'hA)   ;
  WR(2'd2,4'h2,16'hABCD,4'h2)   ;
  WR(2'd2,4'h1,16'h6543,4'h1)   ;
  WR(2'd2,4'h2,16'h6542,4'h2)   ;
  WR(2'd2,4'h0,16'h6541,4'h0)   ;
  WR(2'd2,4'h2,16'h6540,4'h2)   ;

end


task WR (
    input   [1:0]   write_read  ,
    input   [3:0]   write_addr  ,
    input   [15:0]  write_data  ,
    
    input   [3:0]   read_addr     
);

begin
    if(write_read==2'd0)begin
        rd_en   =   1           ;
        rd_addr =   read_addr   ;
        #20                     ;
        rd_en   =   0           ;
    end
    else if(write_read==2'd1)begin
        wr_en   =   1           ;
        wr_addr =   write_addr  ;
        wr_data =   write_data  ;
        #20                     ;
        wr_en   =   0           ;
    end
    else begin
        wr_en   =   1           ;
        wr_addr =   write_addr  ;
        wr_data =   write_data  ;
        rd_en   =   1           ;
        rd_addr =   read_addr   ;        
        #20                     ;
        wr_en   =   0           ;
        rd_en   =   0           ;            
    end
    
end   
endtask


endmodule

3.2 同步时钟代码仿真

  如下是对代码的仿真,先在0地址写入1234、1地址写入5678,然后同时对0、1地址进行读写操作,读端口输出为当前写入的数。
在这里插入图片描述

图15 同步时钟RAM仿真

3.3 异步时钟代码实现

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

module sdpram #(
    parameter DATA_WIDTH = 8,
    parameter ADDR_WIDTH = 4
)(
    input  wire                         wr_clk                     ,
    input  wire                         wr_en                      ,
    input  wire        [ADDR_WIDTH-1:0] wr_addr                    ,
    input  wire        [DATA_WIDTH-1:0] wr_data                    ,

    input  wire                         rd_clk                     ,
    input  wire                         rd_en                      ,
    input  wire        [ADDR_WIDTH-1:0] rd_addr                    ,
    output reg         [DATA_WIDTH-1:0] rd_data                     
);

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

// 写操作处理
always @(posedge wr_clk) begin
    if (wr_en) begin
        // 写入新数据到主存储器
        mem[wr_addr] <= wr_data;
    end
end

// 跨时钟域同步器
reg                     wr_flag_sync0   ;
reg [ADDR_WIDTH-1:0]    wr_addr_sync0   ;
always @(posedge rd_clk) begin
    wr_flag_sync0 <= wr_en          ;

    wr_addr_sync0 <= wr_addr        ;
end

// 读操作处理
always @(posedge rd_clk) begin
    if (rd_en) begin
        if(wr_flag_sync0 && (wr_addr_sync0 == rd_addr))begin
            rd_data <= wr_data      ;       
        end
        else begin
            rd_data <= mem[rd_addr] ;
        end 
    end
end

endmodule

  testbench如下:

module tb_sdpram();

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

// 信号声明
reg                                     wr_clk, rd_clk             ;
reg                                     wr_en, rd_en               ;
reg                    [ADDR_WIDTH-1:0] wr_addr, rd_addr           ;
reg                    [DATA_WIDTH-1:0] wr_data                    ;
wire                   [DATA_WIDTH-1:0] rd_data                    ;



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
sdpram #(
    .DATA_WIDTH                        (DATA_WIDTH                ),
    .ADDR_WIDTH                        (ADDR_WIDTH                )
) u_ram (
    .wr_clk                            (wr_clk                    ),
    .wr_en                             (wr_en                     ),
    .wr_addr                           (wr_addr                   ),
    .wr_data                           (wr_data                   ),
    .rd_clk                            (rd_clk                    ),
    .rd_en                             (rd_en                     ),
    .rd_addr                           (rd_addr                   ),
    .rd_data                           (rd_data                   ) 
);

// 时钟生成
initial begin
    wr_clk = 0;
    forever #11 wr_clk = ~wr_clk;  
end

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

// 测试逻辑
initial begin
  wr_clk  =   0                 ;  
  rd_clk  =   0                 ;  
  #15                           ;  
  WR(2'd1,4'hA,16'hAABB,4'h0)   ;
  WR(2'd1,4'h0,16'h1234,4'h0)   ;
  WR(2'd1,4'h1,16'h5678,4'h0)   ;
   
  #10
  WR(2'd0,4'hA,16'hAABB,4'hA)   ;
  WR(2'd2,4'h2,16'hABCD,4'h2)   ;
  WR(2'd2,4'h1,16'h6543,4'h1)   ;
  WR(2'd2,4'h2,16'h6542,4'h2)   ;
  WR(2'd2,4'h0,16'h6541,4'h0)   ;
  WR(2'd2,4'h2,16'h6540,4'h2)   ;

end

task WR (
    input   [1:0]   write_read  ,
    input   [3:0]   write_addr  ,
    input   [15:0]  write_data  ,
    
    input   [3:0]   read_addr     
);

begin
    if(write_read==2'd0)begin
        rd_en   =   1           ;
        rd_addr =   read_addr   ;
        #20                     ;
        rd_en   =   0           ;
    end
    else if(write_read==2'd1)begin
        wr_en   =   1           ;
        wr_addr =   write_addr  ;
        wr_data =   write_data  ;
        #20                     ;
        wr_en   =   0           ;
    end
    else begin
        wr_en   =   1           ;
        wr_addr =   write_addr  ;
        wr_data =   write_data  ;
        rd_en   =   1           ;
        rd_addr =   read_addr   ;        
        #20                     ;
        wr_en   =   0           ;
        rd_en   =   0           ;            
    end
    
end   
endtask

endmodule

3.4 异步时钟代码仿真

  如下是写操作为慢时钟,读操作为快时钟,写入,在读时钟上升沿检测读操作时,正常读出数据。
在这里插入图片描述

图16 异步时钟RAM慢到快仿真

  将testbench 中写时钟更改为快时钟,如下代码,写操作为快时钟,读操作为慢时钟,先在0地址写入1234、1地址写入5678,在读时钟上升沿检测读操作时,正常读出数据。

// 时钟生成
initial begin
    wr_clk = 0;
    forever #9 wr_clk = ~wr_clk;  
end

在这里插入图片描述

图17 异步时钟RAM快到慢仿真

  以上即是简单双端口RAM的实现和仿真。

  欢迎相互讨论交流!!!

参考文档:Block Memory Generator v8.4

Logo

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

更多推荐