带你读《FPGA应用开发和仿真》之二:Verilog HDL和SystemVerilog-阿里云开发者社区

开发者社区> 华章出版社> 正文
登录阅读全文

带你读《FPGA应用开发和仿真》之二:Verilog HDL和SystemVerilog

简介: 本书是笔者多年FPGA开发和教学经验的总结,弥补了多年来面向创新中心学生讲授FPGA应用课程时的教材缺失——虽然优秀教材有很多,但并没有特别吻合笔者思路和学生要求的。希望本书能对正在学习FPGA应用技术的学生给予有力的帮助,也希望能给正在使用FPGA进行项目开发的在校研究生、在业工程师一点借鉴。

第2章:Verilog HDL和SystemVerilog

在本书中,Verilog HDL(IEEE 1364—2005)和SystemVerilog(IEEE 1800—2012)将被统一简称为Verilog。
本章主要介绍Verilog的常用语法,并将以SystemVerilog为主,包含SystemVerilog中很多新的、具备更优特性的语法,包括可被综合的和用于仿真验证的。但本章并不会太多地深入语法细节,依笔者浅见,语法本身只是用来描述硬件和承载电路设计思想的工具,诚然,语法本身也很复杂,也饱含了规范制定者们对于数字电路及其描述方法的先进思想,也有太多需要学习和理解的地方,不过笔者更希望读者能够在后续章节的各种设计实例中学习和理解,而不要拘于语法本身。本章的结构和内容也更像是“简明参考”,而非详述语法的“教科书”,且远未能涵盖Verilog语法标准的所有内容,如果需要了解语法细则,可查阅IEEE 1800—2012原文。
本章会使用一些符号和特征来书写语法规则,与IEEE标准中使用的有些相似,但因为只用通配一些常用的规则,所以做了大幅简化,这些符号和特征包括:

  • <>,尖括号,其内容为后面将进一步解释的内容。
  • [],方括号,表示其中内容为可选。
  • [|],方括号和竖线,表示由竖线隔开的内容选其一或不选。
  • {},花括号,表示其中内容可选或可重复多次。
  • {|},花括号和竖线,表示由竖线隔开的内容选其一或不选或可重复选择多次。
  • (|),圆括号和竖线,表示由竖线隔开的内容选其一。
  • 粗体字,表示语句中应有的关键字或符号。

上述符号本身不包含在语句之中。
对于初学者,学习至2.13节即可掌握常用的Verilog语法,可以进行后面章节的学习,2.14节及以后的内容主要是一些进阶语法的示例,可以在以后遇到相关语法时再回来学习。

2.1 硬件描述语言简介

在没有电子设计自动化(EDA)软件的年代,数字电路设计依赖于手绘电路原理图,在EDA软件出现的初期,也继续沿用了绘制原理图这一方式(或称为原理图输入)。EDA原理图输入虽方便修改,具有一定的可重用性,但对于稍大规模的电路,布线工作仍然要花费大量时间,设计者仍要对器件和工艺非常熟悉。
在20世纪80年代,硬件描述语言开始大量出现,其使用贴近自然语言的文字来描述电路的连接、结构甚至直接描述电路功能。对于计算机,文字输入更方便快捷;对于设计者,文字更抽象,与具体器件和实现工艺无关,更能集中精力在功能的描述上而不是繁琐的连线中。当然,最后从文字到具体电路实现的转换,依赖于成熟的、标准化的器件和电路单元,但这些是IC工艺设计者和EDA软件设计者的任务,不需要数字逻辑设计者来操心了。
Verilog HDL(Verilog Hardware Description Language)诞生于1983年,同时期出现的数字电路硬件描述语言不下百种,发展至今最主流的有Verilog HDL和VHDL(Very high speed integrated circuit Hardware Description Language)两种,VHDL在1987年成为IEEE标准,Verilog HDL在1995年成为IEEE标准(IEEE 1364)。
对于数字逻辑设计者来说,描述硬件的抽象层次自上而下主要可分为:

  • 系统级(System level),描述电路系统的整体结构、行为和性能。
  • 算法级(Algorithm level),描述单元电路的算法实现。
  • 寄存器传输级(Register transfer level,RTL),描述数据在寄存器间的流动、处理和交互。
  • 门级(Gate level),描述逻辑门及它们之间的电路连接。

比门级更低层次还有开关级(Switch level),它与器件工艺相关,对于普通数字逻辑设计者并不需要掌握。
描述硬件的方法又可分为行为描述和结构描述:

  • 行为描述(Behavioral modeling)描述电路的行为,由EDA软件负责生成符合该行为的电路。
  • 结构描述(Structural modeling)描述电路的组成结构。

抽象层次和描述方法在Verilog标准中并没有明确定义,它们之间也没有严格对应关系,不过一般认为从系统级到RTL基本属于行为描述,底层的RTL和门级基本属于结构描述,而事实上系统级的大模块组织结构也有结构描述的意思。RTL描述数据流动和处理时也称为数据流描述。
稍大的数字系统几乎不可能设计出来就正确无误,对设计结果的测试验证也是设计过程中非常重要的步骤。得益于EDA工具,许多测试验证工作可以在计算机上模拟,而不必等到真实电路制造出来,这一过程称为仿真。就像对一块电路板进行测试需要仪器仪表和模拟工作环境的测试电路一样,对数字系统进行仿真验证也需要设计测试系统,而且由于仿真测试系统最终不必变成实际电路,Verilog专为仿真验证提供了更灵活的语法。  

1.png


图2-1 抽象层次、描述方法、Verilog和VHDL的建模能力


IEEE在2001年至2012年间多次对Verilog进行了修订和扩展。在2005年扩展了大量支持系统级建模和验证的特性,并形成了独立标准(IEEE 1800),被称为SystemVerilog(注意并没有空格);2009年,原1364标准被终止,并被合并进1800标准,原Verilog HDL成为SystemVerilog的子集(如图2-1所示),SystemVerilog最新的正式标准是IEEE 1800—2012。因而我们现在所说的Verilog HDL严格来说都是SystemVerilog,本书后面统称为Verilog。
新的SystemVerilog标准是一个“统一的硬件设计、规范和验证语言”(Unified Hardware Design, Specification and Verification Language),承载了硬件设计和验证两大目标,但因SystemVerilog提出之初主要是扩展系统级建模和验证功能,因而人们常常有SystemVerilog只适用于验证,而不适用于实际电路的设计和描述的误解。

2.2 设计方法和流程

复杂的数字逻辑电路一般采用层次化、模块化的设计方法,即将复杂的整体功能拆分成许多互联的功能模块,而这些功能模块又可以再细分为更小的模块。合理的功能拆分、模块功能定义和模块间的交互规则将有效地降低整个系统的设计难度、提高设计的可行性和性能。图2-2为功能和模块拆分的示意图。

2.png


图2-2 功能和模块拆分的示意图


这样从整体功能设计出发逐渐拆分至底层的设计方法称为自顶向下的设计方法。合理的功能拆分、模块及模块间交互的定义是需要对系统有全局掌握,并对重要细节足够了解才能做到的,往往需要设计者有足够的设计经验,读者应从简单的系统开始,逐步累积知识和经验。

3.png


图2-3 数字逻辑系统设计流程


数字逻辑系统,特别是FPGA中的数字逻辑系统设计,按顺序一般可分为以下几个步骤(见图2-3):
  • 规格制定:定义整个系统的功能、性能指标和重要参数,这一步主要是文档编撰。
  • 上层设计:功能拆分、主要模块定义和模块间交互设计,这一步也主要是文档编撰。
  • 细节设计:主要模块内功能的实现,包括小型算法、状态机、编解码等,这一步一般包含文档编撰和初步的编码。
  • 编码:使用可综合的HDL编写代码。
  • 功能验证:使用仿真工具验证模块或系统功能,需要编写测试代码。
  • 综合:使用编译器将HDL代码转换为门、触发器级网络表,往往又分为展述(Elaboration,展开所有层次结构并建立连接)和综合(Synthesis,生成门、触发器级网络表)两步。
  • 实现(Implementation),也常常称为布局布线(Fit and Route),使用编译器将门、触发器级网络表适配到特定的器件工艺(比如FPGA芯片)中,这一步常常还需要配合一些物理、时序上的约束输入。
  • 时序验证:带有时序信息(门、线路延迟等)对布局布线结果进行验证,除验证功能正确外,还验证时序和性能合乎要求,常常也称为“门级仿真”。
    对于FPGA设计,实现过程的最后往往还包含汇编(Assembler)这一步,用于生成配置FPGA的二进制文件。

在单元模块设计时,常常也会重复“细节设计-编码-功能验证”步骤,直到模块合乎上层设计的定义为止,所有模块设计验证完成后,再连接好整个系统进行功能仿真。布局布线中也可能会出错,比如FPGA芯片资源不够,时序验证也可能失败,这些情况可能都需要修改细节设计甚至上层设计。
对于小规模的系统或独立的模块设计,从规格制定至编码未必需要细分成四步,特别是对于有经验的设计者,部分细节设计过程可以在编码时完成。而对于初学者,最好是先理清系统或模块的组织结构和各个部分的细节,做到“先有电路,再去描述”。一些底层的时序逻辑功能也可以“先有波形,再去描述”,以目标波形为参照去描述功能。
在Verilog中,并非所有的语法都可以被综合成实际电路,具体哪些语法可以被综合也取决于不同厂商的EDA工具,甚至工具的不同版本。除可综合的语法外,还有大量语法是为了支持仿真验证。本章将着重介绍主流FPGA开发工具可综合的语法,以及部分在仿真验证中常用的语法。

2.3 标识符和关键字

标识符是代码中对象(比如变量、模块等)的唯一名字,用于在代码中引用该对象。标识符只可以由字母、下划线、数字和美元符号“$”构成,并且只能由字母或下划线开头,标识符是区分大小写的。不过考虑到EDA工具及其工作环境的兼容性,非常不建议仅依赖大小写来区分不同的标识符。
关键字是Verilog预定义的,用于构建代码结构的特殊标识符,在Verilog中所有关键字都是全部小写的。代码中用户定义的标识符不能与关键字相同。IEEE 1800—2012的全部关键字见附录A。

2.4 值、数和字面量

在Verilog中,线网和变量可取的基本值有四个:

  • 0,表示逻辑0、低电平、条件假。
  • 1,表示逻辑1、高电平、条件真。
  • x或X,表示未知的逻辑值。
  • z或Z,表示高阻态。

在Verilog中,有些数据类型可以取全部4个值(称为四值),而有些数据类型仅能取0和1两个值(称为二值)。
Verilog的常数包括整型常数、浮点常数,字面量包括时间字面量、字符串字面量、结构字面量和数组字面量,为简便起见,本书将数和字面量统称为常数。
2.4.1 整型常数
整型常数除包含数值信息外,还可包含位宽信息,可以写成二、八、十或十六进制,一般形式如下:[-]<位宽>'[s| S]<进制标识><数值>其中:

  • “-”,仅用于有符号负数。
  • 位宽,十进制书写的正整数,无论后面进制标识指定什么进制,均表示二进制位的数量。
  • “'”为英文撇点(ASCII码0x27)。
  • “s”或“S”,用来指定该常数为有符号数,在Verilog中,负数被以补码形式表达。
  • 进制标识,字母b或B、o或O、d或D、h或H分别表示二进制、八进制、十进制和十六进制。
  • 数值,用进制标识指定的进制书写的无符号数值,即一串数字。

x或z可用于二、八、十六进制,每个x或z分别代表1位、3位和4位。十进制如果使用x或z,则只能有一个x或z,表达所有位均为x或z,有时为了可读性可用“?”代替z。
为了方便阅读,任意两位数字之间还可增加下划线。
对于无符号数,如果指定的位宽大于数值部分写出的宽度,高位一般将填充0,如果最高位为x或z,高位将填充x或z;如果指定的位宽小于数值部分写出的宽度,高位将舍弃。对于有符号数,如果数值超过了指定宽度能表达的范围,也将直接舍弃高位。
也有无位宽的形式:[-]['[s| S]<进制标识>]<数值>它表达至少32位二进制的数值,而至多多少位在编译时由它将赋给的对象的位数决定。如果没有进制标识,则该数必须是十进制有符号数。
还有无位宽的单一位形式:'[0| 1| x|X| z| Z]它表达无位宽的数值,具体多少位在编译时由它将赋给的对象的位数决定,将使被赋值对象的全部位赋为指定值。
代码2-1是一些整型常数的例子。

代码2-1 整型常数示例(注意左侧行编号不是代码内容)

1  //以下是指定位宽的
2  4'b1010             // 4位无符号数0b1010=10
3  6'd32 // 6位无符号数0b10000=32
4  8'b1010_0101 // 8位无符号数0b10100101=165
5  -8'sh55// 8位有符号数-0x55=-85,以“10101011”记录
6  -5'sd4 // 5位有符号数-4,以“11100”记录
7  5'sd-4 // 语法错误
8  8'd256 // 实际值为0
9  8'sd128// 实际值为-128
10  -8'sd129 // 实际值为127
11  1'sb1 // 实际值为-1!
12  3'b01z // 最低位为z(高阻态)
13  8'hx9 // 高4位为x(未知)
14  10'hz0 // 高6位均为z
15  16'd? // 全部16位为z
16  // 以下是未指定位宽的
17  'b1// 无符号数1
18  'h27EF // 无符号数0x27EF=10223
19  -'sb101// 有符号数-0b101=-5
20  127// 有符号数127
21  -1 // 有符号数-1
22  A5 // 语法错误
23  // 以下是无位宽的单一位
24  '1 // 全1,将使被赋值对象全部位填充1
25  'z // 全z,将使被赋值对象全部位填充z
2.4.2 浮点常数
Verilog中的浮点常数是遵循IEEE 754标准表达的双精度浮点常数,可以使用小数形式(必须包含小数点)或科学计数法(e或E代表10的次幂),当中也可插入下划线,如代码2-2所示。

代码2-2 浮点常数示例

1  0.0
2  3.1415926535898
3  2.99_792_458e8
4  4.2e-3
5  .80665              // 语法错误,小数点左右必须至少有一个数字(与C语言不同)
6  9. // 语法错误,同上
7  2333 // -_-#这是一个整型常数
FPGA编译工具一般不支持浮点常数参与实时运算,但支持浮点常量在编译期的运算。
2.4.3 时间常数和字符串常数
时间常数是以时间单位结尾的常数,时间单位包括s、ms、us(即μs)、ns、ps、fs。例如代码2-3:

代码2-3 时间常数示例

1  2.1ns
2  40ps
时间常数主要用于仿真验证。
字符串常数是使用双引号包裹的一串字符,在二进制中以ASCII码形式表达(每个字符8位),可以被赋给整型量,赋值时左侧字符位于高位。如:

代码2-4 字符串常数示例

1  "Hello world!\n"
字符串常数中可以用“\”引导一些特殊字符,比如 “\n”(换行)、“\t”(制表符)、“\\”(右斜杠)、“\"”(双引号)。

2.5 线网

线网和变量在Verilog中用来保持逻辑值,也可统称为值保持器。线网一般对应电路中的连线,不具备存储能力,线网使用一个或多个持续赋值或模块端口来驱动,当有多个驱动时,线网的值取决于这多个驱动的强度。
线网有以下几类:

  • wire,用于被门或持续赋值驱动的线网,可以有多个驱动源。
  • tri,用于被多个驱动源驱动的线网。
  • uwire,用于被单个驱动源驱动的线网。
  • wand、wor、triand、trior,用于实现线逻辑(线与和线或)。
  • tri0、tri1,由电阻下拉或上拉的线网。
  • trireg,容性储值的线网。
  • supply0、supply1,电源。

    4.png

表2-1 wire和tri的多重驱动

其中wire是FPGA开发中最常用的线网类型,其他线网类型多数在FPGA中无法实现,一般用于编写测试代码。
多个不同强度驱动源驱动wire和tri时的情况如表2-1所示。
多个不同强度驱动源驱动wand和triand时的情况如表2-2所示,多个同强度驱动源驱动wor和trior时的情况如表2-3所示。多个同强度驱动源驱动tri0和tri1时的情况分别如表2-4和表2-5所示。这些多重驱动情况的真值表不应死记硬背,均是有逻辑规律的。

5.png


多重驱动在FPGA设计中几乎只用于片外双向IO口,在驱动为z时,接收外部输入,在FPGA内部逻辑中并没有三态门,也不支持多重驱动。
线网定义的常用形式:<线网类型> [[<数据类型>] [signed| unsigned] [<位宽>]] <标识符>[=<赋值>] {, <标识符>[=<赋值>]};其中:
  • 线网类型为上述wire、tri等。
  • 数据类型为下述变量的数据类型中除reg外的四值类型之一,如果为确定长度的类型,则后面不能有位宽说明,如果为整型才可以使用signed或unsigned指定有无符号。
  • 位宽,按以下形式定义的位宽:[,]这里的方括号是实际的方括号,其中msb和lsb分别为最高位的索引和最低位的索引,值得注意的是,msb的值可以小于lsb,但仍然为最高位,因与通常的习惯不一致,所以一般应避免使msb小于lsb。
    如果省略数据类型,默认为logic类型,如果还省略了位宽说明,默认为1bit。

代码2-5是一些线网类型定义的示例:

代码2-5 线网类型定义的示例

1  wire a;                   // 1位线网,等价于 wire logic a;
2  wire b = 1'b1;// 1位线网,并赋常数1
3  wire c = b; // 1位线网,并连接至线网b
4  wire [6:0] d;// 7位线网
5  wire [6:0] e0 = d, e1 = '1; // 7位线网,e0连接至d,e1赋常数127
6  wire signed [7:0] f, g = -8'sd16;// 8位线网,有符号类型,g赋常数-16
7  wire integer h; // 32位线网,有符号
8  wire integer unsigned i; // 32位线网,无符号
9  wire reg j; // 语法错误,reg不能作为线网数据类型
10  wire int k; // 语法错误,数据类型不能是二值类型
11  wire struct packed {
12  logic a;
13  logic signed [7:0] b;
14  } l; // struct类型线网,总计9位
Verilog的语句以分号结尾,单行注释(到行尾)使用双左斜杠“// ”,块注释(可以跨行)使用“/”和“/”包裹。代码2-5中,前6行最为常用,初学者只需掌握这6行的形式即可。
除上述线网类型之外,还有一种专用于端口连接的线网interconnect,不能被持续赋值或过程赋值。

2.6 变量

变量是抽象的值存储单元,一次赋值之后,变量将保持该值直到下一次赋值。变量使用“过程”中的赋值语句对其赋值,变量的作用类似于触发器,不过是否形成触发器取决于代码上下文。
变量有以下几种常用数据类型:
(1)整型

  • bit,二值,默认无符号,常用于测试代码中。
  • logic,四值,默认无符号,推荐在新设计中使用。
  • reg,四值,默认无符号,SystemVerilog出现之前最常用的变量类型。
    (2)定长整型
  • byte、shortint、int、longint,二值,分别为8位、16位、32位和64位,默认有符号。
  • integer,四值,32位,默认有符号。
  • time,四值,64位,默认无符号。
    (3)浮点型
  • shortreal、real,遵循IEEE 754标准表达的浮点小数,分别为32位和64位。
  • realtime,同real。
    (4)数组

(5)结构
(6)枚举

前三者称为简单类型。而后面数组、结构和枚举称为复合类型。
变量一般由过程赋值驱动,并且不能在多个过程块中被驱动。
简单类型变量定义的常用形式:[var] [<数据类型>] [signed| unsigned] [<位宽>] <标识符>[=<初值>] {, <标识符>[=<初值>]};其中:

  • var关键字和数据类型至少要存在一个,如果未指定数据类型,则默认为logic。
  • 数据类型,如果为确定长度的类型则后面不能有位宽说明,如果为整型才可以使用signed或unsigned指定有无符号。
  • 位宽,与线网定义中一样。

注意,在使用logic关键字定义对象时,编译器会自动根据代码上下文决定该对象是logic变量还是logic类型线网,非常方便,这也是SystemVerilog推荐的定义值保持器的方法。
代码2-6是变量定义的示例:

代码2-6 变量定义的示例

1  var a;                      // 1位变量,等价于var logic a
2  logic b; // 1位变量,等价于var logic b
3  logic [11:0] c = 1234;// 12位变量,并赋初值1234
4  logic signed [19:0] d = c, e; // 20位变量,有符号,d赋初值1234
5  integer f;// 32位变量,有符号
6  integer [63:0] g; // 语法错误,定长类型不能有位宽说明
7  integer unsigned h; // 32位变量,无符号
8  reg [31:0] i; // 32位变量,无符号
9  reg signed [31:0] j; // 32位变量,有符号
10  bit [5:0] k; // 6位二值变量
11  bit signed [5:0] l; // 6位二值变量,有符号
12  byte m; // 8位二值变量,有符号
13  byte unsigned n; // 8位二值变量,无符号
14  byte [6:0] o; // 语法错误,定长类型不能有位宽说明
15  longint p;// 64位二值变量,有符号
16  longint unsigned q; // 64位二值变量,无符号

2.7 参数和常量

参数和常量在运行时值不变。在标准中,参数也是一种常量,不过参数常常用来指定模块、接口的数据位宽等,因而得名。
参数和常量均可在定义时由常数或常量表达式赋值。
参数包括以下类型:

  • parameter,可以被模块外部修改的参数,在模块实例化时修改或由defparam关键字修改。
  • localparam,不能被模块外部修改的参数。
  • specparam,专用于指定时序、延迟信息的参数。
    常量为:
  • const,类似于localparam,但可以在仿真期间更改值。

参数和常量定义的常用形式:<参数或常量类型> [<数据类型>] [signed| unsigned] [<位宽>] <标识符> = <常数或常量表达式>] {, <标识符> = <常数或常量表达式>};其中:

  • 数据类型如果为确定长度的类型则后面不能有位宽说明,如果为整型才可以使用signed或unsigned指定有无符号。
  • 位宽,与线网定义中一样。

数据类型、符号指定和位宽也可以都省略,这时参数或常量的数据类型和位宽由定义时所赋予的初始值的类型和位宽确定,而在被赋予新值时,类型和位宽会自动变化。如果没有指定位宽,默认LSB索引为0,MSB索引为位宽减一。
参数和常数的数据类型也可以是复合类型,将在后续小节介绍。
代码2-7是参数和常量定义的例子。

代码2-7 参数和常量定义的示例

1  parameter integer DW = 24;            // 32位有符号参数,值为24
2  parameter DataWidth = 24;// 同上
3  parameter WordSize = 64; // 32位有符号,值为64
4  localparam ByteSize = 8, WordBytes = WordSize / ByteSize;
5  // 两个整型参数,后者由常量表达式赋值
6  parameter Coef1r = 0.975;// 双精度浮点参数
7  localparam wire signed [15 : 0] Coef1 = Coef1r * 32768;
8   // 16位有符号参数,自动四舍五入值为31949
9  parameter c0 = 4'hC; // 4位无符号参数,值为0xC=12
10  parameter [4 : 0] c1 = 27; // 5位无符号参数,值为27
11  localparam signed [19 : 0] c2 = 101325; // 20位有符号参数
12  localparam integer unsigned c3 = 299792458; // 32位无符号参数
const g = 9.08665; // 双精度浮点常量
还可以用来定义参数化的数据类型,使用下面的格式:parameter type <标识符> = <数据类型>;然后便可以用标识符来定义线网或变量。例如代码2-8。

代码2-8 定义线网或变量

1  parameter DW = 24;
2  parameter type TData = logic signed [DW-1:0];
3  parameter type TDataL = logic signed [DW*2-1:0];
4  TData x1, x2;
5  TDataL px;
参数化的数据类型不能用defparam修改,可在模块实例化时修改。

2.8 类型和位宽转换

类型和位宽转换分为隐式转换和显式转换。赋值时,如果左值(被赋值者)和右值类型或位宽不同,编译器会自行处理转换;操作数类型如果与相关操作符对其类型要求不符,但可以转换时,编译器也会自行处理转换,这样一些转换不需要我们在代码中写出转换语句,称为隐式转换。
常见的几种隐式转换的规则:

  • 从整数转换为浮点,保持值意不变。
  • 从浮点转换为整数,会四舍五入处理。
  • 等长的有符号与无符号之间,直接位对位赋值(所以最高位为1时,表达的值意会发生变化)。
  • 从长数转换为短数,无论左右值有无符号,直接舍弃高位。
  • 从短数转换为长数,如果短数为有符号数,高位填充符号位,否则填充0。

显式转换可以使用Verilog系统函数$cast()、$signed()和$unsigned(),或使用类型转换表达式。系统函数是Verilog内置的函数,大部分不能综合成实际电路,主要用于编写仿真测试代码。
$cast()函数用于转换并赋值,如$cast(a, b),将把b转换为a的类型并赋值给a。$cast()函数带有返回值,返回0表示无法转换,否则表示转换成功。$cast()函数不能被综合,仅可用于仿真测试代码。
$signed(a)、$unsigned(a)函数将a转换为有符号或无符号,可被综合,有无符号的转换并不对a进行任何操作,实际影响的是相关操作符或位宽转换时高位填充什么。
类型转换表达式的常用形式是:(<目标类型>| <位宽>| signed| unsigned)'(<待转换内容>)其中,目标类型应为简单变量类型,位宽是常数或常量表达式。
代码2-9是一些类型转换的例子。

代码2-9 类型转换的示例

1  logic [3:0] a = 4'he;
2  logic [1:0] b = a;                      // b=2'b10
3  logic [5:0] c = a; // c=6'b001110
4  logic [5:0] d = 6'(a); // 同上
5  logic [5:0] e = 6'($signed(a));// e=6'b111110
6  logic signed [7:0] f = 4'sd5 * a; // f=70
7  logic signed [7:0] g = 4'sd5 * $signed(a); // g=-10
其中第2行隐式转换位宽,长数赋给短数,舍弃高位。第3行隐式转换位宽,短无符号数赋给长数,填充0;第4行显式转换,结果与第3行相同。
第5行将a转换成有符号数(-4'sh2)之后再扩展位宽,将填充符号位1最后赋给无符号数,结果为6'h3e。
第6行有符号数5和无符号数14相乘,按8位无符号计算(见下节),结果为70;第7行先将a转换为有符号数(-2),再与5相乘,结果为-10。

2.9 操作符和表达式

表达式由操作符和操作数构成,整个表达式代表了操作数经过运算后的结果。Verilog中可以用作操作数的包括。

  • 常数和字面量。
  • 参数及其中的一位或多位。
  • 线网、变量及其中的一位或多位。
  • 结构、联合及其成员,对齐的结构中的一位或多位。
  • 数组及其中的元素,对齐的数组中的一位或多位。
  • 返回上述内容的函数或方法。

Verilog中操作符的详情见表2-6。
表2-6 Verilog操作符的功能、优先级、结合方向和操作数

6.png


7.png


关于表2-6和一些运算规则(如有冲突,靠前的规则优先)如下。
1)表中优先级数值越小优先级越高,同优先级操作符按结合方向区分先后,结合方向中的“左”意为自左向右、“右”意为自右向左。
2) 位选取、位拼接、流运算的结果为无符号数,无论操作数是否有符号。
3) 比较(含相等、不等)、非按位逻辑运算、缩减运算的结果均为1位无符号。
4) 比较(含相等、不等)运算:
  • 如果操作数中有浮点数,则另一个将被转换为等值浮点数进行比较。
  • 仅当两个整型操作数均为有符号数时,比较按有符号进行,否则按无符号进行。
  • 如果两个整型操作数位宽不一致,短操作数将被补充高位。
    5) 逻辑非、与、或、隐含和等价运算,操作数为0等价于1'b0(假),操作数非0等价于1'b1(真)。

6) 算术运算和按位逻辑运算符:

  • 如果操作数中有浮点数,则另一个将被转换为等值浮点进行运算。
  • 表中整型结果位数均是指运算后不立即赋值时的情况,如果立即赋值,则以被赋值对象的位数为准。
  • 如果操作数中有无符号数,则运算按无符号进行,结果为无符号数。
  • 如果操作数均为有符号数,则运算按有符号进行,结果为有符号数。
  • 如果有操作数位数不够,则补充高位。
    7) 短操作数补充高位的规则:
  • 无符号数补充0。
  • 有符号常数补充符号位。
  • 有符号线网、变量和常量在操作按有符号进行时,补充符号位,否则补充0。

上述规则比较烦琐,特别是在有无符号和长短不一的操作数混合在一起的时候。我们在编写Verilog代码的时候,尽量避免混合不同格式操作数的运算,在不可避免的时候再来考虑应用这些规则。读者需了解有这样一些规则,遇到问题时方便查阅,但在初学时不需要熟记它们。
代码2-10是有关位宽的一些例子。

代码2-10 运算位宽的相关示例

1  logic [3:0] a = 4'hF;
2  logic [5:0] b = 6'h3A;
3  logic [11:0] c = {a*b};   // c的值为38
4  logic [11:0] d = a*b;// d的值为870
5  logic signed [15:0] a0 = 16'sd30000, a1 = 16'sd20000;
6  logic signed [15:0] sum0 = (a0 + a1) >>> 1;      // sum0=-7768
7  logic signed [15:0] sum1 = (17'sd0 + a0 + a1) >>> 1; // sum1=25000
8  logic signed [15:0] sum2 = (17'(a0) + a1) >>> 1; // sum2=25000
其中,第3行因计算后没有立即赋值而是先位拼接,因而按6位计算,0xf×0x3a=15×58=870,870取低6位为38;而第4行运算后直接赋给12位变量d,因而按12位计算,结果为870。
第6行,本意是取a0和a1的平均值,但a0和a1相加时并未立即赋值,而是先右移,因而加法按16位计算,其和因溢出得到-15536,再右移1位得到-7768,与本意不符;而第7行的加法自左向右结合,先计算17'sd0+a0,得到结果17'sd30000,再与a1相加,得到结果17'sd50000,避免了溢出,最后右移仍然得到17位结果25000,赋给16位sum1时舍弃最高位,结果符合意图;第8行则使用类型转换达到了意图。
代码2-11是混合不同符号和位宽的操作数的一些例子。

代码2-11 混合不同符号和位宽的相关示例

1  logic [7:0] a = 8'd250;                   // 8'd250=8'hFA
2  logic signed [3:0] b = -4'sd6; // -4'sd6(4'hA)
3  logic c = a == b;// c=0
4  logic d = a == -4'sd6; // d=1
5  logic e = 8'sd0 > -4'sd6;// e=1
6  logic f = 8'd0 < b; // f=1
7  logic [7:0] prod0 = 4'd9 * -4'sd7; // prod0=193
8  logic signed [7:0] prod1 = 4'd9 * -4'sd7;// prod1=-63
9  logic [7:0] prod2 = 8'd5 * b;// prod2=50
10  logic [7:0] prod3 = 8'd5 * -4'sd6; // prod3=226
第3行,因a是无符号数,比较按无符号进行,b将被高位填充0到8位得到8'h0a后与a比较,显然不相等,结果为0。第4行,按无符号进行,但有符号常数高位填充符号位,得到8'hfa后与a比较,两者相等,结果为1。
第5行,比较按有符号进行,结果为1。第6行,比较按无符号进行,b被高位填充0,得到8'h0A与8'h0比较,结果为1。
第7行,乘法按无符号进行,4'd9被填充成8'd9,-4'sd7高位填充1,得到8'd249,与9相乘取8位,得到193。第8行与第7行一样,结果为193,但最后赋给有符号数,得到数值-63,可以看到,这里有符号数和无符号数相乘,按无符号相乘,结果与按有符号相乘是一致的。
第9行,乘法按无符号进行,b被高位填充0,得到8'd10与8'd5相乘,结果为50。第10行,乘法按无符号进行,-4'sd6被高位填充符号位,得到8'hfa=250,与5相乘取8位,得到226。
表2-6中大部分操作符,特别是算术运算符的功能读者应该都能直接理解,下面几节将介绍一些初学者不易理解的操作符。
2.9.1 位选取操作符
位选取操作符用于选取多位数据中的1位或多位。无论原数据是否有符号,结果均为无符号。位选取操作符有两种使用形式,第一种使用MSB和LSB:<操作数>[ : ]MSB表达式和LSB表达式必须为常量表达式,如果操作数本身在定义的时候MSB索引大于LSB索引,则MSB表达式的值应不小于LSB表达式的值,如果操作数本身MSB索引小于LSB索引,则MSB表达式的值应不大于LSB表达式的值。
第二种使用M/LSB和位宽:<操作数>[ (+:|-:) <位宽表达式>]使用“+:”时,M/LSB表达式为MSB和LSB中较小的一个,使用“-:”时,M/LSB表达式为MSB和LSB中较大的一个。
M/LSB表达式可以是变量表达式,而位宽表达式则必须是常量表达式。
代码2-12是一些例子。

代码2-12 位选取操作符的示例

1  logic [15:0] a = 16'h5e39;          // 16'b0101_1110_0011_1001
2  logic b = a[15], c = a['ha]; // b=1'b0, c=1'b1
3  logic [3:0] d = a[11:8], e = a[13:10]; // d=4'b1110, e=4'b0111
4  logic [7:0] f = a[7:0], g = a[2*4:1]; // f=8'h39, g=8'b0001_1100
5  logic [7:0] h = a[4+:8], i = a[15-:8]; // h=8'he3, i=8'h5e
6  logic [3:0] j;
7  logic [2:0] k = a[j+2:j]; // 语法错误,索引不能为变量表达式
8  logic [2:0] l = a[j+:3];// 假设j=3, l=3'b111
9  ...
10  a[7:4] = 4'h9; // a=16'h5e99
11  a[4] = 1'b0;// a=16'h5e89 
12  ...
注意第10、11行,赋值语句实际需要放置在过程块中,这里只是示意,表示位选取操作可以作为赋值语句的左值。
2.9.2 位拼接和流运算符
位拼接运算符用于将多个数据拼接成更长的数据,常用形式如下:{<操作数1>{, <操作数2>}}花括号中可以有一个或多个操作数,多个操作数间以逗号隔开,无论操作数有无符号,位拼接的结果都是无符号数。左侧的操作数会放置在结果的高位。
还有将一个或多个操作数重复多次的形式:{<重复次数>{<操作数1>{, <操作数2>}}}其中重复次数必须是非负的常数或常量表达式。
代码2-13是位拼接操作符的一些例子。

代码2-13 位拼接操作符的示例

1  logic [7:0] a = 8'hc2;            // a=1100_0010
2  logic signed [3:0] b = -4'sh6;// b=4'b1010=4'ha
3  logic [11:0] c = {a, b}; // c=12'hc2a
4  logic [15:0] d = {3'b101, b, a, 1'b0};// d=16'b101_1010_1100_0010_0
5  logic [63:0] e = {4*4{b}};// e=64'haaaa_aaaa_aaaa_aaaa
6  logic [17:0] f = {3{b, 2'b11}}; // f=18'b101011_101011_101011
7  logic [15:0] g = {a, {4{2'b01}}}; // g=16'hc255
8  ...
9  {a, b} = 12'h9bf; // a=8'h9b, b=4'hf=-4'sh1
10  ...
第9行的赋值语句实际需要放置在过程块中,这里只是示意,表示位拼接操作可以作为赋值语句的左值。
流操作符用于将操作数按位、按几位一组或按元素重新组织次序,常用的形式为:{(<<| >>)[<宽度>| <类型>]{<操作数1>{, <操作数2>}}}其中宽度为常数或常量表达式,类型可以是2.6节中定长整型中的一种。代码2-14是流操作符的例子。

代码2-14 流操作符的示例

1  logic [15:0] a = 16'h37bf;          // 16'b0011_0111_1011_1111
2  logic [15:0] b = {>>{a}}; // b=16'h37bf
3  logic [15:0] c = {<<{a}}; // c=16'hfdec=16'b1111_1101_1110_1100
4  logic [19:0] d = {<<{4'ha, a}}; // d=16'hfdec5
5  logic [15:0] e = {<< 4 {a}}; // e=16'hfb73
6  logic [15:0] f = {<< 8 {a}}; // f=16'hbf37
7  logic [15:0] g = {<< byte {a}}; // g=16'hbf37
8  logic [8:0] h = {<< 3 {{<< {9'b110011100}}}};    // h=9'b011_110_001
9  logic [3:0] i;
10  ...
11  {<<{i}} = 4'b1011; // i=4'b1101
12  {<< 2 {i}} = 4'b1011;// i=4'b1110
13  ...
其中第2行将a从左至右,即从高位到低位逐位排列,得到结果与a本身一致,第3行将a从右至左逐位排列,得到按位反向的结果。第4行将4'ha=4'b1010与a拼接,然后按位反向。
第5行,将a逐4位一组反向;第6行,将a逐8位一组反向,第7行效果与第6行一致。
第8行,将9'b110_011_100逐位反向后得到9'b001_110_011,然后3位一组反向,得到9'b011_110_001,整体可以理解为3位一组,组内按位反向,组间位置不变。
第11、12行,赋值语句实际应放在过程块中,这里只是示意,表示流操作可以作为赋值的左值。
2.9.3 按位逻辑运算符
按位逻辑运算符包括&(与)、|(或)、^(异或)、~&(与非)、~|(或非)、~^、^~(同或),是二元操作符,形式上与一元的缩减运算符是一样的,具体是按位逻辑运算符还是缩减运算符取决于左侧是否有操作数。
按位逻辑运算符将两个操作数的各个位一一对应作逻辑运算,如果两个操作数位宽不一致,则较短的那个将被补充高位,运算后得到的结果与两个操作数中较长者的位宽相同。按位逻辑运算符的作用容易理解,这里不举例,不过在有x和z参与时,情况稍稍复杂。
表2-7和表2-8是“与”和“或”逻辑运算符在有x和z参与运算时的情况。

8.png


表2-9和表2-10是“异或”和“非”逻辑运算符在有x和z参与运算时的情况。
其他几个运算符的情况可以由上述四个组合而来。
2.9.4 缩减运算符
缩减运算符包括&(与)、|(或)、^(异或)、~&(与非)、~|(或非)、~^、^~(同或),是一元操作符。
缩减运算符将操作数中的所有位逐个进行逻辑运算(每次结果继续跟下一位进行逻辑运算),得到1位输出。表2-11是缩减运算符的例子。其中异或缩减和同或缩减的作用相当于检测操作数中1的个数是奇数或偶数。

表2-11 缩减运算符的例子操作


9.png


如果操作数中有x、z,则规则与按位逻辑运算符一致。
2.9.5 移位
移位运算符分为逻辑移位运算符(“<<”和“>>”)和算术移位运算符(“<<<”和“>>>”),它们将左侧的操作数按位左或右移动右侧操作数指定的位数。逻辑左移“<<”和算术左移“<<<”将左侧操作数左移,高位舍弃,低位填充0,两个功能一致。逻辑右移“>>”和算术右移“>>>”将左侧操作数右移,低位舍弃,逻辑右移移出的高位将填充0;而算术右移移出的高位在左侧操作数为无符号数时填充0,为有符号数时填充符号位。代码2-15是移位操作符的例子。

代码2-15 移位操作符的示例

1  logic [7:0] a = 8'h9c;                 // 8'b10011100 = 156
2  logic signed [7:0] b = -8'sh64; // 8'b10011100 = -100
3  logic [7:0] c = a << 2; // c=8'b01110000
4  logic [7:0] d = b << 2; // d=8'b01110000
5  logic [7:0] e = b <<< 2; // e=8'b01110000
6  logic [7:0] f = b >> 2; // f=8'b00100111 = 39
7  logic [7:0] g = a >>> 2; // g=8'b00100111 = 39
8  logic [7:0] h = b >>> 2; // h=8'b11100111 = -25
9  logic [7:0] i = 9'sh9c; // i=9'b010011100
10  logic [7:0] j = i >>> 2; // j=8'b00100111
其中,第8、10行右移时,因左侧操作数为有符号数,高位填充符号位,分别为“1”和“0”。
算术左移运算a<<>>b,可理解为a/2b。
2.9.6 自增赋值和自减赋值
自增/自减运算符可写成这样几种形式:a++、a--、++a、--a,如果它们自成一句,则表示a值加1或减1赋回给a,它们实际上也是赋值语句的一种。如果它们是表达式中的一部分, 则运算符位于操作数左侧表示先自增/减,再参与表达式计算;而运算符位于操作数右侧则表示操作数先参与表达式计算,表达式完成之后再自增/减。代码2-16是自增/自减的一些例子。

代码2-16 自增/自减示例

1  logic [3:0] a = 4'h3;
2  logic [3:0] b;
3  ...
4  a++;                           // a=4
5  a--; // a=3
6  b = 4'd1 + a++; // b=4, a=4
7  b = 4'd1 + ++a; // b=6, a=5
8  ...
第6行,a的原值3先与1相加赋给b,然后a自增;第7行,a先自增得到5,再与1相加赋给b。
注意,在Verilog标准里并没有规定类似:b = a++ + (a = a - 1);这样有多个赋值在一个语句中的情况,赋值和运算的先后顺序、b最后的值会是多少取决于不同编译器的实现。我们应避免写出这样的语句。
2.9.7 条件判断相关运算符
逻辑非(!)、与(&&)、或(||)、隐含(->)和等价(<->)都将操作数当作1位无符号来处理,0值当作1'b0,意为“假”,非0值当作1'b1,意为“真”。如操作数中包含x或z,则当作1'bx。1'bx与其他1位值的逻辑运算同按位逻辑运算规则一致。
对于与和或运算,如果左侧操作数已经能决定结果(在“与”中为0或在“或”中为1),则右侧表达式将不会被求值(意为当中如有赋值、函数都不会执行)。而对于隐含和等价运算,无论如何,两侧表达式均会被求值。
隐含运算(->):<操作数1> -> <操作数2>等价于:(!<操作数1> || <操作数2>)即操作数1为假或操作数2为真,如0->1、0->0、1->1、0->1'bx、1'bx->1的结果均为1'b1。
而等价运算(<->):<操作数1> <-> <操作数2>等价于:((<操作数1> -> <操作数2>) && (<操作数2> -> <操作数1>))相当于操作数1和操作数2同为真或同为假,如0<->0、1<->1的结果为1'b1。
算术比较运算符(<、<=、>=、>)比较简单,根据字面意义理解即可。如果算术比较运算的操作数中含有x或z,则结果均为1'bx。
相等、不等和条件相等、条件不等运算符的区别在于对x和z的处理。在相等、不等运算符中,当x或z引起结果不确定时,结果为1'bx;而在条件相等、条件不等运算符中,x和z位也参与比对。
通配相等和通配不等运算a ==? b和a !=? b中,操作数b中的x或z将当作通配符,不参与比较。
代码2-17是条件判断中与x和z相关的例子。

代码2-17 条件判断中带有x和z的例子

1  logic a = 4'b0010 || 2'b1z;                // a = 1'b1 | 1'bx = 1'b1
2  logic b = 4'b1001 < 4'b11xx; // b = 1'bx
3  logic c = 4'b1001 == 4'b100x;// c = 1'bx
4  logic d = 4'b1001 ! = 4'b000x;// d = 1'b1
5  logic e = 4'b1001 === 4'b100x; // e = 1'b0
6  logic f = 4'b100x === 4'b100x; // f = 1'b1
7  logic g = 4'b1001 ==? 4'b10xx; // g = 1'b1
8  logic h = 4'b1001 !=? 4'b11??; // h = 1'b1,?即为z
9  logic i = 4'b10x1 !=? 4'b100?; // i = 1'bx
第1行或运算,未知值与1“或”的结果为1。第2行,虽然无论4'b11xx的后两位是什么值都大于4'b1001,但根据标准,结果仍为1'bx。第3行,x 会影响结果,因而结果为1'bx。第4行,前几位已经不相同了,因而结果为1'b1。
第7、8行,仅比较高两位。第9行,比较前三位,左侧x导致结果不明确,因而结果为1'bx。
属于集合(inside)运算也是条件判断的一种,判断inside左侧操作数是否属于右侧集合。一般形式如下:<表达式> inside {<集合>}其中的集合可以是逗号分隔的元素列表(<元素1>, {<元素i>})、范围([<下限>, <上限>])或数组,也可以是它们的任意组合。例如,1 inside {1, 2, 3, [5:9]} 为1'b1;4 inside {1, 2, 3, [5:9]} 为1'b0;6 inside {1, 2, 3, [5:9]} 为1'b1。
2.9.8 条件运算符
条件运算符的使用格式如下:<表达式1> ? <表达式2> : <表达式3>如果表达式1为真,则返回表达式2的值,否则返回表达式3的值,未被返回值的表达式不会被求值。

代码2-18 条件运算符示例

1  logic [2:0] grade = (score >= 90) ? 4 :
2  (score >= 80) ? 3 :
3  (score >= 70) ? 2 :
4  (score >= 60) ? 1 : 0;
注意条件运算符是自右向左结合的,因此可以有代码2-18所示的例子。
这个例子并不是将“(s >= 90) ? 4 : (s >= 80)”作为一个整体当作第二个“?”的条件,而是自右向左结合,因而第2行及以后的内容均为第一个条件运算符的“表达式3”。以此类推。
2.9.9 let语句
let语句用来定义表达式模板,带有简单的端口列表,可理解为带参数的表达式,可在其他表达式里使用它。一般形式是:let <表达式名>(<端口1>{, <端口i>}) = <引用端口的表达式>;代码2-19是let语句的一些例子。

代码2-19 let语句示例

1  let max(a, b) = a > b ? a : b;
2  let abs(a) = a > 0 ? a : -a;
3  logic signed [15:0] a, b, c;
4  ...
5  c = max(abs(a), abs(b));
6  ...
let语句看起来与后面将讲到的带有参数的宏定义编译指令(define)类似,但let语句有作用域,而带参数的define则是全局有效的。对于带参数的表达式的定义,应尽量使用let语句。

2.10 结构和联合

结构是多个相关量的集合,可以作为整体引用,也可引用其中的单个成员。
结构分为不紧凑的(unpacked)和紧凑的(packed),默认是不紧凑的。不紧凑的结构可以包含任意数据类型,成员之间可能会因为要对齐到整字节而出现间隙。具体对齐方式标准中没有定义,取决于编译器,因而不能整体当作简单类型参与相关运算。而对齐的结构的内部成员之间没有位间隙,整体可以理解为一个多位数据,先定义的成员位于高位,可以整体参与算术或逻辑运算,可以指定有无符号。
代码2-20是结构定义的例子。

代码2-20 结构定义示例

1  struct {
2  logic signed [15:0] re;
3  logic signed [15:0] im;
4  } c0, c1;
5  struct {
6  time t; integer val;
7  } a;

代码2-21 使用typedef关键字定义结构类型

1  typedef struct {
2  logic signed [15:0] re;
3  logic signed [15:0] im;
4  } Cplx;
5  Cplx c0, c1;
6  wire Cplx c2 = c0;
代码2-20中定义了两个匿名的结构,并同时定义了该结构类型的变量。
也可以使用“typedef”关键字为结构类型命名,而后使用该类型名定义变量或线网,如代码2-21所示。
其中第1~4行定义了名为Cplx的类型,第5行定义两个Cplx类型的变量c0和c1,第6行则定义了Cplx类型的线网,并与c0相连接。
结构既可以整体引用或赋值,也可以使用成员运算符“.”选取内部成员引用或赋值。对结构整体的赋值可使用结构常数,一般形式如下:'{<成员1值>{, <成员i值>}}事实上,其中成员的值也可以是变量,理解为“常形式、变成员”。
代码2-22是结构和成员访问的例子。

代码2-22 结构和成员访问示例

1  logic signed [15:0] a = 16'sd3001;
2  logic signed [15:0] b = -16'sd8778;
3  Cplx c0, c1, c2;                 // c0=c1=c2='{x,x}
4  wire Cplx c3 = c1; // c3=c1='{x,x}
5  wire Cplx c4 = '{a, b};// c4={3001,-8778}
6  ...
7  c0.re = 16'sd3001; // c0='{3001,x}
8  c0.im = b; // c0='{3001,-8778}
9  c1 = '{16'sd3001, -16'sd8778}; // c3=c1={3001,-8778}
10  c2 = '{a, -16'sd1};// c2={3001,-1}
11  c2 = '{c2.im, c2.re}; // c2={-1,3001}
12  a = 16'sd1;// c4={1,-8778}
13  ...
注意,线网类型是随着驱动它的变量的变化而变化的。
对齐的结构使用“packed”关键字定义,还可以使用signed或unsigned关键字指定当作整体运算时是否有符号。代码2-23是对齐的结构相关的例子。

代码2-23 紧凑型结构示例

1  typedef struct packed signed {
2  logic signed [15:0] re;
3  logic signed [15:0] im;
4  } Cplx;
5  Cplx c0 = {16'sd5, -16'sd5};
6  logic signed [15:0] a = c0.re;       // a=5
7  logic signed [15:0] b = c0[31:16];// b=5
8  logic [3:0] c = c0[17:14];// c=4'b0111
9  Cplx c1 = {<<16{c0}}; // c1='{-5,5}
联合与结构类似,只不过其中的成员共用值保持单元(线网或变量的位)。联合分为紧凑和不紧凑两类。不紧凑的联合的对齐方式在标准中没有定义,因而视编译器不同,有可能各成员未必严格共用值保持单元。FPGA编译工具一般不支持不紧凑的联合。
代码2-24是联合的例子。

代码2-24 联合示例

1  typedef union packed {
2  logic [15:0] val;
3  struct packed {
4  logic [7:0] msbyte;
5  logic [7:0] lsbyte;
6  } bytes;
7  } Abc;
8  Abc a;
9  ...
10  a.val = 16'h12a3;               // a.byte.msbyte=8'h12, lsbyte=8'a3
11  a.bytes.msbyte = 8'hcd; // a.val=16'hcda3
12  ...
联合Abc中,val和bytes结构占用相同单元,因而给a.val赋值时,a.bytes中的内容同时变化,而给a.bytes.msbyte赋值时,a.val的高字节同时变化。
联合还有带标签的类型(tagged),使用额外的位记录标签,指示联合中的当前有效成员。其可整体赋值,赋值的同时使用tagged关键字设定有效成员并对其赋值。访问时,只能访问当前有效成员。FPGA编译器对带标签的联合的支持有一定限制。
代码2-25是带标签的联合的例子。

代码2-25 带标签的联合示例

1  typedef union tagged {
2  logic [31:0] val;
3  struct packed {
4  byte b3, b2, b1, b0;
5  } bytes;
6  } Abct;
7  Abct ut;
8  logic [31:0] c;
9  byte d;
10  ...
11  ut.val = 32'h7a3f5569;              // 无效语句
12  ut = tagged val 32'h1234abcd; // 被赋值,并标记val为有效成员
13  d = ut.bytes.b0;// 无效语句
14  ut = tagged bytes '{'h11, 'h22, 'h33, 'h44};
15  // 被赋值,并标记bytes为有效成员
16  d = ut.bytes.b0;// 有效访问,d=8'h44
17  ...
第11行,因ut还没有被标记为有效成员,因而不能被赋值;第13行,当前有效成员为val,因而不能访问bytes。

2.11 数组

数组是一系列变量或线网的顺序组合。数组有以下几种常用的定义形式:<简单整型> [<位索引1范围>]{[<位索引i范围>]} <数组名>;
<简单整型> {[<位索引范围>]} <数组名>[<元素索引1范围>]{[<元素索引i范围>]};
<复合类型> <数组名>[<元素索引1范围>]{[<元素索引i范围>]};其中第一种定义的数组称为紧凑(packed)数组。事实上,多位的logic、bit本身就是紧凑数组的一种,位选择也就是数组元素选择。多索引,即多维的紧凑数组本身相当于一个长整型数据,可以当作一个整型数据使用。
后两种是非紧凑数组,复合类型可以是结构、联合等。即使是整型非紧凑数组,一般也能当作整型整体使用。但任何数组或数组中的一段连续部分都可以用对等的类型整体赋值。
位索引范围的写法与变量位宽定义的写法一样,元素索引范围的写法除了可以与变量位宽定义的写法一样,还可以只写明元素数量。引用数组元素与变量的位选取写法一样。
对于形如:<类型> [i - 1 : 0][j - 1 : 0] <数组名> [0 : m - 1][0 : n - 1];等价于<类型> [i - 1 : 0][j - 1 : 0] <数组名> [m][n];一般称它有m×n个元素,每个元素为i×j位。访问时:<数组名> [m - 1][n - 1]是它的最后一个元素,有i×j位。<数组名> [m - 1][n - 1][i - 1]是它的最后一个元素的最高一个j位组。<数组名> [m - 1][n - 1][i - 1][j - 1]是它的最后一个元素的最高位。
数组的赋值可使用数组常数,与结构常数类似。
代码2-26是数组的一些例子。

代码2-26 数组示例

1  logic [3:0][7:0] a[0:1][0:2] ='{
2  '{32'h00112233, 32'h112a3a44, 32'h22334455},
3  '{32'h33445566, 32'h4455aa77, 32'hf5667788}};
4  logic [31:0] b = a[0][2];          // 32'h22334455;
5  logic [15:0] c = a[0][1][2:1]; // 16'h2a3a;
6  logic [7:0] d = a[1][1][1]; // 8'haa;
7  logic [3:0] e = a[1][2][3][4+:4];// 4'hf;
8  ...
9  a[0][0][3:2] = a[1][0][1:0]; // a[0][0]=32'h55662233
10  ...

2.12 赋值、过程和块

赋值用来驱动线网或变量,驱动线网可理解为构建线网的连接,驱动变量则是赋予变量新的值。Verilog中赋值主要有两种:
1) 持续赋值,持续赋值使得线网持续接收右值,相当于直接连接到组合逻辑的输出。
2) 过程赋值,只是在特定事件发生时更新变量的值,而变量则会保持该值直到下一次更新。
此外还有过程持续赋值,但其在FPGA设计中并不常用,这里不作介绍。持续赋值的左值只能是线网,而过程赋值的左值只能是变量。
持续赋值有两种形式:在线网定义时赋值和使用assign语句赋值。在线网定义时赋值在前面几节的例子中已经出现很多了。assign语句赋值的一般形式如下:assign <线网名1> = <表达式1>{, <线网名i> = <表达式i>};过程赋值只能位于过程中。过程是受一定条件触发而执行的代码结构,在Verilog中,有以下几种过程。
1) initial过程,在启动(仿真开始或实际电路启动)时开始,执行一次。initial多用于仿真,其中的内容根据编译器和FPGA的具体情况,部分可综合。一般在编译期处理,用于初始化寄存器的上电值或初始化存储器的内容。
2) always过程,又分为以下几种。

  • always过程,可指定或不指定触发事件。不指定触发事件时,从启动时开始周而复始地执行。指定触发事件时,在事件发生时执行。
  • always_comb过程,专用于描述组合逻辑,在过程内的语句所依赖的任何一个值发生变化时执行,输出随着输入的变化而立即变化,正是组合逻辑的行为。
  • always_latch过程,专用于描述锁存器逻辑,由指定的线网或变量的值触发内部语句执行,在FPGA中应避免出现锁存器,因而,本书不会专门介绍always_latch过程。
  • always_ff过程,专用于描述触发器逻辑(ff是触发器flip-flop的缩写),当指定的线网或变量(即时钟)出现指定跳沿时执行。
    3) final过程,在仿真结束时执行一次。

4) task过程,即任务。
5) function过程,即函数。

任务和函数将在后续小节讲到。
过程中的语句常常不止一句,Verilog使用块组合多条语句,块有两种:
1) 顺序块,使用begin-end包裹,其中的多条语句是逐条执行的。
2) 并行块,使用fork-join包裹,其中的多条语句是同时执行的。
顺序块是可综合的,而并行块一般是不可综合的,多用于编写测试代码。
2.12.1 赋值的延迟
实际电路是有传输延迟的。在Verilog中,可在赋值语句中增加延迟,以模拟实际情况或产生需要的时序。延迟常用于编写测试代码,对实际电路来说是不可综合的,在综合过程中会忽略代码中的延迟。
在赋值语句中指定延迟有以下几种常用形式:#<时间> <变量> = <表达式>;
<变量> = #<时间> <表达式>;
assign #<时间> <线网> = <表达式>;前两种延迟赋值在顺序块(begin-end)中,语句本身占用执行时间。第三种形式意为表达式的值变化之后,线网受到其影响将延迟。
在定义线网时,也可以指定线网的延迟,意为任何驱动该线网的变化都将延迟:<线网类型> [<数据类型符号位宽>] #<时间> <标识符>...;代码2-27是关于延迟的例子。

代码2-27 begin-end块中的延迟示例

1  logic [7:0] a = 8'd0, b = 8'd0;
2  wire [7:0] #5ns c = a;
3  wire [7:0] d;
4  assign #2ns d = c;
5  initial begin
6  #10ns a = 8'd10;
7  #20ns a = 8'd20;
8  b = #10ns 8'd30;
9  b = #20ns 8'd40;
10  #30ns a = 8'd30;
11  end
第2行定义c,并使得c随着a的变化而变化,但延迟5ns;第4行将d连接到c,随着c的变化而变化,但延迟2ns,相对于a则延迟7ns。
第5~11行是initial过程,其内容为一个begin-end块。在仿真启动时,块内容开始执行。10ns时a值变为10,于是15ns时c变为10,17ns时d变为10;30ns时,a值变为20;40ns时,b变为30;60ns时,b变为40;90ns时,a变为30。其波形如图2-4所示。

10.png


图2-4 代码 2-27的波形

如果将其中的begin-end块换为fork-join块,如代码2-28所示。
第6~10行语句将同时执行。a在10ns时变为10,在20ns时变为20,在30ns时变为30;b在10ns时变为30,在20ns时变为40。波形如图2-5所示。

代码2-28 fork-join中的延迟示例

1  ...
5  initial fork
6  #10ns a = 8'd10;
7  #20ns a = 8'd20;
8  b = #10ns 8'd30;
9  b = #20ns 8'd40;
10  #30ns a = 8'd30;
11  join

11.png


图2-5 代码2-28的波形


在Verilog中,赋值中除了可指定延迟外,还能指定触发事件,后续章节将有提及,这里不专门介绍。
2.12.2 赋值的强度
在2.5节中提到了驱动强度。驱动强度在FPGA设计里是不能综合的,但对编写测试代码比较有用,初学者不必熟记本节知识。Verilog中驱动强度有以下几种:
1) 1的强度,由强到弱:supply1、strong1、pull1、weak1、highz1。
2) 0的强度,由强到弱:supply0、strong0、pull0、weak0、highz0。
如果要对线网指定驱动强度,可以在线网定义时:<线网类型> (<1的强度>, <0的强度>) [<数据类型符号位宽>] <标识符>...;也可在持续赋值时:assign (<1的强度>, <0的强度>) <线网名1>...;其中1的强度和0的强度不能同为highz。线网被不同强度驱动时,以最强的为准;当有多个最强强度时,则根据2.5节多重驱动的规则而定。
代码2-29是有关驱动强度的例子。

代码2-29 驱动强度的示例

1  logic [1:0] data = '1;
2  logic pup = '0;
3  wire (pull1, highz0) sda = pup;
4  assign (highz1, strong0) sda = data[0];
5  assign (highz1, strong0) sda = data[1];
6  initial begin                  // pup + data[0] + data[1]
7  #10ns data[0] = '0; // sda: hz0 + st0 + hz1 = st0
8  #10ns data[1] = '0; // sda: hz0 + st0 + st0 = st0
9  #10ns data = '1;// sda: hz0 + hz1 + hz1 = hz
10  #10ns pup = '1; // sda: pu1 + hz1 + hz1 = pu1
11  #10ns data[0] = '0; // sda: pu1 + st0 + hz1 = st0
12  #10ns data[1] = '0; // sda: pu1 + st0 + st0 = st0
13  end
第7行,10ns时,驱动sda的三个源分别是:pup为highz0、data[0]为strong0、data[1]为highz1,所以结果为strong0;第9行,30ns时,全部三个源为highz,所以结果为highz;第10行,一个pull1、两个high1,所以结果为pull1;第11行,data[0]为strong0,所以结果为strong0。
2.12.3 流程控制语句
在过程中,除了赋值,更多的逻辑功能使用流程控制语句实现。常用的流程控制语句如下。
1) if-else语句。
2) case语句,包括:

  • case语句
  • casez语句
  • casex语句
    3) 循环语句,包括:
  • forever语句
  • repeat语句
  • while语句
  • do-while语句
  • for语句
  • foreach语句

if-else语句的形式一般是:[unique| unique0| priority] if(<条件表达式1>) <单一语句或块1>
{else if(<条件表达式i>) <单一语句或块i>}
[else <单一语句或块j>]其中的语句和块也可以是仅包含一个分号的空语句。上述形式的意义是:如果条件表达式1为真,则执行语句或块1;否则如果条件表达式i为真,则执行语句或块i,有多个else if的以此类推;最后,如果前面的条件表达式均为假,则执行语句或块j。一旦任何一个语句或块被执行,整个if-else语句结束,因而,if-else语句中的条件表达式将逐一求值,直到遇到值为真的表达式,则后续表达式将不被求值。
case语句的一般形式是:[unique| unique0| priority](case| casez| casex)(<条件表达式>) [inside| match]
<条件项1>{, <条件项2>}: <单一语句或块1>
{<条件项i>{, <条件项i+1>}: <单一语句或块i>}
[default: <单一语句或块j>]
endcase其中条件表达式和条件项可以但只能有一个是常数或常量表达式。意为如果条件表达式与条件项1匹配,则执行语句或块1;如果条件表达式与条件项2匹配,则执行语句或块2;以此类推,如果没有匹配,则指定语句或块j。熟悉C语言的读者需注意,Verilog的case本身不能穿越条件,因而没有也不需要break语句。
case语句的匹配需要条件表达式和条件项中的z和x均一致。
casez则将条件表达式和条件项中的z(?)视为无关,不参与匹配。
casex则将条件表达式和条件项中的x和z(?)均视为无关,不参与匹配。
如果使用inside关键字,则条件项可以是集合(见2.9.7节inside操作符部分);如果使用match关键字,则可匹配带标签的联合的活动元素和元素内容。
if-else语句和case语句还可以使用unique、unique0或priority关键字修饰(置于if或case关键字之前)。它们的意义分别如下。
1) unique:

  • 要求所有条件互斥,即不能有任何情况使得多个条件表达式为真或匹配多个条件项。
  • 要求分支完备,即不能有任何情况使得所有条件表达式为假或不匹配任何条件项(有else或default除外)。

2) unique0,要求所有条件互斥,但不要求分支完备。
3) priority,当条件重叠,即有情况使得多个条件表达式为真或匹配多个条件项时,靠前者优先。
forever语句的形式如下:forever <单一语句或块>意为语句或块将被一直重复执行。
repeat语句的形式如下:repeat(<次数表达式>) <单一语句或块>意为语句或块将重复执行由次数表达式指定的次数;如果次数表达式的值包含z或x,则按0处理。
while语句的一般形式如下:while(<条件表达式>) <单一语句或块>意为如果条件表达式为真,则重复执行语句或块,直到条件表达式为假,一般条件表达式的值应依赖于语句或块,否则将形成无限循环。
do-while语句的一般形式如下:do <单一语句或块> while(<条件表达式>)意为先执行一次语句或块,然后求条件表达式。如为真,则重复执行,否则结束。一般条件表达式的值应依赖于语句或块,否则将形成无限循环。
for语句的一般形式如下:for([<初始语句>]; [<条件表达式>]; [<步进语句>]) <单一语句或块>括号中初始语句和步进语句也可以是由逗号分隔的多句:for([<初始语句1>{, <初始语句i>}]; [<条件表达式>]; [<步进语句1>{, <步进语句j>}]) <单一语句或块>意为先执行初始语句,然后求条件表达式,如为真则执行语句或块,然后执行步进语句,再次求条件表达式,如为真,依此循环,直到条件表达式为假,整个语句结束。步进语句一般是对条件表达式所依赖的变量进行更新,如果语句或块包含对条件表达式所依赖的变量更新,步进语句也可以与条件表达式无关。
foreach语句用于穷举数组中的元素,一般形式如下:foreach(<数组名>[<标识符1>{, <标识符i>}]) <单一语句或块>其中数组名为待穷举的数组名,标识符为新命名的标识符,用来按次序匹配元素的索引,可在语句或块中作为整数使用,例如:foreach(arr[i, j]) arr[i][j] = i + j;将使arr中的每个元素赋值为其两个索引之和。

值得注意的是,硬件描述语言描述的是各电路单元同时运作的电路,上述所有循环语句并不意味着其中的语句按时间先后在一步一步地执行,而是EDA工具综合出一个“庞大的”电路来完成整个循环过程描述的行为。这个电路一般是组合逻辑电路,结果将随着输入的变化而变化,仅有必要的电路延迟,电路规模取决于循环的次数和内容。因此,在使用循环语句编写需要综合成实际电路的代码时要十分小心,以避免过多的资源消耗。
2.12.4 always过程
always过程是Verilog中,特别是FPGA设计中最重要的语法元素。重要的逻辑结构基本都是由always过程实现的。
通用always过程的一般形式有:always <单一语句或块>
always@(<敏感值列表>) <单一语句或块>
always@(*) <单一语句或块>
always@(<事件列表>) <单一语句或块>第一种形式,没有指定过程执行的触发条件,过程将不断重复执行。
第二种形式,敏感值的形式一般是:<变量或线网1> {(,| or) <变量或线网i>}当敏感值列表中的任何一个变量或线网发生值改变时,过程执行。如果块内语句依赖的所有变量和线网都在敏感值列表中列出,则always过程块将形成组合逻辑;如果块内语句依赖的变量和线网只有部分在敏感值列表中列出或者内部语句存在分支不完整,则always过程块将形成锁存器逻辑。
第三种形式,使用“*”代替敏感值列表,编译器将自动找出块内语句依赖的所有变量填充,主要用于实现组合逻辑。
第四种形式,事件的最常用形式是:(posedge| negedge| edge) <变量或线网1> {(,| or) (posedge| negedge| edge) <变量或线网i>}意为事件列表中的任何一个变量或线网出现上升沿(posedge)、下降沿(negedge)或任意沿(edge)时,触发过程执行,主要用于实现触发器(时序逻辑)。在FPGA设计中,限于FPGA的内部结构,一般只能使用posedge。如果使用negedge将占用更多单元,而edge则一般不能使用。
用作沿触发的变量和线网如果有多位,将只有最低位有效。当电平由0变为1、x或z时,或电平由x或z变为1时,均认为是上升沿;当电平由1变为0、x或z时,或电平由x或z变为0时,均认为是下降沿。
代码2-30是always过程使用的一些例子。

代码2-30 通用always过程示例

1  logic ck = '1;
2  wire #2ns clk = ck;            // 将ck延迟2ns得到clk
3  logic [7:0] a = '0, b = '0;
4  logic [7:0] c, d, e, f;
5  always begin
6  #10ns ck = ~ck; // 产生周期20ns的时钟ck
7  end
8  always begin
9  #5ns a = a + 8'b1; // 产生10ns周期递增的a
10  #5ns b = b + 8'b1; // 产生10ns周期递增的b
11  end
12  always@(a, b) begin // 组合逻辑加法器
13  c = a + b;
14  end
15  always@(*) begin // 与上一个always过程等价
16  d = a + b;
17  end
18  always@(clk, a) begin// clk控制的锁存器
19  if(clk) e = a;
20  end
21  always@(posedge clk) begin // clk上升沿触发的触发器
22  f = a;
23  end
第5行的always过程没有敏感值或事件列表,它将周而复始地执行,每10ns将ck反相,产生20ns周期的时钟,第2行将ck延迟2ns得到clk。第8行的always过程也没有敏感值或事件列表,产生10ns周期递增的a和b,a相对于b超前5ns变化。
第12行的always过程带有敏感值列表,当a或b的值发生变化时,c赋值为a和b的和,形成组合逻辑,注意其中的逗号也可替换为or关键字。第15行的always块使用了“*”,与第12行的always过程等价。

12.png


图2-6 代码2-30的波形


第18行的always过程带有敏感值列表,当clk或a的值发生变化,且clk为高时e赋值为a,因而clk为高电平时,e随着a的变化而变化,clk为低的情况没有写出,e保持原值,形成锁存器。
第18行的always过程带有事件列表,在clk出现上升沿时,f赋值为a,形成触发器。
仿真波形如图2-6所示。
值得注意的是,虽然c和d由组合逻辑驱动,但直到a或b的值在5ns时第一次变化,它们才出现正确的值,这与真实的组合逻辑电路稍有不符。
Verilog还提供了专用于组合逻辑、锁存器和触发器的always_comb、always_latch和always_ff过程,这里介绍always_comb和always_ff过程。建议在描述组合逻辑和触发器逻辑时使用这两个专用的过程,而不是使用通用always过程。
always_comb过程的形式为:always_comb<单一语句或块>非常简单,与用always@(*)描述的组合逻辑相比有以下优点:
  • 启动时会执行一次,与真实组合逻辑电路的行为相符。
  • 不允许被驱动的变量在任何其他过程中驱动。
  • 如果内部条件分支不完整,会形成锁存器,编译器会给出警告。

always_ff过程的常用形式为:always_ff@(posedge<时钟> iff <时钟使能条件> or posedge <异步复位>)当然,其中的posedge也可以是negedge或edge,后面还可以增加更多的异步控制。不过对于FPGA来说,限于其内部结构,一般应使用posedge,并且至多一个异步复位。

代码2-31是always_comb和always_ff的例子。

代码2-31 always_comb和always_ff过程示例

1  always_comb begin                  // 组合逻辑加法器
2  c = a + b;
3  end
4  always_comb begin // 错误,因分支不完备,实际为锁存器
5  if(clk) d = a + b;
6  end
7  always_comb begin // 组合逻辑,包含加法器和减法器,
8  if(clk) e = a + b; // 并由clk控制数据选择器选择
9  else e = a - b;
10  end
11  always_ff@(posedge clk) begin // clk上升沿触发的触发器
12  f = a;
13  end
14  always_ff@(posedge clk iff en) // clk上升沿触发的触发器,并带有使能
15  begin // 输入en,当en为高时,时钟有效
16  g = a;
17  end
18  always_ff@(posedge clk iff en| rst) // clk上升沿触发
19  begin // 带有使能和同步复位
20  if(rst) h = '0;
21  else h = a;
22  end
23  always_ff@(posedge clk iff en or posedge arst) // clk上升沿触发
24  begin // 带有使能和异步复位
25  if(arst) i = '0; // arst上升沿或为高时i复位
26  else i = a;
27  end
“iff”关键字描述的使能采用的是门控时钟的方式,但限于FPGA结构,门控时钟并不利于FPGA综合,最好采用Q-D反馈的形式。上述例子中最后三个always_ff过程可修改为代码2-32的写法,推荐在FPGA设计中使用。

代码2-32 避免门控时钟的触发器使能和复位

1  always_ff@(posedge clk)          // clk上升沿触发的触发器,并带有使能
2  begin
3  if(en) g = a; // 当en为高时,才更新g
4  end
5  always_ff@(posedge clk) // clk上升沿触发
6  begin // 带有使能和同步复位
7  if(rst) h = '0;
8  else if(en) h = a;
9  end
10  always_ff@(posedge clk or posedge arst) // clk上升沿触发
11  begin // 带有使能异步复位
12  if(arst) i = '0;// arst上升沿或为高时i复位
13  else if(en) i = a;
14  end
2.12.5 阻塞和非阻塞赋值
除非阻塞赋值(<=)以外的所有赋值均为阻塞赋值。
在顺序块中,阻塞赋值语句将“阻塞”后面语句的求值和赋值,即阻塞赋值语句是按书写次序逐条求值和赋值的;而非阻塞赋值语句不会“阻塞”后面语句的求值和赋值,所有非阻塞语句将与最后一条阻塞赋值语句同时执行。例如代码2-33所示。

代码2-33 initial过程中混合阻塞赋值和非阻塞赋值示例

1  logic a = '0, b = '0, c;
2  initial begin                 // 执行次序结果
3  a = '1; //  1a = 1
4  b = a; //  2b = 1, 使用次序1之后的a
5  a <= '0;//  4a = 0
6  b <= a; //  4b = 1, 使用次序3之后的a
7  c = '0; //  3c = 0
8  c = b; //  4c = 1, 使用次序3之后的b
9  end
最终的结果将是a=0,b=1,c=1。
在always_ff过程中混合阻塞和非阻塞赋值语句的示例见代码2-34。

代码2-34 always_ff过程中混合阻塞赋值和非阻塞赋值示例

1  logic clk = '1;
2  always #10 clk = ~clk;
3  logic [1:0] a[4] = '{'0, '0, '0, '0};
4  always_ff@(posedge clk) begin :eg0    // 一个时钟过后a[0]=2'b11
5  a[0][0] = '1;
6  a[0][1] = a[0][0];
7  end
8  always_ff@(posedge clk) begin :eg1// 一个时钟过后a[1]=2'b11
9  a[1][0] = '1;
10  a[1][1] <= a[1][0];
11  end
12  always_ff@(posedge clk) begin :eg2// 一个时钟过后a[2]=2'b01
13  a[2][0] <= '1;
14  a[2][1] = a[2][0];
15  end
16  always_ff@(posedge clk) begin :eg3// 一个时钟过后a[3]=2'b01
17  a[3][0] <= '1;
18  a[3][1] <= a[3][0];
19  end
在上面的例子中,前两个块的两条语句是分两个次序赋值的,一个时钟过后,a[0]和a[1]的两位均为1;后两个块的两条语句是同一个次序赋值的,一个时钟过后,a[2]和a[3]均为2'b01。因此,前两个always_ff过程实际上只综合出一个触发器,a[?][0]和a[?][1]均连接到这个D触发器的输出,如图2-7所示;后两个always_ff过程实际上综合出两个D触发器,一个是a[?][0],另一个是a[?][1],它们级联,形成一个移位寄存器,如图2-8所示。

13.png


在always_comb过程中混合阻塞和非阻塞赋值语句的示例见代码2-35。

代码2-35 always_comb过程中混合阻塞赋值和非阻塞赋值示例

1  logic [3:0] a = 4'd0;
2  always #10 a = a + 4'd1;
3  logic [3:0] b[4], c[4];
4  always_comb begin
5  b[0] = a + 4'd1;             // b[0]=a+1
6  c[0] = b[0] + 4'd1;// c[0]=a+2
7  end
8  always_comb begin
9  b[1] = a + 4'd1; // b[1]=a+1
10  c[1] <= b[1] + 4'd1; // c[1]=a+2
11  end
12  always_comb begin
13  b[2] <= a + 4'd1; // b[2]=a+1
14  c[2] = b[2] + 4'd1;// c[2]将形成触发器锁定a变化前的b+1
15  end
16  always_comb begin
17  b[3] <= a + 4'd1; // b[3]=a+1
18  c[3] <= b[3] + 4'd1; // c[3]将形成触发器锁定a变化前的b+1
19  end
其中,前两个过程块将正常构成组合逻辑,如图2-9所示。但后两个过程块将形成一个由a的值改变触发的触发器,如图2-10所示。这与预期不符。

14.png


从上面两段示例代码可以看出,混有阻塞赋值语句和非阻塞赋值语句的过程块是比较难于理解和确定代码行为的。因此,强烈建议:
  • 描述触发器逻辑的时候一律使用非阻塞赋值,这样每个被赋值的变量都形成触发器。
  • 描述组合逻辑的时候一律使用阻塞赋值,这样可以确保不会出现触发器。
  • 在测试代码中使用initial过程或不带敏感值/事件列表的always过程时使用阻塞赋值,以形成便于理解的时序。

2.13 模块

模块是Verilog中最基础的结构,逻辑设计的层次化就是由模块实现的。模块用来封装数字逻辑的数据、功能以及时序。模块可大可小,或小到实现一个逻辑门,或大到实现一个CPU。
模块本身只是一张“图纸”,在需要用到模块功能的时候,需要将它“实例化”,一个模块可以在不同地方被多次实例化,实现了代码的可重用性。一个设计最顶层的模块是由仿真器或编译器来实例化的。
使用关键字module和endmodule来定义一个模块,例如代码2-36。

代码2-36 hello_world模块

1  module hello_world;
2  initial begin
3  $display("Hello World!");
4  end
5  endmodule
该代码描述了一个名为“hello_world”的模块,功能是在仿真环境中显示“Hello World!”字符串,这个功能只能用于仿真验证,是不能被综合为实际电路的。注意在模块标识符后面有分号,而在endmodule关键字后没有分号。
再如代码2-37。

代码2-37 非门模块

1  module not_gate (
2  input wire loigc a,          // 输入端口,logic型线网
3  output var logic y/ 输出端口,logic型变量 /
4  );
5  assign y = ~a;
6  endmodule
该代码描述了一个名为not_gate的非门,功能就是一个非门,可以被综合为实际电路。实际的电路总是有输入输出的,因而它比代码2-36增加了模块的端口,对于非门,有一个输入a和一个输出y。第2和第3行的input和output关键字指明端口的方向,这两行后面还包含注释,分别是由“// ”引导的单行注释和由“/”和“/”包裹的块注释。第5行的assign赋值语句将a取反赋给y。
模块定义的常见形式是:module<模块名> [(
(input| output| inout) (<线网定义1>| <变量定义1>){,
(input| output| inout) (<线网定义i>| <变量定义i>)}
)];
{<模块内容>}
endmodule[:<模块名>]其中,圆括号中的内容称为端口列表,每一行可定义一个端口或多个同向同类型的端口。
input、output、inout用于指定端口方向为输入、输出或双向,其后的线网或变量定义与2.5节和2.6节的形式一致,数据类型可以是结构、数组等复合类型。在FPGA设计中,只有output可以搭配变量定义。除顶层模块以外,一般也不应使用inout端口。
端口定义常常也可以简略为:(input| output| inout) <数据类型> [signed| unsigned] [<位宽>] <端口名>[=<初值>] {, ...}或:(input| output| inout) [signed| unsigned] [<位宽>] <端口名>[=<默认值>] {, ...}这两种简略写法都没有指明端口是线网还是变量,后者还没有指定数据类型。那么对于input或inout端口,两者都将默认为线网,后者的数据类型将默认为logic;而对于output端口,前者将默认为变量,后者将默认为logic类型的线网。线网类型都默认为wire,如果需要默认为其他类型线网,可使用`default编译指令修改。在FPGA设计中,input和inout端口只支持线网类型,而output端口可以是线网也可以是变量。
对于输入线网或输出的变量,可以指定默认值或初始值,必须是常数或常量表达式。如果输入线网在模块实例化时没有连接,则连接默认值;输出变量的初始值是仿真开始或电路启动时的初始值。目前,FPGA开发工具对端口默认值并不能很好地支持,应避免在FPGA设计中为端口指定默认值。
因而,在FPGA设计中,整型端口最常见的简略写法为:(input| inout) [wire](signed| unsigned) <位宽> <端口名1> {,<端口名i>}
output logic (signed| unsigned) <位宽> <端口名1> {,<端口名i>}后者指定数据类型logic,而logic类型又可根据模块中使用它的上下文自动适应为变量或线网,因而可算是“万能”的整型输出端口定义方式了。
模块内容主要可以有:

  • 数据定义(线网、变量)
  • 数据类型定义(结构、联合等)
  • 参数、常量定义
  • 任务、函数定义
  • 模块、接口实例化
  • 持续赋值
  • 过程
  • 生成结构

parameter型的参数定义也可以定义在模块定义的头部,一般形式是:module<模块名> #(
{,
}
)[(
(input| output| inout) (<线网定义1>| <变量定义1>){,
(input| output| inout) (<线网定义i>| <变量定义i>)}
)];
{<模块内容>}
endmodule[:<模块名>]其中parameter型参数定义与2.7节中的形式一致。
例如代码2-38中的模块描述了一个带有使能和同步复位的时序逻辑加法器。

代码2-38 参数化的时序逻辑加法器

1  module my_adder #(
2  parameter DW = 8             // integer类型参数,默认值8
3  )(
4  input clk, rst, en,
5  input [DW - 1 :0] a, b,// 使用参数定义位宽
6  output logic [DW : 0] sum // 使用参数定义位宽
7  );
8  always_ff@(posedge clk) begin
9  if(rst) sum <= '0;
10  else if(en) sum <= a + b;
11  end
12  endmodule
上面例子先定义了参数DW(意为数据宽度),然后用DW来定义输入和输出端口的位宽。像这样用参数来定义模块的某些特征的好处是,当设计中多处需要功能相似但特征(比如位宽、系数等)不同的模块时,我们不必重复设计模块,而只需要在实例化时修改参数即可。参数化的模块设计显著提高了模块的可重用性。
模块实例化语句的一般形式为:<模块名> [#(.<参数名1>(<参数赋值1>){, .<参数名i>(参数赋值i)})]
<实例名1> [(
.<端口名1>(<实例1端口连接1>){,
.<端口名i>(<实例1端口连接i>)}
)] {,
<实例名k> [(
.<端口名1>(<实例k端口连接1>){,
.<端口名i>(<实例k端口连接i>)}
)]};其中参数名和端口名都是模块定义时指定的名字,参数值必须是常数或常量表达式,端口连接则是实例化该模块的上层模块中的线网或变量,也可以是常数或常量表达式。
对于没有参数的模块,不应有参数赋值部分,即“#(...)”部分;对于有参数的模块,如果不需要修改模块定义时赋予参数的默认值,也可省略参数赋值部分。
相同参数的多个实例化可以写在一个模块实例化语句中,以逗号分隔多个实例名及其端口连接,如代码2-39所示。

代码2-39 模块实例化示例

1  module test_sum_ff;
2  logic clk = '0;
3  always #5 clk = ~clk;
4  logic [7:0] a = '0, b = '0, sum_ab;
5  logic co_ab;
6  logic [11:0] c = '0, d = '0;
7  logic [12:0] sum_cd;
8  always begin
9  #10 a++; b++; c++; d++;
10  end
11  my_adder #(.DW(8)) the_adder_8b(
12  .clk(clk), .rst(1'b0), .en(1'b1),
13  .a(a), .b(b), .sum({co_ab, sum_ab})
14  );
15  my_adder #(.DW(12)) the_adder_12b(
16  .clk(clk), .rst(1'b0), .en(1'b1),
17  .a(c), .b(d), .sum(sum_cd)
18  );
19  endmodule
第11~14行实例化了8位加法器the_adder_8b。注意sum输出端口连接使用了位拼接运算符,将sum的低8位连接到了sum_ab,而第9位连接到了co_ab。因为代码2-38中模块定义时,DW参数的默认值就是8,所以第11行中“#(.DW(8))”可以省略。
第15~18行实例化了12位加法器the_adder_12b,其sum端口直接连接到了13位的sum_cd。两个加法器的rst和en输入使用常数连接。
在上面的一般形式中,使用的参数和端口连接写法都是形如“.<名字>(<值/连接>)”,称为按名字的参数赋值和端口连接。此外还有按顺序的参数赋值和端口连接。例如代码2-39中的第15~18行还可以写成这样:
15  my_adder #(12) the_adder_12b(
16  clk, 1'b0, 1'b1,
17  c, d, sum_cd
18  );
此时,必须保证端口连接的书写顺序和端口定义时的顺序完全一致。
如果端口连接的线网或变量与端口名一致,还可以省略括号及其中内容,如:
15  my_adder #(.DW(12)) the_adder_12b(
16  .clk, .rst(1'b0), .en(1'b1),
17  .a(c), .b(d), .sum(sum_cd)
18  );
其中的.clk等价于.clk(clk)。
有多个输出端口的模块在实例化时,可能有些端口并不需要引出,可以空缺括号中的内容,如 “.outx()”。
注意,模块实例化中端口的连接并不是赋值,只是连线。端口和连接到端口的线网或变量中位宽较宽的,高位将获得值z,对于FPGA一般将获得值0。

2.14 接口

初学者可跳过此节,当觉得在很多模块中写一模一样的长长的端口定义很烦琐时,可再来学习本节。
接口用来将多个相关的端口组织在一起,为了说明接口的作用,先考虑下面代码:

代码2-40 存储器及其测试

1  module mem #(
2  parameter LEN = 256, DW = 8
3  )(
4  input wire clk, rst,
5  input wire [$clog2(LEN) - 1 : 0] addr,
6  input wire [DW - 1 : 0] d,
7  input wire wr,
8  output logic [DW - 1 : 0] q
9  );
10  logic [DW - 1 : 0] m[LEN] = '{LEN{'0}};
11  always_ff@(posedge clk) begin
12  if(rst) m <= '{LEN{'0}};
13  else if(wr) m[addr] <= d;
14  end
15  always_ff@(posedge clk) begin
16  if(rst) q <= '0;
17  else q <= m[addr];
18  end
19  endmodule
20  
21  module mem_tester #(
22  parameter LEN = 256, DW = 8
23  )(
24  input wire clk, rst,
25  output logic [$clog2(LEN) - 1 : 0] addr,
26  output logic [DW - 1 : 0] d,
27  output logic wr,
28  input wire [DW - 1 : 0] q
29  );
30  initial addr = '0;
31  always@(posedge clk) begin
32  if(rst) addr <= '0;
33  else addr <= addr + 1'b1;
34  end
35  assign wr = 1'b1;
36  assign d = DW'(addr);
37  endmodule
38
39  module testmem;
40  logic clk = '0, rst = '0;
41  always #5 clk = ~clk;
42  initial begin
43  #10 rst = '1;
44  #20 rst = '0;
45  end
46  logic [5:0] addr;
47  logic [7:0] d, q;
48  logic wr;
49  mem_tester #(64,8) the_tester(clk, rst, addr, d, wr, q);
50  mem #(64,8) the_mem(clk, rst, addr, d, wr, q);
51  endmodule
第一个模块mem描述了一个带有同步复位的存储器,存储单元是用数组描述的,输出是先读模式,模块内的代码应容易理解,此处不赘述。第二个模块mem_tester用测试存储器,它在clk的驱动下产生了递增的addr和数据,并一直将wr置1。第三个模块是顶层,实例化前两者,并产生前两者共同需要的clk和rst信号。
从这个例子中可以看出,存储器需要用到的几个端口addr、d、wr、q、clk和rst在模块端口定义中出现了两次,在顶层模块中定义了它们,并在模块实例化时重复书写了两遍。对于更复杂的设计,可能存在更多的模块需要用到同样的端口,全部分散书写出来,降低了代码效率、可读性和可维护性。
接口可以将众多端口组织在一起,形成一个模板,在需要时实例化接口,便可使用一个标识符引用。
接口定义的常用形式如下:interface <接口名> [#(
{,
}
)][(
(input| output| inout) (<外部共享端口定义1>){,
(input| output| inout) (<外部共享端口定义i>)}
)];
{<线网或变量定义>;}
{<其他内容>}
{modport <角色名> (
(input| output| inout) <端口1>{,
(input| output| inout) <端口i>}
);}
endinterface[:<接口名>]其中,<其他内容>可以是参数/常量定义、任务或函数。接口内部定义的线网或变量是应用该接口的模块内需要用到的端口,而定义在接口头部的外部共享端口还可以在实例化接口时与接口外部的线网或变量连接。modport关键字引导角色定义,用于指定不同端口在接口的不同角色(比如主、从、监听)下的方向。
接口的实例化与模块的实例化形式几乎一样,此处不赘述。在模块的端口定义列表中引用接口时,可使用“<接口名>.[<角色名>]”的形式,在模块中引用接口内的端口时,使用“<接口名>.[<端口名>]”的形式。
使用接口,可将代码2-40修改为代码2-41。

代码2-41 存储器及其测试

1  interface membus #(                 // 定义名为membus的接口
2  parameter LEN = 256, DW = 8
3  )(
4  input wire clk, input wire rst// 外部共享端口clk和rst
5  );
6  logic [$clog2(LEN) - 1 : 0] addr;
7  logic [DW - 1 : 0] d, q;
8  logic wr;
9  modport master(output addr, d, wr, // 定义角色master
10   input clk, rst, q);
11  modport slave(input clk, rst, addr, d, wr, // 定义角色slave
12   output q);
13  endinterface
14  
15  module mem #(parameter LEN = 256, DW = 8)
16  (membus.slave bus); // 引用接口,并命名为bus
17  logic [DW - 1 : 0] m[LEN] = '{LEN{'0}};
18  always_ff@(posedge bus.clk) begin // 引用bus中的clk
19  if(bus.rst) m <= '{LEN{'0}};
20  else if(bus.wr) m[bus.addr] <= bus.d;
21  end
22  always_ff@(posedge bus.clk) begin
23  if(bus.rst) bus.q <= '0;
24  else bus.q <= m[bus.addr];
25  end
26  endmodule
27  
28  module mem_tester #(parameter LEN = 256, DW = 8)
29  (membus.master bus);
30  initial bus.addr = '0;
31  always@(posedge bus.clk) begin
32  if(bus.rst) bus.addr <= '0;
33  else bus.addr <= bus.addr + 1'b1;
34  end
35  assign bus.wr = 1'b1;
36  assign bus.d = bus.addr;
37  endmodule
38  
39  module testintfmem;
40  logic clk = '0, rst = '0;
41  always #5 clk = ~clk;
42  initial begin
43  #10 rst = '1;
44  #20 rst = '0;
45  end
46  membus #(64,8) the_bus(clk, rst); // 实例化端口
47  mem_tester #(64,8) the_tester(the_bus); // 在实例化模块时使用端口
48  mem #(64,8) the_mem(the_bus); // 在实例化模块时使用端口
49  endmodule
2.15 生成块
初学者可跳过此节,当觉得在模块中重复写类似的有规律的内容比较烦琐时,再来学习本节。

代码2-42 8位格雷码到二进制码转换

1  module gray2bin (
2  input wire [7:0] gray,
3  output logic [7:0] bin
4  );
5  assign bin[7] = ^gray[7:7];
6  assign bin[6] = ^gray[7:6];
7  assign bin[5] = ^gray[7:5];
8  assign bin[4] = ^gray[7:4];
9  assign bin[3] = ^gray[7:3];
10  assign bin[2] = ^gray[7:2];
11  assign bin[1] = ^gray[7:1];
12  assign bin[0] = ^gray[7:0];
13  endmodule
生成块可根据一定的规律,使用条件生成语句、循环生成语句等,重复构造生成块的内容,等效于按照规律重复书写了生成块中的内容。考虑代码2-42。
该代码描述了一个将8位格雷码转换到二进制码的组合逻辑,可以看到书写了8行很有规律的持续赋值。试想,如果需要像这样描述64位格雷码到二进制码的转换呢?如果需要参数化位数呢?生成块可完成类似的需求。
生成常用形式如下:generate
{| | }
endgenerate其中,for生成语句的形式为:{for(genvar<生成变量>=<初始值>; <条件表达式>; <步进语句>) begin [:<块标识>]
{<生成内容>}
end这与过程中的for语句形式相似,不过,循环条件所用的变量必须使用genvar关键字定义,循环步进和条件必须只由生成变量决定。
if生成语句和case生成语句的形式与过程中的if语句和case语句相似,不过,所有的条件表达式必须是常量表达式。
生成语句中的生成内容与模块中能包含的内容基本一致,生成内容中还可以再嵌套其他生成语句。
如果使用生成块,代码2-42可参数化,改写为代码2-43。

代码2-43 参数化位数的格雷码到二进制码转换

1  module gray2bin #(
2  parameter DW = 8
3  )(
4  input wire [DW - 1 : 0] gray,
5  output logic [DW - 1 : 0] bin
6  );
7  generate
8  for(genvar i = 0; i < DW; i++) begin :binbits
9  assign bin[i] = ^gray[DW - 1 : i];
10  end
12  endgenerate
13  endmodule

2.16 任务和函数

任务和函数将一些语句实现的一定功能封装在一起,以便重复使用。任务和函数都只能在过程块中调用。
任务定义的一般形式:task [static| automatic] <任务名> (
(input| output| inout| [const] ref) <变量定义1>{,
(input| output| inout| [const] ref) <变量定义i>}
);
{<变量或常量定义>| <语句>}
endtask函数定义的一般形式:function [static| automatic] <数据类型符号位宽> <函数名> (
(input| output| inout| [const] ref) <变量定义1>{,
(input| output| inout| [const] ref) <变量定义i>}
);
{<变量或常量定义>| <语句>| <函数名>=<表达式>| return <表达式>}
endfunction其中的static和automatic关键字用于指定任务和函数的生命周期,使用automatic关键字的任务和函数中的变量均为局部变量,在每次任务或函数调用时均会重新初始化,可被多个同时进行的过程调用,或被递归调用,类似于编程语言的可重入。FPGA开发工具一般只支持automatic类型的任务和函数。
任务和函数本身类似于顺序块,因而在顺序块中能使用的语句(过程赋值、流程控制等)都能在任务和函数中使用。
任务中可以有时序控制(延时、事件),而函数中不能有。
代码2-44是任务和函数的例子。

代码2-44 任务和函数示例

1  module test_task_func;
2  localparam DW = 8;
3  
4  task automatic gen_reset(
5  ref reset, input time start, input time stop
6  );
7  #start reset = 1'b1;
8  #(stop - start) reset = 1'b0;
9  endtask
10  logic rst = 1'b0;
12  initial gen_reset(rst, 10ns, 25ns);
13  
14  function automatic [$clog2(DW) - 1 : 0] log2(
15  input [DW - 1 : 0] x
16   );
17  log2 = 0;
18  while(x > 1) begin
19  log2++;
20  x >= 1;
21  end
22  endfunction
23  logic [DW - 1 : 0] a = 8'b0;
24  logic [$clog2(DW) - 1 : 0] b;
25  always #10 a++;
26  assign b = log2(a);
27  endmodule
第4行的任务gen_reset用于在reset上产生复位信号,第14行的函数用于求输入x的底2对数。

2.17 包

包(package)用来封装一些常用的常量变量定义、数据类型定义、任务和函数定义等,在需要使用时,可使用import关键字导入。
包定义的形式是:package <包名>;
<包内容,数据定义、数据类型定义、任务定义、函数定义等>
endpackage引用包时,使用:import <包名>::(<内容名>|);使用内容名(数据、数据类型、任务、函数等)时,只导入相应内容;使用“”时,将导入包内全部内容。
import语句可以用在模块内,也可以用在模块头部。
代码2-45是包的例子。

代码2-45 包的示例

1  package Q15Types;
2  typedef logic signed [15:0] Q15;
3  typedef struct packed { Q15 re, im; } CplxQ15;
4  function CplxQ15 add(CplxQ15 a, CplxQ15 b);
5  add.re = a.re + b.re;
6  add.im = a.im + b.im;
7  endfunction
8  function CplxQ15 mulCplxQ15(CplxQ15 a, CplxQ15 b);
9  mulCplxQ15.re = (32'(a.re)b.re - 32'(a.im)b.im) >>> 15;
10  mulCplxQ15.im = (32'(a.re)b.im + 32'(a.im)b.re) >>> 15;
12  endfunction
13  endpackage
14  
15  module testpackage;
16  import Q15Types::*;
17  CplxQ15 a = '{'0, '0}, b = '{'0, '0};
18  always begin
19  #10 a.re += 16'sd50;
20  a.im += 16'sd100;
21  b.re += 16'sd200;
22  b.im += 16'sd400;
23  end
24  CplxQ15 c;
25  always_comb c = mulCplxQ15(a, b);
26  real ar, ai, br, bi, cr, ci, dr, di;
27  always@(c) begin
28  ar = real'(a.re) / 32768;
30  ai = real'(a.im) / 32768;
31  br = real'(b.re) / 32768;
32  bi = real'(b.im) / 32768;
33  cr = real'(c.re) / 32768;
34  ci = real'(c.im) / 32768;
35  dr = ar br - ai bi;
36  di = ar bi + ai br;
37  if(dr < 1.0 && dr > -1.0 && di < 1.0 && di > -1.0) begin
38  if(cr - dr > 1.0/32768.0 || cr - dr < -1.0/32768.0)
39  $display("err:\t", cr, "\t", dr);
40  if(ci - di > 1.0/32768.0 || ci - di < -1.0/32768.0)
41  $display("err:\t", ci, "\t", di);
42  end
43  end
44  endmodule
这个例子在Q15Types包中定义了Q15(Q1.15格式)和CplxQ15数据类型,并定义了CplxQ15的加法和乘法运算。第15行之后的testpackage模块中,对CplxQ15类型的乘法进行测试。
Verilog规范中还定义了一些标准包,包括信号量、邮箱、随机和进程,可用于复杂测试代码的编写,读者可适当了解。

2.18 系统任务和函数

系统任务和函数是标准中定义的用于在仿真和编译过程中执行一些特殊功能的任务和函数,全部以“$”符号开头,有的可带参数,有的无参数,无参数或可不带参数的系统任务和函数在调用时可以不带括号。
大多数系统任务和函数都应该在过程中被调用。系统任务和函数本身都是不能综合成实际电路的,主要用于仿真测试。但部分任务和函数会干预综合过程,影响综合结果,是可综合的代码中有用的内容,比如类型转换和存储器相关函数和任务。
标准中定义的系统任务和函数有近两百个,这里介绍一些常用的。
1) 显示相关:$display、$write、$strobe、$monitor。
2) 文件相关:$fopen、$fclose、$fdisplay、$fwrite、$fstrobe、$fmonitor、$fscanf。
3) 存储器相关:$readmemh、$readmemb、$writememh、$writememb。
4) 仿真相关:$stop、$finish。
5) 错误和信息:$fatal、$error、$warning、$info。
6) 类型转换:$itor、$rtoi、$bitstoreal、$realtobits、$bitstoshortreal、$shortrealtobits。
7) 数学:$clog2、$ceil、$floor、$ln、$log10、$exp、$pow、$sqrt、$sin、$cos、$tan、$asin、$acos、$atan、$atan2、$hypot、$sinh、$cosh、$tanh、$asinh、$acosh、$atanh。
其他部分系统任务和函数在后续章节中也会有些许提及,未提及的系统任务和函数读者可查阅标准适当了解。
2.18.1 显示相关
显示相关任务的一般使用形式是:($display| $write| $strobe| $monitor)(<参数1>{, <参数i>});这些任务将把待显示的内容输出到仿真环境的终端或仿真工具的控制台窗口。参数可以是字符串(以双引号包裹)、线网、变量或带有返回值的表达式。如果是字符串,还可在字符串中加入格式说明,每个格式说明将按次序对应后面一个参数,并按照一定的格式被后面的对应参数替换。没有对应格式说明的参数,如果是紧凑类型将按默认格式显示;如果是非紧凑类型,则只有字节数组会按照字符串显示,其他会被认为不合法。
$display用于即时显示,并会在最后添加换行。
$write用于即时显示,但不会添加换行。
$strobe会在一个仿真时间步的最后显示,即当同时并行执行的所有语句执行完之后,它才会输出显示,常用于检测每个时间步线网或变量的变化结果。
$monitor一经运行,便会在引用到的任何一个变量发生变化时,输出一次显示,可用于持续监测线网或变量的变化。
字符串中的格式说明均以“%”开始,常用的格式说明如表2-12所示。

表2-12 格式说明符说明符说明示例


15.png


对于整数,显示宽度与该整数位宽下所能表达的最大数值的宽度一致,如对于16位无符号数,十六进制宽4位、十进制宽5位、八进制宽6位、二进制宽16位。如果数值达不到显示位宽,则十进制高位填充空格,其他进制填充0。也可指定最小显示位宽,形式为“%<最小位宽>(h|d|o|b)”,其中的最小位宽为非负整数,如果实际数值窄于这个值则填充空格或0,如果实际数值宽于这个数值则扩展显示宽度。
二进制显示时,为z或x的位显示“z”或“x”。
八进制或十六进制显示时,如果对应的3位或4位:
  • 全是z或x则显示“z”或“x”。
  • 不全是但含有x则显示“X”。
  • 不全是但含有z且不含x则显示“Z”。
    十进制显示时,如果所有位:
  • 全是z或x则显示“z”或“x”。
  • 不全是但含有x则显示“X”。
  • 不全是但含有z且不含x则显示“Z”。

字符串也可用同样的形式指定显示的宽度,窄于指定宽度的左侧填充空格,宽于指定宽度的扩展宽度显示。
对于浮点数,%e、%f、%g可指定左右对齐、显示宽度和小数位数,形式为“%(+|-)<宽度>.<小数位>(e|f|g)”,其功能与C语言格式输出的格式说明完全兼容,规则较为复杂,在FPGA设计中也很少用到,读者可参考C语言相关资料,这里不赘述。
2.18.2 文件相关
文件相关任务和函数用于读写EDA工具运行的计算机上的文件,可用于读取激励文件、写仿真结果到文件等。
$fopen用于打开文件,并返回一个用于后续访问打开的文件的描述符,形式如下:<多通道描述符> = $fopen(<文件名>);或:<文件描述符> = $fopen(<文件名>, <类型>);其中多通道描述符是32位二进制整数,每一位代表一个通道,第0位默认代表标准输出(同显示任务),采用多通道描述符,可以将输出信息同时写入多个文件(含标准输出)中,只需要将多个通道的描述符按位或用作描述符即可。文件描述符也是32位二进制整数,不过每个数值代表一个文件,0、1、2默认为标准输入、标准输出和标准错误。
文件名和类型都是字符串,文件名可以带有相对路径或绝对路径,类型则如表2-13所示。
表2-13 打开文件的类型说明符说明"r"、"rb"只读"w"、"wb"只写,文件存在则覆盖,不存在则创建"a"、"ab"追加,文件存在则从结尾追加内容,文件不存在则创建"r+"、"r+b"、"rb+"可读可写"w+"、"w+b"、"wb+"可读可写,文件存在则覆盖,不存在则创建"a+"、"a+b"、"ab+"可读可写,从文件结尾开始,文件不存在则创建$close用于关闭文件,形式如下:$fclose(<多通道描述符>| <文件描述符>)文件一旦关闭,便不能再读写,已使用$fmonitor和$fstrobe任务发起的读写操作将被隐式地取消。
$fdisplay、$fwrite、$fstrobe和$fmonitor几个任务的使用方式如下:($fdisplay| $fwrite| $fstrobe| $fmonitor)(<多通道或文件描述符>, <参数1>{, <参数i>});除增加了多通道或文件描述符作为第一个参数,其他与对应的显示任务几乎一样,当然内容从显示到终端变成了写入文件。
$fscanf用于以一定的格式从文件中读入数据,形式如下: = $fscanf(<文件描述符>, <格式说明>, <变量1>{, <变量i>});其中的格式说明与显示任务的格式说明类似,可以使用一连串多个格式说明符匹配多个变量,从文件中读取文本内容并按指定格式转换后赋值给变量。对于整数和字符串,每个说明符匹配到空白(空格、制表符、换行等)前的一段内容;对于字符,每个说明符匹配一个字符;对于时间,还将四舍五入到系统的时间精度;对于层次路径,直接返回当前层次路径,并不读文件。
Integer类型的返回值表示成功匹配并赋值的变量个数,如果为-1,表示文件已结束。
除了上述这些文件操作任务和函数外,还有二进制读函数$fread、获取操作位置的函数$ftell、设定操作位置的函数$fseek和重置操作位置的函数$rewind,这里不赘述,读者可参考标准文档。
2.18.3 存储器相关
存储器相关的任务用于从文件中读取内容来初始化存储器或将存储器的内容保存到文件中。所谓存储器就是整型数组。
$readmemh和$readmemb分别从文件中读取十六进制和二进制文本到存储器中。形式是:($readmemh| $readmemb)(<文件名>, <数组名>[, <起始地址> [, <终止地址>]]);其中文件名对应的文件的内容必须符合以下规则:

  • 只能包含空白、注释(行注释或块注释)、“@”字符和常数,常数由z、Z、x、X和对应进制的数字组成。
  • 被空白或注释分隔的数表达数据,每个数据内容对应数组中的一个元素。
  • “@”字符后紧跟的数表达地址,必须是十六进制,指定下一个数据的地址。
  • 未被指定地址的数据的地址为前一数据地址加1,文件开头先出现数据时,该数据地址为0。

典型的可用于$readmemh任务的文件内容(注意左侧为行号,并非文件内容)如下:
1    @0000  00
2    @0001  5A
3    @0002  7F
4    @0003  5A
5    @0004  00
6    @0005  A6
7    @0006  81
8    @0007  A6
$writememh和$writememb分别将存储器中的内容以十六进制形式或二进制形式写入文件。形式是:($writememh| $writememb)(<文件名>, <数组名>[, <起始地址> [, <终止地址>]]);写到文件里的内容符合上面的规则,并且一般带有“@”开头的地址,除非数组元素为非紧凑类型。
2.18.4 仿真相关
$stop用于暂停仿真,$finish用于结束仿真,它们的使用形式如下:($stop| $finish)[([0| 1| 2])];可带圆括号或不带圆括号,可带参数或不带参数。带参数0表示不显示信息到终端,带参数1表示显示仿真时间和位置到终端,带参数2表示显示仿真时间、位置以及仿真占用计算机CPU和存储器的统计信息。不带参数等价于带参数1。
2.18.5 错误和信息
$fatal、$error、$warning、$info用于在编译期(准确地说是展述时)或在仿真运行时给出严重错误、错误、警告和信息。严重错误将终止展述或仿真;错误不会终止展述或仿真,但展述时如出现错误将不会启动仿真;警告和信息只给出信息。它们经常用来做编译时的常量和参数合法性的报告或运行时变量合法性的报告,也常常用来做测试时功能校验的报告。
它们都类似显示任务,可带有字符串或常量、变量表达式作为参数,并支持字符串中的格式说明符。$fatal则多一个结束号参数。它们的使用形式如下:$fatal[(<结束号>{, <参数>})];
($error| $warning| $info)[([<参数1>{, <参数i>}])];2.18.6 类型转换和数学函数
1.类型转换函数
$itor(x),将整型数据x转换为real型(双精度浮点)。
$rtoi(x),将real型数据x转换为integer型。
$bitstoreal(x),将符合IEEE 754规范的64位编码x转换为real型。
$realtobits(x),将real型数据x转换为符合IEEE 754规范的64位编码。
$bitstoshortreal(x),将符合IEEE 754规范的32位编码x转换为shortreal型。
$shortrealtobits(x),将shortreal型数据x转换为符合IEEE 754规范的32位编码。
例如:$itor(123)→ 123.0
$rtoi(456.7)→ 457
$bitstoreal(64'h3fd8_0000_0000_0000) → 0.375
$realtobits(0.375) → 64'h3fd8_0000_0000_000
$bitstoshortreal(32'h3ec0_0000) → 0.375
$realtoshortbit(0.375)→ 32'h3ec0_0000$cast、$signed和$unsigned三个函数在2.8节已有介绍,此处不赘述。
2.数学函数
$clog2(x),返回不小于x的以2为底的对数的最小整数。
$ceil(x),返回不小于x的最小整数。
$floor(x),返回不大于x的最大整数。
以上三个返回值为整数,以下返回值均为real型。
$ln(x),返回x的自然对数。
$log10(x),返回x的常用对数。
$exp(x),返回e(自然对数的底)的x次幂。
$pow(x, y),返回x的y次幂,等价于x**y。
$sqrt(x),返回x的平方根。
$sin(x)、 $cos(x) 、$tan(x),返回x(弧度)的正弦、余弦和正切。
$asin(x)、$acos(x)、$atan(x),返回x的反正弦、反余弦和反正切(均为弧度)。
$atan2(y, x),返回复数x+yi的辐角,值域为(-π,π)。
$hypot(x, y),返回复数x+yi的模。
$sinh(x)、$cosh(x)、$tanh(x),返回x的双曲正弦、双曲余弦和双曲正切。
$asinh(x)、$acosh(x)、$atanh(x),返回x的反双曲正弦、反双曲余弦和反双曲正切。

2.19 编译指令

编译指令用来设置编译过程的一些属性、控制编译流程等,Verilog所有的编译指令均以沉音符号“`”(ASCII码0x60)开头。注意不要将沉音符号与撇点“'”混淆。编译指令均独占一行,并不以分号结尾,可带有注释。这里简单介绍几个常用的编译指令。

  • `default_nettype,设定默认线网类型
  • define、undef和`undefineall,宏定义。
  • `include,包含文件。
  • ifdef、ifndef、elsif、else和`endif,条件编译。
  • `timescale,时间单位和精度设置。
  • `resetall,重置所有编译指令。

default_nettype用来设置默认的线网类型,形式为:default_nettype(<线网类型>| none)2.13节提到了模块端口的默认线网类型为wire,便可以使用这个编译指令来更改。Verilog有一个比较危险的特性是可以隐式定义线网,即编译器将把未定义过的标识符认定为默认类型的线网,因而任何地方一个笔误,都将形成一个默认类型的线网,这多半是不可预期的,所以建议初学者将线网类型的默认值设置为none:`default_nettype none这样便杜绝了编译器将笔误认定为新线网,当然也使得我们在简写模块的端口定义时,不能省略wire关键字。
define、undef和undefineall用来定义宏和解除宏定义,宏可以在代码中使用或用于条件编译指令中,编译器直接将宏按定义时的文本展开。它们的使用形式如下:define<宏名>[(<参数1>{, <参数i>})] <宏内容>
`undef<宏名>
undefineall其中define还可以带有参数,宏内容中参数部分会以使用时的参数内容替代,undefineall用于解除所有已定义的宏。例如:define PI 3.14159265358979324
`define MAX(a, b) ((a) > (b) ? (a) : (b))
`undefine PI注意其中MAX宏的内容大量使用了括号,这是为了防止处于复杂表达式中的宏展开时优先级错乱。
使用宏的格式是:`<宏名>。注意宏名前面带有沉音符。
include用于包含文件,等同于直接将被包含的文件的全部内容替换在当前位置,当需要实例化位于其他文件中的模块、导入位于其他文件中的包时,往往需要使用该编译指令。一般形式为:include (<<文件路径和文件名>>| "<文件路径和文件名>")其中文件路径可以是绝对路径或相对路径。使用双引号时,相对路径以编译器当前工作目录(常常是文件所在目录)为起点;使用尖括号时,以编译器和规范设定的目录为起点。
大多数EDA工具,特别是带有图形界面的工具,都以工程的形式管理多个源文件,在同一个工程中的任何源文件中均可直接实例化定义在其他源文件中的模块,并不需要使用`include编译指令。
ifdef、ifndef、elsif、else和endif为条件编译指令,常用形式为:(ifdef <宏名1>| `ifndef <宏名1>)
<代码段1>
{`elsif
<代码段i>}
[`else
<代码段k>]
endif使用ifdef时,如果宏名1被定义,则代码段1将被编译,否则如果宏名i被定义,代码段i将被编译; 如果宏名1至宏名i均未定义,则代码段k将被编译。使用`ifndef时,如果宏名1未被定义,则代码段1将被编译,否则,如果宏名i被定义,代码段i将被编译;如果宏名1被定义而宏名i均未被定义,则代码段k将被编译。
timescale用于设定时间单位和精度,在2.12.1节中介绍延迟时,所有的时间都带有单位,比如“ns”,而如果使用timescale编译指令设定了单位和精度,则可省略单位。一般形式是:`timescale(1| 10| 100)[m| u| n| p| f]s / (1| 10| 100)[m| u| n| p| f]s其中m、u(μ)、n、p、f为国际单位制词头,“/”左侧为时间单位,右侧为时间精度,时间精度必须不大于时间单位。定义了时间单位和精度之后,所有不带单位的时间均会乘以时间单位,所有时间均会被四舍五入到时间精度。例如:

代码2-46 begin-end块中延迟的示例

1  `timescale 10ns/1ns
2  initial begin
3  #1.55 a = 8'd10;// 实际延迟量为16ns
4  end
resetall用于重置除了宏定义以外所有被编译指令设置的项目到默认状态,形式为:resetall。

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

分享:

华章出版社

官方博客
官网链接