review josh-verilog 1-3, correct some mistakes, update some layout

This commit is contained in:
2023-04-17 00:56:17 +08:00
parent ca83c80da0
commit cd92265be1
3 changed files with 517 additions and 459 deletions
+29 -16
View File
@@ -20,44 +20,49 @@ toc:
expand_all: true
max_depth: 4
abbrlink: 460433dd
description: 本文阐述了 HDL 语言/设计方法的基本概念。主要介绍了数字系统建模方法的演变,引入了 HDL 语言的基本概念,对 Verilog HDL 和 VHDL、Verilog HDL 和 C 的联系与区别进行了说明,在最后引入了 HDL 的设计和验证流程,并对各个流程进行了简单介绍。
---
本文阐述了 HDL 语言/设计方法的基本概念。主要介绍了数字系统建模方法的演变,引入了 HDL 语言的基本概念,对 Verilog HDL 和 VHDL、Verilog HDL 和 C 的联系与区别进行了说明,在最后引入了 HDL 的设计和验证流程,并对各个流程进行了简单介绍。
# 1. 设计方法的变迁<a name="01"></a>
# 1. 设计方法的变迁<a id="01"></a>
随着微电子设计技术与工艺的发展,数字集成电路从电子管、晶体管、中小规模集成电路、超大规模集成电路(Very Large Scale Integrated Circuit, VLSIC)逐步发展到今天的专用集成电路(Application Specific Integrated Circuit, ASIC),愈发数字化的产品如计算机、手机、数字电视等都运用了复杂的专用数字集成电路。数字逻辑器件从简单的逻辑门到复杂的 SoC,种类繁多,提供了对复杂系统的灵活支撑。
<!-- more -->
随着数字电路系统的不断发展,系统的逻辑复杂度与规模日益增加,数字系统的设计方法也随之不断演进。早期简单的门逻辑设计阶段,EDA 工具应用范围十分有限,工程师们习惯于使用**{% label @卡诺图 %}**简化设计,然后通过面包板等实验系统验证设计;系统相对复杂后,工程师们借助 EDA 工具通过原理图描述数字系统。
随着数字电路系统的不断发展,系统的逻辑复杂度与规模日益增加,数字系统的设计方法也随之不断演进。早期简单的门逻辑设计阶段,EDA 工具应用范围十分有限,工程师们习惯于使用**{% label prime@卡诺图 %}**简化设计,然后通过面包板等实验系统验证设计;系统相对复杂后,工程师们借助 EDA 工具通过原理图描述数字系统。
{% note default %}
原理图由元件库中的元件构成,使用 EDA(Electronic Design Automation,电子设计自动化)工具可以对原理图进行仿真并分析其性能。
{% endnote %}
当数字系统发展到 ASIC 与 PLD(Programmable Logic Device,可编程逻辑器件) 设计阶段后,原理图不利于移植、维护费时费力等缺点逐步显现,这时需要抽象度更高、更灵活的设计方式 —— HDL(Hardware Description Language,硬件描述语言)。
使用 HDL 语言,可以从算法、系统级(System Level)、功能模块级(Functional Model Level)、行为级(Behavioral Level)、寄存器传输级(RTL, Register Transfer Level)、门级(Gate Level)到开关级(Switch Level)等不同层次描述数字电路系统,然后通过 EDA 工具综合、仿真并实现该系统。可以说 HDL 语言的出现是数字系统设计方法的一个重大飞跃。
数字电路系统设计方法的另一个重大飞跃是随着 EDA 工具的不断推陈出新发生的。早期 ASIC 和 PLD 设计时,人们需要直接描述 CMOS 的开关电路或门级电路,其设计量庞大,仿真速度也非常慢,像当今系统门数量为千万门级的 FPGA 用开关级或门级方法描述在当时是不可想象的。这时人们就希望能够使用 HDL 直接从更高的层次描述电路,然后使用 EDA 工具自动将高层次的 HDL 电路描述解析到门级,这些将大大节约设计与仿真时间,这个**{% label @从高层次的电路描述通过 EDA 工具解析到门级等低层次的电路描述的过程就叫“综合”(Synthesize),或者逻辑综合 %}**。
数字电路系统设计方法的另一个重大飞跃是随着 EDA 工具的不断推陈出新发生的。早期 ASIC 和 PLD 设计时,人们需要直接描述 CMOS 的开关电路或门级电路,其设计量庞大,仿真速度也非常慢,像当今系统门数量为千万门级的 FPGA 用开关级或门级方法描述在当时是不可想象的。这时人们就希望能够使用 HDL 直接从更高的层次描述电路,然后使用 EDA 工具自动将高层次的 HDL 电路描述解析到门级,这些将大大节约设计与仿真时间,这个**{% label prime@从高层次的电路描述通过 EDA 工具解析到门级等低层次的电路描述的过程就叫“综合”(Synthesize),或者逻辑综合 %}**。
{% note default %}
综合工具能将高层次的 HDL 语言、原理图等设计描述翻译成由与、或、非门等基本逻辑单元组成的门级连接(**网表**)并根据设计目标与要求(约束条件)优化所生成的逻辑连接,输出门级网表文件。
{% endnote %}
目前最成熟的综合工具是 RTL 级综合工具,它能将 RTL 级描述翻译并优化为门级网表。综合工具的产生支撑了直接从 RTL 级描述硬件电路,这种从门级描述抽象到 RTL 级描述数字电路是设计方法的又一次伟大飞跃。
从上面描述可以看到 HDL 语言是分层次的,关于上述 HDL 语言设计层次的含义在 [3. HDL 的设计与验证流程](#03)小节和 [《Part 3——描述方式和设计层次》的 5. 设计层次](https://josh-gao.blog.csdn.net/article/details/105262549#5__1023)小节有详细论述。)
从上面描述可以看到 HDL 语言是分层次的,关于上述 HDL 语言设计层次的含义在本文的[“HDL 的设计与验证流程”](#03)小节和 [《Part 3——描述方式和设计层次》的“设计层次”][]小节有详细论述。)
# 2. Verilog 语言的特点<a name="02"></a>
# 2. Verilog 语言的特点<a id="02"></a>
## 2.1. Verilog 的由来
Verilog 是 Verilog HDL 的简称。
{% note info %}
Verilog 语言最初于1983年由 Gateway Design Automation 公司开发,于1995年接受为 IEEE 标准。
{% endnote %}
Verilog 语言不仅定义了语法,而且对每个语法结构都清晰定义了仿真语义,从而便于仿真调试。而且 Verilog 从 C 语言继承了很多操作符和语法结构,对初学者而言易学易用。另外 Verilog 语言具有很强的扩展能力,最新的 Verilog 2001 标准[^1]大大扩展了 Verilog 的应用灵活性。
@@ -93,14 +98,16 @@ Verilog 语言是根据 C 语言发明而来的,因此 Verilog 具备了 C 语
硬件描述语言(HDL)同软件语言(如 C、C++ 等)是有本质区别的。Verilog 作为硬件描述语言,**它的本质作用在于描述硬件**。Verilog 虽然采用了 C 语言形式,但是它的最终描述结果是芯片内部的实际电路。所以评判一段 HDL 代码优劣的最终标准是其描述并实现的硬件电路的性能(包括面积和速度两个方面)。评价一个设计的代码水平较高,仅仅是说这个设计由硬件向 HDL 代码这种表现形式转换得更流畅、合理。而一个设计的最终性能更大程度上取决于设计工程师所构想的硬件实现方案的效率以及合理性。
{% note warning %}
初学者,特别是由软件转行的初学者,片面追求代码的整洁、简短,这是错误的,是与评价 HDL 的标准背道而驰的。正确的编写代码的方法是,首先要做到对所需实现的硬件电路“胸有成竹”,对该部分硬件的结构与连接十分清晰,然后再用适当的 HDL 语句表达出来即可。
{% endnote %}
3. Verilog 与 C 的结合
Verilog 毕竟是硬件描述语言,在抽象层次上比 C 语言要差一些,语法不如 C 灵活,例如文件输入和输出方面,Verilog 的功能显然不如 C。为了克服 Verilog 高级抽象的缺陷,Verilog 的设计者们发明了 PLI。PLI 可以在仿真器中将 C 语言的程序和 Verilog 的程序互相通信,或者在 Verilog 中调用 C 语言的函数库,这样就大大扩展了 Verilog 语言的灵活性和高层次抽象的能力。开发时,一方面,硬件设计者使用 Verilog 进行硬件建模;另一方面,验证工程师却常常使用 C 来编写测试向量,然后通过 Verilog 的 PLI 将 Verilog 和 C 联系起来。这样,C 语言就很好地弥补了 Verilog 高层次建模的缺陷。
# 3. HDL 的设计与验证流程<a name="03"></a>
# 3. HDL 的设计与验证流程<a id="03"></a>
HDL 的基本功能就是有效地描述并仿真硬件系统。在此部分,抛开具体的 PLD 或 ASIC 设计流程,从 HDL 语言层次入手,分析典型的 HDL 设计与验证流程。基于 HDL 的设计、仿真流程如下图所示。图中,虚线框表示的步骤可以根据项目的复杂度省略,而实线框表示的步骤为必须执行的步骤。
@@ -110,31 +117,31 @@ HDL 的基本功能就是有效地描述并仿真硬件系统。在此部分,
## 3.1. 系统与功能模块定义(系统与功能模块级)
在大型系统的设计与实现中,首先要进行详细的系统规划和描述,此时 HDL 描述 **{% label @侧重于整体系统的划分和实现 %}**。对系统级的仿真侧重于对整个系统的功能和性能指标的考量。功能模块级设计时将系统整体功能划分为可实现的具体功能模块,大致确定模块间的接口,如时钟、读写信号、数据流、控制信号等。在有些情况下,还要根据系统要求,描述每个模块或进程的时序约束。另外在此层次,必须权衡整个系统多种的实现方式的优劣,选出系统性能指标优并且可以高效实现的设计方案。功能模块级仿真主要是考察每个功能的功能和基本时序情况。系统与功能模块级设计与仿真常常要借助于 C 语言和抽象程度较高的 HDL 语言描述,如 SystemC、Coware C、SystemVerilog、Superlog 等。
在大型系统的设计与实现中,首先要进行详细的系统规划和描述,此时 HDL 描述 **{% label prime@侧重于整体系统的划分和实现 %}**。对系统级的仿真侧重于对整个系统的功能和性能指标的考量。功能模块级设计时将系统整体功能划分为可实现的具体功能模块,大致确定模块间的接口,如时钟、读写信号、数据流、控制信号等。在有些情况下,还要根据系统要求,描述每个模块或进程的时序约束。另外在此层次,必须权衡整个系统多种的实现方式的优劣,选出系统性能指标优并且可以高效实现的设计方案。功能模块级仿真主要是考察每个功能的功能和基本时序情况。系统与功能模块级设计与仿真常常要借助于 C 语言和抽象程度较高的 HDL 语言描述,如 SystemC、Coware C、SystemVerilog、Superlog 等。
## 3.2. 行为级描述测试激励(Behavioral Level)
行为级模块描述的最大特点是 **{% label @必须明确每个模块间的所有接口和边界 %}**。此时模块内部的功能已经明确,模块间的所有接口,顶层的输入、输出信号等在行为级已经被清晰地描述出来。在 PLD 和 ASIC 设计流程中,常用行为级描述方式编写测试激励。延时描述、监视描述等命令都是在编写测试激励过程中常用的行为级语法。行为级描述常用 HDL 语言如 Verilog 和 VHDL 等。
行为级模块描述的最大特点是 **{% label prime@必须明确每个模块间的所有接口和边界 %}**。此时模块内部的功能已经明确,模块间的所有接口,顶层的输入、输出信号等在行为级已经被清晰地描述出来。在 PLD 和 ASIC 设计流程中,常用行为级描述方式编写测试激励。延时描述、监视描述等命令都是在编写测试激励过程中常用的行为级语法。行为级描述常用 HDL 语言如 Verilog 和 VHDL 等。
## 3.3. 寄存器传输级(RTL)
寄存器传输级指不关注寄存器和组合逻辑的细节(如使用了多少逻辑门,逻辑门之间的连接拓扑结构等),**{% label @通过描述寄存器到寄存器之间的逻辑功能描述电路 %}**的 HDL 层次。RTL 级是比门级更高的抽象层次,一般使用 RTL 级语言描述硬件电路比门级描述电路简单、高效得多。寄存器传输级描述的最大特点是可以直接用综合工具综合门级网表。RTL 设计直接决定着设计的功能和效率。好的 RTL 设计能在满足逻辑功能的前提下,使设计的速度和面积达到一种平衡的优化。RTL 级描述最常用的 HDL 语言是 Verilog 和 VHDL 语言。
寄存器传输级指不关注寄存器和组合逻辑的细节(如使用了多少逻辑门,逻辑门之间的连接拓扑结构等),**{% label prime@通过描述寄存器到寄存器之间的逻辑功能描述电路 %}**的 HDL 层次。RTL 级是比门级更高的抽象层次,一般使用 RTL 级语言描述硬件电路比门级描述电路简单、高效得多。寄存器传输级描述的最大特点是可以直接用综合工具综合门级网表。RTL 设计直接决定着设计的功能和效率。好的 RTL 设计能在满足逻辑功能的前提下,使设计的速度和面积达到一种平衡的优化。RTL 级描述最常用的 HDL 语言是 Verilog 和 VHDL 语言。
## 3.4 对 RTL 级描述进行功能仿真
一般来说需要对 RTL 级设计进行功能仿真,仿真的目的为 **{% label @验证 RTL 级描述是否与设计意图一致 %}**。为了提高效率,一般功能仿真的测试激励使用行为级的 HDL 语言描述。
一般来说需要对 RTL 级设计进行功能仿真,仿真的目的为 **{% label prime@验证 RTL 级描述是否与设计意图一致 %}**。为了提高效率,一般功能仿真的测试激励使用行为级的 HDL 语言描述。
## 3.5. 逻辑综合(使用 RTL 级 EDA 工具)
RTL 级综合指 **{% label @将 RTL 级 HDL 语言翻译成由与、或、非门等基本逻辑单元组成的门级连接(网表),并根据设计目标与要求(约束条件)优化所生成的逻辑连接,输出门级网表文件 %}**。随着综合工具的不断智能化,使用 RTL 级语言描述硬件电路越来越方便,特别是在 PLD(主要指 FPGA 和 CPLD)设计领域,最重要的代码设计层次就是 RTL 级。
RTL 级综合指 **{% label prime@将 RTL 级 HDL 语言翻译成由与、或、非门等基本逻辑单元组成的门级连接(网表),并根据设计目标与要求(约束条件)优化所生成的逻辑连接,输出门级网表文件 %}**。随着综合工具的不断智能化,使用 RTL 级语言描述硬件电路越来越方便,特别是在 PLD(主要指 FPGA 和 CPLD)设计领域,最重要的代码设计层次就是 RTL 级。
## 3.6. 门级(Gate Level)
由于目前 FPGA 设计大多数依靠专业综合工具完成从 RTL 级代码向门级代码的转换,所有设计者直接用 HDL 语言描述门级模型的情况越来越少,高效的综合工具将设计者从复杂繁琐的门级描述中彻底解脱出来。目前直接使用门级描述的场合一般是 ASIC 和 FPGA 设计中 **{% label @某些面积或时序要求较高的模块 %}**。门级描述的特点是整个设计用逻辑门实现,通过逻辑门的组合显化描述了设计的引脚、功能、时钟周期等所有信息。
由于目前 FPGA 设计大多数依靠专业综合工具完成从 RTL 级代码向门级代码的转换,所有设计者直接用 HDL 语言描述门级模型的情况越来越少,高效的综合工具将设计者从复杂繁琐的门级描述中彻底解脱出来。目前直接使用门级描述的场合一般是 ASIC 和 FPGA 设计中 **{% label prime@某些面积或时序要求较高的模块 %}**。门级描述的特点是整个设计用逻辑门实现,通过逻辑门的组合显化描述了设计的引脚、功能、时钟周期等所有信息。
## 3.7. 综合后门级仿真
综合完成后,如果需要检查综合结果是否与原设计一致,就需要做综合后仿真。在仿真时,把综合生成的标准延时文件反标注到综合仿真模型中去,可估计门延时带来的影响。综合后仿真虽然比功能仿真精确一些,但是**只能估计门延时,不能估计线延时**,仿真结果**与布线后的实际情况还有一定的差距**,并不十分准确。这种仿真的主要目的在于 **{% label @检查综合器的综合结果是否与设计输入一致 %}**。目前主流综合工具日益成熟,对于一般性设计,如果设计者确信自己表述明确,没有综合歧义发生,则可以省略综合后仿真步骤。**一般情况下,综合后仿真与功能仿真的仿真激励相同**。
综合完成后,如果需要检查综合结果是否与原设计一致,就需要做综合后仿真。在仿真时,把综合生成的标准延时文件反标注到综合仿真模型中去,可估计门延时带来的影响。综合后仿真虽然比功能仿真精确一些,但是**只能估计门延时,不能估计线延时**,仿真结果**与布线后的实际情况还有一定的差距**,并不十分准确。这种仿真的主要目的在于 **{% label prime@检查综合器的综合结果是否与设计输入一致 %}**。目前主流综合工具日益成熟,对于一般性设计,如果设计者确信自己表述明确,没有综合歧义发生,则可以省略综合后仿真步骤。**一般情况下,综合后仿真与功能仿真的仿真激励相同**。
## 3.8. 布局规划与布局布线
@@ -144,4 +151,10 @@ RTL 级综合指 **{% label @将 RTL 级 HDL 语言翻译成由与、或、非
将最终布局规划或布局布线的时延信息反标注到设计网表中,所进行的仿真就叫时序仿真或布局规划与布局布线后仿真,简称后仿真。布局规划与布局布线之后生成的仿真时延文件包含的时延信息最全,不仅包含门延时,还包含实际布线延时,所以时序仿真最准确,能较好地反映芯片的实际工作情况。一般来说,建议进行时序仿真步骤,通过时序仿真能检查设计时序与芯片实际运行情况是否一致,确保设计的可靠性和稳定性。时序仿真的主要目的在于发现时序违规(Timing Violation),即不满足时序约束条件或者器件固有时序规则(建立时间、保持时间等)的情况。
[^1]: 最新的 Verilog HDL标准为 1364-2005。
# 参考文献
EDA 先锋工作室. *轻松成为设计高手——Verilog HDL 实用精解.* 北京航空航天大学出版社, 2012.
[《Part 3——描述方式和设计层次》的“设计层次”]: https://josh-gao.top/posts/fd117896.html#%E8%AE%BE%E8%AE%A1%E5%B1%82%E6%AC%A1
[^1]: 最新的 Verilog HDL标准为 IEEE 1364-2005。
+196 -149
View File
@@ -20,19 +20,18 @@ toc:
expand_all: true
max_depth: 4
abbrlink: fd2ca242
description: 本文从一个 Verilog 用例出发,对 Verilog 语言的语法进行了阐释。包括的内容主要有 Verilog 语言的基本词法,常用的编译指令,逻辑值、常量、变量的含义,操作符的概念和分类等,还对 Verilog 中的参数、并发以及系统任务和系统函数进行了简要说明。
---
本文从一个 Verilog 用例出发,对 Verilog 语言的语法进行了阐释。包括的内容主要有 Verilog 语言的基本词法,常用的编译指令,逻辑值、常量、变量的含义,操作符的概念和分类等,还对 Verilog 中的参数、并发以及系统任务和系统函数进行了简要说明。
# 1. 两种设计方法(Top-Down 和 Bottom-Up)
在传统意义上,设计硬件电路主要是使用自底向上(Bottom-Up)的设计方法。工程师们总是从最底层的逻辑门开始,逐渐搭建成较大的模块,然后再将这些模块组成更大的模块,最后形成整个设计。
在 [Part 1——初识 HDL 设计方法](https://josh-gao.top/posts/460433dd.html)中已经提到,随着 HDL 和逻辑综合技术的进步,工程师们逐渐可以使用自顶向下(Top-Down)的方法来设计硬件。这样,工程师们**首先关注于设计的规格(Specification),然后将规格分解为一个个模块,再分解为更小的模块**。然后采用 HDL 的可综合子集直接描述硬件的行为,由逻辑综合工具自动完成由 HDL 到门级电路的转换。
在 [Part 1——初识 HDL 设计方法][]中已经提到,随着 HDL 和逻辑综合技术的进步,工程师们逐渐可以使用自顶向下(Top-Down)的方法来设计硬件。这样,工程师们**首先关注于设计的规格(Specification),然后将规格分解为一个个模块,再分解为更小的模块**。然后采用 HDL 的可综合子集直接描述硬件的行为,由逻辑综合工具自动完成由 HDL 到门级电路的转换。
<!-- more -->
最近几年,随着 IP核 市场的逐渐兴起,许多设计者逐渐意识到**利用现有的 IP核 可以帮助节约设计成本、减少设计周期**,有许多设计工程师,甚至希望所有的设计模块都使用现成的模块,自己仅仅开发一些简单的粘合逻辑。这就有点像电路板设计的过程,工程师将各种芯片集成到一块电路板上,自己完成这些芯片间的互连和一些简单的 CPLD 逻辑设计,以及对微处理器的编程。使用现有的 IP 来搭建系统实际上也是一种自底向上(Bottom-Up)的设计方法。
最近几年,随着 IP核 市场的逐渐兴起,许多设计者逐渐意识到**利用现有的 IP核可以帮助节约设计成本、减少设计周期**,有许多设计工程师,甚至希望所有的设计模块都使用现成的模块,自己仅仅开发一些简单的粘合逻辑。这就有点像电路板设计的过程,工程师将各种芯片集成到一块电路板上,自己完成这些芯片间的互连和一些简单的 CPLD 逻辑设计,以及对微处理器的编程。使用现有的 IP 来搭建系统实际上也是一种**自底向上**(Bottom-Up)的设计方法。
从以上描述可以看出,设计硬件电路时,这两种设计方法都有可能采用。Verilog HDL 可以完全支持这两种设计方法。
@@ -50,26 +49,28 @@ EDA 行业的先行者们发明了 Verilog 硬件描述语言,其最根本的
## 2.1. Verilog 实例
<a name="JoshVerilog模块"></a>首先看一个简单的如下图所示的电路(模块):JoshVerilog。它可以是一个独立的设计,也可以是更大的系统的一个组成部分。
<a id="HelloVlog"></a>
![ ](https://josh-blog-1257563604.cos.ap-beijing.myqcloud.com/img/2020-11-28-josh-verilog-part-2/2020-11-28-josh-verilog-part-2-010-JoshVerilog.png?imageMogr2/thumbnail/!100p|watermark/2/text/QEpvc2ggR2Fv/font/YWhyb25iZC50dGY=/fontsize/18/dissolve/20/gravity/southeast/dx/5/dy/5){width=800px}
首先看一个简单的如下图所示的电路(模块):`HelloVlog`。它可以是一个独立的设计,也可以是更大的系统的一个组成部分。
![HelloVlog 模块](https://josh-blog-1257563604.cos.ap-beijing.myqcloud.com/img/2020-11-28-josh-verilog-part-2/2020-11-28-josh-verilog-part-2-010-JoshVerilog.png!sign){width=800px}
该电路首先在两个 2 位的输入数据 `A_in[1:0]` 的和 `B_in[1:0]` 之间,由 `sel` 信号做二选一。后面是一个 2-4 译码电路,将输入的信号 `result[1:0]`解析成 `eq0`、`eql`、`eq2` 和 `eq3` 这4个信号,它们同时只有一个为 1。
将 `eq0` 和 `eql` 相异或(xor),`eq2` 和 `eq3` 相异或。然后,将两个异或的结果 `A_xor_wire` 和 `XOR_B` 分别寄存输出给 `A_xor_out` 和 `B_xor_out` 两个输出寄存器带有低有效(active low)的异步复位端。
将 `eq0` 和 `eql` 相异或(xor),`eq2` 和 `eq3` 相异或。然后,将两个异或的结果 `A_xor_wire` 和 `XOR_B` 分别寄存输出给 `A_xor_out` 和 `B_xor_out`。两个输出寄存器带有低有效(active low)的异步复位端。
**这些电路单元都是同时并行工作的,相互之间并没有先后顺序关系,这一点与软件设计程序很不一样。**
下面具体分析 [JoshVerilog 模块](#JoshVerilog模块) 的电路功能。源代码如下。
下面具体分析 [HelloVlog 模块](#HelloVlog) 的电路功能。源代码如下。
<a name="JoshVerilog代码"></a>
<a id="HelloVlogCode"></a>
```verilog Josh.v
``` verilog HelloVlog.v
// 编译指令, 定义时间单位和时间精度
`timescale 1ns/100p // 每个时间单位是1ns, 仿真精度是100ps
// module名 + 端口列表
module Josh( clk, rst_n, A_in, B_in, sel, A_xor_out, B_xor_out );
module HelloVlog( clk, rst_n, A_in, B_in, sel, A_xor_out, B_xor_out );
// IO端口声明
input clk;
@@ -143,19 +144,19 @@ endmodule
对于 `MUX21` 这样二选一的电路,将采用如下的数据流描述(assign语句):
```verilog Josh.v
``` verilog HelloVlog.v
assign #3 result = sel ? B_in : A_in;
```
`assign` 是 Verilog 中的关键词,用它赋值的语言称为**连续赋值语句**。
如果 `sel`为真(1),则选择 `B_in`,否则将选择 `A_in`,而 `#3` 表示经过 3 个延时单位,再将选择结果赋值给 `result`,这也模拟了组合逻辑的延时。由于在[代码](#JoshVerilog代码)最开头已经使用 Verilog 的编译指令将延时单位定义为 1ns(`timescale 1ns/100ps`),因此这里的 `#3` 代表 3ns 的延时。
如果 `sel`为真(1),则选择 `B_in`,否则将选择 `A_in`,而 `#3` 表示经过 3 个延时单位,再将选择结果赋值给 `result`,这也模拟了组合逻辑的延时。由于在[代码](#HelloVlogCode)最开头已经使用 Verilog 的编译指令将延时单位定义为 1ns(`timescale 1ns/100ps`),因此这里的 `#3` 代表 3ns 的延时。
2. 2 - 4 译码器
关于实现 2 - 4 译码器,则采用了如下的描述:
```verilog Josh.v
``` verilog HelloVlog.v
always @(result) begin
case(result)
2'b00: begin
@@ -168,31 +169,31 @@ endmodule
end
```
这里采用了另一种描述方式:用 `always` 语句来描述电路的行为。通过 `case` 判断 `result` 的值来决定 `eq0`、`eq3` 的值。同样,也采用 `#2` 来模拟组合逻辑的延时。`always` 和 `case` 也是 Verilog 中的关键词。这样的描述方法称之为**行为描述**,它侧重于描述电路的行为。
这里采用了另一种描述方式:用 `always` 语句来描述电路的行为。通过 `case` 判断 `result` 的值来决定 `eq0` ~ `eq3` 的值。同样,也采用 `#2` 来模拟组合逻辑的延时。`always` 和 `case` 也是 Verilog 中的关键词。这样的描述方法称之为**行为描述**,它侧重于描述电路的行为。
```verilog Josh.v
``` verilog HelloVlog.v
{eq3, eq2, eq1, eq0} = #2 4'b0001;
```
该语句表示将 `4'b0001` 赋值给 `eq3~eq0` 合并成的 4 位变量。`{}` 是 Verilog 的合并符号。
该语句表示将 `4'b0001` 赋值给 `eq3` ~ `eq0` 合并成的 4 位变量。`{}` 是 Verilog 的合并符号。
3. 异或门
对于 `A_xor_wire` 和 `XOR_B` 的两个完全一样的异或门,也采用了两种不同的描述方式:
```verilog Josh.v
``` verilog HelloVlog.v
assign #1 A_xor_wire = eq0 ^ eq1;
```
和
```verilog Josh.v
``` verilog HelloVlog.v
xor #1 XOR_B(B_xor_wire, eq2, eq3);
```
`xor` 是 Verilog 中自带的基本逻辑门原语。这里相当于调用了该 `xor` 门。而 `B_xor_wire`、`eq2` 和 `eq3` 是代入到 `xor` 门中的参数。`eq2` 和 `eq3` 是输入,`B_xor_wire` 是输出。这里 `B_xor_wire` 是 `eq2` 和 `eq3` 相异或的结果。
在 Verilog 中,**将调用其他功能模块(包括 Verilog 的内嵌基本逻辑门)称为“实例化(lnstantiate)”**。实例化**类似**于软件设计中的调用,但不能简单理解为软件中的调用。软件调用过程是顺序执行的,而实例化的硬件电路在设计中是独立于其他功能块,并行运行的。
在 Verilog 中,**将调用其他功能模块(包括 Verilog 的内嵌基本逻辑门)称为“实例化(Instantiate)”**。实例化**类似**于软件设计中的调用,但不能简单理解为软件中的调用。软件调用过程是顺序执行的,而实例化的硬件电路在设计中是独立于其他功能块,并行运行的。
这种**在模块中实例化其他功能模块**的描述方式被称之为**结构化描述**。
@@ -201,59 +202,75 @@ endmodule
从以上的描述可以看出 Verilog 语言的 3 种基本的描述方法(基本上所有的 Verilog 功能模块都是由这 3 种方式来描述):
- **数据流描述**:采用 `assign` 语句,该语句被称为连续赋值语句;
- **行为描述**:使用 `always` 或 `initial` 语句块,其中出现的语句被称为过程赋值语句;
- **结构化描述**:实例化已有的功能模块。结构化描述主要有以下3种:
- Module实例化:实例化已有的 module;
- **行为描述**:使用 `always` 或 `initial` 语句块,其中出现的语句被称为过程赋值语句;
- **结构化描述**:实例化已有的功能模块。结构化描述主要有以下 3 种:
- Module 实例化:实例化已有的 module;
- 门实例化:实例化基本的门电路原语;
- 用户定义原语(UDP)实例化:实例化用户定义的原语。
在[《Part 3——描述方式和设计层次》](https://josh-gao.blog.csdn.net/article/details/105262549)中会将详细介绍 Verilog 语言中的这 3 种描述方式。
在[《Part 3——描述方式和设计层次》][]中会详细介绍 Verilog 语言中的这 3 种描述方式。
# 3. Verilog 的基本词法
Verilog HDL 是一种**大小写敏感**的语言,这一点与 VHDL 不同,因此在书写的时候要格外注意。在 Verilog 语言中,**所有的关键字(又叫保留字)都为小写**。Verilog 的内部信号名(或称为标识符)使用大写和小写都可以。标识符可以是字母、数字、`$`(美元符号)和 `_`(下划线)任意组合,只要**第一个字符是字母或者下划线**即可。
1. Verilog HDL 是一种**大小写敏感**的语言,这一点与 VHDL 不同,因此在书写的时候要格外注意。在 Verilog 语言中,**所有的关键字(又叫保留字)都为小写**。Verilog 的内部信号名(或称为标识符)使用大写和小写都可以。标识符可以是字母、数字、`$`(美元符号)和 `_`(下划线)任意组合,只要**第一个字符是字母或者下划线**即可。
> 因为 Verilog HDL 对大小写敏感,因此养成良好的信号定义习惯非常重要,否则容易因为大小写不同造成信号的歧义。推荐的信号定义习惯为:**所有信号和网线名均小写,用下划线分割单词,而宏变量全部用大写字母。**
{% note warning %}
在上述[代码](#JoshVerilog代码)中,用双反斜线 `//` 表示注释。另外,还有一种注释方式,用 `/* ...... */` 来表示。所不同的是前者为单行注释,而后者将 `/*` 和 `*/` 之间的内容全部看作注释内容。
因为 Verilog HDL 对大小写敏感,因此养成良好的信号定义习惯非常重要,否则容易因为大小写不同造成信号的歧义。推荐的信号定义习惯为:**所有信号和网线名均小写,用下划线分割单词,而宏变量全部用大写字母。**
> 通常,注释的内容只是作者为了增强代码的可读性而增加的内容,对整个代码的功能没有任何影响。不过,在一些工具中,尤其是逻辑综合工具,定义了一些特殊的指令,用于控制工具编译过程。这些指令也是以注释的方式出现的。例如:
>
> ```verilog
> moduleblbox(out, data, clk) /* synthesis syn_black_box */;
> ```
>
> 在 `modulebl_box` 的声明处有一行注释,用 `/* ...... */` 表示。它看起来是一个注释,实际上,是综合工具Synplify中的一个指令,指示 Synplify 将该模块看作一个黑盒(black_box),不处理模块内部的描述。
{% endnote %}
在 Verilog 中,通常使用空格符、跳格符和换行符作为间隔。在书写代码的时候,适当运用间隔符可以提高代码的可读性。比如在声明 4 个 `reg` 型数据 `eq0~eq3` 时,可以采用上述[例子](#JoshVerilog代码)中的方法:
2. 在上述[代码](#HelloVlogCode)中,用双反斜线 `//` 表示注释。另外,还有一种注释方式,用 `/* ...... */` 来表示。所不同的是前者为单行注释,而后者将 `/*` 和 `*/` 之间的内容全部看作注释内容。
```verilog Josh.v
reg eq0, eq1, eq2, eq3;
```
{% note info %}
也可以用换行符将其分开:
通常,注释的内容只是作者为了增强代码的可读性而增加的内容,对整个代码的功能没有任何影响。不过,在一些工具中,尤其是逻辑综合工具,定义了一些特殊的指令,用于控制工具编译过程。这些指令也是以注释的方式出现的。例如:
```verilog
reg eq0;
reg eq1;
reg eq2;
reg eq3;
```
``` verilog
module bl_box(out, data, clk) /* synthesis syn_black_box */;
```
在verilog中,还有一些转义字符。比如 `\n` 表示换行符;`\t` 表示 Tab 键;防止引起歧义,就用 `\` 表示 `\` 符号本身等等。这里不再对 Verilog 的词法做过多的描述,更多可以参考 Verilog IEEE 国际标准([IEEE Std. 1364-2005](https://standards.ieee.org/standard/1364-2005.html))。
在 `module bl_box` 的声明处有一行注释,用 `/* ...... */` 表示。它看起来是一个注释,实际上,是综合工具 Synplify 中的一个指令,指示 Synplify 将该模块看作一个黑盒(black_box),不处理模块内部的描述。
{% endnote %}
3. 在 Verilog 中,通常使用空格符、跳格符和换行符作为间隔。在书写代码的时候,适当运用间隔符可以提高代码的可读性。比如在声明 4 个 `reg` 型数据 `eq0~eq3` 时,可以采用上述[例子](#HelloVlogCode)中的方法:
``` verilog HelloVlog.v
reg eq0, eq1, eq2, eq3;
```
也可以用换行符将其分开:
``` verilog
reg eq0;
reg eq1;
reg eq2;
reg eq3;
```
4. 在verilog中,还有一些转义字符。比如 `\n` 表示换行符;`\t` 表示 Tab 键;防止引起歧义,就用 `\` 表示 `\` 符号本身等等。这里不再对 Verilog 的词法做过多的描述,更多可以参考 Verilog IEEE 国际标准([IEEE Std. 1364-2005][])。
# 4. 模块和端口
大型设计往往是由一个个模块构成的。实际上,模块可大可小,大到一个复杂的微处理器系统,小到一个基本的晶体管,都可以作为一个模块来设计。例如,在上述代码中描述的 [JoshVerilog](#JoshVerilog代码) 就是一个模块。
大型设计往往是由一个个模块构成的。实际上,模块可大可小,大到一个复杂的微处理器系统,小到一个基本的晶体管,都可以作为一个模块来设计。例如,在上述代码中描述的 [HelloVlog](#HelloVlogCode) 就是一个模块。
在 Verilog 中,模块(module)是基本的组成单位。
> 通常,建议在一个 Verilog 文件中,只放一个 `module` 定义,而且使文件名称和 `module` 名称一致。这是一个良好的设计习惯。
{% note info %}
通常,建议在一个 Verilog 文件中,只放一个 `module` 定义,而且使文件名称和 `module` 名称一致。这是一个良好的设计习惯。
{% endnote %}
以下是 Verilog 中 `module` 的基本语法:
```verilog
``` verilog
module 模块名称( 端口列表 );
// 声明:
reg, parameter,
@@ -265,7 +282,7 @@ module 模块名称( 端口列表 );
module 实例化
门实例化
用户定义原语 (UDP) 实例化
连续赋值 (Continous assignment)
连续赋值 (Continuous assignment)
endmodule
```
@@ -275,9 +292,9 @@ endmodule
有些 `module` 也不包含端口。例如,在仿真平台的顶层模块中,其内部已经实例化了所有的设计模块和激励模块,是一个封闭的系统,没有输入和输出。一般这种没有端口的模块都是用于仿真的,不用作实际电路。
在 `module` 内部的声明部分,需要声明端口的方向,(`input`, `output` 和 `inout`)和位宽。按照 Verilog 的习惯,**高位写在左边,低位写在右边**。比如
在 `module` 内部的声明部分,需要声明端口的方向(`input`, `output` 和 `inout`)和位宽。按照 Verilog 的习惯,**高位写在左边,低位写在右边**。比如
```verilog
``` verilog
input [1:0] A_in;
```
@@ -297,12 +314,16 @@ input [1:0] A_in;
- `always` 语句;
- 其他子 `module` 实例化;
- 门实例化;
- 用户定义原语(UDP)实例化;
- 连续赋值(Continous assignment)。
- 用户定义原语(User-Defined Primitive, UDP)实例化;
- 连续赋值(Continuous assignment)。
Verilog 中所有的功能描述都是通过以上几种描述方式进行的。
> 需要格外注意的是,以上几种语句如果出现在同一个 `module` 内,其**相互之间是没有任何顺序关系的**,它们在 `module` 中出现顺序的改变不会改变 `module` 的功能,这正是硬件的一大特点。有硬件电路原理图设计经验的读者们可以想象一下画原理图的过程,先画哪个,后画哪个器件根本没有任何关系。在 Verilog 中也是类似的道理。
{% note warning %}
需要格外注意的是,以上几种语句如果出现在同一个 `module` 内,其**相互之间是没有任何顺序关系的**,它们在 `module` 中出现顺序的改变不会改变 `module` 的功能,这正是硬件的一大特点。有硬件电路原理图设计经验的读者们可以想象一下画原理图的过程,先画哪个,后画哪个器件根本没有任何关系。在 Verilog 中也是类似的道理。
{% endnote %}
# 5. 编译指令
@@ -316,18 +337,18 @@ Verilog 中的编译指令是从 C 语言中的“预处理指令”演变得来
- `` `include``;
- `` `resetall``。
与 C 语言中使用的 `#` 不同,Verilog 中使用反引号一来标识编译指令。编译器一旦遇到某个编译指令,则其在整个编译过程中有效,可以包含多个文件,直到编译器遇到另一个相同的编译指令。
与 C 语言中使用的 `#` 不同,Verilog 中使用反引号“`` ` ``”来标识编译指令。编译器一旦遇到某个编译指令,则其在整个编译过程中有效,可以包含多个文件,直到编译器遇到另一个相同的编译指令。
## 5.1. `` `timescale``
在每一个 `module` 文件前面加上`` ` ``的编译指令,就可以保证该文件中的延时信息受其自身文件中的 `` `timescale`` 编译指令指导。**否则在编译过程中,该模块将沿用上一个** `` `timescale`` **的值,或者使用缺省值**。
在每一个 `module` 文件前面加上 `` ` `` 的编译指令,就可以保证该文件中的延时信息受其自身文件中的 `` `timescale`` 编译指令指导。**否则在编译过程中,该模块将沿用上一个** `` `timescale`` **的值,或者使用缺省值**。
在上述实例代码中描述的 [JoshVerilog](#JoshVerilog代码) 模块中使用了一个`` `timescale 1ns/100ps`` 编译指令。其中 `1ns` 表示延时单位,`100ps` 表示时间精度,也就是编译器所能接收的最小仿真时间粒度。`` `timescale`` 编译指令在模块外部出现,并且影响后面模块中所有的时延值,直到遇到下一个 `` `timescale`` 或 `` `resetall``指令。
在上述实例代码描述的 [HelloVlog](#HelloVlogCode) 模块中使用了一个`` `timescale 1ns/100ps`` 编译指令。其中 `1ns` 表示延时单位,`100ps` 表示时间精度,也就是编译器所能接收的最小仿真时间粒度。`` `timescale`` 编译指令在模块外部出现,并且影响后面模块中所有的时延值,直到遇到下一个 `` `timescale`` 或 `` `resetall``指令。
比如语句:
```verilog
assign #1.16 A_xor_wire = eq0 ^ eq1;
``` verilog
assign #(1.16) A_xor_wire = eq0 ^ eq1;
```
如果采用 `` `timescale 1ns/100ps`` 编译指令由于延时单位是 `1ns`,同时最小时间粒度是 `100ps`,即 `0.1ns` ,那么根据四舍五入的规则,`1.16ns` 则实际上对应 `1.2ns` 延时。如果采用 `` `timescale 1ns/10ps`` 编译指令,由于延时单位是 `1ns`,同时最小时间粒度是 `10ps`,即 `0.01ns`,那么 `1.16ns` 则实际上对应 `1.16ns` 延时。
@@ -336,7 +357,7 @@ assign #1.16 A_xor_wire = eq0 ^ eq1;
`` `define`` 用于定义宏。例如,首先定义一个总线宽度的宏为 16,然后利用这个宏定义一个宽度为 16 的 `reg`类型数据 `Data`,方法如下:
```verilog
``` verilog
`define BUS_WIDTH 16
// ...
reg [`BUS_WIDTH - 1 : 0] Data
@@ -346,7 +367,7 @@ reg [`BUS_WIDTH - 1 : 0] Data
`` `define`` 指令被编译以后,则在整个编译过程中都有效,直到遇到 `` `undef`` 指令,该宏就不再有效。如下:
```verilog
``` verilog
`undef BUS_WIDTH
```
@@ -356,11 +377,11 @@ reg [`BUS_WIDTH - 1 : 0] Data
再来看看如下的条件编译指令:
```verilog
``` verilog
`ifdef NARROW
parameter BUS_WIDTH = 16;
`else
paramete rBUS_WIDTH = 32;
parameter BUS_WIDTH = 32;
`endif
```
@@ -370,7 +391,7 @@ reg [`BUS_WIDTH - 1 : 0] Data
在 Verilog 中,可以使用 `` `include`` 指令来嵌入某个文件的内容。例如:
```verilog
``` verilog
`include "HEADFILE.h"
```
@@ -380,26 +401,35 @@ reg [`BUS_WIDTH - 1 : 0] Data
`` `resetall`` 编译指令会将所有其他编译指令重新设置为缺省值,要谨慎使用。
> Verilog 语言中的编译指令不止这几条,其他不常用的指令这里不一一介绍,有兴趣可以参考其他文献。
{% note info %}
Verilog 语言中的编译指令不止这几条,其他不常用的指令这里不一一介绍,有兴趣可以参考其他文献。
{% endnote %}
# 6. 逻辑值与常量
## 6.1. 逻辑值
在二进制计数中,单位逻辑值只有 `1` 和 `0` 两种状态。而在 Verilog 语言中,为了对电路进行精确建模,于是增加了两种逻辑状态 `X` 和 `Z`。
在二进制计数中,单位逻辑值只有 `1` 和 `0` 两种状态。而在 Verilog 语言中,为了对电路进行精确建模,增加了两种逻辑状态 `X` 和 `Z`。
- `X` 表示未知值(unknown),或者不关心(don't care),`X` 用作信号状态时表示未知,用在条件判断时(在 `casex` 或 `casez`中)表示不关心;
- `Z` 表示高阻状态,也就是**没有任何驱动**,通常用来对**三态总线**进行建模。
在后面将提到 Verilog 语言中的变量类型。通常 `net` 型变量如果没有初始化,其值为 `Z`,`register` 型变量如果没有初始化,其值为 `X` 。这一点在仿真时显得比较有意义。
在[第 7 节](#7)将提到 Verilog 语言中的变量类型。通常 `net` 型变量如果没有初始化,其值为 `Z`,`register` 型变量如果没有初始化,其值为 `X` 。这一点在仿真时显得比较有意义。
> 但是,对综合工具而言(或者说在实际电路中),并没有什么 `X` 值,只存在 `0`、`1` 和 `Z` 这 3 种状态。在实际电路中还可能出现亚稳态,它既不是 `0`,也不是 `1`,是一种暂时的不稳定状态。
{% note warning %}
但是,对综合工具而言(或者说在实际电路中),并没有什么 `X` 值,只存在 `0`、`1` 和 `Z` 这 3 种状态。在实际电路中还可能出现亚稳态,它既不是 `0`,也不是 `1`,是一种暂时的不稳定状态。
{% endnote %}
Verilog 语言中所有数据都是由以上描述的 4 种基本的逻辑值 `0`、`1`、`X` 和 `Z` 构成。同时,`X` 和 `Z` 是不区分大小写的。例如:`0z1x` 和 `0Z1X` 表示同一个数据。
## 6.2. 常量
常量是 Verilog 中不变的数值。在上述[实例代码](#JoshVerilog代码)中,`4'b0001` 就表示一个 4 位的二进制整型常量:0001。
常量是 Verilog 中不变的数值。在上述[实例代码](#HelloVlogCode)中,`4'b0001` 就表示一个 4 位的二进制整型常量:0001。
Verilog中的常量有3种:
@@ -416,18 +446,18 @@ Verilog中的常量有3种:
整数型常量也可以采用基数表示法,例如:
- `8'haa`:表示 8 位的**十六进制数**,换算成二进制是 1010_1010。
- `6'o33`:表示 6 位的**八进制数**,换算成二进制是 011_011。
- `4'b1011`:表示 4 位的**二进制数** 1011。
- `3'd7`:表示 3 位**十进制**的 7。
- `8'haa`:表示 8 位的**十六进制数**,换算成二进制是 `1010_1010`。
- `6'o33`:表示 6 位的**八进制数**,换算成二进制是 `011_011`。
- `4'b1011`:表示 4 位的**二进制数** `1011`。
- `3'd7`:表示 3 位**十进制**的 `7`。
在基数表示法中,都是以如下格式写的:
```verilog
``` verilog
[长度]'数值符号 数字
```
其中长度可有可无,数值符号中,`h` 表示十六进制,`o` 表示八进制,`b` 表示二进制,`d` 表示十进制数据。如果长度比后面数字的实际位数多,则**自动在数字的左边补足 0**;如果位数少,则**自动截断数字左边超出的位数**。
其中**长度可有可无**,数值符号中,`h` 表示十六进制,`o` 表示八进制,`b` 表示二进制,`d` 表示十进制数据。如果长度比后面数字的实际位数多,则**{% label primary @自动在数字的左边补足 0 %}**;如果位数少,则**{% label primary @自动截断数字左边超出的位数 %}**。
如果将数字写成 `'haa`,那么这个十六进制数的长度就决定于数字本身的长度。
@@ -439,23 +469,23 @@ Verilog中的常量有3种:
Verilog 语言中的实数型变量可以采用十进制,也可以采用科学计数法,例如:
```verilog
``` verilog
5.512
13_2.18e2 // 表示13218
```
### 6.2.3. 字符串型常量
字符串是双引号中的字符序列,例如:`"HeIIo World"`。字符串是 8 位 ASCII 码值的序列,`"HeIIo World"` 就需要 11 字节存储,方法如下:
字符串是双引号中的字符序列,例如:`"Hello World"`。字符串是 8 位 ASCII 码值的序列,`"Hello World"` 就需要 11 字节存储,方法如下:
```verilog
reg[1:8 * 11] Message;
Message = "HeIIo World";
``` verilog
reg [1:8 * 11] Message;
Message = "Hello World";
```
这样就将字符串常量存入到 Message 变量中。
这样就将字符串常量存入到 `Message` 变量中。
# 7. 变量类型
# 7. 变量类型<a id=7></a>
在 Verilog 语言中,有两大变量类型:
@@ -464,9 +494,13 @@ Message = "HeIIo World";
对于初学者而言,则需要首先遵守如下的简单规则:
{% note primary %}
- **凡是在** `always` **或** `initial` **语句中赋值的变量,一定是寄存器变量**;
- **在** `assign` **中赋值的一定是线网变量**。
{% endnote %}
## 7.1. 线网类型
在线网类型下,分为几种子类,它们具有线网的共性:
@@ -475,14 +509,14 @@ Message = "HeIIo World";
- `wor`、`trior`:表示该连线具有“线或”功能;
- `wand`、`triand`:表示该连线具有“线与”功能;
- `trireg`:表示该连线具有总线保持功能;
- `tri1`、`tri0`:表示当无驱动时,连线状态为1(`tri1`)和0(`tri0`);
- `tri1`、`tri0`:表示当无驱动时,连线状态为 1(`tri1`)或 0(`tri0`);
- `supply1`、`supply0`:分别表示电源和地信号。
在以上描述的线网类型中,除了 `trireg` 未初始化时为 `X` 以外,其余的未初始化时的值为 `Z`。
线网类型主要用在连续赋值语句中,以及作为模块之间的互连信号。
在后面的内容中会详细阐述常用线网类型的使用方法。
在[《Part 3——描述方式和设计层次》][]中会详细阐述常用线网类型的使用方法。
## 7.2. 寄存器类型
@@ -499,14 +533,14 @@ Message = "HeIIo World";
`reg` 类型可以定位为一个寄存器,可以定义一位或者多位,例如:
```verilog
``` verilog
reg AB; // 定义一个名为 AB 的 1 位寄存器
reg [3:0] ABC; // 定义一个名为 ABC 的 4 位寄存器
```
在多位寄存器中,可以作“位选择”或“部分选择”,例如:
```verilog
``` verilog
ABC [3] = 1; // 将 ABC 的第 3 位赋值为 1
ABC [0] = 0; // 将 ABC 的第 0 位赋为 0
ABC [2:1] = 2'b01; // 将 ABC 的第 1、2 位赋值为 1 和 0
@@ -518,25 +552,25 @@ Message = "HeIIo World";
`reg` 类型可以作为二维数组,也就是存储器,例如:
```verilog
``` verilog
reg [3:0] MEMABC [0:7]; // 定义一个存储器,地址为 0~7,每个存储单元是 4 位
```
与一维的 `reg` 变量不同的是,存储器中的存储单元不能再做位选择或部分选择,而每个单元可以单独赋值。比如:
```verilog
``` verilog
MEMABC[1] = 4'b0101; // 为 MEMABC 中的第 1 个存储单元赋值 4'b0101
```
同时,在 Verilog 中,不存可以对整个存储器赋值的语句,必须对每个单元独立赋值。除非使用 `$readmemb` 或 `$readmemh` 系统任务从文件中读入整个或者部分存储器的数据。
同时,在 Verilog 中,**不存可以对整个存储器赋值的语句**,必须对每个单元独立赋值。除非使用 `$readmemb` 或 `$readmemh` 系统任务从文件中读入整个或者部分存储器的数据。
[comment]: <> (在第7章将讨论如何从文件中读入数据给存储器赋值。)
> 在[《Part 7—— 逻辑验证与 testbench 编写》][]中将讨论如何从文件中读入数据给存储器赋值。
### 7.2.2. `integer` 类型
`integer` 变量通常用于高层次建模,也常用在 `for` 语句的索引中,例如:
```verilog
``` verilog
initial
begin: ACCESS
integer i;
@@ -553,59 +587,62 @@ end
这里需要引起重视,**“线网"变量可以理解为电路模块中的连线,但“寄存器"并不严格对应于电路上的存储单元**,包括触发器(flip-flop)或锁存器(latch)。从纯粹语言表达的语义角度来说,寄存器类型变量的值,从一个赋值到下一个赋值被保存下来,并且在仿真过程中会保持而不会丢失。
实际上,从语义上来讲,在 Verilog 仿真工具对语言进行仿真的时候,**寄存器类型的变量是占用仿真环境的物理内存的**,这与 C 语言中的变量类似。寄存器在被赋值后,便一直保存在内存中,保持该值不变,直到再次对该寄存器变量进行赋值。而**线网类型是不占用仿内存的**,它的值是由当前所有驱动该线网的其他变量(可以是寄存器或线网)决定的。这是寄存器和线网最大的区别,也是当初 Verilog 的发明者定义“线网”和“寄存器”变量的根本动机。
实际上,从语义上来讲,在 Verilog 仿真工具对语言进行仿真的时候,**寄存器类型的变量是占用仿真环境的物理内存的**,这与 C 语言中的变量类似。寄存器在被赋值后,便一直保存在内存中,保持该值不变,直到再次对该寄存器变量进行赋值。而**线网类型是不占用仿真内存的**,它的值是由当前所有驱动该线网的其他变量(可以是寄存器或线网)决定的。这是寄存器和线网最大的区别,也是当初 Verilog 的发明者定义“线网”和“寄存器”变量的根本动机。
在下一小节中,将引入“驱动”和“赋值”两个概念,深入探讨两种变量的含义。
## 7.4. 驱动和赋值
为了更清楚地描述寄存器和线网变量的概念,将以上述[实例代码](#JoshVerilog代码)中的语句来说明。首先,引入Verilog 语言中两个重要的概念:驱动(Driving)和赋值(Assigning)。
为了更清楚地描述寄存器和线网变量的概念,将以上述[实例代码](#HelloVlogCode)中的语句来说明。首先,引入 Verilog 语言中两个重要的概念:驱动(Driving)和赋值(Assigning)。
{% note primary %}
- **线网是被驱动的,该值不被保持,在任意一个仿真步进上都需要重新计算**;
- **寄存器是被赋值的,且该值在仿真过程中被保持,直到下一个赋值的出现**。
在上述[实例代码](#JoshVerilog代码)中,定义了一个 `A_xor_wire` 的 `wire`,它是 `eq0` 和 `eq1` 相异或的结果。采用如下描述方式:
{% endnote %}
```verilog
在上述[实例代码](#HelloVlogCode)中,定义了一个 `A_xor_wire` 的 `wire`,它是 `eq0` 和 `eq1` 相异或的结果。采用如下描述方式:
``` verilog
assign #1 A_xor_wire = eq0 ^ eq1;
```
实际上,也可以采用如下的另一种描述方式:
```verilog
``` verilog
always @(eq0 or eq1)
A_xor_wire = #1 eq0 ^ eq1;
```
当然需要在 `module` 的声明处,将 `A_xor_wire` 首先定义成 `reg` 变量,而不是 `wire` 变量,即需要做如下定义:
```verilog
``` verilog
reg A_xor_wire
```
这两者描述的目的一样,都是一个异或门,如下图所示。
<div align="center">
<img src="https://josh-blog-1257563604.cos.ap-beijing.myqcloud.com/img/2020-11-28-josh-verilog-part-2/2020-11-28-josh-verilog-part-2-020-XORGate.png!700px"/>
</div>
<a id=XorGate></a>
![异或门](https://josh-blog-1257563604.cos.ap-beijing.myqcloud.com/img/2020-11-28-josh-verilog-part-2/2020-11-28-josh-verilog-part-2-020-XORGate.png!sign){width=700px}
下面从语义上的角度探讨两种描述方式的不同。
第一种描述方式使用 `assign` 语句,Verilog 中将其称为连续赋值语句(Continously Assignment)实际上是**连续驱动**的过程。也就是说,在任意一个仿真时刻,当前时刻 `eq0` 和 `eq1` 相异或的结果决定了 `1ns` 以后(语句 `#1` 的延时控制)的线网变量 `A_xor_wire` 的值,**不管 `eq0` 和 `eq1` 变化与否,这个驱动过程一直存在**,因此称为连续驱动。(在仿真器中,线网变量是不占用仿真内存空间的。)如上[异或门图](#异或门)中的时序所示,这个驱动过程在任意时刻都存在。
第一种描述方式使用 `assign` 语句,Verilog 中将其称为连续赋值语句(Continuously Assignment),实际上是**连续驱动**的过程。也就是说,在任意一个仿真时刻,当前时刻 `eq0` 和 `eq1` 相异或的结果决定了 `1ns` 以后(语句 `#1` 的延时控制)的线网变量 `A_xor_wire` 的值,**不管 `eq0` 和 `eq1` 变化与否,这个驱动过程一直存在**,因此称为连续驱动。(在仿真器中,线网变量是不占用仿真内存空间的。)如上[异或门图](#XorGate)中的时序所示,这个驱动过程在任意时刻都存在。
在第二种描述方式中使用了 `always` 语句,后面紧跟着一个敏感列表:`@(eq0 or eq1)` 因此,这个语句只有在 `eq0` 或 `eq1` 发生变化时才会执行。如上[异或门图](#异或门)中,在时刻 2、3 和 6,该语句都将执行,将 `eq0` 和 `eq1` 赋值的结果延时 1ns 以后赋值给 `A_xor_wire` 变量。**在其他时刻,`A_xor_wire` 变量必须保持**。因此,**从仿真语义上讲,需要一个存储单元,也可以说是寄存器,来保存 `A_xor_wire` 变量的中间值**。这就是Verilog 语言的“寄存器类型”变量的来历,而这个 `A_xor_wire` 变量首先需要定义为 `reg` 类型。
在第二种描述方式中使用了 `always` 语句,后面紧跟着一个敏感列表:`@(eq0 or eq1)` 因此,这个语句只有在 `eq0` 或 `eq1` 发生变化时才会执行。如上[异或门图](#XorGate)中,在时刻 2、3 和 6,该语句都将执行,将 `eq0` 和 `eq1` 赋值的结果延时 1ns 以后赋值给 `A_xor_wire` 变量。**在其他时刻,`A_xor_wire` 变量必须保持**。因此,**从仿真语义上讲,需要一个存储单元,也可以说是寄存器,来保存 `A_xor_wire` 变量的中间值**。这就是 Verilog 语言的“寄存器类型”变量的来历,而这个 `A_xor_wire` 变量首先需要定义为 `reg` 类型。
不管采用哪种方式,所描述的是一样的组合逻辑电路。尤其是第二种描述,虽然其在语言中被定义为 reg 型,但并不是对应硬件上的触发器(flip-flop),而是 Verilog 语言仿真语义上的寄存器概念。
不管采用哪种方式,所描述的是一样的组合逻辑电路。尤其是第二种描述,虽然其在语言中被定义为 `reg` 型,但并不是对应硬件上的触发器(flip-flop),而是 Verilog 语言仿真语义上的寄存器概念。
但是,在对实际电路中D触发器建模的时候,必须采用 `reg` 型的变量。下图是 D 触发器的模型。
但是,在对实际电路中 D 触发器建模的时候,必须采用 `reg` 型的变量。下图是 D 触发器的模型。
<div align="center">
<img src="https://josh-blog-1257563604.cos.ap-beijing.myqcloud.com/img/2020-11-28-josh-verilog-part-2/2020-11-28-josh-verilog-part-2-030-DFlipFlop.png!300px"/>
</div>
![D 触发器模型](https://josh-blog-1257563604.cos.ap-beijing.myqcloud.com/img/2020-11-28-josh-verilog-part-2/2020-11-28-josh-verilog-part-2-030-DFlipFlop.png!sign){width=300px}
在上述[实例代码](#JoshVerilog代码)中相应的描述如下:
```verilog Josh.v
在上述[实例代码](#HelloVlogCode)中相应的描述如下:
``` verilog HelloVlog.v
always @(posedge clk or negedge rst_n)
if (~rst_n)
A_xor_out <= 0;
@@ -613,7 +650,7 @@ always @(posedge clk or negedge rst_n)
A_xor_out <= A_xor_wire;
```
D 触发器只对时钟和复位(置位)敏感,因此在敏感列表中,列出了 `clk` 的上升沿和 `rst_n` 的下降沿。如果 `rst_n` 为 0,就对触发器的输出复位,否则在 `clk` 的时钟上升沿发生的时候,将输入的 `A_xor_wire` 寄存到触发器的输出端 `A_xor_out`。
D 触发器**只对时钟和复位(置位)敏感**,因此在敏感列表中,列出了 `clk` 的上升沿和 `rst_n` 的下降沿。如果 `rst_n` 为 0,就对触发器的输出复位,否则在 `clk` 的时钟上升沿发生的时候,将输入的 `A_xor_wire` 寄存到触发器的输出端 `A_xor_out`。
这样的代码精确地描述了一个 D 触发器的行为。这里的 `reg` 变量就对应了硬件中的 D 触发器。
@@ -623,7 +660,7 @@ D 触发器只对时钟和复位(置位)敏感,因此在敏感列表中,
参数是一种常量,通常出现在 `module` 内部。它常常被用作定义状态机的状态、数据位宽和延时大小等等。例如:
```verilog
``` verilog
parameter and_delay = 2;
parameter xor_delay = 4;
```
@@ -632,7 +669,7 @@ parameter xor_delay = 4;
前面介绍的 `` `define`` 是一种全局的定义,而 `parameter` 是出现在模块内部的**局部定义**,而且可以被**灵活改变**,这是 `parameter` 的一个重要特征。
在[《Part 3——描述方式和设计层次》的 4.2 小节](https://josh-gao.blog.csdn.net/article/details/105262549#42__865)中会详细探讨改变模块参数的方法。
在[《Part 3——描述方式和设计层次》的 4.2 小节][]中会详细探讨改变模块参数的方法。
# 9. Verilog 中的并发与顺序
@@ -640,7 +677,7 @@ parameter xor_delay = 4;
与在处理器上运行的软件不同的是,**硬件电路之间的工作是并行的**。
为了描述硬件的并行性,Verilog 语言本身就具有并发的特性。在 Verilog 语言的 `module` 中,所有的描述语句(包括连续赋值语句,行为语句块:`always` 和 `initial`,模块实例化等)之间都是并行发生的。任何功能描述语句,在 Verilog 的 module 中的顺序都不重要。
为了描述硬件的并行性,Verilog 语言本身就具有并发的特性。在 Verilog 语言的 `module` 中,所有的描述语句(包括连续赋值语句,行为语句块:`always` 和 `initial`,模块实例化等)之间都是并行发生的。任何功能描述语句,在 Verilog 的 `module` 中的顺序都不重要。
但是,在语句块(`always` 和 `initial`)内部,则可以存在两种语句组:
@@ -662,7 +699,7 @@ Verilog 仿真器用来模拟硬件的并行行为的方式类似于软件中的
操作符是操作数之间的运算符号。在介绍操作数之前,先来看看 Verilog 中的各种操作符。
| 操作符 | 含义 | 操作符 | 含义 |
| :-- | :-- | :-- | :-- |
| :--: | :----: | :--: | :----: |
| `+` | 一元加 | `>>` | 右移 |
| `-` | 一元减 | `<` | 小于 |
| `!` | 一元逻辑非 | `<=` | 小于等于 |
@@ -671,13 +708,13 @@ Verilog 仿真器用来模拟硬件的并行行为的方式类似于软件中的
| `~&` | 归约与非 | `==` | 逻辑相等 |
| `^` | 归约异或 | `!=` | 逻辑不等 |
| `^~` 或 `~^`| 归约异或非 | `===` | 全等 |
| <code>&#124;</code> | 归约或 | `!==` | 非全等 |
| <code>~&#124;</code> | 归约或非 | `&` | 按位与 |
| `\|` | 归约或 | `!==` | 非全等 |
| `~\|` | 归约或非 | `&` | 按位与 |
| `*` | 乘 | `^` | 按位异或 |
| `/` | 除以 | `^~` 或 `~^` | 按位异或非 |
| `%` | 取模 | <code>&#124;</code> | 按位或 |
| `%` | 取模 | `\|` | 按位或 |
| `+` | 二元加 | `&&` | 逻辑与 |
| `-` | 二元减 | <code>&#124;>&#124;</code> | 逻辑或 |
| `-` | 二元减 | `\|\|` | 逻辑或 |
| `<<` | 左移 | `? :` | 条件操作符 |
其中,一元操作表示仅有一个操作数,二元操作表示有两个操作数。归约操作也是只有一个操作数,它是该操作数中的所有位之间的计算。
@@ -685,7 +722,7 @@ Verilog 仿真器用来模拟硬件的并行行为的方式类似于软件中的
### 10.1.1. 算术操作符
| 操作符 | 表达式 | 说明
| :--: | :--: | :--: |
| :--: | :--: | :------: |
| `+` | `m + n` | 将 `n` 与 `m` 相加 |
| `-` | `m - n` | 将 `m` 减去 `n` |
| `-` | `-m` | 将 `m` 取反(二进制补码)|
@@ -696,7 +733,7 @@ Verilog 仿真器用来模拟硬件的并行行为的方式类似于软件中的
### 10.1.2. 按位操作符
| 操作符 | 表达式 | 说明
| :--: | :--: | :--: |
| :--: | :--: | :------: |
| `~` | `~m` | 将 `m` 的每个位取反 |
| `&` | `m & n` | 将 `m` 的每个位与 `n` 的相应位相与
| <code>&#124;</code> | <code>m &#124; n</code> | 将 `m` 的每个位与 `n` 的相应位相或
@@ -706,7 +743,7 @@ Verilog 仿真器用来模拟硬件的并行行为的方式类似于软件中的
### 10.1.3. 归约操作符
| 操作符 | 表达式 | 说明
| :--: | :--: | :--: |
| :--: | :--: | :------: |
| `&` | `&m` | 将 `m` 中的所有位相与(1位结果) |
| `~&` | `~&m` | 将 `m` 中的所有位相与非(1位结果) |
| <code>&#124;</code> | <code>&#124;m</code> | 将 `m` 中的所有位相或(1位结果) |
@@ -717,7 +754,7 @@ Verilog 仿真器用来模拟硬件的并行行为的方式类似于软件中的
### 10.1.4. 逻辑操作符
| 操作符 | 表达式 | 说明
| :--: | :--: | :--: |
| :--: | :--: | :------: |
| `!` | `!m` | `m` 是否不为真?(1 位 真/假结果) |
| `&&` | `m && n` | `m` 和 `n` 是否都为真?(1位 真/假结果) |
| <code>&#124;&#124;</code> | <code>m &#124;&#124; n</code> | `m` 或 `n` 是否为真?(1位 真/假结果) |
@@ -725,7 +762,7 @@ Verilog 仿真器用来模拟硬件的并行行为的方式类似于软件中的
### 10.1.5. 相等操作符
| 操作符 | 表达式 | 说明
| :--: | :--: | :--: |
| :--: | :--: | :------: |
| `==` | `m == n` | `m` 和 `n` 相等吗?(1 位 正确/错误结果) |
| `!=` | `m != n` | `m` 和 `n` 不等吗?(1 位 正确/错误结果) |
> 相等操作符仅比较逻辑 1 和 0。
@@ -733,14 +770,14 @@ Verilog 仿真器用来模拟硬件的并行行为的方式类似于软件中的
### 10.1.6. 全等操作符
| 操作符 | 表达式 | 说明
| :--: | :--: | :--: |
| :--: | :--: | :------: |
| `===` | `m === n` | `m` 和 `n` 全等吗?(1 位 正确/错误结果) |
| `!==` | `m !== n` | `m` 和 `n` 不全等吗?(1 位 正确/错误结果) |
### 10.1.7. 关系操作符
| 操作符 | 表达式 | 说明
| :--: | :--: | :--: |
| :--: | :--: | :------: |
| `<` | `m < n` | `m` 小于 `n` ?(1 位 正确/错误结果) |
| `>` | `m > n` | `m` 大于 `n` ?(1 位 正确/错误结果) |
| `<=` | `m <= n` | `m` 小于等于 `n` ?(1 位 正确/错误结果) |
@@ -749,20 +786,20 @@ Verilog 仿真器用来模拟硬件的并行行为的方式类似于软件中的
### 10.1.8. 逻辑移位操作符
| 操作符 | 表达式 | 说明
| :--: | :--: | :--: |
| :--: | :--: | :------: |
| `<<` | `m << n` | 将 `m` 左移 `n` 位 |
| `>>` | `m >> n` | 将 `m` 右移 `n` 位 |
### 10.1.9. 条件操作符
| 操作符 | 表达式 | 说明
| :--: | :--: | :--: |
| :--: | :--: | :------: |
| `? :` | `sel ? m : n` | 如果 `sel` 为真,选择 `m`,否则选择 `n` |
### 10.1.10. 连接复制操作符
| 操作符 | 表达式 | 说明
| :--: | :--: | :--: |
| :--: | :--: | :------: |
| `{ }` | `{m,n}` | 将 `m` 和 `n` 连接起来,产生更大的向量 |
| `{{ }}` | `{n{m}}` | 将 `m` 重复 `n` 次 |
@@ -778,9 +815,9 @@ Verilog 仿真器用来模拟硬件的并行行为的方式类似于软件中的
| `==`、`!=`、`===`、`!==` | $\vdots$
| `&`、`~&` | $\vdots$
| `^`、`~^`| $\vdots$
| <code>&#124;</code>、<code>~&#124;</code>| $\vdots$
| `\|`、`~\|` | $\vdots$
| `&&` | $\vdots$
| <code>&#124;&#124;</code>| $\vdots$
| `\|\|` | $\vdots$
| `? :` | 最低优先级
> 例如:`A + B & C + D` 就表示 `(A + B) & (C + D)`,而不是 `A + (B & C) + D`。
@@ -809,7 +846,7 @@ Verilog 仿真器用来模拟硬件的并行行为的方式类似于软件中的
- 存储器单元;
- 系统函数或用户自定义函数调用的返回值。
在选择操作数时,需要用户尤其注意的是操作数的极性。通常在 Verilog 中,无符号数用以下3种形式存在:
在选择操作数时,需要用户尤其注意的是**操作数的极性**。通常在 Verilog 中,无符号数用以下 3 种形式存在:
- 线网变量;
- 一般寄存器变量;
@@ -826,21 +863,25 @@ Verilog 仿真器用来模拟硬件的并行行为的方式类似于软件中的
这里定义两个变量,一个是无符号的 `reg` 型,另一个是有符号的整型:
```verilog
``` verilog
reg [4:0] Opreg; // 一个 5 位的 reg 型,存储无符号数
integer Opint; // 一个 32 位的 integer 型,存储有符号数
```
做如下运算:
```verilog
Opreg = -4d'12/4; // Opreg 被赋值 29,(-4d'12/4)的最低5位
``` verilog
Opreg = -4d'12/4; // Opreg 被赋值 29,(-4d'12/4) 的最低5位
Opint = -4d'12/4; // Opint 被赋值为 1073741821,共 32 位
Opreg = -12/4; // Opreg 被赋值29,(-12/4)的最低5位
Opreg = -12/4; // Opreg 被赋值29,(-12/4) 的最低5位
Opint = -12/4; // Opint 被赋值为 -3,采用 32 位的二进制补码表示方式
```
通过以上的计算结果可以看出,无符号数和有符号数的算术运算非常不同,用户在设计常量和变量并用它们计算的时候,一定要搞清楚它们中哪些表示有符号数,哪些表示无符号数,这很重要。
{% note warning %}
通过以上的计算结果可以看出,**无符号数和有符号数的算术运算非常不同**,用户在设计常量和变量并用它们计算的时候,一定要搞清楚它们中哪些表示有符号数,哪些表示无符号数,这很重要。
{% endnote %}
# 11. 系统任务和系统函数
@@ -852,7 +893,7 @@ Opint = -12/4; // Opint 被赋值为 -3,采用 32 位的二进制
- 拟控制任务(simulation control task);
- 时序验证任务(timing check task);
- PLA建模任务(PLA modeling task);
- 机建模任务(stochastic modeling task);
- 随机建模任务(stochastic modeling task);
- 实数变换函数(conversion functions for real);
- 概率分布数(probabilistic distribution function)。
@@ -862,17 +903,17 @@ Opint = -12/4; // Opint 被赋值为 -3,采用 32 位的二进制
`$display` 是显示任务,通常用来显示变量值、字符串,以及仿真时间等信息。
在 [JoshVerilog 模块](#JoshVerilog代码)中使用了这样的系统任务。
在 [HelloVlog 模块](#HelloVlogCode)中使用了这样的系统任务。
```verilog Josh.v
``` verilog HelloVlog.v
$display ("At time %t-", $time, "eq0 = 1"); // 显示时间
```
其中,双引号中的是字符串,`%t` 是时间格式。`$time` 是产生模拟时间的系统函数,它的返回值显示在字符串中的 `%t` 位置。
其中,双引号中的是字符串,`%t` 是时间格式。`$time` 是产生模拟时间的系统函数,它的返回值显示在字符串中的 `%t` 位置。
再如:
```verilog
``` verilog
$display("The value of ABC is %d", ABC); // 显示当前 ABC 变量的值
```
@@ -882,8 +923,8 @@ $display("The value of ABC is %d", ABC); // 显示当前 ABC 变量的值
系统函数 `$fopen` 用于打开一个文件,并返回一个整数的文件指针。然后,`$fdisplay` 就可以使用这个文件指针向文件中写入信息。写完后,则可以使用 `$fclose` 这个系统关闭这个文件。例如:
```verilog
integer Write_Out_File; // 定义一个文件指针
``` verilog
integer Write_Out_File; // 定义一个文件指针
Write_Out_File = $fopen("Write_Out_File.txt");
$fdisplay(Write_Out_File, "@ %h\n%h", Mpi_addr, Data_in);
$fclose(Write_Out_File);
@@ -893,16 +934,16 @@ $fclose(Write_Out_File);
用户可以通过 `$readmemb` 或者 `$readmemh` 来从文件中读入数据,但是这个文件中的数据格式是一定的。例如:
```verilog
``` verilog
reg [7:0] DataSource [0:47];
$readmemh ("Read_In_File.txt, DataSource);
$readmemh ("Read_In_File.txt", DataSource);
```
就是将 `Read_In_FiIe` 文件中数据读入到 `DataSource` 数组中,然后就可以直接使用。
`Read_In_File` 数据文件的格式,可以参考如下写法:
```verilog
``` verilog
@2f
24
@2e
@@ -935,6 +976,12 @@ $readmemh ("Read_In_File.txt, DataSource);
除了系统任务和系统函数之外,VeriIog还允许用户自己定义任务和函数。关于自定义的任务和函数的特点及使用方法,请参考后续的相关内容。
---
# 参考文献
在本篇笔记中,以一个实例为出发点,系统介绍了 Verilog 的基本语法,相信能为大家深入学习 Verilog 语言打下坚实的基础。
EDA 先锋工作室. *轻松成为设计高手——Verilog HDL 实用精解.* 北京航空航天大学出版社, 2012.
[Part 1——初识 HDL 设计方法]: https://josh-gao.top/posts/460433dd.html
[《Part 3——描述方式和设计层次》]: https://josh-gao.top/posts/fd117896.html
[IEEE Std. 1364-2005]: https://standards.ieee.org/standard/1364-2005.html
[《Part 7—— 逻辑验证与 testbench 编写》]: https://josh-gao.top/posts/e060f513.html
[《Part 3——描述方式和设计层次》的 4.2 小节]: https://josh-gao.top/posts/fd117896.html#%E5%8F%82%E6%95%B0
File diff suppressed because it is too large Load Diff