【转】编写Testbench的一些技巧

编写Testbench的一些技巧
【转】编写Testbench的一些技巧 - ZEDIS - ZEDIS HABITAT(2009-06-03 08:26:41) 
【转】编写Testbench的一些技巧 - ZEDIS - ZEDIS HABITAT转载


Testbench的结构

  1) 单顶层结构

  一种结构是testbench 只有一个顶层,顶层再把所有的模块实例化进去。打个比方,类似树结构,只有一个模块有子节点而没有父节点,其它模块都有父节点。如下图结构所示:

单顶层结构

  测试模块是一些接口模型,接口模型还可能包含了一些激励在内。测试模块和DUV之间通过端口映射进行互连。

  2) 多顶层结构

  另外一种结构是多顶层结构,如下图所示:

多顶层结构

  在这种结构中,有一个顶层是作为测试向量模块,一个或多个顶层是一些公用子程序,这些子程序由于完成一些通用的功能被封装成任务、函数等被公用。

  还有一个叫harness的顶层,该顶层由DUV和一些接口模型构成一个狭义上的测试平台,其它模块可以调用BFM里面的 task 或 event 等,向DUV施加激励。注意这些顶层之间是没有端口映射的,它们之间的互相调用和访问是通过层次路径名的方式来访问,上图的虚线表示层次路径名的访问。下面举例说明层次路径是如何访问的。

  由于大部分人对C都有所认识,在这里作个比较,便于了解。Verilog HDL的顶层类似于C的结构体,而实例化的模块、任务、函数、变量等就是结构体里的成员,可以通过句点( . )隔开的方式访问结构体里面的每一个成员。如:顶层 harness 实例化进来的模块 BFM1 里面有一个任务SEND_DATA , 该任务可以产生激励输入到DUV,在 testcase 里调用该任务就可写为:

initial
begin
  ……
  harness . BFM1 . SEND_DATA ( …… ) ;
end

  多顶层结构的可扩展和重用性比单顶层结构强得多。层次路径的访问方式非常有用,在下一节会讲述更多的应用。

  2 如何编写Testbench

  1) 何时使用initial和always

  initial和always 是2个基本的过程结构语句,在仿真的一开始即开始相互并行执行。通常被动的检测响应使用always语句,而主动的产生激励使用initial语句。

  initial和always的区别是always 语句不断地重复执行,initial语句则只执行一次。但是,如果希望在initial里的多次运行一个语句块,怎么办?这时可以在initial里嵌入循环语句(while,repeat,for,forever 等),如:

initial
begin
forever
begin
  ……
  end
end

  其它循环语句请参考一些教材,这里不作赘述。

  另外,如果希望在仿真的某一时刻同时启动多个任务,可以使用fork....join语句。例如,在仿真开始的 100 ns 后,希望同时启动发送和接收任务,而不是发送完毕后再进行接收,如下所示:

initial
begin
  #100 ;
  fork
Send_task ;
Receive_task ;
  join
End

  2) 如何作多种工作模式的遍历测试

  如果设计的工作模式很多,免不了做各种模式的遍历测试,而遍历测试是需要非常大的工作量的。我们经常遇到这样的情况:很多时候,各种模式之间仅仅是部分寄存器配置值的不同,而各模式间的测试都是雷同的。有什么方法可以减轻这种遍历测试的工作量?不妨试试for循环语句,采用循环变量来传递各种模式的配置值,会帮助减少很多测试代码,而且不会漏掉每一种模式.

initial
begin
  for ( i = 0 ; i < m ; i = i + 1 )
for ( j = 0 ; j < n ; j = j +1 )
  0 : 配置值 = a ;
  1 : 配置值 = b ;
  2 : 配置值 = c ;
  ……
  endcase
end
end

  3) 如何加速问题定位过程

  在这部分里,通过一些实际例子,介绍在出现问题时如何借助 testbench 加快问题的定位过程。

  1、监测内存分配
 
  内存分配和回收示意图

监测内存分配

 在这个例子里,假设总共有2K的内存块,希望在测试程序里监测内存分配和回收的块号是否正确,监测是否存在同一块号重复分配、重复回收的情况。设置一个 2K位的变量对内存的使用情况进行记录,每一位对应一个内存块,空闲的块号记为1,被占用的块号记为0。该变量的初始值为全1,当分配一个块号出去时先判断该位是否为空闲,若是空闲则将该位设置为被占用,否则就为重复分配错误。相反,当回收一个块号时,先判断该位是否被占用,若是被占用则将该位设置为空闲,否则就为重复回收错误。程序如下:

 

always @(posedge Clk or negedge Rst )
begin
  if ( Rst == 1'b0 )
Mem_status <= 2048 {1'b1} ;
  else
  begin
if ( 层次路径 . rd )
  if ( Mem_status [ block_rd ] == 1'b1 )
Mem_status [ block_rd ] <= 1'b0 ;
  else
  begin
$display ( "Error! 重复分配同一内存块! ") ;
$stop ;
  end
if ( 层次路径 . wr )
  if ( Mem_status [ block_wr ] == 1'b0 )
Mem_status [ block_wr ] <= 1'b1 ;
  else
  begin
  $display ( "Error! 重复回收同一内存块! ") ;
  $stop ;
  end
end
End

  2、监测内部接口

  如果你是位验证工程师,在做芯片级的仿真时,相信你会或曾遇到过这样的问题:在一个端口输入了激励数据,但另一端口却得不到正确的响应,而且这条路径涉及到很多模块和很多个不同设计者,为了定位问题,你可能很盲目地逐个找来设计人员,逐个模块地记录仿真波形,到解决问题时,可能几天已经过去了。

  我们都知道,如果问题定位在越小的范围,就越便于解决问题。所以,我们可以把模块接口间交换的数据记录到文件里,当出现问题时,就可以查看各接口的记录数据,看问题到底出现在哪个区间,简单地查看记录文件后,你就明确该找那位designer来解决问题。

  3、记录有用的DEBUG信息

  记录有用的debug信息,输出到标准的I/O设备上(屏幕或文件),会给你的debug带来很大的便利,由上面的例子也可见一斑,在检测到有错误时也可使用$stop令仿真停下来。

  值得注意的是,UNIX系统只有32个I/O,每个输出文件占用1个I/O设备号,其中第1个是屏幕显示,设备号是32'b1,其它I/O设备号由输出文件占用,一个信息可同时输出到屏幕和文件,如:

initial
begin
  Ptr_log = $fopen ("log.txt ") ;
  Ptr_log = Ptr_log | 32'b1 ;
end
always @(……)
begin
  $fwrite ( Ptr_log, "useul message ",……) ;
  ……
end

  虽然记录文件会给debug带来很多便利,但文件操作会降低仿真的速度,因此应当适可而止。

  另外写文件通常有2种方式,不同的仿真工具有所差异。一种是每写一个字节打开关闭一次文件,如Verilog-XL。另一种是先把字符暂存到内存,等累积到一定数量(如8K字节)后再通过DMA方式把字符从内存写到文件,如Verilog-NC。因此,后一种方式就大大地降低了文件的操作次数,有利于提高仿真速度。

  3 编写Testbench的一些高级技巧

  Verilog HDL提供很多方便和高效的建模语句,这在大多数参考书上都有介绍,在这节,只介绍一些参考教材很少介绍而较有用的建模语句。

  1) force 和 release

  望文生义,force即是可以对变量和信号强制性地赋予确定的值,而release就是解除force的作用,恢复为驱动源的值。例如:

wire a ;
assign a = 1'b0 ;
initial
begin
  #10 ;
  force a = 1'b1 ;
  #10
  release a ;
end

在10 ns时,a 的值由0变为1,在20ns时,a 的值又恢复为0 。

  force 和release并不常用,有时,可以利用它们和仿真工具做简单的交互操作。例如,Verilog-XL的图形界面可以方便的将一个信号或变量force 为0或1,在 Testbench 里,可以检测变量是否被force为固定的值,当被force为固定的值时就执行预定的操作,实现了简单交互操作。

 

  2) 事件

  事件有些类似于任务。首先需要定义一个事件,而事件可以作为敏感变量激活一个语句块的操作,事件可由“->”符号进行触发,如下例:

event e1 ;
always @( e1 )
begin
  .....
end
initial
begin
  —> e1 ;
  .....
end

  事件(event )与任务(task)的区别是:执行事件触发后可以立即继续往下执行语句,只起一个触发作用,至于被触发的事件何时执行完毕并不影响程序继续执行。而调用一个任务后,必须等待任务完成才能返回控制权。

  3) 模块参数

  当一个模块引用另外一个模块时,高层模块可以改变低层模块用parameter定义的参数值,改变低层模块的参数值可采用以下两种方式:

  1)defparam 重定义参数

  语法:defparam path_name = value ;

  低层模块的参数可以通过层次路径名重新定义,如下例:

module top ( .....)
input....;
output....;
defparam U1 . Para1 = 10 ;
M1 U1 (..........);
endmodule
module M1(....);
parameter para1 = 5 ;
input...;
output...;
......
endmodule

  在上例中,模块M1参数 para1 的缺省值为5,而模块top实例了M1后将参数的值改为10。

  2) 实例化时传递参数

  在这种方法中,实例化时把参数传递进去,如下例所示:

module top ( .....)
input....;
output....;
M1 #( 10 ) U1 (..........);
endmodule

  在该例中,用#( 10 )修改了上例中的参数para1,当有多个参数时,用逗号隔开,如#( 10 , 5 ,

  3 )传递了3个参数值。

  模块参数的方法使得模块的重用性更强,当需要在同一个设计中多次实例化同样的模块,只是参数值不同时,就可以采用模块参数的方式,而不必只因为参数不同产生了多个文件。

  4) 其他要注意的几个点

  4) 注意@与wait的区别

  @都是使用沿触发。

  wait语句都是使用电平触发。

  5) 注意$sreadmemb(h)与$readmemb(h)的区别

  $sreadmemb(Memory, StartAddr, FinishAddr, String , ……) :读字符串到Memory。
$readmemb("File", Memory [, StartAddr, [FinishAddr]]) :读取的第一个数字存储在地址
StartAddr,直到FinishAddr。

  6) 常用系统任务

$time 返回64位整型时间 。
$stime 返回32位整型时间 。
$realtime 向调用它的模块返回实型 模拟 时间。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值