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


  SystemVerilog引进了一些新的数据类型,它们具有如下优点:

(1)双状态数据类型:更好的性能,更低的内存消耗;

(2)队列、动态和关联数组:减少内存消耗,自带搜索和分类功能;

(3)类和结构:支持抽象数据结构;

(4)联合和合并结构:允许对同一数据有多种视图(view);

(5)字符串:支持内建的字符序列;

(6)枚举类型:方便代码编写,增加可读性;


2.1 内建数据类型

  logic类型不能有多个结构性的驱动,在双向总线建模的时候要使用线网类型。

  logic类型只能有一个驱动,否则编译报错,所以logic可以用来查找顶层多驱动的错误。

双状态数据类型

  最简单的双状态数据类型是bit,它是无符号的。另外4种带有符号的双状态数据类型是byte、shortint、int和longint,如下所示:

bit           b      ;    // 双状态,单比特无符号
bit [31:0] b32 ; // 双状态,32比特无符号整数
int unsigned ui ; // 双状态,32比特无符号整数 int i ; // 双状态,32比特有符号整数
byte b8 ; // 双状态,8 比特有符号整数
shortint s ; // 双状态,16比特有符号整数
longint l ; // 双状态,64比特有符号整数 integer i4 ; // 四状态,32比特有符号整数
time t ; // 四状态,64比特无符号整数 real r ; // 双状态,双精度浮点数

记:integer和time是四状态数据类型,integer是32位有符号,time是64位无符号。

注:

  你可能会乐意用byte数据类型代替logic[7:0]的声明,以使得程序更加简洁。但是需要注意的是这些新的数据类型是带符号的,所以byte变量的最大值只有127(取值范围-128 ~ 127)。可以使用byte unsigned,但这其实比使用bit[7:0]还要麻烦。在进行随机化时,带符号变量可能会造成意想不到的结果。

  若把双状态变量连接到待测设计,务必小心,如果待测设计产生了X或者Z,这些值会被转换为双状态值,而测试代码可能永远也察觉不了。这些值被转换为了0还是1并不重要,重要的是要随时检查未知值的传播。使用 $isunknown 操作符,可以在表达式的任意位出现X或者Z时返回1,如下例所示:

if ($isunknown(iport) == 1)
$display("@%0t: 4-state value detected on iport %b", $time, iport);

2.2 定宽数组

2.2.1 声明

  SystemVerilog允许只给出数组宽度的便捷声明方式,和C语言类似。

  可以通过在变量名后面指定维度的方式来创建多维数组。紧凑型声明方式是SystemVerilog特有的。

int       lo_hi      [0:15]       ; // 16个整数[0]...[15]
int c_style [16] ; // 便捷声明,16个整数[0]...[15] int array2 [0:7][0:3] ; // 完整的声明
int array3 [8][4] ; // 紧凑的声明
array2[7][3] = 1 ; // 设置最后一个元素为1

  若代码中试图从一个越界的地址中读取数据,那么SystemVerilog将返回数组元素的缺省值。比如,四状态logic返回X,双状态int或bit则返回0。这适用于所有数组类型,包括定宽数组、动态数组、关联数组和队列,也同时适用于地址中包含有X和Z的情况。线网在没有驱动时输出是Z。

  对于非压缩数组(非合并数组),很多SystemVerilog仿真器存放数组元素时使用32bit的字符边界,所以byte、shortint和int都是存放在相同长度的一个字中,而longint则存放到两个字中。例如:

bit   [7:0] b_unpack [3] ;  // 定义3个8位的非压缩数组

注:非压缩数组(非合并数组) 占用更多的内存空间。

2.2.2 常量数组

  使用:一个单引号加大括号来初始化数组。

  可以部分赋值;可以重复次数赋值;可以为那些没有显式赋值的元素指定一个缺省值。如下所示:

int     ascend  [4] = '{0, 1, 2, 3} ;    // 对4个元素进行初始化
int descend [5] ; descend = '{4, 3, 2, 1, 0} ; // 为5个元素赋值
descend [0:2] = '{5, 6, 7} ; // 为前3个元素赋值
ascend = '{4{8}} ; // 4个值全部为8
descend = '{9, 8, default:1} ; // {9, 8, 1, 1, 1}

2.2.3 基本的数组操作 -- for和foreach

  操作数组最常见的方式是使用 for 或者 foreach 循环。

  $size函数返回数组的宽度。

  在 foreach 循环中,只需要指定数组名并在后面的方括号中给出索引变量,SystemVerilog便会自动遍历数组中的元素,索引变量将自动声明,并只在循环内有效。

// 在数组操作中使用 for 和 foreach 循环

initial begin
bit [31:0] src [5] ; // 声明5个32位整数
bit [31:0] dst [5] ; // 声明5个32位整数
//bit [31:0] src[5], dst[5] ; for (int i = 0; i < $size(src); i++)
src[i] = i ;
foreach (dst[j])
dst[j] = src[j] * 2 ; // dst的值是srcd的2倍
end
// 初始化并遍历多维数组
int md[2][3] = '{'{0, 1, 2}, '{3, 4, 5}} ; // 定义常量数组
initial begin
$display ("Initial Value:") ;
foreach (md[i, j]) // 正确语法格式
$display ("md[%0d][%0d]=%0d", i, j, md[i][j]) ; $display ("New Value:") ;
// 对最后3个元素重复赋值5
md = '{'{9, 8, 7}, '{3{32'd5}}} ;
foreach (md[i, j]) // 正确语法格式
$display ("md[%0d][%0d]=%0d", i, j, md[i][j]) ;
end

  打印结果:

Initial Value:
md[0][0]=0
md[0][1]=1
md[0][2]=2
md[1][0]=3
md[1][1]=4
md[1][2]=5 New Value:
md[0][0]=9
md[0][1]=8
md[0][2]=7
md[1][0]=5
md[1][1]=5
md[1][2]=5

注意:foreach 的使用

  如果不需要遍历数组中的所有维度,可以在 foreach 循环里忽略掉它们。看下面的例子,把一个二维数组打印成一个方形的阵列。它在外层循环遍历一个维度,然后在内层循环遍历第二个维度。

// 打印一个多维数组
initial begin
byte twoD [4][6] ; //
foreach (twoD[i, j])
twoD [i][j] = i * 10 + j ; //赋初值 foreach (twoD[i]) begin
$write ("%2d: ", i) ;
foreach (twoD[, j])
$write ("%3d", twoD[i][j]) ;
$display ;
end
end

  打印结果如下:

0: 0 1 2 3 4 5
1: 10 11 12 13 14 15
2: 20 21 22 23 24 25
3: 30 31 32 33 34 35

  补充:foreach 循环会遍历原始声明中的数组范围。如,

数组 f[5]等同于f[0:4],而 foreach (f[i]) 等同于 for (int i = 0; i <= 4; i++)。

数组 rev[6:2]来说,foreach (rev[i]) 等同于 for (int i=6; i >= 2; i--)。

2.2.4 基本的数组操作 -- 复制和比较

  你可以在不使用循环的情况下对数组进行聚合比较和复制(聚合操作适用于整个数组而不是单个元素),其中比较只限于等于或不等于比较。

    // 数组的复制和比较操作
initial begin
bit [31:0] src [5] = '{0, 1, 2, 3, 4} ,
dst [5] = '{5, 4, 3, 2, 1} ;
// 两个数组的聚合比较
if (src == dst)
$display ("src == dst") ;
else
$display ("src != dst") ; // 把 src 所有元素复制给 dst
dst = src ; // 只改变一个元素的值
src[0] = 5 ; // 所有元素的值是否相等(否!)
$display("src %s dst", (src == dst) ? "==" : "!=") ; // 使用数组片段对第 1-4 个元素进行比较
$display ("src[1:4] %s dst[1:4]", (src[1:4] == dst[1:4]) ? "==" : "!=") ;
end

  对数组的算术运算不能使用聚合操作,而应该使用循环;

  对于逻辑运算只能使用循环;

2.2.6 合并数组(压缩数组)

  声明合并数组时,合并的位和数组大小作为数据类型的一部分必须在变量名前面指定。数组大小定义的格式必须是 [msb:lsb] ,而不是 [size]

// 合并数组的声明和用法
bit [3:0] [7:0] bytes ; // 4个字节组装成32bit
bytes = 32'hCafe_Dada ;
$displayh (bytes, // 显示所有的 32bit
bytes[3], // 最高字节 “Ca"
bytes[3][7]); // 最高bit位 “1”

  合并和非合并数组可以混合使用。你可能会使用数组来表示存储单元,这些单元可以按 比特、字节、长字节 的方式进行存取。

// 合并/非合并混合数组的声明
bit [3:0] [7:0] barray [3] ; // 合并:3x32 bit
bit [31:0] lw = 32'h0123_4567 ; // 字
bit [7:0] [3:0] nibbles ; // 合并数组
barray[0] = lw ;
barray[0][3] = 8'h01 ;
barray[0][1][6] = 1'b1 ;
nibbles = barray[0] ; // 复制合并数组的元素值

2.2.8 合并数组和非合并数组的选择

  

  1. 当需要和标量进行相互转换时,使用合并数组会较方便;

    比如你需要以字节或字为单位进行操作,上例 barray 可满足要求。

  2. 任何数组类型都可以合并,包括动态数组、队列和关联数组;

  3. 需要等待数组中的变化,则必须使用合并数组;

    当测试平台需要通过存储器的变化来唤醒时,你会想到使用@操作符,但是这个操作符只能用于标量或者合并数组。在上例中,lw 和 barray[0] 可以用做敏感信号,但是整个 barray 不行,除非扩展: @(barray[0] or barray[1] or barray[2])。

2.3 动态数组

  SystemVerilog提供了动态数组类型,可以在仿真时分配或调整宽度,这样在仿真中就可以使用最小的存储量。

  动态数组在声明时使用空的下标 [] 。数组在最开始的时候是空的,所以你必须调用 new[] 操作符来分配空间,同时在方括号中传递数组宽度。可以吧数组名传递给 new[] 构造符,并把已有数组值赋值到新数组里。

// 使用动态数组
int dyn[], d2[] ; // 声明动态数组 initial begin
dyn = new[5] ; // A: 分配5个元素
foreach (dyn[j])
dyn[j] = j; // B: 对元素进行初始化
d2 = dyn ; // C: 复制一个动态数组
d2[0] = 5 ; // D: 修改复制值
$display (dyn[0], d2[0]) ; // E: 显示数值(0和5)
dyn = new[20](dyn) ; // F: 分配20个整数值并进行复制
dyn = new[100] ; // G: 分配100个新的整数值,旧值不复存在
dyn.delete() ; // H: 删除所有元素,删除 dyn 数组
end // 使用动态数组来保存元素数量不定的列表
bit [7:0] mask [] = '{8'b0000_0000, 8'b0000_0001,
8'b0000_0011, 8'b0000_0111,
8'b0000_1111, 8'b0001_1111,
8'b0011_1111, 8'b0111_1111,
8'b1111_1111
} ;

  只要基本数据类型相同,定宽数组和动态数组之间就尅相互赋值,在元素数目相同的情况下,可以把动态数组的值复制到定宽数组。

2.4 队列

  队列,结合了链表和数组的优点

  队列和链表相似,可以在一个队列中的任何地方增加或删除元素,这类操作在性能上的损失比动态数组小的多,因为动态数组需要分配新的数组并复制所有元素的值。

  队列与数组相似,可以通过索引实现对任一元素的访问,而不需要像链表那样去遍历目标元素之前的所有元素。

  队列的声明使用带美元符号的下标:[$]。队列元素的编号从0到$。 使用方法(method)在队列中增加和删除元素。

  注意队列的常量(literal)赋值是没有单引号的大括号。不要对队列使用构造函数 new[]。

// 队列的操作
int j = 1 ,
q2[$] = {3, 4} , // 队列的常量不需要使用“'”
q[$] = {0, 2, 5} ; // {0, 2, 5} initial begin
q.insert (1, j) ; // {0, 1, 2, 5} 在2之前插入1
q.insert (3, q2) ; // {0, 1, 2, 3, 4, 5}在q中插入一个队列
q.delete (1) ; // {0, 2, 3, 4, 5} 删除第1个元素 // 下面的操作执行速度很快
q.push_front (6) ; // {6, 0, 2, 3, 4, 5}在队列前插入
j = q.pop_back ; // {6, 0, 2, 3, 4} j = 5
q.push_back (8) ; // {6, 0, 2, 3, 4, 8}在队列尾插入
j = q.pop_front ; // {0, 2, 3, 4, 8} j = 6
foreach (q[i])
$display (q[i]) ; // 打印整个队列
q.delete () ; // {} 删除整个队列
end

  可以使用下标字符串来替代方法。如果把 $ 放到一个范围表达式的左边,那么 $ 将代表最小值,如 [$:2] 代表 [0:2]。如果把 $ 放到一个表达式的右边,那么 $ 将代表最大值,如 [1:$] 代表 [0:max_num]。

// 队列操作
int j = 1 ,
q2[$] = {3, 4} ,
q[$] = {0, 2, 5} ; // {0, 2, 5} initial begin
q = {q[0], j, q[1:$]} ; // {0, 1, 2, 5} 在2之前插入1
q = {q[0:2], q2, q[3:$]} ; // {0, 1, 2, 3, 4, 5} 在q中传入一个队列
q = {q[0], q[2:$]} ; // {0, 2, 3, 4, 5} 删除第1个元素 // 下面的操作执行速度很快
q = {6, q} ; // {6, 0, 2, 3, 4, 5} 在队列前面插入
j = q[$] ; // j = 5
q = q[0:$-1] ; // {6, 0, 2, 3, 4} 在队列末尾取走数据
q = {q, 8} ; // {6, 0, 2, 3, 4, 8} 在队列末尾插入
j = q[0] ; // j = 6
q = q[1:$] ; // {0, 2, 3, 4, 8} 从队列前面取走数据 q = {} ; // {} 删除整个队列
end

  队列中元素是连续存放的,所以在队列的前面或后面存取整数非常方便,无论队列有多大,这种操作所消耗的时间是一样的。

  在队列中间增加或删除元素需要对已经存在的数据进行搬移以便腾出空间,相应操作所消耗的时间会随着队列的大小线性增加。

2.5 关联数组

  如果你需要超大容量的数据存放,假设你正在对一个有着几个G字节寻址范围的处理器进行建模。在典型的测试中,这个处理器可能只访问了用来存放可执行代码和数据的几百或几千个字节,这种情况下对几个G字节的存储空间进行分配和初始化显然是浪费的。

  关联数组类型可以用来保存稀疏矩阵元素。这意味着当你对一个非常大的地址空间进行寻址时,SystemVerilog只为实际写入的元素分配空间。如下图所示,关联数组只保留0...3、42、1000、4521和200000等位置上的值。

  仿真器可以采用树或哈希表的形式存放关联数组,但有一定的额外开销。

  关联数组采用在方括号中放置数据类型的形式进行声明

// 关联数组的声明、初始化和使用
initial begin
bit [63:0] assoc[bit [63:0]] , // 关联数组声明
idx = 1 ; // 对稀疏分布的元素进行初始化
repeat (64) begin
assoc[idx] = idx ;
idx = idx << 1 ;
end // 使用 foreach 遍历数组
foreach (assoc[i])
$display ("assoc[%h] = %h", i, assoc[i]) ; //使用函数遍历数组
if (assoc.first (idx)) begin
do // 得到第一个索引
$display ("assoc[%h] = %h", idx, assoc[idx]) ;
while (assoc.next (idx)) ; // 得到下一个索引
end // 找到并删除第一个元素
assoc.first (idx) ;
assoc.delete (idx) ;
$display ("The array now has %0d elements", assoc.num) ;
end

  对于关联数组,简单的for循环并不能遍历数组,需要使用 foreach 循环遍历数组。如果你想控制的更好,可以在 do...while 循环中使用 first 和 next 函数。这些函数可以修改索引参数的值,然后根据数组是否为空返回 0 或 1。

  关联数组也可以使用字符串索引进行寻址。下例使用字符串索引读取文件,并建立关联数组switch,以实现从字符串到数字的映射。可以使用函数 exists() 来检查元素是否存在。

// 使用带字符串索引的关联数组
/*
输入文件的内容如下:
42 min_address
1492 max_address
*/ int switch [string],
min_address ,
max_address ;
initial begin
int i, r, file ;
string s ;
file = $open("switch.txt", "r") ;
while (! $feof (file)) begin // 当读到文件末尾时 $feof (<文件句柄>) != 0 ,否则为0
r = $fscanf (file, "%d %s", i, s) ; // $fscanf 一行一行读取数据
switch [s] = i ;
end
$fclose (file) ; // 获取最小地址值,缺省为0
min_address = switch ["min_address"] ;
// 获取最大地址值,缺省为1000
if (switchl.exists ("max_address"))
max_address = switch["max_address"] ;
else
max_address = 1000 ; // 打印数组的所有元素
foreach (switch[s])
$display ("switch['%s'] = %0d", s, switch[s]) ;
end

2.7 数组的方法

  SystemVerilog提供了很多数组的方法,可用于任何一个非合并的数组类型,包括定宽数组、动态数组、队列和关联数组。如果不带参数,则方法中的圆括号可以省略。

2.7.1 数组的缩减方法

  基本的数组缩减方法是把一个数组缩减成一个值。

// 数组求和
bit on[10] ; // 单比特数组
int total ; initial begin
foreach (on[i])
on[i] = i ; // 注意, on[i] 的值为 0/1 // 打印出单比特和
$display ("on.sum = %0d", on.sum) ; // on.sum = 1 // 打印出32比特和
$display ("on.sum = %0d", on.sum + 32'd0) ; // on.sum = 5 // 由于total是32比特变量,所以数组和也是32比特
total = on.sum ;
$display ("total = %0d", total) ; // total = 5 // 将数组和与一个32比特数进行比较
if (on.sum >= 32'd5) // 条件成立
$display ("sum has 5 or more 1's") ; // 使用带32比特有符号运算的with表达式
$display ("int sum = %0d", on.sum with (int '(item))) ;
end

  其它数组缩减方法还有 product(积),and(与),or(或),xor(异或)

  SystemVerilog没有提供专门从数组里随机选取一个元素的方法。所以对于定宽数组、队列、动态数组和关联数组可以使用 $urandom_range ($size (array) - 1) ,而对于队列和动态数组还可以使用 $urandom_range (array, size () - 1)。

  如果想从一个关联数组中随机选取一个元素,你需要逐个访问它之前的元素,原因是没有办法能够直接访问到第 N 个元素。

int  aa [int] ,
rand_idx ,
element ,
count ; element = $urandom_range (aa, size () - 1) ;
foreach (aa[i])
if (count++ == element) begin
rand_idx = i ; // 保存关联数组的索引
break ; // 并退出
end $display ("%0d element aa[%0d] = %0d", element, rand_idx, aa[rand_idx]) ;

2.7.2 数组的定位方法

  查找数组最大值、特定值,在非合并(非压缩)数组中可以使用数组定位方法,这些方法的返回值通常是一个队列

方法 作用
max() 找出数组中的最大值
min() 找出数组中的最小值
unique() 返回数组中具有唯一值的队列(即去重)
find 可结合with使用
find_index 可结合with使用,找出索引值
find_first 可结合with使用,找出第一个匹配值
find_first_index 可结合with使用,找出第一个匹配的索引
find_last 可结合with使用,找出最后一个匹配值
find_last_index 可结合with使用,找出最后一个匹配的索引
sum 求出满足条件的和,即条件表达式为真的次数
// 数组定位方法:min、max、unique
int f [6] = '{1, 6, 2, 6, 8, 6} ;
int d [] = '{2, 4, 6, 8, 10} ;
int q [$] = {1, 3, 5, 7}, tq[$] ; tq = q.min() ; // {1}
tq = d.max() ; // {10}
tq = f.unique() ; // {1, 6, 2, 8}

  使用 foreach 循环固然可以实现数组的完全搜索,但如果使用SystemVerilog的定位方法,则只需要一个操作便可以完成。表达式with可以指示SystemVerilog如何进行搜索

// 数组定位方法:find
int d [] = '{9, 1, 8, 3, 4, 4} ,
tq [$] ;
// 找出所有大于3的元素
tq = d.find with (item > 3) ; // {9, 8, 4, 4}
// 等效代码
tq.delete() ;
foreach (d[i])
if (d[i] > 3)
tq.push_back (d[i]) ; tq = d.find_index with (item > 3) ; // {0, 2, 4, 5}
tq = d.find_first with (item > 99) ; // {} 没有找到
tq = d.find_first_index with (item == 8) ; // {2} d[2]=8
tq = d.find_last with (item == 4) ; // {4}
tq = d.find_last_index with (item == 4) ; // {5} d[5]=4

  在条件语句with中,item被称为重复参数,它代表了数组中一个单独元素。item是缺省的名字,也可以指定其他名称,只要在数组方法的参数列表中列出来就可以了。

// 重复参数的声明
tq = d.find_first with (item == 4) ; // 本例4个语句等价
tq = d.find_first () with (item == 4) ;
tq = d.find_first (item) with (item == 4) ;
tq = d.find_first (x) with (x == 4) ;
// 数组定位方法
int count,
total,
d[] = '{9, 1, 8, 3, 4, 4} ; // 第一次求和(total)是先把元素值和7进行比较,比较表达式返回1(为真)或0(为假),然后再把返回结果与对应元素相乘
count = d.sum with (item > 7) ; // 2: {9, 8}
total = d.sum with ((item > 7) * item) ; // 17=9+8 count = d.sum with (item < 8) ; // 4: {1, 3, 4, 4}
total = d.sum with (item < 8 ? item : 0) ; // 12=1+3+4+4
count = d.sum with (item == 4) ; // 2: {4, 4}

  当吧数组缩减方法与条件语句with结合使用时,sum操作符的结果是条件表达式位真的次数

  返回值为索引的数组定位方法,其返回的队列类型是int而非integer,例如 find_index 方法。

2.7.3 数组的排序

  数组的定位方法是新建一个队列来保存返回值,而数组的排序方法则改变了原始数据。

// 对数组排序
int d [] = '{9, 1, 8, 3, 4, 4} ;
d.reverse () ; // '{4, 4, 3, 8, 1, 9}
d.sort () ; // '{1, 3, 4, 4, 8, 9}
d.rsort () ; // '{9, 8, 4, 4, 3, 1}
d.shuffle () ; // '{9, 4, 3, 8, 1, 4}

注意:reverse 和 shuffle 方法不能带 with 条件语句,所以它们的作用范围是整个数组。

  下例示范了如何使用子域对一个结构进行排序:

// 对数组结构进行排序
struct packed {byte red, green, blue;} c[] ;
initial begin
c = new [100] ; // 分配100个像素
foreach (c[i])
c[i] = $urandom ; // 填上随机数 c.sort with (item, red) ; // 只对红色像素进行排序 // 先对绿色像素后对蓝色像素进行排序
c.sort (x) with ({x.green, x.blue}) ;
end

2.7.4 使用数组定位方法建立计分板

  数组定位方法可以用来建立计分板。下例定义了一个包结构(Packet),然后建立了一个由包结构队列组成的计分板。

  下例中的 check_addr () 函数在计分板里寻找和参数匹配的地址。find_index () 方法返回一个 int 队列。如果该队列为空 (size == 0),则说明没有匹配。如果该队列有一个成员 (size == 1),则说明有一个匹配,该匹配随后被删除。如果该队列有多个成员 (size > 1),则说明计分板里有多个包地址和目标匹配。

  对于包信息的存储,更好的方式是采用类。

// 带数组方法的计分板
typedef struct packed
{bit [7:0] addr ;
bit [7:0] pr ;
bit [15:0] data; } Packet ; Packet scb [$] ;
function void check_addr (bit [7:0] addr) ;
int intq [$] ; intq = scb.find_index () with (item.addr == addr) ;
case (intq.size())
0: $display ("Addr %h not found in scoreboard", addr) ;
1: scb.delete (intq[0]) ;
default: $display ("Error: Multiple hits for addr %h", addr) ;
endcase
endfunction : check_addr

2.8 选择存储类型

  介绍基于灵活性、存储用量、速度和排序要求正确选择存储类型的一些准则。这些准则只是一些经验法则,其结果可能随仿真器的不同而不同。

2.8.1 灵活性

  如果数组的索引是连续的非负整数,则应使用定宽数组或动态数组。当数组的宽度在编译时已经确定则选择定宽数组,如果要等到程序运行时才知道数组宽度则选择动态数组

  例如长度可变的的数据包使用动态数组存储会很方便。

  当你编写处理数组的子程序时,最好使用动态数组,因为只要在元素类型(如int、string)匹配的情况下,同一个子程序可以处理不同宽度的数组。同样地,只要元素类型匹配,任意长度的队列都可以传递给子程序。关联数组也可以作为参数传递,而不需要考虑数组宽度的问题。相比之下,带定宽数组参数的子程序则只能接受指定宽度的数组。

  对于那些在仿真过程中元素数目变化很大的数组,例如保存预期值的计分板,队列是一个很好的选择。

2.8.2 存储器用量

  使用双状态类型可以减少仿真时的存储用量。为了避免浪费空间,应尽量选择32bit的整数倍作为数据位宽。仿真器通常会把位宽小于32bit的数据存放到32bit的字里。使用合并数组有助于节省存储空间。

  因为需要额外的指针,队列的存取效率比定宽数组或动态数组稍差。但是,如果你把一个长度经常变化的数据集存放到动态数组里,那么你需要手工调用 new[] 来分配和复制内存。这个操作的代价会很高,可能会抵消动态数组所带来的全部好处。

  对兆字节量级的存储器建模应该使用关联数组。注意,因为指针带来的额外开销,关联数组里每个元素所占用的空间可能会比定宽数组或动态数组占用的空间大好几倍。

2.8.3 速度

  因为定宽数组和动态数组都是被存放在连续的存储空间里,所以访问其中的任何元素耗时都相同,而与数组的大小无关。

  队列的读写速度与定宽或动态数组基本相当。队列的首位元素的存取几乎没有任何开销,而在队列中间插入或删除元素则需要对很多其他元素进行搬移以便腾出空间。当你需要在一个很长的队列里插入新元素是,你的测试程序可能会变得很慢,这时最好考虑改变新元素的存储方式。

  对关联数组进行读写时,仿真器必须在存储器里进行搜索。所以关联数组的存取速度是最慢的

2.8.5 选择最优的数据结构

  以下是针对数据结构选择的一些建议:

  1. 网络数据包。特点:长度固定,顺序存取。针对长度固定或可变的数据包可分别采用定宽数组或动态数组。
  2. 保存期望值的计分板。特点:仿真前长度未知,按值存取,长度经常变化。一般情况下可使用队列,这样方便在仿真期间连续增加和删除元素。
  3. 有序结构。如果数据按照可预见的顺序输出,那么可以使用队列;如果输出顺序不确定,则使用关联数组。如果不用对计分板进行搜索,那么只需要把预期的数值存入信箱(mailbox)。
  4. 对超过百万个条目的特大容量存储器进行建模。如果你不需要用到所有的存储空间,可以使用关联数组实现稀疏存储。

2.9 使用 typedef 创建新的类型

  typedef 语句可以用来创建新的类型。

// Verilog 中用户自定义的类型宏,只进行文本替换
// 老的Verilog风格
`define OPSIZE 8
`define OPREG reg [`OPSIZE-1:0]
`OPREG op_a, op_b ; // SystemVerilog中用户自定义类型
// 新的SystemVerilog风格
parameter OPSIZE = 8 ;
typedef reg [OPSIZE-1:0] opreg_t ;
opreg_t op_a, op_b ;

  一般来说,即时数据位宽不匹配,SystemVerilog都允许在这些基本类型之间进行赋值而不会给出警告。

  用户自定义的最有用的类型是双状态的32bit无符号整型。如下例定义:

// uint 的定义
typedef bit [31:0] uint ; // 32bit双状态无符号数
typedef int unsigned uint ; // 32bit双状态无符号数,和上面的等效

2.10 创建用户自定义结构

  Verilog最大的缺陷之一就是没有数据结构。在SystemVerilog中可以使用struct语句创建结构,跟C语言类似。但是struct功能比类少,所以还不如直接在测试平台使用类。类里面包含数据和程序,以便于重用,struct只是把数据组织到一起,所以只解决了一半问题。

  由于struct只是一个数据的集合,所以它是可综合的

2.10.1 使用struct创建新类型

  下例中创建了一个名为pixel的结构,它有3个无符号的字节变量,分别代表红绿蓝。

// 创建一个pixel类型
struct {bit [7:0] r, g, b ;} pixel ;

  上例中的声明只是创建了一个pixel变量。要想在端口和程序中共享它,则必须创建一个新的类型,如下所示:

// pixel 结构
typedef struct {bit [7:0] r, g, b ;} pixel_s ;
pixel_s my_pixel ;

2.10.2 对结构进行初始化

  如同数组赋值一样,赋值时把数值放到带单引号的大括号中。

// 对struct类型进行初始化
initial begin
typedef struct {
int a ;
byte b ;
shortint c ;
int d ;
} my_struct_s ; my_struct_s st = '{
32'haaaa_aaaad,
8'hbb,
16'hcccc,
32'hdddd_dddd
} $display ("str = %x %x %x %x ", st.a, st.b, st.c, st.d) ;
end

2.10.3 创建可容纳不同类型的联合

  在硬件中,寄存器里的某些位的含义可能与其他位的值有关。下例把整数i和实数f存放到同一位置上。

// 使用typedef创建联合
typedef union { int i; real f; } num_u ;
num_u un ;
un.f = 0.0 ; // 把数值设为浮点形式

  如果需要以若干个不同的格式对同一寄存器进行频繁读写时,联合体相当有用。但是不要滥用。

2.10.4 合并结构

  合并结构是以连续比特集方式存放的,中间没有闲置的空间。2.10.1节的pixel结构使用了3个数值,所以它占用了3个长字的存储空间,即使它实际只需要3个字节。你可以指定把它合并到尽可能小的空间里。

// 例2.40 合并结构
typedef struct packed {bit [7:0] r, g, b; } pixel_p_s ;
pixel_p_s my_pixel ;

2.10.5 合并结构和非合并结构的选择

  必须考虑结构通常的使用方式和元素的对齐方式

  如果对结构的操作很频繁,例如需要经常对整个结构体进行复制,那么使用合并结构的效率会比较高。但是,如果操作经常是针对结构内的个体成员而非整体,那就应该使用非合并结构。当结构的元素不按字节对齐,或者元素位宽与字节不匹配,又或者元素是处理器的指令字时,使用合并和非合并结构在性能上的差别会更大。对合并结构中尺寸不规则的元素进行读写,需要移位和屏蔽操作票,代价很高。

2.11 类型转换

  如果源变量和目标变量的比特分布完全相同,例如整数和枚举类型(一般枚举的存储是int),那么它们之间可以直接相互赋值。

  如果源变量和目标变量的比特分布不同,例如字节数组和字数组,则需要使用流操作符对比特分布重新安排。

2.11.1 静态转换

  静态转换操作不对转换值进行检查。转换时指定目标类型,并在需要转换的表达式前加上单引号即可。注意,Verilog对整数和实数类型,或者不同位宽的向量之间进行隐式转换。

// 例2.41 在整型和实型之间进行静态转换
int i ;
real r ; i = int '{10.0 - 0.1} ; // 非强制转换
r = real '{42} ; // 非强制转换

2.11.2 动态转换

  动态转换函数 $cast 允许对越界的数值进行检查。

2.11.3 流操作符

  流操作符 << 和 >> 用在赋值表达式的右边,后边带表达式、结构或数组。流操作符用于把其后的数据打包成一个比特流。操作符 >> 把数据从左至右变成流,而 << 则把数据从右至左变成流。不能将比特流的结果直接赋值给非合并数组,而应该在赋值表达式的左边使用流操作符把比特流拆分到非合并数组中。

// 例2.42 基本的流操作
initial begin
int h ;
bit [7:0] b, g[4], j[4] = '{8'ha, 8'hb, 8'hc, 8'hd} ;
bit [7:0] q, r, s, t ; h = {>> { j }} ; // 0a0b0c0d 把数组打包成整型
h = {<< { j }} ; // b030d050 位倒序
h = {<< byte { j }} ; // 0d0c0b0a 字节倒序
g = {<< byte { j }} ; // 0d, 0c, 0b, 0a 拆分成数组
b = {<< { 8'b0011_0101 }} ; // 1010_1100 位倒序
b = {<< 4 { 8'b0011_0101 }} ; // 0101_0011 半字节倒序
{>> {q, r, s, t}} = j ; // 把j分散到4个字节变量里
h = {>> {t, s, r, q}} ; // 把字节集中到h里
end

  也可以使用很多连接符 {} 来完成同样的操作,但是流操作符用起来更简洁并且易于阅读。

  如果需要打包或拆分数组,可以使用流操作符来完成具有不同尺寸元素的数组间的转换。

// 例 2.43 使用流操作符进行队列间的转换
initial begin
bit [15:0] wq [$] = {16'h1234, 16'h5678} ;
bit [7 :0] bq [$] ; //把字数组转换成字节数组
bq = {>> { wq }} ; // 12 34 56 78 // 把字节数组转换成字数组
bq = {8'h98, 8'h76, 8'h54, 8'h32} ;
wq = {>> { bq }} ; // 9876 5432
end

  数组下标失配是在数组间进行流操作时常见的错误。数组声明中的下标 [256] 等同于 [0:255]。由于很多数组使用 [high:low] 的下标形式进行声明,使用流操作把它们的值赋给带 [size] 下标形式的数组会造成元素倒序。同样,如果把声明形式为 bit [7:0] src [255:0] 的非合并数组使用流操作赋值给声明形式为 bit [7:0] [255:0] dst 的合并数组,则数值的顺序会被打乱。对于合并的字节数组,正确的声明形式应该是 bit [255:0] [7:0] dst。

  流操作符也可以用来将结构打包或拆分到字节数组中。下例中,使用流操作符把结构转换成动态的字节数组,然后字节数组又被反过来转换成结构。

// 例2.44 使用流操作符在结构和数组间进行转换
initial begin
typedef struct {
int a ;
byte b ;
shortint c ;
int d ;
} my_struct_s ; my_struct_s st = '{
32'haaaa_aaaa ,
8'hbb ,
16'hcccc ,
32'hdddd_dddd
} ; byte b [] ; // 将结构转换成字节数组
b = {>> { st }} ; // {aa aa aa aa bb cc cc dd dd dd dd} // 将字节数组转换成结构
b = '{
8'h11, 8'h22, 8'h33, 8'h44,
8'h55,
8'h66, 8'h77,
8'h88, 8'h99, 8'haa, 8'hbb
} ;
st = {>> { b }} ; // st = 11223344,55,6677,8899aabb
end

2.12 枚举类型

  在学会使用枚举类型之前,你只能使用文本宏。宏的作用范围太大,而且大多数情况下对于调试者是可见的。枚举创建了一种强大的变量类型,它仅限于一些特定名称的集合,例如指令中的操作码或者状态机中的状态名。定义常量的另一种方法是使用参数。但参数需要对每个值进行单独的定义,而枚举类型却能够自动为列表中的每个名称分配不同的数值

  简单的枚举类型声明包含了一个常量名称列表以及一个或多个变量。下例中的方式创建的是一个匿名的枚举类型,它只能用于这个例子中声明的变量。

// 例2.45 一个简单的枚举类型
enum {RED, BLUE, GREEN} color ;

  创建一个署名的枚举类型有利于声明更多新变量,尤其是当这些变量被用作子程序参数或模块端口时。你需要首先创建枚举类型,然后再创建相应的变量。使用内建的 name() 函数,你可以得到枚举变量值对应的字符串,如下例所示:

// 例2.46 枚举类型
// 创建代表 0, 1, 2 的数据类型
typedef enum {INIT, DECODE, IDLE} fsmstate_e ;
fsmstate_e pstate, nstate ; // 声明自定义类型变量 initial begin
case (pstate)
IDLE : nstate = INIT ; // 数据赋值
INIT : nstate = DECODE ;
default : nstate = IDLE ;
endcase
$display ("Next state is %s", nstate.name()) ; // 显示状态的符号名
end

2.12.1 定义枚举值

  枚举值缺省为从 0 开始递增的整数。你可以定义自己的枚举值。如下例所示,使用INIT代表缺省值 0,DECODE代表 2,IDLE代表 3。

// 例2.47 指定枚举值
typedef enum {INIT, DECODE = 2, IDLE} fsmtype_e ;

  如果没有特别指出,枚举类型会被当成 int 类型存储。由于int类型的缺省值是0,所以在给枚举常量赋值时务必小心。在下例中,position 会被初始化为0,这并不是一个合法的 ordinal_e 变量。这种情况是语言本身所规定的,而非工具上的缺陷。因此把0指定给一个枚举常量可以避免这个错误。

// 例2.48 指定枚举值,不正确
typedef enum {FIRST = 1, SECOND, THIRD} ordinal_e ;
ordinal_e position ; // 例2.49 指定枚举值,正确
typedef enum {BAD_O, FIRST = 1, SECOND, THIRD} ordinal_e ;
ordinal_e position ;

2.12.2 枚举类型的子程序

  SystemVerilog提供一些可以遍历枚举类型的函数:

序号 函数名 作用
1 first () 返回第一个枚举常量
2 last () 返回最后一个枚举常量
3 next () 返回下一个枚举常量
4 next (N) 返回以后第N个枚举常量
5 prev () 返回前一个枚举变量
6 prev (N) 返回以前第N个枚举变量

  当到达枚举常量列表的头或尾时,函数 next 和 prev 会自动以环形的方式绕回

  注意,由于环形绕回特性,用 for 循环变量枚举类型中所有成员会导致永远不会退出。此时需要使用 do...while 循环来遍历所有值,如下例所示。

// 例2.50 遍历所有枚举成员
typedef enum {RED, BLUE, GREEN} color_e ;
color_e color ;
color = color.first ;
do
begin
$display ("Color = %0d/%s", color, color.name) ;
color = color.next ;
end
while (color != color.first) ; // 环形绕回时即完成循环

2.12.3 枚举类型的转换

  枚举类型的缺省类型位双状态 int。可以使用简单的赋值表达式把枚举变量的值直接赋给非枚举变量如 int。但SystemVerilog不允许在没有显式类型的情况下把整型变量赋给枚举变量。SystemVerilog要求显式类型转换的目的在于让你意识到可能存在的数值越界情况。

// 例2.51 整型和枚举类型之间相互赋值
typedef enum {RED, BLUE, GREEN} COLOR_E ;
COLOR_E color, c2 ;
int c ; initial begin
color = BLUE ; // 赋一个已知的合法值
c = color ; // 将枚举类型转换成整型 (1)
c++ ; // 整型递增 (2)
if (!$cast(color, c)) // 将整型显式转换回枚举类型
$display ("Cast failed for c = %0d", c) ;
$display ("Color is %0d /%s", color, color.name) ;
c++ ; // 3对于枚举类型已经越界
c2 = COLOR_E '(c) ; // 静态类型转换,不做类型检查
$display ("c2 is %0d/%s", c2, c2.name) ;
end

  在上例中,$cast 被当成函数进行调用,目的在于把其右边的值赋给左边的量。如果赋值成功,$cast() 返回 1。如果因为数值越界而导致失败,则不进行任何赋值,函数返回 0。如果把 $cast 当成任务使用并且操作失败,则SystemVerilog会打印出错误信息。

2.13 常量

  SystemVerilog中有好几种类型的常量。

  1. 文本宏。它的好处是:宏具有全局作用范围并且可以用于位段和类型定义。它的缺点同样是因为宏具有全局作用范围,在你只需要一个局部常量时可能会引发冲突。此外,宏定义需要使用“`”符号,这样它才能被编译器识别和扩展。
  2. parameter。Verilog中的parameter并没有严格的类型界定,而且其作用范围仅限于单个模块里。
  3. const修饰符。允许在变量声明时对其进行初始化,但不能在过程代码中改变其值。

  在SystemVerilog中,参数可以在程序包里声明,因此可以在多个模块中共同使用。这种方式可以替换掉Verilog中很多用来表示常量的宏。你可以用 typedef 来替换掉那些单调乏味的宏。

// 例2.52 const 变量的声明
initial begin
const byte colon = ":" ;
...
end

2.14 字符串

  SystemVerilog新增的string类型可以用来保存长度可变的字符串。单个字符是byte类型。长度为N的字符串中,元素编号从0到N-1。注意,和C语言不同的是,字符串的结尾并不带标识符null,所有尝试使用字符“\0”的操作都会被忽略。字符串使用动态的存储方式,所以不用担心存储空间会被全部用完。

  字符串相关的几种操作:

操作 说明
getc (N) 返回位置N上的字节
toupper 返回一个所有字符大写的字符串
tolower 返回一个所有字符小写的字符串
大括号 { } 用于串接字符
putc(M, C) 把字节C写到字符串的M位上,M必须介于0和len所给出的长度之间
substr (start, end) 提取从位置 start 到 end 之间的所有字符
// 例2.53 字符串方法
string s ;
initial begin
s = "IEEE " ;
$display (s.getc (0)) ; // 显示:73 (‘I’)
$display (s.tolower ()) ; // 显示:ieee s.putc (s.len() -1, "-"); // 将空格变成‘-’
s = {s, "P1800"} ; // "IEEE-P1800 $display (s.substr (2, 5)) ; // 显示:EE-P //创建临时字符串,注意格式
my_log ($psprintf ("%s %5d", s, 42)) ;
end task my_log (string message) ;
// 把信息打印到日志里
$display ("@%0t: %s", $time, message) ;
endtask

  在上例中,函数 $psprintf () 替代了Verilog-2001 中的函数 $sformat () 。这个新函数返回一个格式化的临时字符串,并且可以直接传递给其他子程序。这样你就可以不用定义新的临时字符串并在格式化语句与函数调用过程中传递这个字符串。

2.15 表达式位宽

  在Verilog中,表达式的位宽是造成行为不可预测的主要源头之一。下例使用4种不同方式实现 1+1。

  方式A,使用2个单比特变量,在这种精度下得到 1+1 = 0。

  方式B,由于赋值表达式的左边有一个8比特的变量,所以其精度是8比特,得到的结果是 1+1 = 2。

  方式C,采用一个哑元常数强迫SystemVerilog使用2比特精度。

  方式D,第一个值在转换符的作用下被指定为2比特的值,所以 1+1 = 2。

// 例2.54 表达式位宽依赖于上下文
bit [7:0] b8 ;
bit one = 1'b1 ; // 单比特
$displayb (one + one) ; // A;1+1=0 b8 = one + one ; // B:1+1=2
$displayb (b8) ; $displayb (one + one + 2'b0) ; // C:1+1=2,使用了常量 $displayb (2'(one) + one) ; // D:1+1=2,使用了静态类型转换

最新文章

  1. 3D坦克大战游戏源码
  2. NYOJ题目842整除的尾数
  3. 【转】linux中的sed命令
  4. G - 小希的迷宫(并查集)
  5. trimpath javascript的学习
  6. 从开发到部署会用到的 Docker 命令
  7. [LeetCode] Construct String from Binary Tree 根据二叉树创建字符串
  8. SVN系列操作(一)
  9. HTTPS通信原理
  10. Python里的拷贝
  11. 在eclipse 导入简单的Android studio 简单项目
  12. eclipse安装maven3
  13. 初步认识session
  14. Python super执行基类的构造方法
  15. imx6 android SD卡启动
  16. Lua的协同程序初探
  17. ERROR 1682 (HY000)
  18. su切换用户报错cannot set user id: Resource temporarily unavailable
  19. CodeChef - UASEQ Chef and sequence
  20. Java时间类总结

热门文章

  1. [SpringCloud教程]3. Eureka服务注册中心集成
  2. 别再面向 for 循环编程了,Spring 自带的观察者模式就很香!
  3. [深搜]C. 【例题3】虫食算
  4. Spring Security Oauth2 认证(获取token/刷新token)流程(password模式)
  5. Webpack的基本配置和打包与介绍(二)
  6. OO第四单元总结暨期末总结
  7. Android+Spring Boot 选择+上传+下载文件
  8. Selenium3自动化测试【17】元素定位之Link定位
  9. 软件篇-01-为Jetson TX2扫清科研的障碍
  10. Vue学习(三)-Vue-router路由的简单使用