跳转至

Verilog 语言

Verilog语言

本章介绍Verilog语言,但是不准备把Verilog语言进行详尽的讨论。一个是没有篇幅,另外一个也没有必要。在掌握本章的内容之后可以进行实际的硬件设计,等待有需要的时候再学习更多的内容。在实践中学习是一个更好的学习硬件语言的方法。

概述

硬件描述语言是用来描述硬件各个组成模块以及它们之间关系的语言。当前广泛使用的硬件描述语言包括VHDL以及Verilog HDL。相对来说,Verilog HDL在实际的生产实践中使用得更多。在本实验教材中,就使用Verilog HDL语言来描述各个实验的设计。由于Verilog HDL语言有很多语法借鉴了非常广泛使用的C编程语言,因此相对来说学习Verilog HDL硬件描述语言是非常容易的。

在没有硬件描述语言之前,设计硬件的时候往往使用绘制电路图的方式。实际上,电路图方式仍然是非常重要的设计硬件电路的方法,特别是在设计硬件电路板,将多个芯片进行组合的时候。在实验课程中,硬件电路图也是非常重要的方法。在设计处理器的过程中,设计处理器内部的硬件电路图是一项非常重要的技能,对于编写硬件程序起到了非常重要的作用。在同学们实现自己处理器的过程中,也建议先使用图形化的方式来构建处理器的各个模块以及它们之间的连接关系,之后再通过硬件描述语言进行描述,会起到降低难度,易于理解的效果。

随着电子技术的发展以及大规模集成电路的规模与复杂程度的不断扩大,图形化的描述受到了极大的限制,已经不太可能跟得上所需要设计硬件的复杂性。在这种情况下,硬件描述语言应运而生,用以描述硬件的各个组成模块以及它们之间的连接关系。硬件描述语言可以被认为是与电路的图形化描述等价的描述,在当前的硬件设计领域占到了主导的地位。相对于图形化的电路设计方法,硬件描述语言具有以下的特点与优势:

  1. 硬件描述语言的抽象程度高,方便进行精确的描述。

  2. 可以应对不同层次的设计,方便进行自底向上和自顶向下进行硬件设计。

  3. 具有易于修改的特点,这一点尤为对图形描述有优势。

  4. 可以适合更大规模硬件的设计,模块化的方法使得各个部分可以协同工作。

  5. 匹配硬件描述语言的设计工具以及文档现在都非常丰富。

  6. 硬件描述语言本身就是对于硬件最好的说明文档。

下面Verilog HDL部分将简单介绍这个硬件描述语言的基本语法,并通过实例化的方式来说明各个语法单元的作用以及基本的硬件设计方法。

Verilog HDL的模块程序基本结构

在学习语言的时候,最方便的就是使用一个例子。在描述语言语法结构之前,可以通过一个具体的例子来看一下Verilog HDL程序的基本组成。下面是一个最简单的与门电路,用以将输入的信号做与操作,然后输出。

例 17.1 一个2输入的与门的逻辑描述。

module and2x(a,b,r);
  input a,b;
  output r;
  wire a,b,r;
  assign r=a & b;
endmodule

这个与门电路的程序非常的简明,但是已经突出了Verilog HDL进行电路描述的基本精髓。可以想象的出来,这里的程序最终会被转换为一个与门电路,在电路的物理实现上只需要一个组合数字电路中的与门逻辑就可以实现上述程序所需要的功能。下面看一下这个程序所能够体现出来的Verilog HDL程序的语言元素。

首先是Verilog HDL的程序是由模块来构造而成的,这也是硬件开发人员的基本思考单元。这样,开发人员在构建硬件的时候可以一个模块一个模块的进行,并可以通过多层次的连线,将小的模块连接成大的模块,最终来构造出目标的硬件。每一个模块的内容都包含在module和endmodule之间。

与软件程序一样,硬件模块编写的时候也需要定义输入输出。这一点对于硬件模块定义来说尤为重要,因为模块基本上就是定义输出是如何随着输入的变化而变化的。在上述的模块定义中,输入为端口a和b,输出为端口r。模块即定义r如何随着a和b的变化而变化。对于每一个输入输出来说,也同时需要定义其类型,这里的所有类型即wire信号类型,表示1位的数据。在类型之后,看到模块的主体部分即功能定义。这里的输出端口r的值为a和b的与操作。

在程序的书写上,VerilogHDL是自由书写的程序,并不需要特别地遵循固定的格式。这一点与C语言非常像,方便了设计人员。除了endmodule等几个少数的语句之外,每一个语句都需要以分号";"作为结尾。在注释上与C和C++是一样,使用"/* */"进行单行或者多行的注释,或者使用"//"进行单行的注释。

总结来看,以下几个元素是在Verilog HDL语言中最基本的组成部分。

(1)模块的声明和结束,模块以module开始,以endmodule结束,格式如下:

module 模块名称(端口1,端口2,端口3,......);

endmodule

这里的模块名称可以任取为符合要求的标识符。

(2)端口的定义:

对于模块的每一个端口都需要定义出其是输入端口(input),还是输出端口(output),还是双向端口(inout)。不仅要定义端口的输入输出类型,还需要定义端口的信号类型。输入和双向端口不能被定义为寄存器(reg,见下)类型。

(3)信号类型的声明:

在本实验教程中,信号类型声明只使用了两类,即寄存器类型reg以及连线类型wire。在Verilog HDL语言中定义的各种信号的类型实际上都对应着具体电路中的各种电路连接以及电路实体。如果没有任何声明的话,默认类型就是连线wire类型。为了书写方便,Verilog HDL还可以将定义和类型说明放在一起。并且可以直接放置在模块声明的端口列表中。例如,上述例子的类型声明可以使用下面的形式:

input wire a,b;

output wire r;

或者直接放到端口列表中:

module and2x(input wire a,b,output wire r);

因为wire是默认的,这里的所有的wire都可以省略。

(4)模块的功能定义:

这部分是编写模块最为主要的部分,这将定义当前模块所能够完成的功能。可以有多种方法来完成这个任务。上述的例子给出了一个最为基本的方法,即通过assign语句来进行持续的赋值。这个语句通常被用来进行组合逻辑的设计,在描述上也很明显,即持续对输入进行响应。当然,在实际的工作过程中,上述的逻辑还需要考虑一定的延迟,所有的电路都会有一定的延迟。延迟在编写程序的时候体现不出来,但是在综合出电路的时候会有很大的影响,相同的功能低延迟的电路会获得更高的性能。如何编写一个更加低延迟而且高效的电路需要长期的经验。

语言元素

这一节将介绍一下Verilog HDL的一些语言元素。本手册不打算全面介绍整个Verilog HDL语言,只会对其中的比较重要的部分展开讨论。可以先学习本章的内容之后再做进一步深入的学习,当然,本章的内容已经足够可以做一些比较大型的硬件设计。

Verilog HDL的语言元素

在Verilog HDL的语言元素中包括了空白字符,注释,操作符,数字,字符串,标识符以及关键字等几个部分。其中比较重要的是操作符,各种数字,标识符,以及关键字等。这几个语言元素直接决定了最后"综合"出来的电路的功能。这里的"综合"这个概念非常重要。综合类比于高级语言编译为机器语言并最终可以在物理硬件上执行。类似的,"综合"的含义就是将硬件描述语言的功能翻译为能够直接实现的电路,可以放到FPGA或者直接转化为硬件电路,用以执行所描述的功能。这些语言元素的大部分都是不言自明的,特别是如果有C语言基础的话非常容易理解。下面分别来看一下。

空白字符:

与其它的编程语言类似,在Verilog HDL中也由一系列的语言元素组成。与C语言一样,各个语言元素之间是通过空白的字符来隔开的。空白字符包括了空格,制表符'\t',回车,换行等。在进行编译的时候,空白字符被编译器自动用来切分出有功能的语言元素。由于Verilog HDL是自由风格的编程语言,空白字符对于程序的可读性是非常重要的。在进行程序编写的时候,建议可以通过合适的空白字符提高程序的可读性。

注释:

Verilog HDL的注释与C++语言的完全一样,前面也已经进行了说明,在这里不再赘述。

标识符:

Verilog HDL开发者所能够使用的最基本的元素是标识符,用以表示信号等在实际综合过程中所需要定义的连线以及寄存器等。在Verilog HDL中的标识符是由任意的英文字母、数字以及符号'\$'与'_'(下划线)构成的。标识符的第一个字符必须是字母或者是下划线,不能是别的内容。另外一个值得注意的是在Verilog HDL中与C语言中一样,标识符是区分大小写的,大小写不同的标识符被认为是不同的。以下都是一些合法的标识符。

例 17.2 合法的标识符举例

a
total
_delay
_d1_c2
D$
SOURCE

以下是一些非法的标识符举例,在实际开发中避免使用。

例 17.3 非法的标识符举例

1total //以数字开头
number# //使用了非法的字符
$5 //以$开头

关键字:

当然,还有另外一类的合法标识符是程序员所不能使用的,这就是关键字。和其它的所有的编程语言一样,关键字是语言内部的保留字,用以完成程序的部分功能。保留字是不能作为变量的名字的,不能为开发者所使用。另外,在Verilog HDL中,所有的关键字都是小写的。在前面已经接触到了Verilog HDL中的一些关键字。在后面的学习过程中,会接触到更多的关键字。本书接触到的关键字也不是Verilog HDL的所有的关键字,而是其中一部分所需要使用到的。对于关键字的全集,读者可以查询语言本身的规范。

参数:

在C语言中,一个良好的编程习惯是使用符号来代替直接进行常数硬编码。这一点在Verilog HDL中也是一样的。在Verilog HDL中可以使用parameter来定义一个符号常量。一个最为典型的应用就是来指定变量的宽度,即变量可以取几位。使用parameter的语法形式为:

parameter 参数名1=表达式1,参数名2=表达式2......;

例如:parameter WIDTH=16;

这样,可以使用WIDTH在程序中替代数值16。将来希望程序扩展到32位宽的时候,直接修改WIDTH的取值即可,而不需要在每一个地方都将16修改为32。这是一个良好的编程习惯,一方面便于修改,另外一个方面也增加了程序的可读性。

编译指导语句:

最后介绍一下在Verilog HDL中的编译指导语句。与C语言中的编译指导语句(例如#include)类似,在Verilog HDL中也有类似的编译指导语句来指示Verilog HDL编译器的工作。编译指导语句都是不可综合的,会在编译的时候进行字符串等替换操作,很快会看到它们与C语言对应的语言组成部分功能完全一样。

常用的编译指导语句包括`define宏替换语句,`include文件包含语句,`ifdef, `else, `elsif, `endif条件编译语句。

`define语句相当于C语言中的#define,使用在一定程度上与前面的parameter类似。在编译的时候,`define出来的宏名称被替换为后面的字符串。例如

`define WIDTH 16

reg[`WIDTH:1] r;

这就与reg[16:1]相当。也可以注意到,这里在每次使用宏名称的时候,需要加上`这样一个反向的单引号(美式键盘上1左边的那个键)。另外,在`define这一行的行末不需要分号,这与其它的实际功能语句不同的地方。

`define的宏替换功能与C语言中的宏替换功能一样强大,能够用来替换比较复杂的表达式。例如:

`define sum a+b

定义之后,可以使用:

assign res=sum;

来获得将两个a和b信号相加的效果,因为最终会被字符串替换,获得语句:

assign res=a+b;

`include语句相当于C语言中的#include语句,用来包含其它的文件。例如`include "adder.v",注意,这里同样也没有行末的分号。如果被包含的文件与源文件不在同一个目录下,则需要设置对应的目录,例如:

`include "../common/adder.v"

`ifdef, `else, `elsif, `endif条件编译语句也相当于在C语言中的#ifdef, #else, #elif, #endif语句,用来设定哪一部分的源代码会最终编译。这里的条件判断则需要看一下是否有对应的宏名称已经被定义过。例如:

例 17.4:条件编译语句

`define sum a+b

`ifdef sum

assign res=sum;

`else

assign res=a+b;

`endif

这样,就可以通过控制sum是否定义来选择对应的需要编译的源代码,不需要编译的源代码就被编译系统所忽略。如果需要嵌套更多的判断的话,可以使用`elsif编译指导语句进行进一步判断。

上述是在Verilog HDL中的常用的编译指导语句,对于平时使用来说已经足够了。当然,在Verilog HDL中还有其它的编译指导语句,在某些特定的条件下会需要用到。好在这些编译指导语句都非常直观明了,查询语言的手册很快就能够明白。

Verilog HDL中的数据

在数字逻辑电路,包括处理器的设计中,最为常用的数据显然是0和1,分别代表了逻辑真和假,电平的高和低。当然,在Verilog HDL中还提供了其它的数值类型。下面仅仅讨论一些必要的数据类型和取值,其它的一些数据类型和取值由于不常用将不再介绍。

常量,整数数值:

在硬件开发的过程中,不能够改变的量被称为是常量(constants)。在Verilog HDL中有多种常量的形式,包括了整数,实数以及字符串。在处理器设计中,最为重要的常量形式为整数,下面仅仅对整数常量进行介绍。

整数的常量是按照一定的格式写出的,下面是格式规范:

+/- \<位宽> ' \<进制>\<数字>

通常也表示为:

+/- \<size> ' \<base>\<value>

这里,size代表了二进制数的宽度,base为进制的情况,而value则是用对应的进制所表达的数值。

与其它的语言一样,进制包括了二进制(b或者B),十进制(d或者D),八进制(o或者O)以及十六进制(h或者H)。

下面是一些有效的整数数值例子:

例 17.5 整数数值举例

8'b01001010
16'H45EF
-8'D123
-16'o3333

上述是整数数值的规范的编写方式。另外,在'前面或者在进制与数字之间可以有空格,但是建议不要使用,统一到一个风格即可。至于实数以及字符串,由于使用不多,并且字符串也经常用在仿真的时候,是不可综合的,在这里就不再赘述。

数据取值:

有必要谈一下在Verilog HDL中的数据取值的问题。实际上,在进行硬件编码的时候,除了0和1的这样两个常用的信号取值之外,还有其它的一些逻辑状态可以作为信号的赋值。其中有两个比较重要的即为x(或者X)和z(或者Z)。这两个取值不区分大小写,使用大写或者使用小写代表有同样的含义。

x或者X的取值一般表明为不确定,或者未知的逻辑状态,用于不关心对应信号值的情况,不影响整个逻辑电路的功能。

z或者Z代表高阻态。在处理器设计中,高阻态一个非常典型的应用就是用于对内存的输入时序(从内存中读数据到处理器内部)。在进行数据输入的时候,先将处理器引脚的赋值状态置于z即高阻态,之后经过一定的时间延迟,就可以从对应的引脚处获得外部内存的输入值。

数据类型:

基本数据类型包括了wire类型和reg类型。wire类型代表了在硬件电路中的连线,其特征就是输出的值紧随着输入值的变化而变化。reg数据类型会放到过程语句中(例如always),通过过程赋值语句进行赋值。reg类型不一定必然会对应到硬件的寄存器,综合的时候会依据实际的情况使用连线或者寄存器来完成reg数据类型的功能。

数据类型还有向量和标量的区别。这一点非常简单,如果没有指定位宽,那么默认就是1位的位宽,即是一个标量。向量使用位宽来表达,使用中括号指定,形式为[msb:lsb]。其中,msb为最高位(most significant bit),lsb是最低位(least significant bit)。

例如:

wire[7:0] data; //这是一个8位的连线。

reg[31:0] res; //32位的数据变量

如果使用向量的话,在Verilog HDL中有非常方便的向量访问方式,获取其中的一位或者几位。

l = data[7]; //获取data的最高位

lob = res[7:0]; //获取数据res中的最低8位,即最低一个字节

Verilog HDL中的运算

运算的功能对于Verilog HDL来说非常重要,程序的功能描述大部分是通过运算来表达完成的。显然,在数字逻辑的硬件电路设计中,最为基本的就是位运算。在基本的运算基础之上,可以实现其它更为丰富的运算功能,例如加法运算。在Verilog HDL中也首先提供了各种位运算的功能,并且也提供了在此基础上的较为高层的运算功能。更为高级的运算就依赖于硬件设计人员的电路设计了。下面是一些常用的在Verilog HDL中的运算操作。

位运算符:

位运算符是最基本的运算符,从位运算符上开发人员直接可以想象得出最终的电路是如何通过逻辑门来实现的。与所有的编程语言一样,位运算符表达了两个操作数对应的位进行位运算的结果。在Verilog HDL中的运算符包括以下的操作。(在下面的例子中,假设a=8\'b00001001,b=8'b01010101。)

\~ 按位进行取反操作。\~a=8\'b11110110。

& 按位进行与操作。a&b=8\'b00000001。

| 按位进行或操作。a|b=8\'b01011101。

\^ 按位进行异或操作。a\^b=8\'b01011100。

\^\~或者\~\^ 这两个运算符都是按位同或操作。a\^\~b=8\'b10100011。

>> 右移运算符。a>>2=8\'b00000010。

\<\< 左移运算符。a\<\<2=8\'b00100100。

缩位运算符:

在位运算符中还有一类特殊的运算符需要注意一下,即缩位运算符,值得单独提出来作为一类描述。缩位运算符可以将一个向量按照一定的运算"缩"成1位,因此被称为是缩位运算符。缩位运算符有下面的几个。

& 与

\~& 与非

| 或

\~| 或非

\^ 异或

\^\~, \~\^ 同或

由于缩位运算符能够将多个位缩成一位,其表达式即可以写为操作符后面跟着一个向量的操作数。

例如:reg [7:0] value;

如果 value = 7'b01010101。则最终:

&value 结果为0,|value结果为1,\~\^value结果为1。

下面列出所涉及的位运算符的真值表:

a b a&b a\~&b a|b a\~|b a\^b a\~\^b


0 0 0 1 0 1 0 1 0 1 0 1 1 0 1 0 1 0 0 1 1 0 1 0 1 1 1 0 1 0 0 1

关系和逻辑运算符:

与位运算符直接相关的操作就是关系和逻辑运算符。这些运算符的直接应用就是应用在条件判断上(例如使用在if语句中)。下面是常用的关系和逻辑运算符,这些运算符的含义与其它语言的相同。这些运算符的取值结果可以被认为是true(1位的逻辑值1)或者false(1位的逻辑值0)。

&& 逻辑与运算符。

|| 逻辑或运算符。

! 逻辑非运算符。

\< 关系运算符小于。

\<= 关系运算符小于或者等于。

> 关系运算符大于。

>= 关系运算符大于或者等于。

== 关系运算符等于。

!= 关系运算符不等于。

=== 关系运算符全等。

!== 关系运算符不全等。

上述的大部分逻辑运算符无须解释,这里一个需要值得关注的就是全等运算符和不全等运算符,需要理解其与等于和不等于运算符的区别。

相等运算符(==)在进行比较的时候,需要按每位进行比较,只有所有的位都相等的时候,最后的结果值才会是true。但是如果其中的某一位是高阻态(用Z表示)或者是不定值(用X表示),那么最终的结果是不定值,这一点是需要值得注意的。对于全等(===)来说,对于这些高阻态或者不定值也需要进行比较,只有完全一致才会获得true的结果。因此,在进行真正比较的时候,需要注意这一点来选择合适的运算符。下表的例子清楚的说明了这一点。

A B A==B A===B


4b1101 4b1101 1 1 4b1100 4b1101 0 0 4b110Z 4b110Z X 1 4b11XX 4b11XX X 1

算术运算符:

算术运算符是常用的运算符,包括了加(+)、减(-)、乘(×)、除(÷)运算符,这些运算符可以用在整数的运算中。还有一个算术运算符与C语言中的运算符一致,即求余运算符(%)。可以看到,这些运算符并不是最基本的运算符,在其背后综合出的电路中,需要使用对应的门电路组织成的组合逻辑来完成。可以说,这是语言内部提供的高层的逻辑单元功能,方便在开发的时候直接集成使用,而不需要采用模块调用的方式。在处理器的设计中,算术运算符的最重要的作用是用来构成ALU,这样在设计ALU运算的时候会非常方便。当然,语言内部加法器,乘法器和除法器都是最基本的设计(但是不一定是性能差的,因为也经过了优化的过程),如果要设计更加高效的功能单元,则需要开发人员进行自行设计。

条件运算符:

在C语言中有一个需要三个参数的运算符,即条件运算符 "condition? result1 : result2"。这个运算符在Verilog HDL中也存在,也完成了相同的功能,即signal = condition ? expression1 : expression2。如果condition为true的话,signal取expression1的值,否则取expression2的值。

位拼接运算符:

还有一个重要的运算符即位拼接运算符"{ }"。这个运算符能够很方便把多个信号拼接为向量的形式。位拼接运算符的使用也非常方便,只需要把信号排列在大括号中间即可。{a[3:0], b[7:6],c}代表了将a的第3至第0位,b的第7位和第6位,以及信号c拼接在一起,构成一个新的信号向量。

以上就是在Verilog HDL中的常用的各种运算符,这些运算符在设计处理器的时候会被使用到,需要熟练掌握。还有一点没有提的是运算符的优先级,就不在这里列出了。在大部分的情况下只会在一个表达式中使用少数几个运算符,如果不太清楚优先级顺序的话,使用括号就可以了。

Verilog HDL的行为语句

关于硬件描述语言功能的讨论

行为语句是Verilog HDL的最为重要的功能语句,用来定义具体模块的行为。在开始讲述行为语句之前,先针对硬件描述语言的特性来讨论一下。可以看到,使用的是硬件描述语言这样的叙述方式,而不是硬件设计语言。这一点很重要,因为在硬件描述语言中,还真的不是进行硬件的设计,没有说明在一个模块内部是如何进行设计的,更多的是阐述模块对外的功能是怎样的,如何响应外部输入信号,如何形成输出到外部的输出信号。也就是说,在使用硬件描述语言的时候,更多的是描述对应的电路模块应该具有什么样的功能,而不是电路模块内部是由哪些元器件组成的,内部的构成如何。这样,就会有一个问题,由于符合Verilog HDL语法标准的代码是对硬件行为的一种描述,而不是对应的电路的设计,因此描述完成之后不一定是可以综合的。特别是对于高层的设计方法来说,更是这样,过于复杂的描述,往往很有可能就综合不出来。以当前大部分的EDA软件的综合能力来说,只有比较低层(例如RTL级别的,或者更低层)的行为描述才是能够保证可以综合的。高层的描述则需要注意,不要编写符合语法要求但是不能综合的行为描述。至于什么样的语句能够被综合,什么样的语句不能够被综合,除了一些常用的模式之外,这与软件的综合能力也相关。随着软件综合能力的增强,原来不能综合的也可能可以综合了。

对于开发人员来说,就需要增加自己的经验,使得自己开发出来的硬件描述可以在更多的平台上能够综合。这也没有捷径可以走,重要的一点就是在编写代码的时候有意识保证自己编写的语句可以综合,避免对综合软件造成困扰。在开发的时候,时刻要注意自己的程序最终是需要被转化为硬件电路的。开发的时候,需要极力避免按照程序顺序执行的方式去思考问题,而是按照电路的模块去思考,所有的功能都是并行执行的。哪怕是按照顺序方式描述出来的电路模块,只是描述了这个电路模块应具有的功能,在最终综合完成之后,电信号会在所有的导线上进行并行传播。特别的,各个模块之间都是并行执行的,而不是串行的。下面就要进入到Verilog HDL语言的行为语句部分的讨论,上述的关于硬件描述语言的讨论尤为重要。需要在阅读代码以及自己进行开发的时候牢记在心。

Verilog HDL的行为语句综述

Verilog HDL的行为语句与其它语言的语法功能(可以将前面的运算功能当成其它语言中的表达式)相当,包括了赋值语句,过程语句,条件语句,编译指导语句等。这些语句构成了Verilog HDL中对于功能描述的基本模式。

由于在Verilog HDL中,并不是所有的行为语句都是可以综合的,而不可综合的行为语句往往会被应用到仿真环境中。在实际编写Verilog HDL代码的时候,需要时刻注意这一点,清楚知道每一部分的代码是否需要反映到最终的硬件中。

在本章的后面部分主要讨论Verilog HDL的可综合的行为语句,对于仿真使用的不可综合的行为语句读者可以在理解本章的内容之后参考其它的Verilog HDL书籍。在Verilog HDL中的可综合的行为语句主要包括以下几个部分。

  1. always过程语句;

  2. 使用begin-end组合起来的语句块;

  3. 可以进行持续赋值的语句assign;

  4. 阻塞的过程赋值语句=,非阻塞的过程赋值语句\<=;

  5. for循环语句;

always过程语句

在Verilog HDL中,always过程语句显得尤为重要。可以说,绝大多数的硬件功能都是放在always过程语句中描述完成的。在一个模块中,always过程语句使用是不受限制的,可以有多个always过程语句。基于前面的讨论,一个模块的多个always过程语句是并行执行的。

always过程语句的使用方法如下:

always@(敏感信号列表)

语句(可以是一条语句,或者是语句块)

如果这里的语句只有一个语句,不需要加begin end来构成语句块,如果超过一个语句则需要通过begin end来构造成语句块。因此,实际上更加经常使用的形式是如下的通过语句块来表达的形式。

always@(敏感信号列表)

begin

//本过程的功能描述

end

下面通过一个四选一数据选择器的模块例子来说明always过程语句的各个组成部分。下面是这个四选一数据选择器的描述:

例 17.6:使用always过程语句描述四选一数据选择器

module mux4_1(din1, din2, din3, din4, se1, se2, out);
    input din1, din2, din3, din4, se1, se2;
    output reg out;
    always @(din1 or din2 or din3 or din4 or se1 or se2)
        case({se1,se2})
            2'b00 : out=din1;
            2'b01 : out=din2;
            2'b10 : out=din3;
            2'b11 : out=din4;
        endcase
endmodule

可以看到,在这个四选一的数据选择器中,四个输入信号分别为din1\~din4,两个选择信号分别为se1和se2,一个输出信号为out。在always过程语句的敏感信号列表中,列出了当前这个过程语句需要响应的敏感信号,即在敏感信号列表中的任何信号发生改变,那么这个always过程语句都需要被执行一遍。

这里可以看到,这个always过程语句的敏感信号包括了四个输入信号以及两个选择信号,任何一个发生变化,依据这个过程语句的描述,输出out都将发生变化。关于敏感信号列表方式可以有下面几种不同的形式:

always @(din1, din2, din3, din4, se1, se2)//所有的or都可以使用逗号','替换,书写起来更加方便。

另外,Verilog 2001对敏感信号的书写提供了更进一步的方便,在列表位置可以使用@(*)作为通配符来匹配所有输入信号作为敏感信号,即可以描述为:

always @(*)

或者,甚至可以把括号都省略 always @*

最后一点是关于列表中的敏感信号的类型。敏感信号可以分为两个类型,一个是电平敏感型,一个是边沿敏感型。电平敏感型在发生电平变化,从0变成1或者从1变成0的时候,always过程语句会依据变化完成的值执行一次。而边沿敏感型则可以进一步分为上升沿触发或者下降沿触发。在发生了一次上升沿事件,或者下降沿事件的时候,触发always过程语句的执行。在Verilog HDL中,使用posedge指定上升沿,使用negedge指定下降沿。可以将边沿敏感类型的信号放置到always过程块的敏感信号列表中,下面是一个常用的always过程的敏感信号列表。

always @(posedge clk)

这里响应的是一个时钟clk的上升沿信号,一旦一个时钟的上升沿发生,那么下面的always过程语句就发生执行。这个是驱动处理器执行的基础,在进行综合的时候会综合出时序电路。

在上述电路中,由于在always过程语句的功能描述部分只有一条case语句,因此不需要使用begin/end来组织成语句块。case语句的使用非常方便,从上面的例子中可以看到,其格式为:

case({se1,se2})

endcase

在case语句的内部对不同的情况进行判断,然后进行处理,具体可以参见18.4.6节。另外,这里又看到了使用大括号'{}'构成的位拼接运算符的作用。

begin/end 块语句

begin/end能够将多条语句组合成语句块。当然,如果只是一条语句的话,使用begin/end也没有问题,就是多此一举而已。下面是一个译码器的例子,可以清楚看到通过begin/end来构造出一个语句块。

例 17.7:使用begin/end构造语句块

module decoder2_4(in,out);
    input [1:0] in;
    output reg [3:0] out;
    always @(in) begin
        out = 4'b0000;
        case(din)
            2'b00 : out = 4'b0001;
            2'b01 : out = 4'b0010;
            2'b10 : out = 4'b0100;
            2'b11 : out = 4'b1000;
        endcase
    end
endmodule

明显看到在上述的语句块中,因为需要一个初值,在begin/end中有两条语句,就必须构造出一个语句块。另外一个值得注意的一点是,begin/end构造出的语句块也往往被称为是串行块,其含义就是在begin/end之间的语句是顺序执行的。这里也体现出了硬件描述语言的特点。硬件不可能像软件一样会逐步的一条一条执行指令,在硬件综合完成之后,各个部分的电信号就开始驱动整个硬件电路信号扩散,并逐步稳定下来。而这里所谓的顺序执行,实际上是为了开发者阅读方便,"说明"这部分电路的功能,而不是实际上硬件就是这样执行的。这里的硬件在被综合出来之后就是一个常用的2-4的译码器,输出会随着输入的变化而随时变化。

赋值语句

赋值语句可以说是任何一门编程语言的最基本的部分。对于Verilog HDL来说也不例外,通过赋值语句可以将不同的信号组织起来。在Verilog HDL中,赋值语句包括了持续赋值语句和过程赋值语句。持续赋值语句在过程外使用,与过程语句并行执行。过程赋值语句在过程内使用,串行执行,用于描述过程的功能。

先来看一下持续赋值语句。在Verilog HDL中使用assign作为持续赋值语句使用,用于对wire类型的变量进行赋值。其对应的硬件也非常好理解,即通过对输出进行赋值,当输入变化的时候,经过一定的延迟,输出就会按照assign所描述的那样发生变化。

例如:assign res = input_a & input_b;

在这个例子中,输入input_a和input_b,输出res都是wire类型的变量。当两个输入的任意一个发生变化的时候,输出res都会发生变化。当然,这样的一个变化不会是立即的,而是需要经过一定的延迟,因为任何一种电路都会有延迟。在一个模块中,可以有多个assign的持续赋值语句,这些持续赋值都是并行执行的,一旦赋值语句中的任何信号发生变化,那么这个赋值语句的输出信号(赋值等号的左边那个信号)就会跟着变化。一个模块的持续赋值语句和前面所说的always过程语句都是可以出现多次的,他们之间的执行关系也是并行的,对应于电路上的信号值的变化。不同的是assign持续赋值语句由于表达能力的限制,只能反映一些简单的变化,而always过程语句则可以复杂很多,用以描述复杂的输出信号和输入信号的关系。

在always过程里面也可以有赋值语句,这也是必不可少的。在过程里面的赋值语句被称为是过程赋值语句,一般用来对reg类型的变量进行赋值。过程赋值语句分为两种类型,一个是非阻塞赋值语句(\<=),一个是阻塞赋值语句(=)。它们之间的区别是:

1)非阻塞non-blocking赋值语句(\<=)在赋值语句出现的地方不是立即发生的,而是等到整个过程块结束的时候才发生。由于不是立即发生的,在过程内的描述中,仿佛这条语句不存在一样,因此被称为是非阻塞的。只是在过程的最后会执行所有的非阻塞赋值语句,在这个执行的过程中,所有的右值会维持原来的值不变。

2)阻塞blocking赋值语句(=)在赋值语句出现的地方就立即完成赋值操作,左值立刻发生变化。一个块语句中存在多条阻塞赋值语句的话,这些阻塞赋值语句会按照先后顺序关系,一条一条的执行,前面的赋值语句没有执行完,后面的赋值语句不会执行。这样的一种行为模式,就跟网络IO编程中的阻塞函数调用方式一样,一定要完成函数执行之后,这个函数调用才会退出。

可以看到,这里涉及的阻塞和非阻塞概念与传统的网络编程中的阻塞和非阻塞的概念是一致的。不同的是传统的非阻塞编程需要最后使用一个同步语句(例如select和poll)来完成同步,而这里的非阻塞会统一在过程块结束的时候执行。

下面的例子非常适合用来说明阻塞和非阻塞在执行上的效果差别。

例 17.8 阻塞赋值语句的使用:

module blocking(clk, a, c);
    input clk, a;
    output wire c;
    reg b;

    // 这是正确的语法,但是不建议在时序逻辑中使用阻塞赋值
    always @(posedge clk) begin
        b = a;
        c = b;
    end
endmodule

例 17.9:非阻塞赋值语句的使用

module nonblocking(clk, a, c);
    input clk, a;
    output reg c;
    reg b;

    always @(posedge clk) begin
        b <= a;
        c <= b;
    end
endmodule

在上述的例子中,前面一个是阻塞赋值语句的例子,后面一个是非阻塞赋值语句的例子。在前面的例子中,最后b和c的值都统一改为a的值。因为由于阻塞的特性,b=a必须要先完成,之后的c=b执行的就是c=a的操作。而后面的例子中,由于是非阻塞的赋值,所有赋值起到效果的是在过程结束的时候。在过程结束的时候,b就被赋值为a的值,而c被赋值为b之前的值。这个效果从最终的模拟信号波形图可以看得很清楚。

图18.1 阻塞赋值的仿真波形图

图18.2 非阻塞赋值的仿真波形图

上述的阻塞赋值和非阻塞赋值的最终的综合出来的电路图当然是不同的。

图18.3 阻塞赋值综合结果

图18.4 非阻塞赋值综合结果

上述两个图可以看到,非阻塞赋值要比阻塞赋值多加一个触发器。这显然是因为在非阻塞情况下,信号b和c的变化不是同步的,这样就需要通过一个触发器进行一个周期的延迟。

条件语句

几乎所有的编程语言中都会包含条件语句,并且几乎都使用了if这样的最为常见的方式。在Verilog HDL中,条件语句包括了if-else语句以及case语句。这两个语句在Verilog HDL中的使用方法与在其它的语言中几乎完全一样。

对于if-else的条件语句来说,主要有下面的三种使用形式:

(1)if(逻辑表达式) 语句1;

(2)if(逻辑表达式) 语句1;

else 语句2;

(3)if (逻辑表达式1)语句1;

else if (逻辑表达式2) 语句2;

else if (逻辑表达式3) 语句3;

........

else if (逻辑表达式n) 语句n;

else 语句n+1;

上面这三种形式无非就是只需要一个判断,或者需要多个判断,以及是否需要增加else匹配后面的动作等的形式。这三种形式的使用方式是不言自明的,这里就不再进行解释。

例 17.10:使用if-else语句实现译码器

module decoder2_4(din,dout);
    input [1:0] din;
    output reg [3:0] dout;
    always @(din) begin
        dout = 4'b0000;
        if (din == 2'b00)
            dout = 4'b0001;
        else if (din == 2'b01)
            dout = 4'b0010;
        else if (din == 2'b10)
            dout = 4'b0100;
        else if (din == 2'b11)
            dout = 4'b1000;
    end
endmodule

在Verilog HDL中也提供了case这样的条件判断语句。case的条件判断语句主要是为了方便使用,避免使用过多的if-else进行编写。对于条件语句中的case语句来说,使用的是下面的语法形式:

case (敏感表达式)

条件判断1:语句1;

条件判断2:语句2;

.........

条件判断n:语句n;

default:语句n+1

endcase

可以看到,在敏感表达式中会计算出不同的条件,进而会跳到不同的语句去执行。需要注意的是,这里的语句就直接是分支的语句,不需要像C语言一样在其中插入break,在语句执行完成后,直接跳出了case语句本身。也就是说,执行完语句1之后,就直接退出了整个case语句,而不是继续执行语句2。在C语言中,如果没有break语句,剩余的语句会逐一执行的,直到碰到break或者case语句完成。Verilog HDL在这一点上来看,对于程序员来说更加友好。前面已经有case语句的例子,在这里就不再进行举例了。

循环语句

在Verilog HDL中也存在循环语句。可以综合的循环语句为for语句,并且这个for语句和C语句中的for语句使用形式完全一样。for语句的形式为:

for (表达式1;表达式2;表达式3)语句;

也即:

for(循环变量赋初值;循环结束条件;循环变量增值)执行语句;

上述的展开表达分别解释了表达形式中的表达式1,表达式2和表达式3的含义。可以看到,确实与C语言中的对应的for循环语句完全是一样的。

对于循环语句来说,不太容易想象得出综合之后的效果,因为这方面并不直观。这一点确实是这样,对于循环来说,综合器处理起来也并不容易,并且对于不同的综合器来说,不一定是可以综合的。相比于前面讨论的语句(赋值和判断语句),for循环语句描述的功能更加高层和抽象(也包括其它类型的循环语句)。虽然写程序容易,但是转化为硬件的难度会更大。即便转化完成,可能所需要的硬件资源也很多,效率不高。因此,除非是一些对语句进行重复设置的情况,尽量不要使用循环语句,以免对综合器造成困扰。

除了上述的for语句之外,在Verilog HDL中也有其它三个循环语句,分别为forever语句,repeat语句,while语句。其中forever语句会连续执行语句,主要在仿真中使用,用以生成周期性的波形,例如时钟信号。

repeat语句的使用格式为:

repeat(循环次数的表达式)

begin

语句或者语句块

end //单个语句不需要begin和end

while语句的使用格式为:

while(循环执行的条件表达式)

begin

语句或者语句块

end //单个语句不需要begin和end

由于repeat语句和while语句的功能实际上都可以通过for语句表达出来,另外也由于for语句在大部分的EDA工具中都是可以综合的,而repeat和while往往是不可综合的,因此在自己编写代码的时,如果需要生成可以综合的代码,尽量使用for语句来实现循环。但是,正如上面所说的一样,综合出来的效率不一定很高,应该谨慎使用。

例子3.11:for循环语句

module for_adder(a,b,cin,sum,cout);
    input [7:0] a,b; input cin;
    output reg[7:0] sum; output reg cout;
    reg c;
    integer i;
    always @* begin
        c = cin;

        for (i = 0; i < 8; i++) begin
            {c,sum[i]} = a[i] + b[i] + c;
        end
        cout = c;
    end
endmodule

这里是一个通过for循环语句来实现的一个加法器。当然,真正的加法器并不需要这样实现,这里是对for语句的功能进行举例。可以看到,在过程语句里面的for循环描述了每一位加法的过程,并最终获得结果。从这个for循环语句看不出最终的电路会是什么样子的,因此这是一个非常明显的功能描述的代码,描述的层次已经比较抽象,而不是功能设计的代码。

Verilog HDL的设计层次与风格

在Verilog HDL中,可以使用不同的方式来进行电路的设计,有的时候也会给初学者很大的困扰。因为语言有很大的灵活性,对于相同的电路可以有不同的设计方法。这种灵活性可能会对初学者来说不太好掌握。下面就通过一个1位全加器的简单例子来说明在Verilog HDL中的不同的设计层次与设计方法。

1位的全加器的输入包括1位的低位进位cin,两个一位的输入信号a和b,输出则包括了一个当前位的和sum以及向高位的进位cout。全加器的电路图可以直接从1位全加器的真值表中获得,大部分的数字电路以及组成原理的教科书都有1位全加器的例子,可做参考。

a b cin cout sum
0 0 0 0 0
0 0 1 0 1
0 1 0 0 1
0 1 1 1 0
1 0 0 0 1
1 0 1 1 0
1 1 0 1 0
1 1 1 1 1

从1位全加器的真值表可以获得1位全加器的逻辑表达式(注意这里只使用了与或非门的表达,如果使用其它的逻辑运算符可以进一步简化):

CarryOut = (¬A*B*CarryIn)+(A*¬B*CarryIn)+(A*B*¬CarryIn)+(A*B*CarryIn)

=(B*CarryIn)+(A*CarryIn)+(A*B)

Sum = (¬A*¬B*CarryIn) + (¬A*B*¬CarryIn) + (A*¬B*¬CarryIn) +(A*B*CarryIn)

这样就可以很容易获得1位全加器的电路表达形式:

图 18.5 位全加器的电路图

当然,上面已经完成了一个一位全加器的设计,并且已经设计出了电路。也就是说,真正的工作已经完成了,这实际上就是传统的硬件设计方法。作为例子,在这里首先把一个已经实现的电路绘制出来,然后再逐步讨论使用Verilog HDL进行不同的描述。目的就是展示出真正的硬件之后,再展示不同的描述,用以说明硬件与对应的描述之间的关联。

在上述的电路中,使用了三个非门(not),四个3输入的与门(and),三个2输入的与门(and),一个4输入的或门(or),一个3输入的或门(or)。这里的非门,与门和或门都是Verilog HDL中的内置的门电路,可以直接使用。这样,可以将上述电路中的每一条线进行命名,然后就可以直接构造出Verilog HDL的结构描述(调用门元件)。当然,如果是直接是输入线,就不必命名了,使用输入的名称即可,同理对于输出线可以使用输出的名称。

例 17.12:1位全加器的门级结构描述

module full_adder1(a,b,cin,sum,cout);
    input a,b,cin;
    output sum,cout;
    wire a_n, b_n, cin_n, sum_p1,sum_p2,sum_p3,sum_p4, cout_p1, cout_p2, cout_p3;
    not(a_n,a),(b_n,b),(cin_n,cin);
    and(sum_p1,a_n,b_n,cin),(sum_p2,a_n,b,cin_n),(sum_p3,a,b_n,cin_n),(sum_p4,a,b,cin),(cout_p1,b,cin),(cout_p2,a,cin),(cout_p3,a,b);
    or(sum,sum_p1,sum_p2,sum_p3,sum_p4),(cout,cout_p1,cout_p2,cout_p3);
endmodule

注意,上述所有的内置门电路(not,and,or门),第一个参数为输出,剩余的参数为输入。可以看到,上述1位全加器的门级结构描述直接描述了各个门之间是如何连线的。由于是文本文件,因此连线不像图形那么直观。但是,从代码中的各个信号线的命名,以及对应于命名完成的信号线作为门级的调用参数来看,各个门原件的连线就比较清楚了。门级结构描述虽然不是最底层的描述(比如直接用晶体管搭建),但是已经非常接近底层的描述,可以被直接综合出来,使用元件进行直接映射即可。但是,门级结构描述的缺点也很明显,要求用户自行完成门级的设计,直接映射到硬件。这种方法一般用于设计比较简单的电路,或者设计非常高效工作的电路,方便综合器直接进行综合。当然,这里的门级结构描述还有一个作用,即其结构描述的方式可用于通过部分的逻辑电路模块来构造更加大型的电路模块。这个等讨论完其它两种描述之后再回顾一下会更加清楚。

门级结构描述虽然方便了底层的综合器,但是对于编程来说不太方便,开发者希望能够进行更加高层的设计。一个选择就是将上述的逻辑表达式直接写到程序里面这就是数据流描述的方法。

例 17.13:1位全加器的数据流描述

module full_adder1(a,b,cin,sum,cout);
    input a,b,cin;
    output sum,cout;
    assign sum = (~a&~b&cin)|(~a&b&~cin)|(a&~b&~cin)|(a&b&cin);
    assign cout = (b&cin)|(a&cin)|(a&b);
endmodule

这里的数据流描述方法,说明的就是在组合逻辑中,输出是如何随着输入数据的变化而变化,使用的是持续赋值语句assign。这些持续赋值语句说明了数据流的变化情况以及它们之间的逻辑关系。可以看到,持续赋值语句的抽象层次要比前面的门级结构描述更加抽象。只要有了逻辑表达式关系,直接翻译为Verilog HDL中的运算符即可,而不用仔细考虑底层的门电路构成。但是,数据流描述的抽象层次还不是很高,因为已经很难从给出的数据流中看到这是一个1位的全加器。实际上,对于复杂的硬件逻辑设计来说,使用行为级描述更为妥当,即直接描述出硬件所需要完成的功能,而不需要考虑这些硬件具体是如何实现的。具体如何实现交给EDA综合软件去做。

例 17.14:1位全加器的行为级描述

module full_adder1(a,b,cin,sum,cout);
    input wire a, b,cin;
    output reg sum,cout;
    always @* begin
        {cout,sum}=a+b+cin;
    end
endmodule

从行为级描述中完全看不到最终的电路是怎样使用元件以及怎样布线的,但是这个程序非常简单明了。这对于开发人员来说极为方便,直接能够看出是一个全加器,因为这一段程序完整的描述了一个全加器所需要完成的功能。

上面的结构级描述,数据流级描述以及行为级描述就是Verilog HDL开发过程中可以使用的三种不同层次的对于硬件的描述方法。结构级描述直接描述了硬件电路的结构,最为具体,但是不够抽象。数据流描述更加接近传统的逻辑设计,抽象程度中等。行为级描述只需要抽象描述一个硬件的功能单元完成什么样的功能即可,不需要说明硬件是如何构造的,最为抽象。在实际的设计过程中,这三种方式可以互相混合使用,针对不同的电路可以选择不同的描述方式。

在设计更加大型的硬件电路的时候,使用结构级描述是必不可少的。在前面的例子中,已经看到是如何通过调用门级的基本逻辑单元来完成全加器的功能。这样的方法在设计更加大型的电路中也是相同的。可以设计一些小型的电路模块,然后通过结构的描述来设计出规模更大的电路。下面就通过设计4位的加法器来说明结构描述在设计大型电路时候的作用。

4位的加法器的构成使用了4个1位的加法器,通过级联之后就可以获得。这里的构成如图所示。

图18.6 由4个1位的加法器构成一个4位的加法器

通过对其中的信号进行命名,可以通过结构描述的方式来描述上述的电路。实际上,这里需要命名的信号就是进位到前一级的进位,可以分别命名为cin1,cin2,cin3。上述电路的描述如下。

例 17.15:1位全加器的行为级描述

module full_adder4(a,b,cin,sum,cout);
    input cin;
    input [3:0]a,b;
    output [3:0]sum;
    output cout;
    full_adder1 a0(a[0],b[0],cin,sum[0],cin1);
    full_adder1 a1(a[1],b[1],cin1sum[1],cin2);
    full_adder1 a2(a[2],b[2],cin2sum[2],cin3);
    full_adder1 a3(a[3],b[3],cin3sum[3],cout);
endmodule

可以看到,这里的结构级描述与之前的门级结构描述在形式上是完全一样的。例 17.12门级结构描述调用的是语言内建的元件,但是在这里调用的是开发者自己的模块。同时,在这里也看到了调用的另外一种形式,即可以对每一次调用进行命名,分别命名为a0, a1, a2, a3。

到现在为止,基本上对于Verilog HDL的编程语言进行了一个基本的介绍。当然,这里介绍的是Verilog HDL中的最基本的内容,希望能够帮助读者对Verilog HDL这样一个语言有一个最基本的了解。在实际进行硬件设计的时候,最基本的方法还是自顶向下的方法,对硬件的总体先分成多个互相独立的模块,然后定义模块之间的连线关系。之后,每一个独立的模块可以进行分别设计,连线关系即是它们之间的接口。最终完成的硬件通过结构描述的方式将设计完成的模块连接在一起。

一些编程建议与经验

下面的内容与Verilog语言的语法不直接相关,但是会讨论一些经验以及常见的错误。这里的一些常见错误同学平时也会碰到,建议先阅读理解大概的含义,在具体编程碰到问题的时候也可以返回本节查看与理解。也注意多与同学讨论,少走弯路,减少调试难度。另外,一些具体实验中可能碰到的困难与错误已经列举在前面的实验提示部分。

计算机组成原理的实验是硬件实验,使用的是硬件描述语言Verilog。做实验之前一定要首先熟悉一下Verilog硬件描述语言的语法以及惯用的方法。惯用的方法可以从例子中学习。当然,编写正确代码也需要经验。Verilog代码是硬件描述语言,这个跟软件的代码语言完全不同,不能想当然从软件的角度去思考。Verilog只是描述硬件模块应该达到的功能,但是没有描述内部的结构,不能直接说明内部是如何的。这里关键的一点是所有的硬件模块都是并行执行的,是信号从输入到输出的传递的过程。除了Verilog的语法之外,下面的一些与语言相关的提示需要大家注意一下,会有一些帮助。

`default_nettype none

`default_nettype none 这是建议的做法。在Verilog中,所有没有被定义的标记label都被默认认为是wire类型的。但是,这种默认的行为有的是非常危险的,比如在信号名字上出现拼写错误不会被探测出来。因此,建议在所有的源文件的开始加上这一句,取消默认行为。

锁相环电路

PLL是FPGA上专用的时钟生成模块,内部是模拟电路。在启动时需要一段时间才能进入稳定状态,所以有个locked信号输出,表示它稳定了。具体的可以参考前面关于锁相环电路的讨论。

调时序:

调时序:不同模块之间,由于寄存器的关系,会有相位差,然后需要增加几个空的状态机节拍。哪个path太长了就加寄存器打一拍。

阻塞赋值语句和非阻塞赋值语句:

关于=和\<=:一般来说,组合逻辑用=,时序逻辑用\<=。但是实际上wire和reg仅仅是语法层面的东西,assign的左值必须是wire,always里的左值必须是reg,否则综合就会报错。最终是否综合成触发器,是根据有没有时钟信号决定的。综合器怎么知道哪个是时钟信号呢?通过posedge的描述方法可以知道对应的模块里面需要响应正边沿(posedge)或者是负边沿(negedge),从而会综合出触发器。

在Verilog的语法上,阻塞的过程赋值语句=,非阻塞的过程赋值语句\<=。在always过程里面的赋值语句被称为是过程赋值语句,一般用以对reg类型的变量进行赋值。过程赋值语句分为两种类型,一个是非阻塞赋值语句(\<=),一个是阻塞赋值语句(=)。它们之间的区别是:

1)非阻塞non-blocking赋值语句(\<=)在赋值语句出现的地方不是立即发生的,而是等到整个过程块结束的时候才发生。由于不是立即发生的,在过程内的描述中,仿佛这条语句不存在一样,因此被称为是非阻塞的。只是在过程的最后会执行所有的非阻塞赋值语句,在这个执行的过程中,所有的右值会维持原来的值不变。

2)阻塞blocking赋值语句(=)在赋值语句出现的地方就立即完成赋值操作,左值立刻发生变化。一个块语句中存在多条阻塞赋值语句的话,这些阻塞赋值语句会按照先后顺序关系,一条一条的执行,前面的赋值语句没有执行完,后面的赋值语句不会执行。

程序的可读性:

写Verilog的代码和写软件应用程序的代码一样,有一件事情需要特别注意,就是提高程序的可读性,增加程序的可维护性。在选择信号名称的时候需要按照名称选择的惯例,有一些名称是常用的,按照其惯常的用法就可以了,不要改变其名字的用法。下面是一些名字使用的惯例,同学们应当遵守来提高自己程序的可读性。

_i, _o,分别代表一个模块的输入信号和输出信号。

n或者_n为后缀,表明这个信号是0使能。

clk,clock时钟信号,后面或者前面接上频率,可以显示时钟信号的频率。

rst,reset重置信号,使得信号可以重置,一般在重置响应中写入状态机的初值。

we,write enable信号,对应于模块的写入使能。

oe,output entable信号,对应于模块的读取使能。

ce,chip enable,对应于模块的总体使能信号。上述的信号几乎在所有的模块中都会有。(注意信号是正向的还是反向的,即1使能还是0使能。在说明书中,0使能会在信号名称的上面带有横线。)

select,sel,这个信号一般用于对芯片的选择。

其它的信号也会有惯例使用的情况,大家在平时的时候可以多注意学习。

代码检查工具:

下面的网址中有一些Verilog的工具可以供大家参考。

https://www.veripool.org/

verilator --lint-only -Wall [source_files.v]... 可以帮助做一些检查。

一些特殊的语法点:

ram_address = pc[2+:21];

含义就是从第2位开始的21位,就是2:22,就是把最后两位去掉。

或者写成pc[22:2]也是一样的。

case语句可能出现错误的情况:

在使用case的时候,一定要注意把所有的信号在所有的情况下写全了,要不然有一些信号就没有定义,会造成错误。或者可以灵活使用赋值语句,=,在过程最前面的时候先进行赋值。

下面的case代码块是正确的:

always @(*) begin
    ram_we_n = 1'b1;
    ram_oe_n = 1'b1;
    ram_address = 21'h0;
    case(state)
        STATE_FETCH: begin
            ram_oe_n = 1'b0;
            ram_address = pc[22:2];
        end
        STATE_MEM: begin
            ram_oe_n = mem_op_write;
            ram_we_n = ~mem_op_write;
            ram_address = ex_val_o[22:2]; 
        end
    endcase
end

下面的case语句代码块也是正确的:

always @(*) begin
    case(state)
        STATE_FETCH: begin
            ram_oe_n = 1'b0;
            ram_we_n = 1'b1;
            ram_address = pc[22:2];
        end
        STATE_MEM: begin
            ram_oe_n = mem_op_write;
            ram_we_n = ~mem_op_write;
            ram_address = ex_val_o[22:2]; 
        end
        default: begin
            ram_oe_n = 1'b1;
            ram_we_n = 1'b1;
            ram_address = 21'h0;
        end
    endcase
end

但是下面的case语句代码块是错误的,为什么?(所有情况没有考虑完善)

always @(*) begin
    case(state)
        STATE_FETCH: begin
            ram_oe_n = 1'b0;
            ram_address = pc[22:2];
        end
        STATE_MEM: begin
            ram_oe_n = mem_op_write;
            ram_we_n = ~mem_op_write;
            ram_address = ex_val_o[22:2]; 
        end
        default: begin
            ram_oe_n = 1'b1;
            ram_we_n = 1'b1;
            ram_address = 21'h0;
        end
    endcase
end

always过程语句的敏感信号

Verilog规定,always@(*)中的*是指该always块内的所有输入信号的变化为敏感列表,也就是仿真时只有当always@(*)块内的输入信号产生变化,该块内描述的信号才会产生变化。

Warning: empty statement in seqential block

如果两个分号放在一起的话";;"",就会出现这个警告。删掉一个分号即可。这里一个容易出现的错误是在信号常数定义`define的时候,不小心在信号后面跟了一个分号,例如`define OP_ADD 4'h9; 这里多了一个分号。在模块代码里面直接使用的时候就会出现上面的情况。


最后更新: 2021年10月25日