第三章 硬件描述语言verilog
第1节 Verilog语言的历史
在传统硬件电路的设计方法中,当设计工程师需要设计一个新的硬件、数字电路或数字逻辑系统时,需要为此设计并画出一张线路图,随后在 CAE(计算机辅助工程分析)工作站上进行设计。所设计的线路图由线和符号组成,其中线代表了线路,符号代表了基本设计单元,其取自于工程师构造此线路图使用的零件符号库。对于不同逻辑器件的设计,需要选择对应的符号库,如当设计工程师选择的时标准逻辑器件(74 系列等)作为板级设计线路图,那么此线路图的符号则需要取自标准逻辑零件符号库;若设计工程师进行了 ASIC 设计,线路图的符号就要取自 ASIC 库专用的宏单元。
这就是传统的原理图设计方法,原理图设计法存在着许多弊端,如当设计者想要实现线路图的逻辑优化时,就需要利用 EDA 工具或者人工进行布尔函数逻辑优化。除此之外,传统原理图设计还存在难以验证的缺点,设计工程师想要验证设计,必须通过搭建硬件平台(比如电路板),为设计验证工作带来了麻烦。
随着人们对于科技的要求与期待越来越高,电子设计技术发展也越来越快,设计的集成度、复杂程度也逐渐加深,传统的设计方法已经无法满足高级设计的需求,最终出现了借助先进 EDA 工具的一种描述语言设计方法,可以对数字电路和数字逻辑系统进行形式化的描述,这种语言就是硬件描述语言。
硬件描述语言,英文全称为 Hardware Description Language,简称 HDL,HDL 是一种用形式化方法来描述数字电路和数字逻辑系统的语言。设计工程师可以使用这种语言来表述自己的设计思路,通过利用 EDA 工具进行仿真、自动综合到门级电路,最终在 ASIC 或 FPGA 实现其功能。
以 2 输入的与门为例来对比原理图设计方法与 HDL 设计方法之间的区别,在传统的设计方法中设计 2 输入与门可能需到标准器件库中调用 74 系列的器件,但在硬件描述语言中“&”就是一个与门的形式描述,“C = A & B”就是一个 2 输入与门的描述。而“&”就代表了一个与门器件。
硬件描述语言发展至今已有二十多年历史,当今业界的标准中(IEEE 标准)主要有 VHDL 和Verilog HDL 这两种硬件描述语言。本书采用的是 VerilogHDL 硬件描述语言,接下来着重对其发展的历史及特点进行介绍。
Verilog HDL 语言最初是在 1983 年由 Gateway Design Automation 公司为其模拟器产品开发的硬件建模语言,当时这只是公司产品的专用语言。随着公司模拟、仿真器产品的广泛使用,Verilog HDL 作为一种实用语言逐渐为众多设计者所接受。1990 年一次致力于增加语言普及性的活动中,Verilog HDL 语言被推向公众领域从而被更多人熟知。Open Verilog International(OVI)是促进 Verilog 发展的国际性组织。1992 年,OVI 决定致力于推广 Verilog OVI 标准成为 IEEE 标准。这一推广最后获得成功,Verilog 语言于 1995 年成为IEEE 标准,称为 IEEE Std1364-1995。其完整标准在 Verilog 硬件描述语言参考手册中有详细描述。
Verilog HDL 语言具有许多优点,例如 Verilog HDL 语言提供了编程语言接口,通过该接口可以在模拟、验证期间从设计外部访问设计,包括模拟的具体控制和运行。Verilog HDL 语言不仅定义了语法,而且对每个语法结构都定义了清晰的模拟、仿真语义。因此,用这种语言编写的模型能够使用 Verilog 仿真器进行验证。Verilog HDL 提供了扩展的建模能力,其中许多扩展最初很难理解,但是 Verilog HDL 语言的核心子集非常易于学习和使用,这对大多数建模应用来说已经足够。当然,完整的硬件描述语言足以对从最复杂的芯片到完整的电子系统进行描述。
第2节 综合和仿真
2.1 综合
Verilog 是硬件描述语言,顾名思义,就是用代码的形式描述硬件的功能,最终在硬件电路上实现该功能。在 Verilog 描述出硬件功能后需要使用综合器对 Verilog 代码进行解释并将代码转化成实际的电路来表示,最终产生实际的电路,也被称为网表。这种将 Verilog 代码转成网表的工具就是综合器。
上图左上角是一段 Verilog 代码,该代码实现了一个加法器的功能。在经过综合器解释后该代码被转化成一个加法器电路。QUARTUS、ISE 和 VIVADO 等 FPGA 开发工具都是综合器,而在集成电路设计领域常用的综合器是 DC。
2.2 仿真
在 FPGA 设计的过程中,不可避免会出现各种 BUG。如果在编写好代码、综合成电路、烧写到FPGA 后才发现问题,此时再去定位问题就会非常地困难。而在综合前,设计师可以在电脑里通过仿真软件对代码进行仿真测试,检测出 BUG 并将其解决,最后再将程序烧写进 FPGA。一般情况下可以认为没有经过仿真验证的代码,一定是存在 BUG 的。
为了模拟真实的情况,需要编写测试文件。该文件也是用 Verilog 编写的,其描述了仿真对象的输入激励情况。该激励力求模仿最真实的情况,产生最接近的激励信号,将该信号的波形输入给仿真对象,查看仿真对象的输出是否与预期一致。需要注意的是:在仿真过程中没有将代码转成电路,仿真器只是对代码进行仿真验证。至于该代码是否可转成电路,仿真器并不关心。
由此可见,Verilog 的代码不仅可以描述电路,还可以用于测试。事实上,Verilog 定义的语法非常之多,但绝大部分都是为了仿真测试来使用的,只有少部分才是用于电路设计,详细可以参考本书的“可综合逻辑设计”一节。Verilog 中用于设计的语法是学习的重点,掌握好设计的语法并熟练应用于各种复杂的项目是技能的核心。而其他测试用的语法,在需要时查找和参考就已经足够了。本书旨在方便本科、研究生的教学,因此将重点讲解设计用的语法。
2.3 可综合设计
Verilog 硬件描述语言有类似高级语言的完整语法结构和系统,这些语法结构的应用给设计描述带来很多方便。但是,Verilog 是描述硬件电路的,其建立在硬件电路的基础之上。而有些语法结构只是以仿真测试为目的,是不能与实际硬件电路对应起来的。也就是说在使用这些语法时,将一个语言描述的程序映射成实际硬件电路中的结构是不能实现的,也称为不可综合语法。
综合就是把编写的 rtl 代码转换成对应的实际电路。比如编写代码 assign a=b&c;EDA 综合工具就会去元件库里调用一个二输入与门,将输入端分别接上 b 和 c,输出端接上a。
同样地,如果设计师编写了一些如下所示的语句
assign a=b&c; assign c=e|f; assign e=x^y; ..
综合工具就会像搭积木一样把这些“逻辑”电路用一些“门”电路来搭起来。当然,工具会对必要的地方做一些优化,比如编写一个电路 assing a=b&~b,工具就会将 a 恒接为 0,而不会去调用一个与门来搭这个电路。
综述所述,“综合”要做的事情有:编译 rtl 代码,从库里选择用到的门器件,把这些器件按照“逻辑”搭建成“门”电路。
不可综合,是指找不到对应的“门”器件来实现相应的代码。比如“#100”之类的延时功能,简单的门器件是无法实现延时 100 个单元的,还有打印语句等,也是门器件无法实现的。在设计的时候要确保所写的代码是可以综合的,这就依赖于设计者的能力,知道什么是可综合的代码,什么是不可综合的代码。对于初学者来说,最好是先记住规则,遵守规则,先按规则来设计电路并在这一过程中逐渐理解,这是最好的学习路径。
下面表格中列出了不可综合或者不推荐使用的代码。
下表为推荐使用的设计代码。
第3节 模块结构
3.1 模块介绍
模块(module)是 Verilog 的基本描述单位,是用于描述某个设计的功能或结构及与其他模块
通信的外部端口。
模块在概念上可等同一个器件,就如调用通用器件(与门、三态门等)或通用宏单元(计数器、ALU、CPU)等。因此,一个模块可在另一个模块中调用,一个电路设计可由多个模块组合而成。一
个模块的设计只是一个系统设计中的某个层次设计,模块设计可采用多种建模方式。
Verilog 的基本设计单元是“模块”。采用模块化的设计使系统看起来更有条理也便于仿真和测试,因此整个项目的设计思想就是模块套模块,自顶向下依次展开。在一个工程的设计里,每个模块实现特定的功能,模块间可进行层次的嵌套。对大型的数字电路进行设计时,可以将其分割成大小不一的小模块,每个小模块实现特定的功能,最后通过由顶层模块调用子模块的方式来实现整体功能,这就是Top-Down 的设计思想。
本书主要以 Verilog 硬件描述语言为主,模块是 Verilog 的基本描述单位,用于描述每个设计的功能和结构,以及其他模块通信的外部接口。模块有五个主要部分:端口定义、参数定义(可选)、 I/O 说明、内部信号声明、功能定义。
模块总是以关键词 module 开始,以关键词 endmodule 结尾。它的一般语法结构如下所示:
3.2 模块名和端口定义
第 1 至 5 行声明了模块的名字和输入输出口。其格式如下:
module 模块名(端口 1,端口 2,端口 3,……);
其中模块是以 module 开始,以 endmodule 结束。模块名是模块唯一的标识符,一般建议模块名
尽量用能够描述其功能的名字来命名,并且模块名和文件名相同。
模块的端口表示的是模块的输入和输出口名,也是其与其他模块联系端口的标识。
3.3 参数定义
第 8 行参数定义是将常量用符号代替以增加代码可读性和可修改性。这是一个可选择的语句,
用不到的情况下可以省略,参数定义一般格式如下:
parameter DATA_W = x
3.4 接口定义
第 9 至 12 行是 I/O(输入/输出)说明,模块的端口可以是输入端口、输出端口或双向端口。其
说明格式如下。
输入端口: input [信号位宽-1 : 0] 端口名 1;
input [信号位宽-1 : 0] 端口名 2;
……;
输出端口: output [信号位宽-1 : 0] 端口名 1;
output [信号位宽-1 : 0] 端口名 2;
……;
双向端口:inout [信号位宽-1 : 0] 端口名 1;
inout [信号位宽-1 : 0]
3.5 信号类型
第 14 至 17 行定义了信号的类型。这些信号是在模块内使用到的信号,并且与端口有关的 wire
和 reg 类型变量。其声明方式如下:
reg [width-1 : 0] R 变量 1, R 变量 2 ……; wire [width-1 : 0] W 变量 1,W 变量 2……;
如果没有定义信号类型,默认是 wire 型,并且信号位宽为1。
3.6 功能描述
第 21 至 31 行是功能描述部分。模块中最重要的部分是逻辑功能定义部分,有三种方法可在模块
中产生逻辑。
1. 用“assign”声明语句,如描述一个两输入与门:assign a = b & c。详细功能见“功能描述- 组合逻辑”一节。
2. 用“always”块。即前面介绍的时序逻辑和组合逻辑。
3. 模块例化。详细功能见“模块例化”一节
3.7 模块例化
对数字系统的设计一般采用的是自顶向下的设计方式,可将系统划分成几个功能模块,每个功能模块再划分成下一层的子模块。每个模块的设计对应一个 module ,每个 module 设计成一个 Verilog HDL 程序文件。因此,对一个系统的顶层模块采用结构化设计,即顶层模块分别调用了各个功能模块。
一个模块能够在另外一个模块中被引用,这样就建立了描述的层次。模块实例化语句形式如下:
module_nameinstance_name(port_associations) ;
信号端口可以通过位置或名称关联,但是关联方式不能够混合使用。端口关联形式如下:
port_expr / /通过位置。 .PortName (port_expr) / /通过名称
第4节 信号类型
Verilog HDL 的信号类型有很多种,主要包括两种数据类型:线网类型(net type) 和寄存器类型(reg type)。在进行工程设计的过程中也只会使用到这两个类型的信号。
4.1 信号位宽
定义信号类型的同时,必须定义好信号的位宽。默认信号的位宽是 1 位,当信号的位宽为 1 时可不表述,如定义位宽为 1 的 wire 型信号 a 可直接用“wire a;”来表示。但信号的位宽大于 1 位时就一定要表示出来,如用“wire [7:0]”来表示该 wire 型信号的位宽为 8 位。
信号的位宽取决于要该信号要表示的最大值。该信号能表示的无符号数最大值是:2^n-1,其中 n表示该信号的位宽。例如,信号 a 的最大值为 1000,那么信号 a 的位宽必须大于或等于 10 位。
下面分享一个位宽计算技巧:打开电脑的“计算器”后选用程序员模式,在 10 进制下输入信号值,如 1000,随后可以查看信号位宽。
4.2 线网类型wire
线网类型用于对结构化器件之间的物理连线的建模,如器件的管脚,芯片内部器件如与门的输出等。由于线网类型代表的是物理连接线,因此其不存储逻辑值,必须由器件驱动。通常用 assign 进行赋值,如 assign A = B ^ C
wire 类型定义语法如下:
wire [msb: lsb] wire1, wire2, . . .,wireN;
msb 和 lsb 定义了范围,表示了位宽。例如[7:0]是 8 位位宽,也就是可以表示成 8’b0 至 8’b1111_1111;
msb 和 lsb 必须为常数值;
如果没有定义范围,缺省值为 1 位;
wire a;//定义了一个1位的wire型数据
没有定义信号数据类型时,缺省为 wire 类型。
注意数组类型按照降序方式,如[7:0] ,不要写成[0:7]。
下面对上述情况进行举例说明:
wire [3:0] Sat; // Sat 为 4 位线型信号 wire Cnt; //1 位线型信号 wire [31:0] Kisp, Pisp, Lisp ;// Kisp, Pisp, Lisp 都是 32 位的线型信
4.3 寄存器类型reg
reg 是最常用的寄存器类型,寄存器类型通常用于对存储单元的描述,如 D 型触发器、ROM等。寄存器类型信号的特点是在某种触发机制下分配了一个值,在下一触发机制到来之前保留原值。但必须注意的是:reg 类型的变量不一定是存储单元,如在 always 语句中进行描述的必须是用 reg类型的变量。
reg 类型定义语法如下:
reg [msb: lsb] reg1, reg2, . . . reg N;
msb 和 lsb 定义了范围,表示了位宽。例如[7:0]是 8 位位宽,也就是可以表示成 8’b0 至 8’b1111_1111;
msb 和 lsb 必须为常数值;
如果没有定义范围,缺省值为 1 位;
没有定义信号数据类型时,缺省为 wire 类型,不是 reg 型。
对数组类型按照降序方式,如[7:0] ;不要写成[0:7]。
例如:
reg [3:0] Sat; // Sat 为 4 位寄存器型信号。 reg Cnt; //1 位寄存器。 reg [31:0] Kisp, Pisp, Lisp ; // Kisp, Pisp, Lisp 都是 32
4.4 wire与reg的区别
reg 型信号并不一定生成寄存器。针对什么时候使用 wire 类型,什么时候用 reg 类型这一问题,本书总结出一套解决方法:在本模块中使用 always 设计的信号都定义为 reg 型,其他信号都定义为 wire 。
上述代码中,cnt1 是用 always 设计的,所以定义为 reg 型。
add_cnt1 和 end_cnt 不是由 always 产生的,所以定义为 wire 型。
上述代码中,信号 x 是用 always 设计的,所以要定义为 reg 型。
注意:实际的电路中信号 x 不哈哈是寄存器类型,但仍然定义为 reg 型
以上是例化的代码,其中 df 是例化模块的输出。由于 df 不是由 always 产生的,而是例化产生的,因此要定义成 wire 型。