当前位置:网站首页>[转载]Verilog testbench总结
[转载]Verilog testbench总结
2022-08-11 05:14:00 【LateLinux】
原文地址:https://blog.csdn.net/wordwarwordwar/article/details/53885209
1. 激励的产生
对于testbench而言,端口应当和被测试的module一一对应。端口分为input,output和inout类型产生激励信号的时候,input对应的端口应当申明为reg, output对应的端口申明为wire,inout端口比较特殊,下面专门讲解。
1)直接赋值。
一般用initial块给信号赋初值,initial块执行一次,always或者forever表示由事件激发反复执行。举例,一个module
module exam();
reg rst_n;
reg clk;
reg data;
initial
begin
clk=1'b0;
rst=1'b1;
#10
rst=1'b0;
#500
rst=1'b1;
end
always
begin
#10
clk=~clk;
end
endmodule大家应该注意到有个#符号,该符号的意思是指延迟相应的时间单位。该时间单位由timscale决定.一般在testbench的开头定义时间单位和仿真 精度,比如`timescale 1ns/1ps,前面一个是代表时间单位,后面一个代表仿真时间精度。以上面的例子而言,一个时钟周期是20个单位,也就是20ns。而仿真时间精度的概 念就是,你能看到1.001ns时对应的信号值,而假如timescale 1ns/1ns,1.001ns时候的值就无法看到。对于一个设计而言,时间刻度应该统一,如果设计文件和testbench里面的时间刻度不一致,仿真器默认以testbench为准。一个较好的办法是写一个global.v文件,然后用include的办法,可以防止这个问题。
对于反复执行的操作,可写成task,然后调用,比如
task load_count;
input [3:0] load_value;
begin
@(negedge clk_50);
$display($time, " << Loading the counter with %h >>", load_value);
load_l = 1’b0;
count_in = load_value;
@(negedge clk_50);
load_l = 1’b1;
end
endtask //of load_count
initial
begin
load_count(4’hA); // 调用task
end
其他像forever,for,function等等语句用法类似,虽然不一定都能综合,但是用在testbench里面很方便,大家可以自行查阅参考文档。自行添加一个forever的例子,生成时钟很有用。
forever begin
#0 clk=0;
#10 clk=1;
#10 clk=0;
end
或者
clk = 0;
forever #10 clk = ~clk;2) 文件输入
有时候,需要大量的数据输入,直接赋值的话比较繁琐,可以先生成数据,再将数据读入到寄存器中,需要时取出即可。用 $readmemb系统任务从文本文件中读取二进制向量(可以包含输入激励和输出期望值)。$readmemh 用于读取十六进制文件。例如:
reg [7:0] mem[1:256] // a 8-bit, 256-word 定义存储器mem
initial $readmemh ( "E:/readhex/mem.dat", mem ) // 将.dat文件读入寄存器mem中
initial $readmemh ( "E:/readhex/mem.dat", mem, 128, 1 ) // 参数为寄存器加载数据的地址始终
2. 查看仿真结果
对于简单的module来说,要在modelsim的仿真窗口里面看波形,就用add wave ..命令
比如,testbench的顶层module名叫tb,要看时钟信号,就用add wave tb.clk
要查看所有信号的时候,就用 add wave /*
当然,也可以在workspace下的sim窗口里面右键单击instance来添加波形
对于复杂的仿真,免不了要记录波形和数据到文件里面去。
1)波形文件记录
常见的波形文件一般有两种,vcd和fsdb,debussy是个很好的工具,支持fsdb,所以最好是modelsim+debussy的组合
默认情况下,modelsim不认识fsdb,所以需要先装debussy,再生成fsdb文件。
$dumpfile和$dumpvar是verilog语言中的两个系统任务,可以调用这两个系统任务来创建和将指定信息导入VCD文件.
对于fsdb文件来说,对应的命令是fsdbDumpfile,dumpfsdbvars
(什么是VCD文件? 答:VCD文件是在对设计进行的仿真过程中,记录各种信号取值变化情况的信息记录文件。EDA工具通过读取VCD格式的文件,显示图形化的仿真波形,所以,可以把VCD文件简单地视为波形记录文件.)下面分别描述它们的用法并举例说明之。
$dumpfile系统任务:为所要创建的VCD文件指定文件名。
举例("//"符号后的内容为注释文字):
initial
$dumpfile ("myfile.dump"); //指定VCD文件的名字为myfile.dump,仿真信息将记录到此文件
$dumpvar系统任务:指定需要记录到VCD文件中的信号,可以指定某一模块层次上的所有信号,也可以单独指定某一个信号。
典型语法为$dumpvar(level, module_name); 参数level为一个整数,用于指定层次数,参数module则指定要记录的模块。整句的意思就是,对于指定的模块,包括其下各个层次(层次数由 level指定)的信号,都需要记录到VCD文件中去。
举例:
initial
$dumpvar (0, top); //指定层次数为0,则top模块及其下面各层次的所有信号将被记录
initial
$dumpvar (1, top); //记录模块实例top以下一层的信号
//层次数为1,即记录top模块这一层次的信号
//对于top模块中调用的更深层次的模块实例,则不记录其信号变化
initial
$dumpvar (2, top); //记录模块实例top以下两层的信号
//即top模块及其下一层的信号将被记录假设模块top中包含有子模块module1,而我们希望记录top.module1模块以下两层的信号,则语法举例如下:
initial
$dumpvar (2, top.module1); //模块实例top.module1及其下一层的信号将被记录假设模块top包含信号signal1和signal2(注意是变量而不是子模块), 如我们希望只记录这两个信号,则语法举例如下:
initial
$dumpvar (0, top.signal1, top.signal2); //虽然指定了层次数,但层次数是不影响单独指定的信号的
//即指定层次数和单独指定的信号无关我们甚至可以在同一个$dumpvar的调用中,同时指定某些层次上的所有信号和某个单独的信号,假设模块top包含信号signal1,同时包含有子模 块module1,如果我们不但希望记录signal1这个独立的信号,而且还希望记录子模块module1以下三层的所有信号,则语法举例如下:
initial
$dumpvar (3, top.signal1, top.module1); //指定层次数和单独指定的信号无关
//所以层次数3只作用于模块top.module1, 而与信号top.signal1无关
上面这个例子和下面的语句是等效的:
initial
begin
$dumpvar (0, top.signal1);
$dumpvar (3, top.module1);
end
$dumpvar的特别用法(不带任何参数):
initial
$dumpvar; //无参数,表示设计中的所有信号都将被记录最后,我们将$dumpfile和$dumpvar这两个系统任务的使用方法在下面的例子中综合说明,假设我们有一个设计实例,名为 i_design,此设计中包含模块module1,模块module1下面还有很多层次,我们希望对这个设计进行仿真,并将仿真过程中模块 module1及其以下所有层次中所有信号的变化情况,记录存储到名为mydesign.dump的VCD文件中去,则例示如下:
initial
begin
$dumpfile ("mydesign.dump"); //指定VCD文件名为mydesign.dump
$dumpvar (0, i_design.module1); //记录i_design.module1模块及其下面层次中所有模块的所有信号
end对于生成fsdb文件而言,也是类似的
initial
begin
$fsdbDumpfile("tb_xxx.fsdb");
$fsdbDumpvars(0,tb_xxx);
end
2)文件输出结果
integer out_file; // out_file 是一个文件描述,需要定义为 integer类型
out_file = $fopen ( " cpu.data " ); // cpu.data 是需要打开的文件,也就是最终的输出文本设计中的信号值可以通过$fmonitor, $fdisplay,$fwrite
其中$fmonitor只要有变化就一直记录,$fdisplay和$fwrite需要触发条件才记录
例子:
initial begin
$fmonitor(file_id, "%m: %t in1=%d o1=%h", $time, in1, o1);
end
[email protected](a or b)
begin
$fwrite(file_id,"At time%t a=%b b=%b",$realtime,a,b);
end3 testbench的技巧
1).如果激励中有一些重复的项目,可以考虑将这些语句编写成一个task,这样会给书写和仿真带来很大方便。例如,一个存储器的testbench的激励可以包含write,read等task。
2).如果DUT中包含双向信号(inout),在编写testbench时要注意。需要一个reg变量来表示其输入,还需要一个wire变量表示其输出。
3).如果initial块语句过于复杂,可以考虑将其分为互补相干的几个部分,用数个initial块来描述。在仿真时,这些initial块会并发运行。这样方便阅读和修改。
4).每个testbench都最好包含$stop语句,用以指明仿真何时结束。
5).加载测试向量时,避免在时钟的上下沿变化,比如数据最好在时钟上升沿之前变化,这也符合建立时间的要求。
4.一个简单的例子
module counter (clk, reset, enable, count);
input clk, reset, enable;
output [3:0] count;
reg [3:0] count;
always @ (posedge clk) begin
if (reset == 1'b1) begin
count <= 0;
end else if ( enable == 1'b1) begin
count <= count + 1;
end
end
endmodule
testbench
module counter_tb;
reg clk, reset, enable;
wire [3:0] count;
counter U0 (
.clk (clk),
.reset (reset),
.enable (enable),
.count (count)
);
initial begin
clk = 0;
reset = 0;
enable = 0;
end
always
#5 clk = ! clk;
initial begin
$dumpfile ("counter.vcd");
$dumpvars;
end
initial begin
$display("\t\ttime,\tclk,\treset,\tenable,\tcount");
$monitor("‰d,\t‰b,\t‰b,\t‰b,\t‰d",$time, clk,reset,enable,count);
end
initial
#100 $finish;
//Rest of testbench code after this line
endmodule
5 双向端口
这个我没用过,完全是从网上google的,如果有问题,大家再讨论吧
芯片外部引脚很多都使用inout类型的,为的是节省管腿。一般信号线用做总线等双向数据传输的时候就要用到INOUT类型了。就是一个端口同时做输入和 输出。 inout在具体实现上一般用三态门来实现。三态门的第三个状态就是高阻'Z'。当inout端口不输出时,将三态门置高阻。这样信号就不会因为两端同时 输出而出错了,更详细的内容可以搜索一下三态门tri-state的资料.
1 使用inout类型数据,可以用如下写法:
inout data_inout;
input data_in;
reg data_reg; //data_inout的映象寄存器
reg link_data;
assign data_inout = link_data ? data_reg : 1’bz;
//link_data控制三态门
//对于data_reg,可以通过组合逻辑或者时序逻辑根据data_in对其赋值.通过控制link_data的高低电平,从而设置data_inout是输出数据还是处于高阻态,如果处于高阻态,则此时当作输入端口使用.link_data可以通过相关电路来控制.2 编写测试模块时,对于inout类型的端口,需要定义成wire类型变量,而其它输入端口都定义成reg类型,这两者是有区别的.
当上面例子中的data_inout用作输入时,需要赋值给data_inout,其余情况可以断开.此时可以用assign语句实现:assign data_inout=link?data_in_t:1’bz;其中的link ,data_in_t是reg类型变量,在测试模块中赋值.
另外,可以设置一个输出端口观察data_inout用作输出的情况:
Wire data_out;
Assign data_out_t=(!link)?data_inout:1’bz;
else,in RTL
inout use in top module(PAD)
don't use inout(tri) in sub module
也就是说,在内部模块最好不要出现inout,如果确实需要,那么用两个port实现,到顶层的时候再用三态实现。理由是:在非顶层模块用双向口的话,该 双向口必然有它的上层跟它相连。既然是双向口,则上层至少有一个输入口和一个输出口联到该双向口上,则发生两个内部输出单元连接到一起的情况出现,这样在 综合时往往会出错。
对双向口,我们可以将其理解为2个分量:一个输入分量,一个输出分量。另外还需要一个控制信号控制输出分量何时输出。此时,我们就可以很容易地对双向端口建模。
例子CODE:
module dual_port (
....
inout_pin,
....
);
inout inout_pin;
wire inout_pin;
wire input_of_inout;
wire output_of_inout;
wire out_en;
assign input_of_inout = inout_pin;
assign inout_pin = out_en ? output_of_inout : 高阻;
endmodule可见,此时input_of_inout和output_of_inout就可以当作普通信号使用了。
在仿真的时候,需要注意双向口的处理。如果是直接与另外一个模块的双向口连接,那么只要保证一个模块在输出的时候,另外一个模块没有输出(处于高阻态)就可以了。
如果是在ModelSim中作为单独的模块仿真,那么在模块输出的时候,不能使用force命令将其设为高阻态,而是使用release命令将总线释放掉
很多初学者在写testbench进行仿真和验证的时候,被inout双向口难住了。仿真器老是提示错误不能进行。下面是我个人对inout端口写 testbench仿真的一些总结,并举例进行说明。在这里先要说明一下inout口在testbench中要定义为wire型变量。
先假设有一源代码为:
module xx(data_inout , ........);
inout data_inout;
........................
assign data_inout=(! link)?datareg:1'bz;
endmodule
方法一:使用相反控制信号inout口,等于两个模块之间用inout双向口互连。这种方法要注意assign 语句只能放在initial和always块内。
module test();
wire data_inout;
reg data_reg;
reg link;
initial begin
..........
end
assign data_inout=link?data_reg:1'bz;
endmodule
方法二:使用force和release语句,但这种方法不能准确反映双向端口的信号变化,但这种方法可以反在块内。
module test();
wire data_inout;
reg data_reg;
reg link;
#xx; //延时
force data_inout=1'bx; //强制作为输入端口
...............
#xx;
release data_inout; //释放输入端口
endmodule
很多读者反映仿真双向端口的时候遇到困难,这里介绍一下双向端口的仿真方法。一个典型的双向端口如图1所示。
其中inner_port与芯片内部其他逻辑相连,outer_port为芯片外部管脚,out_en用于控制双向端口的方向,out_en为1时,端口为输出方向,out_en为0时,端口为输入方向。
用Verilog语言描述如下:
module bidirection_io(inner_port,out_en,outer_port);
input out_en;
inout[7:0] inner_port;
inout[7:0] outer_port;
assign outer_port=(out_en==1)?inner_port:8'hzz;
assign inner_port=(out_en==0)?outer_port:8'hzz;
endmodule用VHDL语言描述双向端口如下:
library ieee;
use IEEE.STD_LOGIC_1164.ALL;
entity bidirection_io is
port ( inner_port : inout std_logic_vector(7 downto 0);
out_en : in std_logic;
outer_port : inout std_logic_vector(7 downto 0) );
end bidirection_io;
architecture behavioral of bidirection_io is
begin
outer_port<=inner_port when out_en='1' else (OTHERS=>'Z');
inner_port<=outer_port when out_en='0' else (OTHERS=>'Z');
end behavioral;仿真时需要验证双向端口能正确输出数据,以及正确读入数据,因此需要驱动out_en端口,当out_en端口为1时,testbench驱动 inner_port端口,然后检查outer_port端口输出的数据是否正确;当out_en端口为0时,testbench驱动 outer_port端口,然后检查inner_port端口读入的数据是否正确。由于inner_port和outer_port端口都是双向端口(在 VHDL和Verilog语言中都用inout定义),因此驱动方法与单向端口有所不同。
验证该双向端口的testbench结构如图2所示。
这是一个self-checking testbench,可以自动检查仿真结果是否正确,并在Modelsim控制台上打印出提示信息。图中Monitor完成信号采样、结果自动比较的功能。
testbench的工作过程为
1)out_en=1时,双向端口处于输出状态,testbench给inner_port_tb_reg信号赋值,然后读取outer_port_tb_wire的值,如果两者一致,双向端口工作正常。
2)out_en=0时,双向端口处于输如状态,testbench给outer_port_tb_reg信号赋值,然后读取inner_port_tb_wire的值,如果两者一致,双向端口工作正常。
用Verilog代码编写的testbench如下,其中使用了自动结果比较,随机化激励产生等技术。
`timescale 1ns/10ps
module tb();
reg[7:0] inner_port_tb_reg;
wire[7:0] inner_port_tb_wire;
reg[7:0] outer_port_tb_reg;
wire[7:0] outer_port_tb_wire;
reg out_en_tb;
integer i;
initial begin
out_en_tb=0;
inner_port_tb_reg=0;
outer_port_tb_reg=0;
i=0;
repeat(20)
begin
#50
i=$random;
out_en_tb=i[0]; //randomize out_en_tb
inner_port_tb_reg=$random; //randomize data
outer_port_tb_reg=$random;
end
end
//**** drive the ports connecting to bidirction_io
assign inner_port_tb_wire=(out_en_tb==1)?inner_port_tb_reg:8'hzz;
assign outer_port_tb_wire=(out_en_tb==0)?outer_port_tb_reg:8'hzz;
//instatiate the bidirction_io module
bidirection_io bidirection_io_inst(.inner_port(inner_port_tb_wire),
.out_en(out_en_tb),
.outer_port(outer_port_tb_wire));
//***** monitor ******
[email protected](out_en_tb,inner_port_tb_wire,outer_port_tb_wire) begin
#1;
if(outer_port_tb_wire===inner_port_tb_wire)
begin
$display("\n **** time=%t ****",$time);
$display("OK! out_en=%d",out_en_tb);
$display("OK! outer_port_tb_wire=%d,inner_port_tb_wire=%d",
outer_port_tb_wire,inner_port_tb_wire);
end
else begin
$display("\n **** time=%t ****",$time);
$display("ERROR! out_en=%d",out_en_tb);
$display("ERROR! outer_port_tb_wire != inner_port_tb_wire" );
$display("ERROR! outer_port_tb_wire=%d, inner_port_tb_wire=%d",
outer_port_tb_wire,inner_port_tb_wire);
end
end
endmodule
边栏推荐
- Pytorch最全安装教程(一步到位)
- 分库分表之sharding-proxy
- flaks框架学习:在 URL 中添加变量
- 【Redis】Redis 的安装及图形化界面 Redis DeskTop Manager 的安装与使用
- Weekly recommended short video: your commonly used Polaroid, its predecessor turned out to be like this!
- 金仓数据库 KingbaseGIS 使用手册(6.10. 几何对象操作运算符)
- 基于TF-IDF 文本相似性实战 详细教程
- Thymeleaf
- 阿里天池学习赛 新闻文本分类
- tensorflow代码翻译成pytorch代码 -详细教程+案例
猜你喜欢
随机推荐
基于 TF-IDF 文本匹配实战详细教程 数据+代码 可直接运行
Mysql入门练习
(1) Docker installs Redis in practice (one master, two slaves, three sentinels)
Golden Warehouse Database KingbaseGIS User Manual (6.10. Geometric Object Operation Operator)
【Redis】Redis 的安装及图形化界面 Redis DeskTop Manager 的安装与使用
博客目录管理 :机器学习 深度学习 nlp
CentOS卸载Oracle 11gR2(x64)_转载
元宇宙社交应用,靠什么吸引用户「为爱发电」?
代理模式(简要介绍)
PCIe 接口 引脚定义 一览表
Flask框架学习:模板渲染与Get,Post请求
搭建PX4开发环境
imx6 yocto编译备忘
Trilium使用总结
Redis-数据类型(基本指令、String、List、Set、Hash、ZSet、BitMaps、HyperLogLog、GeoSpatial)/发布和订阅
总结:交叉验证
Idea 2021.3.3版本文件目录展开
Delphi7学习记录-demo实例
MySQL必知必会(初级篇)
Day38 LeetCode









