原文网站: vivado 仿真工程中$readmemh 使用 – 芯片天地
在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
例化一个 8-bit 宽的ram存储器,深度为128。 将文本文件test.txt 文件中每一行最后一个byte 数据,存储到ram中, 从ram[0] 开始, 直到test.txt 文件的最后一行结束,或者是读出的数据达到 ram 的最大深度。文本文件的格式需要注意只能使用0-9,a-f,每一行的末尾需要以回车换行结束。使用$readmemh的仿真文件如例1所示:
例1:
`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
其中仿真引入的文件led_sim.sim 为文本文件, 缺省的目录为仿真执行的目录:
图1
只要将文本文件led_sim.sim 放到xsim目录下,就可以打开了。在仿真文件中可以按如下方式引用,
localparam FILE_NAME = “led_sim.sim”;
使用xsim目录的缺点是每次仿真复位后(右键点击SIMULTAION,选择Reset,如图2所示),会删除led_sim.sim 文件,用户需要重新制作文件,再次放入的xsim 这个目录中,比较麻烦; 如果将led_sim.sim 放到其他的目录下,可以避免这个问题的出现。
图2
因此上例中,并没有将led_sim.sim 放到xsim 这个目录下,而是使用的../../../led_sim.sim, 这时使用重定向来指定文本文件的位置:
图3
led_sim.sim 文件的内容及格式如下:
00abcdef
01
02
03
04
05
06
07
08
09
0a
0b
0c
0d
0e
0f
10
11
12
13
14
仿真输出结果如下:
图4
图4中显示了读取16 个 ram 中数据的结果。
修改这个仿真文件:
将 $readmemh (FILE_NAME, ram); 替换为 $readmemh (FILE_NAME, ram, 2, 8); 表示将led_sim.sim 文本文件中的数据 写入ram中 (从ram[2] 开始写, 写到ram[8] 结束)
仿真结果如下:
图5
注意图5中显示ram[00],ram[01],ram[09]-ram[15]的值都是0xxx(未初始化的值),真正初始化的只有ram[02]- ram[08]。使用 $readmemh (FILE_NAME, ram, 2, 8); 这种格式可以在一定范围内对存储器进行局部修改。
-
利用$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系统函数在仿真中使用较多,在实际综合中使用还是需要注意一些限制条件。