本文主要参考了锆石FPGA文字教程《硬件语法篇》和夏宇闻《Verilog数字系统设计教程》(第三版)
目录
一、Verilog的基础知识
1.Verilog的四值逻辑系统
Verilog的四值逻辑系统如下图所示:
在Verilog的逻辑系统中有四种值,对应着四种状态:逻辑0:表示低电平,也就对应电路中的GND;逻辑1:表示高电平,也就是对应电路中的VCC;逻辑X:表示未知,有可能是高电平,也有可能是低电平;逻辑Z:表示高阻态,外部没有激励信号是一个悬空状态。
2.Verilog的数据类型
Verilog语言中,主要有三大数据类型,即寄存器数据类型、线网数据类型(物理连线)和参数数据类型(常量)。因此,真正在数字电路中起作用的数据类型是寄存器数据类型和线网数据类型,它们共同遵守Verilog的四值逻辑系统。
2.1寄存器数据类型
寄存器数据类型就是表示一个抽象的数据存储单元,它只能在always语句和initial语句等过程语句中被赋值,它的缺省值为X。在实际的数字电路中,如果该过程语句描述的是时序逻辑,则该寄存器变量对应为寄存器;如果该过程语句描述的是组合逻辑;则该寄存器变量对应为硬件连线;如果该过程语句描述的是不完全组合逻辑,那么该寄存器变量也可以对应为锁存器。由此可见,寄存器类型的变量不一定会综合为寄存器。寄存器数据类型有很多种,如reg、integer、real等,其中最常用的就是reg类型(默认1bit),reg类型的使用方法如下:
2.2线网数据类型
线网数据类型就是表示Verilog结构化元件间的物理连线。它的值由驱动元件的值决定,例如连续赋值与门的输出。如果没有驱动原件连接到线网,线网的缺省值为Z。线网数据类型同寄存器数据类型一样也是有很多种,如tri、wand 和wire等,其中最常用的就是wire类型(默认1bit),wire类型的使用方法如下:
2.3参数数据类型
参数类型其实就是一个常量,通常出现在module 内部,常被用于定义状态机的状态、数据位宽和延迟大小等,由于它可以在编译时修改参数的值,因此它又常被用于一些参数可调的模块中,使用户在实例化模块时,可以根据需要配置参数。在定义
参数时,我们可以一次定义多个参数,参数与参数之间需要用逗号隔开。这里我们需要注意的是参数的定义是局部的,只在当前模块中有效,参数类型的使用方法如下:
3.Verilog的基本运算符
Verilog 硬件描述语言的运算符范围很广,其运算符按其功能可分为以下八类:1、算术运算符、2、关系运算符、3、逻辑运算符、4、条件运算符、5、位运算符、6、移位运算符、7、拼接运算符。
3.1算数运算符
所谓算术逻辑运算符就是常说的加、减、乘、除等,这类运算符的抽象层级较高,从数字逻辑电路实现上来看,它们都是基于与、或、非等基础门逻辑组合实现的,如下表所示:
3.2关系运算符
关系运算符主要是用来做一些条件判断用的,在进行关系运算符时,如果声明的关系是假的,则返回值是0,如果声明的关系是真的,则返回值是1;所有的关系运算符有着相同的优先级别,关系运算符的优先级别低于算术运算符的优先级别。关系运算符如下表所示:
3.3逻辑运算符
逻辑运算符包括与、或和非,是连接多个关系表达式用的,可实现更加复杂的判断,一般不单独使用,都需要配合具体语句来实现完整的意思,逻辑运算符如下表所示:
3.4条件运算符
Verilog语言为了让连续赋值的功能更加完善,于是又从C语言中引入了条件操作符(是一个三目运算符)来构建从两个输入中选择一个作为输出的条件选择结构,功能等同于always 中的if-else 语句,条件运算符如下表所示:
3.5位运算符
位运算符是一类最基本的运算符,可以认为它们直接对应数字逻辑中的与、或、非门等逻辑门。位运算符的与、或、非与逻辑运算符逻辑与、逻辑或、逻辑非,虽然它们处理的数据类型不一样,但是从硬件实现角度上来说,它们没有区别的,位运算符如下表所示:
3.6移位运算符
在Verilog 中有两种移位运算符:左移位运算符和右移位运算符,这两种移位运算符都用0来填补移出的空位(相当于移动二进制中的“1”),移位运算符如下表所示:
3.7拼接运算符
在Verilog中有一个特殊的运算符,就是位拼接运算符。用这个运算符可以把两个或多个信号的某些位拼接起来进行运算操作,拼接运算符如下表所示:
3.8运算符的优先级别
运算符一多,必然涉及到优先级的问题,运算符的优先级别制成表如下表所示:
二、Verilog的基础语法
虽然Verilog 硬件描述语言有很完整的语法结构和系统,这些语法结构的应用给设计描述带来很多方便。但是Verilog是描述硬件电路的,它是建立在硬件电路的基础上的。有些语法结构是不能与实际硬件电路对应起来的,比如for 循环,它是不能映射成实际的硬件电路的,因此,Verilog 硬件描述语言分为可综合和不可综合语言。
下面简单的介绍一下可综合与不可综合。
(1)可综合,就是说编写的Verilog代码能够被综合器转化为相应的电路结构。因此,常用可综合语句来描述数字硬件电路。
(2)不可综合,就是说编写的Verilog 代码无法综合生成实际的电路。因此,不可综合语句一般在描述数字硬件电路中是用不到的,不过,可以用它来仿真、验证所描述的数字硬件电路。
1.Verilog的关键字
Verilog是由C语言衍生而来,但是却有着比C语言更多的关键字,但是其中绝大部分是很少用到的,因此只要熟练掌握其中的可综合的关键字就足够了,常用的可综合关键字如下表所示:
module和endmodule,它们是成对使用的,模块是Verilog 设计中基本功能块,一个最简单的模块是由模块命名、端口列表两个部分组成。整个模块是由module开头,endmodule结尾,module后面紧跟着的是模块名,每个模块都有它自己的名字。这些关键词的基本使用方法如下所示(关键词的学习不是孤立的,需要结合实例进行学习和熟悉):
input、output和inout 用于端口定义。
wire 和reg是用来声明数据类型,parameter是用来声明参数类型。
always 是过程赋值语句,assign是连续赋值语句。
if 和else成对使用,是条件判断语句,和C语言中的if和else是一样的功能。
begin和end也是成对使用,相当于C 语言中的大括号。
case、endcase 和default 成对使用,是一个多分支条件语句,和C 语言中的switch 一样的功
能。posedge、negedge 和or 这三个关键字是和always 关键字联合使用的,posedge 是上升沿触发,negedge 是下降沿触发,posedge or negedge是既有上升沿又有下降沿。
2.Verilog的基本程序框架
以最简单的门电路为例简介Verilog的基本程序框架,门电路的Verilog代码如下所示:
上述代码中,a和b是与门的输入,c是与门的输出,也就是说,上述代码实现的是一个两输入的与门的逻辑功能。
- Verilog HDL程序是由模块构成的,每个模块的内容都是嵌在module和endmodule两个语句之间
- 每个模块要进行端口定义,并说明输入输出口,然后对模块的功能进行行为逻辑描述
- Verilog HDL程序的书写格式自由,一行可以写几个语句,一个语句也可以分写多行
- 除了endmodule 语句外,每个语句和数据定义的最后必须有分号
三、Verilog中的关键问题
1.Verilog的抽象级别
所谓抽象级别,实际上是指同一个物理电路,可以在不同的层次上用Verilog语言来描述它。Verilog硬件描述语言支持以下五种级别(抽象级别由高到低):(1)系统级;(2)算法级;(3)RTL级;(4)门级;(5)开关级。
其中,系统级和算法级属于行为级描述;RTL级又称为数据流描述方式;门级和开关级属于结构化描述方式。
1.1结构化描述方式
结构化描述方式是最原始的描述方式,也是抽象级别最低的描述方式,不过,它却是最接近于实际的硬件结构的描述方式。因为采用结构化的描述方式来编写Verilog 代码,其思路就跟在面包板上搭建数字电路时一样的,唯一的不同点就是通过Verilog 的形式来描述数字电路都需要哪些元器件以及它们之间的连接关系是怎么样的(实际上就相当于直接在描述逻辑电路)。
以三人表决器的结构化描述方式为例:
1 module Example_Structure //Example_Structure,即模块的开始,
2 (
3 //输入端口
4 A,B,C,
5 //输出端口
6 L
7 );
8
9 input A; //模块的输入端口A
10 input B; //模块的输入端口B
11 input C; //模块的输入端口C
12 output L; //模块的输出端口L
13
14 wire AB,BC,AC; //内部信号声明AB,BC,AC
15
16 and U1(AB,A,B); //与门(A,B 信号进入)(A 与B 信号即AB 输出)
17 and U2(BC,B,C); //与门 同上
18 and U3(AC,A,C); //与门 同上
19
20 or U4(L,AB,BC,AC); //或门 同上
21
22 endmodule //模块的结束
1.2数据流描述方式
数据流描述方式要比结构化描述方式的抽象级别高一些,因为它不再需要清晰地刻画出具体的数字电路架构,而是可以比较直观地表达底层逻辑的行为。基于数据流的描述方式,形象点来说,每个模块就好比一个容器,大量外部信息从模块的输入端口流入,相应的,大量的处理后信息也会从模块的输出端口流出(实际上就相当于直接在描述逻辑表达式),因此,基于这种思路编写的Verilog代码被称为数据流描述方式。数据流描述方式又可称为RTL级描述方式,即寄存器传输级描述。
三人表决器的数据流描述方式的Verilog代码如下:
1 module Example_Dataflow //Example_Dataflow,即模块的开始
2 (
3 //输出端口
4 A,B,C,
5 //输入端口
6 L
7 );
8
9 input A; //模块的输入端口A
10 input B; //模块的输入端口B
11 input C; //模块的输入端口C
12 output L; //模块的输出端口L
13
14 assign L = ((!A) & B & C) | (A & (!B) & C) | (A & B & (!C)) | (A & B & C);
15
16 endmodule //模块的结束
1.3行为级描述方式
和前面两种描述方式比起来,行为级描述方式的抽象级别最高,概括力也最强,因此规模稍大些的设计,往往都是以行为级描述方式为主。高级语言的执行思路都是串行的,例如C语言。顺序执行的语句更容易帮助我们来表达我们的设计思想,尤其是使描述时序逻辑变得容易。虽然FPGA的设计思路都是并行的,但是Verilog中还是支持大量的串行语句元素。由此可见,行为级描述方式的主要载体就是串行语句,同时辅以并行语句用于描述各个算法之间的连接关系。
三人表决器的行为级描述方式的Verilog代码如下:
1 module Example_Behavior //Example_Behavior,即模块的开始
2 (
3 //输入端口
4 A,B,C,
5 //输出端口
6 L
7 );
8
9 input A; //模块的输入端口A
10 input B; //模块的输入端口B
11 input C; //模块的输入端口C
12 output reg L; //模块的输出端口L
13
14 always @ (A,C,B) //always 在组合逻辑中的用法
15 begin //always @ (A,B,C)解析:只要A,B,C
16 case({A,B,C}) //其中有一个信号有变化便会执行begin 中的case 语句
17 3'b000: L = 1'b0; //也可以写成always @ (*),与always @ (A,B,C)功能相同
18 3'b001: L = 1'b0; //{A,B,C}解析:把A,B,C 三条线合成一条总线
19 3'b010: L = 1'b0; //举例说明:{1'b1,1'b0}=2'b10
20 3'b011: L = 1'b1; //
21 3'b100: L = 1'b0;
22 3'b101: L = 1'b1;
23 3'b110: L = 1'b1;
24 3'b111: L = 1'b1;
25 default:L = 1'bx; //不要省略
26 endcase //case 语句的结束
27 end //begin 语句的结束
28
29 endmodule //module 语句的结束
2.Verilog的模块化设计
模块化设计是FPGA设计中一个很重要的技巧(一般整个设计的顶层只做例化,不做逻辑),它能够使一个大型设计的分工协作和仿真测试更加容易,使代码维护和升级更加便利。所谓模块化设计,就是将一个比较复杂的系统按照一定的规则划分为多个小模块,然后再分别对每个小模块进行设计,当这些小模块全都完成以后,再将这些小模块有机的组合起来,最终就能够完成整个复杂系统的设计。
以半加器为例,采用模块化设计的半加器的结构图如下所示:
从上图中可以看出,采用模块化设计的方法将半加器分成了与门模块和异或模块,接下来只需要完成实现与门模块和异或模块,我们再将实现的与门模块和异或模块相结合,最终就能实现半加器。首先给出半加器顶层模块代码,如下所示:
1 module Example_Module
2 (
3 input a,
4 input b,
5 output s,
6 output c
7 );
8
9 Yumen_Module Yumen_Init
10 (
11 .yumen_a(a),
12 .yumen_b(b),
13 .yumen_c(c)
14 );
15
16 Yihuo_Module Yihuo_Init
17 (
18 .yihuo_a(a),
19 .yihuo_b(b),
20 .yihuo_s(s)
21 );
22
23 endmodule
从上述代码中可以看出, 第1行是模块的开始,模块名为Example_Module。第2至7行是端口声明,一共定义了2个输入端口a和b,2个输出端口s和c。其中a和b代表两个加数,s代表两个加数的和,c代表进位。第9行是例化与门模块,其Yumen_Module 是Yumen_Module.v模块里相对应的模块名,Yumen_Init可以任意命名,它主要是用来区分例化多个相同的模块。第10至14行是信号的例化,其中.yumen_a是与门模块中的信号,它必须和与门模块中的信号名一致才行,(a) 是顶层模块中的信号,它必须和顶层模块中的信号名一致才行。第16至21行是例化异或模块,与例化与门同理。看完了顶层模块,下面再来看下与门模块的代码,如下所示:
1 module Yumen_Module
2 (
3 input yumen_a,
4 input yumen_b,
5 output yumen_c
6 );
7
8 assign yumen_c = yumen_a && yumen_b;
9
10 endmodule
下面是异或模块的代码:
1 module Yihuo_Module
2 (
3 input yihuo_a,
4 input yihuo_b,
5 output yihuo_s
6 );
7
8 assign yihuo_s = yihuo_a ^ yihuo_b;
9
10 endmodule
通过模块化设计的方法不仅层次清晰,而且思路明确。从半加器的RTL视图更能深刻感受到模块化设计带来的层次感,半加器的RTL视图如下图所示:
3.给端口选择正确的数据类型
Verilog中关于reg和wire的数据类型的使用常常让人对此感到疑惑,以下是一个综合出的实际电路模块。
从上图中可以看出, 对于输入A和输入B这两个端口来说,只能使用线网类型,但是,输入A和输入B这两个端口可以由寄存器和线网所驱动,也就是说,寄存器和线网可以连接到这两个输入端口上作为输入源。对于输出端口Y 来说,它可以是线网,也可以是寄存器类型,但是,对于输出端口Y,它只能驱动线网类型。
以下述代码为例:
1 module Example_Datatype
2 (
3 a,b,c,d,o1,o2
4 );
5
6 input a,b,c,d;
7 output o1,o2;
8
9 reg c,d;
10 reg o1;
11 reg o2;
12
13 assign o2 = c && d;
14
15 always @ (a or b)
16 begin
17 if(a)
18 o1 = b;
19 else
20 o1 = 1'b0;
21 end
22
23 endmodule
在编译的过程中出现如下错误:
根据上图可以看出,c不能被申明为reg类型(2001Verilog标准中,已经将register更改为variable变量类型),原因是,输入端口不能被定义为reg(寄存器类型),只能被定义为wire型。对代码进行修改,将对c和d的reg声明注释掉(默认就是wire型),修改完成后,仍出现如下报错:
从上图可以看出,o2必须为线网类型,但是前面所述的是对于输出端口,它既可以是wire(线网类型),也可以是reg(寄存器类型),这里出所的原因是o2是由关键字assign指定的。对于assign这种连续赋值语句,必须是wire型。而对于o1,由于o1是在always中赋值的,因此必须为reg型。因此,将o2声明为reg型注释掉,再次编译,通过。
结论为:assign连续赋值语句只能声明为wire;always中只能声明为reg。
以下是上述代码的RTL视图:
从上图中可以看出, 尽管将o1声明为了reg类信号,但是它并没有综合成触发器。这是因为,对于寄存器reg类型,在always等过程块中被赋值的信号,如果该always模块描述的是时序逻辑电路,那么该信号常常被综合为D触发器,如果该always模块描述的是组合逻辑电路,那么该信号会被综合成连线(详见2.1)。
4.Latch的产生
当在always过程块中描述组合逻辑电路时,如果条件语句中没有说明全部的条件,那么就会有可能产生锁存器,也就是所谓的latch(只有在always中描述组合逻辑电路才可能产生latch)。
以下述产生latch的电路模块的代码为例:
1 module Example_Latch
2 (
3 clk,q,data,enable
4 );
5
6 input clk;
7 input data;
8 input enable;
9 output q;
10
11 reg q;
12
13 //always @ (posedge clk)
14 //begin
15 // if(enable)
16 // q <= data;
17 // else
18 // q <= q;
19 //end
20
21 always @ (*)
22 begin
23 if(enable)
24 q = data;
25 // else
26 // q = 1'b0;
27 end
28
29 endmodule
上述代码的功能是用always实现了一个组合电路,当enable为1时才会将data的值赋给q。由于q是在always中进行赋值的,所以q为reg类型。生成的RTL视图如下:
从上图可以看出,前述代码最终产生了latch。原因在于,当enable为1时,它将data端输入到q;但是当enable为0时,没有执行任何程序,电路默认情况下也就保持原值不变,因此产生了锁存器(else没有发生的情况下,默认是保持原值的,但是由于组合逻辑电路没有记忆功能,因此只能生成锁存器)。将if语句的条件补全,生成RTL视图如下:
从上图可以看出,将条件语句补全以后,电路中不再生成latch,而是生成了一个二选一的数据选择器。因此,在描述组合逻辑的时候一定要注意,对于if-else语句尽量把它的每种条件都考虑进去,要把条件都写全,这样就能避免latch的产生。
需要补充的一点是,当在always过程块中描述时序逻辑电路时,即使条件语句没有说明全部的条件,该电路也不会产生锁存器。将上述代码中的组合逻辑部分注释掉(21-27行),将时序逻辑部分(13-19行)解注释,生成的RTL视图如下:
从上图可以看出, 电路没有综合成latch,而是综合成了D触发器。对于时序逻辑电路来说,else有和没有是完全一样的,因为对于D触发器来说,它默认情况下就是保持原值的,而enable这个端口相当于一个使能端口。q是保持原值,还是改变为新值,是由使能端口enable决定的。所以Verilog语言中latch的产生一般是组合电路中条件不完整造成的。
当遇到综合所需case条件与现实条件不一致,此时不能直接使用综合器。例如,红绿灯一共有三种状态(红、黄和绿),但是两bit一共有四种情况,如果在always中只写三个条件的话,会产生latch。解决方法是使用综合器指令full case。
以简单的三人表决器为例,按键用来表示表决输入,LED灯与按键对应(按下按键对应LED等点亮),数码管用来显示表决通过的认数。
以下是case条件齐全,未产生latch的verilog代码:
module Three_Vote
(
KEY,LED,SEG_EN,SEG_DATA
);
input [2:0] KEY;
output [2:0] LED;
output [5:0] SEG_EN;
output reg [7:0] SEG_DATA;
parameter SEG_NUM0 = 8'hbf, //数字0
SEG_NUM1 = 8'h86, //数字1
SEG_NUM2 = 8'hdb, //数字2
SEG_NUM3 = 8'hcf, //数字3
SEG_NUM4 = 8'he6, //数字4
SEG_NUM5 = 8'hed, //数字5
SEG_NUM6 = 8'hfd, //数字6
SEG_NUM7 = 8'h87, //数字7
SEG_NUM8 = 8'hff, //数字8
SEG_NUM9 = 8'hef, //数字9
SEG_NUMA = 8'hf7, //数字A
SEG_NUMB = 8'hfc, //数字B
SEG_NUMC = 8'hb9, //数字C
SEG_NUMD = 8'hde, //数字D
SEG_NUME = 8'hf9, //数字E
SEG_NUMF = 8'hf1; //数字F
always @ (*)
begin
case(KEY)
3'b000 : SEG_DATA = SEG_NUM0;
3'b001 : SEG_DATA = SEG_NUM1;
3'b011 : SEG_DATA = SEG_NUM2;
3'b010 : SEG_DATA = SEG_NUM1;
3'b110 : SEG_DATA = SEG_NUM2;
3'b111 : SEG_DATA = SEG_NUM3;
3'b101 : SEG_DATA = SEG_NUM2;
3'b100 : SEG_DATA = SEG_NUM1;
endcase
end
assign LED = ~ KEY;
assign SEG_EN = 6'b00_0000;
endmodule
其对应的RTL视图如下:
现在将case的条件任一注释(这里选择注释最后一个:3'b100) ,生成的RTL视图如下:
显然,用always块描述时序逻辑电路条件不完全导致了latch的产生。下面,在代码中case处添加“//synopsys full_case”(这里的引号仅作示意),变动后的case语句代码如下(其余部分不变):
case(KEY)//synopsys full_case
3'b000 : SEG_DATA = SEG_NUM0;
3'b001 : SEG_DATA = SEG_NUM1;
3'b011 : SEG_DATA = SEG_NUM2;
3'b010 : SEG_DATA = SEG_NUM1;
3'b110 : SEG_DATA = SEG_NUM2;
3'b111 : SEG_DATA = SEG_NUM3;
3'b101 : SEG_DATA = SEG_NUM2;
//3'b100 : SEG_DATA = SEG_NUM1;
endcase
生成的RTL视图如下:
显然,在告知综合器case语句已经完全的情况下,电路中不会再产生latch。
注:常用的综合器指令还有parallel case,用于条件分支不互斥的情况(正常的条件分支都是互斥的),如果case语句的条件不互斥,则会存在优先级,就可以使用parallel case的原语告知DC(Design Compiler)所有条件互斥,且并行,无优先级。命令为:“//synopsys parallel_case”如“//synopsys full_case“一样添加在case语句旁。
5.组合逻辑反馈环
组合逻辑反馈环路是数字同步逻辑设计的大忌,它最容易因振荡、毛刺、时序违规等问题引起整个系统的不稳定和不可靠。
示例如下:
1 module Example_Feedback
2 (
3 data_in1,data_in2,data_out
4 );
5
6 input data_in1;
7 input data_in2;
8 output data_out;
9
10 assign data_out = (data_in2) ? data_in1 : (~data_out | data_in1);
11
12 endmodule
可以看到,这就是一个典型的组合逻辑反馈环电路。简单的介绍一下该代码,第1至8行是用来端口声明的,第10行是一个assign连续赋值语句,当data_in2为1 时,就将data_in1赋值给data_out,当data_in2为0时,便将(~data_out | data_in1)赋值给
data_out。由于data_out 是由assign关键字指定的,所以data_out为wire 类型。介绍完了代码,接下来看下它的RTL视图。
上述电路的输出端口data_out直接通过组合逻辑反馈到与门的输入端口上,此时,假设data_in1和data_in2均都为1,那么毋容置疑,电路会输出1;而如果data_in1和data_in2都为0,这时候,就需要判断data_out这个反馈信号,如果此时data_out为1,输出就为0,但是如果此时data_out为0,那么输出就为1,因此可以看出,它没有一个稳定的状态,电路存在不确定性。
将上述代码在Quartus II中编译,出现如下警告:
从上图可以看到,Quartus II 软件提示说Found combinational loop of 2 nodes。当然这种警告不是致命错误,它不影响编译。通过组合逻辑电路反馈环来实现这种功能的话是不允许这样做的,如果确实需要这样做的话,那么可以通过时序电路同步反馈来实现。下面将上述的组合逻辑电路改为时序逻辑电路,代码如下所示:
1 module Example_Feedback
2 (
3 data_in1,data_in2,data_out,clk,rst_n
4 );
5
6 input clk;
7 input rst_n;
8 input data_in1;
9 input data_in2;
10 output data_out;
11
12 reg data_out_r;
13
14 always @ (posedge clk or negedge rst_n)
15 begin
16 if(!rst_n)
17 data_out_r <= 1'b0;
18 else
19 data_out_r <= (data_in2) ? (data_in1) : (~data_out_r | data_in1);
20 end
21
22 assign data_out = data_out_r;
23
24 endmodule
由于该代码与前面的代码功能是一样的,所以这里就不再进行介绍了,需要说明的是,由于data_out_r是在always模块中赋值的,因此需要将data_out_r定义成reg类型,由于data_out是由assign指定的,所以需要将data_out定义成wire类型。如果将该代码放到Quartus II软件中进行编译,那么在警告窗口中是看不到combinational loop警告信息的。逻辑时序电路生成的RTL视图如下所示:
因此,如果需要将输出信号反馈到输入端,必须通过时序电路实现。
6.阻塞赋值和非阻塞赋值
阻塞赋值使用等号(=)来表示,非阻塞赋值使用的是小于等于号(<=)来表示。
结合实例分析,采用阻塞赋值和非阻塞赋值示例的顶层模块代码如下所示:
1 module Example_Block
2 (
3 /* 时钟和复位端口 */
4 clk,rst_n,
5 /* 数据输入端口 */
6 data_in,
7 /* 阻塞端口 */
8 block_out1,block_out2,
9 /* 非阻塞端口 */
10 no_block_out1,no_block_out2
11 );
12
13 input clk;
14 input rst_n;
15 input data_in;
16 output block_out1;
17 output block_out2;
18 output no_block_out1;
19 output no_block_out2;
20
21 Block_Module Block_Init
22 (
23 .clk (clk ),
24 .rst_n (rst_n ),
25 .block_in (data_in ),
26 .block_out1 (block_out1 ),
27 .block_out2 (block_out2 )
28 );
29
30 No_Block_Module No_Block_Init
31 (
32 .clk (clk ),
33 .rst_n (rst_n ),
34 .no_block_in (data_in ),
35 .no_block_out1 (no_block_out1 ),
36 .no_block_out2 (no_block_out2 )
37 );
38
39 endmodule
从上述代码可以知道,顶层模块Example_Block中包含了两个模块,一个模块是Block_Module也就是阻塞赋值,另一个模块是No_Block_Module也就是非阻塞赋值,这个代码很简单,没有任何逻辑设计。
阻塞赋值Block_Module代码如下所示:
1 module Block_Module
2 (
3 /* 时钟和复位端口 */
4 clk,rst_n,
5 /* 阻塞输入端口 */
6 block_in,
7 /* 阻塞输出端口 */
8 block_out1,block_out2
9 );
10
11 input clk;
12 input rst_n;
13 input block_in;
14 output block_out1;
15 output block_out2;
16
17 reg block_out1;
18 reg block_out2;
19
20 always @ (posedge clk or negedge rst_n)
21 begin
22 if(!rst_n)
23 begin
24 block_out1 = 1'b0;
25 block_out2 = 1'b0;
26 end
27 else
28 begin
29 block_out1 = block_in;
30 block_out2 = block_out1;
31 end
32 end
33
34 endmodule
从上述代码中可以知道, 第1至18行是端口的声明,一共声明了5个端口,其中3个输入端口,2个输出端口;第20至32行是一个always模块,在always模块中,先将输入端口block_in赋值给block_out1,然后我们再将block_out1赋值给block_out2。,这里需要注意的是,使用的是阻塞赋值(=)。
非阻塞赋值No_Block_Module代码如下所示:
1 module No_Block_Module
2 (
3 /* 时钟和复位端口 */
4 clk,rst_n,
5 /* 非阻塞输入端口 */
6 no_block_in,
7 /* 非阻塞输出端口 */
8 no_block_out1,no_block_out2
9 );
10
11 input clk;
12 input rst_n;
13 input no_block_in;
14 output no_block_out1;
15 output no_block_out2;
16
17 reg no_block_out1;
18 reg no_block_out2;
19
20 always @ (posedge clk or negedge rst_n)
21 begin
22 if(!rst_n)
23 begin
24 no_block_out1 <= 1'b0;
25 no_block_out2 <= 1'b0;
26 end
27 else
28 begin
29 no_block_out1 <= no_block_in;
30 no_block_out2 <= no_block_out1;
31 end
32 end
33
34 endmodule
分别通过Block_Module和No_Block_Module实现了阻塞赋值和非阻塞赋值的例化,顶层模块生成的RTL视图如下所示:
尽管这两个模块的代码基本上一样,但是它们两个实现的功能却是完全不同。从上述RTL视图可以看出,使用阻塞赋值生成的电路是两个并联的D触发器;使用非阻塞赋值生成的电路是两个串联的D触发器,生成的电路是完全不同的。
注:这里需要注意的是,阻塞赋值意味着语句是顺序执行的,并不代表电路是串联的,这两者没有必然关系。由于阻塞赋值中是先将block_in赋值给block_out1,再将block_out1赋值给block_out2,实际上相当于同时将block_in赋值给block_out1和block_out2,因此在阻塞赋值的RTL视图中对应的电路是并联的;而非阻塞赋值中同时将no_block_in和no_block_out1分别赋值给no_block_out1和no_block_out2,因此在非阻塞赋值的RTL视图中对应的电路是串联的,级联意味着第二个触发器的输入是第一个触发器原本状态的输出no_block_out1,而不是将第一个D触发器现态的输出直接送到第二个D触发器的输入端。
可以想象它们所产生的波形也是完全不同的,下面给出顶层模块的仿真波形图。
从该仿真波形图中我们可以看出,block_out1、block_out2 和no_block_out1,它们三个的波形是一致的,但是no_block_out2波形有很大不同。出现这种情况的原因如下:
(1) 阻塞赋值(=):写在前面的语句先执行,写在后面的语句后执行,也就是说,它是顺序执行的,一般用于组合逻辑电路。
(2) 非阻塞赋值(<=):写在前面的语句与后面的语句同时执行,也就是说,它是并行执行的,跟书写顺序没有关系,一般用于时序逻辑电路。
下面是一个简单的阻塞赋值和非阻塞赋值的示例(仅示意)。
初始化m=1,n=2,p=3,执行以下语句
begin begin
m=n; m<=n;
n=p; n<=p;
p=m; p<=m;
end end
结果分别是:m=2,n=3,p=2和m=2,n=3,p=1。
7.状态机
Verilog中程序都是并行执行的,但是在一些在实际的工程应用中往往需要实现一些具有一定顺序的工作,因此就需要状态机来解决上述问题。
所谓状态机就是根据控制信号按照预先设定的状态进行状态转移,是协调相关信号、完成特定操作的控制中心。通过状态机这种方法,可降低抽象难度,同时也可提高代码的可读性及维护性。状态机是表示多个状态以及这些状态之间转移和动作的数学模型。状态存储关于过去的信息,它反映从系统到现在时刻输入的变化;转移指示状态变更,用必须满足来确使转移发生的条件来描述它;动作是给定时刻要进行的活动描述。
状态机简写为FSM(Finite State Machine),主要分为两大类:
(1) 输出只和状态有关而与输入无关,则称为摩尔(Moore)状态机;
(2) 输出不仅和状态有关而且和输入有关系,则称为米利(Mealy)状态机。
状态机通常包括组合逻辑和时序逻辑,其中,时序逻辑由一组触发器组成,用来记忆当前的状态;而组合逻辑又可以分为次态逻辑和输出逻辑两部分,次态逻辑的功能是确定有限状态机的下一个状态,输出逻辑的功能是确定有限状态机的输出。
7.1状态机的设计步骤
通常设计一个状态机分为以下四个步骤:
(1) 根据实际使用需要来选择状态机的结构,确定采用摩尔型状态机还是米里型状态机;
(2) 根据实际情况分析设计要求并列出状态机的所有状态,然后对每个状态进行状态编码;
(3) 根据状态转移关系和输出函数画出状态图。这里我们需要注意的是,对同一个设计问题来说,不同的人可能会构造出不同的状态图。状态图直观地反映了状态机各个状态之间的转换关系以及转换条件,因而有利于理解状态机的工作原理,但此时要求设计的状态个数不能太多对于状态个数较多的状态机一般采用状态表的方法列出状态机的转移条件。
(4) 根据所画的状态图,采用硬件描述语言对状态机进行描述。
7.2状态机的状态编码
所谓状态编码就是通过不同的编码值去区分各个不同的状态,使得每一个状态都有唯一的标识。在使用Verilog描述状态机时,通常用参数定义语句parameter指定状态编码。常用的状态编码有三种分别是:递增二进制编码,格雷编码和one-hot编码,如下表所示:
(1) 递增二进制编码:这种编码方式的效率很高,但是在译码过程中需要让所有的二进制位参与译码,在状态较多且状态跳转条件比较复杂时会导致很大的组合逻辑。
(2) 格雷编码:这种编码方式能够避免进入错误的状态,常用于高可靠性设计。
(3)one-hot编码:这种编码方式所占用的D触发器资源通常要比递增二进制编码多一些,状态数等于触发器的数据,冗余的触发器使得译码电路比较简单,因此它的速度非常快。
注:需要注意的是,不管使用哪种编码方式,状态机中的各个状态都应使用符号常量(parmeter),而不应该直接使用编码数值,赋予各状态有意义的名字对与设计的验证和代码的可读性都是有益的。
7.3状态机的描述方法
状态机的描述代码风格有三种:分别是一段式状态机、二段式状态机、三段式状态机。对于这三种方法下面分别进行介绍。
(1) 一段式状态机:是将所有逻辑都写在了一个always模块中,虽然这种写法从功能上来说并没有错误,但是它的可读性差,在编写的时候容易出错,往往不利于维护;
(2) 二段式状态机:是将组合逻辑和时序逻辑分开,具有较好的可读性,相对容易维护,不过组合逻辑输出较易出现毛刺等常见问题;
(3) 三段式状态机:除了具有两段式的优点(将组合逻辑和时序逻辑分开),还对状态输出进行了寄存,可以有效地滤除毛刺(推荐使用三段式状态机,养成良好的代码风格)。