当前位置 博文首页 > 八衛門狸:01-Verilog基本语法元素
不知道能不能更新完,毕竟咱学校计院对硬件向来不太重视,现在对竞赛也不咋地重视了,也不加分,也没啥用。嘛,就随便写写玩玩吧。
一只狸无聊的时候对Verilog的业余描述笔记:以《Verilog数字系统设计教程》第三版·夏宇闻为基础。
刚初学几周,很多地方理解不透。不过学Verilog前学C确实会很有帮助,再理解一点点编译原理,有种自顶向下的快感。有些地方渲染有点奇怪,改了一些,不知道有没有漏的。
Verilog HDL行为描述语言作为一种结构化和过程性的语言,其语法结构非常适合于算法级和RTL级的模型设计。
在C语言中我们有函数,在Verilog中我们有模块。“模块”(block)是Verilog的基本设计单元,每个模块由 module
和 endmodule
声明,描述了模块的接口和功能。每个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
模块可以被引用,在引用模块时其端口可以用以下两种方法连接:
两种引用的示例如下:
adder myAdder0(myA, myB, myCin, myCout, mySum);
adder myAdder1(.a(myA), .b(myB), .cin(myCin), .cout(myCout), .sum(mySum));
模块内容包括I/O说明、内部信号声明和功能定义。
I/O包括输入口、输出口和输入/输出口,均示例如下:
//输入口
input 端口名;
input [信号位宽-1:0] 端口名;
//输出口
output 端口名;
output [信号位宽-1:0] 端口名;
//输入输出口
inout 端口名;
inout [信号位宽-1:0] 端口名;
I/O说明也可以写在端口声明语句里,值得注意的是前面的adder模块就是如此。
在模块内用到的和端口有关的 wire
和 reg
类型变量的声明。示例如下:
wire 变量名;
wire [信号位宽-1:0] 变量名;
reg 变量名;
reg [信号位宽-1:0] 变量名;
功能定义决定了这个模块的逻辑功能。一共有三种方法可在模块中产生逻辑:
assign a = b & c;
,用于描述组合逻辑;add #2 u1(q, a, b);
,这创建了一个双输入与门实例,延时为两个单位时间,注意实例名必须唯一;Verilog HDL中总共有19种数据类型,数据类型是用来表示数字电路硬件中的数据储存和传送元素的。4种基本数据类型是: reg
型、 wire
型、 interger
型和 parameter
型。其他数据类型有 large
、 medium
、 scalared
、 time
、 small
、 tri
、 trio
、 tril
、 triand
、 trior
、 trireg
、 vectored
、 wand
、 wor
。这14种数据类型除了 time
以外都与基本逻辑单元建库有关,与系统设计没有很大关系。
整型常量具有下面4种进制表示形式:二进制(b或B)、十进制(d或D)、十六进制(h或H)、八进制(o或O)。
整数表达方式有以下3种:
示例如下:
8'hff //位宽为8十六进制整数
x和z分别代表不定值和高阻值,注意 wire
和 reg
均为四态(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
在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。常用的网络数据类型包括 wire
和 tri
。
wire型数据常用于表示以 assign
关键字指定的组合逻辑信号。Verilog程序块中输出、输出信号类型默认时自动定义为wire型。wire型信号可以用做任何方程式的输入,也可以用作“assign”语句或实例元件的输出。其定义示例如下:
wire a;
wire [7:0] b;
wire [4:0] c, d;
寄存器时数据存储单元的抽象,寄存器(register)数据类型的关键字是reg。通过赋值语句可以改变寄存器存储的值,其作用与改变触发器存储的值相当。
reg类型的数据默认初始值为不定值x,它可以赋正值,也可以赋负值,但当一个reg型数据是一个表达式的操作数时,它的值被当作无符号的值,即正值(对于4位的寄存器,-1会被认为是+15)。
注意reg型只表示被定义的信号将用在“always”语句块内,,尽管其常常是寄存器或触发器的输出,但并不一定总是这样。
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中运算符所带的操作数是不同的,按其所带的操作数个数可分为三种:
其所用运算符和C语言非常相像。
在Verilog HDL中,算数运算符又称二进制运算符,列如下:
在进行关系运算时,如果声明的关系为真(true),则返回值是0,反之(false)为1;如果某个操作数的值不定,则返回值为不定值。所有关系运算符的优先级相同,且均低于算数运算符。
Verilog HDL中具有3种逻辑运算符:
其中“&&”和“||”的优先级低于关系运算符,“!”的优先级高于关系运算符。
和C语言一样,为三目运算符“?:”,用法参考C语言即可。
在Verilog HDL中具有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 | 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中有两种移位运算符:
这两种移位运算符都用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位进行运算,一直运算到最后一位得出答案。下面给出几个示例:
a = 4’b1010;
&a //为1’b0
|a //为1’b1
^a //为1’b0
优先级由高到低排列如下:
!
~
*
/
%
+
-
<<
>>
<
<=
>
>=
==
!=
===
!==
&
^
~^
|
&&
||
?:
Verilog HDL中,信号具有两种赋值方式:
上面是书上的描述,在时序逻辑电路中,或者在always块中,我们通常都是使用非阻塞的。阻塞的赋值方式更像是用导线之间连接起来的形式,它们是同时发生的;非阻塞的赋值方式似乎更像是接续进行的。
块语句通常用来将两条或多条语句组合在一起,使其在结构上看更像一条语句,某种程度上很像C语言的大括号(大括号事实上已经被用作位拼接了对吧)。块语句有两种:一种是begin_end语句,通常用来表示顺序执行的语句,用它来标识的块称为顺序块;另一种是fork_join语句,通常用来标识并并行执行的语句,用它来表示的块称为并行块。
顺序块有以下特点:
顺序块的格式如下:
begin
语句1;
语句2;
...
语句n;
end
或
begin: 块名
块内声明语句
语句1;
语句2;
...
语句n;
end
语句中,块名即该块的名字,一个标识名;块内声明语句可以是参数声明语句、reg型变量声明语句、integer型变量声明语句和real型变量声明语句。
并行块有以下特点:
并行块的格式如下:
fork
语句1;
语句2;
...
语句n;
join
或
fork: 块名
块内声明语句
语句1;
语句2;
...
语句n;
join
语句中,块名即该块的名字,一个标识名;块内声明语句可以是参数声明语句、reg型变量声明语句、integer型变量声明语句、real型变量声明语句、time型变量声明语句和时间(event)说明语句。
在Verilog HDL语言中,可以给每个块取一个名字,只需将名字加在关键词begin或fork后面即可。这样做的原因有以下几点:
基于以上原因,块名提供了一个在任何仿真时刻确认变量值的方法。
这里先引入Verilog HDL中的延迟控制语句。“#”是延迟控制的关键字符,延迟语句用于对各条语句的执行时间进行控制,从而快速满足用户对时序的需求。的延迟控制语句格式共有两种:
其中延迟时间可以是直接指定的延迟时间量,并以多少个仿真时间单位的形式给出,可以是常量数字,也可以是变量或表达式。例子在后面就会看到。
在并行块和顺序块中都有一个起始时间和结束时间的概念。
对于顺序块,起始时间就是第一条语句开始执行的时间,结束时间就是最后一条语句执行完的时间。
而对于并行块来说,起始时间对于块内所有的语句是相同的,即程序流程控制进入块的时间,其结束时间是按时间排序在最后的语句执行结束的时间(当然这说的是指定了语句开始执行的延迟时间的情况下,如果并没有,那应该就是语句执行时间最长的那句执行结束的时间了吧[待考证]。有趣的是在这之前并没有讲关于语句延迟时间的问题,但是所给的示例程序中却已经出现了)。
这里引用了书上的两个例子如下:
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