Menu Close

仿真工程中$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 为文本文件, 缺省的目录为仿真执行的目录:
%title插图%num
图1
只要将文本文件led_sim.sim 放到xsim目录下,就可以打开了。在仿真文件中可以按如下方式引用,
localparam FILE_NAME = “led_sim.sim”;
使用xsim目录的缺点是每次仿真复位后(右键点击SIMULTAION,选择Reset,如图2所示),会删除led_sim.sim 文件,用户需要重新制作文件,再次放入的xsim 这个目录中,比较麻烦; 如果将led_sim.sim 放到其他的目录下,可以避免这个问题的出现。
%title插图%num
图2
因此上例中,并没有将led_sim.sim 放到xsim 这个目录下,而是使用的../../../led_sim.sim, 这时使用重定向来指定文本文件的位置:
%title插图%num
图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

 

仿真输出结果如下:
%title插图%num
图4
图4中显示了读取16 个 ram 中数据的结果。

修改这个仿真文件:

$readmemh (FILE_NAME, ram); 替换为 $readmemh (FILE_NAME, ram, 2, 8);  表示将led_sim.sim 文本文件中的数据 写入ram中 (从ram[2] 开始写, 写到ram[8] 结束)
仿真结果如下:
%title插图%num
图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;

注意:

  1. *.coe文件里的memory_initialization_radix有效的选择只有 2,10或是16。
  2. 注意数字之间可以用一个空格,一个逗号,或者回车隔开。
  3. 在memory_initialization_radix行或者memory_initialization_vector行结尾时需要使用分号。

RAM_LED_VAL  例化步骤如下:

 

%title插图%num

图6

%title插图%num

 

图7

%title插图%num

 

图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

%title插图%num

 

图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 核,工作效率要高很多。

 

仿真结果如下:

%title插图%num

图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 输出结果)。

%title插图%num

图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读取的文本文件每行的数据宽度必须小于等于存储器定义宽度。

%title插图%num

图12

修改方案: 将led_sim.sim 文本文件中的第一行由00abcdef 修改为 ef 即可。修改后的仿真结果如图13所示。

%title插图%num

图13

从图13中可以看出,ModelSim下对存储器型变量使用$readmemh的仿真结果是正确的。

  • Quartus 下利用$readmemh进行综合测试

例4中工程综合后并在FII-PRA006(Altera FPGA)开发板上进行测试,结果与仿真一致,可以正确运行。同时从例4也可以看出,在Quartus常量索引存储器或者使用变量索引存储器都是可行的。

由此可见,$readmemh系统函数在仿真中使用较多,在实际综合中使用还是需要注意一些限制条件。

vivado  工程文件下载(注册用户可见)

本文隐藏内容 登陆 后才可以浏览

 

Posted in FPGA, FPGA, Verilog, Verilog, 教材与教案, 文章

发表评论

相关链接