From cd92265be10789853c1ef344017b6f4d5905cec8 Mon Sep 17 00:00:00 2001 From: Josh Gao Date: Mon, 17 Apr 2023 00:56:17 +0800 Subject: [PATCH] review josh-verilog 1-3, correct some mistakes, update some layout --- .../_posts/2020-11-27-josh-verilog-part-1.md | 45 +- .../_posts/2020-11-28-josh-verilog-part-2.md | 345 ++++++----- .../_posts/2020-11-30-josh-verilog-part-3.md | 586 +++++++++--------- 3 files changed, 517 insertions(+), 459 deletions(-) diff --git a/source/_posts/2020-11-27-josh-verilog-part-1.md b/source/_posts/2020-11-27-josh-verilog-part-1.md index fbf035c..c7bbd23 100644 --- a/source/_posts/2020-11-27-josh-verilog-part-1.md +++ b/source/_posts/2020-11-27-josh-verilog-part-1.md @@ -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. 设计方法的变迁 +# 1. 设计方法的变迁 随着微电子设计技术与工艺的发展,数字集成电路从电子管、晶体管、中小规模集成电路、超大规模集成电路(Very Large Scale Integrated Circuit, VLSIC)逐步发展到今天的专用集成电路(Application Specific Integrated Circuit, ASIC),愈发数字化的产品如计算机、手机、数字电视等都运用了复杂的专用数字集成电路。数字逻辑器件从简单的逻辑门到复杂的 SoC,种类繁多,提供了对复杂系统的灵活支撑。 -随着数字电路系统的不断发展,系统的逻辑复杂度与规模日益增加,数字系统的设计方法也随之不断演进。早期简单的门逻辑设计阶段,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 语言的特点 +# 2. Verilog 语言的特点 ## 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 的设计与验证流程 +# 3. HDL 的设计与验证流程 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。 diff --git a/source/_posts/2020-11-28-josh-verilog-part-2.md b/source/_posts/2020-11-28-josh-verilog-part-2.md index 00d9ac6..c3fd055 100644 --- a/source/_posts/2020-11-28-josh-verilog-part-2.md +++ b/source/_posts/2020-11-28-josh-verilog-part-2.md @@ -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 到门级电路的转换。 -最近几年,随着 IP核 市场的逐渐兴起,许多设计者逐渐意识到**利用现有的 IP核 可以帮助节约设计成本、减少设计周期**,有许多设计工程师,甚至希望所有的设计模块都使用现成的模块,自己仅仅开发一些简单的粘合逻辑。这就有点像电路板设计的过程,工程师将各种芯片集成到一块电路板上,自己完成这些芯片间的互连和一些简单的 CPLD 逻辑设计,以及对微处理器的编程。使用现有的 IP 来搭建系统实际上也是一种自底向上(Bottom-Up)的设计方法。 +最近几年,随着 IP核 市场的逐渐兴起,许多设计者逐渐意识到**利用现有的 IP核可以帮助节约设计成本、减少设计周期**,有许多设计工程师,甚至希望所有的设计模块都使用现成的模块,自己仅仅开发一些简单的粘合逻辑。这就有点像电路板设计的过程,工程师将各种芯片集成到一块电路板上,自己完成这些芯片间的互连和一些简单的 CPLD 逻辑设计,以及对微处理器的编程。使用现有的 IP 来搭建系统实际上也是一种**自底向上**(Bottom-Up)的设计方法。 从以上描述可以看出,设计硬件电路时,这两种设计方法都有可能采用。Verilog HDL 可以完全支持这两种设计方法。 @@ -50,26 +49,28 @@ EDA 行业的先行者们发明了 Verilog 硬件描述语言,其最根本的 ## 2.1. Verilog 实例 -首先看一个简单的如下图所示的电路(模块):JoshVerilog。它可以是一个独立的设计,也可以是更大的系统的一个组成部分。 + -![ ](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) 的电路功能。源代码如下。 - + -```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. 变量类型 在 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 ``` 这两者描述的目的一样,都是一个异或门,如下图所示。 -
- -
+ + +![异或门](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 触发器的模型。 -
- -
+![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 仿真器用来模拟硬件的并行行为的方式类似于软件中的 | `~&` | 归约与非 | `==` | 逻辑相等 | | `^` | 归约异或 | `!=` | 逻辑不等 | | `^~` 或 `~^`| 归约异或非 | `===` | 全等 | -| | | 归约或 | `!==` | 非全等 | -| ~| | 归约或非 | `&` | 按位与 | +| `\|` | 归约或 | `!==` | 非全等 | +| `~\|` | 归约或非 | `&` | 按位与 | | `*` | 乘 | `^` | 按位异或 | | `/` | 除以 | `^~` 或 `~^` | 按位异或非 | -| `%` | 取模 | | | 按位或 | +| `%` | 取模 | `\|` | 按位或 | | `+` | 二元加 | `&&` | 逻辑与 | -| `-` | 二元减 | |>| | 逻辑或 | +| `-` | 二元减 | `\|\|` | 逻辑或 | | `<<` | 左移 | `? :` | 条件操作符 | 其中,一元操作表示仅有一个操作数,二元操作表示有两个操作数。归约操作也是只有一个操作数,它是该操作数中的所有位之间的计算。 @@ -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` 的相应位相与 | | | m | n | 将 `m` 的每个位与 `n` 的相应位相或 @@ -706,7 +743,7 @@ Verilog 仿真器用来模拟硬件的并行行为的方式类似于软件中的 ### 10.1.3. 归约操作符 | 操作符 | 表达式 | 说明 -| :--: | :--: | :--: | +| :--: | :--: | :------: | | `&` | `&m` | 将 `m` 中的所有位相与(1位结果) | | `~&` | `~&m` | 将 `m` 中的所有位相与非(1位结果) | | | | |m | 将 `m` 中的所有位相或(1位结果) | @@ -717,7 +754,7 @@ Verilog 仿真器用来模拟硬件的并行行为的方式类似于软件中的 ### 10.1.4. 逻辑操作符 | 操作符 | 表达式 | 说明 -| :--: | :--: | :--: | +| :--: | :--: | :------: | | `!` | `!m` | `m` 是否不为真?(1 位 真/假结果) | | `&&` | `m && n` | `m` 和 `n` 是否都为真?(1位 真/假结果) | | || | m || n | `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$ -| |、~|| $\vdots$ +| `\|`、`~\|` | $\vdots$ | `&&` | $\vdots$ -| ||| $\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 diff --git a/source/_posts/2020-11-30-josh-verilog-part-3.md b/source/_posts/2020-11-30-josh-verilog-part-3.md index c8a65f8..f51f2e6 100644 --- a/source/_posts/2020-11-30-josh-verilog-part-3.md +++ b/source/_posts/2020-11-30-josh-verilog-part-3.md @@ -20,13 +20,12 @@ toc: expand_all: true max_depth: 4 abbrlink: fd117896 +description: Verilog 语言可以用多种方式来描述硬件,同时,使用这些描述方式又可以在多个抽象层次上设计硬件。这是 Verilog 语言的重要特征。本文主要侧重于阐述 Verilog 语言的描述方法和设计层次,重点介绍了 Verilog 的 3 种描述方式以及 Verilog 可以应用的设计层次,体现出 Verilog 语言是一种非常灵活、强大的硬件描述语言。 --- -Verilog 语言可以用多种方式来描述硬件,同时,使用这些描述方式又可以在多个抽象层次上设计硬件。这是 Verilog 语言的重要特征。本文主要侧重于阐述 Verilog 语言的描述方法和设计层次。 - # 1. 描述方式 -在[《Part 2 —— Verilog 语言基础》](https://josh-gao.top/posts/fd2ca242.html#3-%E7%A7%8D%E6%8F%8F%E8%BF%B0%E6%96%B9%E6%B3%95)中己经介绍过,Verilog 语言有 3 种最基本的描述方式. +在[《Part 2——Verilog 语言基础》的第 3 小节][]中己经介绍过,Verilog 语言有 3 种最基本的描述方式. - 数据流描述,采用 `assign` 连续赋值语句; - 行为描述,使用 `always` 语句或 `initial` 语句块中的过程赋值语句; @@ -48,13 +47,11 @@ Verilog 语言可以用多种方式来描述硬件,同时,使用这些描述 ## 2.2. 连续赋值语句 -
- -
+![一个电路模型](https://josh-blog-1257563604.cos.ap-beijing.myqcloud.com/img/2020-11-30-josh-verilog-part-3/2020-11-30-josh-verilog-part-3-010-XORGate.png!sign){width=800px} 上图中的电路模型可以用如下的语句来描述: -```verilog +``` verilog assign #1 A_xor_wire = eq0 ^ eq1; ``` @@ -64,7 +61,7 @@ assign #1 A_xor_wire = eq0 ^ eq1; 1. **连续驱动** - 在 [Part 2 的 7.2 驱动和赋值](https://josh-gao.top/posts/fd2ca242.html#%E5%AF%84%E5%AD%98%E5%99%A8%E7%B1%BB%E5%9E%8B)部分,已经解释了驱动和赋值之间的微妙区别。 + 在[《Part 2——Verilog 语言基础》的 7.4 驱动和赋值][]部分,已经解释了驱动和赋值之间的微妙区别。 连续赋值语句是连续驱动的。也就是说,任何时刻输入的任何变化都将导致该语句的重新计算。 @@ -72,7 +69,7 @@ assign #1 A_xor_wire = eq0 ^ eq1; 由于仿真器中不会存储连续赋值语句中被赋值的变量值,因此该变量是线网类型(Net),不能是寄存器类型。需要记住,只有线网类型的变量才可以在 `assign` 语句中被赋值。 - 另外,线网类型的变量可以被多重驱动,也就是说,可以在多个连续赋值语句中驱动同一个线网。在[2.4 驱动源线网](#drive)中将进一步介绍。但是,寄存器变量就不同了,它不能被不同的行为进程(例如 `always` 语句块)驱动。 + 另外,线网类型的变量可以被多重驱动,也就是说,可以在多个连续赋值语句中驱动同一个线网。在 [2.4 驱动源线网](#drive)中将进一步介绍。但是,寄存器变量就不同了,它不能被不同的行为进程(例如 `always` 语句块)驱动。 3. **使用 `assign` 对组合逻辑建模** @@ -84,15 +81,13 @@ assign #1 A_xor_wire = eq0 ^ eq1; ### 2.2.2. 实例 -
- -
+![两个半加器和一个或门组成全加器](https://josh-blog-1257563604.cos.ap-beijing.myqcloud.com/img/2020-11-30-josh-verilog-part-3/2020-11-30-josh-verilog-part-3-020-FullAdder.png!sign){width=800px} -在上中,用两个半加器和一个或门组成一个全加器,这里将使用连续赋值语句描述这个电路。代码如下。 +在上图中,用两个半加器和一个或门组成一个全加器,这里将使用连续赋值语句描述这个电路。代码如下。 - + -```verilog HalfAdd.v +``` verilog HalfAdd.v module HalfAdd(X, Y, sum, C_out); // IO端口声明 @@ -107,7 +102,7 @@ assign C_out = X & Y; endmodule ``` -```verilog FullAdd.v +``` verilog FullAdd.v module FullAdd(X, Y, C_in, sum, C_out); // IO端口声明 @@ -147,7 +142,7 @@ endmodule 在连续赋值语句中,可以对电路的延时进行建模。当然,也可以没有延时。比如: -```verilog +``` verilog assign #1 A_xor_wire = eq0 ^ eq1; // `timescale 1ns/1ns ``` @@ -162,7 +157,7 @@ assign #1 A_xor_wire = eq0 ^ eq1; // `timescale 1ns/1ns 用户可以分别描述这几种延时,比如: -```verilog +``` verilog assign #(1,2) A_xor_wire = eq0 ^ eq1; assign #(1,2,3) A_xor wire = eq0 ^ eq1; ``` @@ -173,7 +168,7 @@ assign #(1,2,3) A_xor wire = eq0 ^ eq1; 在一些电路模型中,延时分为最大、典型和最小3种情况。连续赋值语句中的延时也可以采用 `min:typ:max` 的格式来表示。例如: -```verilog +``` verilog assign #(4:5:6, 3:4:5) A_xor_wire = eq0 ^ eq1; ``` @@ -183,13 +178,13 @@ assign #(4:5:6, 3:4:5) A_xor_wire = eq0 ^ eq1; 另外,`assign` 语句中的延时特性通常是被逻辑综合工具忽略的。因为综合工具要将 Verilog 语言模型综合成逻辑电路,而逻辑电路的延时是由基本的单元库和走线延时决定的。用户无法对逻辑单元指定延时,但是,用户可以在综合和实现工具中加时序约束,让工具尽量满足设计的时序要求。 -## 2.4. 驱动源线网 +## 2.4. 驱动源线网 下面分为几种线网类型来描述当线网具有多重驱动源时的情况。 ### 2.4.1. 多重驱动 `wire`(错误) -```verilog WS.v +``` verilog WS.v module WS (A, B, C, D, WireShort); input A, B, C, D; @@ -209,7 +204,7 @@ endmodule 可以使用 `wor` 线网类型来将不同的输出“线或”在一起,如下: -```verilog // WO.v +``` verilog // WO.v module WO (A, B, C, D, WireOr); input A, B, C, D; @@ -225,13 +220,11 @@ endmodule 逻辑综合以后,它具体对应的逻辑电路如下图所示。 -
- -
+![线或的功能](https://josh-blog-1257563604.cos.ap-beijing.myqcloud.com/img/2020-11-30-josh-verilog-part-3/2020-11-30-josh-verilog-part-3-030-WireOr.png!sign){width=700px} 同样,可以使用 `wand` 线网类型来将不同的输出“线与”在一起,如下: -```verilog WA.v +``` verilog WA.v module WA (A, B, C, D, WireAnd); input A, B, C, D; @@ -247,15 +240,13 @@ endmodule 它对应的逻辑电路如下图所示。 -
- -
+![线与的功能](https://josh-blog-1257563604.cos.ap-beijing.myqcloud.com/img/2020-11-30-josh-verilog-part-3/2020-11-30-josh-verilog-part-3-040-WireAnd.png!sign){width=700px} ### 2.4.3. 三态总线功能 如果要实现多个三态总线相连,可以采用 `tri` 型线网: -```verilog WT.v +``` verilog WT.v module WT (A, B, C, D, WireTri, en1_n, en2_n); input A, B, C, D, en1_n, en2_n; @@ -271,25 +262,23 @@ endmodule 其实现的电路如下图所示。 -
- -
+![三态驱动总线电路](https://josh-blog-1257563604.cos.ap-beijing.myqcloud.com/img/2020-11-30-josh-verilog-part-3/2020-11-30-josh-verilog-part-3-050-WireTri.png!sign){width=700px} -# 3. 行为描述 +# 3. 行为描述 -在 [Part 2](https://josh-gao.top/posts/fd2ca242.html#3-%E7%A7%8D%E6%8F%8F%E8%BF%B0%E6%96%B9%E6%B3%95) 中介绍过行为描述的概念。所谓行为描述,是指用语言描述电路的行为。行为描述的语句有两种:`initial` 和 `always` 语句。 +在 [《Part 2——Verilog 语言基础》的第 2.2 小节][]中介绍过行为描述的概念。所谓行为描述,是指用语言描述电路的行为。行为描述的语句有两种:`initial` 和 `always` 语句。 ## 3.1. 行为描述的语句格式 -`initial` 和 `always` 的后面一般跟语句或语句组(statement group)。语句可以是:非阻塞过程赋值、阻塞过程赋值、连续过程赋值或高级编程语句。在以后会一一介绍。 +`initial` 和 `always` 的后面一般跟语句或语句组(statement group)。语句可以是:非阻塞过程赋值、阻塞过程赋值、连续过程赋值或高级编程语句。下面开始介绍。 -### 3.1.1. `initial` 或 `always` 过程块(procedual block) +### 3.1.1. `initial` 或 `always` 过程块(procedural block) `initial` 语句在 0 仿真时间执行,而且只执行一次;`always` 语句同样在 0 仿真时间开始执行,但是它将一直循环执行。这样的特点单单从它们的命名上就能看得出来:一个是 `initial`,就是初始化一次的意思;另一个是 `always`,就是总在运行的意思。 下面利用 `initial` 和 `always` 语句各自的特点,产生一个时钟发生器的模型。 -```verilog ClkGen.v +``` verilog ClkGen.v `timescale 1ns/1ns module ClkGen (clk); @@ -306,11 +295,15 @@ always // 每 5ns 将 clk 翻转一次 endmodule ``` -在 0 时刻,`initial` 和 `always` 语句同时执行,顺序随机。假设先运行 `initial` 语句,那么 `clk` 变量被赋值为 0,这时 `initial` 语句进程将永远被挂起,再也不会执行。 +在 0 时刻,`initial` 和 `always` 语句同时执行,**顺序随机**。假设先运行 `initial` 语句,那么 `clk` 变量被赋值为 0,这时 `initial` 语句进程将永远被挂起,再也不会执行。 然后,开始运行 `always` 语句。该 `always` 语句每隔 `5ns` 将 `clk` 信号翻转一次,一直不停地运行,这样就产生了一个周期是 `10ns` 的时钟信号。 -> 在以上代码中,0 时刻,`initial` 和 `always` 语句同时执行,顺序随机,这样就存在一个冲突的问题。假设先执行的是 `always` 语句, `clk` 就不会被初始化为 0。 +{% note warning %} + +在以上代码中,0 时刻,`initial` 和 `always` 语句同时执行,顺序随机,这样就存在一个冲突的问题。假设先执行的是 `always` 语句, `clk` 就不会被初始化为 0。 + +{% endnote %} ### 3.1.2. 过程块中的语句种类 @@ -320,14 +313,14 @@ endmodule 下面是 `always` 过程块中直接跟阻塞赋值语句: -```verilog +``` verilog always // 每 5ns 将 clk 翻转一次 #5 clk = ~clk; ``` 下面的代码描述了语句组和高级编程语句的互相嵌套: -```verilog +``` verilog always @(posedge clk or negedge rst_n) begin // 语句组 if (~rst_n) // 高级编程语句 @@ -357,13 +350,11 @@ end #### 3.1.3.1. 事件语句(`@`)的用法 -
- -
+![D 触发器模型](https://josh-blog-1257563604.cos.ap-beijing.myqcloud.com/img/2020-11-30-josh-verilog-part-3/2020-11-30-josh-verilog-part-3-060-DFlipFlop.png!sign){width=600px} 要实现上图中这样一个 D 触发器,通常用以下代码: -```verilog TYP_DFF.v +``` verilog TYP_DFF.v module TYP_DFF (clk, D, Q); input clk, D; @@ -378,15 +369,15 @@ end endmodule ``` -在 0 仿真时刻,`always` 语句块开始执行。当遇到 `@ (posedge clk)` 语句时,该进程被挂起。等待 `clk` 的上升沿到来,才重新激活该进程。当 `clk` 的上升沿出现后,将 `D` 的值赋给 `Q`,`always` 语句块执行完成。 +在 0 仿真时刻,`always` 语句块开始执行。当遇到 `@(posedge clk)` 语句时,该进程被挂起。等待 `clk` 的上升沿到来,才重新激活该进程。当 `clk` 的上升沿出现后,将 `D` 的值赋给 `Q`,`always` 语句块执行完成。 -由于 `always` 语句的特点,`always` 语句马上开始重新执行,当遇到 `@ (posedge clk)` 语句时,进程再一次挂起,等待 `clk` 的上升沿到来,才继续往下执行。 +由于 `always` 语句的特点,`always` 语句马上开始重新执行,当遇到 `@(posedge clk)` 语句时,进程再一次挂起,等待 `clk` 的上升沿到来,才继续往下执行。 这样,在 `always` 语句中使用 `@` 事件语句很好地模拟了触发器的行为。综合工具会马上将上述代码映射成上图中的 D 触发器。 同样的道理,采用如下的代码也可以得到一样的 D 触发器功能: -```verilog TYP_DFF.v +``` verilog TYP_DFF.v module TYP_DFF (clk, D, Q); input clk, D; @@ -404,7 +395,7 @@ endmodule 当有多个条件语句时,一般将它们用 `or` 分隔开。例如,要实现一个带异步复位端的 D 触发器可以采用如下代码: -```verilog TYP_DFF.v +``` verilog TYP_DFF.v module TYP_DFF (clk, D, Q, rst); input clk, D, rst; @@ -426,7 +417,7 @@ endmodule #### 3.1.3.2. 延时语句(`#`)的用法 -```verilog +``` verilog always // 每 5ns 将 clk 翻转一次 #5 clk = ~clk; ``` @@ -437,7 +428,7 @@ always // 每 5ns 将 clk 翻转一次 以下代码利用延时语句产生一个复位信号: -```verilog +``` verilog initial begin rst_n =1; #5 rst_n = 0; @@ -451,7 +442,7 @@ end 下面分析一下等待语句如何做时序控制: -```verilog +``` verilog module MY_LATCH (Strobe, D, Q); input Strobe, D; @@ -479,7 +470,7 @@ endmodule 阻塞赋值的语法如下: -```verilog +``` verilog 寄存器变量 = 表达式; ``` @@ -487,16 +478,16 @@ endmodule 1. 右边表达式的计算和对左边寄存器变量的赋值是一个统一的原子操作中的两个动作。**这两个动作之间不能插入任何其他的动作**。 -2. 如果多个阻塞赋值语句顺序出现在 `begin. .. end` 语句中,**前面的语句在执行时,将完全阻塞后面的语句,直到前面语句的赋值完成以后,才会执行下一句的右边表达式计算**。例如 `begin m = n; n = m; end` 语句中,当 `m` 被完全赋值以后,再开始执行 `n=m`,将 `m` 的新值赋给 `n`。这样执行的结果就是的初始值不变,而且与 `n` 相等。 +2. 如果多个阻塞赋值语句顺序出现在 `begin ... end` 语句中,**前面的语句在执行时,将完全阻塞后面的语句,直到前面语句的赋值完成以后,才会执行下一句的右边表达式计算**。例如 `begin m = n; n = m; end` 语句中,当 `m` 被完全赋值以后,再开始执行 `n=m`,将 `m` 的新值赋给 `n`。这样执行的结果就是的初始值不变,而且与 `n` 相等。 由于阻塞賦值的这一特点,通常会建议在对组合逻辑建模的时候,采用阻塞賦值,先看一段代码: -
+ -```verilog +``` verilog wire A_in, B_in, C_in; reg Temp, D_out; -... +// ... always @(A_in or B_in or C_in) begin Temp = A_in & B_in; // 阻塞赋值 D_out = Temp | C_in; // 阻塞赋值 @@ -505,17 +496,15 @@ end 设计者的根本目的是得到如下图所示的电路。 -
- -
+![一个组合逻辑](https://josh-blog-1257563604.cos.ap-beijing.myqcloud.com/img/2020-11-30-josh-verilog-part-3/2020-11-30-josh-verilog-part-3-070-ACombinationalLogic.png!sign){width=700px} 在以上代码仿真时,正是利用了阻塞赋值的这一特点才模拟了该组合逻辑的行为。 首先,任何一个输入发生变化,`D_out` 必然发生变化。因此,在 `always` 的敏感列表中,包括 `A_in`,`B_in` 和 `C_in`。在内部计算时,首先将 `A_in` 和 `B_in` 相与,得到一个中间结果 `Temp`。等 `Temp` 被完全赋值后,才开始执行下一个语句:将 `Temp` 的新值与 `C_in` 相或,得到 `D_out` 的值。我们也许可以感受到,这个 `always` 语句非常精确地模拟了上图中电路的行为。 -另外,有一点会使得初学者产生较大的疑问。为什么 Verilog 规定只有寄存器(register)类型的变量才能够在过程赋值语句中被赋值呢?有时候在Verilog中定义的寄存器变量,在综合时并不一定映射成一个实在的触发器硬件。比如在以上的例子中,`Temp` 和 `D_out` 被定义成 `reg` 变量,而综合结果它们却还是组合逻辑,并不是存储单元。 +另外,有一点会使得初学者产生较大的疑问。为什么 Verilog 规定只有寄存器(`register`)类型的变量才能够在过程赋值语句中被赋值呢?有时候在 Verilog 中定义的寄存器变量,在综合时并不一定映射成一个实在的触发器硬件。比如在以上的例子中,`Temp` 和 `D_out` 被定义成 `reg` 变量,而综合结果它们却还是组合逻辑,并不是存储单元。 -在 Verilog 语言中,寄存器变量的特点是需要在仿真运行器件上保持它的值,也就是说,这个变量在仿真时需要占据内存空间。 +在 Verilog 语言中,寄存器变量的特点是**需要在仿真运行器件上保持它的值**,也就是说,这个变量在仿真时需要占据内存空间。 在以上的 `always` 实例中,`always` 语句块只对 `A_in` 等 3 个输入变化敏感。如果没有这 3 个变量的变化事件,`Temp` 和 `D_out` 变量将需要保存其值,因此它们必须被定义为寄存器类型变量。但是,它们在综合之后,并不对应硬件锁存器或者触发器。 @@ -523,19 +512,19 @@ end 非阻寒赋值的语法如下: -```verilog +``` verilog 寄存器变量 <= 表达式; ``` 它的特点是在执行该语句时,首先计算右边的表达式,然后并不立刻对左边的变量赋值。由于这个赋值操作在当前仿真时间事件队列中的优先级比较低,因此**将赋值推迟到当前仿真时刻的后期运行**。关于具体非阻塞赋值的右式计算和左式更新是何时完成的,请参考后续内容。 -[comment]:<> (第8章中“分层事件队列”部分内容。) + 与阻塞赋值不同的是,如果多个非阻塞赋值语句顺序出现在 `begin ... end` 语句中,前面语句的执行,并不会阻塞后面语句的执行。前面语句的计算完成,还没有赋值时,就会执行下一句的右边表达式计算。例如 `begin m <= n; n <= m; end` 语句中,最后的结果是将m与n值互换了。 -如果想要采用如下代码来描述[前述组合逻辑电路](#一个组合逻辑)的功能: +如果想要采用如下代码来描述[前述组合逻辑电路](#CombinationalLogicDemo)的功能: -```verilog +``` verilog wire A_in, B_in, C_in; reg Temp, D_out; ... @@ -548,7 +537,7 @@ end 如果使用仿真工具去仿真这段代码,就会发现它的功能并不是所想要的功能。比如当 `A_in` 发生变化,执行 `always` 语句,其中 `Temp <= A_in & B_in;` 这句话并没有立刻对 `Temp` 赋值,而是放在当前仿真时刻的后期才开始执行 `Temp` 的更新。这样,当执行 `D_out <=Temp | C_in;` 表达式的右式计算事件时,`Temp` 的值还是 旧的值,因此这时 `D_out` 并不会发生变化。 -大家也许可以尝试将如上的代码用综合工具综合一下,可能同样会得到[前述组合逻辑电路](#一个组合逻辑)的情况。这是由于一些综合工具可以容忍用户的这些代码缺陷。这就造成了 RTL 仿真和综合的结果不一致的现象。 +大家也许可以尝试将如上的代码用综合工具综合一下,可能同样会得到[前述组合逻辑电路](#CombinationalLogicDemo)的情况。这是由于一些综合工具可以容忍用户的这些代码缺陷。这就造成了 RTL 仿真和综合的结果不一致的现象。 为什么会造成这个现象呢?因为 RTL 的仿真器严格按照 Verilog 的仿真语义执行 RTL 的仿真过程,而综合工具通常只是根据用户的代码推断设计者的意图,然后生成相应的电路结构。因此,综合的过程有一定的主观推断性,并不严格遵守 Verilog 的语义,不同的综合工具判决标准也不一样。 @@ -556,11 +545,15 @@ end 所以保证仿真器和综合器都能正确理解设计的代码,是非常关键的。 +{% note info %} + **通常利用非阻塞赋值的特点来对时序逻辑进行建模。** +{% endnote %} + 代码如下: -```verilog +``` verilog always @(posedge clk) begin q1 <= d; q2 <= q1; @@ -570,9 +563,7 @@ end 其实现结果如下图: -
- -
+![三级流水的寄存器](https://josh-blog-1257563604.cos.ap-beijing.myqcloud.com/img/2020-11-30-josh-verilog-part-3/2020-11-30-josh-verilog-part-3-080-ThreeFlowStateRegister.png!sign){width=600px} 正是利用了语句之间的非阻塞性,才实现了这个 3 级流水线的逻辑功能。 @@ -587,7 +578,7 @@ end 例如,在下面的代码实例中用 `assign` 和 `deassign` 描述了一个带异步清零端的 D 触发器。 -```verilog DEF.V +``` verilog DEF.V module DEF(D, clr, clk, Q); input D, clr, clk; @@ -610,7 +601,7 @@ endmodule 这里不再对过程连续赋值进行过多阐述,有兴趣可以参考其他文献。 -[comment]:<> (在第8章中的8.6“再谈阻塞与非阻塞赋值”一节中,通过一个实例,将再次深入阐述非阻塞和阻塞赋值语句的本质。) + ## 3.3. 语句组 @@ -622,7 +613,7 @@ endmodule 在顺序语句组中,其中的语句是一条一条顺序执行的。比如下面的语句: -```verilog +``` verilog always @(A_in or B_in or C_in) begin Temp = A_in & B_in; // 阻塞赋值 D_out = Temp | C_in; // 阻塞赋值 @@ -635,7 +626,7 @@ end 再比如要产生一个值序列: -```verilog +``` verilog initial begin DataBin = 0; @@ -647,15 +638,13 @@ end 由于语句是顺序执行的,产生的波形如下图所示。 -
- -
+![顺序语句块的执行波形](https://josh-blog-1257563604.cos.ap-beijing.myqcloud.com/img/2020-11-30-josh-verilog-part-3/2020-11-30-josh-verilog-part-3-090-SequentialWave.png!sign){width=500px} ### 3.3.2. 并行语句组 `fork ... join` 在`fork ... join` 语句组中,语句是并行执行的。将上一小节的代码改写如下: -```verilog +``` verilog initial fork DataBin = 0; @@ -667,24 +656,22 @@ join 由于其中的所有语句并行执行,也就是以上 4 条语句都是从 0 时刻开始同时执行的,产生的波形如下图。 -
- -
+![并行语句块的执行波形](https://josh-blog-1257563604.cos.ap-beijing.myqcloud.com/img/2020-11-30-josh-verilog-part-3/2020-11-30-josh-verilog-part-3-100-ParallelWave.png!sign){width=500px} ### 3.3.3. 语句组的标识符 语句组可以有标识符,也可以没有。 -当一个语句组有标识符时,在语句组内部可以定义局部变量,而不会传递到语句组的外部。然而,在仿真语义上,这个变量是静态变量,它的值在整个仿真运行周期中是不变的,但是不会与其他语句组中同一个名称的变量发生冲突。例如: +**当一个语句组有标识符时,在语句组内部可以定义局部变量,而不会传递到语句组的外部**。然而,在仿真语义上,这个变量是静态变量,它的值在整个仿真运行周期中是不变的,但是不会与其他语句组中同一个名称的变量发生冲突。例如: -```verilog +``` verilog integer i; // always 语句以外的 i 变量 always @(...) begin: SORT - ... - integer i; // 语句组内部的土变量 + // ... + integer i; // 语句组内部的 i 变量 for (i = 0;i <= 7; i = i + 1) - ... + // ... end ``` @@ -714,7 +701,7 @@ Verilog 作为硬件描述语言,最重要的特性就是其设计层次比较 现考虑以下代码: -```verilog +``` verilog always @(sel_a or sel_b or a or b or c) begin if (sel_a) q = a; @@ -727,17 +714,19 @@ end 它要实现的逻辑如下图。 -
- -
+![`if ... else` 语句](https://josh-blog-1257563604.cos.ap-beijing.myqcloud.com/img/2020-11-30-josh-verilog-part-3/2020-11-30-josh-verilog-part-3-110-IfElseStatement.png!sign){width=600px} 在 `if ... else` 语句中,条件是从上到下逐条检查的。因此,当满足一个条件时,就执行其后的语句,跳过 `else` 后面的语句、当所有条件都不满足,便执行最后一条 `else` 后面的语句。因此 `if ... else` 语句实际上是有优先级顺序的。 -实际在上例中使用了 `if ... else` 优先级编码的特点,`sel_a` 的判断优先级最高,因此在逻辑中的级数要明显少一些,参考上图。如果 `sel_a` 为关键路径的话,就可以利用这样的优先级编码提高设计的性能。 +{% note info %} + +实际在上例中使用了 `if ... else` 优先级编码的特点,`sel_a` 的判断优先级最高,因此在逻辑中的级数要明显少一些,参考上图。**如果 `sel_a` 为关键路径的话,就可以利用这样的优先级编码提高设计的性能**。 + +{% endnote %} 在使用 `if ... else` 语句时,**尤其是用在组合逻辑中,需要注意不要引入 Latch 电路**。先来看如下的代码: -```verilog +``` verilog always @(sel or a or b or c) begin if (sel == 2'b00) q = a; @@ -750,15 +739,19 @@ end 由于,最后一个条件 `sel == 2'b11` 的语句没有被显式地写出,言下之意是,当 `sel` 为 `2'b11` 时,`q` 值需要保持不变。这个代码在综合时自然就会产生锁存器,如下图。 -
- -
+ + +![产生锁存器](https://josh-blog-1257563604.cos.ap-beijing.myqcloud.com/img/2020-11-30-josh-verilog-part-3/2020-11-30-josh-verilog-part-3-120-CombinationalLogicwithLatch.png!sign){width=800px} + +{% note warning %} **锁存器在数字同步逻辑设计中应该尽量避免。因为锁存器容易引起竞争冒险,同时静态时序分析工具也不好分析穿过锁存器的路径。** +{% endnote %} + 在下面的代码中,己经明确写出:当 `se` 为 `2'b11` 时,`q` 值不关心,赋值为 `x`。 -```verilog +``` verilog always @(sel or a or b or c) begin if (sel == 2'b00) q = a; @@ -773,15 +766,13 @@ end 既然不关心 `sel` 为 `2'b11` 时 `q` 的值,那么有的综合工具就顺手将 `sel` 等于 `2'b11` 时 `q` 的值也赋值为 `c`,这样就避免了锁存器的产生。实现电路如下图。 -
- -
+ - +![无锁存器的组合逻辑电路](https://josh-blog-1257563604.cos.ap-beijing.myqcloud.com/img/2020-11-30-josh-verilog-part-3/2020-11-30-josh-verilog-part-3-130-CombinationalLogicwithoutLatch.png!sign){width=800px} 然而,在描述时序逻辑时,通常将利用 `if` 语句的隐式条件对带时钟使能的 D 触发器建模。代码如下: -```verilog +``` verilog always @(posedge clk or negedge rst_n) begin if (~rst_n) sum <= 0; @@ -790,11 +781,9 @@ always @(posedge clk or negedge rst_n) begin end ``` -以上语句表示在时钟正沿来临时,如果 `en` 为 1,则将 `a + b` 的值付给 `sum`。言下之意:如果 `en` 为 0,那么 `sum` 保持原值不变。因此,这里综合工具会把代码综合成一个时钟使能的寄存器,如图3一14所示。 +以上语句表示在时钟正沿来临时,如果 `en` 为 1,则将 `a + b` 的值付给 `sum`。言下之意:如果 `en` 为 0,那么 `sum` 保持原值不变。因此,这里综合工具会把代码综合成一个时钟使能的寄存器,如下图所示。 -
- -
+![带时钟使能的 D 触发器](https://josh-blog-1257563604.cos.ap-beijing.myqcloud.com/img/2020-11-30-josh-verilog-part-3/2020-11-30-josh-verilog-part-3-140-DFlipFlopClock.png!sign){width=800px} 其中,`en` 信号时 D 触发器的时钟使能端,`rst_n` 是 D 触发器的异步清零信号。 @@ -804,7 +793,7 @@ end 如下例: -```verilog +``` verilog always @(sel or a or b or c) begin case(sel) 2'b00: q = a; @@ -817,13 +806,13 @@ end 在 `case` 语句中,`default:` 一条描述了所有没有明确说明的其他可能情况。比如,这里的 `default` 就包含了 `sel` 为 `2'b11`、`2'bzz` 和 `2'bxx` 等情况。 -以上的代码将实现如上的[无锁存器的组合逻辑电路](#无锁存器的组合逻辑电路)。 +以上的代码将实现如上的[无锁存器的组合逻辑电路](#CombinationalLogicWithoutLatch)。 与 `if ... else` 语句不同的是,在 `case` 语句中,所有被判断的分支条件都具有一样的优先级。 与 `if ... else` 类似的是,语句同样需要考虑所有可能的情况,否则将会产生不想要的锁存器。如果将代码改为: -```verilog +``` verilog always @(sel or a or b or c) begin case(sel) 2'b00: q = a; @@ -833,17 +822,17 @@ always @(sel or a or b or c) begin end ``` -这样,将会产生与如上的[带锁存器的组合逻辑电路](#带锁存器的组合逻辑电路)。这是设计者不愿意看到的。 +这样,将会产生与如上的[带锁存器的组合逻辑电路](#CombinationalLogicWithLatch)。这是设计者不愿意看到的。 对于 `case` 语句,有两个派生语句,即 `casez` 及 `casex`。 `casez` 语句将分支条件中所有的 `z` 看作“不关心"的值,而不看作任何逻辑值。条件中的 `z` 可以改写为 `?`,如下例: -```verilog +``` verilog casez (encoder) - 4'b1???: hign_lvl = 3; - 4'b01??: hign_lvl = 2; - 4'b001?: hign_lvl = 1; + 4'b1???: high_lvl = 3; + 4'b01??: high_lvl = 2; + 4'b001?: high_lvl = 1; 4'b0001: high_lvl = 0; default: high_lvl = 0; endcase @@ -851,13 +840,13 @@ endcase 这里,如果 `encoder` 为 `4b'1zzz` 则 `high_lvl` 取值为 3。 -`casex` 语句将分支条件中所有的x和z看作“不关心”的值,而不看作任何逻辑值。如下例: +`casex` 语句将分支条件中所有的 `x` 和 `z` 看作“不关心”的值,而不看作任何逻辑值。如下例: -```verilog +``` verilog casex (encoder) - 4'b1xxx: hign_lvl = 3; - 4'b01xx: hign_lvl = 2; - 4'b001x: hign_lvl = 1; + 4'b1xxx: high_lvl = 3; + 4'b01xx: high_lvl = 2; + 4'b001x: high_lvl = 1; 4'b0001: high_lvl = 0; default: high_lvl = 0; endcase @@ -873,7 +862,7 @@ endcase 1. `forever` 循环:永远执行 - ```verilog + ``` verilog initial begin clk = 0; forever #25 clk = ~clk; @@ -884,7 +873,7 @@ endcase 2. `repeat` 循环:执行固定的次数 - ```verilog + ``` verilog if (rotate == 1) repeat (8) begin @@ -893,11 +882,11 @@ endcase end ``` - 以上语句中,当为 1 时,重复对 `data` 数据做8次循环左移。 + 以上语句中,当 `rotate` 为 1 时,重复对 `data` 数据做 8 次循环左移。 3. `while` 循环:当表达式为真时执行 - ```verilog + ``` verilog initial begin count = 0; while (count < 101) begin @@ -911,7 +900,7 @@ endcase 4. `for` 循环:从初始值开始,如果表达式为真就执行 - ```verilog + ``` verilog integer i; // 为 for 循环声明索引 always @(inp or cnt) begin result[7:4] = 0; @@ -930,11 +919,11 @@ endcase # 4. 结构化描述 -结构化描述就是在设计中**实例化已有的功能模块**,这些功能模块包括:门原语、用户自定义原语(UDP)、其他模块(module)。以下是结构化描述的3种实例类型:实例化其他模块、实例化门及实例化UDP。 +结构化描述就是在设计中**实例化已有的功能模块**,这些功能模块包括:门原语、用户自定义原语(UDP)、其他模块(module)。以下是结构化描述的 3 种实例类型:实例化其他模块、实例化门及实例化UDP。 下例是由两个半加器组成的全加器的模型,其中所有模块都采用了结构化描述方法。 -```verilog HalfAdd.v +``` verilog HalfAdd.v module HalfAdd(X, Y, sum, C_out); // 半加器模块 input X; @@ -950,7 +939,7 @@ and u_and(C_out, X, Y); // 门级原语实例 endmodule ``` -```verilog FullAdd.v +``` verilog FullAdd.v module FullAdd(X, Y, C_in, sum, C_out); // 全加器模块 input X; @@ -983,15 +972,13 @@ HalfAdd u_HalfAdd_B( // 半加器实例 B endmodule ``` -上例已将[原半加器代码](#半加器代码)中的语句改为门原语的实例化。在全加器的模块中,有两个半加器模块的实例和一个 `or` 门原语的实例。 +上例已将[原半加器代码](#HalfAdder)中的语句改为门原语的实例化。在全加器的模块中,有两个半加器模块的实例和一个 `or` 门原语的实例。 在以上代码中,实例化的 `or` 门原语是 Verilog 语言自带的电路,实例化的半加器模块则是用户自己设计的模块。 实现电路如下图。 -
- -
+![全加器](https://josh-blog-1257563604.cos.ap-beijing.myqcloud.com/img/2020-11-30-josh-verilog-part-3/2020-11-30-josh-verilog-part-3-150-FullAdder.png!sign){width=800px} 以上内容提到了实例化基本门和其他模块。 @@ -1017,13 +1004,11 @@ endmodule 下图清楚地了模块端口在内部和外部的类型。 -
- -
+![模块实例端口连接规则](https://josh-blog-1257563604.cos.ap-beijing.myqcloud.com/img/2020-11-30-josh-verilog-part-3/2020-11-30-josh-verilog-part-3-160-ModuleConnection.png!sign){width=800px} 下面举例说明模块内部和外部的端口: -```verilog +``` verilog HalfAdd u_HalfAdd_A( // 半加器实例 A .X (C_in), .Y (Y), @@ -1036,39 +1021,43 @@ HalfAdd u_HalfAdd_A( // 半加器实例 A 模块实例的端口对应方式有以下两种:名称对应及位置对应。 -所谓名称对应是指:将模块实例外部的信号直接对应于模块的端口名称。在实例化 `HalfAdd` 时就是采用了名称对应的方法。 +1. 名称对应 -在这种端口对应方式下,端口对应的顺序可以是任意的。在没有对应外部信号的时候,可以将端口后面的括号留空。如下: + 所谓名称对应是指:将模块实例外部的信号直接对应于模块的端口名称。在实例化 `HalfAdd` 时就是采用了名称对应的方法。 -```verilog -模块 实例名称( - .模块端口名称 (实例外部信号), - .模块端口名称 (实例外部信号), - .模块端口名称 (), //无对应信号 - ...); -``` + 在这种端口对应方式下,**端口对应的顺序可以是任意的**。在没有对应外部信号的时候,可以将端口后面的括号留空。如下: -位置对应方式就是在模块实例化的时候外部的信号需要按照该模块端口声明的顺序一一对应。例如: + ``` verilog + 模块 实例名称( + .模块端口名称 (实例外部信号), + .模块端口名称 (实例外部信号), + .模块端口名称 (), //无对应信号 + ...); + ``` -```verilog -module HalfAdd(X, Y, sum, C_out); -// ... -endmodule -``` +2. 位置对应 -在实例化该模块的时候,可以使用: + 位置对应方式就是在模块实例化的时候外部的信号需要按照该模块端口声明的顺序一一对应。例如: -```verilog -HaIfAdd u_HalfAdd(E_X, E_Y, E_sum, E_C_out); -``` + ``` verilog + module HalfAdd(X, Y, sum, C_out); + // ... + endmodule + ``` -其中 `E_X`、`E_Y`、`E_sum` 和 `E_C_out` 分别对应 `HalfAdd` 的端口 `X`、`Y`、`sum` 和 `C_out`,严格按照`HalfAdd` 模块的端口位置顺序。如果在没有对应外部信号的时候,就需要将位置留空。例如: + 在实例化该模块的时候,可以使用: -```verilog -HaIfAdd u_HalfAdd(E_X, E_Y, , E_C_out); -``` + ``` verilog + HaIfAdd u_HalfAdd(E_X, E_Y, E_sum, E_C_out); + ``` -其中 `E_X`、`E_Y` 和 `E_C_out` 分别对应 `HaIfAdd` 的端口 `X`、`Y` 和 `C_out`,模块端口 `sum` 没有对应的外部信号。 + 其中 `E_X`、`E_Y`、`E_sum` 和 `E_C_out` 分别对应 `HalfAdd` 的端口 `X`、`Y`、`sum` 和 `C_out`,严格按照`HalfAdd` 模块的端口位置顺序。如果在没有对应外部信号的时候,就需要**将位置留空**。例如: + + ``` verilog + HaIfAdd u_HalfAdd(E_X, E_Y, , E_C_out); + ``` + + 其中 `E_X`、`E_Y` 和 `E_C_out` 分别对应 `HaIfAdd` 的端口 `X`、`Y` 和 `C_out`,模块端口 `sum` 没有对应的外部信号。 ## 4.2. 参数 @@ -1080,7 +1069,7 @@ HaIfAdd u_HalfAdd(E_X, E_Y, , E_C_out); 如下的代码中定义了半加器的“与门”和“异或门"的延时分别为 2 和 4 个时间单位: -```verilog +``` verilog module half_adder(co, sum, a, b) input a, b; @@ -1108,174 +1097,172 @@ Verilog 模块参数的这一特性非常有用,用户可以定义一个通用 2. 参数直接在实例化模块时代入。 有意思的是,两家最大的 PLD 供应商 Altera 和 Xilinx 的通用模块定制恰好分别采用了这两种方法。 -比如 Altera 的 Quartus II 开发环境中,用 MegaWiard 工具定制一个宽 8 深 32 位的单口 RAM 时,将产生如下的代码: -```verilog ran_w8_d32.v -module ran_w8_d32(address, clock, data, wren, q); +- 比如 Altera 的 Quartus II 开发环境中,用 MegaWiard 工具定制一个宽 8 深 32 位的单口 RAM 时,将产生如下的代码: -input [4:0] address; -input clock; -input [7:0] data; -input wren; -output q; + ``` verilog ran_w8_d32.v + module ran_w8_d32(address, clock, data, wren, q); -altsyncram altsyncram_component( - .wren_a (wren), - .clock0 (clock), - .address_a (address), - .data_a (data), - .q_a (q), - .aclr0 (1'b0), - .aclr1 (1'b0), - .q_b (), - .clocken1 (1'b1), - .clocken0 (1'b0), - .data_b (1'b1), - .rden_b (1'b1), - .address_b (1'b1), - .wren_b (1'b0), - .byteena_b (1'b0), - .addressstall_a(1'b0), - .byteena_a (1'b1), - .addressstall_b(1'b0), - .clocken1 (1'b1) - ); + input [4:0] address; + input clock; + input [7:0] data; + input wren; + output q; -defparam - altsyncram_component.clock_enable_input_a = "BYPASS", - altsyncram_component.clock_enable_output_a = "BYPASS", - altsyncram_component.intended_device_family = "Stratix II", - altsyncram_component.lpm_hint = "ENABLE_RUNTIME_MOD = NO", - altsyncram_component.lpm_type = "altsyncram", - altsyncram_component.numwords_a = 32, // 32 个字 - altsyncram_component.operation_mode = "SINGLE_PORT", - altsyncram_component.outdata_aclr_a = "NONE", - altsyncram_component.outdata_reg_a = "UNREGISTERED", - altsyncram_component.power_up_uninitialized = "FALSE", - altsyncram_component.ram_block_type = "M4K", - altsyncram_component.widthad_a = 5, // 5 位地址 - altsyncram_component.width_a = 8, // 8 位宽的数据 - altsyncram_component.width_byteena_a = 1; + altsyncram altsyncram_component( + .wren_a (wren), + .clock0 (clock), + .address_a (address), + .data_a (data), + .q_a (q), + .aclr0 (1'b0), + .aclr1 (1'b0), + .q_b (), + .clocken1 (1'b1), + .clocken0 (1'b0), + .data_b (1'b1), + .rden_b (1'b1), + .address_b (1'b1), + .wren_b (1'b0), + .byteena_b (1'b0), + .addressstall_a(1'b0), + .byteena_a (1'b1), + .addressstall_b(1'b0), + .clocken1 (1'b1) + ); -endmodule -``` + defparam + altsyncram_component.clock_enable_input_a = "BYPASS", + altsyncram_component.clock_enable_output_a = "BYPASS", + altsyncram_component.intended_device_family = "Stratix II", + altsyncram_component.lpm_hint = "ENABLE_RUNTIME_MOD = NO", + altsyncram_component.lpm_type = "altsyncram", + altsyncram_component.numwords_a = 32, // 32 个字 + altsyncram_component.operation_mode = "SINGLE_PORT", + altsyncram_component.outdata_aclr_a = "NONE", + altsyncram_component.outdata_reg_a = "UNREGISTERED", + altsyncram_component.power_up_uninitialized = "FALSE", + altsyncram_component.ram_block_type = "M4K", + altsyncram_component.widthad_a = 5, // 5 位地址 + altsyncram_component.width_a = 8, // 8 位宽的数据 + altsyncram_component.width_byteena_a = 1; -其中,`altsyncram` 是 Altera 的块 RAM 的通用模型。它可以在 Altera 的仿真文件“alteramf.v”中找到。 + endmodule + ``` -`altsyncram-component` 是实例名称,`.` 后面是参数的名称,定义的参数值,有的是字符串,有的是整数。 + 其中,`altsyncram` 是 Altera 的块 RAM 的通用模型。它可以在 Altera 的仿真文件“alteramf.v”中找到。 -在 `defparam` 关键字后面的是参数重定义的语句: + `altsyncram-component` 是实例名称,`.` 后面是参数的名称,定义的参数值,有的是字符串,有的是整数。 -```verilog -altsyncram_component.width_a = 8, // 8 位宽的数据 -``` + 在 `defparam` 关键字后面的是参数重定义的语句: -以上就是将 `altsyncram-component` 中的参数 `width_a` 重新定义为 8。 + ``` verilog + altsyncram_component.width_a = 8, // 8 位宽的数据 + ``` -使用 `defparam` 的方法重新定义参数时,可以根据需要对部分的参数重新定义,其他的会保留模块的缺省值。 + 以上就是将 `altsyncram-component` 中的参数 `width_a` 重新定义为 8。 -用 Xilinx 设计工具 ISE 中的 CORE Generator 产生一个单端口 8 位宽、32 位深的 RAM。注意观察,其中RAM的参数是如何带入的: + 使用 `defparam` 的方法重新定义参数时,可以根据需要对部分的参数重新定义,其他的会保留模块的缺省值。 -```verilog ram_w8_d32.v -module ram_w8_d32(addr, clk, din, dout, we) +- 用 Xilinx 设计工具 ISE 中的 CORE Generator 产生一个单端口 8 位宽、32 位深的 RAM。注意观察,其中RAM的参数是如何带入的: -input [4:O] addr; -input clk; -input [7:O] din. -output [7:0] dout; -input we; + ``` verilog ram_w8_d32.v + module ram_w8_d32(addr, clk, din, dout, we) -// synopsys translate_off -BLKMEMSP_V6_1 #( - 5, // c_addr_width - "0", // c_default_data - 32, // c_depth - 0, // c_enable_rlocs - 1, // c_has_default_data - 1, // c_has_din - 0, // c_has_en - 0, // c_has_limit_data_pitch - 0, // c_has_nd - 0, // c_has_rdy - 0, // c_has_rfd - 0, // c_has_sinit - 1, // c_has_we - 18, // c_limit_data_pitch - "mif_file_16_1", // c_em_init_file - 0, // c_pipe_stages - 0, // c_reg_inputs - "0", // c_sinit_value - 8, // c_width - 0, // c_write_mode - "0", // c_ybottom_addr - 1, // c_yclk_is_rising - 1, // c_yen_is_high - "hierarchy1", // c_hierarchy - 0, // c_ymake_bmm - "32kx1", // c_yprimitive_type - 1, // c_ysinit_is_high - "1024", // c_ytop_addr - 0, // c_yuse_single_primitive - 1, // c_ywe_is_high - 1 // c_yydisable_warnings - ) + input [4:O] addr; + input clk; + input [7:O] din. + output [7:0] dout; + input we; -inst( - .ADDR (addr), - .CLK (clk), - .DIN (din), - .DOUT (dout), - .WE (we), - .EN (), - .ND (), - .RFD (), - .RDY (), - .SINIT() - ); -// synopsys translate_on -endmodule -``` + // synopsys translate_off + BLKMEMSP_V6_1 #( + 5, // c_addr_width + "0", // c_default_data + 32, // c_depth + 0, // c_enable_rlocs + 1, // c_has_default_data + 1, // c_has_din + 0, // c_has_en + 0, // c_has_limit_data_pitch + 0, // c_has_nd + 0, // c_has_rdy + 0, // c_has_rfd + 0, // c_has_sinit + 1, // c_has_we + 18, // c_limit_data_pitch + "mif_file_16_1", // c_em_init_file + 0, // c_pipe_stages + 0, // c_reg_inputs + "0", // c_sinit_value + 8, // c_width + 0, // c_write_mode + "0", // c_ybottom_addr + 1, // c_yclk_is_rising + 1, // c_yen_is_high + "hierarchy1", // c_hierarchy + 0, // c_ymake_bmm + "32kx1", // c_yprimitive_type + 1, // c_ysinit_is_high + "1024", // c_ytop_addr + 0, // c_yuse_single_primitive + 1, // c_ywe_is_high + 1 // c_yydisable_warnings + ) -其中 `BLKMEMSP_V6_1` 是 Xilinx 的块状RAM的通用模型。`inst` 是用户实例名称。用户需要在通用RAM类型的基础上,通过参数带入实现。采用 `#(...)` 方法来实现参数逐个覆盖。 + inst( + .ADDR (addr), + .CLK (clk), + .DIN (din), + .DOUT (dout), + .WE (we), + .EN (), + .ND (), + .RFD (), + .RDY (), + .SINIT() + ); + // synopsys translate_on + endmodule + ``` -使用上面这种参数直接带入法时,要注意一点,所有的参数都需要全部按顺序列出来,不能遗漏,也不能颠倒顺序,否则就容易对应不上。 + 其中 `BLKMEMSP_V6_1` 是 Xilinx 的块状RAM的通用模型。`inst` 是用户实例名称。用户需要在通用RAM类型的基础上,通过参数带入实现。采用 `#(...)` 方法来实现参数逐个覆盖。 + + 使用上面这种参数直接带入法时,要注意一点,所有的参数都需要全部按顺序列出来,不能遗漏,也不能颠倒顺序,否则就容易对应不上。 # 5. 设计层次 在前面几节中,介绍了 Verilog 的描述方式。 + Verilog 语言是一种强大的硬件描述语言,可以支持多个设计层次。在这里将进行简单的介绍。 -## 5.1. 系统级和行为级 +## 5.1. 系统级和行为级 Verilog 语言作为一种用户工具,提供给用户许多描述硬件的手段,如前面所述:数据流描述、行为描述(`always` 和 `initial` 语句)、结构化描述。同时,不同用户可以根据自己的需要,在不同抽象层次上对硬件进行描述。有如下设计层次金字塔供参考。 -
- -
+![设计层次金字塔](https://josh-blog-1257563604.cos.ap-beijing.myqcloud.com/img/2020-11-30-josh-verilog-part-3/2020-11-30-josh-verilog-part-3-170-DesignLevelPyramid.png!sign){width=600px} 下面简单介绍几种不同角色的工作特点,以及它们所处的设计层次: -系统构架师:在目前业界主流的设计方法学中,系统构架师(System Architect)通常用高级语言,如SystemC,来描述一个系统的规格,仿真整个系统的功能和性能等。这种早期的设计和探索往往不涉及到具体的实现细节,甚至于软件和硬件的划分都没有开始。系统构架师也可以采用 Verilog 来描述系统的功能,它们往往不考虑硬件实现的细节。因而称这种设计层次为系统级或算法级。 +1. 系统构架师:在目前业界主流的设计方法学中,系统构架师(System Architect)通常用高级语言,如 SystemC,来描述一个系统的规格,仿真整个系统的功能和性能等。这种早期的设计和探索往往不涉及到具体的实现细节,甚至于软件和硬件的划分都没有开始。系统构架师也可以采用 Verilog 来描述系统的功能,它们往往不考虑硬件实现的细节。因而称这种设计层次为系统级或算法级。 -逻辑设计工程师:他们利用前面所讲的 Verilog 各种描述手段,设计 RTL 级的代码,精确到时钟周期。逻辑设计工程师的代码,通过综合工具的综合,可以转换为 Verilog 的门级网表,其中所有的功能块都是由基本的门单元组成的。 +2. 逻辑设计工程师:他们利用前面所讲的 Verilog 各种描述手段,设计 RTL 级的代码,精确到时钟周期。逻辑设计工程师的代码,通过综合工具的综合,可以转换为 Verilog 的门级网表,其中所有的功能块都是由基本的门单元组成的。 -物理设计工程师:他们将这些门级网表进行布局和布线,做成实际的芯片。 +3. 物理设计工程师:他们将这些门级网表进行布局和布线,做成实际的芯片。 -验证工程师:他们负责对设计的电路进行验证,他们编写的代码主要是用来产生激励,这些激励大部分需要的抽象层次更高,以使仿真的效率更高。然后在工具中对电路进行仿真,检查响应结果。这些代码不会实现为具体癭件,有些并不需要精确到时钟周期,而只是在软件的仿真工具中运行,实现一定的功能即可,称这种描述层次为行为级。 +4. 验证工程师:他们负责对设计的电路进行验证,他们编写的代码主要是用来产生激励,这些激励大部分需要的抽象层次更高,以使仿真的效率更高。然后在工具中对电路进行仿真,检查响应结果。这些代码不会实现为具体癭件,有些并不需要精确到时钟周期,而只是在软件的仿真工具中运行,实现一定的功能即可,称这种描述层次为行为级。 这里的行为级描述,不同于 [3. 行为描述](#03)中所述的“行为描述方式”。这里特指一种描述的抽象层次。 下面举一个实例说明 RTL 级和行为级的区别,要实现的状态机一部分如下图所示。 -
- -
+![一个简单的握手协议状态机](https://josh-blog-1257563604.cos.ap-beijing.myqcloud.com/img/2020-11-30-josh-verilog-part-3/2020-11-30-josh-verilog-part-3-180-ASimpleHandshakingStateMachine.png!sign){width=500px} 上图中只有两个状态,当在左边的状态时,`REQ` 输出 1;当在右边的状态时,`REQ` 输出 0。`ACK` 是状态机的输入信号,决定了状态的跳转。 如果是让一个 RTL 设计工程师来设计,他马上就会与硬件实现的细节联系起来,很可能就会用如下代码实现: -```verilog +``` verilog /************************ RTL 描述 ************************/ @@ -1318,17 +1305,17 @@ end 然而,如果让一个逻辑验证工程师来设计这个状态机,他不考虑硬件实现的细节,只需要在语义上满足要求即可。甚至可以不出现状态寄存器。如下例: -```verilog +``` verilog /************************ 行为描述 ************************/ initial begin - ... + // ... REQ = 1; wait (ACK == 1); REQ = 0; wait (ACK == 0); - ... + // ... end ``` @@ -1342,15 +1329,19 @@ end Verilog 设计电路最常用的设计层次就是 RTL 级。在 RTL 描述时,设计者需要关注寄存器的行为,其中保存着数据;同时需要关注寄存器和寄存器之间的组合逻辑功能,是否能满足功能需求和时序需求。RTL 级模型是严格精确到时钟周期的模型。 -后续会重点介绍 RTL 设计的方法和技巧。 +[《Part 4 —— RTL 概念与常用 RTL 建模》][]、[《Part 5 —— RTL 设计与编码指导》][]和[《Part 6—— 如何写好状态机》][]会重点介绍 RTL 设计的方法和技巧。 ->RTL 级是 HDL 语言最重要的概念之一,RTL 级是综合器最常用的设计输入层次。目前使用 FPGA/CPLD 等可编程逻辑器件时,设计输入都为 RTL 级。这是因为门级输入过于繁琐;而对于行为级和系统级设计输入,很多综合器不支持,并容易产生综合歧义。 +{% note info %} + +RTL 级是 HDL 语言最重要的概念之一,RTL 级是综合器最常用的设计输入层次。目前使用 FPGA/CPLD 等可编程逻辑器件时,设计输入都为 RTL 级。这是因为门级输入过于繁琐;而对于行为级和系统级设计输入,很多综合器不支持,并容易产生综合歧义。 + +{% endnote %} ## 5.3. 门级 在 Verilog 语义中,使用一些基本的门原语可以直接描述电路的门级功能。例如: -```verilog HalfAdd.v +``` verilog HalfAdd.v module HalfAdd(X, Y, sum, C_out); // 半加器模块 input X; @@ -1380,6 +1371,13 @@ Verilog 支持不同设计层次的混描述。 实际上,这些描述层次之间没有严格的界限。这里只是想将概念描述清楚,这一点对初学者非常重要。 ---- +# 参考文献 -在本篇笔记中,重点介绍了 Verilog 的 3 种描述方式以及 Verilog 可以应用的设计层次。至此,大家应该能感受到 Verilog 语言是一种非常灵活、强大的硬件描述语言。 +EDA 先锋工作室. *轻松成为设计高手——Verilog HDL 实用精解.* 北京航空航天大学出版社, 2012. + +[《Part 2——Verilog 语言基础》的第 3 小节]: https://josh-gao.top/posts/fd117896.html#%E8%A1%8C%E4%B8%BA%E6%8F%8F%E8%BF%B0 +[《Part 2——Verilog 语言基础》的 7.4 驱动和赋值]: https://josh-gao.top/posts/fd2ca242.html#%E9%A9%B1%E5%8A%A8%E5%92%8C%E8%B5%8B%E5%80%BC +[《Part 2——Verilog 语言基础》的第 2.2 小节]: https://josh-gao.top/posts/fd2ca242.html#%E7%A7%8D%E6%8F%8F%E8%BF%B0%E6%96%B9%E6%B3%95 +[《Part 4 —— RTL 概念与常用 RTL 建模》]: https://josh-gao.top/posts/ecb88422.html +[《Part 5 —— RTL 设计与编码指导》]: https://josh-gao.top/posts/53b8b42e.html +[《Part 6—— 如何写好状态机》]: https://josh-gao.top/posts/d860a67e.html