Verilog HDL(Hardware Description Language)是在用途最广泛的C语言的基础上发展起来的一种硬件描述语言,具有灵活性高、易学易用等特点。Verilog HDL可以在较短的时间内学习和掌握,目前已经在FPGA开发/IC设计领域占据绝对的领导地位。
简单的编程案例
为快速入门Verilog语言,先从简单的编程案例开始。以LED流水灯程序为例来给大家展示Verilog的程序框架,代码如下所示。
module led(
input sys_clk , //系统时钟
input sys_rst_n, //系统复位,低电平有效
output reg [3:0] led //4位LED灯
);
//parameter define
parameter WIDTH = 25 ;
parameter COUNT_MAX = 25_000_000; //板载50M时钟=20ns,0.5s/20ns=25000000,需要25bit
//位宽
//reg define
reg [WIDTH-1:0] counter ;
reg [1:0] led_ctrl_cnt;
//wire define
wire counter_en ;
//***********************************************************************************
//** main code
//***********************************************************************************
//计数到最大值时产生高电平使能信号
assign counter_en = (counter == (COUNT_MAX - 1'b1)) ? 1'b1 : 1'b0;
//用于产生0.5秒使能信号的计数器
always @(posedge sys_clk)
begin
if (sys_rst_n == 1'b0)
counter <= 1'b0;
else if (counter_en)
counter <= 1'b0;
else
counter <= counter + 1'b1;
end
//led流水控制计数器
always @(posedge sys_clk)
begin
if (sys_rst_n == 1'b0)
led_ctrl_cnt <= 2'b0;
else if (counter_en)
led_ctrl_cnt <= led_ctrl_cnt + 2'b1;
end
//通过控制IO口的高低电平实现发光二极管的亮灭
always @(posedge sys_clk)
begin
if (sys_rst_n == 1'b0)
led <= 4'b0;
else begin
case (led_ctrl_cnt)
2'd0 : led <= 4'b0001;
2'd1 : led <= 4'b0010;
2'd2 : led <= 4'b0100;
2'd3 : led <= 4'b1000;
default : led <= 4'b1111;
endcase
end
end
endmodule
需要注意的几个点:
注释:两种方式,一种是以“/*”符号开始,“*/”结束,在两个符号之间的语句都是注释语句,因此可扩展到多行。另一种是以//开头的语句,它表示以//开始到本行结束都属于注释语句。
模块定义:以module开始,endmodule结束;
端口定义:input output
数据类型的定义:reg 、wire、parameter
assign语句:条件成立选择1,否则选择0
always语句:语句中的posedge代表在时钟上升沿进行信号触发。begin/end代表语句的开始和结束。
If-else语句:和C语言是比较类似的。
Case语句:需要一个case关键字开始,endcase关键字结束,default作为默认分支,和C语言也是类似的。
问号语句:与if-else类似。
Verilog的常用语法
Verilog的数字进制格式
Verilog数字进制格式包括二进制、八进制、十进制和十六进制,一般常用的为二进制、十进制和十六进制。
二进制表示如下:4’b0101表示4位二进制数字0101;
十进制表示如下:4’d2表示4位十进制数字2(二进制0010);
十六进制表示如下:4’ha表示4位十六进制数字a(二进制1010),十六进制的计数方式为0,1,2,…,9,a,b,c,d,e,f,最大计数为f(f:十进制表示为15)。
当代码中没有指定数字的位宽与进制时,默认为32位的十进制,比如100,实际上表示的值为32’d100
Verilog的数据类型
在Verilog语法中,主要有三大类数据类型,即寄存器类型、线网类型和参数类型。从名称中,我们可以看出,真正在数字电路中起作用的数据类型应该是寄存器类型和线网类型。
寄存器类型
寄存器类型表示一个抽象的数据存储单元,它只能在always语句和initial语句中被赋值,并且它的值从一个赋值到另一个赋值过程中被保存下来。如果该过程语句描述的是时序逻辑,即always语句带有时钟信号,则该寄存器变量对应为寄存器;如果该过程语句描述的是组合逻辑,即always语句不带有时钟信号,则该寄存器变量对应为硬件连线;
寄存器类型的缺省值是x(未知状态)。
寄存器数据类型有很多种,如reg、integer、real等,其中最常用的就是reg类型,它的使用方法如下:
//reg define
reg [31:0] delay_cnt; //延时计数器
reg key_flag ; //按键标志
线网类型
线网表示Verilog结构化元件间的物理连线。它的值由驱动元件的值决定,例如连续赋值或门的输出。如果没有驱动元件连接到线网,线网的缺省值为z(高阻态)。线网类型同寄存器类型一样也是有很多种,如tri和wire等,其中最常用的就是wire类型,它的使用方法如下:
//wire define
wire data_en; //数据使能信号
wire [7:0] data ; //数据
参数类型
参数类型其实就是一个常量,常被用于定义状态机的状态、数据位宽和延迟大小等,由于它可以在编译时修改参数的值,因此它又常被用于一些参数可调的模块中,使用户在实例化模块时,可以根据需要配置参数。在定义参数时,我们可以一次定义多个参数,参数与参数之间需要用逗号隔开。这里我们需要注意的是参数的定义是局部的,只在当前模块中有效。它的使用方法如下:
//parameter define
parameter DATA_WIDTH = 8; //数据位宽为8位
Verilog的运算符
大家看完了Verilog的数据类型,我们再来介绍下Verilog的运算符。Verilog中的运算符按照功能可以分为下述类型:1、算术运算符、 2、关系运算符、3、逻辑运算符、 4、条件运算符、 5、位运算符、 6、移位运算符、 7、拼接运算符。下面我们分别对这些运算符进行介绍。
算术运算符
算术运算符,简单来说,就是数学运算里面的加减乘除,数字逻辑处理有时候也需要进行数字运算,所以需要算术运算符。常用的算术运算符主要包括加减乘除和模除(模除运算也叫取余运算)如表 所示:
需要注意的是,Verilog实现除法与模除比较浪费组合逻辑资源,尤其是除法。一般2的指数次幂的乘除法使用移位运算来完成运算,详情可以看移位运算符章节。非2的指数次幂的乘除法一般是调用现成的IP,QUARTUS/ISE等工具软件会有提供,不过这些工具软件提供的IP也是由最底层的组合逻辑(与或非门等)搭建而成的。
关系运算符
关系运算符主要是用来做一些条件判断用的,在进行关系运算符时,如果声明的关系是假的,则返回值是0,如果声明的关系是真的,则返回值是1;所有的关系运算符有着相同的优先级别,关系运算符的优先级别低于算术运算符的优先级别如表所示。
逻辑运算符
逻辑运算符是连接多个关系表达式用的,可实现更加复杂的判断,一般不单独使用,都需要配合具体语句来实现完整的意思。
条件运算符
条件操作符一般来构建从两个输入中选择一个作为输出的条件选择结构,功能等同于always中的if-else语句。
如果a为真,则表达式的值为b,反之为c
位运算符
位运算符是一类最基本的运算符,可以认为它们直接对应数字逻辑中的与、或、非门等逻辑门。位运算符的与、或、非与逻辑运算符逻辑与、逻辑或、逻辑非使用时候容易混淆,逻辑运算符一般用在条件判断上,位运算符一般用在信号赋值上。
移位运算符
移位运算符包括左移位运算符和右移位运算符,这两种移位运算符都用0来填补移出的空位。
假设a有8bit数据位宽,那么a<<2,表示a左移2bit,a还是8bit数据位宽,a的最高2bit数据被移位丢弃了,最低2bit数据固定补0。如果a是3(二进制:00000011),那么3左移2bit,3<<2,就是12(二进制:00001100)。一般使用左移位运算代替乘法,右移位运算代替除法,但是这种也只能表示2的指数次幂的乘除法。
拼接运算符
Verilog中有一个特殊的运算符是C语言中没有的,就是位拼接运算符。用这个运算符可以把两个或多个信号的某些位拼接起来进行运算操作。
将a与b拼接起来,作为一个新的信号
运算符的优先级
介绍完了这么多运算符,大家可能会想到究竟哪个运算符高,哪个运算符低。为了便于大家查看这些运算符的优先级,我们将它们制作成了表格。
具体还可以参考这个表格,是我在培训中完善的表格,不过实际应用中,为避免优先级错误,可多加括号:
Verilog的常用关键字
关键字
注意只有小写的关键字才是保留字。例如,标识符always(这是个关键词)与标识符ALWAYS(非关键词)是不同的。
Verilog的高级知识点
阻塞赋值和非阻塞赋值
在Verilog中有两种类型的赋值语句:阻塞赋值语句(“=”)和非阻塞赋值语句(“<=”)。正确地使用这两种赋值语句对于Verilog的设计和仿真非常重要。
Verilog语言中讲的阻塞赋值与非阻塞赋值,但从字面意思来看,阻塞就是执行的时候在某个地方卡住了,等这个操作执行完在继续执行下面的语句,而非阻塞就是不管执行完没有,我不管执行的结果是什么,反正我继续下面的事情。而Verilog中的阻塞赋值与非阻塞赋值正好也是这个意思,通过执行一个例子,就可以简单地明白了:
1、阻塞赋值可以理解为语句的顺序执行,因此语句的执行顺序很重要;
2、非阻塞赋值可以理解为语句的并行执行,所以语句的执行不考虑顺序;
3、在assign的结构中,必须使用的是阻塞赋值。
也就是说:
阻塞:在本语句中“右式计算”和“左式更新”完全完成之后,才开始执行下一条语句;
非阻塞:当前语句的执行不会阻塞下一语句的执行。
阻塞语句的时序:(串行)
非阻塞语句的时序(并行)
阻塞语句的使用
(1)在时序逻辑电路中一般使用非阻塞赋值。
非阻塞赋值在块结束后才完成赋值操作,此赋值方式可以避免在仿真出现冒险和竞争现象。
(2)在组合逻辑电路中一般使用阻塞赋值。
使用阻塞方式对一个变量进行赋值时,此变量的值在在赋值语句执行完后就立即改变。
(3)在assign语句中必须使用阻塞赋值语句。
assign和always区别
assign语句和always语句是Verilog中的两个基本语句,这两个都是经常使用的语句。
assign语句使用时不能带时钟。always语句可以带时钟,也可以不带时钟。在always不带时钟时,逻辑功能和assign完全一致,都是只产生组合逻辑。比较简单的组合逻辑推荐使用assign语句,比较复杂的组合逻辑推荐使用always语句。示例如下:
什么是latch?
latch是指锁存器,是一种对脉冲电平敏感的存储单元电路。锁存器和寄存器都是基本存储单元。
锁存器是电平触发的存储器,是组合逻辑产生的。
寄存器是边沿触发的存储器,是在时序电路中使用,由时钟触发产生的。
latch的主要危害的是会产生毛刺(glitch),这种毛刺对下一级电路是很危险的。并且其隐蔽性很强,不易查出。因此,在设计中,应尽量避免latch的使用。
出现latch的原因:代码里面出现latch的两个原因是在组合逻辑中,if或者case语句不完整的描述,比如if缺少else分支,case缺少default分支,导致代码在综合过程中出现了latch。
解决办法就是if必须带else分支,case必须带default分支。
大家需要注意下,只有不带时钟的always语句if或者case语句不完整才会产生latch,带时钟的语句if或者case语句不完整描述不会产生latch。下面为缺少else分支的带时钟的always语句和不带时钟的always语句,通过实际产生的电路图可以看到第二个是有一个latch的,第一个仍然是普通的带有时钟的寄存器。
状态机
Verilog是硬件描述语言,硬件电路是并行执行的,当需要按照流程或者步骤来完成某个功能时,代码中通常会使用很多个if嵌套语句来实现,这样就增加了代码的复杂度,以及降低了代码的可读性,这个时候就可以使用状态机来编写代码。状态机相当于一个控制器,它将一项功能的完成分解为若干步,每一步对应于二进制的一个状态,通过预先设计的顺序在各状态之间进行转换,状态转换的过程就是实现逻辑功能的过程。
状态机,全称是有限状态机(Finite State Machine,缩写为FSM),是一种在有限个状态之间按一定规律转换的时序电路,可以认为是组合逻辑和时序逻辑的一种组合。状态机通过控制各个状态的跳转来控制流程,使得整个代码看上去更加清晰易懂,在控制复杂流程的时候,状态机优势明显,因此基本上都会用到状态机,如SDRAM控制器等。
根据状态机的输出是否与输入条件相关,可将状态机分为两大类,即摩尔(Moore)型状态机和米勒(Mealy)型状态机。
Ø Mealy状态机:组合逻辑的输出不仅取决于当前状态,还取决于输入状态。
Moore状态机:组合逻辑的输出只取决于当前状态。
三段式状态机
根据状态机的实际写法,状态机还可以分为一段式、二段式和三段式状态机。
一段式:整个状态机写到一个always模块里面,在该模块中既描述状态转移,又描述状态的输入和输出。不推荐采用这种状态机,因为从代码风格方面来讲,一般都会要求把组合逻辑和时序逻辑分开;从代码维护和升级来说,组合逻辑和时序逻辑混合在一起不利于代码维护和修改,也不利于约束。
二段式:用两个always模块来描述状态机,其中一个always模块采用同步时序描述状态转移;另一个模块采用组合逻辑判断状态转移条件,描述状态转移规律以及输出。不同于一段式状态机的是,它需要定义两个状态,现态和次态,然后通过现态和次态的转换来实现时序逻辑。
三段式:在两个always模块描述方法基础上,使用三个always模块,一个always模块采用同步时序描述状态转移,一个always采用组合逻辑判断状态转移条件,描述状态转移规律,另一个always模块描述状态输出(可以用组合电路输出,也可以时序电路输出)
推荐使用三段式状态机
实际应用中三段式状态机使用最多,因为三段式状态机将组合逻辑和时序分开,有利于综合器分析优化以及程序的维护;
并且三段式状态机将状态转移与状态输出分开,使代码看上去更加清晰易懂,提高了代码的可读性,推荐大家使用三段式状态机,本文也着重讲解三段式。
三段式状态机的基本格式是:
第一个always语句实现同步状态跳转;
第二个always语句采用组合逻辑判断状态转移条件;
第三个always语句描述状态输出(可以用组合电路输出,也可以时序电路输出)。
在开始编写状态机代码之前,一般先画出状态跳转图,这样在编写代码时思路会比较清晰,下面以一个7分频为例(对于分频等较简单的功能,可以不使用状态机,这里只是演示状态机编写的方法),状态跳转图如下图所示。
这里是使用独热码的方式来定义状态机,每个状态只有一位为1,当然也可以直接定义成十进制的0,1,2……7。
因为我们定义成独热码的方式,每一个状态的位宽为7位,接下来还需要定义两个7位的寄存器,一个用来表示当前状态,另一个用来表示下一个状态;
parameter S0 = 7'b0000001; //独热码定义方式
parameter S1 = 7'b0000010;
parameter S2 = 7'b0000100;
parameter S3 = 7'b0001000;
parameter S4 = 7'b0010000;
parameter S5 = 7'b0100000;
parameter S6 = 7'b1000000;
reg [6:0] curr_st ; //当前状态
reg [6:0] nextst ; //下一个状态
接下来就可以使用三个always语句来开始编写状态机的代码,第一个always采用同步时序描述状态转移;
第二个always采用组合逻辑判断状态转移条件;
第三个always是描述状态输出;
评论
查看更多