01-Verilog基本语法元素

时间:2024-02-01 12:13:01

不知道能不能更新完,毕竟咱学校计院对硬件向来不太重视,现在对竞赛也不咋地重视了,也不加分,也没啥用。嘛,就随便写写玩玩吧。

一只狸无聊的时候对Verilog的业余描述笔记:以《Verilog数字系统设计教程》第三版·夏宇闻为基础。

学Verilog前学C确实会很有帮助,学一点编译原理,有种自顶向下的快感。

Verilog模块

Verilog HDL行为描述语言作为一种结构化和过程性的语言,其语法结构非常适合于算法级和RTL级的模型设计。

在C语言中我们有函数,在Verilog中我们有模块。“模块”(block)是Verilog的基本设计单元,每个模块由 moduleendmodule 声明,描述了模块的接口和功能。每个Verilog程序都包括4个主要部分:端口定义、I/O说明、内部信号声明、功能定义。

端口定义

我们可以通过下面这个简单的3位加法器简单理解Verilog的模块:

module adder(
	input [2:0] a,
	input [2:0] b,
	input cin,
	output cout,
	output [2:0] sum
);	//端口声明语句

	assign {cout, sum} = a + b + cin;
endmodule

模块可以被引用,在引用模块时其端口可以用以下两种方法连接:

  1. 在引用时,严格按照模块定义的端口顺序来连接,而不用标明原模块定义时规定的端口名;
  2. 在引用时用“.”符号,标明原模块定义时的端口名。

两种引用的示例如下:

adder myAdder0(myA, myB, myCin, myCout, mySum);
adder myAdder1(.a(myA), .b(myB), .cin(myCin), .cout(myCout), .sum(mySum));

模块内容

模块内容包括I/O说明、内部信号声明和功能定义。

I/O说明

I/O包括输入口、输出口和输入/输出口,均示例如下:

//输入口
input 端口名;
input [信号位宽-1:0] 端口名;
//输出口
output 端口名;
output [信号位宽-1:0] 端口名;
//输入输出口
inout 端口名;
inout [信号位宽-1:0] 端口名;

I/O说明也可以写在端口声明语句里,值得注意的是前面的adder模块就是如此。

内部信号说明

在模块内用到的和端口有关的 wirereg 类型变量的声明。示例如下:

wire 变量名;
wire [信号位宽-1:0] 变量名;
reg 变量名;
reg [信号位宽-1:0] 变量名;

功能定义

功能定义决定了这个模块的逻辑功能。一共有三种方法可在模块中产生逻辑:

  1. 用“assign”声明语句,如 assign a = b & c; ,用于描述组合逻辑;
  2. 用实例元件,如 add #2 u1(q, a, b); ,这创建了一个双输入与门实例,延时为两个单位时间,注意实例名必须唯一;
  3. 用“always”块,它既可描述组合逻辑,又可描述时序逻辑。

Verilog数据类型及其常量和变量

Verilog HDL中总共有19种数据类型,数据类型是用来表示数字电路硬件中的数据储存和传送元素的。4种基本数据类型是: reg 型、 wire 型、 interger 型和 parameter 型。其他数据类型有 largemediumscalaredtimesmalltritriotriltriandtriortriregvectoredwandwor 。这14种数据类型除了 time 以外都与基本逻辑单元建库有关,与系统设计没有很大关系。

常量

数字

整型常量具有下面4种进制表示形式:二进制(b或B)、十进制(d或D)、十六进制(h或H)、八进制(o或O)。

整数表达方式有以下3种:

  1. <位宽><进制><数字>,这是一种全面的描述方式;
  2. <进制><数字>,数字的位宽将采用默认位宽(由具体机器系统决定,但至少32位);
  3. <数字>,在这种描述形式中,进制将采用默认十进制。

示例如下:

8'hff	//位宽为8十六进制整数

x和z分别代表不定值和高阻值,注意 wirereg 均为四态(0,1,x,z)的数据类型。使用和整数一样,示例如下:

8'h4x	//其低4位值为不定值

负数的表示只需要在表达式前加一个负号,注意这个负号必须写在最前面。示例如下:

-8'hff	//位宽为8十六进制补码
8'h-ff	//这是错误的

我们可以使用下划线来提高数字的可读性,但是它只能被使用在数字之间,不能被使用在位宽和进制处。示例如下:

8'b1111_1111	//位宽为8十六进制补码
8'b_1111_1111	//这是错误的

当常量不说明位数时,默认是32位;字符串则每个字母用8位的ASCII值表示。下面列出几个示例:

10=32'd10=32'b1010
-1=-32'd1=32'hFFFFFFFF
`BX=32'bX=32'bXXXX···X
"AB"=16'b01000001_01000010=16'h4142

参数(parameter)型

在Verilog HDL中用 parameter 来定义一个符号常量,即定义一个标识符代表一个常量。其格式如下:

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

注意表达式必须是一个常数表达式,且只能包含数字和先前定义过的参数。示例如下:

parameter msb = 7;
parameter e = 25, f = 29;
parameter r = 3.1;
parameter delay = (r + f) / 2;

参数型常数经常用于定义延迟时间和变量宽度。在模块或实例引用时,可通过参数传递改变在被引用模块或实例中已定义的参数。

变量

变量是在程序运行过程中可以改变的量。网络数据类型表示结构实体(例如门)之间的物理连接。网络类型的变量不能储值,且必须受到驱动器(例如门或连续复制语句,assign)的驱动,否则该变量就是高阻的,即值为z。常用的网络数据类型包括 wiretri

wire型

wire型数据常用于表示以 assign 关键字指定的组合逻辑信号。Verilog程序块中输出、输出信号类型默认时自动定义为wire型。wire型信号可以用做任何方程式的输入,也可以用作“assign”语句或实例元件的输出。其定义示例如下:

wire a;
wire [7:0] b;
wire [4:0] c, d;

reg型

寄存器时数据存储单元的抽象,寄存器(register)数据类型的关键字是reg。通过赋值语句可以改变寄存器存储的值,其作用与改变触发器存储的值相当。

reg类型的数据默认初始值为不定值x,它可以赋正值,也可以赋负值,但当一个reg型数据是一个表达式的操作数时,它的值被当作无符号的值,即正值(对于4位的寄存器,-1会被认为是+15)。

注意reg型只表示被定义的信号将用在“always”语句块内,,尽管其常常是寄存器或触发器的输出,但并不一定总是这样。

memory型

Verilog HDL通过对reg型变量建立数组来对存储器建模,可以描述RAM型存储器、ROM存储器和reg文件。数组中的每一个单元通过一个数组索引进行寻址。在Verilog中没有多维数组,memory型数据是通过扩展reg型数据的地址范围来生成的。其格式如下:

reg[n-1:0] 存储器名[m-1:0];
reg[n-1:0] 存储器名[m:1];

注意:对存储器进行地址索引的表达式必须是常数表达式。

在这里,reg[n-1:0]定义了存储器中每一个存储单元的大小,即该存储单元是一个n位的寄存器;存储器名后的[m-1:0]或[m:1]则定义了该存储器中有多少个这样的寄存器。另外,在同一个数据类型声明语句中,可以同时定义存储器型数据和reg型数据,示例如下:

parameter wordsize = 16, memsize = 256;
reg[wordsize-1:0] mem[memsize-1:0], writereg, readreg;

尽管memory型数据和reg型数据的定义格式很相似,但要注意其不同之处。一个由n个一位寄存器构成的存储器组是不同于一个n位的寄存器的,一个完整的存储器组不能在一条赋值语句中赋值,见下例:

reg [n-1:0] rega;	//一个n位的寄存器
reg mema [n-1:0];	//一个由n个1位寄存器构成的存储器组

rega = 0;	//赋值
mema = 0;	//这是非法的
mema[3] = 0;	//合法赋值

如果想对memory中的存储单元进行读写操作,必须指定该单元在存储器中的地址。进行寻址的地址索引可以是表达式,而表达式的值可以取决于电路中其他的寄存器的值。

运算符

Verilog HDL中运算符所带的操作数是不同的,按其所带的操作数个数可分为三种:

  1. 单目运算符(unary operator):可以带一个操作数,操作数放在运算符的右边;
  2. 双目运算符(binary operator):可以带两个操作数,操作数放在运算符的两边;
  3. 三目运算符(ternary operator):可以带三个操作数,这三个操作数用三目运算符分隔开。

其所用运算符和C语言非常相像。

算数运算符

在Verilog HDL中,算数运算符又称二进制运算符,列如下:

  1. +(双目:加法运算符,单目:正值运算符)
  2. -(双目:减法运算符,单目:负值运算符)
  3. *(双目:乘法运算符)
  4. /(双目:除法运算符,整数除法结果值要略去小数部分)
  5. %(双目:模运算符,要求两侧均为整型数据,符号位采用第一个操作数的符号位)

关系运算符

  1. < (双目:小于)
  2. > (双目:大于)
  3. <= (双目:小于等于)
  4. >= (双目:大于等于)

在进行关系运算时,如果声明的关系为真(true),则返回值是0,反之(false)为1;如果某个操作数的值不定,则返回值为不定值。所有关系运算符的优先级相同,且均低于算数运算符。

逻辑运算符

Verilog HDL中具有3种逻辑运算符:

  1. && (双目:逻辑与)
  2. || (双目:逻辑或)
  3. ! (单目:逻辑非)

其中“&&”和“||”的优先级低于关系运算符,“!”的优先级高于关系运算符。

条件运算符

和C语言一样,为三目运算符“?:”,用法参考C语言即可。

等式运算符

在Verilog HDL中具有4种等式运算符:

  1. == (双目:等于)
  2. != (双目:不等于)
  3. === (双目:等于)
  4. !== (双目:不等于)

这四个等式运算符的优先级是相同的。

其中“”和“!”为逻辑等式运算符,当操作数的某些位可能是不定值x和高阻值z时,其结果可能为不定值x。而“=”和“!”对不定值x和高阻值z也进行比较,“=”只有在两个操作数完全一致时结果才为1,它们常常用于case表达式的判别,所以又被称为“case等式运算符”。下面列出了“”与“===”的真值表:

=== 0 1 x z
0 1 0 0 0
1 0 1 0 0
x 0 0 1 0
z 0 0 0 1
== 0 1 x z
0 1 0 x x
1 0 1 x x
x x x x x
z x x x x

位运算符

注意硬件电路中有4种状态值,即0、1、x、z。对于不同长度,系统会自动将两者按右端对齐,位数少的操作数会在相应的高位用0填满。

  1. ~(单目:取反)
  2. &(双目:按位与)
  3. |(双目:按位或)
  4. ^(双目:按位异或)
  5. ^~(双目:按位同或/异或非)

运算规则如下:

~ 结果
1 0
0 1
x x
& 0 1 x
0 0 0 0
1 0 1 x
x 0 x x
| 0 1 x
0 0 1 x
1 1 1 1
x x 1 x
^ 0 1 x
0 0 1 x
1 1 0 x
x x x x
^~ 0 1 x
0 1 0 x
1 0 1 x
x x x x

移位运算符

Verilog HDL中有两种移位运算符:

  1. << (双目:左移位运算符)
  2. >> (双目:右移位运算符)

这两种移位运算符都用0来填补移出的空位。

位拼接运算符

Verilog中有一个特殊的运算符:位拼接运算符(Concatation)“{}”。用这个运算符可以将两个或多个信号的某几位拼接起来进行运算操作。使用方法如下:

{信号1的某几位, 信号2的某几位, 信号3的某几位, ... , 信号n的某几位}

注意每个信号必须指明位宽。

位拼接运算符也可以用重复法简化,示例如下:

{4{w}}	//等同于{w, w, w, w}

也可以嵌套:

{b, {4{a, b}}}	//等同于{b, a, b, a, b, a, b, a, b}

缩减运算符

缩减运算符(reduction operator)一共有三种:

  1. & (单目:与)
  2. | (单目:或)
  3. ^ (单目:非)

书上写的是“缩减运算符……也有与、或、非运算”,但是我瞅着这个非不是本来就是单目的嘛,最后查到应该是“^”,那不就是异或来着,具体是啥待考证……

缩减运算符是在信号的每个位之间进行运算:第一步先将操作数的第1位和第2位进行运算,然后将结果和第3位进行运算,一直运算到最后一位得出答案。下面给出几个示例:

a = 4’b1010;
&a //为1’b0
|a //为1’b1
^a //为1’b0

优先级

优先级由高到低排列如下:

  1. ! ~
  2. * / %
  3. + -
  4. << >>
  5. < <= > >=
  6. == != === !==
  7. &
  8. ^ ~^
  9. |
  10. &&
  11. ||
  12. ?:

语句和块

赋值语句

Verilog HDL中,信号具有两种赋值方式:

  1. 非阻塞(Non_Blocking)赋值方式(如b<=a;)
    ⑴ 在语句块中,上面语句所赋的值不能立即就为下面的语句所用;
    ⑵ 块结束后才能完成这次赋值操作,而所赋的变量值是上一次赋值得到的;
    ⑶ 在编写可综合的时序逻辑模块时,这是最常用的赋值方法。
  2. 阻塞(blocking)赋值方式(如b=a;)
    ⑴ 在赋值语句执行完后,块才结束;
    ⑵ b的值在赋值语句执行完后立即就改变的;
    ⑶ 在时序逻辑中使用时,可能会产生意想不到的结果。

上面是书上的描述,在时序逻辑电路中,或者在always块中,我们通常都是使用非阻塞的。阻塞的赋值方式更像是用导线之间连接起来的形式,它们是同时发生的;非阻塞的赋值方式似乎更像是接续进行的。

块语句

块语句通常用来将两条或多条语句组合在一起,使其在结构上看更像一条语句,某种程度上很像C语言的大括号(大括号事实上已经被用作位拼接了对吧)。块语句有两种:一种是begin_end语句,通常用来表示顺序执行的语句,用它来标识的块称为顺序块;另一种是fork_join语句,通常用来标识并并行执行的语句,用它来表示的块称为并行块。

顺序块

顺序块有以下特点:

  1. 块内的语句是按顺序执行的,即只有上面一条语句执行完成后下面的语句才能执行;
  2. 每条语句的延迟时间是相对于前一条语句的仿真时间而言的;
  3. 直到最后一条语句执行完,程序流程控制才跳出该语句块。

顺序块的格式如下:

begin
	语句1;
	语句2;
	...
	语句n;
end

begin: 块名
	块内声明语句
	语句1;
	语句2;
	...
	语句n;
end

语句中,块名即该块的名字,一个标识名;块内声明语句可以是参数声明语句、reg型变量声明语句、integer型变量声明语句和real型变量声明语句。

并行块

并行块有以下特点:

  1. 块内的语句是同时执行的,即程序流程控制一进入到该并行块,块内则开始并行地执行;
  2. 块内每条语句的延迟时间是相对于程序流程控制进入到块内的仿真时间的;
  3. 延迟时间是用来给赋值语句提供执行时序的;
  4. 当按时间时序排序在最后的语句执行完后或一个disable语句执行时,程序流程控制跳出该程序块。

并行块的格式如下:

fork
	语句1;
	语句2;
	...
	语句n;
join

fork: 块名
	块内声明语句
	语句1;
	语句2;
	...
	语句n;
join

语句中,块名即该块的名字,一个标识名;块内声明语句可以是参数声明语句、reg型变量声明语句、integer型变量声明语句、real型变量声明语句、time型变量声明语句和时间(event)说明语句。

块名

在Verilog HDL语言中,可以给每个块取一个名字,只需将名字加在关键词begin或fork后面即可。这样做的原因有以下几点:

  1. 可以在块内定义局部变量,即只在块内使用的变量;
  2. 可以允许块被其他语句调用,如disable语句;
  3. 在Verilog HDL语言中,所有的变量都是静态的,即所有的变量都只有一个唯一的存储地址,因此进入或跳出块并不影响存储在变量内的值。

基于以上原因,块名提供了一个在任何仿真时刻确认变量值的方法。

延迟控制

这里先引入Verilog HDL中的延迟控制语句。“#”是延迟控制的关键字符,延迟语句用于对各条语句的执行时间进行控制,从而快速满足用户对时序的需求。的延迟控制语句格式共有两种:

  1. 延迟时间 行为语句;

  2. 延迟时间;

其中延迟时间可以是直接指定的延迟时间量,并以多少个仿真时间单位的形式给出,可以是常量数字,也可以是变量或表达式。例子在后面就会看到。

起始时间和结束时间

在并行块和顺序块中都有一个起始时间和结束时间的概念。

对于顺序块,起始时间就是第一条语句开始执行的时间,结束时间就是最后一条语句执行完的时间。

而对于并行块来说,起始时间对于块内所有的语句是相同的,即程序流程控制进入块的时间,其结束时间是按时间排序在最后的语句执行结束的时间(当然这说的是指定了语句开始执行的延迟时间的情况下,如果并没有,那应该就是语句执行时间最长的那句执行结束的时间了吧[待考证]。有趣的是在这之前并没有讲关于语句延迟时间的问题,但是所给的示例程序中却已经出现了)。

例子

这里引用了书上的两个例子如下:

parameter d = 50;	//声明d是一个参数
reg [7:0] r;		//声明r是一个8位的寄存器变量
begin				//由一系列延迟产生的波形
	#d r = 'h35;
	#d r = 'hE2;
	#d r = 'h00;
	#d r = 'hF7;
	#d -> end_wave;	//表示触发时间end_wave使其翻转
end

fork
	#50 r = 'h35;
	#100 r = 'hE2;
	#150 r = 'h00;
	#200 r = 'hF7;
	#250 -> end_wave;	//表示触发时间end_wave使其翻转
end

其中的begin_end块和fork_end块的效果是一样的,虽然我们暂时还不知道这个end_wave在干啥,但是这并不影响我们对并行块和顺序块的理解。

by SDUST weilinfox