1. 数据类型
1.1 内建数据类型
相比于Verilog将寄存器(register) 类型reg和线网 (net) 类型,例如wire区分地如此清楚,在SV中新引入了一个数据类型logic。它们的区分和联系在于:
- Verilog作为硬件描述语言,倾向于设计人员自身懂得所描述的电路中哪些变量应该被实现为寄存器,而哪些变量应该被实现为线网类型。这不但有利于后端综合工具,也更便于阅读和理解。
- SV作为侧重于验证的语言,并不十分关切logic对应的逻辑应该被综合为寄存器还是线网,因为logic被使用的场景如果是验证环境,那么它只会作为单纯的变量进行赋值操作,而这些变量也只属于软件环境构建
- logic被推出的另外一个原因也是为了方便验证人员驱动和连接硬件模块、而省去考虑究竟该使用reg还是wire的精力。这既节省了时间,也避免了出错的可能
与logic相对应的是bit类型,它们均可以构建矢量类型(vector)而它们的区别在于:
- logic为四值逻辑,即可以表示0、1、x(0/1)、z(高阻态)
- bit为二值逻辑,只可以表示0和1.
SV为什么在已经有了四值逻辑的基础上还要再引入二值逻辑呢? - 这是因为,SV在一开始设计的时候,就期望将硬件的世界与软件的世界分离开。在这里,硬件的世界指的就是硬件设计,所以四值逻辑属于那里,而软件的世界即验证环境,更多的是二值逻辑
我们将四值逻辑的类型和二值逻辑的类型分别摘列出来:
- 四值逻辑类型: integer、logic、 reg、net-type ( 例如wire、tri )
- 二值逻辑类型: byte、shortint、int、longint、 bit
如果按照有符号和无符号的类型进行划分,那么可以将常见的变量类型划分为: - 有符号类型: byte、shortint、int、 longint、integer
- 无符号类型: bit、logic、 reg、net-type (例如wire、 tri)
- 在遇到这些变量类型时,应该注意它们的逻辑类型和符号类型,因为在变量运算中,应该尽量避免两种不一致的变量进行操作,而导致意外的错误。譬如从下面的例子里,我们可以看到有符号变量和无符号变量混用的运算结果会出乎意料:
- 通过上面的例子我们可以发现,在编码时一定要注意操作符左右两侧的符号类型是否一致,如果不一致,应该首先将其转换为同一类型再进行运算。
- 对于转换方式,我们已经上面已经展示了一种转换方式一一静态转换,即需要在转换的表达式前加上单引号即可,而该方式并不会对转换值做检查。如果发生转换失败,我们也无从得知,所以与之对应的动态转换 $cast(tgt,src) 也被经常运用到转换操作中
- 静态转换和动态转换均需要操作符号或者系统函数介入,我们统称为显式转换
- 而不需要进行转换的一些操作,我们称之为隐式转换
1.1.1 内建数据类型(总结)
在不同数据类型进行操作时应该注意变量的:
- 逻辑数值类型
- 符号类型
- 适量位宽
1.2 软件常用类型(定宽数组)
数组声明
多维数组声明和使用
初始化和赋值
存储空间考量
基本数组操作 for 和 foreach循环
基本数组操作 赋值和比较
1.3 软件常用类型(动态数组)
- 定宽数组类型的宽度在编译时就确定了,但如果像在程序运行时再确定数组的宽度就需要使用**[动态数组]**了
- 动态数组最大的特点即是可以在仿真运行时灵活调节数组的大小即存储量
- 动态数字在一开始声明时,需要利用 [ ] 来声明,而数组此时是空的,即0容量。其后,需要使用 new [ ] 来分配空间,在方括号中传递数组的宽度。
- 此外,也可以在调用 new[ ] 时将数组名也一并传递,将已有数组的值复制到新数组中。
1.4 软件常用类型(队列)
- 队列结合了链表和数组的优点,可以在它的任何地方添加或删除元素,并且通过索引实现对任一元素的访问。
- 队列的声明是使用带有美元符号的下标:[$],队列元素的标号从 0 到 $
- 队列不需要 new[ ] 去创建空间,你只需要使用队列的方法为其增减元素,一开始其空间为0。
- 队列的一个简单使用即是通过其自带方法 push_back () 和 pop_front() 的结合来实现 FIFO 的用法
insert操作
- 如果队列定义为int类型,那么队列里的元素 也只能插入 int 型
- 队列插入队列
1.5 软件常用类型(关联数组)
- 如果你只是偶尔需要创建一个大容量的数组,那么动态数组就足够了,但是如果你需要一个超大容量的呢? 动态数组的限制在于其存储空间在一开始创建时即被固定下来,那么对于超大容量的数组这种方式无疑存在着浪费,因为很有可能该大容量的数组中有相当多的数据不会被存储和访问。
- 【关联数组】可以用来保存稀疏矩阵的元素。当你对一个非常大的地址空间寻址时,该数组只为实际写入的元素分配空间,这种实现方法所需要的空间比定宽或动态数组所占用的空间要小得多
- 此外,关联数 组有其它灵活的应用,在其它软件语言也有类似的数据存储结构,被称之为哈希 (Hash) 或者词典 (Dictionary) ,可以灵活赋予键值 (key) 和数值 (value) 。
1.6 软件常用类型(结构体)
- Verilog的最大缺陷之一是没有数据结构,在 sv 中可以使用 struct 语句创建结构,跟 c 语言类似。
- 不过 struct 的功能少,它只是一个数据的集合,其通常的使用的方式是将若干相关的变量组合到一个 struct 结构定义中
- struct 伴随 typedef 可以用来创建新的类型,并利用新类型来声明更多变量。
1.7 软件常用类型(枚举类型)
- 规范的操作码和指令,例如ADD , WRITE, IDLE等有利于代码的编写和维护,它比直接使用 'h01 这样的常量 使用起来可读性和可维护性更好
- 枚举类型 **enum 经常和 typedef 搭配 **使用,由此便于用户自定义枚举类型的共享使用
- 枚举类型的出现保证了一些非期望值的出现,降低的设计风险
1.8 软件常用类型(字符串)
- 感谢字符串 string 的出现,让我们可以脱离 VHDL 和 verilog 这种“荒蛮时代”,可以完整想用 string 类型带来的便利(尽管它与其它软件语言的字符串功能使用仍然有差距)
- 所有与字符串相关的处理,都请使月 string 来保存和处理
- 与字符串处理相关的还包括字符串格式化函数即如 何形成一个你想要的字符串句子呢? 可以使用sv 系统方法 $sformatf()
- 如果你只需要将它打印输出,那么就 使用 $dispay() 吧
2. 过程快和方法
2.1 硬件过程块
initial 和 always
- 在SV中同学们首先需要清楚哪些语句应该被放置于硬件世界哪些程序应该被放置于软件世界
- 怎么来区分硬件世界和软件世界呢? 我们先引由出一个概念域(scope) 。为了区分硬件设计、软件世界,我们将定义的软件变量或者例化的硬件其所在的空间称之为 域
- 因此,module / endmodule,interface / endinterface 可以被视为硬件世界,program/ endprogram和class / endclass可以被视为软件世界。掌握了这一清晰的概念,有助于我们接下来分析 initial 和 always 的使用域
always
- always是为了描述硬件的行为,而在使用时需要注意哪种使用方式是时序电路描述,哪种使用方式是组合电路描述
- always中的**@(event…)敏感列表** 是为了模拟硬件信号的触发行为,同学们需要正确对标硬件行为和always过程块描述。需要理解硬件行为的核心要素有哪些?
- 所以说,always过程块是用来描述硬件时序电路和组合电路的正确打开方式,因此只可以在module或者interface中使用
initial
- initial从名字也可以看得出来,与always在执行路径上有明显区别,即initial非常符合软件的执行方式,即只执行一次
- initial 和 always 一样,无法被延迟执行,即在仿真一开始它们都会同时执行,而不同 initial 和 always 之间在执行顺序上是没有顺序可言的,因此小白们不应该将它们在代码中的前后顺序与它们的执行顺序画上等号
- initial从其执行路径的属性来看,它不应该存在于硬件设计代码中,它本身不可综合,对于描述电路没有任何帮助。
- initial就是为了测试而生的,由于测试需要按照时间顺序的习惯即软件方式来完成,所以initial便可以实现这一要求
- 在Verilog时代,所有的测试语句都可以被放置在initial中,为了便于统一管理测试顺序,建议将有关测试语句都放置在同一个initial过程块中
- initial 过程块可以在 module、interface 和 program 使用
- 对于过程块的书写方式,请记住用 begin…end 其作用域 包 住。这一建议同样适用于稍后提到的控制语句、循环语句等等,初学者可以将其对应于C语言中的花括号{},方便记忆。
2.2 软件方法(函数function)
SV函数定义同C语言非常类似
- 可以在参数列表中指定输入参数 (input) 、输出参数(output)、输入输出参数 (inout) 或者引用参数(ref)
- 可以返回数值或者不返回数值 (void)
除此之外,function还有以下的属性
- 默认的数据类型是为logic,例如 input [7:0] addr。
- 数组可以作为形式参数传递
- function可以返回或者不返回结果,如果返回即需要用关键词return, 如果不返回则应该在声明function时采用void function()
- 只有数据变量可以在形式参数列表中被声明为ref类型,而线网类型则不能被声明为ref类型。
- 在使用ref时,有时候为了保护数据对象只被读取不被写入,可以通过const的方式来限定ref声明的参数。
- 在声明参数时,可以给入默认数值,例如 input [7:0] addr = 0,同时在调用时如果省略该参数的传递,那么默认值即会被传递给function.
2.2 软件方法(任务 task )
任务相比于函数要更加灵活,且以下不同点
- task无法通过return返回结果,因此只能通过 output、inout 或者ref 的参数来返回
- task内可以置入耗时语句,而function则不能。常见的耗时语句包括 @event、wait event、# delay等
通过上面的比较,我们对function和task建议的使用方式是:
- 对于初学者,傻瓜式用法即全部采用task来定义方法,因为它可以内置常用的耗时语句
- 对于有经验的使用者,请今后对这两种方法类型加以区别,在非耗时方法定义时使用function,在内置耗时语句时使用 task。这么做的好处是在遇到了这两种方法定义时,就可以知道 function 只能运用于纯粹的数字或者逻辑运算,而 task 则可能会被运用于需要耗时的信号采样或者驱动场景。
- 如果要调用function,则使用function和task均可对其调用;而如果要调用task,我们建议使用task来调用,这是因为如果被调用的task内置有耗时语句,则外部调用它的方法类型必须为task
2.2 软件方法(变量生命周期)
- 在SV中,我们将数据的生命周期分为动态(automatic) 和静态(static)
- 局部变量的生命周期同其所在域共存亡,例如 function / task 中的临时变量,在其方法调用结束后,临时变量的生命也将终结所以它们是动态生命周期。
- 全局变量即伴随着程序执行开始到结束一直存在,例如 module 中的变量默认情况下全部为全局变量,用户也可理解为module中的变量由于在模拟硬件信号,所以它们是静态生命周期。
- 如果数据变量被声明为automatic,那么在进入该进程/方法后automatic变量会被创建,而在离开该进程/方法后automatic变量会被销毁。而static变量在仿真开始时即会被创建,而在进程/方法执行过程中,自身不会被销毁,且可以被多个进程和方法所共享。
-
上面的三个function被定义在了 module 内,分别被声明为了automatic、static 和 默认类型
-
对于 automatic 方法,其内部的所有变量默认也是automatic,即伴随automatic方法的生命周期建立和销毁
-
对于 static 方法,其内部的所有变量默认也是static类型
-
对于automatic或者static方法,用户可以对其内部定义的变量做单个声明,使其类型被显式声明为automatic或者static。
-
对于 static 变量,用户在声明变量时应该同时对其做初始化,而初始化只会伴随它的生命周期发生一次,并不会随着方法调用被多次初始化。
-
在module、program、interface、 task 和 function 之外 声明的变量拥有静态的生命周期,即存在于整个仿真阶段,这同C定义的静态变量一致。
-
在module、interface 和 program 内部声明,目在task、process或者function外部声明的变量也是static变量,且作用域在该块中。
-
在 module 、program 和 interface 中定义的 task、function默认都是static类型
-
在过程块中 (task、function、process) 定义的变量均跟随它的作用域,即过程块的类型。如果过程块为static,则它们也默认为static,反之亦然。这些变量也可以由用户显式声明为automatic或者static
-
为了使得在过程块中声明的变量有统一默认的生命周期,可以在定义module、interface、 package或者program时,通过限定词 automatic 或者 static 来区分。对于上述程序块默认的生命周期类型为static。
3. 设计例化和连接
3.1 设计例化(模块定义)
- module为硬件域,在定义时需要标注方向、位宽和端口名
3.2 设计例化(模块例化)
- 在上层例化底层模块,或者TB例化DUT时均需要完成模块例化。例化时需要注意模块名、参数例化传递 (如果有) 、例化名和端口例化对应。
3.3 设计例化(模块连接)
- 在 testbench 中的连接 (connection) 指的是:有硬件模块参与作为信号驱动方 (drive) 或者负载方 (load)
- TB中常见的连接有两个硬件模块之间的连接,警如实例A与实例B的连接,可由logic类型完成连接;如果是硬件模块与TB中发送数据激励的连接,则需要考虑数据激励一端如何正确产生数据并发送至DUT一侧,同时数据激励一端也需要对DUT反馈的信号做出正确响应