[Verilog HDL] Verilog 仿真激励

[复制链接]
 楼主| gaochy1126 发表于 2023-1-31 21:21 | 显示全部楼层 |阅读模式
Verilog 代码设计完成后,还需要进行重要的步骤,即逻辑功能仿真。仿真激励文件称之为 testbench,放在各设计模块的顶层,以便对模块进行系统性的例化调用进行仿真。
毫不夸张的说,对于稍微复杂的 Verilog 设计,如果不进行仿真,即便是经验丰富的老手,99.9999% 以上的设计都不会正常的工作。不能说仿真比设计更加的重要,但是一般来说,仿真花费的时间会比设计花费的时间要多。有时候,考虑到各种应用场景,testbench 的编写也会比 Verilog 设计更加的复杂。所以,数字电路行业会具体划分设计工程师和验证工程师。
下面,对 testbench 做一个简单的学习。
testbench 结构划分testbench 一般结构如下:
其实 testbench 最基本的结构包括信号声明、激励和模块例化。
根据设计的复杂度,需要引入时钟和复位部分。当然更为复杂的设计,激励部分也会更加复杂。根据自己的验证需求,选择是否需要自校验和停止仿真部分。
当然,复位和时钟产生部分,也可以看做激励,所以它们都可以在一个语句块中实现。也可以拿自校验的结果,作为结束仿真的条件。
实际仿真时,可以根据自己的个人习惯来编写 testbench,这里只是做一份个人的总结。
testbench 仿真举例
前面的章节中,已经写过很多的 testbench。其实它们的结构也都大致相同。
下面,我们举一个数据拼接的简单例子,对 testbench 再做一个具体的分析。
一个 2bit 数据拼接成 8bit 数据的功能模块描述如下:
  1. module  data_consolidation
  2.     (
  3.         input           clk ,
  4.         input           rstn ,
  5.         input [1:0]     din ,          //data in
  6.         input           din_en ,
  7.         output [7:0]    dout ,
  8.         output          dout_en        //data out
  9.      );

  10.    // data shift and counter
  11.     reg [7:0]            data_r ;
  12.     reg [1:0]            state_cnt ;
  13.     always @(posedge clk or negedge rstn) begin
  14.         if (!rstn) begin
  15.             state_cnt     <= 'b0 ;
  16.             data_r        <= 'b0 ;
  17.         end
  18.         else if (din_en) begin
  19.             state_cnt     <= state_cnt + 1'b1 ;    //数据计数
  20.             data_r        <= {data_r[5:0], din} ;  //数据拼接
  21.         end
  22.         else begin
  23.             state_cnt <= 'b0 ;
  24.         end
  25.     end
  26.     assign dout          = data_r ;

  27.     // data output en
  28.     reg                  dout_en_r ;
  29.     always @(posedge clk or negedge rstn) begin
  30.         if (!rstn) begin
  31.             dout_en_r       <= 'b0 ;
  32.         end
  33.         //计数为 3 且第 4 个数据输入时,同步输出数据输出使能信号
  34.         else if (state_cnt == 2'd3 & din_en) begin  
  35.             dout_en_r       <= 1'b1 ;
  36.         end
  37.         else begin
  38.             dout_en_r       <= 1'b0 ;
  39.         end
  40.     end
  41.     //这里不直接声明dout_en为reg变量,而是用相关寄存器对其进行assign赋值
  42.     assign dout_en       = dout_en_r;

  43. endmodule
对应的 testbench 描述如下,增加了文件读写的语句:
  1. `timescale 1ns/1ps

  2.    //============== (1) ==================
  3.    //signals declaration
  4. module test ;
  5.     reg          clk;
  6.     reg          rstn ;
  7.     reg [1:0]    din ;
  8.     reg          din_en ;
  9.     wire [7:0]   dout ;
  10.     wire         dout_en ;

  11.     //============== (2) ==================
  12.     //clock generating
  13.     real         CYCLE_200MHz = 5 ; //
  14.     always begin
  15.         clk = 0 ; #(CYCLE_200MHz/2) ;
  16.         clk = 1 ; #(CYCLE_200MHz/2) ;
  17.     end

  18.     //============== (3) ==================
  19.     //reset generating
  20.     initial begin
  21.         rstn      = 1'b0 ;
  22.         #8 rstn      = 1'b1 ;
  23.     end

  24.     //============== (4) ==================
  25.     //motivation
  26.     int          fd_rd ;
  27.     reg [7:0]    data_in_temp ;  //for self check
  28.     reg [15:0]   read_temp ;     //8bit ascii data, 8bit \n
  29.     initial begin
  30.         din_en    = 1'b0 ;        //(4.1)
  31.         din       = 'b0 ;
  32.         open_file("../tb/data_in.dat", "r", fd_rd); //(4.2)
  33.         wait (rstn) ;    //(4.3)
  34.         # CYCLE_200MHz ;

  35.         //read data from file
  36.         while (! $feof(fd_rd) ) begin  //(4.4)
  37.             @(negedge clk) ;
  38.             $fread(read_temp, fd_rd);
  39.             din    = read_temp[9:8] ;
  40.             data_in_temp = {data_in_temp[5:0], din} ;
  41.             din_en = 1'b1 ;
  42.         end

  43.         //stop data
  44.         @(posedge clk) ;  //(4.5)
  45.         #2 din_en = 1'b0 ;
  46.     end

  47.     //open task
  48.     task open_file;
  49.         input string      file_dir_name ;
  50.         input string      rw ;
  51.         output int        fd ;

  52.         fd = $fopen(file_dir_name, rw);
  53.         if (! fd) begin
  54.             $display("--- iii --- Failed to open file: %s", file_dir_name);
  55.         end
  56.         else begin
  57.             $display("--- iii --- %s has been opened successfully.", file_dir_name);
  58.         end
  59.     endtask

  60.     //============== (5) ==================
  61.     //module instantiation
  62.     data_consolidation    u_data_process
  63.     (
  64.       .clk              (clk),
  65.       .rstn             (rstn),
  66.       .din              (din),
  67.       .din_en           (din_en),
  68.       .dout             (dout),
  69.       .dout_en          (dout_en)
  70.      );

  71.     //============== (6) ==================
  72.     //auto check
  73.     reg  [7:0]           err_cnt ;
  74.     int                  fd_wr ;

  75.     initial begin
  76.         err_cnt   = 'b0 ;
  77.         open_file("../tb/data_out.dat", "w", fd_wr);
  78.         forever begin
  79.             @(negedge clk) ;
  80.             if (dout_en) begin
  81.                 $fdisplay(fd_wr, "%h", dout);
  82.             end
  83.         end
  84.     end

  85.     always @(posedge clk) begin
  86.         #1 ;
  87.         if (dout_en) begin
  88.             if (data_in_temp != dout) begin
  89.                 err_cnt = err_cnt + 1'b1 ;
  90.             end
  91.         end
  92.     end

  93.     //============== (7) ==================
  94.     //simulation finish
  95.     always begin
  96.         #100;
  97.         if ($time >= 10000)  begin
  98.             if (!err_cnt) begin
  99.                 $display("-------------------------------------");
  100.                 $display("Data process is OK!!!");
  101.                 $display("-------------------------------------");
  102.             end
  103.             else begin
  104.                 $display("-------------------------------------");
  105.                 $display("Error occurs in data process!!!");
  106.                 $display("-------------------------------------");
  107.             end
  108.             #1 ;
  109.             $finish ;
  110.         end
  111.     end

  112. endmodule // test
仿真结果如下。由图可知,数据整合功能的设计符合要求:
[url=https://www.runoob.com/wp-content/uploads/2020/09/Yp7UIofA10Bynfgh.png][/url]

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?注册

×
 楼主| gaochy1126 发表于 2023-1-31 21:22 | 显示全部楼层
testbench 具体分析
1)信号声明
testbench 模块声明时,一般不需要声明端口。因为激励信号一般都在 testbench 模块内部,没有外部信号。
声明的变量应该能全部对应被测试模块的端口。当然,变量不一定要与被测试模块端口名字一样。但是被测试模块输入端对应的变量应该声明为 reg 型,如 clk,rstn 等,输出端对应的变量应该声明为 wire 型,如 dout,dout_en。
2)时钟生成
生成时钟的方式有很多种,例如以下两种生成方式也可以借鉴。
  1. initial clk = 0 ;
  2. always #(CYCLE_200MHz/2) clk = ~clk;

  3. initial begin
  4.     clk = 0 ;
  5.     forever begin
  6.         #(CYCLE_200MHz/2) clk = ~clk;
  7.     end
  8. end      

需要注意的是,利用取反方法产生时钟时,一定要给 clk 寄存器赋初值。
利用参数的方法去指定时间延迟时,如果延时参数为浮点数,该参数不要声明为 parameter 类型。例如实例中变量 CYCLE_200MHz 的值为 2.5。如果其变量类型为 parameter,最后生成的时钟周期很可能就是 4ns。当然,timescale 的精度也需要提高,单位和精度不能一样,否则小数部分的时间延迟赋值也将不起作用。
3)复位生成
复位逻辑比较简单,一般赋初值为 0,再经过一段小延迟后,复位为 1 即可。
这里大多数的仿真都是用的低有效复位。
4)激励部分
激励部分该产生怎样的输入信号,是根据被测模块的需要来设计的。
本次实例中:
  • (4.1) 对被测模块的输入信号进行一个初始化,防止不确定值 X 的出现。激励数据的产生,我们需要从数据文件内读入。
  • (4.2) 处利用一个 task 去打开一个文件,只要指定文件存在,就可以得到一个不为 0 的句柄信号 fp_rd。fp_rd 指定了文件数据的起始地址。
  • (4.3) 的操作是为了等待复位后,系统有一个安全稳定的可测试状态。
  • (4.4) 开始循环读数据、给激励。在时钟下降沿送出数据,是为了被测试模块能更好的在上升沿采样数据。
利用系统任务 $fread ,通过句柄信号 fd_rd 将读取的 16bit 数据变量送入到 read_temp 缓存。
输入数据文件前几个数据截图如下。因为 $fread 只能读取 2 进制文件,所以输入文件的第一行对应的 ASCII 码应该是 330a,所以我们想要得到文件里的数据 3,应该取变量 read_temp 的第 9 到第 8bit 位的数据。
信号 data_in_temp 是对输入数据信号的一个紧随的整合,后面校验模块会以此为参考,来判断仿真是否正常,模块设计是否正确。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?注册

×
 楼主| gaochy1126 发表于 2023-1-31 21:22 | 显示全部楼层
  • (4.5) 选择在时钟上升沿延迟 2 个周期后停止输入数据,是为了被测试模块能够正常的采样到最后一个数据使能信号,并对数据进行正常的整合。
当数据量相对较少时,可以利用 Verilog 中的系统任务 $readmemh 来按行直接读取 16 进制数据。保持文件 data_in.dat 内数据和格式不变,则该激励部分可以描述为:
  1. reg [1:0]    data_mem [39:0] ;
  2.     reg [7:0]    data_in_temp ;  //for self check
  3.     integer      k1 ;
  4.     initial begin
  5.         din_en    = 1'b0 ;
  6.         din       = 'b0 ;
  7.         $readmemh("../tb/data_in.dat", data_mem);
  8.         wait (rstn) ;
  9.         # CYCLE_200MHz ;

  10.         //read data from file
  11.         for(k1=0; k1<40; k1=k1+1)  begin
  12.             @(negedge clk) ;
  13.             din    = data_mem[k1] ;
  14.             data_in_temp = {data_in_temp[5:0], din} ;
  15.             din_en = 1'b1 ;
  16.         end

  17.         //stop data
  18.         @(posedge clk) ;
  19.         #2 din_en = 1'b0 ;
  20.      end

5)模块例化
这里利用 testbench 开始声明的信号变量,对被测试模块进行例化连接。
6)自校验
如果设计比较简单,完全可以通过输入、输出信号的波形来确定设计是否正确,此部分完全可以删除。如果数据很多,有时候拿肉眼观察并不能对设计的正确性进行一个有效判定。此时加入一个自校验模块,会大大增加仿真的效率。
实例中,我们会在数据输出使能 dout_en 有效时,对输出数据 dout 与参考数据 read_temp(激励部分产生)做一个对比,并将对比结果置于信号 err_cnt 中。最后就可以通过观察 err_cnt 信号是否为 0 来直观的对设计的正确性进行判断。
当然如实例中所示,我们也可以将数据写入到对应文件中,利用其他方式做对比。

 楼主| gaochy1126 发表于 2023-1-31 21:22 | 显示全部楼层
7)结束仿真
如果我们不加入结束仿真部分,仿真就会无限制的运行下去,波形太长有时候并不方便分析。Verilog 中提供了系统任务 $finish 来停止仿真。
停止仿真之前,可以将自校验的结果,通过系统任务 $display 在终端进行显示。
文件读写选项
用于打开文件的系统任务 $fopen 格式如下:
  1. fd = $fopen("<name_of_file>", "mode")

和 C 语言类似,打开方式的选项 "mode" 意义如下:
r
只读打开一个文本文件,只允许读数据。

w只写打开一个文本文件,只允许写数据。如果文件存在,则原文件内容会被删除。如果文件不存在,则创建新文件。
a追加打开一个文本文件,并在文件末尾写数据。如果文件如果文件不存在,则创建新文件。
rb只读打开一个二进制文件,只允许读数据。
wb只写打开或建立一个二进制文件,只允许写数据。
ab追加打开一个二进制文件,并在文件末尾写数据。
r+读写打开一个文本文件,允许读和写
w+读写打开或建立一个文本文件,允许读写。如果文件存在,则原文件内容会被删除。如果文件不存在,则创建新文件。
a+读写打开一个文本文件,允许读和写。如果文件不存在,则创建新文件。读取文件会从文件起始地址的开始,写入只能是追加模式。
rb+读写打开一个二进制文本文件,功能与 "r+" 类似。
wb+读写打开或建立一个二进制文本文件,功能与 "w+" 类似。
ab+读写打开一个二进制文本文件,功能与 "a+" 类似。


您需要登录后才可以回帖 登录 | 注册

本版积分规则

个人签名:这个社会混好的两种人:一是有权有势,二是没脸没皮的。

1148

主题

11651

帖子

26

粉丝
快速回复 在线客服 返回列表 返回顶部
个人签名:这个社会混好的两种人:一是有权有势,二是没脸没皮的。

1148

主题

11651

帖子

26

粉丝
快速回复 在线客服 返回列表 返回顶部