《SystemVerilog验证-测试平台编写指南》学习 - 第3章 过程语句和子程序


  做设计验证的大部分代码在任务和函数里。SystemVerilog增加了许多改进使它更接近C语言,从而使代码编写更容易。

3.1 过程语句

  SystemVerilog从C和C++引入了很多操作符和语句。你可以在 for 循环中定义循环变量,它的作用范围仅限于循环内部,从而有助于避免一些代码漏洞。自动递增符”++“和自动递减符”--“既可以作为前缀也可以作为后缀。如果在begin和fork语句中使用标识符,那么在相对应的end和join语句中可以放置相同的符号,这使得程序块的收尾匹配更加容易。你也可以把标识符放在SystemVerilog的其他结束语句里,例如 endmodule、endtask、endfunction 以及其他语句。如下例所示:

// 例3.1  新的过程语句和操作符
initial
begin :example
integer array [10], sum, j ; // 在 for 语句中声明 i
for (int i = 0; i < 10; i++) // 定义i,i 递增
array [i] = i ; // 把数组里的元素相加
sum = array [9] ;
j = 8 ;
do // do ... while 循环
sum += array[j] ; // 累加
while (j--) ; // 判断 j=0 是否成立
$display ("Sum = %4d", sum) ; // %4d 指定宽度
end : example

  SystemVerilog为循环功能增加了两个新语句。

循环功能新增语句 功能
continue 用于在循环中跳过本轮循环剩下的语句而直接进入下一轮循环
break 用于终止并跳出循环
//例3.2  在读取文件时使用break和continue
initial begin
bit [127:0] cmd ;
int file , c ; file = $fopen ("commands.txt", "r") ;
while (!$feof(file)) begin // $feof() 当读到文件末尾时(eof)为非0,否则为0
c = $fscanf (file, "%s", cmd) ; // $fscanf() 一行一行读取,成功返回 1
case (cmd)
"" : continue ; // 空行 - 跳过本轮循环
"done" : break ; // Done - 终止并跳出循环
...
endcase // case (cmd)
end
$fclose (file) ;
end

3.2 任务、函数以及void函数

  在Verilog中,任务task和函数function之间有很明显的区别,其中最重要的是:

  1. 任务可以消耗时间而函数不能。函数里面不能带有诸如 #100 的时延语句或诸如 @(posedge clock) 、wait(ready) 的阻塞语句。
  2. 函数不能调用任务
  3. 另外,Verilog中的函数必须有返回值,并且返回值必须被使用,例如用到赋值语句中。

  SystemVerilog对这条限制稍有放宽,允许函数调用任务,但只能在由 fork...join_none 语句生成的线程中调用

注:如果你有一个不消耗时间的SystemVerilog任务,你应该把它定义成void函数,这种函数没有返回值。这样它就能被任何任务或函数所调用了。从最大灵活性的角度考虑,所有用于调试的子程序都应该定义成void函数而非任务,以便以被任何其他任务或函数所调用。如下例所示:

// 例3.3  用于调试的void函数
function void print_state (...) ;
$display ("@%0t: state = %s", $time, cur_state.name()) ;
endfunction

  在SystemVerilog中,如果你想调用函数并且忽略它的返回值,可以使用 void 进行结果转换,如下例所示。有些仿真器,如VCS,允许你在不使用上述void语法的情况下忽略返回值。

// 例3.4  忽略函数的返回值
void '($fscanf (file, "%d", i)) ;

3.3 任务和函数概述

  一般情况下,不带参数的子程序在定义或调用时并不需要带括号()。

  在SystemVerilog中,begin...end块变成可选的了,而在Verilog-1995中则对单行以外的子程序都是必须的。如下例所示,task/endtask和function/endfunction的关键词已经足以定义这些子程序的边界了。

// 例3.5 不带 begin...end 的简单任务
task multiple_lines ;
$display ("First line") ;
$display ("Second line") ;
endtask : multiple_lines

3.4 子程序参数

  SystemVerilog对子程序的很多改进使参数声明变得更加方便,同时也扩展了参数的传递方式。

3.4.1 C语言风格的子程序参数

// 例3.6 Verilog-1995的子程序参数
task mytask2 ;
output [31:0] x ;
reg [31:0] x ;
input y ;
...
endtask // 例3.7 C语言风格的子程序参数
task mytask1 (output logic [31:0] x,
input logic y) ;
...
endtask

3.4.2 参数的方向

  在子程序参数方面还可以有更多的便捷。因为缺省的类型和方向是“logic 输入”,所以在声明类似参数时可不必重复。下例采用SystemVerilog的数据类型但以Verilog-1995的风格编写的一个子程序头。

// 例3.8 带Verilog风格的繁冗的子程序参数
task T3
input a, b ;
logic a, b ;
output [15:0] u, v ;
bit [15:0] u, v ;
...
endtask // 例3.9 带缺省类型的子程序参数
task T3 (a, b, output bit [15:0] u, v) ;

  尽管这种间接的编程方式,但是不建议使用这种方式,这种方式可能会使代码滋生一些细小而难以发现的漏洞。所以建议对所有子程序的声明都带上类型和方向。

3.4.3 高级的参数类型

  Verilog对参数的处理方式很简单:

在子程序的开头把input和inout的值复制给本地变量,在子程序退出时则复制output和inout的值。除了标量以外,没有任何把存储器传递给Verilog子程序的办法。

  在SystemVerilog中,参数的传递方式可以指定为引用而不是复制。这种 ref 参数类型比input、output或inout更好用。首先,你现在可以吧数组传递给子程序。

// 例3.10  使用ref和const传递数组
function void print_checksum (const ref bit [31:0] a []) ;
bit [31:0] checksum = 0 ;
for (int i = 0; i < a.size(); i++)
checksum ^= a[i] ;
$display ("The array checksum is %0d", checksum) ;
endfunction

  SystemVerilog允许不带ref进行数组参数的传递,这时数组会被复制到堆栈区里。这种操作的代价很高,除非是对特别小的数组。

  SystemVerilog的语言参考手册(LRM)规定了ref参数只能被用于带自动存储的子程序中。如果你对子程序或模块指明了automatic属性,则整个子程序内部都是自动存储的。

  上例中使用了const修饰符,虽然数组变量 a 指向了调用程序中的数组,但子程序不能修改数组的值。如果你试图改变数组的值,编译器将报错。向子程序传递参数时应尽量使用ref以获取最佳性能。如果你不希望子程序改变数组的值,可以使用 const ref 类型。这种情况下,编译器会进行检查以确保数组不被子程序修改。

  ref参数的第二个好处是在任务里可以修改变量而且修改结果对调用它的函数随时可见。

// 例3.11 在多线程间使用 ref
task bus_read (input logic [31:0] addr,
ref logic [31:0] data) ;
// 请求总线并驱动地址
bus.request = 1'b1 ;
@(posedge bus.grant) bus.addr = addr ; // 等待来自存储器的数据
@(posedge bus.enable) data = bus.data ; // 释放总线并等待许可
bus.request = 1'b0 ;
@(posedge bus.grant) ;
endtask logic [31:0] addr, data ; initial
fork
bus_read (addr, data) ;
thread2 : begin
@data ; // 只要数据变化时即可触发
$display ("Read %h from bus", data) ;
end
join

3.4.4 参数的缺省值

  当测试程序越来越复杂时,你可能希望在不破坏原有代码的情况下增加额外的控制。在SystemVerilog中,可以为参数指定一个缺省值,如果在调用时不指名参数,则使用缺省值。

// 例3.12 带缺省值的函数
function void print_checksum (ref bit [31:0] a [] ,
input bit [31:0] low = 0,
input int high = -1) ;
bit [31:0] checksum = 0 ;
if (high == -1 || high >= a.size())
high = a.size() - 1 ;
for (int i = low; i <= high; i++)
checksum += a[i] ;
$display ("The array checksum is %0d", checksum) ;
endfunction // 例3.13 使用参数的缺省值
print_checksum (a) ; // a[0:size()-1] 中所有元素的校验和 -- 缺省情况
print_checksum (a, 2, 4) ; // a[2:4]中所有元素的校验和
print_checksum (a, 1) ; // 从1开始
print_checksum (a,,2) ; // a[0:2]中所有元素的校验和
print_checksum () ; // 编译错误,a没有缺省值

  使用-1(或其它任何越界值)作为缺省值,对于获知调用时是否有指定值,不失为一种好方法。

3.4.5 采用名字进行参数传递

  在SystemVerilog的语言参数手册(LRM)中,任务或函数的参数有时也被称为端口“port”,就跟模块的接口一样。所以可以采用端口名关联法进行参数传递。

// 例3.14 采用名字进行参数传递
task many (input int a = 1, b = 2, c = 3, d = 4) ;
$display ("%0d %0d %0d %0d", a, b, c, d) ;
endtask initial begin
many (6, 7, 8, 9) ; // 6 7 8 9 指定所有值
many () ; // 1 2 3 4 使用缺省值
many (.c(5)) ; // 1 2 5 4 只指定c,其它使用缺省值
many (, 6, .d(8)) ; // 1 6 3 8 混合方式
end

3.4.6 常见代码错误

  在编写代码时最容易犯的错误就是,你往往会忘记,在缺省的情况下参数的类型是与其前一个参数相同的,而第一个参数的缺省类型是单比特输入

// 例3.15 原始的任务头
task sticky (int a, b) ; // 例3.16 加入额外数组参数的任务头
task sticky (ref int array[50] ,
int a, b) ; // 这些变量的方向是什么?

  在例3.16中,a和b的参数类型是什么呢?它们在方向上实际采用的是与前一个参数一致的 ref 类型。对简单的int变量使用ref通常并无必要,但编译器不会对此作出任何反应,连警告都没有,所以你不会意识到正在使用一个错误的方向类型。

  如果在子程序中使用了非缺省输入类型的参数,应该明确指明所有参数的方向,如下所示:

// 例3.17 加入额外数组参数的任务头
task sticky (ref int array[50] ,
input int a, b) ; // 明确指定方向

3.5 子程序的返回

  Verilog中子程序的结束方式比较简单:当你执行完子程序的最后一条语句,程序就会返回到调用子程序的代码上。此外函数还会返回一个值,该值赋给与函数同名的变量。

3.5.1 返回(return)语句

  SystemVerilog增加了return语句,使子程序中的流程控制变得更方便。下例的任务由于发现错误而需要提前返回。如果不这样做,那么任务中剩下的部分就必须放到一个else条件语句中,从而使得代码变得不规整,可读性也降低了。

// 例3.18 在任务中用return返回
task load_array (int len, ref int array []) ;
if (len <= 0) begin
$display ("Bad len") ;
return ;
end // 任务中其余的代码
...
endtask // 例3.19 在函数中使用return返回
function bit transmit (...) ;
// 发送处理
...
return ~ifc.cb.error ; // 返回状态:0=error
endfunction

3.5.2 从函数中返回一个数组

  Verilog的子程序只能返回一个简单值,例如比特、整数或是向量。如果你想计算并返回一个数组,那就不是一件容易的事情了。在SystemVerilog中,函数可以采用多种方式返回一个数组。

  第一种方式是定义一个数组类型,然后在函数的声明中使用该类型。

// 例3.20 使用typedef从函数中返回一个数组
typedef int fixed_array5 [5] ;
fixed_array5 f5 ; function fixed_array5 init (int start) ;
foreach (init[i])
init[i] = i + start ;
endfunction initial begin
f5 = init (5)
foreach (f5[i])
$display ("f5[%0d] = %0d", i, f5[i]) ;
end

  上述代码的一个问题是,函数init创建一个数组,该数组的值被拷贝到数组f5中。如果数组很大,可能会引起性能上的问题。

  另一种方式是通过引用来进行数组参数的传递

// 例3.21 把数组作为ref参数传递给函数
function void init (ref int f [5], input int start) ;
foreach (f[i])
f[i] = i + start ;
endfunction int fa [5] ;
initial begin
init (fa, 5) ;
foreach (fa[i])
$display ("fa[%0d] = %0d", i, fa[i]) ;
end

  从函数返回数组的最后一种方式是将数组包装到一个类中,然后返回对象的句柄

3.6 局部数据存储

3.6.1 自动存储

  在Verilog-1995里,如果你试图在测试程序里的多个地方调用同一个任务,由于任务里的局部变量会使用共享的静态存储区,所以不同的线程之间会窜用这些局部变量。在Verilog-2001里,可以指定任务、函数和模块使用自动存储,从而迫使仿真器使用堆栈区存储局部变量。

  在SystemVerilog中,模块(module)和program块中的子程序缺省情况下仍然使用静态存储。如果要使用自动存储,则必须在程序语句中加入automatic关键词

// 例3.22 在program块中指定自动存储方式
program automatic test ; // program不可包含always/UDP/module/interface/program
task wait_for_mem (input [31:0] addr, expect_data,
output success) ;
while (bus.addr !== addr)
@(bus.addr) ;
success = (bus.data == expect_data) ;
endtask
...
endprogram

  因为参数addr和expect_data在每次调用时都使用不同的存储空间,所以对这个任务同时进行多次调用是没有问题的。但如果没有修饰符automatic,由于第一次调用的任务处于等待状态,所以对wait_for_mem的第二次调用会覆盖它的两个参数。

3.6.2 变量的初始化

  当你试图在声明中初始化局部变量时,类似的问题也会出现,因为局部变量实际上在仿真开始前就被赋了初值。常规的解决办法是避免在变量声明中赋予除常数以外的任何值。

// 例3.23 静态初始化的漏洞
program initialization ;
task check_bus ;
repeat (5) @(posedge clock) ;
if (bus_cmd == 'READ) begin
//何时对 local_addr 赋初值?
logic [7:0] local_addr = addr << 2 ; //有漏洞
$display ("Local Addr = %h", local_addr) ;
end
endtask
endprogram

  存在的漏洞是,变量local_addr是静态分配的,所以实际上在仿真的一开始它就有初始值,而不是等到进入begin...end块才进行初始化。同样地,解决办法是把程序块声明为automatic:

// 例3.24 修复静态初始化的漏洞:使用automatic
program automatic initialization ; // 漏洞修复
...
endprogram

  此外,你如果不在声明中初始化变量,那这个漏洞可以避免,只是这种方式不太好记住,尤其习惯了C语言的程序员。下例给双一种较为可取的编码风格,用于分离声明和初始化。

// 例3.25 修复静态初始化的漏洞:把声明和初始化拆开
logic [7:0] local_addr ;
local_addr = addr << 2 ; // 漏洞

3.7 时间值

  SystemVerilog有几种新结构使你可以非常明确地在你的系统中指明时间值。

3.7.1 时间单位和精度

  当你依赖语句 `timescale 时,在编译文件时就必须按照适当的顺序以确保所有的时延都采用适宜的量和精度。timeunit 和 timeprecision 声明语句可以明确地位每个模块指明时间值,从而避免模糊不清。注意,如果你使用这些语句代替 `timescale ,则必须把它们放到每个带有时延的模块里。

3.7.2 时间参数

  SystemVerilog允许使用数值和单位来明确指定一个时间值。 $timeformat的四个参数分别是时间标度、小数点后的数据精度、时间值之后的后缀字符串、显示数值的最小宽度。

// 例3.26 时间参数和$timeformat
module timing ;
timeunit 1ns ;
timeprecision 1ps ;
initial begin
$timeformat(-9, 3, "ns", 8) ;
#1 $display ("%t", $realtime) ; // 1.000ns
#2ns $display ("%t", $realtime) ; // 3.000ns
#0.1ns $display ("%t", $realtime) ; // 3.100ns
#41ps $display ("%t", $realtime) ; // 3.141ns
end
endmodule

3.7.3 时间和变量

  你可以把时间值存放到变量里,并在计算和延时中使用它们。根据当前的时间量程和精度,时间值会被缩放或舍入。time 类型的变量不能保存小数时延,因为它们是64bit的整数,所以时延的小数部分会被舍入。如果你不希望这样,你应该采用 real 变量。

// 例3.27 时间变量及舍入
`timescale 1ps/1ps
module ps;
initial begin
real rdelay = 800fs ; // 以0.800存储
time tdelay = 800fs ; // 舍入后得到 1
$timeformat (-15, 0, "fs", 5) ;
#rdelay ; // 时延后得到 1ps
$display ("%t", rdelay) ; // "800fs"
#tdelay ; // 再次延时 1ps
$display ("%t", tdelay) ; // "1000fs"
end
endmodule

  系统任务 $time的返回值时一个根据所在模块的时间精度要求进行舍入的整数,不带小数部分,而 $realtime 的返回值则是一个带小数部分的完整实数。

《SystemVerilog验证-测试平台编写指南》学习 - 第3章 过程语句和子程序的更多相关文章

  1. 《SystemVerilog验证-测试平台编写指南》学习 - 第2章 数据类型

    <SystemVerilog验证-测试平台编写指南>学习 - 第2章 数据类型 2.1 内建数据类型 2.2 定宽数组 2.2.1 声明 2.2.2 常量数组 2.2.3 基本的数组操作 ...

  2. 《SystemVerilog验证-测试平台编写指南》学习 - 第1章 验证导论

    <SystemVerilog验证-测试平台编写指南>学习 - 第1章 验证导论 测试平台(testbench)的功能 方法学基础 1. 受约束的随机激励 2. 功能覆盖率 3. 分层的测试 ...

  3. 《JS权威指南学习总结--第九章 类和模板》

    内容要点: 一. 1.第六章详细介绍了JS对象,每个JS对象都是一个属性集合,相互之间没有任何联系.在JS中也可以定义对象的类,让每个对象都共享某些属性,这种"共享"的特性是非常有 ...

  4. 《JS权威指南学习总结--第二章词法结构》

    第二章词法结构 内容要点: 一.注释 1. //表示单行注释 2. /*这里是一段注释*/ 3.一般编辑器里加注释是:选中要加注释的语句,按 ctrl+/ 二.直接量 所谓直接量,就是程序中直接使用的 ...

  5. 学习版pytest内核测试平台开发万字长文入门篇

    前言 2021年,测试平台如雨后春笋般冒了出来,我就是其中一员,写了一款pytest内核测试平台,在公司落地.分享出来后,有同学觉得挺不错,希望能开源,本着"公司代码不要传到网上去,以免引起 ...

  6. Git_学习_09_Commit message 和 Change log 编写指南

    一.前言 二.Commit message编写 1.规范 2.用空行分开主题和正文 提交时只执行 git commit,这时就会跳出文本编辑器,让你写多行. git commit 主题和正文分开 每次 ...

  7. 【PyHacker编写指南】打造URL批量采集器

    这节课是巡安似海PyHacker编写指南的<打造URL批量采集器> 喜欢用Python写脚本的小伙伴可以跟着一起写一写呀. 编写环境:Python2.x 00x1: 需要用到的模块如下: ...

  8. 基于Asterisk的VoIP开发指南——Asterisk 模块编写指南(1)

    原文:基于Asterisk的VoIP开发指南--Asterisk 模块编写指南(1) 1 开源项目概述 Asterisk是一个开源的软件包,通常运行在Linux操作系统平台上.Asterisk可以用三 ...

  9. 支撑Pinterest日均1000+次试验的A/B测试平台揭秘

    编者按:本文详细介绍了 Pinterest 内部A/B测试平台的搭建过程,对于无论是有技术能力和资源想要自建A/B测试系统的大公司,还是想在业务中引入第三方A/B测试方法和工具的中小公司都极具参考意义 ...

随机推荐

  1. 解决删除Azure Active Directory的Enterprise Applications异常

    当我们不需要使用某个Azure Active Directory(以下简称AAD)的时候,我们可以删除它,这个时候Azure会对当前的AAD包含的内容进行检查, 在所有的检查项目中有一个名叫" ...

  2. Python中切片的应用

    Python中切片的应用 Python中可以通过切片实现对列表或者字符串取指定范围的操作,实际就是通过对列表或者字符串通过索引进行操作. 具体细节点击廖雪峰Python教程,其中的课后小问题在此记录下 ...

  3. 全网最清楚的:MySQL的insert buffer和change buffer 串讲

    目录 一.前言 二.问题引入 2.1.聚簇索引 2.2.普通索引 三.change buffer存在的意义 四.再看change buffer 五.change buffer 的限制 六.change ...

  4. BPF for storage:一种受外核启发的反式

    BPF for storage:一种受外核启发的反式 译自:BPF for storage: an exokernel-inspired approach BPF主要用于报文处理,通过绕过网络栈提高报 ...

  5. 如何在 NET 程序万种死法中有效的生成 Dump (下)

    一:背景 上一篇我们聊到了如何通过 procdump 抓取 cpu爆高 和 内存暴涨 两种情况,这一篇再聊聊如何去抓程序 挂死 和 意外退出. 二:程序挂死 1. 定义 程序挂死 简单的说就是程序没有 ...

  6. JavaScript设计模式(二):工厂模式

    工厂模式模式的定义与特点 工厂模式(Factory Pattern)是编程中最常用的设计模式之一.这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式.在工厂模式中,我们在创建对象时不会对 ...

  7. Django模板引擎

    Django作为Web框架,需要一种很便利的方法动态地生成 HTML 网页,因此有了模板这个概念.模板包含所需 HTML 的部分代码以及一些特殊语法,特殊语法用于描述如何将视图传递的数据动态插入HTM ...

  8. 2021S软件工程——结对项目第一阶段

    # 2021S软件工程--结对项目第一阶段 2021春季软件工程(罗杰 任健) 项目地址 1020 1169 --- ## 1 结对感受 总体来说,结对编程与之前的个人编程感觉有很大的不同.有如下几个 ...

  9. hdu 5059 判断数字表示方式以及范围合法(int型之内)

    题意:       给你一个串和两个整数a,b,问你这个串表示的数字是否合法,并且在a,b之间, 和法的要求是无论是正数还是负数都没有前导0,并且注意 -0 是不合法的. 思路:       写了将近 ...

  10. android中Stub Proxy答疑

    在上篇添加账户源码解析的博文中,我们发现功能是由AccountManager的mService成员来实现.而mService其实是AccountManagerService,如果对android系统有 ...