本章目录:
1. 数据类型
1.1 四值变量
四值变量:(0、1、x、z)四种状态
四值逻辑类型:integer、logic、reg、net-type(如 wire、tri)、time(64bit的无符号整数);
SV 并不太常用变量类型是 wire(assign 语句中)还是 reg(initial 和 always 语句中)。logic 用的比较多。可以被连续赋值语句驱动,可用在 assign、initial、always 语句中。
1.2 二值变量
二值变量:(0、1)两种状态
二值逻辑类型:bit、byte(8位)、shortint(16位)、int(32位)、longint(64位)、real(双精度浮点数)。
1.3 有符号与无符号
有符号类型:byte、shortint、int、longint、integer。
无符号类型:bit、logic、reg、net-type(如 wire、tri)、time。
1.4 四值变量与二值变量的特性
四值变量的默认初始值为 x,二值变量的默认初始值为 0,在 initial 中可以直接使用~clk 变 成 1,但是如果是 logic,必须设置初值为 0、或者 1。将四值变量赋值给二值变量,x 和 z 状态会转变为 0。
1.5 转换方式
对于转换方式,可以分为隐式转换和显式转换。显式转换又可以分为静态转换和动态转换
静态转换:unsigned’(signed);注意单引号。
动态转换:$cast(tgt,src)
2. Logic 类型
在 SystemVerilog 中,可以在过去 verilog 中用 reg 型或是 wire 型的地方用 logic 型来代替。
实际上 logic 是对 reg 数据类型的改进,使得它除了作为一个变量之外,还可以被连续赋值、门单元和模块所驱动,显然,logic 是一个更合适的名字。抛去 inout 类型外,其余所有的变量都可以声明为 logic 类型,不用区分线网和变量 reg 之间的不同点。
值得注意的是,logic 不能有多个结构性的驱动,也就是说,logic 不允许使用多于一次的持续赋值语句和输出端口连接的给同一变量赋值。这是因为没有类似于线网的多重驱动变量的定论。因此,假如你通过这些方式给一个变量赋过值,你将不能再用过程赋值语句给变量赋值。
3. 数组
对于前面的一个变量名代表一个数据,如果我们有很多数据要表示就会很麻烦,这样就可以用一个数组来储存,且对数组进行操作可以快速处理多个数据,数组分为:定宽数组、动态数组、队列、关联数组、链表。
3.1 定宽数组
指定了数组宽度的数组
3.1.1 一维定宽数组的声明
int a[8];
int b[0:7];
在变量名后指定数组宽度可创建一维数组,由于数组序号从0开始计算,所以[0:7]的写法和直接写[8]在SV中都是表示创建一个一维的数组,且数组中元素个数为8即a[0]、a[1] … a[7]
3.1.2 多维定宽数组的声明
上图表示创建了二维数组,其大小为8行4列,二者写法等同,注意区分 数组宽度是[0:n] 并写在变量名后,数据位宽是[n:0]写在变量前
3.1.3 合并数组与非合并数组
从内存存放的角度考虑,其数据存放在计算机的内存时可以连续存放,也可以不连续存放,如(bit [7:0] a [4] )为数组宽度为4,数据位宽为8的数组,如下图:
构成了非合并数组(不连续存放)和非合并数组(连续存放),很多SV仿真器在存放数组元素时使用32比特(位)的字边界,所以byte、shortint、int都是存放在一个字中,可以看到上图有颜色的数据部分占了低8位,灰色代表剩下的高位没有数据存放而空着,显然非合并数组占用的内存更多。
3.1.3.1 非合并数组的声明
当索引(数组宽度定义)位于数组名后方时,定义的就是非合并数组,其数据在内存是不连续存放的。
3.1.3.2 合并数组的声明
bit [3:0][7:0] a;
把索引移到最前面时,定义的就是合并数组,其数据在内存是连续存放的,此时的[3:0]不能简写成[4],存放的数据从右到左为a[0]、a[1]、a[2]、a[3]。
3.1.3.3 合并数组和非合并数组可以混合使用
bit [3:0][7:0] a[3];
表示有3个合并元素的非合并数组,即一个含3个元素的非合并数组 a,a中的每个元素都形如合并数组,如下图:
3.1.3.4 合并数组与非合并数组的选择
由于非合并数组是不连续储存,如果要将这个数组内全部位数都赋值为一个值如 0 的时候就必须用{0,0,…}的形式,而合并数组可以直接写成(a = 0),具体数组操作在下面讲,其余直接引用绿皮书的说明:合并数组与非合并数组的选择——当需要和标量进行相互转换时,使用合并数组会非常方便;任何数组类型都可以合并;如果需要等待数组中的变化,则必须使用合并数组。当测试平台需要通过存储器数据的变化来唤醒时,你会想到使用@操作符,但是该操作符只能用于标量或合并数组.
3.1.4 数组的初始化
数组可以在定义时赋值(初始化),也可以在定义后赋值。
3.1.4.1 非合并数组的赋值
蓝色字代表注释的内容不参与仿真时不参与运算,注释用/* */和//表示,后面的赋值要写成 '{ , , } 的形式,注意大括号前有一个 ’ 的标点,如果全赋值成1的时候不能直接写成(a = 1),因为非合并数组内存存放是不连续的,此时打印 a 的值的话仿真结果报错,可以按(a[0]=1,a[1]=1 …)这种格式赋值,所以大括号内要用逗号隔开分别赋值,上图的输出结果为:
其余例子:
3.1.4.2 合并数组的赋值
对于合并数组,多了可以写成(a = 1)将数组a的3个元素一次性赋值成1的方式,上图将{0,1,2}赋值给了合并数组a,输出结果为:
对于{0,1,2}的赋值,结合图7,可以看出,{0,1,2}依次对应内存储存中从左到右的a[2]、a[1]、a[0],与非合并数组内存储存中从上到下的a[0]、a[1]、a[2]顺序相反,而此时 a 的值打印显示也是由3个8位数组成24位数。
3.1.5 基本的数组操作
3.1.5.1 for和foreeach
操作数组最常见的方式是使用for和foreach循环,见下例:
定义了元素个数为4(数组位宽为4)、每个元素为8位宽的一维定宽数组(非合并),对数组a采用for循环赋值,$size(a)为a的数组位宽,即 i 的值小于4就可以一直循环,每次循环令(a[i] = i),定义(int i = 0),这里 i 的类型要定义否则仿真报错,i 的初始值不定义则默认为0。接下来的foreach循环意思为遍历,只要指定了数组名和后面方括号中的索引值(即a[]),SV就会遍历数组中的元素,相当于省略了for循环()里的条件,是简便的写法。这里用foreach循环使得数组 b 的元素等于数组 a 对应元素值的两倍。
for和foreach循环内的循环变量可以是任意字母,如上面还用了k,n,只是习惯for循环内用 i ,foreach里用 j ,注意此时的 i 或 j 或其他字母是循环内自动声明的变量,不会影响到循环外的代码,如果自己在外面定义个 i 或 j 并赋初始值的话,在本次循环内也不起作用,以下是仿真结果:
注意:for和 if 等条件判断语句后面不加分号(;),因为分号代表这一行代码结束,加了会导致条件判断不生效。
二维数组的话用foreach遍历会简单许多,如下图:
注意:在foreach的()内写的是 [i,j] ,其中i,j表示两个维度的循环,而在下面的赋值和打印操作中则必须写成 a[i][j] 的形式,否则仿真报错。
3.1.5.2 数组的复制和比较
两个数组之间可以进行赋值和比较,如下例:
定义了两个数组并赋值,a 赋值{0,1,2,3},b赋值{0,0,0,0},用 if 语句判断数组 a 和 b 中每个元素是否相等,相等则返回1执行if下的语句,否则返回0执行else下的语句,注意大于一行命令的时候,if 和else后面都要用begin end包上(类似C语言的{}),第一个 if 语句肯定是不相等,之后把数组 a 的值赋给数组 b ,此时第二个 if 语句判断为相等,仿真结果如下:
3.2 动态数组
对于定宽数组我们指定了数组宽度,其宽度在编译时就确定了,而SV提供了动态数组类型,可以在仿真时分配空间或调整宽度,这样在仿真中就可以使用最小的存储量。
3.2.1 动态数组的声明
bit a[];
bit [7:0] b[];
与定宽数组的声明基本类似,只是原本的 [ ] 内写的数组宽度现在不写,而在下面用 new 函数分配空间时在传递宽度,动态数组在声明后必须用 new 函数分配内存空间,因为声明时没写宽度相当于数组是空的,告诉系统我要用这个数组,所以此时数组是不占内存的,在通过 new 函数传递指定宽度后,系统才会对应的分配内存空间,如下面例子:
new[ ] 的 [ ]里面的数字就是动态数组宽度,例中的(.delete)是调用了动态数组内建的函数,例如 delete 和 size,分别代表删除元素和返回数组宽度
在声明动态数组时也可以直接将定宽数组赋值给它,此时SV会自动调用构造符 new[ ] 来分配空间并复制数值给它,且数组宽度对应相应的赋值数组,如下例:
3.3 队列
队列是SV引入的一种新的数据类型,它结合了链表和数组的优点。队列与链表相似,可以动态的在队列的任何地方增加或删除元素,这类操作在性能上的损失比动态数组小得多,因为动态数组要分配新的数组并复制所有元素的值。队列与数组相似,可以通过索引实现对任意元素的访问,而不需像链表那样去遍历目标元素之前的所有元素。
3.3.1 队列的声明
bit b[$];
bit [7:0] c[$];
队列是在 [ ] 中写上 $ 来声明,注意后续不要对队列使用构造符new[ ]
3.3.2 队列的具体用例
队列在声明时可进行初始化,注意赋值时在 { } 前不加符号 ',而定宽数组在赋值时是写出 '{} ,利用队列内建的函数可以做到任意位置插入和删除元素、取出任意位置数据等操作,队列的索引从0开始,(.push_front)和(.pop_back)分别为队列前面插入和末尾取出数据,使得队列起到FIFO(缓冲)的作用,一边给数据一边使用,如果两边速度不一致了就可以让数据在队列中存着,使用( q = {}; )也可以删除整个队列(即元素取空)。
队列中的元素是连续存放的,所以在队列的前面或后面存取数据非常方便。无论队列有多大,这种操作所耗费的时间都是一样的。在队列中间增加或删除元素需要对已存在的数据进行搬移以便腾出空间,相应操作所耗费的时间会随着队列的大小线性增加;也可以把定宽或动态数组的值复制给队列。
3.4 关联数组
对于一个大容量数组,可以用动态数组创建,但如果要创建一个超大容量数组(比如模拟大容量储存空间),用动态数组创建的话所占用的内存空间就被定下来了,且在内存中是连续的,如果我们只写了一小部分数据来测试的话,那么大量的内存空间就是空着造成浪费,而通过关联数组来创建的话只会为实际写入的数据分配空间,且关联数组的索引可以是任意类型而不局限于其他数组只能用整型的数字来索引,所以非常灵活,缺点是其内存是不连续的,在进行索引时没有其他内存连续的数组快,故适合用来保存稀疏矩阵的元素。在其他软件语言中也有类似的数据存储结构,被称哈希(Hash)或词典(Dictionary),可以灵活赋予键值(key)和数值(value)。
关联数组的声明就是将 [ ] 内的索引从数字改成数据类型,如 [int] 、[bit] 等,也可以对索引的数据类型加上指定位宽,如 [bit [64:0] ] ,如下例:
a 和 b 分别定义为数组宽度为64的关联数组和定宽数组,对其进行初始化,(idx << 1)代表idx的二进制数左移一位,如从001到010到100,其数值相当于十进制数(idx * 2),所以idx的值变成1、2、4、8、16,一共重复执行5次,并将其赋值给对应的数组元素,完成后打印此时 a 和 b 的数组宽度并用foreach遍历一次数组所有元素,下图为结果:
可以看到我们只赋了5个值,所以关联数组的宽度显示为5,而定宽数组则是64,没有赋值的数组元素都为默认值0。另外关联数组的索引还可以是字符串等任意类型,如下图所示:
上图声明了一个关联数组,其索引为字符串类型,对数组进行赋值并分别将索引以字符串格式、十进制、二进制显示打印出对应的值,如下图:
可以看出关联数组可以通过字符串类型的索引获取对应的赋的值,且字符串是以ASCII码的形式储存,而其他数组类型要获取数组内元素的值只能用数字整型进行索引如 a[0]、a[1]…等,故关联数组优点在于索引灵活,大容量存储小部分数据时不浪费内存空间,学过paython的话可以将其看作为字典类型。下图为关联数组的一些内建函数:
3.5 typedef
当我们需要多次声明某个类型时可以用 typedef 来给这个类型自定义个名称,下次再声明的时候只要换个名字就行,如下图:
注释里的内容和下面的两行代码等价,用 typedef 创建为用户自定义类型后可以省去一些代码,typedef 和下面说到的枚举数据类型(enum)经常搭配使用。
3.6 枚举类型
枚举类型等于Verilog中用 parameter 或 `define 来定义一组常数或者宏名。对于某些常量如 2bit 的(00)、(01)、(10)、(11),或者更高位的如 4bit 的(0000)、(0001)、(0010)… 我们想用名称来表示这些对应的值,如(red = 00,bule = 01)等,这样在赋值的时候可以用(red、bule …)等有意义的名称来代替前面这些数值起到增加代码可读性的作用,枚举类型通常在状态机中使用的多.
3.6.1 枚举类型的定义
枚举类型 enum 通常搭配用户自定义类型 typedef 使用,上图通过自定义创建了一个枚举类型,其内包含了四个名称,枚举类型在创建后其内部名称对应的值就确定了,如上图定义的是 bit 类型,那么每个名称的枚举值依次是(OFF = 00)、(RED = 01)、(YELLOW = 10)、(GREEN = 11)、每个后面的名称对应的值都是上一个值 +1 ,如果不写 bit 类型的话默认是 int 类型, { } 里的名称也可以直接赋值比如定义成 int 类型的话可以{OFF = 3,RED,YELLOW=8,GREEN},那么此时(RED = 3+1=9)、(GREEN = 8+1=9),注意如果出现相同的值时仿真会报错,如果给定的位宽排列小于名称个数也会错误,如上图 2bit 有四种组合正好对应四个名称,再加一个名称的话就会错误
3.6.2 枚举类型的使用
上图通过用户自定义类型 typedef 创建了一个枚举类型,然后定义了两个枚举类型 now_state 和 next_state 代表当前状态和下个状态,我们知道每个名称的枚举值依次是:
(OFF = 00)、(RED = 01)、(YELLOW = 10)、(GREEN = 11)
先把OFF的枚举值赋值给 now_state (即00),接着用case语句:
当 now_state 为——OFF时, next_state 为——RED
当 now_state 为——RED时, next_state 为——YELLOW
当 now_state 为——YELLOW时, next_state 为——GREEN
当 now_state 为——GREEN时, next_state 为——RED
通过repeat重复5次, now_state 初始被赋值为OFF,则会重复(关、红、黄、绿、红)5个状态,下面是仿真结果:
3.6.3 枚举类型的内建函数
3.6.4 枚举类型用 do while 遍历枚举成员
枚举类型的缺省类型为双状态 int,可以使用简单的赋值表达式把枚举变量的值直接赋值给非枚举变量如int。但SV不允许在没有进行显示类型转换的条件下把整型变量赋值给枚举变量。SV要求显示转换的目的在于让你意识到可能存在的数值越界情况.
3.6.5 枚举类型和整型赋值
$cast 被当成函数进行调用,目的在于把其右边的值赋给左边的量。如果赋值成功,$cast返回1;如果因为数值越界而导致赋值失败,则返回0。例子中3对于枚举已经越界,这种强行赋值的方式不推荐。
3.7 结构体(struct)
struct 为定义一个结构体,与数组不同的是,结构体可以包含多种数据类型,一般与 typedef 配合使用。可以在声明或者过程赋值语句中把多个值赋给一个结构体,赋值时要把数值放在带单引号的大括号中,如下图:
结构体一般情况下是非合并的,此时关联结构体中的元素是不连续存放的,合并结构体需要用到关键字 packed,合并结构是以连续比特集的方式存放的,中间没有空闲的空间,如下图:
在合并结构和非合并结构之间进行选择 ——如果对结构的操作很频繁,例如经常对整个结构体进行复制,那么使用合并结构效率会很高;如果操作经常是针对结构内的个体成员而非整体,那么应该使用非合并结构.
3.8 字符串
下集预告
学习 过程快和方法 等相关的知识。
希望我可以坚持下去,不断学习!如果觉得有用,希望关注点赞评论,不迷路!!!
声明:仅供学习参考,侵删!!!
参考文献
写的很好,大佬的笔记,供参考
相关PPT,如有需要,可以滴滴我!!!
声明
本人所有系列的文章,仅供学习,不可商用,如有侵权,请告知,立删!!!
本人主要是记录学习过程,以供自己回头复习,再就是提供给后人参考,不喜勿喷!!!
如果觉得对你有用的话,记得收藏+评论!!!