type
status
date
slug
summary
tags
category
Property
Nov 30, 2023 02:47 PM
icon
password
属性
属性 1
描述
Origin
URL
1.SystemVerilog TestBench(SVTB)入门
一、专栏概述
专栏大纲
- 专栏涵盖数字集成电路的功能验证流程和技术
- 逻辑仿真,激励生成,结果检查,覆盖率,调试技术,断言技术
- 通常验证的 “结果检查” 不去检查时序,仅仅检查逻辑功能。因为如果要去检查时序,随着 DUT 时序的变化,环境的时序也会去变化。
- 通常在断言中去检查时序
- 应用所学验证知识解决数字电路系统中的功能验证问题
预备知识
- 熟悉 Verilog 或 VHDL 硬件描述语言
- Linux 基础
- gvim 基础
SystemVerilog 大纲
- 1、验证计划和验证环境
- Verification Plan 在项目开始是非常重要的!
- 2、SystemVerilog 语言的验证属性
- 3、SystemVerilog Testbench
- 4、接口 Interface
- 5、面向对象编程 OOP
- 6、随机化 Randomization
- 7、线程 Threads
- 8、内部通信 Interprocess Communication
- 9、功能验证 Functional Coverage
- 10、断言 Assertions
参考书籍
- 1、SystemVerilog for Verification, third edition, Springer 2012.
- 2、SystemVerilog Assertions, Springer 2005
注:仅仅作为工具书,不要啃书,浪费精力,多去用!
二、SystemVerilog TestBench 功能
- 产生激励
- 将激励分成了两层,一层是功能(源头),另一层是 Driver。
- 比如要产生报文,本层决定了报文 Head 是产生
55AA
还是0033
? - 比如运输苹果,本层只负责打包!
- 驱动激励
- Driver 专门去产生跟 DUT 接口相关的信号时序
- Driver 相当于物理通道
- 比如运输苹果,本层只负责运输,至于选择何种运输方式跟之前的打包是无关的!(可重用性更高)
- 采样响应
- 检查响应的正确性
- 根据验证目标评估验证进度
- 收集覆盖率(代码 - 行、条件、FSM、Toggle;功能),来看看验证完备性
三、基于 EDA 的数字系统搞设计(SoC Design based on EDA)
- 超大规模 SoC 系统芯片设计依赖于:电子设计自动化工具(Electronic Design Automation,EDA)
- 基于 CMOS 搭建电路无法制作大规模,所以就有了工具
- RTL –(MAP) –> Netlist –(Implentation)–> GDSII
- 三大 EDA 厂商:Synopsys、Cadence、Mentor
- 数字逻辑仿真工具:VCS、IES、Questasim
- 数字逻辑仿真工具:DC、Genus
- 形式验证工具:Formality、Conformal
- 形式验证是验证经过 MAP 之后的 Netlist 功能是否 OK
- 涉及到了 STD CELL 数据分析更复杂一些,如果直接用 EDA 工具去验会很慢,这个过程也是个动态验证(验证用例随着时间在不停的走)
- Formal 是静态验证,点(如 RTL 与门)对点(如 Netlist 与门)的验证逻辑功能,不会涉及到时序
- 静态时序分析工具:PrimeTime、Tempus
- 可测试性实现工具:Tessent
- 数字版图设计工具:ICC、Innovous、Olympus
- 数字物理验证工具:Calibre
四、数字芯片设计工艺
- 主流工艺:28nm CMOS
- 先进工艺:16/14nm 3D
- 2017:16/14nm 工艺
- 2018:10/7nm 工艺
注:x nm指的是 CMOS 晶体管的直径。
管子越小,体积越小,功耗也有相应的收益!
五、数字芯片设计流程中常使用的语言
- 硬件描述语言
- VHDL(欧洲、印度)
- Verilog(中国、美国)
- SystemVerilog Design(很少用)
- 硬件验证语言
- SystemVerilog Verification(OOP:面向对象;属性、行为;Random Constraint 带约束)
- SystemVerilog Assertion(测时序、设计也会用;验证使用 Assertion 更多的是用它的 Cover 去弥补功能覆盖率的描述 - 这种情况通常 SV 不好描述)
- SystemC(很少有)
- C/C++(很少用)
- 脚本语言
- Shell(Bash shell)
- Makefile(Questasim)
- Perl(前几年流行)
- Python(流行、AI)
- TCL(也是前几年流行)
六、数字芯片设计设计方法
- 自顶向下(架构)
- 自底向上(电路)
- 可重用
- 参数化(如:位宽)
- IP 化(DIP - RTL 的一个 IP)
- 低功耗设计
- Clock gating(用的较多)
- Power gating(做 MCU、手机芯片会用的较多;省电)
- 验证方法学
- VMM、OVM、UVM
- VIP
- AIP
七、制定验证计划和分层的验证平台
7.1、内容
- Verification Plan 验证计划
- Verification Environment 验证环境
- Verification Guidelines 验证原则
7.2、验证策略
如何验证 RTL 设计代码?
- 需要哪些资源?
- 硬盘空间有多大(通常 TB 级别)?CPU 资源够不够?EDA License 够不够?
- 需要验证哪些内容?
- RTL 特性?不同特性放到不同平台去实现。
- EDA 验哪些?FPGA 验哪些?应用平台(EMV)验哪些?
- 分解各个平台的测试点,规划测试用例
- 是否输入应用场景所对应的所有可能?
- 输入符合实际应用场景
- 其他的 corner(边界)case
- 其他的异常 case
- 如何发现错误?
- 看波形和自动比较(原则上自动比较,有必要看波形)
- 如何衡量验证进度?
- 覆盖率驱动的验证策略(代码覆盖率、功能覆盖率)
- 什么时间验证结束?
- 覆盖率 100%(或验不到的点可以进行解释)
7.3、验证进度
- Regression:回归,把所有的用例集中起来一轮一轮的去跑,每次跑的随机种子不一样,灌输的激励也不一样,打到的验证点也不一样。
7.4、验证计划的内容
- 验证层次描述(单元 -> 模块 -> IP)
- 需要的工具:逻辑仿真工具、自行开发的工具、软硬件协同
- 风险和前提条件
- 验证功能点
- 特定的验证方法
- 一般是覆盖率驱动验证策略(CDV,Coverage Driven Verification)
- 覆盖率要求:代码覆盖率 + 功能覆盖率
- 测试用例的应用场景:上电复位、数据传输、命令处理、容错处理
- 资源要求:人员、硬件、软件
- 验证组长来考虑
- 时间安排:TestBench、TestCase(用例)、Regression(回归)
7.4.1、验证的层次
- 设计层次结构要明细:unit -> block -> ip -> SoC
- 将不同的电路层次结构组合成功能组件
- 每个功能模块的复杂程度
- 复杂功能需要较高的可控制性和可观察性
- 简单功能不需要,可以在其他不同的层次结构进行控制和观察
- 接口定义和设计规格要清晰
- 可变接口的功能必须独立验证
- 功能简单的稳定接口可以跟其他模块组合验证
7.4.2、需要的 EDA 工具
- 逻辑仿真工具:QuestaSim、IES、VCS
- 形式验证工具:Conformal、Formality
- 基于断言的工具(System Verilog)
- 调试工具:VSIM、Verdi、DVE
- 硬件加速仿真器:Veloce、Palladium(更大的 FPGA 阵列,可以吃进整个系统,运行速度通常在几十 KHz,弱于 FPGA,强于 EDA;成本较高)
- 软硬件联合仿真:FPGA 原型验证(FPGA 通常模块级的功能,仿真速度快,运行速度最高可达 200M)
- 高级验证语言:SystemVerilog、SystemC、C/C++
- 功能库文件(跟工艺库的有关系)
- VIP、AIP
7.4.3、风险和前提条件
- 工具风险
- EDA 工具的购买、EDA 工具的问题
- EDA 工具的使用培训
- 自行开发的工具存在问题
- RTL 设计代码的及时发布
- 先发布第一版的简单功能的 RTL,之后再发布功能复杂的 RTL 代码
- 依赖于独立的验证团队
- 设计架构的收敛
- 悬而未决的设计需求
- 资源是否充足(硬盘、CPU)
7.4.4、功能点划分
- 关键功能
- 设计必须会使用的功能
- 次要功能
- 针对流片而言,而非关键功能
- 与性能相关的功能
- 下个版本可以实现的功能
- 软件可以实现的功能
- 下一个验证层次中,非关键功能
- 可以在不同层次,可并行验证的功能
- 边角条件的情况
- 通用功能
- 正常运行过程中不会发生的操作
- 系统复位和时钟的生成
- 错误处理
- 系统调试
- 本层次不需要验证的功能
- 在逻辑仿真过程中,可以在较低层次的验证,也可以在更高的层次验证的功能
- 在该层次上不使用的功能
7.4.5、特定的验证方法
- 验证类型
- 需要验证的功能(正常)
- 内部结构(设计)
- 错误现象(异常)
- 资源可以用性
- 验证策略
- 确定性仿真 - 简单设计
- 随机化仿真 - 复杂设计(以时间换空间;打到人脑意想不到的点)
- 形式化验证
- 随机验证
- 因为循环导致 hang(挂死)
- 低概率应用场景
- 特定的直接测试用例
- 抽象层次
- 检测策略
- 白盒验证(SVA)
- 灰盒验证
- 黑盒验证(对比 RM)
7.4.6、覆盖率需求
- 定义覆盖率目标:通过反馈机制来确定验证环境的激励生成质量(完备性 -> 覆盖率)
- 所有的命令和响应类型
- 特定的数据类型和数据的数值范围
- 所有有效激励
- 容错处理(异常场景)
- 统计覆盖率
- 分析覆盖率漏洞
- 必要情况下,编写定向测试用例
7.4.7、测试用例应用场景
- 列出所有的实际应用场景(分解测试点的原则)
- 需要验证的配置项(配置寄存器)
- 验证环境中的数据变量
- 数据的重要属性(范围、有无符号)
- 所有 DUT 输入端口的实际序列
- 错误条件(Error handling)
- 边界条件(Corner case)
7.4.8、资源要求
- 人力资源
- 验证环境类型(复杂度)
- 手动检查和比对参考模型需要更多的人手
- 基于事务级的验证环境需要的人手少
- 工程师的项目经验
- 计算资源
- 测试用例的运行时间 乘以 测试案例的数量,决定硬件和软件的资源量(实际可能并不会这么机械操作)
- CPU + 内存 + 磁盘
- EDA 工具 License
7.4.9、时间安排(Schedule)
- 列出不同验证活动的时间安排
- 验证团队提交的结果和内容
- 验证主要工作、流程和标准
- 项目进度安排包括
- 设计架构和文档的正式发布时间(Specification Delivery)
- 验证平台开发(Verification Environment Development)
- 第一版 RTL 设计代码的发布时间
- 一个基本测试用例跑通时间(Base Flow)
- 启动回归测试的时间(Regression Run)
- 流片的时间(Release to Manufacturing)
- 项目时间安排需要考虑设计层次结构(each level of hierarchy)
- 当验证发现的 RTL 问题的几率降低时,验证工作必须进行到下一个层次(尤其是对于继承性的项目)
- Bug Rate
- IC 设计工程师回去做代码的检视(Code Review)
- 单元级验证(UT)、模块级验证(BT)、系统级验证(ST)
- 低层次的验证不利于发现更多的 RTL 代码问题,因为这些问题出现在整个设计周期的早期,每个设计工程师的验证或单元级的验证都是并行开发的。实践经验表明:当 RTL 出现的问题几率降低时,可以将验证工作从低层次迁移到更高的层次,比如从单元级验证迁移到模块级验证。
7.5、验证环境
- 验证平台的组件(TestBench Components)
- 验证平台环绕在 DUT 周围
- 产生激励(Generate stimulus)
- 获取响应(Capture response)
- 检查正确性(Check for correctness)
- 通过覆盖率衡量验证进度(Measure progress through coverage matrix)
- 验证平台的属性(Features of an effective Testbench)
- 可重用性,易于修改
- 面向对象编程(Object oriented programming)
- 分层的验证平台易于重用
- 扁平化的验证平台难于修改和维护
- 分层的验证平台将代码分隔成独立的模块,将通用的功能放在一段代码中
- 迅速获取信息并快速达到较高的覆盖率
- 随机化验证技术(Randomize!!)
- DUT
- 是最终拿去生产的实际电路,通常是 RTL 级描述,使用 Verilog 描述语言编写
- 综合 -> 网表 -> GDSII -> Chip
- TestBench
- 行为级的描述,更多是软件的仿真
7.5.1、分层的验证平台
- 信号层(Signal layer)
- DUT 和 TestBench 的连接(interface,后面详细介绍)
- 命令层(Command layer)
- 驱动器(Driver)
- 将命令如 send()、read()、write 转换成信号,驱动 DUT
- 接收器(Receiver 或 Monitor)
- 将 DUT 的输出信号转换成命令
- 编写断言(Assertions)
- 断言可以对基于时钟周期的系统行为进行建模
- 大部分的比对不带时序,比较与 RM 结果。但是断言能看到时序!
注:代码看不懂,先不用细究!
- 功能层(Functional Layer)
- 将事务级信息(transcations)转换成命令驱动到 DUT,比如 DMA readoperation
- 代理器(Agent)
- 暂存事务级信息,按照一定的顺序发送这些信息
- 并不关注实际的时序,实际的时序是下层的 Driver 来关注!
- 检查器(Checker)
- 接收 DUT 的输出数据,并与期望的结果进行比对
- 计分板(ScoreBoard)
- 将比较结果反馈在计分板中
- 应用层(Scenario Layer)
- 生成器(Generator)
- 生成定向的数据
- 定向测试
- 带约束的随机测试
push_back()
是 SV 中队列(queue)支持的系统函数,可以在队尾插入对象(object)
附:理解各个层次的关系:
类比:发送一堆水果,而水果中包含苹果和香蕉
- Generator 负责把苹果、香蕉采下来给 Agent
- Agent 负责把诸如先发送 5 个苹果再发送 5 个香蕉这个事情组织并打包好,打包好之后给 Driver
- Driver 负责选取交通方式,是火车来发,还是轮船来发
- DUT 代表具体的交通工具,火车或轮船
从上面的描述中不难看出,由于分层,当某一层发生变化,只需要改变部分层,其它层还是可以重用的。
- 测试层(test)
- 测试用例可以控制所有输入到验证环境中的所有内容
- 为输入的激励信息设置约束
- 组合多个测试用例
- 功能覆盖率(Functional Coverage)
- 利用功能覆盖率的统计结果来调整约束,产生下一步的输入激励
附:理解各个层次的关系:
类比:把验证比作一场交响音乐会,Generator、Agent、Scoreboard、Checker、Driver、Receiver 等比作不同的乐器,那么 Test 的作用就是指挥家,整个音乐会的源头!
7.5.2、分层的验证平台好处
- 更新验证环境的时间少
- 通过顶层文件可以很容易配置验证平台
- 测试期间所有的有效的配置(随机化策略关系较大)
- 选择不同的设计配置可以进行回归测试
- 带约束的随机化配置对象
- 提高可重用性、可维护性
八、小结
- 验证层次
- unit 级(arith-alu/shift-alu/Preprocessor)
- block 级(ALU)
- IP 级(ALU+Preprocessor)
- 验证语言:SystemVerilog
- 验证工具:Questasim
- 覆盖率统计:功能覆盖率 + 代码覆盖率
2.SystemVerilog interface 和 program 学习
一、内容概述
interface
- 1、验证平台(TestBench)和待验设计(DUT)的连接
- 2、Verilog 的端口连接方式和缺点
- 3、SystemVerilog interface
- 5、SystemVerilog clocking block
program
- SystemVerilog program block
仿真(了解即可)
- 1、仿真时间(了解)
- 我们做 EDA 验证,更多的是做逻辑功能验证,不会去太多的关注时序(setup、hold 等时序,后端 STA 会更多关心)。
- 2、仿真时间域:timing region(了解)
二、验证平台与待测设计的连接
2.1、Verilog 连接方式
- Verilog module ports
- Verilog 语言使用端口名字连接各个功能模块
- 仲裁器:对于某一个东西,资源比较有限,如果多个人来申请,那就需要仲裁器来决定谁可以用!
- logic 可以当做 wire 和 reg 的组合
隐式
.*
端口连接(实际使用还是不推荐使用)- SystemVerilog
.*
可以自动匹配具有相同名字的线网和端口(灵活性差)
- 自动匹配的名字必须具有相同的端口位宽
- 连接的端口类型必须兼容
隐式
.name
连接(实际使用还是不推荐使用)- .name 是使用端口名字连接的简化形式
- .name 必须满足端口名字和位宽一致
- .name 简化实例模块之间的连接
2.2、Verilog 连接方式
Verilog 的模块端口连接方式比较繁琐!Why?(有点勉强,单纯为了后面引出 SV)
比如将一个信号
request
修改为request1
:- 需要修改所有模块的端口列表
- 需要修改连接模块的所有端口列表
- 需要修改所有的模块实例化的端口名字
- 需要修改所有的层次化的端口的模块端口名字
如果忘记修改所有的端口名字,会出现编译错误。
- SoC(System on Chip )大规模设计中 Verilog 的端口连接方式更加繁杂!
- 在多个模块中都需要声明相应的端口
- 通信协议接口在多个模块中使用
- 不匹配的声明会导致编译错误
- 设计文档修改端口名之后需要修改所有模块
SystemVerilog 引入新的端口类型:
interface
。注:interface 更多的用在 DUT 和 TB 的连接!
2.3、SystemVerilog Interface
2.3.1、SystemVerilog Interface 简要介绍
- interface 中集合了多个 Verilog 类型的信号,是一个独立的端口类型
- interface 可以是一个独立的文件
2.3.2、 利用 interface 简化模块连接
2.3.3、如何使用 interface 中信号?
endmodule:test
仅仅是为了写给读代码的人看的,相当于注释!
- 本身 interface 定义的信号是没有方向性的,是一个双向信号!
2.3.4、接口与端口的连接
- 接口通过名字来引用信号
- 上述
arbiter
和test
的例化方式是等效的!
2.3.5、interface modport
- module port 的简写
- modport 为接口内部的信号提供不同的视角(DUT,Test Program)
- 一个 interface 中可以提供任意数量的 modport
- modport 只是声明连接模块的信号的端口方向:
input
、output
和inout
(inout 很少用!)
- 在 interface 中声明的时候,位宽不需要只需要声明方向!
2.3.6、SystemVerilog interface 小结
- interface 的功能
- 一组信号 / 线网
- 独立的文件
- 在 interface 中定义通信协议(很少用)
- 在 interface 中定义协议检查和验证函数:task、function、assertion(很少用)
- modport 可以定义 interface 的不同视角(DUT,Test Porgram)
- input
- output
- interface 中不能包含设计的层次结构
- interface 中不能包含 module 模块的实例
- interface 可以用作设计的端口,具有可综合的特性(工程上还是更多的用 Verilog 综合,当然现在综合工具也在慢慢支持 SV)
- interface 中可以包括含多个 modport
2.4、Clocking
2.4.1、Clocking:激励的时序(1/8)(了解,不重要)
- 没太大用,因为我们更多的是做逻辑功能验证,不会过多的关注时序!
- 时序是跟用的具体器件信息的工艺有关系,这些延时信息在综合过程中才能体现,得到网表之后才会有时序,才会做 STA。在 RTL 阶段不存在进行 STA。
2.4.2、Clocking:激励的时序(2/8)
- SystemVerilog 中使用 clocking 模块控制同步信号
- 在 interface 中定义 clocking 模块,将信号同步到某一个特定的时钟
- Clocking 模块中所有信号都是同步驱动或采样,可以确保验证平台可以在正确的时间跟设计进行交互
- Clocking 模块主要使用在验证平台中,不能用作 RTL 设计
- 一个 interface 中可以包含多个 clocking 模块
2.4.3、Clocking:激励的时序(3/8)(了解,不重要)
- 当使用 interface 和 clocking 模块进行仿真时:
- 从 DUT 的输出到 TestBench 的输入有 1 个延迟
- 需要在 Testbench 的输入端添加一个 “虚拟的同步器”
- 从 Testbench 的输出到 DUT 的输入没有延迟
2.4.4、Clocking Skew(4/8)(skew 了解,不重要)(学习下语法,在 interface 中定义)
- 定义 input 和 output skew,控制时序
- input skew 表示在时钟有效边沿采样信号的扭斜时间单位
- output skew 表示在时钟有效边沿驱动信号的扭斜时间单位
- 上面的代码是模拟一个 D 触发器的寄存输出,保证信号是同步的,保证激励是同一个时钟域的。
2.4.5、Clocking Blocks(5/8)(重要,学习下用法)
- TEST 中的 request 是在时钟的上沿发出来的!(信号同步到时钟的上升沿)
- TEST 中的 grant 是在时钟的上沿去采集,相当于寄存器打了一拍,延后一拍!
- clk 同步,相当于寄存器打了一拍,同步时钟域!
@arbif.cb
等待下一个时钟周期上沿,等效于@(posedge clk)
- clock 本身可以用其他方法替代,不是必须得有,比如上图右下角的写法还可以写成如下
- 通常在实际使用中我们用上面这种替代方法更多,切记不要为了炫技而写代码!
- clocking 功能其实并没有带来特别大的好处!
- 一定要记住
clocking
和modport
是可选的!了解知识点即可,有其他方式等效替换
2.4.6、Clocking Block:信号同步(6/8)
@arbif.cb
等效于@(posedge clk)
@
等待的是一个信号变化(上下沿用posedge
和negedge
区分),wait
等待的是一个电平
##2
等待 2 个时钟周期等效于repeat(n) @arbif.cb
2.4.7、Clocking Block:信号同步操作 (7/8)
- 时序逻辑用非阻塞
- 组合逻辑用阻塞
2.4.7、Clocking 总结(8/8)
- interface 中的 clocking 只用于验证平台,不能用于 RTL 设计
- Clocking 的好处
- 显式指明同步时钟域
- 验证平台驱动信号符合实际需求,保证同步行为
- Clocking 的功能
- Interface 可以包含多个 clocking 模块
- 一个 clocking 模块只有一个 clock
- Clocking 模块中的信号的方向与 testbench 有关
2.5、Program Block(干货)
2.5.1、Program Block(1/3)
- Program 语句块执行验证平台代码
- Program 语句块类似于 module 语句块,可以包含变量和其他 module 模块的实例化
- Program 不能含有层次化的机构,如其他 module 或 interface 的实例
2.5.2、Program Block(2/3)
#
可以自己加时间单位;##x
表示 x 个 cb 时钟周期,而 cb 用的 posedge。
2.5.3、Program Block(3/3)
- Program 好处
- 将验证平台和待测设计分隔开
- Program 用于执行测试用例(testcase)【面试重要】
- Program 用于封装与测试用例相关的数据【面试重要】
- Program 功能
- 可以例化在任意的层次结构中
- 通常是在最顶层文件中
- 可以像 module 一样使用 interface 和端口进行连接
- 没有 module 层次结构,只有 class 的层次结构
- 可以有 initial、task 和 function 代码,但是不能存在 always 语句
- 当 Program 中的 initial 语句执行到结束时,隐式执行
$finish
注:对于仿真来讲,program跟module一样,唯一区别可能少写一个$finish。还是那句话,这也是可选的!并不是工程上一定会用到,更多的是为了知识体系的完整性!但是interface在工程上还是非常重要的,要特别重视。
2.6、验证平台 - 顶层文件 top
- 顶层示例,与下面的没有大关系
2.6.1、RTL Design with Interface
ref
在 SV 中就是相当于inout
- ref 参数传递变量指针,而不是变量的值,参考 C 语言的指针,在 Interface 中用的不多!
- 在实际中,RTL Design 中用的 Interface 很少,这里仅仅举个例子!
代码示例框图关系:
2.6.2、Test Program with Interface
2.6.3、Connection between DUT & test with interface
- Q:test 和 ctrl 之间的信号是如何连接的?
- A:通过在 Interface 中的 modport 定义好方向,便可以实现自动连接!
三、实践练习
3.1、编写 DUT 文件
arb.v
3.2、编写 Interface 文件
arb_if.sv
- DUT 的 port 一般不会去用 Interface 的 port,这个 clocking 只是针对在 test 环境中使用定义的!
- clocking 本质是模拟时钟沿,把信号打一拍进来。而 DUT 端口信号实际是不含时序信息的,进来之后如果要用时钟打拍,是用实际的电路,即实际的 D 触发器去打拍。不需要用模拟的时钟打拍!
- clocking 一般用在环境里面,模拟时钟打拍的同步时序;不是一定在 test 里面就要用 clocking,灵活的应用,可以 Interface 里面没有 clocking,在 driver 里面去通过时钟沿送数据;仅仅是语法上提供了选择
- 关于 DUT 的 modport 要说的:DUT 是不能用 clocking block 的,DUT 是内部使用 always 逻辑去采的,故在端口上不能用 cb。
- 其实这里的
modport dut
没太大的用!
3.3、编写 test 文件
注:作用主要用来发送激励
test.sv
3.4、编写 TestBench 文件
arb_tb.sv
.*
会自动连接 Interface 里面的信号!
3.5、编译运行
因为只是写了 test 的激励,没有写 monitior,也没有写自动化的比对 Makefile,因此使用 Questasim 来查看波形,打开命令如下
如果 Questasim 有已经启动工程,需要先关闭,然后使用 GUI 的方式新建工程。参考:【数字 IC 验证快速入门】6、Questasim 快速上手使用
按照如上链接新建、编译并开始仿真工程。
添加 DUT 的的波形,在
sim
窗口选中u_arb
,选择Add Wave
或者快捷键ctrl+w
,如下图所示:随后在
Wave
栏选择选择Run -ALL
,出现是否结束的仿真,一定要选择NO
,如下图所示:可以对比 DUT 代码,查看波形,可以发现满足预期
3.SystemVerilog 学习之基本语法 1(数组、队列、结构体、枚举、字符串... 内含实践练习)
2.1、SystemVerilog 语法规则
- 和 Verilog 一样
- 大小写敏感
- 空格不忽略,字符串中的除外
- 注释符号
- 行注释:
//
- 语句块注释:
/*...*/
- 数制格式
<size>' <base> <number>
'b
(binary 二进制):01xXzZ
'd
(decimal 十进制):0123456789
'h
(hexadecimal 十六进制):0123456789abcdefABCDEFxXzZ
- 使用分隔符
“_”
,提高阅读性: 16'b1100_1011_1010_0010
32'hbeef_cafe
2.2、SystemVerilog 新的数据类型
- 二值逻辑:性能更好,占内存少
- 队列,动态数组和联合数组:减少内存的使用量,支持搜索和排序
- 联合数组和填充结构体:相同数据具有不同的视图(View)
- 类和结构体:支持数据结构的抽象化
- 字符串:SV 内建了操作函数
- 枚举类型:便于编码和理解(常用在 TestBench 中)
2.3、SystemVerilog 数据类型
Verilog 语言中的赋值
- 矢量(vector,即位宽大于 1 的变量)容易赋值为全
0/z/x
,但是赋值全1
的时候,需要把全部位都写出来!
SystemVerilog 的赋值
- 不需要指定进制数(二进制、八进制、十进制和十六进制数)就可以填充 0/x/z
- 全部填充 1
四值变量
- Verilog
- reg:通用变量,用在
initial
和always
语句中,对硬件模块建模 - 组合电路和时序电路
- wire:主要起到连接作用,类似金属线;在
assign
中赋值
- SystemVerilog
- logic(logic 取代了 reg)
- 可被连续赋值语句,门电路或 module 驱动
- 不能多驱动,比如双向总线(需要使用 wire 进行建模)
- logic 有四种状态
- 0/1/x/z
- logic 定义的变量是无符号数!
- 举例:
logic [31:0] data;
常见用法:
二值变量:
bit,byte,shortint,int,longint
- 只有两种状态 0 和 1(x 和 z 会转换成 0)
- 提高仿真性能,减少内存使用量
- 不用于做 RTL 设计(因为 x or z 两种状态会被转换成 0)
bit
的位宽是用户自定义的,并且是无符号数;byte/shortint/int/longint
的位宽是固定的,并且是有符号数!logic
的位宽也是用户自定义的,并且是无符号!
- 关于有符号数和无符号数的数据范围再做一点小回顾,eg:2 位宽,即
00 / 01 / 10 / 11
四种形式,在无符号形式下依次代表:0 / 1 / 2 / 3
;在有符号形式下(最高位为符号位)依次代表:0 / 1 / -1 / -2
- 正数的补码是它本身,负数的补码是取反再加 1
- Q:
logic [7:0] x
和byte x
一样吗? - A:不一样,
logic
是四值变量,byte
是二值变量
逻辑仿真特性
- 四值状态变量的默认初始值是
x
;二值状态变量的默认初始值是0
- 二值状态的变量不能表示未初始化状态(
x
)
- 四值状态的变量可以赋值为二值状态的变量。
x
和z
会转换成 0
$isunknown(expression)
可以检查表达式中是否存在 x 或 z
- 支持多维数组(算法类矢量运算用的较多)
- 超出边界的写操作被忽略
- 超出边界的读操作返回值为
x
(四值状态变量),0
(二值状态变量)
byte/shortint/int
存放在 32 位的存储空间中(显然byte
和shortint
有点浪费存储空间)
longint
存放在 64 位的存储空间中
3.1、一维数组
int
等效于bit signed [31:0]
的声明
[0:15]
、[16]
两种方式指定一维数组的深度,所以两种写法是等效。int lo_hi[0:15]
与int lo_hi[16]
写法的元素排列顺序一样(从 0 开始到最大 15),但是int lo_hi[15:0]
写法的元素排列顺序与前面不一样(从 15 开始到最小 0)!- 数组
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--)
。
int lo_hi[16]
相当于16行x32列
的矩阵大小
- 取 lo_hi 中第 1 个数的第 8bit:
lo_hi[1][8]
3.2、多维数组
3.3、固定数组的基本操作
3.3.1、固定数组初始化:'{} '{n{}}
- 全部或部分初始化
3.3.2、for
:利用 for 循环语句进行初始化
- 最常见的数组初始化方法
- 变量
i
为本地循环变量
- 系统函数
$size
返回数组大小(固定数组只能用$size
)
3.3.3、foreach
:利用 foreach 循环语句进行初始化
- 指定数组名称,方括号中是索引号,foreach 会根据索引号遍历所有的数组元素
- 索引号可以自动声明为本地循环变量
3.3.4、固定数组的赋值和比较
- 不需要循环就可以进行数组的赋值和比较(只适用操作符等号
==
和不等号!=
)
3.3.5、数组元素、数组元素部分选取
3.4、非填充数组(unpacked array)
- 存放在 32 位的存储单元中
bit [7:0] up_array[3]
近似等效于byte up_array[3]
(但注意 bit 是无符号的,byte 是有符号的)
简单的非填充数组声明
- 注意数组直接定义深度比如
1024
,那么就是从0-1023
;如果直接指定[64:83]
,那么就是64-83
3.5、填充数组(压缩数组,Packed Array)
相对于非压缩数组的区别,还是在存储上!
- 将一个数组当做一个值
- 连续存储数据
- 维度的书写格式为
[msb:lsb]
填充数组的初始化
- 声明时使用简单的赋值语句进行初始化
- 填充数组初始化赋值不需要加单引号!
3.6、混合数组(Mixed Arrays)
3.6.1、混合数组介绍
- p_array 是一个非填充数组,数组元素则为填充数组:
bit [3:0][7:0] p_array[0:2]
- 非填充数组回顾:
bit [7:0] up_array[3];
3.6.2、混合数组维度
- 混合数组中,非填充数组的维度是第一位的;从最左侧开始到最右侧
- 填充数组的维度是第二位的,从左侧开始到最右侧
3.7、填充数组和非填充数组比较
- 填充数组可以手动跟标量进行转换
bit [3:0][7:0] a;
与bit [31:0] b;
之间是可以相互转换的!即:a = b;
或b = a;
- 按照
byte
引用内存数据 - 即一个字节,
8bit
- 如果仿真中需要等待数组的变化,可以使用填充数组
- 用非填充也行,填充相比非填充存储空间更小,仿真速度更快!
- 只有固定数组可以被填充
- 固定数组分为填充和非填充两类!
- 动态数组、联合数组和队列是不能够被填充的
3.8、填充数组和常量数组初始化与内存存储比较
常量数组:
int a[4] = '{0, 1, 2, 3};
- 常量数组初始化必须在前面加上单引号
- 内存存储:
a[0] = 0; a[1] = 1; a[2] = 2; a[3] = 3;
填充数组:
bit [3:0] [7:0] b = {8'h3, 8'h2, 8'h1, 8'h0};
- 填充数组初始化不需要在前面加单引号
- 内存存储:
b[3] = 3; b[2]=2; b[1] = 1; b[0] = 0;
3.9、小测试
- Q:变量 logic 和 reg 有根本性的不同吗?
- A:没有
- Q:变量 logic 和 bit 有什么不同?
- A:logic 有四种状态:0/1/x/z;bit 只有两种状态:0/1
- Q:两种状态的变量可以用于 RTL 设计吗?为什么?
- A:不可以,因为二值逻辑会把 x/z 两种电路状态转换成 0
- Q:
bit[31:0] src[5] = '{5,6,7,5,5}
,则 src[1]
= 3‘b110’src[3][0]
= 1’b1src[2][3:1]
= 3’b011
- Q:下列哪个不是 2 值数据类型?(B)
- A bit
- B logic
- C int
- D byte
- Q:下列哪种不是 Verilog 语法?(D)
- A data = ’0;
- B data = ’z;
- C data = ’x;
- D data = ’1;
- Q:如上代码,那么
md[2,3]
(A) - A 0
- B 5
- C 7
- D X
解析:
i
取值范围是:0-1
,j
取值范围是:0-2
。int 是二值变量,md[2,3]
毫无疑问越界了,所以应该返回 0,答案 A。但是如果定义的是 logic 四值变量,那么返回的是 x- Q:logic[7:0] 和 byte 的取值范围分别是?(A)
- A
0~255, -128~127
- B
128~127, -128~127
- C
0~255, 0~255
- D
128~127, 0~255
logic 是无符号的,byte 是有符号的
如果在仿真之前不知道数组的元素个数,那就用动态数组!
- 动态数组声明时使用方括号:
[]
,形式如下:
- 在运行仿真时设置数组的元素个数,编译时不需要
- 在仿真过程中,可以分配内存空间和重新设置数组元素的个数
new[]
用于分配内存空间,传递数组元素的个数
- 通过数组名称可以实现数组的赋值
- 当固定数组的数据类型相同时,可以将值赋给动态数组
$size
系统函数返回固定数组和动态数组的元素个数!(固定数组只能用$size
,动态数组可以用$size
和size()
)
dyn = new[20](dyn)
- 原来的元素值还保存
dyn = new[100];
- 原来的元素值就丢了
当满足以下条件时,动态数组和固定数字可以相互赋值:
- 相同的数据类型
- 相同的元素数目
队列结合了数组和链表的特点。队列与链表相似,可以在一个队列中的任何地方增加或删除元素,这类操作在性能上的损失比动态数组小的多,因为动态数组需要分配新的数组并复制所有元素的值。队列与数组相似,可以通过索引实现对任一元素的访问,而不需要像链表那样去遍历目标元素之前的所有元素!
- 队列声明使用
$
在方括号中:data_type queue_name[$];
- 具有排序和搜索的功能
- 循序分配额外的空间和额外的元素
- 支持
push
和pop
操作
- 支持
add
和remove
元素操作
- 固定数组和动态数组的值赋值给队列
- 不需要
new[]
函数
- 队列元素编号是:
0
到$
- 如果把
$
放在一个范围表达式的左边,那么$
将代表最小值,例如[$:2]
就代表[0:2]
。同理,如果放在表达式的右边,则代表最大值。
- 队列常量的初始化同填充数组(合并数组)一样没有单引号!
b
和q
和j
共用变量类型int
,所以它们之间是逗号隔开!队列{3, 4}
左侧 3 是队头,右侧 4 是队尾!
insert(x, y)
:在第 x 个位置元素之前插入值为 y 的元素
push_front(x)
:在队头插入值为 x 的元素;push_back(x)
:在队尾插入值为 x 的元素;
pop_back
:队尾元素出队pop_front
:队头元素出队
push
和pop
只能操作队首或者队尾,相当于一个 FIFO 行为!
delete(x)
:删除第 x 个位置元素
队列中的元素是连续存放的,所以队列的前面或者后面存取数据非常方便。无论队列有多大,这种操作(前后存取数据)所耗费的时间都是一样的。在队列中间增加或删除元素需要对已经存在的数据进行搬移以便腾出空间。相应操作所耗费的时间会随着队列的大小线性增加。
SV 提供了关联数组类型,用来保存稀疏矩阵的元素,这意味着当你对一个非常大的地址空间进行寻址时,SV 只为实际写入的元素分配空间。关联数组可以采用树或哈希表的形式来存放关联数组,但有一定的额外开销。但当保存索引值比较分散的数组时,如 32 位地址或 64 位数据作为索引的数据包,这种额外开销显然是可以接受的!
- 关联数组声明采用在方括号中放置数据类型,
data_type associative_array_name[data_type]
- 使用稀疏的内存空间
- 动态分配,非连续元素(有点类似链表)
- 一维,可以利用整数和字符串作为索引。
注:联合数组声明也可以使用*在方括号中,但是不推荐,这里仅作为了解即可!
联合数组操作
- 联合数组可以使用
foreach
进行初始化
- 读取未分配的元素,4 值逻辑变量返回 x,2 值逻辑变量返回 0
- 支持函数:
first,next,prev,delete,exists
关联数组的更多使用,可参考:
- 数组递减方法(用于非填充数组:固定数组、动态数组、队列和联合数组)
- 求和
sum
,求积product
、与and
、或or
、异或xor
a.sum
单比特数组的求和返回单比特的数值- 这个就能解释通为什么要把
data_in
定义成33bit
了!
- 求最大值
max
,最小值min
,唯一化unique
(去重)【注意,它们的返回值是一个队列】
on.sum
中 sum 后面的括号可加可不加
数组排序操作
- 翻转:reverse
- 乱序:shuffle
- 升序排列:sort
- 降序排列:rsort
数组定位操作
- 查找元素:find
- 可以找某一类元素,定义这一类元素的范围,即特点
- 查找第一个元素:find_first
- 查找第一个元素的索引:find_first_with_index
- with 和 item 都是关键字,item 表示所有元素!
- 注意带 index 是返回索引!
- 注意动态数组的初始化,用的是固定数组的初始化方式,这种是可以的!new 是当元素个数变化时用的!
d.sum(x) with(x > 7)
中x>7
返回的是一个逻辑结果,即结果只有 0 和 1 两种。
上述代码实操
- 可以看到第一个
count
应该打印出 2,这里为什么是 0 呢? - 还是我们上面讲的:
with(x > 7)
返回的逻辑结果是单 bit 的,而单比特数组的求和返回单比特的数值,故这里就错误了!解决办法,强制转换在其后面乘上 1,这个 1 是 32bit 的 - 总结:只有单纯逻辑运算的,都需要去做转换!
- 固定数组
- 编译时,数组元素的个数是固定的
- 连续存放数据(相比联合数组)
- 多维数组
- 动态数组
- 编译时,不知道数组元素的个数
- 连续存放数据(相比联合数组)
- 队列
- FIFO/Stack
- 特殊的动态数组,相比动态数组优势可以实现元素的快速删减,不需要
new()
来重新更新!
- 联合数组
- 稀疏数据和内存(存储不连续)
- 索引号可以是整数或字符串
- 结构体的关键字是:struct
- 结构体是有一组变量或者常数组成的集合,可以作为一个整体进行操作,也可以操作其中的一部分
- 将逻辑上相关的信号放在一起,比如总线协议:
- 使用结构体的名字来操作整个变量
填充结构体
- 结构体默认情况下是非填充的
- 不同的 EDA 工具的排列是不一样
- 使用关键字
packed
可以将结构体声明成填充的结构体 - 填充结构体将所有的数据元素存储在连续的单元内
- 结构体的第一个元素是适量的最左侧域(不同的 EDA 工具可能不一样!)
- 填充结构体可以通过变量名或者部分矢量选择来使用结构体中的变量
- 填充结构体操作
- 填充结构体的赋值
- 抽象变量代表一个数值序列
- 用户可以定义每一个数值
- 增加了可阅读性
- 支持
first、last、next、prev
操作
- 默认标号依次是:0 / 1 / 2
- 枚举类型默认的值为 int
- 第一个值为
0
,第二个值为1
,依次递增
- SystemVerilog 支持显式指定每个数值
- 所有的数值必须唯一
- 对于没有指定数值的元素,其数值是按照前一个元素的数值加 1
- 枚举变量的基础类型
- SystemVerilog 允许显式指定基础类型
- 枚举变量数值
- 赋值的变量的值必须匹配基础类型
- 枚举类型的数值大小
- 取值范围不能超多基础类型的有效范围(系统会自动根据已定义变量确定取值范围!)
- 给四值逻辑变量赋值 X/Z 是合法的
- 必须给 x/z 之后的变量显示赋值
string variable_name [=initial_value];
中括号中的内容是可选的,也即初始值是可选的!
- 在未赋值的情况下,string 类型变量的值初始化为空字符
""
- 系统函数
$psprintf()
生成字符串
- 字符串变量类型具有内建的操作符和函数
==,!=,compare() 和 icompare();
itoa(), atoi(), atohex(), toupper(), tolower()
等len(), getc(), putc(), substr()
12.1、bit/logic 练习
12.1.1、二值和四值变量初始值练习
sv_bit_logic.sv
Makefile
sed '/^[^*].*/d' $(comp_file).log > rst.log
:正则表达,作用是开头不是星号的行就把它删掉,删掉之后剩余有星号的行放到文件
rslt.og
里
运行命令如下:
rslt.log
sig_logic
是个八位的四值变量,所以打印出来的是 8 个x
- 其他都是二值变量,默认初始值是
0
12.1.2、$isunknown
练习
在
sv_bit_logic.sv
中添加相关代码:rslt.log
12.1.3、无符号和有符号数练习
在
sv_bit_logic.sv
中添加相关代码:rslt.log
bit/logic
的位宽是用户自定义的,并且是无符号数;byte/shortint/int/longint
的位宽是固定的,并且是有符号数!
12.1.4、全0/1/x/z
赋值练习
在
sv_bit_logic.sv
中添加相关代码:rslt.log
- 0 有几个无所谓,反正都是 0,所以这里打印是正确的!
12.2、固定数组练习
12.2.1、unpacked array ininial demo(非填充数组初始化 demo)
sv_fix_array.sv
Makefile
文件同5.1
,直接拷贝过来。运行命令如下:make comp_file=sv_fix_array.sv
rslt.log
12.2.2、unpacked array assignment demo(非填充数组赋值 demo)
在
sv_fix_array.sv
中添加相关代码:rslt.log
12.2.3、unpacked array partial assign demo(非填充数组部分赋值 demo)
在
sv_fix_array.sv
中添加相关代码:rslt.log
12.2.4、array over read demo(数组越界读 demo)
在
sv_fix_array.sv
中添加相关代码:rslt.log
- 二值变量越界是
0
态
- 注意四值变量越界是
x
态,并且是对应位数个x
。在此处 src_logic 是 32 位,所以是 32 个x
12.2.5、packed array assign demo(填充数组赋值 demo)
在
sv_fix_array.sv
中添加相关代码:rslt.log
- 填充数组赋值花括号前可以不用加
'
- 注意填充数组的索引是从右往左的,右边的是
0
。
12.3、动态数组练习
12.3.1、dynamic array initial demo(动态数组初始化 demo)
sv_dyn_array.sv
rslt.log
- Makefile 同之前,不过是
comp_file
参数变了:make comp_file=sv_dyn_array.sv
12.3.2、dynamic array assignment demo(动态数组赋值 demo)
sv_dyn_array.sv
rslt.log
dyn1=new[20](dyn1)
不会覆盖已赋值的;而dyn1=new[10]
会覆盖已赋值的!
12.3.3、dynamic array delete demo(动态数组删除 demo)
sv_dyn_array.sv
rslt.log
12.4、队列练习
12.4.1、队列内建函数 demo(有 bug)
sv_queue.sv
rslt.sv
疑问:为什么 max 和 min 打印的值不对呢?
- Makefile 同之前,不过是
comp_file
参数变了:make comp_file=sv_queue.sv
- 语法要求,对于队列,需要
'
初始化,但是现在工具比较强大,不加也没关系!
12.4.2、队列插入删除 demo
sv_queue.sv
rslt.sv
12.4.3、入队出队 demo
sv_queue.sv
rslt.sv
12.5、数组方法练习
12.5.1、求和、求积方法 demo
sv_array_method.sv
- Makefile 同之前,不过是
comp_file
参数变了:make comp_file=sv_array_method.sv
rslt.log
12.5.2、队列找某元素或索引方法,带条件的求和 demo
sv_array_method.sv
rslt.log
sum=on.sum(x) with (x>3)
这种写法最后只会取结果的最低 1 个 bit 位;- 上面大于 3 的只有 5 和 4,所以
x>3
比较后两个 1。两个 1 求和是 2,对应二进制是 0010,因为只取最低 1bit 位,所以此处是 0. - 要想把几个 1 的求和结果显示出来,那就
(x>3)*1
,1 默认是 32bit 的,这样最终结果就是 32bit 的了 - 要想把
x>3
的数求和,那就(x>3)*x
- 注意
find_index
是对应x>3
的索引值:0 1
后面的 4 个 0 是无效的 0,可以对比find
结果来看!
12.6、结构体、枚举练习
12.6.1、结构体(unpacked struct)赋值 demo
sv_struct_enum.sv
rslt.log
- Makefile 同之前,不过是
comp_file
参数变了:make comp_file=sv_struct_enum.sv
12.6.2、结构体(packed struct)赋值 demo
sv_struct_enum.sv
rslt.log
- 注意
packed struct
赋值方式是特有的,当然它也可以用unpacked struct
那种赋值方式!
- 还需要注意
packed struct
的赋值花括号前需要加'
12.6.3、枚举的赋值 demo
sv_struct_enum.sv
- 枚举元素的类型默认是字符串的,如果直接赋值整型编译可能会有 warning:
Warning-[ENUMASSIGN] Illegal assignment to enum variable
。可以使用cast
进行强制转换,即:$cast(curs_st, 3)
rslt.log
12.7、字符串练习
12.7.1、字符串赋值 demo
string.sv
- Makefile 同之前,不过是
comp_file
参数变了:make comp_file=string.sv
rslt.log
- 注意 module 的名字不能是 string,所以这里大写了
12.7.2、获取、写入字符串中的单个字符 demo
string.sv
rslt.log
12.7.3、截取字符串的某一部分
string.sv
rslt.log
substr
还可以直接填索引值,如s.substr(1, 3)
4.SystemVerilog 学习之基本语法 2(操作符、类型转换、循环、Task/Function... 内含实践练习)
3.1、自增和自减操作符
- 类似 C 语言
- 先增 / 减后用和先用后增 / 减
注:SystemVerilog 的自增和自减通常用在 TB 里,不会用在 RTL 代码中!
3.2、逻辑比较操作符
- 等于
==
,不等于!=
(用的比较多,一般有x
态有问题) - 逻辑真为 1,逻辑假为 0;
- 如果比较的值中存在 x 和 z,则逻辑值为 1’bx
- eg:
a=4'b000x
和b=4'b000x
两个比较结果为1'bx
,那这里就有坑了如果if(a == b)
,注意1'bx
是不为真!
- 全等
===
,不全等!==
- 完全匹配四种状态值:
0,1,x,z
- eg:
a=4'b000x
和b=4'b000x
两个比较结果为1'b1
- 通配符逻辑比较
- 匹配等
==?
和匹配不等!=?
- 按位比较,把
x
和z
值当做匹配值 - 仅仅把右侧操作数中的
x
和z
当做屏蔽符号 - eg:
4‘b1010 ==? 4'b101x
匹配等返回1'b1
;4‘b1011 ==? 4'b101x
匹配等返回1'b1
- 匹配符逻辑比较的示例
- 打印出两个
$display
注:上述截图中的注释和display语法稍有问题。
3.3、inside 关键字
- inside 可以匹配一组数据范围内的任何一个数值
4.1、变量类型转换符type' (expression)
- SystemVerilog 增加了变量类型转换符:
type' (expression)
- 变量类型转换符可以在任何时刻对表达式进行类型转换
- 而不像 Verilog 一样只能发生在赋值语句中
4.2、$cast
强制类型转换
$cast(fsm, 1+2)
:把3
赋值给 fsm,并把整型强制类型转换为枚举类型,即此时 fsm 为DATA
4.3、变量位宽转换(Size Casting)
- SystemVerilog 增加了矢量位宽转换
size' (expression)
- 表达式转换成小位宽时,左侧的比特位被删除
- 表达式转换成大位宽时,左侧的比特位被扩充
4.4、变量符号位转换
- SystemVerilog 可以转换符号位
signed' (expression)
和unsigned' (expression)
- 操作数符号位转换
- 表达式结果符号位转换
5.1、for 循环语句
- Verilog 中循环变量必须在 for 语句之外声明
- 当前循环与其他语句相互影响
- SystemVerilog 可以在 for 循环内部声明循环变量
- 每个变量都为本地的唯一变量,所以外部使用的相同名字的变量不会相互影响
- 本地循环变量是自动化的(automatic)
- for 循环内部声明的本地变量在循环语句之外就不存在了
- 在 Verilog 中描述组合逻辑和时序逻辑都是用
always
关键字,时序逻辑always @(posedge clk)
;组合逻辑always @(*)
- 在 SystemVerilog 中,描述时序逻辑用
always_ff @(posedge clk)
或者always @(posedge clk)
;组合逻辑always_comb @(*)
或always @(*)
- continue
- 只能用于循环语句
- 结束本次循环,继续下一次循环
- break
- 只能用于循环语句
- 破坏循环,跳出循环,不再执行本次循环语句
- return
- 可以用于循环语句
- 结束循环
- 也可以用于 task 和 function
- 结束 task 和 function
5.2、do...while
循环语句
- Verilog 中的 while 循环不一定执行
- 如果第一次循环表达式的值为假,则循环语句不会执行
- SystemVerilog 增加了
do...while
循环语句(类似 C 语言) - 至少执行一次循环语句
- 在循环语句的最后判断循环变量
5.3、SystemVerilog 增强 case 语句 - case/casex/casez
- default 是可选项,在一个 case 语句中不能使用多个 default
- 首先计算 case 表达式的值,然后跟下面的实际分支进行匹配,匹配到就执行相应的语句
- case 表达式的值是按位分配,可以处理
x和z
casez
- 不关心
z
注:问号表示通配符
casex
- 不关心
z
和x
- 当
field = 8'b01100110
时,casex 选择分支 statement2 执行
field ^ mask = x1x0x1x0
6.1、Verilog task 和 function 概述
- function
- 函数执行的时候不消耗仿真时间
- 函数中不能有控制仿真时间的语句
- 不能有仿真时间延迟:#100 =(`timescale 1ns/10ps)
- 不能有阻塞语句:
@(posedge clock)
或者wait(ready)
- 不能调用 task
void function
没有返回值- Verilog 的
function
必须有一个返回值(Verilog 通过函数名返回!)
- task
- task 含有 input、output 和 inout 语句
- task 消耗仿真时间
- 延迟:#20
- 时钟周期:@(posedge clock)
- 事件:event
6.2、SystemVerilog task
和 function
- tasks 和 function
- 不需要使用 begin…end 语句
- 增加了
return
语句 - 返回值只有 1 个用 return;返回
bit/logic
这种简单类型的变量 void function
没有返回值function
可以有output
和inout
作为形式参数- 返回值大于 1 个时,用 output 返回比较方便;返回
array/queue/struct
复杂的用 output
可以这样类比,function 不带时序信息,通常来讲描述组合逻辑;task 可以带时序信息,既可以描述组合逻辑,也可以描述时序逻辑!
- return 语句
- SystemVerilog 增加了 return 语句
- return 语句执行时返回表达式的值,否则最后的返回数值赋值给函数名
- return 语句用于退出 task 和 function
void function
- void function 没有返回值
output
和inout
形式参数为 void function 提供了传递变量的途径void function
可以像 task 一样被调用,但必须跟函数的内容约束一致
- 通过名字传递 task 和 function 参数
- SystemVerilog 通过形式参数的名字传递参数
- 减少错误
- 参数的顺序不受限制
- 传递参数的语法与 Verilog 端口连接的方式相同
SystemVerilog 增强函数形式参数
- 增加了 input 和 output
形式参数的默认方向和类型
- 每一个形式参数都有一个默认的类型
- 调用 task 和 function 时,不必给具有默认参数值的参数传递参数
- 如果不传递参数值,就会使用默认值
eg:
使用引用(
reference
)替代复制的方式传递参数- 常见的向任务(task)和函数(function)传递参数值的方法是复制
- 使用引用(reference)的方式显式的向任务(task)和函数(function)传递参数
- 关键字是:
ref
(取代了 input, output 或者 inout) - 只有自动(
automatic
)任务和函数才可以使用ref
参数
(8*i)+:8
中的:
含义:把 for 循环展开,比如当 i=0 时,(8*0)+:8 0+:8
表示(0+8-1):0(7:0)
;当 i 的 = 1 时,(8*1)+:8 8+:8
表示(8+8-1):8 15:8
,这样通过 for 循环,遍历了data[63:0]
- 表达一个向量可以有三种表示方法:
[MSB:LSB]
、[MSB-:WIDTH]
、[LSB+:WIDTH]
,如依次对应[7:0]
、[7-:8]
([7:0]
)、[0+:8]
([7:0]
)
使用引用(reference)替代复制的方式传递参数
- 通过引用传递的参数可以是只读(read-only)
- 允许 task/function 在调用的范围内引用信息
- 阻止 task/function 修改引用的信息
- 修改 task ref 参数是非常敏感的
- ref 参数可以读取当前的值
- ref 参数可以立即传递信息的变化
参数传递
- 参数类型默认情况下与左侧的参数类型保持一致
- input - 默认情况下,在开始时输入复制一份数值
- output - 在结束时输出复制一份数值
- inout - 在开始时输入,在结束时输出,一份复制的数值
- ref - 通过引用的方式传递,效果立即显现
- 当传递数组给 task 和 function 时,可以节省时间和内容
- const - 参数不允许修改
常见面试题:task 和 function 区别?
- 消耗仿真时间与否,即 task 可以有消耗仿真时间的语句,function 不能有消耗时间的语句,task 不一定就消耗仿真时间。
- task 可以调用 function,function 不能调用 task。
- 在 verilog 中: task 可以返回多个值(output),function 只能返回一个值
- task 是没有 return 的, void function 也是没有 return 的
task 和 function 是否可以被综合?
- 能否被综合,取决于使用者在里面的语句是 RTL 还是行为级描述
- 比如 task 或 function 中有
$display("xxx");
,那么这个 task 或 function 肯定是不能被综合的。wait/#10
等也是不能被综合的!
7.1 逻辑操作符和运算操作符练习
7.1.1、比较运算符 demo
sv_operation.sv
rslt.log
- Makefile 同第 14 篇博文,不过是
comp_file
参数变了:make comp_file=sv_operation.sv
sig_a
表示单比特,sig_m_a
表示多比特
sig_c = 8'sb1100_0111;
中的 s 表示有符号
- 单比特逻辑取反
!
和按位取反~
一样;但是多比特不一样!
comp_xxx
表示比较用的变量
7.1.2、逻辑运算符!
和算术运算符~
的差异 demo
sv_operation.sv
rslt.log
- 对于单比特逻辑取反
!
和按位取反~
无差别;而多比特是有差别的
7.1.3、移位操作 demo
sv_operation.sv
rslt.log
- 逻辑左移 / 右移,移完之后就用
0
来补
- 算数左移同逻辑左移;算数右移如果最高位为
1
,那么就补1
。同理如果最高位为0
,那么就补0
7.1.4、++i
和i++
区别 demo
sv_operation.sv
rslt.log
7.1.5、inside
关键字 demo
sv_operation.sv
rslt.log
7.2、循环练习
7.2.1、不同的initial
块共用同一个全局变量 demo
sv_loop_case.sv
rslt.log
- 可以看到两个 initial 块共用同一个全局变量 i,for 循环有影响!
7.2.2、不同的initial
块使用本地变量 demo
sv_loop_case.sv
rslt.log
- 可以看到两个 intial 块的 for 循环不影响!
7.2.3、不同的initial
块使用automatic
定义变量 demo
sv_loop_case.sv
rslt.log
- 可以看到使用 automatic 定义变量和使用本地变量的效果相同,两个 initial 块的 for 循环也是互不影响
7.2.4、while
和 do...while
执行过程 demo
sv_loop_case.sv
rslt.log
- 可以看到
do...while
会先执行一次,再进行判断
7.3、case/casez/casex 分支区别练习
sv_loop_case.sv
rslt.log
casez
不关心z
,即z
可以当做 0 或 1。
casex
不关心z
和x
,即x
和z
可以当做 0 或 1。
- 问号表示通配符,case 里面的通配
?
,只代表 0 和 1;casez 里面的?
通配,可以代表0/1/z
;casex 里面的?
通配,可以代表0/1/x/z
。
z01z
和001?
也是可以匹配上的,不过已经匹配了前面的1???
,所以后面的001?
就不再匹配了
- 在 case 中,如果有个分支是
z01z
,那么 sel_z 是可以匹配上这个分支的,严格匹配!
7.4、task/function 练习
7.4.1、function 封装结构体 demo
sv_function_task.sv
rslt.log
7.4.2、ref
参数 demo
sv_function_task.sv
rslt.log
- 可以看到更改
ref
引用的变量,那么再调用该变量,该变量就是更改后的值了。
ref
容易犯错误,实际使用并不推荐。
参考
5.SystemVerilog 学习之基本语法 3(面向对象编程... 内含实践练习)
2.1、为什么要使用面向对象编程的方法?
- 建立和维护大型的验证平台
- 创建复杂数据类型以及对数据的操作,封装在 class 中
- 提高开发效率
- 不是使用信号建模,而是利用事务级抽象层次,创建验证平台和系统级模型
- 提供验证平台的可重用性
- 面向对象编程的方法将验证平台跟 RTL 代码的实际细节分隔开,提高了验证平台的鲁棒性、重用性和易维护
- 如何将数据和对数据的操作封装在一起?
- 将输入和输出 RTL 设计的数据放在一起,称为事务(
transcation
) - 事务级建模更容易组织验证平台
2.2、面向对象的基本概念
- 类 Class
- 编码元素,包含所有的属性和功能
- 将数据和对数据的操作封装在一起 Encapsulates
- 提供建立对象的模板
- 可以看做一种数据结构【struct 的升级,struct 只有变量,没有方法】
- 对象 Object
- 对象 object 是类 class 的实体
- 句柄 Handle
- 类型安全的指向对象(object)的指针(pointer)【起始地址】
- 属性 Properties
- 类(class)的实体(object)中包含的各种变量(Variables)【名词】
- 方法 Methods
- 操作变量的任务(task)和函数(function)【动词】
2.3、面向对象编程的基本术语
2.4、面向对象编程的优势
- 传统的编程:分开处理数据结构和算法
- 面向对象编程:通过封装的方式对数据进行组织和管理
- 类(class)封装了数据和对数据的处理算法
- 对象(object)是一个类的实例
- 类(class)是有成员(members)组成的
- 成员(members)可以是属性(properties)(数据或者变量)或者方法(methods)(任务 task 或函数 function)
- OOP 具有继承的特性 - 允许对已经存在的类的成员进行扩展
- OOP 具有多态的特性 - 在运行时将数据和函数进行绑定
2.5、第一个类
面向对象编程 - class
- 类封装了数据和对数据的操作
- 类中的数据称为属性(properties)
- 类中对数据的操作子程序称为方法(methods)【task 和 function】
注:endclass后面的:BusTran是可选的,仅仅是为了人的可读性!
addr ^ data.xor
:变量 data 的各个元素间先相互异或,结果再和变量 addr 进行异或。
对象 objects(类的实例 class instance)
- 一个对象是一个类的实例
2.5、句柄(Handle)
- 使用一个类主要由三步
- 定义一个类
- 声明一个句柄
- 创建一个对象
- 创建对象使用的 new 是圆括号
()
(括号可以省略不写)
2.6、对象内存空间释放
- 释放句柄所指的对象的内存空间
- 如果没有句柄指向一个对象,SystemVerilog 将释放该对象的内存空间
- 当句柄指向一个对象时,SystemVerilog 不会释放该对象的内存空间
- 当句柄设置为 null,将手动释放所有句柄
2.7、使用对象
- 使用点操作符
.
使用变量和函数
2.8、类的子程序(subprocedure)(方法)
- 类中的子程序可以是在类中的 function 或者 task
2.9、在一个类中使用另外一个类
- 一个类可以包含另外一个类的实例,使用句柄指向这个对象
- 类似于 Verilog 中的实例化
- 提供可重用性和控制复杂度
- Statistics Class
- 使用层次化语法 hierarchical
- 显式声明 new 可以对其中具体要做什么事情进行定义(如变量赋予初值);如果默认(没有显式声明),new 只做空间分配的作用!
- 显式声明 new,会新增 new 的功能。
- 上述程序中实例化 BusTran 的时候(new 的时候),也会进行实例化 status,应为我们定义了 new,并在 new 中进行了 stats 的实例化。
2.10、句柄的用法
用法 1/4
- 获取一个对象的句柄
Shallow copy
浅复制
注意,浅拷贝只会拷贝数据,对于数据操作是不会拷贝的!
b2 = b1;
就是一个赋值,即 b2 也是指向同一个 object- 赋值:两个句柄指向同一个对象
用法 2/4
- b2 会浅拷贝 b1 的 i,但是 a 还是会指向同一个!所以改变 i ,不影响其他;改变 a ,所有都受影响。
用法 3/4
- 获取对象的句柄
- 类的属性和实例化对象可以在类声明时直接被初始化
- 浅赋值不会赋值嵌套的对象(知识复制了句柄)
- 可以通过点号
.
操作符对对象中的变量进行操作,比如b1.a.j
- 可以通过手动编写代码实现对所有变量的全复制,包括嵌套的对象
注:上面的copy是需要自己去实现的
用法 4/4
- Deep Copy 深复制
句柄赋值
- 当一个句柄赋值给另一个句柄时,会产生什么效果?
- 目标会获取原对象的所有值
2.11、静态变量
- 如果创建一个变量,这个变量仅仅可以被一个类的所有对象共享,同时这个变量不是全局变量?
- SystemVerilog 中允许在类中声明一个静态变量
- 静态变量跟类的定义相关,跟实例对象无关
- 静态变量用于存储可变数据,如:创建的实例的数量
- 静态变量被类的所有对象共享
- 不管创建多少个 Transaction 的对象,只有一个静态变量 count
- 变量 id 不是静态变量,因此每一个 Transaction 的对象都有一个独立的 id 变量
2.12、OOP:继承 Inheritance
- 如何在类之间共享代码
- 在另外一个类中实例化一个类(或者用嵌套)
- 从一个类继承,生成一个新类(称为继承或派生)
- 继承的特点是可以在原来类的属性基础上添加新的属性
- 添加属性
properties
(数据变量) - 添加方法
methods
(对数据的操作) - 更改方法的行为
- 把通用型的代码编写在一个基类中(
base class
) - 在派生类中添加新的属性
- 优点
- Reuse existing classes from previous projects with less debug
- 可以重用前期项目进行过验证和检查的 bug 少的类
- 不会破坏原有的代码架构
在原有的类基础上增加新功能
单一继承:修改一个类的当前功能
- 在派生类中使用关键字 “
super
” 可以引用基类中的成员
- 如果在派生类中修改了基类中的成员,这必须使用关键字 “super” 获取基类的相关成员
2.13、数据保护
2.13.1、数据保护:本地变量 local
- 派生类不能操作基类中的本地变量(local)
2.13.2、数据保护:保护变量 protected
- 派生类可以操作基类中的保护变量(protected)
- 但是外部代码不能操作该变量
2.14、抽象类和虚方法 virtual methods
virtual class
虚类- 通过通用型的基类可以派生一组相应的子类
- 通用性的基类如
BasePacket
,没有任何实体只是设置了数据包的基本结构,但可以通过派生,生成有用的子类 - 如果基类不会被实例化,所以可以通过关键字 “
virtual
” 声明为抽象类
2.15、OOP:多态 Polymorphism
动态方法检查
- 动态的特性是通过基类的变量可以使用子类的对象,通过基类的变量直接引用子类的方法
- BasePacket 定义的所有虚函数(virtual function)都是公共方法,可以被子类使用
- 创建多个数据包的对象,并放入一个数组
- packets[1] 可以调用 TokenPacket 的 send 函数
调用哪个函数 / 任务?
crc
的形参是父类的packet
,所以crc(p2)
也是执行的父类的compute_crc
- 如果
compute_crc()
声明为virtual
2.16、参数化类
参数化的类
- 定义一个通用性的类,该类中的对象可以根据实际情况进行实例化,比如不同的数组深度或者数据类型
- 参数化的类的语法与 Verilog 的参数机制类似
- 类的实例化类似于 module 或者 Interface 的实例化
- 可以像实例化 module 或者 Interface 一样,实例化参数类
2.17、初始化类的属性
- 使用 OOP 内建的或者用户自定义的 new 函数创建类的实例对象时,可以初始化类的属性
- 用户可以自己定义 new 函数,对数据进行赋值
SystemVerilog 如何确定调用哪个 new 函数?
- 看句柄类型
3.1、OOP Basic Class Demo
3.1.1、句柄地址分配以及 CRC 校验计算
sv_oop_basic.sv
rslt.log
- 任何值和 0 异或是其本身;
bt1.ADDR.xor
结果仍然是5a5a5a5a
,然后与0000_FFFF
异或。5A5A
与FFFF
异或是A5A5
;5A5A
与0000
异或是5A5A
。故最终结果是:5a5a_a5a5
3.1.2、句柄赋值和内存释放 demo
sv_oop_basic.sv
rslt.log
3.1.3、浅复制 demo
sv_oop_basic.sv
rslt.log
- 实验也证明了:浅拷贝不会拷贝嵌套的对象!
- bt2 是浅拷贝过去的,所以
count
不会自加 1!!!
3.2、OOP Extend Class Demo
3.2.1、类继承基本使用 demo
sv_oop_extend.sv
rslt.log
super.calc_crc();
// 调用父类的calc_crc
new
这类特殊函数,会有一些固定的功能(即便不显式表示),比如分配空间。显式表示的功能是在分配空间这些功能之上额外增加的一些功能
- 但是
calc_crc
这类函数是普通函数,是用户自定义的,如果继承类不显式调用父类的calc_crc
,那么它就不具备父类实现的功能!
3.2.2、多态 demo
sv_oop_extend.sv
rslt.log
- 上述最后一行还是调用的
BusTran
的calc_crc
,原因在于形式参数是BusTran bt
在基类和继承类的
calc_crc
前加上virtual
,再来打印如下:rslt.log
- 最后一行的 crc 调用的是继承类的
calc_crc
,bad_crc
我们是在前面initial
块中进行的赋值 1
6.SystemVerilog 学习之基本语法 4(随机化 Randomization)
2.1、为什么使用随机化验证策略(重要)
- 设计复杂度提高之后,直接测试(定向测试)(directed testcase),没有办法通过穷举法验证所有的矢量
- 定向测试案例用于检查确定的设计属性,仅仅用于检查可以预期的错误
- 定向测试方式跟测试时间是线性关系
- 定向测试案例不能检查隐形的错误
方案:带约束的随机化测试案例,对输入的激励随机化
随机化验证策略可以检测设计中的不可预期的错误和隐形的错误
2.2、随机内容有哪些(抽象,了解)
注:随机内容需要根据实际 DUT 要求来确定!
- RTL 设计的配置信息
- 不同的设计配置(随机化)
- 例如验证一个路由器,可以对输入和输出端口配置不同的数量
- 验证环境的配置
- 随机配置整个环境
- 输入数据数量
- 输入数据类型
- 主要的输入数据
- 对输入数据进行随机化
- 在输入数据的有效范围内进行随机化
- 封装的输入数据
- 如果数据进行了一层层封装,不同层的封装可以随机化,比如 TCP/IP(网络报文)
- 协议例外,容错处理和协议违例(DFX)
- 验证系统如何处理错误
- 期望的错误类型,并将错误引入到系统中,确保系统设计可以正确处理这些错误
- 随机的插入这些错误
- 时间延迟(时钟周期)
- 根据协议要求随机插入时间延迟(latency)
- 检查设计对时钟周期的敏感性
- 不需要对建立和保持时间进行验证
2.3、SystemVerilog 的随机化
- 随机化允许用户自动生成随机的输入激励,用于验证功能
- SystemVerilog 允许用户使用特定的约束,将随机输入的数据页数在有效的范围
- 必须在 OOP 中指定随机约束(放到一个 Class 中,语法规定)
2.3.1、rand 随机变量
- 随机变量的值在指定范围内均匀分布
- 如果不添加约束,随机变量的值可以是指定有效范围内的任何值
2.3.2、randc 随机变量
- 周期性随机变量使用关键字
randc
进行声明 - 其取值按照声明的有效范围周期性出现
- 数据类型只可以是
bit
或者enum
- randc 随机变量重复出现范围内的所有值,在依次循环过程中,不会重复出现相同的数值(一个循环介绍后,新的循环自动开始)
2.3.3、带随机变量的类
- Bus 类包含两个随机变量:addr 和 data
constraint
名为 range1 指定了 addr 的数值范围- 确保约束没有冲突
2.3.4、randomize() 函数(启动随机变量)
- 调用
randomize()
函数可以为对象中的所有随机变量赋值 - 启动随机产生:
类的句柄.randomize();
,每调用一次产生一次随机结果
- 随机变量的值要符合约束
- randomize 函数成功时返回 1,失败时返回 0
- 如果随机变量没有添加约束,那么它的随机值可以是有效范围内的任意值
- 产生 50 个 addr 和 data
b.randomize()
调用时刻对b
这个句柄所指内存空间变量值进行随机化
2.3.5、约束解释器
- 解析约束的关系
- 相同的种子(seed)生成相同的随机数(伪随机)
- 使用不同的种子,可以生成一组不同的随机数
- 不同 EDA 工具厂商的约束解析器是定制的
2.3.6、constraint 约束语句块
- 对随机变量的取值进行限制
- 还可以对不同的变量之间的关系进行约束
- 约束语句块是类的成员,类似于
task
,function
和变量 - 约束语句块声明
约束语句的标识符
:表示约束语句块的名字约束语句块
:是一个表达式语句列表,对变量的取值范围或者变量之间的关系进行限制
- 上述 addr 不是随机变量,所以会约束失败
Q:对于大空间验证,就算采取随机化验证,也一定有覆盖不到的地方,对于没覆盖的范围,怎么保证它的正确性
- A:随机覆盖 + 定向用例。看功能覆盖率来判断随机的点达到没有,如果功能覆盖率没有达到,调整随机范围或者 constraint,如果还不行,那就加定向用例!功能覆盖率是通过 feature 来分解的。(随机 + 功能覆盖率)
- 功能覆盖率 CDV(coverage driven verification)
2.3.7、随机化的约束:简单的表达式
- 约束变量必须具有固定的顺序(fixed order)
- 只有一个关系操作符:
<, <=, ==, >=, >
- 多个变量使用多个表达式
2.3.8、随机化的约束:设置范围操作符
- 如果没有其他的约束,
inside
操作符表示的数值范围内任何数值被选中的几率是一样的
- inside 操作符的取反操作符是
!
,表示取值范围不再 inside 操作符所指示的范围内
! inside
表示不在inside
范围内的
2.3.9、随机化的约束:权重分布
- 数值分布操作符:
dist
- 给部分数值增加权重(目的)
- 属性 1:测试案例需要相关的数值
- 属性 2:为了测试结果指定一定的数值分布
- 两个操作符:
:=
和:/
:=
:操作符表示指定的数值具有相同的分布权重:/
:操作符表示指定的数值均分权重,如果权重为 w,数值有 n 个,则每一个数值的权重为w/n
- 数值可以是一个数,也可以是一个数值范围:
[lo:hi]
- 权重不是百分数,相加之后不一定是 100
randc
关键字声明的随机变量不能设置权重
2.3.10、随机化的约束:双向约束
- 约束语句不是过程化语句(
procedural
)而是声明性语句(declarative
) - 所有约束语句同时生效
2.3.11、随机化的约束:条件约束
- 约束语句提供了两种语法用于声明条件关系:
>
if ... else ...
2.3.12、随机化的约束的结果可能性:无约束情况
- 数值可能分布情况
- 每一种组合等概分布
2.3.13、随机化的约束的结果可能性:有约束情况
条件操作符:
->
>
操作符会影响数值的分布情况
>
操作符是双向操作符
- y 的数值取决于 x,当 x = 0 时,y = 0;
- 因此,当 x = 0 时,y 不可能取其他数值,即 x = 0 并且 y != 0 的几率为 0
>
是双向约束的理解:
- 新增加的约束会影响分布
- 当 x=0 时,y=0,但是当 y=0 时,不满足
y>0
条件,所以 x 不为 0,x 只能为 1!
条件操作符:solve … before …
solve ... before ...
语法不会改变数值的有效范围,但是会改变数值出现的几率
solve y before x
不写这一句,除了不会出现的 3 种情况,其余 5 种情况出现的概率是 1/5;
- 写了
solve y before x
这一句,会优先将 y 出现的组合分组,此处 y 有 4 种取值,所以可分为 4 组,每组两个。且每组的概率是 1/4,如果一组两种情况都会出现,那么每种情况的概率是 1/8,如果一组只有一种情况,那么该情况的概率是 1/4!
迭代约束:
foreach
- 对于数组变量,可以使用循环变量进行约束
- 记住核心:从左到右
2.3.14、随机化的约束:functions
约束语句中的函数
- 一些属性不能使用简单的表达式进行约束
- 例如使用循环计算一个数组中的数值的和
- 如果不使用循环,那么就必须将循环展开
- SystemVerilog 约束语句中的表达式可以调用函数
- 函数中不能含有
output
或者ref
参数 - 函数必须是
automatic
- 函数中的约束语句不能修改其他的约束语句
- 必须在约束有效之前调用函数,并且函数的返回值必须看作是状态变量
- 作为函数的参数的随机变量必须创建一个隐式的变量顺序或者优先级
- 如下述代码中的
y
,必须创建一个约束!
2.3.15、随机化的约束:约束保护
- 约束保护作为预测表示,其功能是保护约束的创建
- 约束保护不是约束解释器必须满足的逻辑关系
- 约束保护可以阻止约束解释器生成错误数据
- 约束保护在约束解析之前实施,主要包括:
- 常数
- 状态变量
- 对象句柄比较
2.3.16、激活或关闭随机变量(randomize
的大开关)
- 使用
rand_mode()
函数可以关闭随机变量 - 可以控制随机变量开始(active)还是关闭(inactive)
- 随机变量处于非激活状态时,表示该变量不被声明为 rand 或 randc
rand_mode()
函数是 SV 内建的函数,不能被覆盖
2.3.17、激活或关闭约束(constraint
的大开关)
- 激活或者关闭约束
- 非激活状态的约束不能调用
randomize()
函数实现随机化 - 所有的约束初始状态都是激活状态
constraint_mode()
函数是 SV 内建的函数,不能被覆盖
- 使用格式:
句柄.约束名.constraint_mode()
2.3.18、测试题
- 指针可能为
null
,需要加一个判断保护
7.SystemVerilog 学习之基本语法 5(并发线程... 内含实践练习)
2.1、并发性含义
- 对于所有的并发线程,在仿真工具的当前仿真时间内,安排好的事件在仿真步进到下一个仿真时间之前都会执行完成
2.2、并发线程执行
- 当一个线程执行时,只有遇到
wait
语句才会停止 - 有正在执行的线程产生的子线程按照队列排序执行
- 当正在执行的线程遇到等待语句时,在队列中的
ready
状态的线程可以执行
- 当所有的线程进入
wait
状态时,仿真时间更新
- 等待语句的例子:
2.3、并发线程执行模式
- 当一个线程执行时,其他所有的线程进入队列等待
READY
- 表示在当前仿真时间内执行的线程WAIT
- 表示语句被阻塞(线程不能执行),当遇到等待条件后可以继续执行
- 当正在执行的线程进入
WAIT
状态时,线程进入到WAIT
序列,下一个READY
状态的线程继续执行
- 当所有的线程进入到
WAIT
状态时,仿真时间步进到下一个仿真周期
3.1、语句集合
Veilog
有典型的并发语句集合initial
语句:在整个仿真时间内只执行一次,initial 语句之间都是并发的always
语句:可以对组合电路和时序电路进行建模,always 语句之间都是并发的assign
语句:可以对组合电路进行建模,assign 语句之间都是并发的begin...end
:语句从上到下,顺序执行fork...join
:语句并行执行,与语句顺序无关
fork...join
fork...join
语句块可以创建并行执行的进程;两个fork...join
在initial
中begin..end
之间,两个fork...join
是串行的关系,如果两个fork...join
两个外围又是fork...join
,那么两个fork...join
是并行的。
3.2、fork-join
创建并发线程
- 使用
fork-join
语句创建并发线程
fork-join
中的线程 1 和线程 2 是并行执行的
fork-join
语句中的封装在begin-end
中的语句会形成单一的子线程(线程2.1
和线程2.2
),并按照语句的顺序,从上往下顺序执行
- 并发线程没有固定的先后执行顺序
- 所有子线程共享父线程的变量
3.3、join
选项
注:这里父线程指的是for...join语句块之外下面的线程
fork...join
(与的关系)- 当所有的子线程执行完成后,父线程才会继续执行
- 在
fork...join
语句块中,可以使用begin...end
语句块封装一个独立的线程,该线程中的所有语句是顺序执行的
fork...join_any
(或的关系)- 当
fork...join_any
语句块中的任何一个子线程完成之后,父线程继续执行
fork...join_noe
- 父线程与 for 语句块中的子线程是并行执行的
- 当父线程执行一个阻塞语句后,子线程才开始执行(父线程先执行)
fork...join
和begin...end
例子
fork...join_any
和begin...end
例子
fork...join_none
和begin...end
例子
Q:上面的这些语句前都有标注时间单位,那如果一些语句前面没有时间单位,
fork
语句如何执行呢?- A:没有时间单位,这些语句的先后执行顺序在编译阶段就可以确定,编译阶段确定之后在实际运行时就是顺序执行。【实际工程肯定是有时间单位的!】
3.4、并发线程控制
wait fork
- 等待所有的
fork
并发进程执行完毕 - 在执行父线程之前,确保所有的
fork
并发子线程执行完成 - 等待所有的
fork
并发子线程全部执行完成
- 如果没有
wait fork
,exec5(); 需要等到fork...join_any
中任何一个执行完毕后后执行,而fork...join_none
需要等到exec5()
执行完毕后才去执行。
disable fork
- 停止掉所有并发子线程的执行
3.5、测试题
- A: 2 个;B:2 个;C:1 个;D:1 个
- 不能正常仿真:线程 1 时钟在仿真 0 时刻行为,线程 2 是在 5 个时间单位后,把 a 变成 5,线程是不会仿真到第 5 个时间单位!
- a = 4;b = 8;
- 虽然打印结果来看是并行,但是仿真器还做不到完全并行,下面的还是可以用上面的数值的!(有微小的
δ
时间差)
- a = 7; b = 4;
- 一般我们在工程上不会在 for 循环里使用
fork...join
- 这个错误了解即可!
X.1、fork...join/join_any/join_none
Demo
rslt.log
8.SystemVerilog 学习之基本语法 6(线程内部通信... 内含实践练习)
- 事件 event
- 线程之间执行先后时间顺序的控制
- 旗语(信号量)
- 线程之间共享区域的管理
- 邮箱:mailbox
- 线程之间数据传输使用
注:三种机制应用场景不一样,不能相互替代。
2.1、Verilog event
- Verilog 语言中使用 event 同步线程
- 触发事件的操作符:
>
- 不会阻塞线程的执行
- 等待事件被触发:
@
- 边沿敏感,总会阻塞线程执行
- 只有当事件发生变化时,线程才会继续执行
- 当阻塞线程之前发生触发线程时,可能引起竞争现象
- 触发晚于等待
2.2、SystemVerilog event
- 同步线程
- event 是一个同步对象的句柄,可以党组作参数传递给子程序
- 不需要声明为全局变量,就可以将 event 作为共享资源使用
- 触发一个事件的操作符:
>
和>>
>>
触发需要时钟的边沿完成
- 等待一个事件被触发的操作符:
@
和wait()
@
是等待一个边沿;wait
是等待 一个状态;@
等待一个边沿容易产生永远等不到触发的情况,SV 做了改进,引入了triggered
.
triggered
函数用于检查一个事件是否被触发过,返回值是一个状态
wait(event_variable.triggered)
- 如果在当前的仿真时间范围内,事件曾经被触发过,语句不会被阻塞
- 否则,
wait(event.triggered)
语句会一直等待被触发 - 所以,它是最安全最保险的等待语句
2.3、事件 event :语句阻塞在事件的边沿
- Verilog 语言中使用 event 同步线程,事件持续时间是整个事件触发的那个时间点。
- 触发事件的操作符:
>
(不会阻塞线程的执行)
- 等待事件被触发:
@
(边沿触发)
initial
在软件上来看是并行的,但是实际运行也是有时间差的,所以第一个 initial 会先执行,第二个 initial 会后执行,不过是两个相差时间特别短!
- 故,e1 在第一个 initial 中先被触发,在第二个 initial 中再等待触发,显然 e1 等待不到;e2 在第一个 initial 中先等待,在第二个 initial 中再被触发,所以 e2 可以被触发。
2.4、事件 event:等待一个事件触发
- event 是边沿敏感
- wait(event_variable.triggered) 是电平敏感
- 与触发的时间没有先后
2.5、事件中的循环
- 在两个线程之间通过事件同步时必须注意
- 如果在循环中使用
wait(evnet.triggered)
,必须保证在等待下一个循环之前更新仿真时间 - 否则,代码将进入
0
延迟的循环,将一次又一次的在单一事件触发时进行等待
- 一定要小心零延时循环,建议不要用
wait(event_variable.triggered)
这种用法,它不会阻塞线程;建议还是用@ event_variable
,它会阻塞线程。
2.6、event 可以用作参数
- program 里面的变量默认是静态的,加了 automatic 之后就不是静态的了。
this.done
是类里面实例化对象的 done
2.7、内部线程通信机制:event
- 阻塞事件触发(立即生效)
- 使用
>
操作符 - 触发一个事件不会阻塞当前等待该事件的所有进程
- 触发事件的行为边沿敏感信号
- 非阻塞事件触发(等待到非阻塞触发区间,如上升沿)
- 使用
>>
操作符 - 在事件发生的时间点创建一个非阻塞赋值
- 在仿真时间的非阻塞区(如上升沿)更新事件
- 等待一个事件(SV & Verilog 都有的)
@event_name
@
操作符将会阻止进程执行,直到事件被阻塞- 触发进程执行触发行为之前,等待进程必须执行
@
语句,触发行为不会阻止进程等待事件 - 如果先发生了触发行为,那么等待进程将继续等待
- 持久触发:
triggered
(SV 独有) - 在当前的仿真时间区间内,一个事件被触发过,那么 triggered 事件属性为真,否则为假。
wait(event_name.triggered)
- 举例:
注: event done_too = done;:称为事件的合并(双向)。操作 done 就相当于操作 done_too,同理操作 done_too 也相当于操作 done;
- 事件序列:
wait_order()
- 当指定的一系列事件按照从左到右的顺序发生时,
wait_order
进程开始执行 - 如果指定的一系列事件是乱序执行的,
wait_order
进程将不会执行
- 事件变量:合并变量
- event 是一个独立的数据类型,可以进行赋值
- 当把一个事件赋值给另一个事件时,原事件与目的事件共享原事件
- 当把一个事件赋值给另一个事件时,两个事件合并为一个事件
- 事件变量
- 当事件合并时,赋值操作仅仅会影响目的事件的执行或等待操作
- 当把 event 赋值给 event1 时,如果一个线程正在等待 event1,那么当前的等待线程将被阻塞,无法执行。
- 由于
E2 = E1
导致 T1 这个线程长期阻塞无法执行。
- 解决办法:在 fork 外面先赋值,再去等待 E2 的触发,如下图:
注:while 这里没有时间更新,所以是有问题,跑仿真会一直 block
- 取消事件
- 当一个事件赋值为 null 时,与该时间变量同步的进程无效
- 比较事件
- 不同的事件可以进行比较
- 等于
==
- 不等于
!=
- 全等于
===
(和等于==
并无区别) - 不全等于
!==
- 如果一个事件为 null,其他事件位 1,那布尔表达式为 0
3.1、旗语(信号量) semaphore
- Semaphore 通常用于对共享资源的分配和同步
- 共享资源在不同进程中是互斥使用
- 在内存中创建
semaphore
时,类似于创建一个篮子(bucket
),篮子中包含一定数量的钥匙(keys
)
- 进程在执行之前必须从篮子中获取一个钥匙
- 当一个特定的进程需要钥匙时,只有一定数量的进程在同时运行
- Semaphore 是 SV 内建的类,提供以下方法(
function / task
) - 创建 semaphore:
new(keys_numbers)
- 获取一个或多个钥匙:
get()
- 返还一个或多个钥匙:
put()
- 非阻塞性的获取一个或多个钥匙:
try_get()
- 在验证平台中,常常使用
semaphore
对共享资源进行分配,比如系统总线,在同一个时间点,只能由一个驱动器使用总线
注意:上述函数,如果不写参数,它是具有默认参数的!
3.2、使用new
函数,创建semaphore buckets
- 注意:
sem_a = new[4];
仅仅是声明了数组,每个信号量还没有指明钥匙数量,所以是无法put
的!
- 类创建(new)实体用的是小括号:
sem = new(2);
;数组创建(new)实体用的是方括号:sem_a = new[4];
3.3、信号量获取与释放以及try_get
与get
释义
try_get
取不到就返回 0;get
取不到就一直等
4.1、邮箱 mailbox
- Mailbox 是 SV 不同进程间的通信方式,通过 mailbox 可以在不同进程之间传递信息
- 将一个进程中的数据,通过 mailbox 传递给另外一个进程;当 mailbox 中没有数据时,线程将等待
- mailbox 类似于一个 FIFO,可以设置一定的深度 queue size
- 当邮箱中的信息数量达到邮箱深度时,邮箱为满
- 如果邮箱已经为满,进程就不能再往邮箱中存放信息,直到邮箱中的信息被取走,邮箱不再为满
- 邮箱是 SV 内建的类,提供以下方法
- 创建邮箱:
new()
- 将信息放入邮箱:
put()
- 非阻塞性将信息试着放入邮箱:
try_put()
- 从邮箱中取出信息:
get()
或peek()
- 非阻塞性从邮箱中取出信息:
try_get()
或try_peek()
4.2、使用 new 新建邮箱
4.3、验证平台中的邮箱
- 如何在两个线程之间传递信息?
- 生成器(
generator
)生成事务数据包,然后传递给驱动器(driver
) driver
将数据包传递给DUT
,driver
也要做一些跟时序相关的工作!- 生成器和驱动器必须是异步操作(邮箱是个异步 FIFO)
- 同步操作还得握手,异步操作的发和取是没有约束关系的
这两个类仅仅是定义好了 Generator 和 Driver,相当于图纸,要想使用起来,还需要代码讲其实例化,代码如下:
- Generator 产生的消息通常定义为 Transaction
小结:比如 generator 要向 driver 发送数据,首先会在 gen 里面创建一个邮箱,然后 put 放入数据,再在 driver 里面创建一个邮箱,通过 get 来获取数据。最后需要在 env 里面进行实例化,将两个邮箱连接起来。
4.4、验证平台中非同步线程之间的通信
- 如果生成器和驱动器之间需要同步,则需要添加额外的握手信息
4.5、验证平台中同步线程之间的通信
- 如果生成器和驱动器之间需要同步,则需要添加额外的握手消息
4.6、验证平台中的线程和内部通信
- 很多组件,组件之间的通信就是通过邮箱
环境 ENV,初次了解邮箱使用
- 下面这个也不用太关注,不是实际完整的代码,意义不大,仅仅了解邮箱本节!
X.1、event 练习
x.1.1、线程等待@
和触发>
demo
thread_communication.sv
rslt.log
- 并发线程的并发行为是仿真工具的行为,在零时刻同时发生,但是实际上在指令执行的时候,还是先执行的上面的那一个线程,两者有微小的
δ
时间差
- 上述代码中的
e1
由于是先触发再等待,所以是等不到的!
x.1.2、线程等待wait(event_name.triggered)
和触发>
demo
thread_communication.sv
rslt.log
wait(event_name.triggered)
等待,只要之前触发过就可以!
x.1.3、event
作为参数传递 demo
thread_communication.sv
rslt.log
- 同时,这里也注意体会双向赋值(事件的合并)
X.2、semaphore 练习
thread_communication.sv
rslt.log
- 如果相加延时,但是不可以直接放到
send
中的,应该在fork...join
外面
X.3、mailbox 练习
X.3.1、基本整型传递(邮箱异步)Demo
thread_communication.sv
rslt.log
X.3.2、基本整型传递(邮箱同步)Demo
thread_communication.sv
rslt.log
X.3.3、传递 class 数据
thread_communication.sv
rslt.log
32'ha5a5a5
即32'h00a5_a5a5
class driver
中的tr
并没有new
,这里我们就要再来深入理解一下new
的功效了;tr = new;
的方式实际上是系统随机产生的一个地址(内存空间实体)赋给了tr
,这里尽管没有使用new
但是我们使用了邮箱的get
表示从外部获取地址(内存空间实体)!
9.SystemVerilog 学习之基本语法 7(覆盖率驱动... 内含实践练习)
一、内容概述
- 基于覆盖率驱动的验证技术
- 覆盖率类型:代码覆盖率(工具自动生成,客观)和功能覆盖率(人为定义覆盖点,主观)
- SV 中的功能覆盖率建模
- 定义覆盖率模型:
covergroup
- 定义覆盖点:
coverpoint
- 覆盖点的
bins
- 覆盖率函数
覆盖率驱动 可以用来衡量我们的验证进度,简而言之就是,看我们验了半天验到了什么程度。当然最核心的还是保证我们验证完备性,我们能识别到的点都要验证到。需要澄清的是,覆盖率达到要求,并不能说验证就真正的 OK 了,其实验证的空间还是非常大的,只是说我们的风险变小了,不能说一点风险都没有!故,验证有时候还是我们尽力而为的一个东西。
二、基于覆盖率驱动的验证技术
- 覆盖率是对 RTL 设计功能进行验证后达到的覆盖百分比(量化数据)
- 检查过程必须满足完整性与正确性,没有冗余的劳动
- 为了最小化验证工作量,使用覆盖率来衡量一个设计哪些功能测试过,哪些功能还没有被测试过
- 功能覆盖率是由验证工程师自己定义的,用于衡量设计规格是否被正确实现,具体内容体现在验证计划中
- 功能覆盖率用于检查设计的应用场景、边界条件、特殊变量或者设计条件是否被完整的正确的测试或者确认过
三、功能覆盖率模型
- 定义覆盖率模型
- 编写覆盖率模型用于衡量验证计划的目标是否被实现
- 功能覆盖率模型不能自动的从设计中获取
- 功能覆盖率模型是由验证工程师自己定义,在验证环境中检查设计意图和设计功能是否被正确实现
- 根据设计规格书和验证计划,验证工程师需要定义哪些内容必须被覆盖到(被测试到或者被验证到)
- 步骤
- 定义采样的信号(coverage group)
- 定义采样的时间
四、功能覆盖率收敛
- 覆盖率收敛
- 采用什么样的策略和行为,使得覆盖率达到 100%
- 功能覆盖率适用于衡量测试案例覆盖了哪些设计属性(design feathures)
- 覆盖率收敛是一个反馈环路,用于分析覆盖率的结果并为下一步达到 100% 的覆盖率确定测试方案
- 使用不同的种子(seed)运行现有的测试哪里
- 新增约束(new constraints)
五、功能覆盖率数据
- 收集覆盖率数据
- 使用多个种子运行同一个测试案例
- 检查测试用例(case)是否正确运行(pass/fail)
- 只有当测试用例(case)的仿真结果正确时,功能覆盖率数据才有效
- 因 RTL 设计中的 bug 导致仿真结果不正确时,功能覆盖率数据无效,必须丢弃
- 分析覆盖率
- 在设计中收集代码覆盖率,在验证中收集功能覆盖率
- 收集覆盖率是一个回归的过程!
六、功能覆盖率数据的归一化和分析
- 收集覆盖率数据库,并归一化处理
- 使用不同的种子,多次重复运行随机化验证平台和测试用例
- 将所有测试用例运行的功能覆盖率结果归一化处理,用于衡量验证进度
- 分析覆盖率数据,确认如何修改测试用例
- 如果获取的覆盖率数据保持稳定,需要使用不同的种子测试用例并且测试用例运行时间要延长
- 如果覆盖率增长缓慢,需要增加约束条件获得更多有效激励
- 如果遇到瓶颈,需要创建更多的直接测试用例,满足边界条件
- 当功能覆盖率接近 100% 时,需要检查 bug 出现的几率,如果 bug 经常被发现,说明有一部分设计的覆盖不完整。如果不出现 bug,说明设计验证工作结束。
七、覆盖率的类型:RTL 代码覆盖率
注:代码覆盖率是一个客观条件,即代码风格维度看覆盖率,代码覆盖率很容易也必须(或虽然达不到但可解释)要求达到 100%。
- 代码覆盖率
- 衡量测试用例验证覆盖了哪些设计规格在 RTL 中实现了,而不能衡量验证计划
- 行(
Line Coverage
):RTL 中的代码行 - 有限状态机(
FSM Coverage
):RTL 代码中的有限状态机的状态和状态之间的转化 - 路径(
Path Coverage
):RTL 代码中的路径分支(if-else
语句) - 信号反转(
Toggle Coverage
):RTL 代码中的一个信号从 0 跳变到 1,以及从 1 跳变到 0 - 比导师(
Expression Coverage
):RTL 代码中的条件表达式,例如if(a & b & c)
八、覆盖率的类型:断言覆盖率
注:功能覆盖率更多的去检查逻辑功能,看不到时序信息是否正确,所以就有了断言覆盖率!
- Assertion Coverage 断言覆盖率
- 断言是一种声明性的代码,用于检查 RTL 代码中的信号之间的(时序)关系
- 断言可以使用过程性的代码或者使用
SystemVerilog Assertions
- 断言可以检查信号的值或者设计的状态
cover property
语句
九、覆盖率的类型:功能覆盖率
- 与设计意图有关系
- 功能覆盖率取决于验证计划!
九、覆盖率的类型:功能覆盖率 VS 代码覆盖率
- 100% 代码覆盖率并不意味着 100% 的功能覆盖率!
十、基于覆盖率驱动的验证策略
- 收集信息而不是数据
- 只能衡量使用了哪些内容
- 衡量完整性
十一、覆盖率与缺陷率(Bug rate)的关系
- Bug 出现的几率
- Bug rate 是指新的 RTL 功能缺陷被发现的几率,可以间接的衡量覆盖率
- Bug rate 随着项目和验证的进度不断变化
十二、定义功能覆盖率模型
- covergroup
- 封装覆盖率模型的规格
- 每个 covergroup 包含以下内容
- 一个时钟事件,用于同步采样覆盖点
- 一组覆盖点
- 覆盖点之间的交叉覆盖
- 可选的形式参数
- 覆盖率选项
- Coverfroup 是用户定义的一种结构类型
- 定义好类型之后,可以在不同的程序中多次例化
- 跟 class 类似,定义完成后,可以通过构造函数
new()
生成covergroup
的实例【OOP】 covergroup
可以额定义在module
,program
,interface
或class
中- 一个
covergroup
可以包含一个或多个覆盖点 - 一个覆盖点可以是一个变量或者一个表达式
- 每个覆盖点有一组
bins
值,这个值跟采样的变量或者变量的转换有关 - Bins 的值可以由用户自己定义,或者由 EDA 工具自动生成
- covergroup 的命名要清晰明了,通过名称就可以确认覆盖的功能是什么,最好跟验证计划统一
十三、功能覆盖率的建模
- 以验证计划为起点,编写可以仿真的功能覆盖率模型
- 在验证平台中采样变量和表达式的值(
coverpoints
)
- 在下面的例子中,验证平台随机产生端口值,验证计划中要求遍历所有值
十四、功能覆盖率报告
十五、功能覆盖率的采样事件
- 带有 event 触发的 covergroup
- 当验证平台触发 trans_ready 事件时,采样 CovPort
十六、功能覆盖率:触发 SystemVerilog Assertion
十七、定义覆盖点:信号和表达式
- 采样数据
- 如何收集覆盖率信息?
- 在覆盖点中指定了变量和表达式,SystemVerilog 创建了一组 bins,用于记录那些采样到的数值
- bins 是一个功能覆盖率的衡量单位
- 在每次仿真结束后,生成的数据库中包含了采样后所有的 bins
- EDA 分析工具可以读取这个数据库,生成一个覆盖率报告,报告中包含了设计中哪一部分被覆盖,以及总的覆盖率数值
- 私有 bins 和总的覆盖率
- 计算一个覆盖点的覆盖率,首先确认所有可能数值的总的数量
- 覆盖率等于采样的 bins 值除以总的 bins 的值
- 采样表达式
- 表达式可以被采样,但必须检查覆盖率报告,确保采样值是正确的
注:lens32的覆盖率应该为:24 / 32 = 75% 。范围计算方法:hdr_len 是 3bit,所以一共 8 个组合,payload_len 时 4bit,所以一共 16 个组合。那么,加起来一共是 24 个组合0-23,而总的空间是0-31即 32 个组合,又最后加的是5'b0故 32 个组合中必定有某些值达不到,所以lens32的覆盖率为:24 / 32 = 75%
- 采样数据:
bins
- 私有 bins 和总覆盖率
- SystemVerilog 自动为覆盖点创建 bins
- 一个 N 位的表达式有 2 N 2^N 2N 个有效值
- 一个 3bit 的变量 port 有 8 有效值
- 限制自动生成的 bins 的数量
- covergroup 选项
auto_bin_max
指定自动生成 bins 的最大数量,默认值为 64bins
注:自动生成 bins 这种在实际中应有的比较少!
- Q:什么算是一个功能点?
- A:UT/BT 功能点更多聚焦模块上下接口和时序上面;IT 对应的可能是数据流;ST 对应的可能是系统级的应用场景
- 故,功能点在不同的 level 验证上分解对应的点是不太一样的
十八、定义覆盖点:bins
- 用户定义 bins
- 显式命名
bins
可以提高精度,方便统计覆盖率
注:为什么 23 不会出现呢?hdr_len 的最大值为7,而payload_len的最大值为22,故 len 的最大值为22不会出现 23!
- 覆盖点 bins 的命名
- 定义 bins 时
- 用户限制覆盖率统计时需要的数值
- SystemVerilog 不再自动创建 bins,并且忽略非用户定义的 bins 值
- 只有 用户定义的 bins 的值才可以用于计算功能覆盖率
- 用户默认 bin 值可能被遗忘
十九、定义覆盖点:条件覆盖
- 条件覆盖(下述两种用法等效)
- 使用关键字
iff
为覆盖点添加条件(更简洁) - 使用
start
和stop
函数
二十、定义覆盖点:状态跳转覆盖
注:上面是收集静态的覆盖点,下面来看看状态切换点的覆盖收集。
- Transition Coverage 跳转覆盖率
- 用户定义覆盖点的状态跳转,并收集相关的信息
- 使用
?
等通配符表示状态和状态跳转
注意:上述
二十一、定义覆盖点:交叉覆盖
Cross Coverage
交叉覆盖率- 在覆盖率组中,可以定义两个或多个覆盖点或者变量之间的交叉 覆盖率
注:kind 一共16种组合,port 一共8种组合,交叉一共有16x8=128种组合
二十二、参数化的覆盖率:提供代码的重用性
- 参数化的 covergroup
- SystemVerilog 允许创建参数化的 covergroup,便于创建通用的定义
二十三、covergroup 实战补充(2021-10-15)
二十四、小结
- 基于覆盖率驱动的验证技术
- 为什么是基于覆盖率驱动?整个验证需要看验证进度,需要覆盖率这个可以量化的标准可以看到进展。功能覆盖率一般是 100%,代码覆盖率接近 100%(条件覆盖率需要根据具体的电路类型,一般来讲很难达到 100%,需要根据不同的代码来定)。总之做验证计划的时候有一个覆盖率目标,达到覆盖率目标,才算达到我们的要求!另外需要强调一点,并不是说达到覆盖率我们的验证就 OK 了,达到覆盖率后,需要去看比如缺陷的情况等。如果覆盖率 ok,但缺陷仍持续很高,此时并不能证明验证是收敛的!另外一点,功能覆盖率是验证人员自己来写的,主观写的话,一开始可能就写的不全,故即便达到 100%,也不能说所有的功能就覆盖掉了!因为本身可能写的覆盖率场景本身就没有 cover 到,故整个过程需要做多轮的迭代来完成。
- 总之,记住几个点。1、覆盖率是用来衡量验证进度的标值。2、体现在验证计划中,在验证一开始需要体现一个覆盖率目标。
- 覆盖率类型:代码覆盖率和功能覆盖率
- 代码覆盖率是一种根据代码描写结构去客观的工具自动收集的覆盖率;功能覆盖率是验证人员根据我们要验的功能规格从规格设计书作为一个入口来分析我们要验的 DUT 有哪些功能,然后把这些功能点写出来!
- 代码覆盖率达到要求并不代表功能达到要求!如果代码覆盖率都没达到要求,那么肯定验证是不完备的。
- SV 中的功能覆盖率建模
- 定义覆盖率模型:covergroup
- 定义覆盖点:coverpoint
- 覆盖点的 bins(类似约束限定范围,还有条件覆盖、交叉覆盖、transition 覆盖(随着时间的延续的覆盖))
- 覆盖率函数(传递参数)
X、实践练习
X.1、编写源代码
修改
Makefile
,由于我们需要看覆盖率,所以需要在Makefile
中添加dve
工具相关的命令。Makefile
dve_wave
在后面的 SVA 会用到,现在暂时用不到
- 更改
all
为:comp run dve_cov
dve_cov
是我们收集覆盖率要用到的
cov_demo.sv
port
和data
冒号前面的仅仅是起个名字,有无都可!
- 不给
port
和data
的bins
赋值,那么它就是默认的自动 bins 范围。即port
默认是 [2:0],一共有 2 3 2^3 23 = 8 个;data
默认是 [31:0],一共是 2 32 2^{32} 232 个
- 在
initial
中发 4 个激励,每发一个就采集一次,就可以看看它会命中哪些coverpoint
中的哪些point
;先发四次话,port 一共八种组合,那么肯定不可能达到 100%
X.2、运行源代码
执行
make comp_file=cov_demo.sv
会自动编译运行并弹出DVE
窗口以供我们查看覆盖率!点击左侧的
<Function Groups>
的+
展开该选项,而后双击cov_demo::cov_grp Covergroup definition
在右侧串口可以看到
cov_demo::cov_grp
对应的覆盖率只有14.84%
,这是很低的!可以继续点击其下的cdata
和cport
在右侧会看到变量的哪些覆盖了,哪些没有覆盖。如下图:注:由于使用的是默认的 bins,所以这里标识的是auto。auto bins 的情形下,bins 的分类特别多,覆盖率特别低!
从下图不难看出,
cport
覆盖率为25%
,在右侧可以看到具体覆盖信息,6
和7
个命中了 1 次和 3 次,还有0-5
6 个值没有被覆盖,所以覆盖率为2/8=0.25
。上述实验中,我们仅仅 repeat 了 4 次,导致命中的范围变小,覆盖率只有 25% 也是情理之中。接下来我们修改 repeat 改为 32,增加次数,可以扩大命中范围,也就是
cport
和cdata
变量的覆盖率也会增加!点击右上角关闭 DVE,修改源代码中的 repeat 参数,并重新运行 Makefile 脚本。
运行结果如下图,可以看到总体覆盖率为
69.53%
,显著提升。其中cport
的 8 种情况全部覆盖,所以覆盖率为 100%!接着我们使用自定义 bins,取消 14-18 行注释,并把第 13 行注释。这里是将 data 分为了三类,拿第一类
min
举例来说,只要出现0~100
中的任意一个数,就算命中 min,就算一个等价的测试点!比如产生一个 50,那么就意味着0~100
这样一个范围就已经测试过了!如下图,可以看到
cdata
只覆盖了max
,对于mid
和min
并没有进行覆盖,这个时候我们只能去调整随机激励的约束,让他产生中间值和小值!取消掉第
6-8
行注释,实现对data
的范围进行约束。重新便于DVE
,运行 Makefile 脚本,可以看到cdata
的覆盖率和整体覆盖率均达到了100%
!注:如果某一类没有覆盖到,我们就可以将约束改小,单独的去覆盖它。这里给我们一个启示,可以跑多个用例,每个用例跑多个范围,那么这样的话最终 merge,data 的覆盖率就可以达到 100% 了!
- 作者:Conor
- 链接:https://www.xzhh.top/article/SV2023_1
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。