Verilog中testbench的设计,文件读取和写入操作

1. 激励的产生

对于 testbench 而言,端口应当和被测试的 module 一一对应。
端口分为 input,output 和 inout 类型产生激励信号的时候,
input 对应的端口应当申明为 reg,
output 对应的端口申明为 wire,
inout 端口比较特殊,下面专门讲解。
 

1)直接赋值


一般用 initial 块给信号赋初值,initial 块执行一次,always 或者 forever 表示由事件激发反复执行。
举例,一个 module

[plain] view plain copy

`timescale 1ns/1ps 

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 里面很方
便,大家可以自行查阅参考文档

2) 文件输入

有时候,需要大量的数据输入,直接赋值的话比较繁琐,可以先生成数据,再将数据读入到寄存器中,
需要时取出即可。
用 $readmemb 系统任务从文本文件中读取二进制向量(可以包含输入激励和输出期望值)。
$readmemh 用于读取十六进制文件。例如:



reg [7:0] mem[256:1] // a 8-bit, 256-word 定义存储器 mem 
initial $readmemh ( "E:/readhex/mem.dat", mem ) // 将.dat 文件读入寄存器 mem 中 
initial $readmemh ( "E:/readhex/mem.dat", mem, 128, 1 ) // 参数为寄存器加载数据的地址始终 


文件调入和打印中,我们 $fread $fwrite 用的更多一些, 大家可以自行查阅参考文档

 

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 文件而言,也是类似的
[plain] view plain copy

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 
always@(a or b) 
begin 
$fwrite(file_id,"At time%t a=%b b=%b",$realtime,a,b); 
end 




3 参考“A Verilog HDL Test Bench Primier.pdf”
1) DUT(Design Under Test)部分



//------------------------------------------------- 
// File: count16.v 
// Purpose: Verilog Simulation Example 
//------------------------------------------------- 
`timescale 1 ns / 100 ps 
module count16 ( 
clk, 
rst_n, 
load_l, 
enable_l, 
cnt_in, 
oe_l, 

count, 
count_tri 
); 

input clk; 
input rst_n; 

input load_l; 
input enable_l; 
input [3:0] cnt_in; 
input oe_l; 

output [3:0] count; 
output [3:0] count_tri; 

reg [3:0] count; 

// tri-state buffers 
assign count_tri = (!oe_l) ? count : 4'bZZZZ; 


// synchronous 4 bit counter 

always @ (posedge clk or negedge rst_n) 
if (!rst_n) begin 
count <= 4'd0; 
end 
else begin 
if (!load_l) begin 
count <= cnt_in; 
end 
else if (!enable_l) begin 
count <= count + 1; 
end 
end 

endmodule //of count16 


2) Test Bench 



//------------------------------------------------- 
// File: tb.v 
// Purpose: Verilog Simulation Example 
// Test Bench 
//----------------------------------------------------------- 
`timescale 1ns / 100ps 
module tb (); 

//--------------------------------------------------------- 
// inputs to the DUT are reg type 
reg clk_50; 
reg rst_n; 
reg load_l; 
reg enable_l; 

reg [3:0] count_in; 
reg oe_l; 

//-------------------------------------------------------- 
// outputs from the DUT are wire type 
wire [3:0] cnt_out; 
wire [3:0] count_tri; 


//---------------------------------------------------------- 
// create a 50Mhz clock 
always #10 clk_50 = ~clk_50; // every ten nanoseconds invert 


//----------------------------------------------------------- 
// initial blocks are sequential and start at time 0 
initial 
begin 
$display($time, " << Starting the Simulation >>"); 
clk_50 = 1'd0; 

// at time 0 
rst_n = 0; 

// reset is active 
enable_l = 1'd1; 

// disabled 
load_l = 1'd1; 

// disabled 
count_in = 4'h0; 
oe_l = 4'b0; 

// enabled 
#20 rst_n = 1'd1; 

// at time 20 release reset 
$display($time, " << Coming out of reset >>"); 

@(negedge clk_50); // wait till the negedge of 
// clk_50 then continue 
load_count(4'hA); 

// call the load_count task 

@(negedge clk_50); 
$display($time, " << Turning ON the count enable >>"); 

enable_l = 1'b0; 
// turn ON enable 
// let the simulation run, 
// the counter should roll 
wait (cnt_out == 4'b0001); // wait until the count 

// equals 1 then continue 
$display($time, " << count = %d - Turning OFF the count enable >>",cnt_out); 
enable_l = 1'b1; 
#40; 

// let the simulation run for 40ns 
// the counter shouldn't count 
$display($time, " << Turning OFF the OE >>"); 
oe_l = 1'b1; 


// disable OE, the outputs of 
// count_tri should go high Z. 
#20; 
$display($time, " << Simulation Complete >>"); 
$stop; 

// stop the simulation 
end 


//-------------------------------------------------------------- 
// This initial block runs concurrently with the other 
// blocks in the design and starts at time 0 

initial begin 
// $monitor will print whenever a signal changes 
// in the design 
$monitor( 
$time, 
" clk_50=%b, rst_n=%b, enable_l=%b, load_l=%b, count_in=%h, cnt_out=%h, oe_l=%b, count_tri=%h",
clk_50, rst_n, enable_l, load_l, count_in, cnt_out, oe_l, count_tri 
); 
end 


//-------------------------------------------------------------- 
// The load_count task loads the counter with the value passed 
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 



//--------------------------------------------------------- 
// instantiate the Device Under Test (DUT) 
// using named instantiation 
count16 count16_m0 ( 
.clk (clk_50), 
.rst_n (rst_n), 
.load_l (load_l), 
.cnt_in (count_in), 
.enable_l (enable_l), 
.oe_l (oe_l), 

.count (cnt_out), 
.count_tri (count_tri) 
); 



//--------------------------------------------------------- 
// read and write data 
reg [7:0] mem[10:1];//read data from file 

initial $readmemh ("F:/IC/prj/testbench/prj0/data/mem.dat", mem ); // 将.dat 文件读入寄存器 mem 中


//设计中的信号值可以通过$fmonitor, $fdisplay,$fwrite 
//其中$fmonitor 只要有变化就一直记录, $fdisplay 和$fwrite 需要触发条件才记录 

integer file_out; // out_file 是一个文件描述,需要定义为 integer 类型 
initial file_out = $fopen("F:/IC/prj/testbench/prj0/data/wr_mem.dat", "w"); 

// wr_mem.dat 是需要打开的文件,也就是最终的输出文本 

always @(posedge clk_50) 
if (/*tb.count16_m0.*/enable_l == 1'd0) begin 
$fwrite (file_out, "%h\n", cnt_out[3:0]); 
// $fdisplay(file_out, "%h", cnt_out[3:0]); 
end 


endmodule //of cnt16_tb 


3) sim.do文件



#Time: 2016-07-26 
#By : times_poem 

quit -sim 

cd F:/IC/prj/testbench/prj0 

if [file exists work] { 
vdel -all 


vlib work 

vlog ./*.v 
vlog ./src/*.v 

vsim -t ps -novopt work.tb 

log -r /* 
do wave.do 
 

run -all 

3 testbench 的技巧



1)如果激励中有一些重复的项目,可以考虑将这些语句编写成一个 task,这样会给书写和仿真带来很大方便。例如,
一个存储器的 testbench 的激励可以包含 write, read 等 task。

2)如果 DUT 中包含双向信号(inout),在编写 testbench 时要注意。需要一个 reg 变量来表示其输入,还需要一个 wire
变量表示其输出。

3)如果 initial 块语句过于复杂,可以考虑将其分为互补相干的几个部分,用数个 initial 块来描述。在仿真时,这些
initial 块会并发运行。这样方便阅读和修改。

4)每个 testbench 都最好包含$stop 语句,用以指明仿真何时结束。

5)加载测试向量时,避免在时钟的上下沿变化,比如数据最好在时钟上升沿之前变化,这也符合建立时间的要求。
4 一个简单的例子
DUT:  测试对象DUT(Device Under Test),dut: device under test.这个只是表示你要调用单元的例化名而已,此名字可以改成任何verilog可识别的字符。



module counter ( 
clk, 
reset, 
enable, 
count 
); 

input clk; 
input reset; 
input enable; 

output [3:0] count; 
reg [3:0] count; 

always @ (posedge clk) 
if (reset == 1'b1) begin 
count <= 0; 
end else if ( enable == 1'b1) begin 
count <= count + 1; 
end 
endmodule 


testbench:


module counter_tb; 
reg clk; 
reg reset; 
reg 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 

4. 高级用法

比如 PLI 之类。有需要的,大家再讨论
总体感觉, testbench 是个很难的事情, 这里讨论的只是一些最基本的东西。 真正有技术含量的是 testcase 的设计,设计
阶段合理层次设计以及模块划分等等,我没有做过很大的项目,所以这方面也没有办法提供更多的帮助。经验丰富的
大牛不妨出来讲讲经验, ^_^

testbench 用的较多的东西是:
1、输入数据文件的产生,一般由 matlab 产生,这方面经常涉及浮点到定点的转换、进制的转换的问题;
2、输入数据文件的输出文件的设置;
3、 VCD、 fsdb 等和其他 eda 软件接口的文件的输出;
4、一定范围内的随机数的产生
5、双向端口的仿真
6、与上层 dsp 等 cpu 接口的时序仿真。
....

panwest: testbench 设计的掌握的技术可多可少,模块级的 HDL 就可以搞定了:
激励产生:高级语言实现( C, C++, Java 等)
DUT: HDL 实现
参考单元:高级语言实现( C, C++, Java 等)
初始化:脚本语言( perl, Tcl/TK)
波形输出:一般脚本自动对比了
数据通过 PLI 进行联系

日后 systemVerilog, e 等语言完善后会减轻一些建立平台的工作量。

synopsys vera support verification.
bench : vera produce

DUT:

ref module: vera
monitor: vera
coverage analysis : VCS or vera
vera can load c, C++ file .

systemverilog 应该是以后的趋势
一些 random scenario 与 tlm base model 都可以用systemverilog 完成
而一个好的 framework 很重要现在有两各 framework 可以选择vmm, ovm
不过要用上述的 framework 需要很大的 oop 基础要花很长时间学习
关于 systemC 或者 system verilog,大家可以查找相关文档。

猜你喜欢

转载自blog.csdn.net/kebu12345678/article/details/86559687
今日推荐