对于RAM IP核(Block Memory Generator核)的使用以及界面的配置介绍,文章RAM的使用介绍进行了较详细的说明,本文对RAM IP核使用中一些注意的地方加以说明。
文章目录
- 三种RAM的区别
- 单端口RAM(Single-port RAM)
- 简单双端口RAM(Simple Dual-port RAM)
- 真双端口RAM(True Dual-port RAM)
- 典型应用
- 使能端口
- 位宽与深度
- 位宽转换是的数据地址对应关系
- BLOCK RAM的读写模式
- 先写模式(Write First Mode)
- 先读模式(Read First Mode)
- 不变模式(No Change Mode)
- 读数据延迟
- 冲突(Collision Behavior)
- 附录
- 附录1
- 附录2
- 附录3
- 参考
三种RAM的区别
单端口RAM(Single-port RAM)
单端口RAM只有一个地址接口addra,对应有一对读写的数据接口dina和douta,ena和wea为1时,向addra地址中写数据;wea为0时,通过addra地址中读数据。
简单双端口RAM(Simple Dual-port RAM)
简单双端口RAM相当于读写分开,addra、dina和wea完成写,addrb和doutb完成读,你读你的,我写我的,互不干扰。
简单双端口RAM有两个地址接口addra和addrb,但却只有一对读写的数据接口dina和doutb,所以也叫他伪双端口;
A端口用来写RAM,B端口用来读RAM,可以同时进行读写数据。
真双端口RAM(True Dual-port RAM)
真双端口RAM是两组地址对同一块Memory进行读写(如下图官方手册上有说明),对于AB两端口中的一个端口A或B其实就是单端口RAM的控制逻辑。我们在外部控制的时候如果只把A端口用作写,B端口用作读,那其实就和伪双端口RAM一样了。
真双端口RAM拥有两个地址接口,并且每个地址接口都有对应的读写数据接口,所以叫真双端口RAM,真双端口RAM支持两个端口同时对Memory进行访问,有效的提高了访问速度。同样的,对于双端口的ROM,我们可以通过两个端口同时读取ROM中的数据,所以如果两个processor是访问同样的一组数据的话,就不需要例化两个相同的ROM了。
总结:无论是单端口RAM、简单双端口RAM还是真双端口RAM,他们都只有一块Memory,并且他们都是通过寻址的方式访问这块Memory,在配置相同的位宽和存储深度时,所使用的BRAM资源是一样的,区别在于对应Memory的接口数量不同,也即是所谓的端口不同。
典型应用
Block Memory Generator(BMG)核用于创建定制存储器,以满足任何应用的需要。典型的应用包括:
存储器类型 | 典型应用 |
---|---|
Single-port RAM | Processor scratch RAM, look-up tables |
Simple Dual-port RAM | Content addressable memories, FIFOs |
True Dual-port RAM | Multi-processor storage |
Single-port ROM | Program code storage, initialization ROM |
Dual-port ROM | Single ROM shared between two processors/systems |
使能端口
ENA、ENB置0时,写或读数据无效,输出的DOUT数据保存不变。
如果使用使能端口(Use ENA Pin),那么会多一个ena/enb。A端口在wea和ena同时为1时写入数据有效。B端口在enb为1时写入数据有效。选择始终使能(Always Enabled)时,在wea为1时写入数据有效。
位宽与深度
读写位宽:读写时的数据位宽,如写一个数8‘hcd即为8位宽。
读写深度:需要存储的数据的个数。例如需要存储4个数据此处填写4,而不要填写地址的位宽2。
位宽转换是的数据地址对应关系
在AB两端口进行位宽转换时无论A端口低位宽数据转换为B端口高位宽数据,或是A端口高位宽数据转换为B端口低位宽数据。
低位宽的起始地址从高位宽起始地址中数据的低位开始取数据。
如下:16bit转32bit数据
16bit转4bit数据
验证代码见附录1。
BLOCK RAM的读写模式
读写支持3种模式,分别是Write First Mode, Read First Mode, No Change Mode。
DOUTA和WEA相关联,DOUTB和WEB相关联,在Single-port RAM和True Dual-port RAM的两种RAM格式中会体现,也就是说Single-port RAM或True Dual-port RAM中,在WEA/WEB写使能置1时DOUTA(WEB写使能置1时DOUTB)也会有输出,并且此时的输出和读写模式有关。
通过下面的介绍可以看出:
单端口和真双端口:该读写模式的设置只会影响在写数据(wea=1)时DOUTA的输出,在wea=0时实际上输出的数据就是上一次写在地址中的数据。模式设置的不同在写数据时的DOUT数据作为一些条件判断等可能会有一定的用途。
简单双端口:读端口B的数据doutb由于有读地址线的控制,所以读出的数据和读地址addrb对应;在读写冲突时,读出的数据为此时写入的数据,所以读写模式对于简单双端口不起作用,无需关心。(验证代码见附录2)
接下来我们向单端口RAM里写入数据,写满后读出RAM中的数据,对比三种效果。
先写模式(Write First Mode)
这种模式下:
1)写操作:设置WEA为1写入当前地址的数据,在下一个时钟DOUTA会输出这个地址新写入的数据。
2)读操作:设置WEA为0读出当前地址的数据,在下一个时钟DOUTA会输出这个地址的数据。
从 Write First 模式仿真图可以看到,当我们往 RAM 里写数据是,就有数据读出了,并且读出的是新写入的数据。
先读模式(Read First Mode)
这种模式下:
1)写操作:设置WEA为1写入当前地址的数据,而且在下一个时钟DOUTA会输出这个地址的原先的数据。
2)读操作:设置WEA为0读出当前地址的数据,在下一个时钟DOUTA会输出这个地址的数据。
由于我们配置的单端口RAM是读优先模式,所以当我们往RAM里写入数据时,RAM读出的数据为写入前RAM中存储的数据,故当我们第一次往RAM写数据时,RAM读出的数据为初始的0,在第二次写入不同数据到地址时,由于读优先,所以DOUTA读出了第一次存入的数据。
不变模式(No Change Mode)
这种模式下:
1)写操作:设置WEA为1写入当前地址的数据,和前面两种方式不一样,DOUT保存不变。
2)读操作:设置WEA为0读出当前地址的数据,在下一个时钟DOUTA会输出这个地址的数据。
由于我们配置的单端口RAM是不变模式,所以当我们往RAM里写入数据时,RAM读出的数据为上一次读出的数据并保持不变,故当我们第一次往RAM写数据时,RAM读出的数据为初始的0,在第二次写入不同数据到地址时,DOUTA保持读0地址的数据1不变。
验证代码见附录3。
读数据延迟
IP核的输出给定读地址后,读数据会滞后至少一个时钟周期输出,具体输出延迟的周期由Port A/B Optional Output Registers决定。
有寄存器和无寄存器输出,可以达到的最高时钟频率不同,所以增加寄存器输出可以提高速度。
最终的延时可在总结页面中查看到。
冲突(Collision Behavior)
冲突一般在真双端口RAM读写会考虑。由于其支持Port同时写入,那么就会存在这个冲突的问题,在冲突发生的时候,存储内容可预知;
冲突主要出现在一下场景 :
- 两个Port的异步时钟,一个port正在往一个地址写数据,另一个port在指定时间内,必须不能够去读写这个地方;
- 两个Port同步时钟的情况下,两个Port同时企图写同一个memory的情况下;
- 使用Byte-Writes的时候,正好同时写到了同一个地方的情况下;
遇到此类问题可参考官方文档与参考链接中的介绍。
附录
附录1
简单双端口RAM、读位宽16、读深度8、写位宽32、写深度4、无输出寄存器、使用使能端口、从0地址开始写。
RTL部分:
module ram_w_r
#(
//=========================< Parameter >==============================
parameter WA_WIDTH = 3 , //写地址位宽
parameter WD_WIDTH = 16 , //写数据位宽
parameter RA_WIDTH = 2 , //读地址位宽
parameter RD_WIDTH = 32 , //读数据位宽
parameter WRAM_ADDR_MAX = 8 //写地址最大深度
)
(
//=========================< Port Name >==============================
//input
input wire i_sys_clk_wr ,
input wire i_sys_clk_rd ,
input wire i_rst_n ,
input wire [WD_WIDTH-1:0] i_data ,//准备写入RAM中的数据
input wire i_data_valid ,//数据有效标志
input wire i_rram_en ,//读取的RAM数据使能
input wire [RA_WIDTH-1:0] i_rram_addr ,//读取的RAM地址
//output
output wire [RD_WIDTH-1:0] o_data ,//读取的RAM数据
output reg o_data_valid ,//读取的RAM数据有效
output wire [WA_WIDTH-1:0] o_wram_addr //写RAM地址
);
//=========================< Variable >==============================
reg [WA_WIDTH-1:0] r_wram_addr ;
reg [WD_WIDTH-1:0] r_wram_data ;
reg r_wram_en ;
reg [1:0] WR_S ;//写状态机
//==========================================================================
//== wr_ram_address
//==========================================================================
assign o_wram_addr = r_wram_addr ;
//==========================================================================
//== write RAM
//==========================================================================
always @(posedge i_sys_clk_wr)begin
if(!i_rst_n)begin
r_wram_en <= 0;
r_wram_addr <= 0;
r_wram_data <= 0;
WR_S <= 2'd0;
end
else begin
case(WR_S)
0:begin
if(i_data_valid)begin //数据有效
r_wram_en <= 1'b1; //设置写使能
r_wram_addr <= 0; //设置地址从0开始
r_wram_data <= i_data; //写数据
WR_S <= 2'd1; //下一个状态
end
else begin
r_wram_en <= 1'b0;
r_wram_addr <= r_wram_addr;
r_wram_data <= r_wram_data;
end
end
1:begin
if(i_data_valid & (r_wram_addr == (WRAM_ADDR_MAX-1)))begin
r_wram_en <= 1'b1;
r_wram_addr <= 0;
r_wram_data <= i_data;
end
else if(i_data_valid)begin
r_wram_en <= 1'b1; //设置写使能
r_wram_addr <= r_wram_addr + 1'b1;//通道A地址,地址增加1
r_wram_data <= i_data;
end
else begin
r_wram_en <= 1'b0;
r_wram_addr <= r_wram_addr;
r_wram_data <= r_wram_data;
end
end
default:WR_S <= 2'd0;
endcase
end
end
//==========================================================================
//== read RAM
//==========================================================================
always @(posedge i_sys_clk_rd)begin//o_data_valid根据配置的输出寄存器有关,至少滞后地址一个时钟周期
if(!i_rst_n)
o_data_valid <= 1'b0;
else
o_data_valid <= i_rram_en;
end
sdp_ram_16_8_32_4 u_sdp_ram_16_8_32_4 (
.clka (i_sys_clk_wr ), // input wire clka
.ena (i_rst_n ), // input wire ena
.wea (r_wram_en ), // input wire [0 : 0] wea
.addra (r_wram_addr ), // input wire [2 : 0] addra
.dina (r_wram_data ), // input wire [15 : 0] dina
.clkb (i_sys_clk_rd ), // input wire clkb
.enb (i_rram_en ), // input wire enb
.addrb (i_rram_addr ), // input wire [1 : 0] addrb
.doutb (o_data ) // output wire [31 : 0] doutb
);
endmodule
仿真部分(5us),每写两个地址读一次:
`timescale 1ns/1ps //时间精度
//========================================================================
// module_name.v :tb_ram_w_r.v
//========================================================================
module tb_ram_w_r();
//=========================< Parameter >==============================
parameter CLK_PERIOD0 = 20 ;//设置时钟信号周期
parameter HALF_CLK_PERIOD0 = CLK_PERIOD0/2;//生成时钟信号半周期
parameter CLK_PERIOD1 = 10 ;//设置时钟信号周期
parameter HALF_CLK_PERIOD1 = CLK_PERIOD1/2;//生成时钟信号半周期
parameter WA_WIDTH = 3 ; //写地址位宽
parameter WD_WIDTH = 16 ; //写数据位宽
parameter RA_WIDTH = 2 ; //读地址位宽
parameter RD_WIDTH = 32 ; //读数据位宽
parameter WRAM_ADDR_MAX = 8 ; //写地址最大深度
//=========================< Port Name >==============================
//input
reg clk_w ;
reg clk_r ;
reg rst_n ;
reg [3:0] cnt_flag ;
reg [1:0] cnt_rd ;
reg [3:0] cnt_data ;
reg [WD_WIDTH-1:0] i_data ;
reg i_data_valid ;
reg i_rram_en ;
reg [RA_WIDTH-1:0] i_rram_addr ;
wire [RD_WIDTH-1:0] o_data ;
wire o_data_valid ;
wire [WA_WIDTH-1:0] o_wram_addr ;
//==========================< Clock block >============================
always #HALF_CLK_PERIOD0 clk_w = ~clk_w;
always #HALF_CLK_PERIOD1 clk_r = ~clk_r;
//==========================< Reset block >============================
initial begin
clk_w = 1'b1 ;
clk_r = 1'b1 ;
rst_n <= 1'b0 ;
#100
rst_n <= 1'b1 ;
end
//==========================< Module Instance >============================
ram_w_r
#(
.WA_WIDTH (WA_WIDTH ),
.WD_WIDTH (WD_WIDTH ),
.RA_WIDTH (RA_WIDTH ),
.RD_WIDTH (RD_WIDTH ),
.WRAM_ADDR_MAX (WRAM_ADDR_MAX )
)
u_ram_w_r(
.i_sys_clk_wr (clk_w ), //input clk_w
.i_sys_clk_rd (clk_r ),
.i_rst_n (rst_n ), //input rst_n
.i_data (i_data ),
.i_data_valid (i_data_valid ),
.i_rram_en (i_rram_en ),
.i_rram_addr (i_rram_addr ),
.o_data (o_data ),
.o_data_valid (o_data_valid ),
.o_wram_addr (o_wram_addr )
);
//==========================================================================
//== cnt_flag 用于控制读有效和数据
//==========================================================================
always @(posedge clk_w)begin
if(!rst_n)
cnt_flag <= 0;
else
cnt_flag <= cnt_flag + 1'b1;
end
//==========================================================================
//== cnt_data 用于构造RAM写入数据
//==========================================================================
always @(posedge clk_w)begin
if(!rst_n)
cnt_data <= 0;
else if(&cnt_flag)
cnt_data <= cnt_data + 4'd4;
else
cnt_data <= cnt_data;
end
//==========================================================================
//== write data
//==========================================================================
always @(posedge clk_w)begin
if(!rst_n)begin
i_data <= 0;
i_data_valid <= 1'b0;
end
else if(&cnt_flag)begin//计数到全1
i_data <= {cnt_data+4'd3,cnt_data+4'd2,cnt_data+4'd1,cnt_data};
i_data_valid <= 1'b1;
end
else begin
i_data <= i_data;
i_data_valid <= 1'b0;
end
end
//==========================================================================
//== cnt_rd 用于读使能判断
//==========================================================================
always @(posedge clk_r)begin
if(!rst_n)
cnt_rd <= 0;
else if((cnt_rd == 2'd3) && ((o_wram_addr == 1) || (o_wram_addr == 3) || (o_wram_addr == 5) || (o_wram_addr == 7)) )
cnt_rd <= cnt_rd;
else if((o_wram_addr == 0) || (o_wram_addr == 2) || (o_wram_addr == 4) || (o_wram_addr == 6))
cnt_rd <= 0;
else if((o_wram_addr == 1) || (o_wram_addr == 3) || (o_wram_addr == 5) || (o_wram_addr == 7))
cnt_rd <= cnt_rd + 1'b1;
end
//==========================================================================
//== 读ram控制
//==========================================================================
always @(posedge clk_r)begin
if(!rst_n)begin
i_rram_en <= 1'b0;
i_rram_addr <= 0;
end
else if( (cnt_rd == 2'd2) && (o_wram_addr == 1) )begin
i_rram_en <= 1'b1;
i_rram_addr <= 0;
end
else if(cnt_rd == 2'd2)begin
i_rram_en <= 1'b1;
i_rram_addr <= i_rram_addr + 1'b1;
end
else begin
i_rram_en <= 1'b0;
i_rram_addr <= i_rram_addr;
end
end
endmodule
仿真结果与数据:
附录2
简单双端口RAM、读写位宽32、读写深度1024、无输出寄存器、使用使能端口、不同读写模式修改IP核即可。
RTL部分:
module bram_test(
input I_rstn, //系统复位输入
input I_sysclk //系统时钟输入
);
reg [9:0]addra; //通道A 地址
reg [7:0]wr_frame; //帧计数器
reg [1:0]WR_S; //写状态机
reg ena; //通道A使能
reg wea; //通道A写使能
reg [9:0]addrb; //读通道B地址
reg [1:0]RD_S; //读状态机
reg enb; //通道B使能
wire [31:0] dina; //bram 数据输入
wire [31:0] doutb; //bram 数据输出
assign dina = wr_frame+addra; //输入的数据包:帧信号+通道A地址
always @(posedge I_sysclk)begin
if(!I_rstn)begin //复位重置相关寄存器
wr_frame <= 8'd0;
addra <= 10'd0;
ena <= 1'b1;
wea <= 1'b0;
WR_S <= 2'd0;
end
else begin
case(WR_S)
0:begin
addra <= 10'd0; //设置地址从0开始
ena <= 1'd1; //设置通道A使能
wea <= 1'b1; //设置写使能
WR_S <= 2'd1; //下一个状态
end
1:begin
if(addra != 10'd1023)begin //如果写地址不等于1023,
wea <= 1'b1; //设置写使能
ena <= 1'b1; //设置通道A使能
addra <= addra + 1'b1;//那么通道A地址,每个时钟地址增加1
end
else begin //否则代表完成了1帧数据写入到BRAM
wea <= 1'b0; //设置写使能为0,停止写
ena <= 1'b0; //设置通道A使能为0
wr_frame <= wr_frame +1'b1;//帧计数器
WR_S <= 2'd2;//下一个状态
end
end
2:begin
if(RD_S == 2'd2) //如果读操作完成
WR_S <= 2'd0; //回到状态0重新开始
end
default:WR_S <= 2'd0;
endcase
end
end
always @(posedge I_sysclk)begin
if(!I_rstn)begin //复位重置相关寄存器
addrb <= 10'd0;
enb <= 1'b0;
RD_S <= 2'd0;
end
else begin
case(RD_S)
0:begin
enb <= 1'b0; //设置读使能0
addrb <= 10'd0; //设置读地址从0开始
if(addra == 10'd1023)begin//读数据在写数据的第1023个地址开始
enb <= 1'b1; //使能读通道
RD_S <= 2'd1; //下一状态
end
end
1:begin
enb <= 1'b1;//设置读使能1
if(addrb != 10'd1023)如果读地址不等于1023,
addrb <= addrb + 1'b1;//那么通道B地址,每个时钟地址增加1
else
RD_S <= 2'd2;//下一状态
end
2:begin
RD_S <= 2'd0;//下一状态
end
default:RD_S <= 2'd0;
endcase
end
end
//例化BRAM IP,简单双口RAM
blk_mem_gen_0 bram_inst (
.clka (I_sysclk ), //通道A时钟输入
.ena (ena ), //通道A使能
.wea (wea ), //写使能
.addra(addra ), //通道A地址
.dina (dina ), //通道A数据输入
.clkb (I_sysclk ), //通道B时钟输入
.enb (enb ), //通道B使能
.addrb(addrb ), //通道B地址
.doutb(doutb ) //通道B数据输出
);
endmodule
仿真部分(100us):
`timescale 1ns / 1ns//仿真时间刻度/精度
module tb_bram_test;
localparam SYS_TIME = 20 ;//定义时钟周期 单位ns
reg I_sysclk; //系统时钟
reg I_rstn; //系统复位
//例化bram_test
bram_test bram_test_inst
(
.I_sysclk (I_sysclk ),
.I_rstn (I_rstn )
);
//初始化
initial begin
I_sysclk = 1'b0;
I_rstn = 1'b0;
#100;//产生100ns的系统复位
I_rstn = 1'b1;//复位完成
end
//产生仿真时钟
always #(SYS_TIME/2) I_sysclk= ~I_sysclk;
endmodule
附录3
单端口RAM、读写位宽2、读写深度4、无输出寄存器、始终使能、不同读写模式修改IP核即可
RTL代码:
module bram_test
(
input wire sys_clk , //系统时钟,频率 50MHz
input wire [2:0] addra , //输入 ram 读写地址
input wire [2:0] dina , //输入 ram 写入数据
input wire wea , //输入 ram 写使能
output wire [2:0] douta //输出读 ram 数据
);
//---------------ram_4x2bit_inst--------------
blk_mem_gen_0 u_blk_mem_gen_0
(
.clka (sys_clk ), //使用系统时钟作为读写时钟
.addra (addra ), //读写地址线
.dina (dina ), //输入写入 RAM 的数据
.wea (wea ), //写 RAM 使能
.douta (douta ) //输出读 RAM 数据
);
endmodule
仿真(1us):
`timescale 1ns/1ns
module tb_bram_test();
//reg define
reg sys_clk ;
reg sys_rst_n ;
reg [1:0] addra ;
reg wea ;
reg wr_flag ;
reg [1:0] cnt;
//wire define
wire [1:0] dina ;
wire [1:0] douta ;
//********************************************************************//
//***************************** Main Code ****************************//
//********************************************************************//
initial
begin
sys_clk = 1'b1 ;
sys_rst_n <= 1'b0 ;
#200 sys_rst_n <= 1'b1 ;
end
//sys_clk:模拟系统时钟,每 10ns 电平取反一次,周期为 20ns,频率为 50Mhz
always #10 sys_clk = ~sys_clk;
//写完成标志信号
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
wr_flag <= 1'b0;
else if(addra == 2'd3)
wr_flag <= wr_flag + 1'b1;
else
wr_flag <= wr_flag;
//wea:产生写 RAM 使能信号
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
wea <= 1'b0;
else if(wr_flag == 1'b1)
wea <= 1'b0;
else
wea <= 1'b1;
//addra:读写地址(0~3 循环)
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
addra <= 2'd0;
else if(addra == 2'd3)
addra <= 2'd0;
else
addra <= addra + 1'b1;
//写使能为高时产生写数据 0~3
assign dina = (wea == 1'b1) ? (addra+cnt) : 2'd0;
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt <= 2'd0;
else if(addra == 2'd3 && wea == 1'b1)
cnt <= cnt+2'd1;
else
cnt <= cnt;
//********************************************************************//
//*************************** Instantiation **************************//
//********************************************************************//
//---------------ram_inst--------------
bram_test bram_test_inst
(
.sys_clk (sys_clk ), //系统时钟,频率 50MHz
.addra (addra ), //输入 ram 读写地址
.dina (dina ), //输入 ram 写入数据
.wea (wea ), //输入 ram 写使能
.douta (douta ) //输出读 ram 数据
);
endmodule
参考
PG058-Block Memory Generator.pdf
RAM(ip 核与原语的使用)介绍
浅谈XILINX BRAM的基本使用 - 米联客(milianke)
不详细的讲一下Xilinx的BMG:单端口和双端口RAM的区别
XILINX BMG (Block Memory Generator)