使用verilog代码实现RAM—真双口RAM
本文以随机存取存储器(RAM)为核心,系统讲解其在FPGA开发中的设计方法与实践技巧。文章主要通过对Vivado Block Memory Generator IP的仿真对比,用verilog 实现真双端口RAM,包含时钟异步时的处理。以下有部分内容摘自Xilinx官方手册,如有理解差异,请参考原手册。
本文以随机存取存储器(RAM)为核心,系统讲解其在FPGA开发中的设计方法与实践技巧。文章主要通过对Vivado Block Memory Generator IP的仿真对比,用verilog 实现真双端口RAM,包含时钟异步时的处理。
以下有部分内容摘自Xilinx官方手册,如有理解差异,请参考原手册。
Xilinx 简单双口RAM框图如下:
1 Xilinx 真双端口RAM的配置
1.1 Basic页
实际配置如下,Basic页Memory Type选择True Dual Port RAM,其他配置属性与单端口RAM相同,要了解详细属性说明,可以参考使用verilog代码实现RAM—单端口RAM。
1.2 Port A Options页
Port A Options页配置如下,配置属性与单端口相同,同样可以配置Operating Mode,后文进行仿真时会验证与单端口是否有区别。
1.3 Port B Options页
相比于单端口RAM,真双端口RAM多出一个PORTB口,如下为Port B Options页配置,B口可配置写和读位宽,相比与PORT A 多出一个Enable ECC PIPE选项(选择ECC时有效),用于ECC时PIPELINE,其他属性相同。
1.4 Other Options页
Other Options页配置与单端口RAM相同。
1.5 Summary页
Summary页属性与单端口RAM相同。
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 一致。
配置模式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 一致。
配置模式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端口输出上一次读取的值。
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端口写入的数据。
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端口之前写入的数据。
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端口无效。
更多推荐



所有评论(0)