update sv blogs

change a few translations, change how markdown images are  linked
This commit is contained in:
2024-06-07 09:43:46 +08:00
parent 7b8d5d80cb
commit 85e6bd77a3
2 changed files with 260 additions and 277 deletions
@@ -24,7 +24,7 @@ categories:
验证工程师能学到的最重要的原则是“Bugs are good”,不要因为害怕 bug 而不敢 debug,每次找到 bug 都应该及时告警并记录下来。任何一个项目中都不可能没有 bug,所以在流片之前每发现一个 bug 都意味着用户手中的产品少一个 bug。我们应该尽可能细致深入地去检验设计,找出所有可能存在的 bug,尽管有时候这些 bug 可能很容易修复。
本文需要 Verilog 语言基础,面向对 SystemVerilog HVL (Hard­ware Verification Language, 硬件验证语言) 感兴趣的读者。与 HDL (Hardware Desription Language, 硬件描述语言) 相比,HVL 具有一些特别的性质:
本文需要 Verilog 语言基础,面向对 SystemVerilog HVL (Hard­ware Verification Language, 硬件验证语言) 感兴趣的读者。与 HDL (Hardware Description Language, 硬件描述语言) 相比,HVL 具有一些特别的性质:
1. **约束下的随机激励生成** (*Constrained-random stimulus generation*)。
2. **功能覆盖率** (*Functional coverage*)。
@@ -86,21 +86,19 @@ Testbench 的用途在于**确定 DUT 的正确性**。包含下列步骤:
这种渐进的方法比较容易取得稳步的进展,因而很受那些喜欢看到项目持续向前推进管理者的欢迎。由于创建每个激励向量时并不需要什么基础,所以定向测试的结果也会很快得到。只要给予足够的时间和人力,定向测试对于大部分设计验证来讲都是可以胜任的。
[图 1.1](#图1.1) 显示了定向测试如何逐步覆盖验证计划中的每个特性。每个测试都针对一个特别的设计元素集合。**如果有足够的时间**,可以写出 100% 覆盖整个验证计划所需要的全部测试。
[图 1](#图1) 显示了定向测试如何逐步覆盖验证计划中的每个特性。每个测试都针对一个特别的设计元素集合。**如果有足够的时间**,可以写出 100% 覆盖整个验证计划所需要的全部测试。
<span id="图1.1"></span>
<div align="center">
<img src="../images/post/2022-05-30-josh-systemverilog-1/2022-05-30-josh-systemverilog-1-010-DirectedTestProgressOverTime.png" width=500px alt="图 1.1 定向测试随时间的进展"/>
</div>
<a id="图1"></a>
如果没有足够的时间和资源来完成定向测试该怎么办?如[图 1.1](#图1.1),当在时间往前推进时,覆盖率 (coverage) 可能维持不变。如果设计复杂度翻倍,那么测试就需要增加一倍的时间或者人力,而这种情况是我们所不愿意看到的。因此为了达到 100% 覆盖率的目标,需要一种可以更快找出 bug 的方法。
![图 1 定向测试随时间的进展](../images/post/2022-05-30-josh-systemverilog-1/2022-05-30-josh-systemverilog-1-010-DirectedTestProgressOverTime.png){width=500px}
[图 1.2](#图1.2) 所示为整个设计空间和各种特性被定向测试案例覆盖的情形。在设计空间里有很多特性 (feature),其中有些存在 bug,需要编写各种测试 (test) 去覆盖所有的特性并找出 bug。
如果没有足够的时间和资源来完成定向测试该怎么办?如[图 1](#图1),当在时间往前推进时,覆盖率 (coverage) 可能维持不变。如果设计复杂度翻倍,那么测试就需要增加一倍的时间或者人力,而这种情况是我们所不愿意看到的。因此为了达到 100% 覆盖率的目标,需要一种可以更快找出 bug 的方法。
<span id="图1.2"></span>
<div align="center">
<img src="../images/post/2022-05-30-josh-systemverilog-1/2022-05-30-josh-systemverilog-1-020-DirectedTestCoverage.png" width=800px alt="图 1.2 定向测试的覆盖率"/>
</div>
[图 2](#图2) 所示为整个设计空间和各种特性被定向测试案例覆盖的情形。在设计空间里有很多特性 (feature),其中有些存在 bug,需要编写各种测试 (test) 去覆盖所有的特性并找出 bug。
<a id="图2"></a>
![图 2 定向测试的覆盖率](../images/post/2022-05-30-josh-systemverilog-1/2022-05-30-josh-systemverilog-1-020-DirectedTestCoverage.png){width=800px}
# 4. 方法学基础
@@ -114,12 +112,11 @@ Testbench 的用途在于**确定 DUT 的正确性**。包含下列步骤:
这些原则是相关联的。随机激励对于测试复杂设计十分关键。**定向测试可以找出设计中预期的 bug,而随机测试则能够找出预料不到的 bug**。当使用随机激励时,需要用功能覆盖率来评估验证的进展情况。一旦开始使用自动生成的激励,就需要一种能够**自动预测结果**的方式——通常是记分板或者参考模型。建立包括自预测在内的 testbench 基础设施,是一件工作量很大的事情。分层 testbench (layered testbench) 能够把问题分解为容易处理的小块,这样有助于控制复杂度。事件处理器 (transactors) 能够为构建这些小块提供有用的模式。在适当的规划下,可以建立一个 testbench 所需的基础设施,它们能在所有测试中通用并且不需要经常修改,只需要在某些地方放置“hook”,以便测试能够在这些地方执行调整激励或注入错误等特定操作。相反,针对单一测试的个性化代码必须与 testbench 分开,这样可以避免增加基础设施的复杂度。
建立这种风格的 testbench 所需的时间要比传统的定向 testbench 多得多——尤其是自检的部分。其结果是,可能需要很长的准备时间才能进行第一次可运行的测试。这会给项目管理带来阵痛,所以需要在测试时间表上把这部分考虑进去。从[图 1.3](#图1.3) 中可以看到,第一个随机测试运行前有比较长的初始延迟。
建立这种风格的 testbench 所需的时间要比传统的定向 testbench 多得多——尤其是自检的部分。其结果是,可能需要很长的准备时间才能进行第一次可运行的测试。这会给项目管理带来阵痛,所以需要在测试时间表上把这部分考虑进去。从[图 3](#图3) 中可以看到,第一个随机测试运行前有比较长的初始延迟。
<span id="图1.3"></span>
<div align="center">
<img src="../images/post/2022-05-30-josh-systemverilog-1/2022-05-30-josh-systemverilog-1-030-ConstraineRandomTestProgressOverTimeVSDirectedTesting.png" width=500px alt="图 1.3 约束下的随机测试与定向测试随时间的进度比较"/>
</div>
<a id="图3"></a>
![图 3 约束下的随机测试与定向测试随时间的进度比较](../images/post/2022-05-30-josh-systemverilog-1/2022-05-30-josh-systemverilog-1-030-ConstraineRandomTestProgressOverTimeVSDirectedTesting.png){width=500px}
随机测试的前期准备工作看起来似乎令人不满意,但是其回报却很高。每个随机测试都可以共享这个通用的 testbench,不像每个定向测试都要从零开始编写。每个随机测试都会包含一部分代码,将激励约束到特定的方向上并触发期望的异常,比如创建一个协议违例。其结果是,约束下的随机 testbench 找起 bug 来会比很多定向测试快很多。随着 bug 出现率的下降,应该创建新的随机约束去探索新的区域。最后的几个 bug 可能只能通过定向测试来发现,但是绝大部分的 bug 都应该会在随机测试中出现。
@@ -127,27 +124,25 @@ Testbench 的用途在于**确定 DUT 的正确性**。包含下列步骤:
虽然我们希望仿真器能产生随机激励,但同时又**不希望这些激励数值完全随机**。使用 SystemVerilog 语言可以描述激励的格式 (例如,地址是 32 位;操作码是 ADD 、 SUB 或 STORE;长度 < 32 字节),然后让仿真器产生满足约束的数值。这些数值会被发送到设计中去,同时也会被发送到一个负责预测仿真结果的高层模块中去。设计的实际输出最终需要和预测输出做对比。
[图 1.4](#图1.4) 所示为约束下的随机测试在整个设计空间中的覆盖率。
[图 4](#图4) 所示为约束下的随机测试在整个设计空间中的覆盖率。
- 值得注意的是,**一个随机测试的覆盖范围往往比一个定向测试大**。多出来的覆盖部分可能会与其他测试发生交叠,或者探测到事先没有预料到的新区域 (new area)。
- 在这些新区域中发现 bug 是一件幸运的事情。如果这些对新区域的测试不合法,则需要编写更多的约束去阻止随机测试产生非法的功能。
- 对于那些约束下的随机测试覆盖不到的地方,可能还需要编写一些定向测试。
<span id="图1.4"></span>
<div align="center">
<img src="../images/post/2022-05-30-josh-systemverilog-1/2022-05-30-josh-systemverilog-1-040-ConstrainedRandomTestCoverage.png" width=800px alt="图 1.4 约束下的随机测试覆盖率"/>
</div>
<a id="图4"></a>
[图 1.5](#图1.5) 所示为达到完全覆盖的技术路线:
![图 4 约束下的随机测试覆盖率](../images/post/2022-05-30-josh-systemverilog-1/2022-05-30-josh-systemverilog-1-040-ConstrainedRandomTestCoverage.png){width=800px}
[图 5](#图5) 所示为达到完全覆盖的技术路线:
1. 从左上角的基本的约束下的随机测试 (constrained random test) 开始,使用不同的种子 (seed) 运行。
2. 当我们查看功能覆盖率 (functional covrage) 报告时,注意找出覆盖率中的间隙 (hole),即覆盖盲区。
3. 针对这些盲区进行最小程度代码修改 (mininal code midification),可能是使用新的约束 (constraint),也可能是把错误或延迟加入到 DUT 中。这个外部循环会花掉我们大部分的时间,只有对少数使用随机测试达不到的特性才编写定向测试。
2. 当我们查看功能覆盖率 (functional coverage) 报告时,注意找出覆盖率中的间隙 (hole),即覆盖盲区。
3. 针对这些盲区进行最小程度代码修改 (minimal code modification),可能是使用新的约束 (constraint),也可能是把错误或延迟加入到 DUT 中。这个外部循环会花掉我们大部分的时间,只有对少数使用随机测试达不到的特性才编写定向测试。
<span id="图1.5"></span>
<div align="center">
<img src="../images/post/2022-05-30-josh-systemverilog-1/2022-05-30-josh-systemverilog-1-050-CoverageConvergence.png" width=500px alt="图 1.5 覆盖率收敛"/>
</div>
<a id="图5"></a>
![图 5 覆盖率收敛](../images/post/2022-05-30-josh-systemverilog-1/2022-05-30-josh-systemverilog-1-050-CoverageConvergence.png){width=500px}
# 6. 我们的随机化对象是什么<a name="06"></a>
@@ -210,12 +205,11 @@ Testbench 应该以多快的速度发送激励呢?使用**约束下的随机
## 7.1. 从功能覆盖率到激励的反馈
随机测试需要使用反馈 (feedback)。最初的测试会被运行很多次,使用不同的种子,创建很多互异的输入序列。但是到了最后,即使使用新的种子,所产生的激励也很可能无法在设计空间中探测到新区域。**随着功能覆盖率逐渐接近极限,需要改变测试,以期能找出新的方法去达到那些尚未被覆盖的区域**。这被称为“覆盖率驱动的验证”,如[图 1.6](#图1.6) 所示。
随机测试需要使用反馈 (feedback)。最初的测试会被运行很多次,使用不同的种子,创建很多互异的输入序列。但是到了最后,即使使用新的种子,所产生的激励也很可能无法在设计空间中探测到新区域。**随着功能覆盖率逐渐接近极限,需要改变测试,以期能找出新的方法去达到那些尚未被覆盖的区域**。这被称为“覆盖率驱动的验证”,如[图 6](#图6) 所示。
<span id="图1.6"></span>
<div align="center">
<img src="../images/post/2022-05-30-josh-systemverilog-1/2022-05-30-josh-systemverilog-1-060-TestProgressWithAndWithoutFeedback.png" width=500px alt="图 1.6 带反馈和不带反馈的测试进展"/>
</div>
<a id="图6"></a>
![图 6 带反馈和不带反馈的测试进展](../images/post/2022-05-30-josh-systemverilog-1/2022-05-30-josh-systemverilog-1-060-TestProgressWithAndWithoutFeedback.png){width=500px}
Testbench 有没有可能为我们做到这一点呢?假设现在需要使用 testbench 在每个周期为处理器产生一个总线事件,并为总线事件做出终止判断(成功、校验错误、重试)。在没有使用 HVL 的时候,通常会编写一个很长的定向测试集,然后花费了很多天的工夫编排终止判断代码,并让它们在合适的周期里给出判断。经过了大量的手动分析以后才得出成功的结论达到 100 %的覆盖率。但之后可能由于处理器的时序有了一点微小的改变,不得不重新分析测试并改变激励。
@@ -227,19 +221,17 @@ Testbench 有没有可能为我们做到这一点呢?假设现在需要使用
# 8. Testbench 的构建
在仿真时,**testbench 会把整个 DUT 包围起来**,就像一个硬件测试器连接到一个物理芯片上一样,如[图 1.7](#图1.7) 所示。testbench 和测试仪器都会产生激励并捕捉响应。不同的是,testbench 需要工作在一个很宽的抽象层次范围内,同时创建事件和激励序列并最终转换成比特向量。而测试仪器则只工作在比特级上。
在仿真时,**testbench 会把整个 DUT 包围起来**,就像一个硬件测试器连接到一个物理芯片上一样,如[图 7](#图7) 所示。testbench 和测试仪器都会产生激励并捕捉响应。不同的是,testbench 需要工作在一个很宽的抽象层次范围内,同时创建事件和激励序列并最终转换成比特向量。而测试仪器则只工作在比特级上。
<span id="图1.7"></span>
<div align="center">
<img src="../images/post/2022-05-30-josh-systemverilog-1/2022-05-30-josh-systemverilog-1-070-TheTestbenchDesignEnvironment.png" width=450px alt="图 1.7 Testbench 与设计环境"/>
</div>
<a id="图7"></a>
Testbench 模块里都包含了什么呢?有很多的 BFM (Bus Functional Models, 总线功能模型),可以把它们看成是 testbench 构件 (components) ——从 DUT 的角度看,它们和真实的构件没什么两样,但它们其实只是 testbench 的组成部分,并非 RTL 设计。如果实际应用中设备被连接到 AMBA、USB、PCI 和 SPI 总线上,那么就必须**在 testbench 中建立能够产生激励并校验响应的等效构件**,如[图 1.8](#图1.8) 所示。这些构件并不是带有细节的可综合模型,而是遵循协议并且执行速度更快的高层次事件处理器。如果把设计原型在 FPGA 上实现或者是进行硬件仿真,那么这些 BFM 就需要是可综合的。
![图 7 Testbench 与设计环境](../images/post/2022-05-30-josh-systemverilog-1/2022-05-30-josh-systemverilog-1-070-TheTestbenchDesignEnvironment.png){width=450px}
<span id="图1.8"></span>
<div align="center">
<img src="../images/post/2022-05-30-josh-systemverilog-1/2022-05-30-josh-systemverilog-1-080-TestbenchComponents.png" width=450px alt="图 1.8 Testbench 构件"/>
</div>
Testbench 模块里都包含了什么呢?有很多的 BFM (Bus Functional Models, 总线功能模型),可以把它们看成是 testbench 构件 (components) ——从 DUT 的角度看,它们和真实的构件没什么两样,但它们其实只是 testbench 的组成部分,并非 RTL 设计。如果实际应用中设备被连接到 AMBA、USB、PCI 和 SPI 总线上,那么就必须**在 testbench 中建立能够产生激励并校验响应的等效构件**,如[图 8](#图8) 所示。这些构件并不是带有细节的可综合模型,而是遵循协议并且执行速度更快的高层次事件处理器。如果把设计原型在 FPGA 上实现或者是进行硬件仿真,那么这些 BFM 就需要是可综合的。
<a id="图8"></a>
![图 8 Testbench 构件](../images/post/2022-05-30-josh-systemverilog-1/2022-05-30-josh-systemverilog-1-080-TestbenchComponents.png){width=450px}
# 9. 分层 testbench
@@ -249,7 +241,7 @@ Testbench 模块里都包含了什么呢?有很多的 BFM (Bus Functional Mode
在刚开始学习 Verilog 并尝试写测试程序的时候,这些程序看起来可能会和[例 1.1](#例1.1) 所示的用于执行一个简单 APB (AMBA Peripheral Bus, AMBA 外设总线)写入的低层次代码很相似。
<span id="例1.1"></span>
<a id="例1.1"></a>
``` verilog 例 1.1 驱动 APB 引脚
module test(PAddr,PWrite,PSel,PWData,PEnable,Rst,clk);
@@ -285,7 +277,7 @@ endmodule
经过几天连续编写这种代码以后,我们可能会意识到这是一种重复性的劳动,所以会尝试创建可用于总线写入这种普通操作的任务,如[例 1.2](#例1.2) 所示。
<span id="例1.2"></span>
<a id="例1.2"></a>
``` verilog 例 1.2 一个用于驱动 APB 引脚的任务
task write(reg [15:0] addr, reg [31:0] data);
@@ -306,7 +298,7 @@ endtask
这样,testbench 就会变得简单一些,如[例 1.3](#例1.3)。
<span id="例1.3"></span>
<a id="例1.3"></a>
``` verilog 例 1.3 低层次的 Verilog 测试
module test(PAddr,PWrite,PSel,PWData,PEnable,Rst,clk);
@@ -331,12 +323,11 @@ endmodule
## 9.2. 信号和命令层
[图 1.9](#图1.9) 所示为一个 testbench 中最低的几个层次。
[图 9](#图9) 所示为一个 testbench 中最低的几个层次。
<span id="图1.9"></span>
<div align="center">
<img src="../images/post/2022-05-30-josh-systemverilog-1/2022-05-30-josh-systemverilog-1-090-SignalAndCommandLlayers.png" width=500px alt="图 1.9 信号和命令层"/>
</div>
<a id="图9"></a>
![图 9 信号和命令层](../images/post/2022-05-30-josh-systemverilog-1/2022-05-30-josh-systemverilog-1-090-SignalAndCommandLlayers.png){width=500px}
在底部的**信号层** (*signal*),包含 DUT 和把 DUT 连接到 testbench 的信号。
@@ -344,36 +335,33 @@ endmodule
## 9.3. 功能层
[图 1.10](#图1.10) 给出了为加上**功能层** (*functional*) 的 testbench,功能层向下面对的是命令层。代理 (agent, 在 VMM 中称为事件处理器) 接收到来自上层的事件,例如,DMA 读或写,把它们分解成独立的命令。这些命令也被送往用于预测事件结果的记分板 (scoreboard)。检验器 (checker) 则负责比较来自监视器和记分板的命令。
[图 10](#图10) 给出了为加上**功能层** (*functional*) 的 testbench,功能层向下面对的是命令层。代理 (agent, 在 VMM 中称为事件处理器) 接收到来自上层的事件,例如,DMA 读或写,把它们分解成独立的命令。这些命令也被送往用于预测事件结果的记分板 (scoreboard)。检验器 (checker) 则负责比较来自监视器和记分板的命令。
<span id="图1.10"></span>
<div align="center">
<img src="../images/post/2022-05-30-josh-systemverilog-1/2022-05-30-josh-systemverilog-1-100-TestbenchWithFunctionalLlayerAdded.png" width=500px alt="图 1.10 加上功能层的 testbench"/>
</div>
<a id="图10"></a>
![图 10 加上功能层的 testbench](../images/post/2022-05-30-josh-systemverilog-1/2022-05-30-josh-systemverilog-1-100-TestbenchWithFunctionalLlayerAdded.png){width=500px}
## 9.4. 场景层
[图 1.11](#图1.11) 所示,功能层被位于**场景层** (*scenario*) 中的发生器 (generator) 所驱动。什么是场景呢?记住一点,验证工程师的工作是确保 DUT 能够完成预期的任务。比如 MP3 播放器,它能一边播放事先存储好的音乐,一边从一台主机上下载新的音乐,并且同时对用户输入如音量调整或音轨控制等操作保持响应。这中间的每一个操作都能称为一个场景。下载一个音乐文件需要若于步骤,例如前期准备时的控制寄存器的读写操作,歌曲传送过程中多次 DMA 写操作,以及之后的很多读写操作。场景层就是负责组织协调这些步骤的,操作的参数如音轨大小和寄存器位置等都采用约束下的随机值。
[图 11](#图11) 所示,功能层被位于**场景层** (*scenario*) 中的发生器 (generator) 所驱动。什么是场景呢?记住一点,验证工程师的工作是确保 DUT 能够完成预期的任务。比如 MP3 播放器,它能一边播放事先存储好的音乐,一边从一台主机上下载新的音乐,并且同时对用户输入如音量调整或音轨控制等操作保持响应。这中间的每一个操作都能称为一个场景。下载一个音乐文件需要若于步骤,例如前期准备时的控制寄存器的读写操作,歌曲传送过程中多次 DMA 写操作,以及之后的很多读写操作。场景层就是负责组织协调这些步骤的,操作的参数如音轨大小和寄存器位置等都采用约束下的随机值。
<span id="图1.11"></span>
<div align="center">
<img src="../images/post/2022-05-30-josh-systemverilog-1/2022-05-30-josh-systemverilog-1-110-TestbenchWithScenarioLayerAdded.png" width=500px alt="图 1.11 加上场景层的 testbench"/>
</div>
<a id="图11"></a>
在 testbench **环境** (*environment*) 中的这些块 ([图 1.11](#图1.11) 虚线框内) 是在刚开始开发的时候画出来的。随着项目的进展,它们可能会有一些变化,也可能会加入一些功能,但是这些块对于每个独立的测试都是不应改变的。可以通过在代码中留下“hook”来做到这一点,这样即使这些块的行为需要在测试时改变,也不必重新编写代码。“hook”可以使用工厂模式 (factory patterns) 和回调函数 (callbacks) 来创建。
![图 11 加上场景层的 testbench](../images/post/2022-05-30-josh-systemverilog-1/2022-05-30-josh-systemverilog-1-110-TestbenchWithScenarioLayerAdded.png){width=500px}
在 testbench **环境** (*environment*) 中的这些块 ([图 11](#图11) 虚线框内) 是在刚开始开发的时候画出来的。随着项目的进展,它们可能会有一些变化,也可能会加入一些功能,但是这些块对于每个独立的测试都是不应改变的。可以通过在代码中留下“hook”来做到这一点,这样即使这些块的行为需要在测试时改变,也不必重新编写代码。“hook”可以使用工厂模式 (factory patterns) 和回调函数 (callbacks) 来创建。
## 9.5. 测试的层次和功能覆盖率
现在到了 testbench 的最顶层——测试层,如[图 1.12](#图1.12)。DUT 模块间的 bug 是比较难以发现的,因为这些模块可能是不同的人按照不同的规范设计出来的。
现在到了 testbench 的最顶层——测试层,如[图 12](#图12)。DUT 模块间的 bug 是比较难以发现的,因为这些模块可能是不同的人按照不同的规范设计出来的。
<span id="图1.12"></span>
<div align="center">
<img src="../images/post/2022-05-30-josh-systemverilog-1/2022-05-30-josh-systemverilog-1-120-FullTestbenchWithAllLayers.png" width=500px alt="图 1.12 带着所有层次的完整 testbench"/>
</div>
<a id="图12"></a>
![图 12 带着所有层次的完整 testbench](../images/post/2022-05-30-josh-systemverilog-1/2022-05-30-josh-systemverilog-1-120-FullTestbenchWithAllLayers.png){width=500px}
这个顶层的测试 (test) 就像一个指挥官:他不演奏任何乐器,但引领着其他人的表演。测试包含了用于创建激励的约束。
功能覆盖率 (fonctional coverage) 可以衡量所有测试在满足验证计划要求方面的进展。随着各项测量标准的完成,功能覆盖率代码在整个项目过程中会经常变化。由于代码经常被修改,所以它不作为测试环境的组成部分。
功能覆盖率 (functional coverage) 可以衡量所有测试在满足验证计划要求方面的进展。随着各项测量标准的完成,功能覆盖率代码在整个项目过程中会经常变化。由于代码经常被修改,所以它不作为测试环境的组成部分。
我们可以在约束下的随机环境中创建“定向测试”。只需在随机序列中间插入定向测试的代码,或者把两部分代码并列。定向代码执行期望的任务,而随机的“背景噪声”可能会使 bug 暴露出来,而且 bug 还有可能来自从未被关注过的模块。
@@ -382,7 +370,7 @@ Testbench 中是否需要所有的层次呢?答案要视 DUT 而定。设计
当然,可能还需要更多的层次。如果 DUT 有多个协议层,那么每个层都应该在 testbench 环境中有对应的层。例如使用 IP 封装了 TCP 流量,然后通过以太网数据包的形式发送,对这种情况的测试应该考虑使用三个独立的层来产生和校验数据,如
果能够使用已有的验证构件则更好。
[图 1.12](#图1.12) 中需要注意的最后一点是,它只给出了各块之间一些可能的连接方式,testbench 模块间的连接可能会与之不同。比如测试层可能需要连接到驱动器层以迫使物理 bug 出现。这里给出的只是一些引导——实际当中应该是,需要什么就创建什么。
[图 12](#图12) 中需要注意的最后一点是,它只给出了各块之间一些可能的连接方式,testbench 模块间的连接可能会与之不同。比如测试层可能需要连接到驱动器层以迫使物理 bug 出现。这里给出的只是一些引导——实际当中应该是,需要什么就创建什么。
# 10. 建立一个分层的 testbench
@@ -390,14 +378,13 @@ Testbench 中是否需要所有的层次呢?答案要视 DUT 而定。设计
## 10.1. 创建一个简单的驱动器
首先,来仔细看看其中的一个模块——驱动器。[图 1.13](#图1.13) 所示的驱动器接收来自代理的命令。驱动器可能会注入错误或者增加时延,然后再把命令分解成一些信号的变化,例如总线请求或握手。这样一个 testbench 模块通常被称为“事件处理器 (transactor)”,它的核心部分是一个循环:有关事件处理器的示范代码如[例 1.4](#例1.4) 所示。
首先,来仔细看看其中的一个模块——驱动器。[图 13](#图13) 所示的驱动器接收来自代理的命令。驱动器可能会注入错误或者增加时延,然后再把命令分解成一些信号的变化,例如总线请求或握手。这样一个 testbench 模块通常被称为“事件处理器 (transactor)”,它的核心部分是一个循环:有关事件处理器的示范代码如[例 1.4](#例1.4) 所示。
<span id="图1.13"></span>
<div align="center">
<img src="../images/post/2022-05-30-josh-systemverilog-1/2022-05-30-josh-systemverilog-1-130-ConnectionsForTheDriver.png" width=120px alt="图 1.13 带着所有层次的完整 testbench"/>
</div>
<a id="图13"></a>
<span id="例1.4"></span>
![图 13 带着所有层次的完整 testbench](../images/post/2022-05-30-josh-systemverilog-1/2022-05-30-josh-systemverilog-1-130-ConnectionsForTheDriver.png){width=120px}
<a id="例1.4"></a>
``` verilog 例 1.4 基本的事件处理器代码
task run();
@@ -437,7 +424,7 @@ endtask
1. **清空** (*Sweep*):在最下层完成以后,需要等待 DUT 清空最后的事务。
2. **报告** (*Report*):一旦 DUT 空闲下来,就可以清空遗留在 testbench 中的数据了。有时候保存在记分板里面的数据从来就没有送出来过,这些数据可能是被 DUT 丢弃掉的。可以根据这些信息创建最终报告,说明测试通过或者失败。如果测试失败,务必把相应的功能覆盖率结果删除,因为它们可能是不正确的。
如[图 1.12](#图1.12) 所示,测试启动环境以后,环境就会按上述步骤运行。
如[图 12](#图12) 所示,测试启动环境以后,环境就会按上述步骤运行。
# 12. 最大限度的代码重用
+187 -191
View File
@@ -25,12 +25,12 @@ categories:
date: 2022-05-31 10:18:34
---
和 Verilog 相比,SystemVerilog 提供了很多改进的数据结构。虽然其中的部分结构最初是为设计者创建的,但对测试者也同样有用。本文将介绍这些对验证很有用的数据结构。
和 Verilog 相比,SystemVerilog 提供了很多改进了的数据结构。虽然其中的部分结构最初是为设计人员创建的,但对测试人员也同样有用。本文将介绍这些对验证很有用的数据结构。
SystemVerilog 引进了一些新的数据类型,它们具有如下优点。
SystemVerilog 引进了一些新的数据类型,它们具有如下优点:
1. **双状态数据类型** (*Two-state*):更好的性能,更低的内存消耗。
2. **队列、动态数组和关联数组** (*Queues, dynamic and associative arrays*):减少内存消耗,自带搜索和分类功能。
1. **双状态数据类型** (*Two-state*):性能更好,内存消耗更低。
2. **队列、动态数组和关联数组** (*Queues, dynamic and associative arrays*):内存消耗减少,自带搜索和分类功能。
3. **类和结构** (*Classes and structures:*):支持抽象数据结构。
4. **联合和合并结构** (*Unions and packed structures*):允许对同一数据有多种视图 (view)。
5. **字符串** (*Strings*):支持内建的字符序列。
@@ -44,7 +44,7 @@ Verilog-1995 有两种基本的数据类型:变量 (variable) 和线网 (net)
- 单比特或多比特的无符号数 (unsigned single or multi-bit, `reg [7:0] m`);
- 32 比特的有符号数 (signed 32-bit variable, `integer`);
- 64 比特的无符号数 (unsigned 64-bit variable, `time`)
- 64 比特的无符号数 (unsigned 64-bit variable, `time`);
- 浮点数 (floating point number, `real`)。
若干变量可以被一起存放到定宽数组 (fixed-size array) 里。所有的存储都是静态 (static) 的,意味着所有的变量在整个仿真过程当中都是存活 (alive) 的,子程序 (routine) 不能通过堆栈来保存参数和局部变量。线网可以用来连接设计当中的不同部分,例如门原语和模块实例。尽管线网有很多种用法,但是大多数设计者还是使用标量或矢量 `wire` 来连接各个设计模块的端口。
@@ -55,11 +55,11 @@ SystemVerilog 增加了很多新的数据类型,对设计和验证工程师都
在 Verilog 中,初学者经常分不清 `reg` 和 `wire` 两者的区别。应该使用哪一个来驱动端口?连接不同模块时又该怎么做?SystemVerilog 对经典的 `reg` 数据类型进行了改进,使得它除了作为一个变量以外,还**可以被连续赋值、门单元和模块所驱动**。为了与寄存器类型相区别,这种改进的数据类型称为 `logic`。任何使用线网的地方均可使用 `logic`,但**要求 `logic` 不能有多个结构性的驱动**,例如在对双向总线建模的时候。此时,需要使用线网类型,例如 `wire`,SystemVerilog 会对多个数据来源进行解析以后确定最终值。
[例 2.1](#例2.1) 示范了在 SystemVerilog 中使用 `logic` 类型。
[例 1](#例1) 给出在 SystemVerilog 中使用 `logic` 类型的例子。
<span id="例2.1"></span>
<a id="例1"></a>
``` verilog 例 2.1 <code>logic</code> 类型的使用
``` verilog 例 1 <code>logic</code> 类型的使用
module logic_data_type(input logic rst_h);
parameter CYCLE = 20;
logic q, q_l, d, clk, rst_l;
@@ -82,11 +82,11 @@ endmodule
## 1.2. 双状态数据类型
相比四状态数据类型,SystemVerilog 引入的双状态数据类型有利于提高仿真器的性能并减少内存的使用量。最简单的双状态数据类型是 `bit`,它是无符号的。另四种带符号的双状态数据类型是 `byte`,`shortint`,`int` 和 `longint`,如[例 2.2](#例2.2) 所示。
相比四状态数据类型,SystemVerilog 引入的双状态数据类型有利于提高仿真器的性能并减少内存的使用量。最简单的双状态数据类型是 `bit`,它是无符号的。另四种带符号的双状态数据类型是 `byte`,`shortint`,`int` 和 `longint`,如[例 2](#例2) 所示。
<span id="例2.2"></span>
<a id="例2"></a>
``` verilog 例 2.2 带符号的数据类型
``` verilog 例 2 带符号的数据类型
bit b; // 双状态,单比特
bit [31:0] b32; // 双状态,32 比特无符号整数
int unsigned ui; // 双状态,32 比特无符号整数
@@ -102,11 +102,11 @@ real r; // 双状态,双精度浮点数
> 虽然使用 `byte` 等数据类型来替代类似 `logic[7:0]` 进行声明可以让程序更简洁,但需要注意,这些新的数据类型都是**带符号**的,所以 `byte` 变量的最大值只有 127,而不是 255 (它的范围是 -128~127)。可以使用 `byte unsigned`,但这其实比使用 `byte [7:0]` 还要麻烦。在进行随机化时,带符号变量可能会造成意想不到的结果,这一点在后面会讨论到。
> 在把双状态变量连接到 DUT 务必要小心,尤其是连接到 DUT 的输出时,因为 DUT 产生的 `X` 或 `Z` 会被转换成双状态值,而测试代码可能永远也察觉不了。这些值被转换成了 `0` 还是 `1` 并不重要,重要的是要**随时检查未知值的传播**。使用 `$isunknown()` 操作符,可以在表达式的任意位出现 `X` 或 `Z` 时返回 `1`,如[例 2.3](#例2.3)。
> 在把双状态变量连接到 DUT 务必要小心,尤其是连接到 DUT 的输出时,因为 DUT 产生的 `X` 或 `Z` 会被转换成双状态值,而测试代码可能永远也察觉不了。这些值被转换成了 `0` 还是 `1` 并不重要,重要的是要**随时检查未知值的传播**。使用 `$isunknown()` 操作符,可以在表达式的任意位出现 `X` 或 `Z` 时返回 `1`,如[例 3](#例3)。
<span id="例2.3"></span>
<a id="例3"></a>
``` verilog 例 2.3 对四状态值的检查
``` verilog 例 3 对四状态值的检查
if ($isunknown(iport) == 1)
$display("@%0t: 4-state value detected on iport %b",
$time, iport);
@@ -122,18 +122,18 @@ if ($isunknown(iport) == 1)
Verilog 要求在声明中必须给出数组的上下界。因为几乎所有数组都使用 `0` 作为索引下界,所以 SystemVerilog **允许只给出数组宽度的便捷声明方式**,跟 C 语言类似。
<span id="例2.4"></span>
<a id="例4"></a>
``` verilog 例 2.4 定宽数组的声明
``` verilog 例 4 定宽数组的声明
int lo_hi[0:15]; // 16 个整数 [0]..[15]
int c_style[16]; // 16 个整数 [0]..[15]
```
可以通过在变量名后面指定维度的方式来创建**多维定宽数组** (*multidimensional fixed-size array*)。[例 2.5](#例2.5) 创建了几个二维的整数数组,大小都是 8 行 4 列,最后一个元素的值被设置为 1。多维数组在 Verilog-2001 中已经引入,但这种紧凑型声明方式却是新的。
可以通过在变量名后面指定维度的方式来创建**多维定宽数组** (*multidimensional fixed-size array*)。[例 5](#例5) 创建了几个二维的整数数组,大小都是 8 行 4 列,最后一个元素的值被设置为 1。多维数组在 Verilog-2001 中已经引入,但这种紧凑型声明方式却是新的。
<span id="例2.5"></span>
<a id="例5"></a>
``` verilog 例 2.5 声明并使用多维数组
``` verilog 例 5 声明并使用多维数组
int array2 [0:7][0:3]; // 完整的声明
int array3 [8][4]; // 紧凑的声明
@@ -147,28 +147,27 @@ array2[7][3] = 1; // 设置最后一个元素
这适用于所有数组类型,包括定宽数组、动态数组、关联数组和队列,也同时适用于地址中含有 `X` 或 `Z` 的情况。线网在没有驱动的时候输出是 `Z`。
很多 SystemVerilog 仿真器**在存放数组元素时使用 32 比特的字 (word) 边界**,所以 `byte`,`shortint` 和 `int` 都是存放在一个字中,`longint` 存放到两个字中。如[例 2.6](#例2.6) 所示,**在非合并数组中,字的低位用来存放数据,高位不使用**。[图 2.1](#图2.1) 所示的字节数组 `b_unpack` 被存放到三个字的空间里。
很多 SystemVerilog 仿真器**在存放数组元素时使用 32 比特的字 (word) 边界**,所以 `byte`,`shortint` 和 `int` 都是存放在一个字中,`longint` 存放到两个字中。如[例 6](#例6) 所示,**在非合并数组中,字的低位用来存放数据,高位不使用**。[图 1](#图1) 所示的字节数组 `b_unpack` 被存放到三个字的空间里。
<span id="例2.6"></span>
<a id="例6"></a>
``` verilog 例 2.6 非合并数组的声明
``` verilog 例 6 非合并数组的声明
bit [7:0] b_unpack[3]; // 非合并数组
```
<span id="图2.1"></span>
<div align="center">
<img src="../images/post/2022-05-31-josh-systemverilog-2/2022-05-31-josh-systemverilog-2-010-UnpackedArrayStorage.png" width=600px alt="图 2.1 非合并数组的存放"/>
</div>
<a id="图1"></a>
![图 1 非合并数组的存放](../images/post/2022-05-31-josh-systemverilog-2/2022-05-31-josh-systemverilog-2-010-UnpackedArrayStorage.png){width=600px}
仿真器通常使用两个或两个以上连续的字来存放 `logic` 和 `integer` 等四状态类型,这**比存放双状态变量多占用一倍的空间**。
## 2.2. 常量数组
[例 2.7](#例2.7) 示范了如何使用常量数组,即**用一个单引号加大括号来初始化数组** (注意这里的单引号并不同于编译器指引或宏定义中的单引号),可以一次性地为数组的部分或所有元素赋值。在大括号前标上重复次数可以对多个元素重复赋值,还可以为没有显式赋值的元素指定一个默认值。
[例 7](#例7) 示范了如何使用常量数组,即**用一个单引号加大括号来初始化数组** (注意这里的单引号并不同于编译器指引或宏定义中的单引号),可以一次性地为数组的部分或所有元素赋值。在大括号前标上重复次数可以对多个元素重复赋值,还可以为没有显式赋值的元素指定一个默认值。
<span id="例2.7"></span>
<a id="例7"></a>
``` verilog 例 2.7 初始化一个数组
``` verilog 例 7 初始化一个数组
int ascend[4] = '{0,1,2,3}; // 对 4 个元素进行初始化
int descend[5];
@@ -180,11 +179,11 @@ descend = '{9, 8, default:1}; // {9,8,1,1,1}
## 2.3. 基本的数组操作—— `for` 和 `foreach`
操作数组的最常见的方式是使用 `for` 或 `foreach` 循环。在[例 2.8](#例2.8) 中,`i` 被声明为 `for` 循环内的局部变量。SystemVerilog 的 `$size` 函数返回数组的宽度。在 `foreach` 循环中,只需要给出数组名并在后面的方括号中给出索引变量,SystemVerilog 便会自动遍历数组中的元素。索引变量将自动声明,并只在循环内有效。
操作数组的最常见的方式是使用 `for` 或 `foreach` 循环。在[例 8](#例8) 中,`i` 被声明为 `for` 循环内的局部变量。SystemVerilog 的 `$size` 函数返回数组的宽度。在 `foreach` 循环中,只需要给出数组名并在后面的方括号中给出索引变量,SystemVerilog 便会自动遍历数组中的元素。索引变量将自动声明,并只在循环内有效。
<span id="例2.8"></span>
<a id="例8"></a>
``` verilog 例 2.8 在数组操作中使用 <code>for</code> 和 <code>foreach</code> 循环
``` verilog 例 8 在数组操作中使用 <code>for</code> 和 <code>foreach</code> 循环
initial begin
bit [31:0] src[5], dst[5];
for (int i=0; i<$size(src); i++)
@@ -194,11 +193,11 @@ initial begin
end
```
注意在[例 2.9](#例2.9) 中,对多维数组使用 `foreach` 的语法可能会与设想的有所不同。使用时并不是像 `[i][j]` 这样把每个下标分别列在不同的方括号里,而是用逗号隔开后放在同一个方括号里,像 `[i,j]`。
注意在[例 9](#例9) 中,对多维数组使用 `foreach` 的语法可能会与设想的有所不同。使用时并不是像 `[i][j]` 这样把每个下标分别列在不同的方括号里,而是用逗号隔开后放在同一个方括号里,像 `[i,j]`。
<span id="例2.9"></span>
<a id="例9"></a>
``` verilog 例 2.9 初始化并遍历多维数组
``` verilog 例 9 初始化并遍历多维数组
int md[2][3] = '{'{0,1,2}, '{3,4,5}};
initial begin
@@ -214,11 +213,11 @@ initial begin
end
```
[例 2.9](#例2.9) 的输出结果如[例 2.10](#例2.10) 所示。
[例 9](#例9) 的输出结果如[例 10](#例10) 所示。
<span id="例2.10"></span>
<a id="例10"></a>
``` plaintext 例 2.10 多维数组元素值的打印输出结果
``` plaintext 例 10 多维数组元素值的打印输出结果
Initial value:
md[0][0] = 0
md[0][1] = 1
@@ -235,11 +234,11 @@ md[1][1] = 5
md[1][2] = 5
```
**如果不需要遍历数组中的所有维度,可以在 `foreach` 循环里忽略不需要的维度**。[例 2.11](#例2.11) 把一个二维数组打印成一个方形的阵列。它在外层循环中遍历第一个维度,然后在内层循环中遍历第二个维度。
**如果不需要遍历数组中的所有维度,可以在 `foreach` 循环里忽略不需要的维度**。[例 11](#例11) 把一个二维数组打印成一个方形的阵列。它在外层循环中遍历第一个维度,然后在内层循环中遍历第二个维度。
<span id="例2.11"></span>
<a id="例11"></a>
``` verilog 例 2.11 打印一个多维数组
``` verilog 例 11 打印一个多维数组
initial begin
byte twoD[4][6];
foreach(twoD[i,j])
@@ -254,11 +253,11 @@ initial begin
end
```
[例 2.11](#例2.11) 的输出结果如[例 2.12](#例2.12) 所示。
[例 11](#例11) 的输出结果如[例 12](#例12) 所示。
<span id="例2.12"></span>
<a id="例12"></a>
``` plaintext 例 2.12 打印多维数组的输出结果
``` plaintext 例 12 打印多维数组的输出结果
Initial value:
0: 0 1 2 3 4 5
1: 10 11 12 13 14 15
@@ -273,11 +272,11 @@ Initial value:
## 2.4. 基本的数组操作——复制和比较
可以在不使用循环的情况下对数组进行聚合比较和复制 (aggregate compare and copy, 聚合操作适用于整个数组而不是单个元素),其中比较**只限于等于比较或不等于比较**。[例 2.13](#例2.13) 列出了几个比较的例子。操作符 `? :` 是一个迷你型的 `if` 语句,在[例 2.13](#例2.13) 中用来对两个字符串进行选择。例子最后的比较语句使用了数组的一部分,`src[1:4]`,它实际上产生了一个有四个元素的临时数组。
可以在不使用循环的情况下对数组进行聚合比较和复制 (aggregate compare and copy, 聚合操作适用于整个数组而不是单个元素),其中比较**只限于等于比较或不等于比较**。[例 13](#例13) 列出了几个比较的例子。操作符 `? :` 是一个迷你型的 `if` 语句,在[例 13](#例13) 中用来对两个字符串进行选择。例子最后的比较语句使用了数组的一部分,`src[1:4]`,它实际上产生了一个有四个元素的临时数组。
<span id="例2.13"></span>
<a id="例13"></a>
``` verilog 例 2.13 数组的复制和比较操作
``` verilog 例 13 数组的复制和比较操作
initial begin
bit [31:0] src[5] = '{0,1,2,3,4},
dst[5] = '{5,4,3,2,1};
@@ -307,11 +306,11 @@ end
## 2.5. 同时使用位下标和数组下标
在 Verilog-1995 中一个很不方便的地方就是数组下标 (array subscript) 和位下标 (bit subscript) 不能同时使用。Verilog-2001 对定宽数组取消了这个限制。[例 2.14](#例2.14) 打印出数组的第一个元素 (二进制 101 )、它的最低位 (1) 以及紧接的高两位 (二进制 10)。
在 Verilog-1995 中一个很不方便的地方就是数组下标 (array subscript) 和位下标 (bit subscript) 不能同时使用。Verilog-2001 对定宽数组取消了这个限制。[例 14](#例14) 打印出数组的第一个元素 (二进制 101 )、它的最低位 (1) 以及紧接的高两位 (二进制 10)。
<span id="例2.14"></span>
<a id="例14"></a>
``` verilog 例 2.14 同时使用数组下标和位下标
``` verilog 例 14 同时使用数组下标和位下标
initial begin
bit [31:0] src[5] = '{5{5}};
$displayb(src[0],, // 'b101 或 'd5
@@ -328,11 +327,11 @@ end
## 2.7. 合并数组的例子
声明合并数组时,**合并的位和数组大小是数据类型的一部分,必须在变量名前面指定**。数组大小定义的格式必须是 `[msb:lsb]`,而不是 `[size]`。[例 2.15](#例2.15) 中的变量 `bytes` 是一个有 4 个字节的合并数组,使用单独的 32 比特的字来存放,如图 2.2 所示。
声明合并数组时,**合并的位和数组大小是数据类型的一部分,必须在变量名前面指定**。数组大小定义的格式必须是 `[msb:lsb]`,而不是 `[size]`。[例 15](#例15) 中的变量 `bytes` 是一个有 4 个字节的合并数组,使用单独的 32 比特的字来存放,如图 2 所示。
<span id="例2.15"></span>
<a id="例15"></a>
``` verilog 例 2.15 合并数组的声明和用法
``` verilog 例 15 合并数组的声明和用法
bit [3:0] [7:0] bytes; // 4 个 字节 组装成 32 比特
bytes = 32'hCafe_Dada;
$displayh(bytes,, // 显示所有的 32 比特
@@ -340,16 +339,15 @@ $displayh(bytes,, // 显示所有的 32 比特
bytes[3][7]); // 显示 MSB “1”
```
<span id="图2.2"></span>
<div align="center">
<img src="../images/post/2022-05-31-josh-systemverilog-2/2022-05-31-josh-systemverilog-2-020-PackedArrayLayout.png" width=600px alt="图 2.2 合井数组存放示意图"/>
</div>
<a id="图2"></a>
合并和非合并数组可以混合使用。可以使用数组来表示存储单元,这些单元可以按比特、字节或长字的方式进行存取。在[例 2.16](#例2.16) 中,`barray` 是一个具有 3 个合并元素的非合并数组。
![图 2 合井数组存放示意图](../images/post/2022-05-31-josh-systemverilog-2/2022-05-31-josh-systemverilog-2-020-PackedArrayLayout.png){width=600px}
<span id="例2.16"></span>
合并和非合并数组可以混合使用。可以使用数组来表示存储单元,这些单元可以按比特、字节或长字的方式进行存取。在[例 16](#例16) 中,`barray` 是一个具有 3 个合并元素的非合并数组。
``` verilog 例 2.16 合并数组的声明和用法
<a id="例16"></a>
``` verilog 例 16 合并数组的声明和用法
bit [3:0] [7:0] barray [3]; // 合并,3×32 比特
bit [31:0] lw = 32'h0123_4567; // 字
bit [7:0] [3:0] nibbles; // 合并数组
@@ -359,12 +357,11 @@ barray[0][1][6] = 1'b1;
nibbles = barray[0]; // 复制合并数组的元素值
```
[例 2.15](#例2.15) 中的变量 `bytes` 是一个具有 4 个字节的合并数组,以单字形式存放。`barray` 则是一个具有 3 个类似 `bytes` 元素的数组,其存放形式如图 2.3。
[例 15](#例15) 中的变量 `bytes` 是一个具有 4 个字节的合并数组,以单字形式存放。`barray` 则是一个具有 3 个类似 `bytes` 元素的数组,其存放形式如图 3。
<span id="图2.3"></span>
<div align="center">
<img src="../images/post/2022-05-31-josh-systemverilog-2/2022-05-31-josh-systemverilog-2-030-PackedArrayBitLayout.png" width=600px alt="图 2.3 合并数组存放示意图"/>
</div>
<a id="图3"></a>
![图 3 合并数组存放示意图](../images/post/2022-05-31-josh-systemverilog-2/2022-05-31-josh-systemverilog-2-030-PackedArrayBitLayout.png){width=600px}
- 使用一个下标,可以得到一个字的数据,如 `barray[2]`;
- 使用两个下标,可以得到一个字节的数据,如 `barray[0][3]`;
@@ -372,13 +369,13 @@ nibbles = barray[0]; // 复制合并数组的元素值
注意数组声明中在变量名后面指定了数组的大小,`barray[3]`,这个维度是非合并的,所以在使用该数组时至少要有一个下标。
[例 2.16](#例2.16) 中的最后一行在两个合并数组间实现复制。由于操作是以比特为单位进行的,所以即使数组维度不同也可以进行复制。
[例 16](#例16) 中的最后一行在两个合并数组间实现复制。由于操作是以比特为单位进行的,所以即使数组维度不同也可以进行复制。
## 2.8. 合井数组和非合并数组的选择
应该选择合并数组还是非合并数组呢?当需要和标量进行相互转换时,使用合并数组会非常方便。例如,以字节或字为单位对存储单元进行操作。[图 2.3](#图2.3) 中所示的 `barray` 可以满足这一要求。任何数组类型都可以合并,包括动态数组、队列和关联数组。
应该选择合并数组还是非合并数组呢?当需要和标量进行相互转换时,使用合并数组会非常方便。例如,以字节或字为单位对存储单元进行操作。[图 3](#图3) 中所示的 `barray` 可以满足这一要求。任何数组类型都可以合并,包括动态数组、队列和关联数组。
**如果需要等待数组中的变化,则必须使用合并数组**。当 testbench 需要通过存储器数据的变化来唤醒时,需要使用 `@` 操作符,但 **`@` 操作符只能用于标量或者合并数组**。在[例 2.16](#例2.16) 中,可以把 `lw` 和 `barray[0]` 用作敏感信号,但却不能用整个的 `barray` 数组,除非把它扩展成:`@(barray[0] or barray[1] or barray[2])`。
**如果需要等待数组中的变化,则必须使用合并数组**。当 testbench 需要通过存储器数据的变化来唤醒时,需要使用 `@` 操作符,但 **`@` 操作符只能用于标量或者合并数组**。在[例 16](#例16) 中,可以把 `lw` 和 `barray[0]` 用作敏感信号,但却不能用整个的 `barray` 数组,除非把它扩展成:`@(barray[0] or barray[1] or barray[2])`。
# 3. 动态数组
@@ -386,11 +383,11 @@ nibbles = barray[0]; // 复制合并数组的元素值
SystemVerilog 提供了动态数组类型,可以**在仿真时分配空间或调整宽度**,这样在仿真中就可以使用最小的存储星。
动态数组在声明时使用空的下标 `[]`,表示数组的宽度不在编译时给出,在程序运行时再指定。数组在最开始时是空的,所以必须调用 `new[]` 操作符来分配空间,同时在方括号中传递数组宽度。可以把数组名传给 `new[]` 构造符,并把已有数组的值复制到新数组里,如[例 2.17](#例2.17) 所示。
动态数组在声明时使用空的下标 `[]`,表示数组的宽度不在编译时给出,在程序运行时再指定。数组在最开始时是空的,所以必须调用 `new[]` 操作符来分配空间,同时在方括号中传递数组宽度。可以把数组名传给 `new[]` 构造符,并把已有数组的值复制到新数组里,如[例 17](#例17) 所示。
<span id="例2.17"></span>
<a id="例17"></a>
``` verilog 例 2.17 使用动态数组
``` verilog 例 17 使用动态数组
int dyn[], d2[]; // 声明动态数组
initial begin
@@ -406,7 +403,7 @@ initial begin
end
```
在[例 2.17](#例2.17) 中:
在[例 17](#例17) 中:
- A:调用 `new[5]` 为动态数组 `dyn` 分配了 5 个整型元素。
- B:把数组的索引值赋给相应的元素。
@@ -418,11 +415,11 @@ end
调用系统函数 `$size` 可以返回数组的宽度。动态数组有一些内建的子程序 (rou­tines),例如 `delete` 和 `size`。
如果想声明一个常数数组但又不想统计元素的个数,可以使用动态数组并使用常量数组进行赋值。在[例 2.18](#例2.18) 中声明的 `mask` 数组具有 9 个 8 比特元素,SystemVerilog 会自动统计元素的个数,这比声明一个定宽数组然后不小心把宽度错指为 8 要好。
如果想声明一个常数数组但又不想统计元素的个数,可以使用动态数组并使用常量数组进行赋值。在[例 18](#例18) 中声明的 `mask` 数组具有 9 个 8 比特元素,SystemVerilog 会自动统计元素的个数,这比声明一个定宽数组然后不小心把宽度错指为 8 要好。
<span id="例2.18"></span>
<a id="例18"></a>
``` verilog 例 2.18 使用动态数组来保存元素数量不定的列表
``` verilog 例 18 使用动态数组来保存元素数量不定的列表
bit [7:0] mask[] = '{8'b0000_0000, 8'b0000_0001,
8'b0000_0011, 8'b0000_0111,
8'b0000_1111, 8'b0001_1111,
@@ -438,13 +435,13 @@ bit [7:0] mask[] = '{8'b0000_0000, 8'b0000_0001,
SystemVerilog 引进了一种新的数据类型——队列 (queue),它结合了链表和数组的优点。队列与链表相似,**可以在一个队列中的任何地方增加或删除元素**,这类操作在性能上的损失比动态数组小得多,因为动态数组需要分配新的数组并复制所有元素的值。队列与数组相似,可以通过索引实现对任一元素的访问,而不需要像链表那样去遍历目标元素之前的所有元素。
队列的声明是使用带有美元符号的下标:`[$]`。队列元素的编号从 `0` 到 `$`。[例 2.19](#例2.19) 示范了如何使用方法 (method) 在队列中增加和删除元素。注意队列的常量 (literal) 只有大括号而没有数组常量中开头的单引号。
队列的声明是使用带有美元符号的下标:`[$]`。队列元素的编号从 `0` 到 `$`。[例 19](#例19) 示范了如何使用方法 (method) 在队列中增加和删除元素。注意队列的常量 (literal) 只有大括号而没有数组常量中开头的单引号。
SystemVerilog 的队列类似于标准模板库 (standard template library) 中的双端队列。通过增加元素来创建队列,SystemVerilog 会分配额外的空间以便新元素能够被快速插入。当元素增加到超过原有空间的容量时,SystemVerilog 会自动分配更多的空间。因此队列可以扩大或缩小,但不用像动态数组那样在性能上付出很大代价,System­Verilog 会随时记录闲置的空间。注意不要对队列使用构造函数 `new[]`。
<span id="例2.19"></span>
<a id="例19"></a>
``` verilog 例 2.19 队列的操作
``` verilog 例 19 队列的操作
int j = 1,
q2[$] = {3,4}, // 队列的常量不需要使用 “'”
q[$] = {0,2,5}; // {0,2,5}
@@ -465,11 +462,11 @@ initial begin
end
```
还可以使用字下标串联来替代方法。如果把 `$` 放在一个范围表达式的左边,那么 `$` 将代表最小值,例如 `[$:2]` 就代表 `[0:2]`。如果 `$` 放在表达式的右边,则代表最大值,如[例 2.20](#例2.20) 中初始化块的第一行里的 `[1:$]` 就表示 `[1:2]`。
还可以使用字下标串联来替代方法。如果把 `$` 放在一个范围表达式的左边,那么 `$` 将代表最小值,例如 `[$:2]` 就代表 `[0:2]`。如果 `$` 放在表达式的右边,则代表最大值,如[例 20](#例20) 中初始化块的第一行里的 `[1:$]` 就表示 `[1:2]`。
<span id="例2.20"></span>
<a id="例20"></a>
``` verilog 例 2.20 队列的操作
``` verilog 例 20 队列的操作
int j = 1,
q2[$] = {3,4}, // 队列的常量不需要使用 “'”
q[$] = {0,2,5}; // {0,2,5}
@@ -499,22 +496,21 @@ end
如果只是偶尔需要创建一个大容量的数组,那么动态数组已经足够好用了,但是如果需要超大容量的呢?假设正在对一个有着几个 G 字节寻址范围的处理器进行建模。在典型的测试中,这个处理器可能只访问了用来存放可执行代码和数据的几百或几千个字节,这种情况下对几 G 字节的存储空间进行分配和初始化显然是浪费的。
SystemVerilog 提供了关联数组 (associative array) 类型,用来保存稀疏矩阵 (sparse matrix) 的元素。这意味着当对一个非常大的地址空间进行寻址时,SystemVerilog 只为实际写入的元素分配空间。在[图 2.4](#图2.4) 中,关联数组只保留 0…3、42、1000、4521 和 200,000 等位置上的值。这样,用来存放这些值的空间比有 200,000 个条目的定宽或动态数组所占用的空间要小得多。
SystemVerilog 提供了关联数组 (associative array) 类型,用来保存稀疏矩阵 (sparse matrix) 的元素。这意味着当对一个非常大的地址空间进行寻址时,SystemVerilog 只为实际写入的元素分配空间。在[图 4](#图4) 中,关联数组只保留 0…3、42、1000、4521 和 200,000 等位置上的值。这样,用来存放这些值的空间比有 200,000 个条目的定宽或动态数组所占用的空间要小得多。
<span id="图2.4"></span>
<div align="center">
<img src="../images/post/2022-05-31-josh-systemverilog-2/2022-05-31-josh-systemverilog-2-040-AssociativeArray.png" width=600px alt="图 2.4 关联数组"/>
</div>
<a id="图4"></a>
![图 4 关联数组](../images/post/2022-05-31-josh-systemverilog-2/2022-05-31-josh-systemverilog-2-040-AssociativeArray.png){width=600px}
仿真器可以采用树或哈希表的形式来存放关联数组,但有一定的额外开销。当保存索引值比较分散的数组时,例如使用 32 位地址或 64 位数据作为索引的数据包,这种额外开销显然是可以接受的。
关联数组采用**在方括号中放置数据类型[^DataTypeNote]**的形式来进行声明,例如 `[int]` 或 `[Packet]`。[例 2.21](#例2.21) 示范了关联数组的声明、初始化和遍历过程。
关联数组采用**在方括号中放置数据类型[^DataTypeNote]**的形式来进行声明,例如 `[int]` 或 `[Packet]`。[例 21](#例21) 示范了关联数组的声明、初始化和遍历过程。
[^DataTypeNote]: 也可以采用通配符作为下标进行关联数组的声明,例如 `wild[*]`。但是不推荐使用这种风格,因为不明确指明数据类型会导致很多问题。一个典型的问题是在 `foreach` 循环中——在 `foreach(wild[j])` 中的变量 `j` 究竟是什么类型?
<span id="例2.21"></span>
<a id="例21"></a>
``` verilog 例 2.21 关联数组的声明、初始化和使用
``` verilog 例 21 关联数组的声明、初始化和使用
initial begin
bit [63:0] assoc[int], idx = 1;
@@ -543,13 +539,13 @@ initial begin
end
```
[例 2.21](#例2.21) 中的关联数组 `assoc` 具有稀疏分布的元素:1、2、4、8、16 等等。简单的 `for` 循环并不能遍历该数组,需要使用 `foreach` 循环来完成。如果想控制得更好,可以在 `do ... while` 循环中使用 `first` 和 `next` 函数。这些函数可以修改索引参数的值,然后根据数组是否为空返回 0 或 1。
[例 21](#例21) 中的关联数组 `assoc` 具有稀疏分布的元素:1、2、4、8、16 等等。简单的 `for` 循环并不能遍历该数组,需要使用 `foreach` 循环来完成。如果想控制得更好,可以在 `do ... while` 循环中使用 `first` 和 `next` 函数。这些函数可以修改索引参数的值,然后根据数组是否为空返回 0 或 1。
和 Perl 语言中的哈希数组类似,关联数组也可以使用字符串索引进行寻址。[例 2.22](#例2.22) 使用字符串索引读取文件,并建立关联数组 `switch`,以实现从字符串到数字的映射。如[例 2.22](#例2.22) 所示,可以使用函数 `exists()` 来检查元素是否存在。如果试图读取尚未被写入的元素,SystemVerilog 会返回数组类型的默认值,例如对于双状态类型是 `0`,对于四状态类型是 `X`。
和 Perl 语言中的哈希数组类似,关联数组也可以使用字符串索引进行寻址。[例 22](#例22) 使用字符串索引读取文件,并建立关联数组 `switch`,以实现从字符串到数字的映射。如[例 22](#例22) 所示,可以使用函数 `exists()` 来检查元素是否存在。如果试图读取尚未被写入的元素,SystemVerilog 会返回数组类型的默认值,例如对于双状态类型是 `0`,对于四状态类型是 `X`。
<span id="例2.22"></span>
<a id="例22"></a>
``` verilog 例 2.22 使用带字符串索引的关联数组
``` verilog 例 22 使用带字符串索引的关联数组
/*
输入文件的内容如下:
42 min_address
@@ -594,11 +590,11 @@ SystemVerilog 提供了很多数组的方法,可用于任何一种非合并的
## 7.1. 数组缩减方法
基本的数组缩减方法是把一个数组缩减成一个值,如[例 2.23](例2.23) 所示。最常用的缩减方法是 `sum`,它对数组中的所有元素求和。这里必须对 SystemVerilog 处理操作**位宽**的规则十分小心。默认情况下,如果把一个单比特数组的所有元素相加,其和也是单比特的。但如果使用 32 比特的表达式,把结果保存在 32 比特的变量里,与一个 32 比特的变量进行比较,或者使用适当的 `with` 表达式,SystemVerilog 都会在数组求和的过程中使用 32 比特位宽。
基本的数组缩减方法是把一个数组缩减成一个值,如[例 23](例23) 所示。最常用的缩减方法是 `sum`,它对数组中的所有元素求和。这里必须对 SystemVerilog 处理操作**位宽**的规则十分小心。默认情况下,如果把一个单比特数组的所有元素相加,其和也是单比特的。但如果使用 32 比特的表达式,把结果保存在 32 比特的变量里,与一个 32 比特的变量进行比较,或者使用适当的 `with` 表达式,SystemVerilog 都会在数组求和的过程中使用 32 比特位宽。
<span id="例2.23"></span>
<a id="例23"></a>
``` verilog 例 2.23 数组求和
``` verilog 例 23 数组求和
bit on[10]; // 单比特数组
int total;
@@ -627,11 +623,11 @@ end
其他的数组缩减方法还有 `product` (积),`and` (与),`or` (或) 和 `xor` (异或)。SystemVerilog 没有提供专门从数组里随机选取一个元素的方法。所以对于定宽数组队列、动态数组和关联数组可以使用 `$urandom_range($size (array)-1)`,而对于队列和动态数组还可以使用 `$urandom_ range(array.size()-1)`。
如果想从一个关联数组中随机选取一个元素,则需要逐个访问它之前的元素,因为无法直接访问到第 $N$ 个元素。[例 2.24](#例2.24) 示范了如何从一个以整数值作为索引的关联数组中随机选取一个元素。如果数组是以字符串作为索引,只需要将 `idx` 的类型改为 `string` 即可。
如果想从一个关联数组中随机选取一个元素,则需要逐个访问它之前的元素,因为无法直接访问到第 $N$ 个元素。[例 24](#例24) 示范了如何从一个以整数值作为索引的关联数组中随机选取一个元素。如果数组是以字符串作为索引,只需要将 `idx` 的类型改为 `string` 即可。
<span id="例2.24"></span>
<a id="例24"></a>
``` verilog 例 2.24 从一个关联数组中随机选取一个元素
``` verilog 例 24 从一个关联数组中随机选取一个元素
int aa[int], rand_idx, element, count;
element = $urandom_range(aa.size()-1);
@@ -649,11 +645,11 @@ $display("%0d element aa[%0d] = %0d",
数组中的最大值是什么?数组中有没有包含某个特定值?要想在非合并数组中查找数据,可以使用数组定位方法。**这些方法的返回值通常是一个队列**。
[例 2.25](#例2.25) 使用一个定宽数组 `f[6]`,一个动态数组 `d[]` 和一个队列 `q[$]`。`min` 和 `max` 函数能够找出数组中的最小值和最大值。注意,它们返回的是一个队列而非标量。这些方法也适用于关联数组。方法 `unique` 返回的是在数组中具有唯一值的队列,即排除掉重复的数值。
[例 25](#例25) 使用一个定宽数组 `f[6]`,一个动态数组 `d[]` 和一个队列 `q[$]`。`min` 和 `max` 函数能够找出数组中的最小值和最大值。注意,它们返回的是一个队列而非标量。这些方法也适用于关联数组。方法 `unique` 返回的是在数组中具有唯一值的队列,即排除掉重复的数值。
<span id="例2.25"></span>
<a id="例25"></a>
``` verilog 例 2.25 数组定位方法:<code>min</code>、<code>max</code>、<code>unique</code>
``` verilog 例 25 数组定位方法:<code>min</code>、<code>max</code>、<code>unique</code>
int aa[int], rand_idx, element, count;
element = $urandom_range(aa.size()-1);
@@ -667,11 +663,11 @@ $display("%0d element aa[%0d] = %0d",
element, rand_idx, aa[rand_idx]);
```
使用 `foreach` 循环固然可以实现数组的完全搜索,但是如果使用 SystemVerilog 的定位方法,则只需一个操作便可完成。表达式 `with` 可以指示 SystemVerilog 如何进行搜索,如[例 2.26](#例2.26) 所示。
使用 `foreach` 循环固然可以实现数组的完全搜索,但是如果使用 SystemVerilog 的定位方法,则只需一个操作便可完成。表达式 `with` 可以指示 SystemVerilog 如何进行搜索,如[例 26](#例26) 所示。
<span id="例2.26"></span>
<a id="例26"></a>
``` verilog 例 2.26 数组定位方法:<code>find</code>
``` verilog 例 26 数组定位方法:<code>find</code>
int d[] = '{9,1,8,3,4,4}, tq[$];
// 找出所有大于 3 的元素
@@ -689,22 +685,22 @@ tq = d.find_last with (item==4); // {4}
tq = d.find_last_index with (item==4); // {5} d[5]=4
```
在条件语句 `with` 中,`item` 被称为重复参数,它代表了数组中一个单独的元素。`item` 是默认的名字,也可以指定别的名字,只要在数组方法的参数列表中列出来就可以了,如[例 2.27](#例2.27) 所示。
在条件语句 `with` 中,`item` 被称为重复参数,它代表了数组中一个单独的元素。`item` 是默认的名字,也可以指定别的名字,只要在数组方法的参数列表中列出来就可以了,如[例 27](#例27) 所示。
<span id="例2.27"></span>
<a id="例27"></a>
``` verilog 例 2.27 重复参数的声明
``` verilog 例 27 重复参数的声明
tq = d.find_first with (item==4); // 本例的四个语句都是等价的
tq = d.find_first() with (item==4);
tq = d.find_first(item) with (item==4);
tq = d.find_first(x) with (x==4);
```
[例 2.28](#例2.28) 示范了几种对数组子集进行求和的方式。第一次求和 (`total`) 是先把元素值与 7 进行比较,比较表达式返回 1 (为真) 或 0 (为假),然后再把返回结果与对应元素相乘,所以 `{9, 0, 8, 0, 0, 0}` 的元素和是 17。第二次求和 (`total`) 则使用条件操作符 `? :` 进行计算。
[例 28](#例28) 示范了几种对数组子集进行求和的方式。第一次求和 (`total`) 是先把元素值与 7 进行比较,比较表达式返回 1 (为真) 或 0 (为假),然后再把返回结果与对应元素相乘,所以 `{9, 0, 8, 0, 0, 0}` 的元素和是 17。第二次求和 (`total`) 则使用条件操作符 `? :` 进行计算。
<span id="例2.28"></span>
<a id="例28"></a>
``` verilog 例 2.28 数组定位方法
``` verilog 例 28 数组定位方法
int count, total, d[] = '{9,1,8,3,4,4};
count = d.sum with (item > 7); // 2: {9, 8}
@@ -714,17 +710,17 @@ total = d.sum with (item < 8 ? item : 0); // 12=1+3+4+4
count = d.sum with (item == 4); // 2: {4, 4}
```
当把数组缩减方法与条件语句 `with` 结合使用时,会产生令人惊讶的结果,例如 `sum` 方法。在[例 2.28](#例2.28) 中,`sum` 操作符的结果是条件表达式为真的次数。对于[例 2.28](#例2.28) 的第一个运算语句来说,总共有两个数组元素大于 7 (9 和 8),所以 `count` 最后得 2。
当把数组缩减方法与条件语句 `with` 结合使用时,会产生令人惊讶的结果,例如 `sum` 方法。在[例 28](#例28) 中,`sum` 操作符的结果是条件表达式为真的次数。对于[例 28](#例28) 的第一个运算语句来说,总共有两个数组元素大于 7 (9 和 8),所以 `count` 最后得 2。
> 返回值为索引的数组定位方法,其返回的队列类型是 `int` 而非 `integer`,例如 `find_index` 方法。如果在这些语句中不小心用错了数据类型,那么代码有可能通不过编译。
## 7.3. 数组的排序
SystemVerilog 有几个可以改变数组中元素顺序的方法。可以对元素进行正排序、逆排序,或是打乱它们的顺序,如[例 2.29](#例2.29) 所示。注意,与前述数组定位方法不同的是,**排序方法改变了原始数组**,而**数组定位方法新建了一个队列来保存结果**。
SystemVerilog 有几个可以改变数组中元素顺序的方法。可以对元素进行正排序、逆排序,或是打乱它们的顺序,如[例 29](#例29) 所示。注意,与前述数组定位方法不同的是,**排序方法改变了原始数组**,而**数组定位方法新建了一个队列来保存结果**。
<span id="例2.29"></span>
<a id="例29"></a>
``` verilog 例 2.29 对数组排序
``` verilog 例 29 对数组排序
int d[] = '{9,1,8,3,4,4};
d.reverse(); // '{4,4,3,8,1,9}
d.sort(); // '{1,3,4,4,8,9}
@@ -732,11 +728,11 @@ d.rsort(); // '{9,8,4,4,3,1}
d.shuffle(); // '{9,4,3,8,1,4}
```
`reverse` 和 `shuffle` 方法不能带 `with` 条件语句,**它们的作用范围是整个数组**。[例 2.30](#例2.30) 示范了如何使用子域对一个结构进行排序。结构和合并结构后面会有解释。
`reverse` 和 `shuffle` 方法不能带 `with` 条件语句,**它们的作用范围是整个数组**。[例 30](#例30) 示范了如何使用子域对一个结构进行排序。结构和合并结构后面会有解释。
<span id="例2.30"></span>
<a id="例30"></a>
``` verilog 例 2.30 对结构数组进行排序
``` verilog 例 30 对结构数组进行排序
struct packed { byte red, green, blue; } c[];
initial begin
c = new[100]; // 分配 100 个像素
@@ -752,11 +748,11 @@ end
## 7.4. 使用数组定位方法建立记分板
数组定位方法可以用来建立记分板。[例 2.31](#例2.31) 定义了包结构 (`Packet`),然后建立了一个由包结构队列组成的记分板。后面会解释如何使用 `typedef` 创建结构。
数组定位方法可以用来建立记分板。[例 31](#例31) 定义了包结构 (`Packet`),然后建立了一个由包结构队列组成的记分板。后面会解释如何使用 `typedef` 创建结构。
<span id="例2.31"></span>
<a id="例31"></a>
``` verilog 例 2.31 带数组方法的记分板
``` verilog 例 31 带数组方法的记分板
typedef struct packed
{bit [7:0] addr;
bit [7:0] pr;
@@ -777,7 +773,7 @@ function void check_addr(bit [7:0] addr);
endfunction : check_addr
```
[例 2.31](#例2.31) 中的 `check_addr()` 函数在记分板里寻找和参数匹配的地址。`find_index()` 方法返回一个 `int` 队列。如果该队列为空 (`size==0`),则说明没有匹配值。如果该队列有一个成员 (`size==1`),则说明有一个匹配,该匹配元素随后被 `check_addr()` 函数删除掉。如果该队列有多个成员 (`size>1`),则说明记分板里有多个包地址和目标值匹配。
[例 31](#例31) 中的 `check_addr()` 函数在记分板里寻找和参数匹配的地址。`find_index()` 方法返回一个 `int` 队列。如果该队列为空 (`size==0`),则说明没有匹配值。如果该队列有一个成员 (`size==1`),则说明有一个匹配,该匹配元素随后被 `check_addr()` 函数删除掉。如果该队列有多个成员 (`size>1`),则说明记分板里有多个包地址和目标值匹配。
对于包信息的存储,更好的方式是采用类 (class),后续会有相关介绍。
@@ -843,11 +839,11 @@ endfunction : check_addr
# 9. 使用 `typedef` 创建新的类型
typedef 语句可以用来创建新的类型。例如,要求一个算术逻辑单元 (ALU) 在编译时可配置,以适应 8 比特、 16 比特、 24 比特或 32 比特等不同位宽的操作数。在 Verilog 中,可以为操作数的位宽和类型分别定义一个宏 (macro),如[例 2.32](#例2.32) 所示。
typedef 语句可以用来创建新的类型。例如,要求一个算术逻辑单元 (ALU) 在编译时可配置,以适应 8 比特、 16 比特、 24 比特或 32 比特等不同位宽的操作数。在 Verilog 中,可以为操作数的位宽和类型分别定义一个宏 (macro),如[例 32](#例32) 所示。
<span id="例2.32"></span>
<a id="例32"></a>
``` verilog 例 2.32 Verilog 中用户自定义的类型宏
``` verilog 例 32 Verilog 中用户自定义的类型宏
// 旧的 Verilog 风格
`define OPSIZE 8
`define OPREG reg [`OPSIZE-1:0]
@@ -857,9 +853,9 @@ typedef 语句可以用来创建新的类型。例如,要求一个算术逻辑
这种情况并没有创建新的类型,而只是在进行文本替换。在 SystemVerilog 中,采用下面的代码可以创建新的类型。本文约定,所有用户自定义类型都带后缀 “`_t`”。
<span id="例2.33"></span>
<a id="例33"></a>
``` verilog 例 2.33 SystemVerilog 中用户自定义类型
``` verilog 例 33 SystemVerilog 中用户自定义类型
// 新的 SystemVerilog 风格
parameter OPSIZE = 8;
typedef reg [OPSIZE-1:0] opreg_t;
@@ -873,18 +869,18 @@ opreg_t op_a, op_b;
> 用户自定义的最有用的类型是双状态的 32 比特的无符号整数。在 testbench 中,很多数值都是正整数,例如字段长度或事件次数,这种情况下如果定义有符号整数就会出问题。把对 `uint` 的定义放到通用定义程序包中,这样就可以在仿真程序的任何地方使用它。
<span id="例2.34"></span>
<a id="例34"></a>
``` verilog 例 2.34 <code>uint</code> 的定义
``` verilog 例 34 <code>uint</code> 的定义
typedef bit [31:0] uint; // 32 比特双状态无符号数
typedef int unsigned uint; // 等效定义
```
对新的数组类型的定义并不是很明显,需要把数组的下标放在新的数组名称中。[例 2.35](#例2.35) 创建了一种新的类型,`fixed_array5`,它是一个有着 5 个元素的定宽数组。[例 2.35](#例2.35) 接着声明了一个这种类型的数组并进行了初始化。
对新的数组类型的定义并不是很明显,需要把数组的下标放在新的数组名称中。[例 35](#例35) 创建了一种新的类型,`fixed_array5`,它是一个有着 5 个元素的定宽数组。[例 35](#例35) 接着声明了一个这种类型的数组并进行了初始化。
<span id="例2.35"></span>
<a id="例35"></a>
``` verilog 例 2.35 用户自定义数组类型
``` verilog 例 35 用户自定义数组类型
typedef int fixed_array5[5];
fixed_array5 f5;
@@ -902,19 +898,19 @@ Verilog 的最大缺陷之一是没有数据结构。在 SystemVerilog 中可以
## 10.1. 使用 `struct` 创建新类型
可以把若干变量组合到一个结构中。[例 2.36](#例2.36) 创建了一个名为 `pixel` 的结构,它有三个无符号的字节变量,分别代表红、绿和蓝。
可以把若干变量组合到一个结构中。[例 36](#例36) 创建了一个名为 `pixel` 的结构,它有三个无符号的字节变量,分别代表红、绿和蓝。
<span id="例2.36"></span>
<a id="例36"></a>
``` verilog 例 2.36 创建一个 <code>pixel</code> 类型
``` verilog 例 36 创建一个 <code>pixel</code> 类型
struct {bit [7:0] r, g, b;} pixel;
```
[例 2.36](#例2.36) 中的声明只是创建了一个 `pixel` 变量。要想在端口和程序中共享它,则必须创建一个新的类型,如[例 2.37](#例2.37) 所示。
[例 36](#例36) 中的声明只是创建了一个 `pixel` 变量。要想在端口和程序中共享它,则必须创建一个新的类型,如[例 37](#例37) 所示。
<span id="例2.37"></span>
<a id="例37"></a>
``` verilog 例 2.37 <code>pixel</code> 结构
``` verilog 例 37 <code>pixel</code> 结构
typedef struct {bit [7:0] r, g, b;} pixel_s;
pixel_s my_pixel;
```
@@ -923,11 +919,11 @@ pixel_s my_pixel;
## 10.2. 对结构进行初始化
可以在声明或者过程赋值语句中把多个值赋给一个结构体,就像数组那样。如[例 2.38](#例2.38) 所示,赋值时要把数值放到带单引号的大括号中。
可以在声明或者过程赋值语句中把多个值赋给一个结构体,就像数组那样。如[例 38](#例38) 所示,赋值时要把数值放到带单引号的大括号中。
<span id="例2.38"></span>
<a id="例38"></a>
``` verilog 例 2.38 对 <code>struct</code> 类型进行初始化
``` verilog 例 38 对 <code>struct</code> 类型进行初始化
initial begin
typedef struct {int a;
byte b;
@@ -944,11 +940,11 @@ end
## 10.3. 创建可容纳不同类型的联合
在硬件中,寄存器里的某些位的含义可能与其他位的值有关。例如,不同的操作码对应的处理器指令格式也不同。带立即操作数的指令,它在操作数位置上存放的是一个常量。整数指令对这个立即数的译码结果会与浮点指令大不相同。[例 2.39](#例2.39) 把整数 `i` 和实数 `f` 存放在同一位置上。
在硬件中,寄存器里的某些位的含义可能与其他位的值有关。例如,不同的操作码对应的处理器指令格式也不同。带立即操作数的指令,它在操作数位置上存放的是一个常量。整数指令对这个立即数的译码结果会与浮点指令大不相同。[例 39](#例39) 把整数 `i` 和实数 `f` 存放在同一位置上。
<span id="例2.39"></span>
<a id="例39"></a>
``` verilog 例 2.39 使用一个下标 <code>typedef</code> 创建联合
``` verilog 例 39 使用一个下标 <code>typedef</code> 创建联合
typedef union { int i; real f; } num_u;
num_u un;
un.f = 0.0; // 把数值设为浮点形式
@@ -960,11 +956,11 @@ un.f = 0.0; // 把数值设为浮点形式
## 10.4. 合并结构
SystemVerilog 提供的合并结构允许对数据在存储器中的排布方式有更多的控制。合并结构是以连续比特集的方式存放的,中间没有闲置的空间。[例 2.37](#例2.37) 中的 `pixel` 结构使用了三个数值,所以它占用了三个长字的存储空间,即使它实际只需要三个字节。可以指定把它合并到尽可能小的空间里。
SystemVerilog 提供的合并结构允许对数据在存储器中的排布方式有更多的控制。合并结构是以连续比特集的方式存放的,中间没有闲置的空间。[例 37](#例37) 中的 `pixel` 结构使用了三个数值,所以它占用了三个长字的存储空间,即使它实际只需要三个字节。可以指定把它合并到尽可能小的空间里。
<span id="例2.40"></span>
<a id="例40"></a>
``` verilog 例 2.40 合并结构
``` verilog 例 40 合并结构
typedef struct packed {bit [7:0] r, g, b;} pixel_p_s;
pixel_p_s my_pixel;
```
@@ -981,11 +977,11 @@ SystemVerilog 数据类型的多样性意味着可能需要在它们之间进行
## 11.1. 静态转换
静态转换操作不对转换值进行检查。如[例 2.41](#例2.41) 所示,转换时指定目标类型,并在需要转换的表达式前加上单引号即可。注意,Verilog 对整数和实数类型,或者不同位宽的向量之间进行隐式转换。
静态转换操作不对转换值进行检查。如[例 41](#例41) 所示,转换时指定目标类型,并在需要转换的表达式前加上单引号即可。注意,Verilog 对整数和实数类型,或者不同位宽的向量之间进行隐式转换。
<span id="例2.41"></span>
<a id="例41"></a>
``` verilog 例 2.41 在整型和实划之间进行静态转换
``` verilog 例 41 在整型和实划之间进行静态转换
int i;
real r;
@@ -999,11 +995,11 @@ r = real'(42); // 转换是非强制的
## 11.3. 流操作符
流操作符 `<<` 和 `>>` 用在赋值表达式的右边,后面带表达式、结构或数组。流操作符用于把其后的数据打包成一个比特流。操作符 `>>` 把数据从左至右变成流,而 `<<` 则把数据从右至左变成流,如[例 2.42](#例2.42) 所示。也可以指定一个片段宽度,把源数据按照这个宽度分段以后再转变成流。不能将比特流结果直接赋给非合并数组,而应该在赋值表达式的左边使用流操作符把比特流拆分到非合并数组中。
流操作符 `<<` 和 `>>` 用在赋值表达式的右边,后面带表达式、结构或数组。流操作符用于把其后的数据打包成一个比特流。操作符 `>>` 把数据从左至右变成流,而 `<<` 则把数据从右至左变成流,如[例 42](#例42) 所示。也可以指定一个片段宽度,把源数据按照这个宽度分段以后再转变成流。不能将比特流结果直接赋给非合并数组,而应该在赋值表达式的左边使用流操作符把比特流拆分到非合并数组中。
<span id="例2.42"></span>
<a id="例42"></a>
``` verilog 例 2.42 基本的流操作
``` verilog 例 42 基本的流操作
initial begin
int h;
bit [7:0] b, g[4], j[4] = '{8'ha, 8'hb, 8'hc, 8'hd};
@@ -1022,11 +1018,11 @@ end
也可以使用很多连接符 `{}` 来完成同样的操作,但是流操作符用起来会更简洁并且易于阅读。
如果需要打包或拆分数组,可以使用流操作符来完成具有不同尺寸元素的数组间的转换。例如,可以将字节数组转换成字数组。对于定宽数组、动态数组和队列都可以这样。[例 2.43](#例2.43) 示范了队列之间的转换,这种转换同时也适用于动态数组。数组元素会根据需要自动分配。
如果需要打包或拆分数组,可以使用流操作符来完成具有不同尺寸元素的数组间的转换。例如,可以将字节数组转换成字数组。对于定宽数组、动态数组和队列都可以这样。[例 43](#例43) 示范了队列之间的转换,这种转换同时也适用于动态数组。数组元素会根据需要自动分配。
<span id="例2.43"></span>
<a id="例43"></a>
``` verilog 例 2.43 使用流操作符进行队列间的转换
``` verilog 例 43 使用流操作符进行队列间的转换
initial begin
bit [15:0] wq[$] = {16'h1234, 16'h5678};
bit [7:0] bq[$];
@@ -1042,11 +1038,11 @@ end
> 数组下标失配是在数组间进行流操作时常见的错误。数组声明中的下标 `[256]` 等同于 `[0:255]` 而非 `[255:0]`。由于很多数组使用 `[high:low]` (由高到低) 的下标形式进行声明,使用流操作把它们的值赋给带 `[size]` 下标形式的数组,会造成元素倒序。同样,如果把声明形式为 `bit [7:0] src [255:0]` 的非合并数组使用流操作赋值给声明形式为 `bit [7:0] [255:0] dst` 的合并数组,则数值的顺序会被打乱。对于合并的字节数组,正确的声明形式应该是 `bit [255:0] [7:0] dst`。
流操作符也可用来将结构 (例如,ATM 信元) 打包或拆分到字节数组中。在[例 2.44](#例2.44) 中使用流操作把结构转换成动态的字节数组,然后字节数组又被反过来转换成结构。
流操作符也可用来将结构 (例如,ATM 信元) 打包或拆分到字节数组中。在[例 44](#例44) 中使用流操作把结构转换成动态的字节数组,然后字节数组又被反过来转换成结构。
<span id="例2.44"></span>
<a id="例44"></a>
``` verilog 例 2.44 使用流操作符在结构和数组间进行转换
``` verilog 例 44 使用流操作符在结构和数组间进行转换
initial begin
typedef struct {int a;
byte b;
@@ -1072,19 +1068,19 @@ end
在学会使用枚举类型之前,只能使用文本宏。宏的作用范围太大,而且大多数情况下对于调试者是可见的。枚举创建了一种强大的变量类型,它仅限于一些特定名称的集合,例如指令中的操作码或者状态机中的状态名。例如,使用 ADD、MOVE 或 ROTW 这些名称有利于编写和维护代码,它比直接使用 `8'h01` 这样的常量或者宏要好得多。定义常量的另一种方法是使用参数。但参数需要对每个数值进行单独的定义,而枚举类型却能够自动为列表中的每个名称分配不同的数值。
最简单的枚举类型声明包含了一个常量名称列表以及一个或多个变量,如[例 2.45](#例2.45) 所示。通过这种方式创建的是一个匿名的枚举类型,它只能用于这个例子中声明的变量。
最简单的枚举类型声明包含了一个常量名称列表以及一个或多个变量,如[例 45](#例45) 所示。通过这种方式创建的是一个匿名的枚举类型,它只能用于这个例子中声明的变量。
<span id="例2.45"></span>
<a id="例45"></a>
``` verilog 例 2.45 一个简单的枚举类型
``` verilog 例 45 一个简单的枚举类型
enum {RED, BLUE, GREEN} color;
```
创建一个署名的枚举类型有利于声明更多新变量,尤其是当这些变量被用作子程序参数或模块端口时。首先需要创建枚举类型,然后再创建相应的变量。使用内建的 `name()` 函数,可以得到枚举变量值对应的字符串,如[例 2.46](#例2.46) 所示。
创建一个署名的枚举类型有利于声明更多新变量,尤其是当这些变量被用作子程序参数或模块端口时。首先需要创建枚举类型,然后再创建相应的变量。使用内建的 `name()` 函数,可以得到枚举变量值对应的字符串,如[例 46](#例46) 所示。
<span id="例2.46"></span>
<a id="例46"></a>
``` verilog 例 2.46 枚举类型
``` verilog 例 46 枚举类型
// 创建代表 0,1,2 的数据类型
typedef enum {INIT, DECODE, IDLE} fsmstate_e;
fsmstate_e pstate, nstate; // 声明自定义类型变量
@@ -1104,28 +1100,28 @@ end
## 12.1. 定义枚举值
枚举值默认为从 0 开始递增的整数。可以定义自己的枚举值。[例 2.47](#例2.47) 中使用 `INIT` 代表默认值 0,`DECODE` 代表 2,`IDLE` 代表 3。
枚举值默认为从 0 开始递增的整数。可以定义自己的枚举值。[例 47](#例47) 中使用 `INIT` 代表默认值 0,`DECODE` 代表 2,`IDLE` 代表 3。
<span id="例2.47"></span>
<a id="例47"></a>
``` verilog 例 2.47 指定枚举值
``` verilog 例 47 指定枚举值
typedef enum {INIT, DECODE=2, IDLE} fsmtype_e;
```
枚举常量,如[例 2.47](#例2.47) 中的 `INIT`,它们的作用范围规则和变量是一样的。因此,如果在不同的枚举类型中用到了同一个枚举常量名,例如把 `INIT` 用于不同的状态机中,那么必须在不同的作用域里声明它们,例如模块、程序块、函数和类。
枚举常量,如[例 47](#例47) 中的 `INIT`,它们的作用范围规则和变量是一样的。因此,如果在不同的枚举类型中用到了同一个枚举常量名,例如把 `INIT` 用于不同的状态机中,那么必须在不同的作用域里声明它们,例如模块、程序块、函数和类。
> 如果没有特别指出,枚举类型会被当成 `int` 类型存储。由于 `int` 类型的默认值是 0,所以在给枚举常量赋值时务必小心。在[例 2.48](#例2.48) 中 `position` 会被初始化为 0,这并不是一个合法的 `ordinal_e` 变量。这种情况是语言本身所规定的,而非工具上的缺陷。因此把 0 指定给一个枚举常量可以避免这个错误,如[例 2.49](#例2.49) 所示。
> 如果没有特别指出,枚举类型会被当成 `int` 类型存储。由于 `int` 类型的默认值是 0,所以在给枚举常量赋值时务必小心。在[例 48](#例48) 中 `position` 会被初始化为 0,这并不是一个合法的 `ordinal_e` 变量。这种情况是语言本身所规定的,而非工具上的缺陷。因此把 0 指定给一个枚举常量可以避免这个错误,如[例 49](#例49) 所示。
<span id="例2.48"></span>
<a id="例48"></a>
``` verilog 例 2.48 指定枚举值:不正确
``` verilog 例 48 指定枚举值:不正确
typedef enum {FIRST=1, SECOND, THIRD} ordinal_e;
ordinal_e position;
```
<span id="例2.49"></span>
<a id="例49"></a>
``` verilog 例 2.49 指定枚举值:正确
``` verilog 例 49 指定枚举值:正确
typedef enum {BAD_O=0, FIRST=1, SECOND, THIRD} ordinal_e;
ordinal_e position;
```
@@ -1147,11 +1143,11 @@ SystemVerilog 提供了一些可以遍历枚举类型的函数:
件。如果使用 `current! =current.last`,则循环会在到达最后一个成员之前终止。如果
使用 `current<=current.last`,则会造成死循环,因为 `next` 给出的值永远也不会大于最后一个值。这类似于 `for` 循环的步长为 0.3,而循环变量定义为 `bit [1:0]`,所以循环永远不会退出。
实际上,可以使用 `do ... while` 循环来遍历所有值,如[例 2.50](#例2.50) 所示。
实际上,可以使用 `do ... while` 循环来遍历所有值,如[例 50](#例50) 所示。
<span id="例2.50"></span>
<a id="例50"></a>
``` verilog 例 2.50 遍历所有枚举成员
``` verilog 例 50 遍历所有枚举成员
typedef enum {RED, BLUE, GREEN} color_e;
color_e color;
color = color.first;
@@ -1167,9 +1163,9 @@ while (color != color.first); // 环形绕回时即完成
枚举类型的默认类型为双状态 `int`。可以使用简单的赋值表达式把枚举变量的值直接赋给非枚举变量如 `int`。但 SystemVerilog 不允许在没有进行显式类型转换的情况下把整型变量赋给枚举变量。SystemVerilog 要求显式类型转换的原因是**可能存在的数值越界情况**。
<span id="例2.51"></span>
<a id="例51"></a>
``` verilog 例 2.51 整型和枚举类型之间的相互赋值
``` verilog 例 51 整型和枚举类型之间的相互赋值
typedef enum {RED, BLUE, GREEN} COLOR_E;
COLOR_E color, c2;
int c;
@@ -1187,9 +1183,9 @@ initial begin
end
```
在[例 2.51](#例2.51) 中,`$cast` 被当成函数进行调用,目的在于把右边的值赋给左边的量。如果赋值成功,`$cast()` 返回 1。如果因为数值越界而导致赋值失败,则不进行任何赋值,函数返回 0。如果把 `cast` 当成任务使用并且操作失败,则 SystemVerilog 会打印错误信息。
在[例 51](#例51) 中,`$cast` 被当成函数进行调用,目的在于把右边的值赋给左边的量。如果赋值成功,`$cast()` 返回 1。如果因为数值越界而导致赋值失败,则不进行任何赋值,函数返回 0。如果把 `cast` 当成任务使用并且操作失败,则 SystemVerilog 会打印错误信息。
也可以像[例 2.51](#例2.51) 所示的那样使用 `type'(val)` 进行类型转换,但这种方式并不进行任何类型检查,所以转换结果可能会越界。例如,在[例 2.51](#例2.51) 中进行静态类型转换以后,赋给 `c2` 的值实际上已经越界,所以应该尽量避免使用这种方式。
也可以像[例 51](#例51) 所示的那样使用 `type'(val)` 进行类型转换,但这种方式并不进行任何类型检查,所以转换结果可能会越界。例如,在[例 51](#例51) 中进行静态类型转换以后,赋给 `c2` 的值实际上已经越界,所以应该尽量避免使用这种方式。
# 13. 常量
@@ -1199,26 +1195,26 @@ SystemVerilog 中有好几种类型的常量。Verilog 中创建常量的最典
SystemVerilog 也支持 `const` 修饰符,允许在变量声明时对其进行初始化,但不能在过程代码中改变其值。
<span id="例2.52"></span>
<a id="例52"></a>
``` verilog 例 2.52 <code>const</code> 变量的声明
``` verilog 例 52 <code>const</code> 变量的声明
initial begin
const byte colon = ":";
...
end
```
在[例 2.52](#例2.52) 中,`colon` 的值在 `initial` 块开头就被初始化。`const` 还可以作为子程序参数。
在[例 52](#例52) 中,`colon` 的值在 `initial` 块开头就被初始化。`const` 还可以作为子程序参数。
# 14. 字符串
Verilog 中使用 `reg` 变量来保存字符串,这一表达很不方便。SystemVerilog 中的 `string` 类型可以用来保存长度可变的字符串。单个字符是 `byte` 类型。长度为 $N$ 的字符串中,元素编号从 0 到 $N — 1$。注意,跟 C 语言 不一样的是,字符串的结尾并不带标识符 null,所有尝试使用字符 “\0” 的操作都会被忽略。字符串使用动态的存储方式,所以不用担心存储空间会全部用完。
[例 2.53](#例2.53) 示范了与字符串相关的几种操作。函数 `getc(N)` 返回位置 `N` 上的字节,`toup­per` 返回一个所有字符大写的字符串,`tolower` 返回一个小写的字符串。大括号 `{}` 用于串接字符串。任务 `putc(M,C)` 把字节 `c` 写到字符串的 `M` 位上。`M` 必须介于 0 和 `len` 所给出的长度之间。函数 `substr(start,end)` 提取出从位置 `start` 到 `end` 之间的所有字符。
[例 53](#例53) 示范了与字符串相关的几种操作。函数 `getc(N)` 返回位置 `N` 上的字节,`toup­per` 返回一个所有字符大写的字符串,`tolower` 返回一个小写的字符串。大括号 `{}` 用于串接字符串。任务 `putc(M,C)` 把字节 `c` 写到字符串的 `M` 位上。`M` 必须介于 0 和 `len` 所给出的长度之间。函数 `substr(start,end)` 提取出从位置 `start` 到 `end` 之间的所有字符。
<span id="例2.53"></span>
<a id="例53"></a>
``` verilog 例 2.53 字符串方法
``` verilog 例 53 字符串方法
string s;
initial begin
@@ -1241,20 +1237,20 @@ task my_log(string message);
endtask
```
稍加留意便可发现动态字符串的用处有多大。在别的语言如 C 里,必须不停地创建临时字符串来接收函数返回的结果。在[例 2.53](#例2.53) 中,函数 `psprintf()` 替代了 Verilog-2001 中的函数 `sformat()`。这个新函数返回一个格式化的临时字符串,并且可以直接传递给其他子程序。这样就可以不用定义新的临时字符串并在格式化语句与函数调用过程中传递这个字符串。
稍加留意便可发现动态字符串的用处有多大。在别的语言如 C 里,必须不停地创建临时字符串来接收函数返回的结果。在[例 53](#例53) 中,函数 `psprintf()` 替代了 Verilog-2001 中的函数 `sformat()`。这个新函数返回一个格式化的临时字符串,并且可以直接传递给其他子程序。这样就可以不用定义新的临时字符串并在格式化语句与函数调用过程中传递这个字符串。
# 15. 表达式的位宽
在 Verilog 中,表达式的位宽是造成行为不可预知的主要原因之一。[例 2.54](#例2.54) 使用四种不同方式实现 `1+1`:
在 Verilog 中,表达式的位宽是造成行为不可预知的主要原因之一。[例 54](#例54) 使用四种不同方式实现 `1+1`:
- 方式 A 使用两个单比特变量,在这种精度下得到 `1+1=0`。
- 方式 B 由于赋值表达式的左边有一个 8 比特的变量,所以其精度是 8 比特,得到的结果是 `1+1=2`。
- 方式 C 采用一个哑元常数强迫 SystemVerilog 使用 2 比特精度。
- 方式 D 中,第一个值在转换符的作用下被指定为 2 比特的值,所以结果是 `1+1=2`。
<span id="例2.54"></span>
<a id="例54"></a>
``` verilog 例 2.54 表达式位宽依赖于上下文
``` verilog 例 54 表达式位宽依赖于上下文
bit [7:0] b8;
bit one = 1'b1; // 单比特
$displayb(one + one); // A: 1+1 = 0