使用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选择Simple 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页配置,相比与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 对读写冲突的仿真
配Operating Mode配置为写优先,仿真结果如下,在0地址和1地址同时读写时,在写入时,doutb马上输出为写入的数据,符合写优先。
Operating Mode配置为读优先,仿真结果如下,同时对同地址读写时,doutb端口马上更新为写入的数据,与单端口RAM不同。
Operating Mode配置为No Change,仿真结果如下,同时对同地址读写时,doutb端口马上更新为写入的数据,与单端口RAM不同。
不使能B端口时,doutb始终为0。
从上述仿真结果可以看出,简单双口RAM读写冲突时均更新为写入的数据。
2.2 对数据位宽转换的仿真
如下为大位宽转换为小位宽的操作,在0地址写入0x1234,然后对0地址和1地址进行读操作,输出为0x34,0x12,可见数据为小端模式存放(低字节存低地址)。
如下为小位宽转换为大位宽的操作,在0地址写入0x1234,在1地址写入0x5678,然后读0地址,读出数据为0x56781234,仍是小端模式(低字节存低地址)
2.3 异步时钟操作的仿真
如下为快时钟写,慢时钟读,写操作在写时钟上升沿采集到写操作时,将数据写入,读操作时,在读时钟上升沿采集到读操作时,读出数据。
如下为慢时钟写,快时钟读,写操作在写时钟上升沿采集到写操作时,将数据写入,读操作时,在读时钟上升沿采集到读操作时,读出数据,互相不影响。
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地址进行读写操作,读端口输出为当前写入的数。
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 异步时钟代码仿真
如下是写操作为慢时钟,读操作为快时钟,写入,在读时钟上升沿检测读操作时,正常读出数据。
将testbench 中写时钟更改为快时钟,如下代码,写操作为快时钟,读操作为慢时钟,先在0地址写入1234、1地址写入5678,在读时钟上升沿检测读操作时,正常读出数据。
// 时钟生成
initial begin
wr_clk = 0;
forever #9 wr_clk = ~wr_clk;
end

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



所有评论(0)