在verilog 仿真中,有时会使用$readmemh系统函数,帮助仿真者快速装载仿真所使用的数据。在一些简单的仿真中, 我们可以通过initial 来加载仿真的数据,但是当仿真数据比较多的情况下, 就需要使用系统函数加载数据了。$readmemh 系统函数就是用来帮助开发者加载仿真数据的。$readmemh系统函数本身具有文件操作功能,因此不需要$fopen等文件操作。通常来说 , $readmemh 也被归为文件操作类型的系统函数。
1.$readmemh 语法格式
$readmemh("<数据文件名>",<存储器名>); $readmemh("<数据文件名>",<存储器名>,<起始地址>); $readmemh("<数据文件名>",<存储器名>,<起始地址>,<终止地址>);
-
- <数据文件名> 是指向一个文本文件,用来保存仿真的数据。每一行代表一个十六进制的数据。
- <存储器名> 为仿真文件中例化的存储器的名称。
- <起始地址>,<终止地址> 指示将文本文件中的数据存储到存储器的位置段。
- 注:如果存储器的位宽是8-bit的, 那么使用$readmemh 将读取文本文件中每行的最后一个byte(8-bit)
2.$readmemh在Vivado下仿真的使用
-
存储器类型变量初始化
以下举例说明:
reg [7:0] ram[0:127]; initial begin $readmemh ("test.txt", ram); end
`timescale 1ns / 1ps module sim_top( ); reg clk = 0; always clk = #10 ~clk; reg [7:0] ram[0:127]; localparam FILE_NAME = "../../../led_sim.sim"; initial begin $readmemh (FILE_NAME, ram); end integer i; initial begin #20; for(i = 0; i < 16; i = i + 1) $display("ram[%02d] = 0x%h ", i, ram[ i ] ); #8000; $stop; end endmodule



00abcdef 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14

修改这个仿真文件:

-
利用$readmemh初始化block memory IP核
reg [7:0] ram [0:127]; 这样的定义既可以在仿真中使用,也可以在综合真实项目中使用。但在真实的Verilog 项目中, 更多的情况下是使用block memory 例化的IP 实现数据存储。 那么block memory IP 中的数据装载,我们可以使用*.coe 文件, 但是*.coe 文件不够灵活,每次修改*.coe 文件,都要重新生成IP ,然后才能仿真。 本文将使用$readmemh 系统函数,快速的加载数据到block memory IP 中, 方便仿真调试。使用$readmemh初始化block memory IP核的工程文件如例2所示。
例2:
使用环境:
Vivado 版本: Vivado 2018.2
开发板: FII-PRX100-D
代码如下:
`timescale 1ns / 1ps module top # ( parameter SIM_DEBUG = "FALSE" ) ( input OSC_CLK, output [7:0] LED ); wire clk_100m; wire locked; SYS_MMCM SYS_MMCM_inst ( // Clock in ports .clk_in1 (OSC_CLK), // input clk_in1 // Clock out ports .clk_out1 (clk_100m), // output clk_out1 // Status and control signals .reset (1'b0), // input reset .locked (locked) // output locked ); wire reset = ~locked; reg [7:0] led_val_in = 0; reg [9:0] led_addr = 0; reg led_wea = 0; wire [7:0] led_val; RAM_LED_VAL RAM_LED_VAL_inst ( .clka (clk_100m), // input wire clka .wea (led_wea), // input wire [0 : 0] wea .addra (led_addr), // input wire [9 : 0] addra .dina (led_val_in), // input wire [7 : 0] dina .douta (led_val) // output wire [7 : 0] douta ); reg [31:0] cnt = 0; always @ (posedge clk_100m) if(reset) cnt <= 0; else cnt <= cnt + 1; wire s_p = (SIM_DEBUG == "FALSE" ) ? cnt[24] : cnt[3]; reg [2:0] led_st = 0; always @ (posedge clk_100m) if(reset) begin led_wea <= 0; led_st <= 0; end else case (led_st) 0: begin led_addr <= 0; led_wea <= 0; led_st <= 1; end 1: begin if(s_p) begin led_addr <= led_addr + 1; led_st <= 2; end end 2: begin if(~s_p) led_st <= 1; end default: led_st <= 0; endcase assign LED = led_val; endmodule
例2中工程文件的主要功能是大约每330 ms,读取block memory (RAM_LED_VAL ) 中的数据,并将读出的数据显示到LED 上。其中在例化RAM_LED_VAL时,使用了*.coe文件,作为初始化的数据。
*.coe 文件内容及格式如下:
memory_initialization_radix = 16; memory_initialization_vector = ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00;
注意:
- *.coe文件里的memory_initialization_radix有效的选择只有 2,10或是16。
- 注意数字之间可以用一个空格,一个逗号,或者回车隔开。
- 在memory_initialization_radix行或者memory_initialization_vector行结尾时需要使用分号。
RAM_LED_VAL 例化步骤如下:
图6
图7
图8
当前工程的仿真文件如例3所示:
例3:
`timescale 1ns / 1ps module sim_top( ); reg clk = 0; always clk = #10 ~clk; localparam FILE_NAME = "../../../led_sim.sim"; initial begin $readmemh (FILE_NAME, top_inst.RAM_LED_VAL_inst.inst.native_mem_module.blk_mem_gen_v8_4_1_inst.memory); end integer i; initial begin #20; for(i = 0; i < 16; i = i + 1) $display("ram[%02d] = 0x%h ", i, ram[ i ] ); #8000; $stop; end wire [7:0] LED; top # ( .SIM_DEBUG ( "TRUE" ) ) top_inst ( .OSC_CLK (clk), .LED (LED) ); endmodule
其中$readmemh (FILE_NAME, top_inst.RAM_LED_VAL_inst.inst.native_mem_module.blk_mem_gen_v8_4_1_inst.memory); 是如何找到这个路径的呢?步骤如下:
- 先注释这一行文件,然后执行仿真工程。
- 在仿真Scope界面上(见图9)观察用户当前例化IP的位置。使用 . 连接上下级模块,例如top_inst.RAM_LED_VAL_inst.inst.native_mem_module.blk_mem_gen_v8_4_1_inst
- 在路径的末尾添加memory,最终完整的路径为top_inst.RAM_LED_VAL_inst.inst.native_mem_module.blk_mem_gen_v8_4_1_inst.memory
图9
在这里,就可以知道我们的IP 在什么地方了, 再将 $readmemh (FILE_NAME, top_inst.RAM_LED_VAL_inst.inst.native_mem_module.blk_mem_gen_v8_4_1_inst.memory); 这一行重新打开(不再注释掉),就可以将led_sim.sim 中的内容存储到RAM_LED_VAL_inst中,从而在仿真中替换了RAM_LED_VAL_inst 中原有的*.coe文件。 由于led_sim.sim 文件修改, 仿真文件执行速度都是比较快的, 所以这种方法要比修改*.coe文件, 重新生成IP 核,工作效率要高很多。
仿真结果如下:
图10
至此, 在不修改verilog 工程的情况下, 只是使用仿真系统任务,就可以轻松替换 IP 中的*.coe文件,快速的进行仿真了。
3.$readmemh在Vivado综合中的使用
$readmemh系统函数也可以在综合(Synthesis)时使用,下面的例3就是对存储器类型变量初始化。(注:Vivado生成的block memory暂时不支持在综合中使用$readmemh系统函数初始化数据,但是综合时不会报错。)
使用$readmemh对存储器类型变量初始化时,应注意存储器单元的引用必须使用变量索引,不能使用常数索引,如下例所示:
错误使用:
reg [7:0] coeff_array [0:7];
reg [7:0] out = 0;
always@(posedge clk)begin
out <= coeff_array[0];
end
正确使用:
reg [7:0] coeff_array [0:7];
reg [7:0] out = 0;
reg [3:0] addr = 0;
always@(posedge clk)begin
out <= coeff_array[addr];
end
例3:
`timescale 1ns / 1ps module test # ( parameter SIM_DEBUG = "FALSE" ) ( input OSC_CLK, output [7:0] LED ); wire clk_100m; wire locked; SYS_MMCM SYS_MMCM_inst ( // Clock in ports .clk_in1 (OSC_CLK), // input clk_in1 // Clock out ports .clk_out1 (clk_100m), // output clk_out1 // Status and control signals .reset (1'b0), // input reset .locked (locked) // output locked ); wire reset = ~locked; reg [7:0] led_val_in = 0; reg [9:0] led_addr = 0; reg led_wea = 0; reg [31:0] cnt = 0; always @ (posedge clk_100m) if(reset) cnt <= 0; else cnt <= cnt + 1; wire s_p = (SIM_DEBUG == "FALSE" ) ? cnt[24] : cnt[3]; reg [7:0] ram[0:127]; reg [7:0] led_reg; reg [2:0] led_st = 0; always @ (posedge clk_100m) if(reset) begin led_addr <= 0; led_wea <= 0; led_st <= 0; end else case (led_st) 0: begin led_addr <= 0; led_reg <= ram[led_addr]; led_wea <= 0; led_st <= 1; end 1: begin if(s_p) begin led_addr <= led_addr + 1; led_st <= 2; end end 2: begin if(~s_p) begin led_reg <= ram[led_addr]; led_st <= 1; end end default: led_st <= 0; endcase localparam FILE_NAME = "D:/PRJ/led_sim.sim"; initial begin $readmemh (FILE_NAME, ram); end assign LED = led_reg; endmodule
综合后生成*.bit文件在FII-PRX100-D开发板上的实验现象与仿真结果不符合。在写完21个数据后,从地址(led_addr )22-31,输出的led_reg结果确实为0。但是,从led_addr 等于32开始,又重复出现了前面的21个数据,如图11所示(ILA 输出结果)。
图11
解决方案:
将led_sim.sim文本文件写满128行数据,以对应128位不同的地址。总结来说,在Vivado下使用$readmemh系统函数进行综合建议保持文本文件的行数和存储器的深度一致。
4.$readmemh在Quartus及ModelSim下使用
考虑到差异性,将上面的例子做简单修改,在Quartus及Modelsim下进行测试,工程文件及仿真文件如例4所示。
例4:
Quartus 下的工程文件
`timescale 1ns / 1ps module readmemh_prj # ( parameter SIM_DEBUG = "FALSE" ) ( input clk_50m, output [7:0] LED ); wire reset = 1'b0; reg [7:0] led_val_in = 0; (* mark_debug = "true" *)reg [6:0] led_addr = 0; reg led_wea = 0; reg [31:0] cnt = 0; always @ (posedge clk_50m) if(reset) cnt <= 0; else cnt <= cnt + 1; (* mark_debug = "true" *)wire s_p = (SIM_DEBUG == "FALSE" ) ? cnt[23] : cnt[3]; reg [7:0] ram[0:127]; (* mark_debug = "true" *)reg [7:0] led_reg; (* mark_debug = "true" *)reg [2:0] led_st = 0; always @ (posedge clk_50m) if(reset) begin led_addr <= 0; led_wea <= 0; led_st <= 0; end else case (led_st) 0: begin led_addr <= 0; // led_reg <= ram[led_addr]; led_reg <= ram[0]; //测试综合后,是否正常运行 led_wea <= 0; led_st <= 1; end 1: begin if(s_p) begin led_addr <= led_addr + 1; led_st <= 2; end end 2: begin if(~s_p) begin led_reg <= ram[led_addr]; led_st <= 1; end end default: led_st <= 0; endcase localparam FILE_NAME = "D:/PRJ/led_sim.sim"; initial begin $readmemh (FILE_NAME, ram); end assign LED = led_reg; endmodule
Quartus 下的仿真文件
`timescale 1ns / 1ps module sim_top( ); reg clk = 0; always clk = #10 ~clk; integer i; initial begin #20; for(i = 0; i < 16; i = i + 1) $display("ram[%02d] = 0x%h ", i, top_inst.ram[ i ] ); #8000; $stop; end wire [7:0] LED; readmemh_prj # ( .SIM_DEBUG ( "TRUE" ) ) top_inst ( .clk_50m (clk), .LED (LED) ); endmodule
-
ModelSim下仿真
对存储器型变量中使用$readmemh在modelsim下进行仿真,有如图12所示警告,ram的值没有按照文件中给定的值初始化。ModelSim 要求$readmemh读取的文本文件每行的数据宽度必须小于等于存储器定义宽度。
图12
修改方案: 将led_sim.sim 文本文件中的第一行由00abcdef 修改为 ef 即可。修改后的仿真结果如图13所示。
图13
从图13中可以看出,ModelSim下对存储器型变量使用$readmemh的仿真结果是正确的。
-
Quartus 下利用$readmemh进行综合测试
例4中工程综合后并在FII-PRA006(Altera FPGA)开发板上进行测试,结果与仿真一致,可以正确运行。同时从例4也可以看出,在Quartus常量索引存储器或者使用变量索引存储器都是可行的。
由此可见,$readmemh系统函数在仿真中使用较多,在实际综合中使用还是需要注意一些限制条件。
vivado 工程文件下载(注册用户可见)