相关文章:
Xilinx IP 解析之 Block Memory Generator v8.4 ——01-手册重点解读(仅Native RAM) – 徐晓康的博客
Xilinx IP 解析之 Block Memory Generator v8.4 ——02-如何配置 IP(仅 Native RAM) – 徐晓康的博客
Verilog功能模块–RAM和ROM(01)–功能说明与关键代码解析 – 徐晓康的博客
Verilog功能模块–RAM和ROM(02)–同步写-写冲突与读-写冲突实测 – 徐晓康的博客
Verilog 功能模块–RAM 和 ROM(03)–自编 RAM 与 Vivado RAM IP 功能对比实测 – 徐晓康的博客
前言
RAM和ROM是FPGA中用于数据交互的基础组件,应用非常广泛。
所有的FPGA厂家在其开发工具中都提供可直接使用的RAM/ROM IP,这些IP大都经过详细测试,功能完善,可直接使用,那为什么还需要自编RAM/ROM呢?主要理由是:
-
自编RAM/ROM能显著增强代码可移植性,例如,我的某些自编IP中需要RAM/ROM,此时如果使用某一厂家RAM/ROM IP,那么换另外厂家的芯片,自编IP就不能用了;或者要为每个厂家的芯片都重写一下自编IP,并且还要在工程中添加RAM/ROM IP,这就使得自编IP的适用性变得很差,难以接受; -
在很多应用场景中,并不需要追求大容量的RAM/ROM,也不要求RAM/ROM的高性能,而只是需要RAM/ROM能实现基本的功能就行了,这时使用自编RAM/ROM比用厂家IP就要方便很多了。也不用担心厂家IP升级收费之类的变化。
当然,如果需要大容量的RAM/ROM,或者高性能的RAM/ROM,还是建议用厂家的RAM/ROM IP,毕竟这些IP都经过严格的测试,也能更好的适应特定的芯片,在可靠性和性能上都会优于自编IP。
本文主要介绍自编RAM/ROM IP的功能与信号接口,并对一些关键功能的实现代码进行解析。
一、自编RAM/ROM IP的功能
-
自编RAM/ROM IP的接口信号和处理逻辑都参考Vivado的BRAM IP。 -
实现了三种RAM类型:1.单端口RAM(SPRAM);2.简单双端口RAM(SDPRAM);3.真双端口RAM(TDPRAM)。 -
实现了两种ROM类型:1.单端口ROM(SPROM);2.双端口ROM(DPROM)。 -
RAM需要实现三种操作模式:1.写优先(Write First);2.读优先(Read First);3.无变化(No Change)。 -
实现了端口使能引脚en,功能等价于Vivado BRAM IP中的ena和enb。 -
实现参数控制的单个/两个输出寄存器,功能等价于Vivado BRAM IP中的Primitives Output Register和Core Output Register。 -
实现了RAM/ROM使用COE文件进行初始化的功能,功能等价于Vivado BRAM IP中的Memory Initialization功能。 -
实现了指定RAM/ROM初始默认值的功能,当未指定COE文件或COE文件数据数小于存储器深度时,此默认值起作用,功能等价于Vivado BRAM IP中的Fill Remaining Memory Locations。 -
暂未实现字节写功能,即Vivado BRAM IP中的Byte Write Enable选项,此功能实现有些复杂,暂不实现。 -
暂未实现读/写数据位宽转换功能,如果需要此功能,可外部添加我开源的带位宽转换功能的FIFO模块。 -
暂未实现两端口数据位宽转换功能,如果需要此功能,可外部添加我开源的带位宽转换功能的FIFO模块。 -
暂未实现复位功能,一般应用通常不需要复位,也难以去对应Vivado BRAM IP中的复位功能,此IP复位功能还需要增加regcn引脚,rst_busy引脚,还有复位优先级区分,以及是否复位存储器值的选项,有些太繁琐了。
总结:实现了Vivado BRAM IP的绝大部分功能,且保证了信号名称完全一致,在大部分应用场景可替换Vivado BRAM IP以增强代码适用性。
二、模块框图与信号说明
TDPRAM模块框图如下,其余模块SDPRAM、SPRAM、DPROM、SPROM略。
参数说明:
参数名 | 描述 |
---|---|
RAM_STYLE | RAM类型, 可选”block”(默认), “distributed” |
DATA_WIDTH | 数据位宽, 可选1, 2, 3, …, 默认为8 |
ADDR_WIDTH | RAM地址位宽, 对应RAM深度, 可选1, 2, 3, …, 默认为6, 对应深度2**6=64 |
OPERATING_MODE_A | 可选”WF”(默认), “RF”, “NC” WF表示Write First,RF表示Read First,NC表示No Change |
OPERATING_MODE_B | 可选”WF”(默认), “RF”, “NC” WF表示Write First,RF表示Read First,NC表示No Change |
USE_ENA | 是否启用ENA信号,0(默认)表示不启用,1表示启用 |
USE_ENB | 是否启用ENB信号,0(默认)表示不启用,1表示启用 |
OUTPUT_REG_NUM_A | A端口输出寄存器数量,可选0(默认), 1, 2 0表示输出不添加寄存器,直接输出 1表示输出添加一个寄存器,输出延时一个时钟周期 2表示输出添加2个寄存器,输出延时两个时钟周期 |
OUTPUT_REG_NUM_B | B端口输出寄存器数量,与OUTPUT_REG_NUM_A一样 |
INIT_FILE | 初始化文件路径,,空“”(默认)表示不初始化,示例目录C:/ram_init.coe,注意斜杆为除法符号 |
INIT_VALUE_HEX | 默认初始值, 在未指定初始化文件或初始化文件行数比RAM深度小时起作用, 使用16进制表示, 默认值0, |
三、使用Verilog对RAM进行初始化
参考:ug901-vivado-synthesis-en-us-2024.2.pdf–Chapter 5: HDL Coding Techniques:
-
RAM HDL Coding Techniques(随机存取存储器(RAM)的硬件描述语言(HDL)编码技术) -
Inferring UltraRAM in Vivado Synthesis(在Vivado综合工具中推断UltraRAM(超高速随机存取存储器)) -
RAM HDL Coding Guidelines(RAM的HDL编码指南) -
Initializing RAM Contents(初始化RAM内容) -
3D RAM Inference(3D RAM推断) -
Black Boxes(黑盒) -
FSM Components(有限状态机组件) -
ROM HDL Coding Techniques(只读存储器(ROM)的HDL编码技术)
3.1 在HDL源代码中指定RAM初始内容
RAM所有存储值都初始化为相同的值,示例代码如下:
reg [DATA_WIDTH-1:0] ram [DEPTH-1:0];
integer i;
initial begin
for (i=0; i<DEPTH; i=i+1) ram[i] = 0;
end
RAM所有存储值都初始化为特定的值,示例代码如下:
reg [19:0] ram [15:0];
initial begin
ram[15] = 20'h00102; ram[14] = 20'h02137; ram[13] = 20'h02036;
ram[12] = 20'h00301; ram[11] = 20'h00102; ram[10] = 20'h02237;
ram[9] = 20'h04004; ram[8] = 20'h00304; ram[7] = 20'h04040;
ram[6] = 20'h02500; ram[5] = 20'h02500; ram[4] = 20'h02500;
ram[3] = 20'h0030D; ram[2] = 20'h02341; ram[1] = 20'h08201;
ram[0] = 20'h0400D;
end
3.2 在外部数据文件中指定RAM初始内容
在硬件描述语言(HDL)源代码里,可借助文件读取功能,从外部数据文件加载随机存取存储器(RAM)的初始内容。
-
外部数据文件要求:它是一个ASCII文本文件,文件名可任意设定。 -
数据组织方式:该文件中的每一行都对应着随机存取存储器(RAM)中一个地址位置的初始内容。 -
行数匹配规则:外部数据文件的行数必须和随机存取存储器(RAM)阵列的行数相同。若行数不足,系统会发出标记提示。 -
地址映射机制:文件中每一行数据对应的随机存取存储器(RAM)地址位置,由对该随机存取存储器(RAM)进行建模的信号主范围方向来确定。 -
数据格式规范:随机存取存储器(RAM)的内容可以用二进制或者十六进制来表示,但这两种格式不能在同一文件中混用。 -
内容纯净性要求:外部数据文件中不能包含任何其他内容,像注释之类的都不允许存在。
RAM初始化Verilog代码示例:
reg [31:0] ram [0:63];
initial begin
// 地址范围0~63,对应文件的0~63行
$readmemb("rams_20c.data", ram, 0, 63);
end
或者
reg [31:0] ram [0:63];
initial begin
// 不指定地址范围,则顺序读取每一行,直到出现空行或者RAM满
$readmemb("rams_init_file.data", ram);
end
注意:用于初始化随机存取存储器(RAM)的外部文件必须采用位向量(bit vector)格式。整数格式或者十六进制格式的外部文件无法正常使用。
rams_init_file.data示例(10bit X 8行):
0000000000
0010101010
0101010101
0111111111
1000100010
1010101010
1100110011
1110100000
不允许添加任何注释和标点符号,经过测试,上述数字可以插入下划线,如11_1111_1111,但也建议不要加,因为Vivado BRAM IP的COE文件不允许数字间添加下划线,保持一致。
Vivado BRAM IP支持的COE文件格式示例如下:
memory_initialization_radix = 2;
memory_initialization_vector =
0000000000,
0010101010,
0101010101,
0111111111,
1000100010,
1010101010,
1100110011,
1110100000;
memory_initialization_radix可以等于2/10/16,分别对应2/10/16进制,memory_initialization_vector需满足radix指定的进制。
3.3 自编RAM/ROM IP中的初始化代码
与初始化相关的参数:
//* 初始化文件名,空(默认)表示不初始化,示例目录C:/_myJGY/ram_init.coe
parameter INIT_FILE = "",
/*
* 默认初始值, 在未指定初始化文件或初始化文件行数比RAM深度小时起作用, 使用16进制表示, 默认值0,
* 对应Vivado BRAM IP的功能Fill Remaining Memory Locations
*/
parameter [DATA_WIDTH-1:0] INIT_VALUE_HEX = 'h0
初始化功能代码:
//++ 初始化RAM ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
integer i;
generate
if (INIT_FILE != "") begin
initial begin
for (i = 0; i < DEPTH; i = i + 1) mem[i] = INIT_VALUE_HEX; //* 默认初始化值
/*
* 使用 $readmemb 从 .coe 文件加载初始值,
* ug901中说明只有二进制格式的初始化文件才是可用Verilog代码进行初始化的
*/
$readmemb(INIT_FILE, mem);
end
end else begin
initial begin : ram_init_all_0 //* 将每个存储单元初始化为设定的初始值
for (i = 0; i < DEPTH; i = i + 1) mem[i] = INIT_VALUE_HEX;
end
end
endgenerate
//-- 初始化RAM ------------------------------------------------------------
四、三种操作模式–写优先、读优先与无变化模式的实现
这三种模式影响RAM的输出值,相关参数:
//* 可选"Write First"(默认), "Read First", "No Change"
parameter OPERATING_MODE_A = "WF",
parameter OPERATING_MODE_B = "WF",
实现代码:
reg [DATA_WIDTH-1:0] douta_tmp = 0; //* 仿真默认值, 对综合无效
generate
if (OPERATING_MODE_A == "WF") begin
always @(posedge clka) begin
if (true_ena)
if (wea)
douta_tmp <= dina;
else
douta_tmp <= mem[addra];
end
end else if (OPERATING_MODE_A == "RF") begin
always @(posedge clka) begin
if (true_ena)
douta_tmp <= mem[addra];
end
end else begin
always @(posedge clka) begin
if (true_ena)
if (~wea)
douta_tmp <= mem[addra];
end
end
endgenerate
代码解析:
-
WF,写优先,写正在进行时,读端口输出写入值 -
RF,读优先,读端口始终输出存储器值 -
NC,无变化,写正在进行时,读端口输出不变
五、输出一/二级寄存器的实现
本自编模块的输出寄存器对RAM/ROM的性能没有影响,增加此功能只是为了与Vivado BRAM IP在增加寄存器时的输出时序保持一致。
输出寄存器相关参数:
parameter [1:0] OUTPUT_REG_NUM = 0, //* 可选0(默认), 1, 2
相关代码:
generate
if (OUTPUT_REG_NUM == 0) begin
assign douta = douta_tmp;
end else if (OUTPUT_REG_NUM == 1) begin
reg [DATA_WIDTH-1:0] douta_tmp_r1 = 'd0;
always @(posedge clka) begin
douta_tmp_r1 <= douta_tmp;
end
assign douta = douta_tmp_r1;
end else begin
reg [DATA_WIDTH-1:0] douta_tmp_r1 = 'd0;
reg [DATA_WIDTH-1:0] douta_tmp_r2 = 'd0;
always @(posedge clka) begin
douta_tmp_r1 <= douta_tmp;
douta_tmp_r2 <= douta_tmp_r1;
end
assign douta = douta_tmp_r2;
end
endgenerate
六、总结与分享
本文对自编RAM/ROM IP的功能、信号接口,关键代码进行了说明,关于模块的仿真验证和上板实测请参考下一篇文章。
本模块开源,两处仓库同步。
Gitee: Verilog 功能模块–RAM 和 ROM https://gitee.com/xuxiaokang/verilog-function-module–RAM_ROM
仿真和实测的Vivado 2024.2工程通过网盘分享(本系列文章所有分享均相同,均需获取一次即可):
欢迎大家关注我的微信公众号:徐晓康的博客,回复以下6位数字获取网盘链接。
402368
建议复制过去不会码错字!
如果本文对你有所帮助,欢迎点赞、转发、收藏、评论让更多人看到,赞赏支持就更好了。
如果对文章内容有疑问,请务必清楚描述问题,留言评论或私信告知我,我看到会回复。

徐晓康的博客持续分享高质量硬件、FPGA与嵌入式知识,软件,工具等内容,欢迎大家关注。