Verilog语言学习!
目录
[1.1 Verilog简介](#1.1 Verilog简介)
[1.2 Verilog 和 C 的区别](#1.2 Verilog 和 C 的区别)
[1.3 Verilog 学习](#1.3 Verilog 学习)
[2.1 Verilog 的逻辑值](#2.1 Verilog 的逻辑值)
[2.2 数字进制](#2.2 数字进制)
[2.3 Verilog标识符](#2.3 Verilog标识符)
[2.4 Verilog 的数据类型](#2.4 Verilog 的数据类型)
[2.4.1 寄存器类型](#2.4.1 寄存器类型)
[2.4.2 线网类型](#2.4.2 线网类型)
[2.4.3 参数类型](#2.4.3 参数类型)
[2.5 Verilog 运算符](#2.5 Verilog 运算符)
[2.5.1 算术运算符](#2.5.1 算术运算符)
[2.5.2 关系运算符](#2.5.2 关系运算符)
[2.5.3 逻辑运算符](#2.5.3 逻辑运算符)
[2.5.4 条件运算符](#2.5.4 条件运算符)
[2.5.5 位运算符](#2.5.5 位运算符)
[2.5.6 移位运算符](#2.5.6 移位运算符)
[2.5.7 拼接运算符](#2.5.7 拼接运算符)
[2.5.8 运算符的优先级](#2.5.8 运算符的优先级)
[2.6 注释](#2.6 注释)
[2.7 Verilog关键字](#2.7 Verilog关键字)
[3.1 模块的结构](#3.1 模块的结构)
[3.2 模块的端口定义](#3.2 模块的端口定义)
[3.3 I/O说明](#3.3 I/O说明)
[3.4 内部信号声明](#3.4 内部信号声明)
[3.5 功能定义](#3.5 功能定义)
[3.5.1 assign声明语句](#3.5.1 assign声明语句)
[3.5.2 实例元件](#3.5.2 实例元件)
[3.5.3 always块](#3.5.3 always块)
[3.6 模块的调用](#3.6 模块的调用)
[4.1 结构语句](#4.1 结构语句)
[4.1.1 initial语句](#4.1.1 initial语句)
[4.1.2 always语句](#4.1.2 always语句)
[(1)always 语句语法](#(1)always 语句语法)
[(2)always 语句的沿触发和电平触发](#(2)always 语句的沿触发和电平触发)
[4.2 赋值语句](#4.2 赋值语句)
[4.2.1 阻塞赋值(blocking)](#4.2.1 阻塞赋值(blocking))
[4.2.2 非阻塞赋值(Non-Blocking)](#4.2.2 非阻塞赋值(Non-Blocking))
[4.2.3 赋值语句的使用](#4.2.3 赋值语句的使用)
[(2)assign 和 always 区别:](#(2)assign 和 always 区别:)
[(3)带时钟和不带时钟的 always](#(3)带时钟和不带时钟的 always)
[4.3 条件语句](#4.3 条件语句)
[4.3.1 if_elas语句](#4.3.1 if_elas语句)
[4.3.2 case语句(多分支选择语句)](#4.3.2 case语句(多分支选择语句))
[5.1 状态机概念](#5.1 状态机概念)
[5.2 状态机模型](#5.2 状态机模型)
[5.2.1 Mealy 状态机](#5.2.1 Mealy 状态机)
[5.2.2 Moore 状态机](#5.2.2 Moore 状态机)
[5.3 状态机设计](#5.3 状态机设计)
[5.3.1 状态空间定义](#5.3.1 状态空间定义)
[5.3.2 状态跳转](#5.3.2 状态跳转)
[5.3.3 下个状态判断](#5.3.3 下个状态判断)
[5.3.4 各个状态下的动作](#5.3.4 各个状态下的动作)
[5.3.5 三段式状态机](#5.3.5 三段式状态机)
前言
在 FPGA 设计里面,Verilog 语言可以对大规模的电路和复杂的逻辑电路进行设计,且目前 Verilog 已经在 FPGA 开发/IC 设计领域占据绝对的领导地位。本文就介绍了Verilog语言的基础内容。
一、Verilog语言是什么?
1.1 Verilog简介
Verilog 是一种硬件描述语言,以文本形式来描述数字系统硬件的结构和行为的语言,用它可以表示逻辑电路图、逻辑表达式,还可以表示数字逻辑系统所完成的逻辑功能。
1.2 Verilog 和 C 的区别
Verilog 是硬件描述语言,在编译下载到 FPGA 之后,会生成电路,所以 Verilog 全部是并行处理与运行的。这里的并行是指电路在上电之后,给各个模块一个时钟信号,这些模块会同时开始工作。所以FPGA是并行处理的。
C 语言是软件语言,编译下载到单片机/CPU 之后,还是软件指令,而不会根据你的代码生成相应的硬件电路,而单片机/CPU 处理软件指令需要取址、译码、执行,是串行执行的。这里的串行是指C语言在编译下载到单片机上后,是存储器中的一组指令,单片机对第一条指令进行要取址、译码、执行处理后,再对第二条处理,再对第三条,... ...,这样一条一条依次处理就是串行执行的一个过程。
Verilog 和 C 的区别也是 FPGA 和单片机/CPU 的区别,由于 FPGA 全部并行处理,所以处理速度非常快,这个是 FPGA 的最大优势,这一点是单片机/CPU 替代不了的。因为单片机/CPU是串行处理。
1.3 Verilog 学习
Verilog 作为一种高级的硬件描述语言,它的很多语法现象与C语言非常相似,因此在有C语言的基础上对照类比学习会非常容易。Verilog 学习过程中要培养硬件设计的思想,把新概念和硬件结构结合联系起来,着重理解Verilog 的并行特性。
二、Verilog基础知识
2.1 Verilog 的逻辑值
先看下逻辑电路中有四种值,即四种状态:
- 逻辑 0:表示低电平,也就是对应我们电路的 GND;
- 逻辑 1:表示高电平,也就是对应我们电路的 VCC;
- 逻辑 X:表示未知,有可能是高电平,也有可能是低电平;
- 逻辑 Z:表示高阻态,外部没有激励信号是一个悬空状态。
![](https://i-blog.csdnimg.cn/blog_migrate/a41505078ab0ff0fa60174bce8e2d3d7.png)
在数字电路里常会使用逻辑值0和1来代表逻辑的假和真,0代表假、1代表真。0和1对应在实际电路中就代表高低电平,0代表低电平、1代表高电平。在Verilog语言中0和1也表示高低电平,逻辑0表示低电平;逻辑1表示高电平。
- 图中第一个模块,左边电路接GND,右边电路输出就是低电平,用逻辑0表示。
- 图中第二个模块,左边电路接VCC,右边电路输出就是高电平,用逻辑1表示。
在Verilog语言中除了逻辑1和逻辑0还有另外两个逻辑,一个是逻辑 X,另外一个是逻辑 Z。逻辑 X 和逻辑 Z 其实是数字电路里的概念,在Verilog语言里面也和数字电路里一样的。对于逻辑 X 而言,它表示的是一个未知的电平,有可能是低电平也有可能是高电平。
- 图中第三个模块,左边两个器件,上面一个接VCC,下面一个接GND,这两个器件同时输出到一个端口,这个端口的值就是一个未知的状态,有可能是高电平逻辑1也有可能是低电平逻辑0,也就是右边的输出端用 X 表示这种未知的状态。
逻辑 Z 就表示高阻态,外部没有激励信号而是是一个悬空状态。高阻态不止是针对FPGA的引脚而言,对于一些内部信号同样会呈献高阻态。
- 图中第四个模块,可以看到器件的使能端给了一个0,也就是左边的输入并没有传递给右边的输出,那么这个输出端的状态就没有被输入端来驱动。
逻辑 X 与逻辑 Z 的区别:
- 逻辑 X 虽然它的状态是未知,但是它只能是逻辑1或逻辑0这两个状态。
- Z高阻态处于悬空的状态。就是它没有输入,也不能判断它是高电平逻辑1还是低电平逻辑0,它甚至有可能处于高电平和低电平之间的状态。对于这种没有外部输入信号的一个状态就称为高阻态。
2.2 数字进制
Verilog 数字进制格式包括二进制、八进制、十进制和十六进制,一般常用的为二进制、十进制和十六进制。
Verilog 语言中二进制用b或B表示、八进制用o或O表示、十进制用d或D表示、十六进制用h或H表示。Verilog 语言数字进制的表示是和C语言一致的。
Verilog 语言中数字表示的语法格式:位宽+进制+数值。
- 二进制表示例子:4'b0101 表示 4 位二进制数字 0101;
4表示数据位宽,也就是这个数据是一个4位的位宽。b表示二进制,表示后面数值是用二进制来表示的。0101表示二进制数值。
- 十进制表示例子:4'd2 表示 4 位十进制数字 2(二进制 0010);
4表示数据位宽,也就是对这个十进制的数值以二进制且4位的位宽( 0010)的形式来存储。d表示十进制,表示后面数值是用十进制来表示的。2表示十进制数值为2。
- 十六进制表示例子:4'ha 表示 4 位十六进制数字 a(二进制 1010),
4表示数据位宽,也就是对这个十六进制的数值以二进制且4位的位宽( 1010)的形式来存储。h表示十六进制,表示后面数值是用十六进制来表示的。4表示十六进制数值为4。
十六进制的计数方式为 0,1, 2...9,a,b,c,d,e,f,最大计数为 f(f:十进制表示为 15)。
注意:数据位宽是表示这个数的二进制位宽,因为在数字电路中,数值实际上都是以二进制的形式来存储的。所以十进制和十六进制表示的数据位宽都是数值的二进制位宽。
- 如果没有指定数值的位宽,默认位宽为32位的位宽。
- 如果既没有指定数值的位宽,也没有指定进制,就默认32位的十进制。
可以给数值加下划线来提高数值的可读性,下划线在编译的时候会自动忽略掉。
例如:16'b1001_1010_1010_1001=16'h9AA9
2.3 Verilog标识符
Verilog标识符类似于C语言里的变量名。
标识符定义:
标识符(identifier)用于定义模块名、端口名和信号名等。
Verilog 的标识符可以是任意一组字母、数字、$和_(下划线)符号的组合。
但标识符的第一个字符必须是字母或者下划线。
标识符是区分大小写的。
规范建议:
- 不建议大小写混合使用。
- 普通内部信号建议全部小写。
- 信号命名最好体现信号的含义,做到简洁、清晰、易懂。
以下是一些书写规范的要求:
- 用有意义的有效的名字如 sum、cpu_addr 等。(sum表示和、addr表示地址)
- 用下划线区分词语组合,如 cpu_addr(CPU地址)。
- 采用一些前缀或后缀,比如:时钟采用 clk 前缀:clk_50m(50MHz时钟),clk_cpu(CPU时钟信号);低电平采用_n 后缀:enable_n(低电平使能)。
- 统一缩写,如全局复位信号 rst。
- 同一信号在不同层次保持一致性,如同一时钟信号必须在各模块保持一致。
- 自定义的标识符不能与保留字(关键词)同名。
- 参数统一采用大写,如定义参数使用 SIZE。
2.4 Verilog 的数据类型
在 Verilog 语法中,主要有三大类数据类型,即寄存器类型、线网类型和参数类型。
从名称中可以看出,真正在数字电路中起作用的数据类型应该是寄存器类型和线网类型。也就是说:
- 寄存器数据类型和线网类型都是可以映射到实际的物理电路上去的。
- 参数类型是给编译器来识别的数据类型。
2.4.1 寄存器类型
寄存器类型表示一个抽象的数据存储单元,通过赋值语句可以改变寄存器的值。
寄存器数据类型的关键字是reg,reg类型数据的默认初始值为不定值X。
reg类型数据只能在 always 语句和 initial 语句中被赋值,不能在定义寄存器时就赋初值。这一点和C语言命名定义变量时就给一个初始值是不一样的。并且Verilog语言中寄存器的值是从一个赋值到另一个赋值过程中被保存下来。
- 如果该过程语句描述的是时序逻辑,即 always 语句带有时钟信号,则该寄存器变量对应为触发器;
- 如果该过程语句描述的是组合逻辑,即 always 语句不带有时钟信号,则该寄存器变量对应为硬件连线。
寄存器类型的缺省值是 X(未知状态)。
寄存器数据类型有很多种,如 reg、integer、real 等,其中最常用的就是 reg 类型。
- reg定义语法格式:关键字reg + [寄存器位宽 ]+ 标识符名称 + ;
- reg位宽高位在前,低位在后。
- reg定义中没给出位宽的时候就默认位宽是1.
代码示例:
cpp
//reg define
reg [31:0] delay_cnt; //延时计数器 :reg类型、位宽为32、名为delay_cnt
reg key_flag ; //按键标志
上述代码定义了一个reg类型,位宽为32,名为 delay_cnt 的寄存器;和一个reg类型,位宽为1,名为 key_flag 的寄存器。
2.4.2 线网类型
线网表示 Verilog 结构化元件间的物理连线。它的值由驱动元件的值决定,例如连续赋值或门的输出。如果没有驱动元件连接到线网,线网的缺省值为 z(高阻态)。
线网数据类型表示结构实体(例如门)之间的物理连线。
线网数据类型的变量不能存储值,它的值是由驱动他的元件所决定的(例如门或连接赋值语句,assign)。
如果没有驱动元件连接到线网类型的变量上,该线网类型的变量就是高阻的,即其值为Z。
线网类型同寄存器类型一样也是有很多种,如 tri 和 wire 等,其中最常用的就是 wire 类型。
Verilog 语言中wire型信号的格式同reg型信号的格式很类似:
- wire定义语法格式:关键字reg + [寄存器位宽 ]+ 标识符名称 + ;
- wire位宽高位在前,低位在后。
- wire定义中没给出位宽的时候就默认位宽是1.
代码示例:
cpp
//wire define
wire data_en; //数据使能信号
wire [7:0] data ; //数据
上述代码定义了一个wire类型,位宽为1,名为 data_en 的线网数据;和一个wire类型,位宽为8,名为 data 的线网数据。
2.4.3 参数类型
参数其实就是一个常量,在Verilog 语言中用 parameter 定义常量。这一点有点类似于C语言中define的作用,但是语法结构不一样。
参数型数据常被用于定义状态机的状态、数据位宽和延迟大小等。
采用标识符来代表一个常量可以提高程序的可读性和可维护性。
由于它可以在编译时修改参数的值,因此它又常被用于一些参数可调的模块中,使用户在实例化模块时,可以根据需要配置参数。
在定义参数时,我们可以一次定义多个参数,参数与参数之间需要用逗号隔开。这里我们需要注意的是参数的定义是局部的,只在当前模块中有效。
- parameter定义语法格式:parameter + 标识符名称 + 等号 + ;
- parameter定义常量可以一次定义多个参数,参数参数之间需要用逗号隔开。
- parameter定义中每个参数定义的右边必须是一个常熟表达式。
代码示例:
cpp
//parameter define
parameter DATA_WIDTH = 8; //数据位宽为8位
上述代码定义了一个parameter类型,名为 DATA_WIDTH ,值为8的参数。
2.5 Verilog 运算符
Verilog 中的运算符按照功能可以 分为下述类型:
1、算术运算符、
2、关系运算符、
3、逻辑运算符、
4、条件运算符、
5、位运算符、
6、移位运算符、
7、拼接运算符。
下面分别对这些运算符进行介绍。Verilog 中的运算符很多和C语言是保持一致的,可以类比学习。
2.5.1 算术运算符
算术运算符,简单来说,就是数学运算里面的加减乘除,数字逻辑处理有时候也需要进行数字运算, 所以需要算术运算符。常用的算术运算符主要包括加减乘除和模除(模除运算也叫取余运算)。
算术运算符如下表所示:
![](https://i-blog.csdnimg.cn/blog_migrate/af52ef91d3cc147c4a25409b5c661e82.png)
需要注意:
- 在除法运算的时候,只能实现整除,也就是说在 Verilog 语言中 a/b 的小数部分是被省略掉的。只取整数部分。
- 模除运算,a%b 的结果就是a除以b的余数。模除运算要求%两侧均为整数数据。进行取模运算时,结果值得符号位采用模运算式里第一个操作数的符号位。
模除运算举例:
- 11 % 3 = 2 余数为2;
- 12 % 3 = 0 余数为0,即无余数;
- -10 % 3 = -1 结果取第一个操作数 -10 的符号位,余数为 -1;
- 11 % -3 = 2 结果取第一个操作数 11 的符号位,余数为 2.
Verilog 实现乘除比较浪费组合逻辑资源,尤其是除法。一般 2 的指数次幂的乘除法使用移位运算来完成运算,下文移位运算符会具体介绍。非 2 的指数次幂的乘除法一般是调用现成的 IP, QUARTUS/ISE 等工具软件会有提供,不过这些工具软件提供的 IP 也是由最底层的组合逻辑(与或非门等) 搭建而成的。
在进行算数运算操作时,如果某一个操作数有不确定的值X,则整个结果也为不定值X。
2.5.2 关系运算符
关系运算符主要是用来做一些条件判断用的。
在进行关系运算符时:
- 如果声明的关系是假的,则返回值是 0;
- 如果声明的关系是真的,则返回值是 1。
- 如果某个操作数的值不定,则关系是模糊的,返回值是不定值。
所有的关系运算符有着相同的优先级别,关系运算符的优先级别低于算术运算符的优先级别。
关系运算符如下表所示:
![](https://i-blog.csdnimg.cn/blog_migrate/bb213a7b7d2b41cbdc838fa44d66a5de.png)
如果要判断两个变量是否是相等的,需要用 ==,一个等于号=是赋值运算,这一点和C语言类似。
2.5.3 逻辑运算符
逻辑运算符是连接多个关系表达式用的,可实现更加复杂的判断,一般不单独使用,都需要配合具体语句来实现完整的意思。
逻辑运算符如下表所示:
![](https://i-blog.csdnimg.cn/blog_migrate/af2d54ae70ec341004c76435714e1dcc.png)
逻辑运算真值表:
|---|---|----|----|--------|--------|
| a | b | !a | !b | a&&b | a||b |
| 真 | 真 | 假 | 假 | 真 | 真 |
| 真 | 假 | 假 | 真 | 假 | 真 |
| 假 | 真 | 真 | 假 | 假 | 真 |
| 假 | 假 | 真 | 真 | 假 | 假 |
2.5.4 条件运算符
条件操作符一般来构建从两个输入中选择一个作为输出的条件选择结构,功能等同于 always 中的 if-else 语句。
条件操作符如下表所示。
![](https://i-blog.csdnimg.cn/blog_migrate/261551f3d4056ac63e1c0c12b56213d5.png)
a ? b : c 这里a是一个判断条件,? 表示是否满足 a 这个判断条件,如果 a 条件满足就执行 b ;如果 a 条件不满足就执行 c ,条件运算符就相当于 if-else 语句的简单写法。
条件运算符举例:
cpp
result = (a > b) ? a : b;
代码对等号左边变量result赋值,
等号右边的条件运算符的运行逻辑是先判断a是否大于b:
- 若a大于b,给result赋值a;
- 若b大于a,给result赋值b。
代码的作用就是将a和b比较取最大值,将最大值赋值给result。
上述代码就相当于如下代码逻辑:
cpp
if(a > b)
result = a;
else
result = b;
2.5.5 位运算符
位运算符是一类最基本的运算符,可以认为它们直接对应数字逻辑中的与、或、非门等逻辑门。
常用的位运算符如下表所示:
![](https://i-blog.csdnimg.cn/blog_migrate/057cf5394e30bd893a832ae7eb89daec.png)
位运算符的与、或、非与逻辑运算符逻辑与、逻辑或、逻辑非使用时候容易混淆,逻辑运算符一般用在条件判断上,位运算符一般用在信号赋值上。
注意:
两个不同长度的数据进行位运算时,系统会自动地将两个数据按右端对齐,位数少的操作数会在相应的高位用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 |
2.5.6 移位运算符
移位运算符包括左移位运算符和右移位运算符。
移位运算符如下表所示:
![](https://i-blog.csdnimg.cn/blog_migrate/27cd09c255c762291fca5f228f57d965.png)
两种移位运算符都用 0 来填补移出的空位。
- 左移时,位宽增加;
- 右移时,位宽不变。
举例:
- 4'b1001 << 2 = 6'b100100;
- 4'b1001 >> 1 = 4'b0100;
假设 a 有 8bit 数据位宽,那么 a<<2,表示 a 左移 2bit,a 还是 8bit 数据位宽,a 的最高 2bit 数据被移位丢弃了,最低 2bit 数据固定补 0。如果 a 是 3(二进制:00000011),那么 3 左移 2bit,3<<2,就是 12 (二进制:00001100)。一般使用左移位运算代替乘法,右移位运算代替除法,但是这种也只能表示 2 的指数次幂的乘除法。
2.5.7 拼接运算符
Verilog 中有一个特殊的运算符是 C 语言中没有的,就是位拼接运算符。用这个运算符可以把两个或多个信号的某些位拼接起来进行运算操作。
位拼接运算符如下表所示。
![](https://i-blog.csdnimg.cn/blog_migrate/4233d6079a07bebee943e6f0799d1518.png)
注意:
在位拼接表达式中不允许存在没有指明位数的信号,这是因为在计算机拼接信号位宽的大小时必须知道其中每个信号的位宽。
举例:
- c = { a ,b [3:0] }; 就相当于 c = { a ,b[3] ,b[2] ,b[1] ,b[0] };
- { 4 { w } } 就相当于 { w ,w ,w ,w }
- { b ,{3 { a ,b } } } 就相当于 { b ,a ,b ,a ,b ,a ,b }
2.5.8 运算符的优先级
运算符的优先级,如下表所示:
![](https://i-blog.csdnimg.cn/blog_migrate/2568ce46bbc058abb8fa24dc0a60d075.png)
为了提程序的可读性,明确表达各运算符之间的优先关系,建议使用括号。
2.6 注释
Verilog HDL 中有两种注释的方式,一种是以"/*"符号开始,"*/"结束,在两个符号之间的语句都是注释语句,因此可扩展到多行。
示例如下:
cpp
/* statement1 ,
statement2,
......
statementn */
以上 n 个语句都是注释语句。
另一种是以//开头的语句,它表示以//开始到本行结束都属于注释语句。
示例如下:
cpp
//statement1
建议使用//作为注释。
2.7 Verilog关键字
Verilog 和 C 语言类似,都因编写需要定义了一系列保留字,叫做关键字(或关键词)。这些关键字用来组织语言结构,是识别语法的关键。
下表列出了 Verilog 中的关键字:
![](https://i-blog.csdnimg.cn/blog_migrate/49ae543ea4d64b0a0a50a395ad51648c.png)
注意:
- 在编写Verilog语言时,定义的变量名不要与关键字重合,产生冲突。
- 只有小写的关键字才是保留字。例如,标识符 always(这是个关键词)与标识符 ALWAYS(非关键词)是不同的。
经常使用的关键字如下表所示:
![](https://i-blog.csdnimg.cn/blog_migrate/f174bc3d41ca46d07afc27dba2aeaad8.png)
三、程序框架
3.1 模块的结构
C语言中基本的功能模块是函数。Verilog语言也与此类似。
Verilog语言中的基本设计单元是"模块"(block)。
一个模块是由两部分组成的,一部分描述接口,另一部分描述逻辑功能,即定义输入是如何影响输出的。
在许多方面,程序模块和电路图符号是一致的,这是因为电路图符号的引脚也就是程序模块的接口。而程序模块描述了电路符号所能实现的逻辑功能。
代码示例:
cpp
module block(a,b,c,d);
input a,b;
output c,d;
assign c = a | b;
assign d = a & b;
endmodule
第 1 行为模块定义,模块定义以 module 开始,endmodule 结束。
- 代码:block(a,s,c,d);是模块端口的定义,这里定义了一个名为block的模块,它有a,s,c,d四个端口。
input表示输入;output表示输出。
- 代码:input a,b;就表示a和b是输入信号;
- 代码:output c,d;就表示c和d是输出信号。
以上这两句代码部分就叫做I/O的说明。
assign是给线网类型的变量赋值的一种方法。这里端口a,b,c,d的数据类型默认的是wire类型的变量,对于输出信号c和d来讲,需要给这两个信号赋值,就可以使用assign的赋值语句来给wire类型的数据变量赋值。
- 代码:assign c = a | b;的意思就是给端口c赋值,c的值为a和b两个信号的或运算。
- 代码:assign d = a & b;的意思就是给端口c赋值,c的值为a和b两个信号的与运算。
以上这俩句代码部分就叫做模块的功能定义,描述了block模块的主要功能。
总结:
上述Verilog代码设计中:
cpp
input a,b;
output c,d;
以上两句代码说明接口的信号流向;
cpp
assign c = a | b;
assign d = a & b;
以上两句代码说明了模块的逻辑功能。
Verilog结构位在module和endmodule声明语句之间。
每个Verilog程序包括4个主要部分:
- 端口定义、
- I/O说明、
- 内部信号声明、
- 功能定义。
![](https://i-blog.csdnimg.cn/blog_migrate/4df8b692ca7fc37a86dc71a4126e7041.png)
上图表示了模块的接口,左侧表示模块的输入接口,有端口a和端口b;右侧表示模块的输出接口,有端口c和端口d。
此图主要表示了模块的端口定义和I/O说明的内容。
![](https://i-blog.csdnimg.cn/blog_migrate/7fb83c7118edc5f521d9e0c5378bbf71.png)
上图表示了模块的功能定义部分,如果Verilog设计的模块是可综合的,那就会综合出和图中类似的电路结构。可以在电路结构中看到,输入端是端口a和端口b;输出端是端口c和端口d。
这个电路的功能就是在内部生成两个门电路:
- 上方是一个或门,是把a和b两个信号进行一个或运算的操作,最终结果输出给c;
- 下方是一个与门,是把a和b两个信号进行一个与运算的操作,最终结果输出给d。
从图中也可以看出,block模块分成了两个部分,一部分描述接口,另一部分描述功能。
可综合与不可综合:
Verilog作为硬件描述语言,可综合就代表着设计的Verilog语句可综合得到一个类似上图的由门级结构组成的电路网表。也就是可综合的模块最终是可以生成一个物理的电路模块的。
而Verilog里还有一种叫不可综合的模块,不可综合的模块就不能对应实际的电路,不可综合模块的作用是给其他模块做测试文件,也就是仿真文件,在仿真文件里就可以用一些不可综合的语句,仿真工具可以识别不可综合的语句。
这里Verilog设计的block模块是一个可综合的模块,可以综合出图中所示的一个电路结构。
3.2 模块的端口定义
模块的端口名声明了模块的输入输出口。
端口定义语法格式:
module 模块名(口1,口2,口3,......);
注意:
- 端口和端口之间用 , 隔开
- 端口定义后要用**;** 结尾
语句后都需要有一个分号,这一点Verilog语言和C语言类似,若忘记在句末加分号,编译的时候就会报错。
模块的端口表示的是模块的输入和输出口名,也就是说端口名是与别的模块联系端口的标识。
3.3 I/O说明
I/O其实就是input和output的缩写,I/O说明作用就是说明这个模块中的端口哪些是输出,哪些是输入。
input 表示输入;output 表示输出。
I/O说明语法说明:
-
输入口:
input [信号位宽-1,0] 端口名;
-
输出口:
output [信号位宽-1,0] 端口名;
-
输入输出口:
Inout [信号位宽-1,0] 端口名;
3.4 内部信号声明
在模块内用到的或者是与端口有关的wire、reg类型变量的声明。
语法格式:
reg [信号位宽-1,0] R变量;
wire [信号位宽-1,0] W变量;
3.5 功能定义
模块中最重要的部分是逻辑功能定义部分,有assign声明语句、实例元件、always块三种方法可在模块中产生逻辑。
3.5.1 assign声明语句
代码示例:
assign a = b & c;
assign 语句的使用只需要写一个 assign ,后面再加一个方程式即可。
代码示例中的方程式描述了一个有两个输入的与门。
3.5.2 实例元件
采用实例元件的方法和在电路图中插入库元件一样,只需写入元件的名字,匹配好相连的引脚即可。
代码示例:
and #2 u1(q,a,b);
代码示例表示在设计中用到一个跟与门(and)一样的名为 u1的与门,其输入端为a、b,输出端为q。输出延迟为2个单位时间。要求每个实例元件的名字必须是唯一的,以避免与其他调用与门(and)的实例混淆。
3.5.3 always块
always语句是描述组合逻辑常用的方法,always语句块既可用于描述组合逻辑,也可用于描述时序逻辑。
代码示例:
always@(posedge clk or posedge clr)
begin
if(clr)
q<=0;
else if(en)
q<=d;
end
always语句块的例子生成了一个带有异步清除端的D触发器。always块可用很多描述手段来表达逻辑,代码示例中就用 if else 语句来表达逻辑关系。
总结:
- assign语句描述组合逻辑。
- always语句描述组合逻辑和时序逻辑。
注意:
- assign语句、实例元件、always块这三种逻辑功能是并行的。
- 在always块中,逻辑是顺序执行的。 而多个always块之间是并行的。
3.6 模块的调用
模块的调用类似于C语言里函数的调用这样一个关系。在模块调用时,信号通过模块端口在模块之间传递。
模块调用时,在调用的模块中,有些信号要输入到被调用的模块中,有的信号需要从被调用的模块中取出来。
调用模块时,其端口可以用以下两种方法连接:
(1)在调用时,严格按照模块定义的端口顺序来连接,不用标明原模块定义时规定的端口名。
模块名(
连接端口1信号名,
连接端口2信号名,
连接端口3信号名,
连接端口4信号名,
......
);
(2)在调用时用" . "符号,标明原模块是定义时规定的端口名,
模块名(
.端口1名(连接1信号名),
.端口2名(连接2信号名),
.端口3名(连接3信号名),
......
);
这样用" . "符号表示的好处在于可以用端口名与被引用模块的端口相对应,而不必严格按端口顺序对应,提高了程序的可读性和可移植性。
代码示例:
//time_count模块端口定义
module time_count(
input clk,
input rst_n,
output reg flag
);
parameter MAX_NUM = 50000_000;
//time_count模块的例化
time_count #(
.MAX_NUM (TIME_SHOW)
) u_time_count(
.clk (sys_clk),
.rst_n (sys_rst_n),
.flag (add_flag)
);
模块的调用也叫做例化,代码示例中例化了一个名为time_count的模块,在调用time_count模块的时候,首先要把time_count模块名列出来,然后需要给例化的模块重新起一个名字,示例中名字就叫做u_time_count。当然可以同时例化多个相同的模块,例化的多个模块需要起不同的名称。实例中只例化了一个模块,叫做u_time_count。
接下来需要连接端口,示例中采用一个点加一个端口名的方式,点加端口名就表示被例化模块中定义的端口名。示例中clk、rst_n、flag都是time_count模块的端口。后面的括号中就是需要连接的信号名,示例中与clk端口连接的信号名为sys_clk;与rst_n端口连接的信号名为sys_rst_n;与flag端口连接的信号名为add_flag
注意 :在模块的输入端可以是reg型也可以是wire型,但是在模块的输出端必须是wire型。在模块调用的时候,用于传递信号的变量的位宽必须保持一致。
示例中模块调用时,有 #(.MAX_NUM (TIME_SHOW)) 这样一个语句,这个是一种参数传递的方法,点后面 MAX_NUM 是一个参数名,点加参数名表示的是被例化模块time_count的参数。括号里面 TIME_SHOW 是例化模块 u_time_count 中传递的新的参数值。
四、Verilog语句
4.1 结构语句
initial 和 always 语句都是结构语句。一个程序模块可以有多个 initial 和 always 过程块。每个 initial 和 always 说明语句在仿真的一开始同时立即开始执行。initial 语句只执行一次,而 always 语句则是不断地重复执行,直到仿真过程结束。但 always 语句后跟着的过程块是否运行,则要看他的触发条件是否满足,满足一次运行一次,直至仿真过程结束。
在一个模块里,使用initial和always语句的次数是不受限制的,它们都是同时开始运行的。
4.1.1 initial语句
initial 语句它在模块中只执行一次。它常用于测试文件的编写,用来产生仿真测试信号(激励信号),或者用于对存储器变量赋初值。
initial语句的语法:
initial
begin
语句1;
语句2;
语句3;
......
语句n;
end
initial语句后有 begin和end框起来很多条语句,这里begin和end相当于C语言中的{ } ,把很多条语句组合成一个大的语句块。
代码示例:
(1)用initial块对存储器变量赋初值
initial
begin
areg = 0; //初始化寄存器areg
for(index = 0 ; index < size ; index = index + 1)
memory[index] = 0; //初始化一个memory
end
在这个例子中initial语句在仿真开始时对各个变量进行初始化,注意这个对初始化过程不需要任何仿真时间,即在0ns时间内,便可以完成存储器的初始化工作
(2)用initial语句来产生激励波形
initial begin
sys_clk <= 1'b0;//给输入信号初始值
sys_rst_n <= 1'b0;
touch_key <= 1'b0;
#200 sys_rst_n <= 1'b1;
#30 touch_key <= 1'b1;
#40 touch_key <= 1'b0;
#40 touch_key <= 1'b1;
#40 touch_key <= 1'b0;
#40 touch_key <= 1'b1;
end
在initial语句里首先给sys_clk、sys_rst_n、touch_key三个信号赋了初值,初值为0.在上电之后,会立刻执行这三个赋初值的语句。因为这三个语句之间没有延时,所以这三个语句是同时执行的。
这里#表示延时,#200表示延时200个单位的时间,延时之后系统复位信号sys_rst_n赋值为1.接下来又是延时30个时间单位,给touch_key赋值为1.然后又延时,再给touch_key赋不同的值。
这个代码是用initial语句来产生激励波形作为电路的测试仿真信号。
总结:一个模块中可以有多个initial块,它们都是并行运行的。initial块常用于测试文件和虚拟模块的编写,用来产生仿真测试信号和设置信号记录等仿真环境。
4.1.2 always语句
(1)always 语句语法
always 语句一直在不断地重复活动。但是只有和一定的时间控制结合在一起才有作用。
always语句在仿真过程中是不断活动着的。但是always语句后跟着的过程块是否执行,则要看它的触发条件是否满足,如满足则运行过程块一次;如不满足,则always语句循环活动但不运行过程块。
always语句格式:
always <时序控制> <语句> ;
always语句由于其不断活动的特性,只有和一定的时序控制结合在一起才有用。如果一个always语句没有时序控制,则这个always语句将会使仿真器产生死锁。
代码如下:
always sys_clk = ~sys_clk;
这个always语句将会生成一个0延迟的无限循环跳变过程。这时会发生仿真死锁。但如果加上时序控制,则这个always语句将会变为一条非常有用的描述语句。
代码如下:
always #10 sys_clk = ~sys_clk;//产生时钟
这个always语句不断的对时钟信号进行取反。#10表示延时10个时间单位,这里延时是对时间的一个控制,有了延时之后这条语句才有真正的含义,延迟10个时间单位后sys_clk取一次反,再延迟10个时间单位后sys_clk再取一次反。一直执行下去就生成了一个周期为20个时间单位的无限延续的信号波形。
常用这种方法来描述时钟信号,并作为激励信号来测试做设计的电路。
(2)always 语句的沿触发和电平触发
always 的时间控制可以是沿触发,也可以是电平触发;可以是单个信号,也可以是多个信号,多个信号中间要用关键字 or 连接。
always 语句后紧跟的过程块是否运行,要看它的触发条件是否满足。
always 的沿触发:
always @ (posedge clock or negedge reset)
begin
......;
end
posedge表示上升沿,negedge表示下降沿。由两个沿触发的always只要其中一个沿出现,就立即执行一次过程块。
always 的电平触发:
always @ (a or b or c)
begin
......;
end
由多个电平触发的always块,只要a、b、c中任何一个发生变化,从高到低或从低到高都会执行一次过程块。
沿触发的 always 块常常描述时序逻辑行为。如有限状态机。
- 如果符合可综合风格要求,则可通过综合工具自动的将其转换为表示寄存器组和门级组合逻辑的结构,而该结构应具有时序所要求的行为;
电平触发的always块常常用来描述组合逻辑的行为。
- 如果符合可综合风格要求,可通过综合工具自动将其转换为表示组合逻辑的门级逻辑结构或带锁存器的组合逻辑结构,而该结构应具有所要求的行为;
一个模块中可以有多个always块,它们都是并行运行的。
- 如果这些always块是可综合的,则表示的是某种结构;
- 如果不可综合,则是电路结构的行为,且多个always块没有先后之分。
根据逻辑功能的不同特点,可以将数字电路分成两大类:组合逻辑电路 和 时序逻辑电路。
- 组合逻辑电路中,任意时刻的输出仅仅取决于该时刻的输入,与电路原来的状态无关。
- 时序逻辑电路中,任一时刻的输出不仅取决于当时的输入信号,而且还取决于电路原来的状态。或者说还与以前的输入有关,因此时序逻辑必须具备记忆功能。
(3)always块的or事件控制
需要多个信号或者事件中任意一个发生的变化都能够触发语句或语句块的执行时,在Verilog语言中,可以使用"或"表达式来表示这种情况。由关键词 or 连接的多个事件名或信号名组成的列表称为"敏感列表"。关键词"ro"被用来表示这种关系,或者使用","来代替。
如果组合逻辑块语句的输入变量很多,那么编写敏感列表会很烦琐并且容易出错。Verilog语言提供了特殊符号:@( * )
@( * )表示对后面语句块中所有输入变量的变化都是敏感的。
@( * )操作符使用示例:
always @(a or b or c or d or e or f or g or h or p or m)
begin
out1 = a ? (b + c) : (d + e);
out2 = f ? (g + h) : (p + m);
end
用符号@( * )来代替,可以把所有输入变量都自动包括进敏感列表。
always @( * )
begin
out1 = a ? (b + c) : (d + e);
out2 = f ? (g + h) : (p + m);
end
(4)电平敏感时序控制
前面所讨论的事件控制都需要等待信号值的变化或者事件的触发,使用符号@和后面的敏感列表来表示。
Verilog同时也允许使用另外一种形式表示的电平敏感时序控制(即后面的语句和语句块需要等待某个条件为真才能执行)。
Verilog语言用关键字wait来表示等待电平敏感的条件为真。
代码示例:
always
wait (count_enable) #20 count = count + 1;
代码示例中,仿真器连续监视count_enable的值,若其值为1,则在20个时间单位之后执行这条语句。如果count_enable始终为1,那么count将每过20个时间单位加1.
4.2 赋值语句
Verilog HDL 语言中,信号有两种赋值方式
1、阻塞赋值(blocking),如 b = a;
赋值语句执行完成后,块才结束。b的值在赋值语句执行完成后立刻就改变的在时序逻辑中使用时,可能会产生意想不到的结果。这是因为这种赋值方式是马上执行的,也就是说执行下一条语句时,b已等于a。这种方式直观但会引起麻烦。
2、非阻塞赋值(Non_Blocking),如 b <= a;
在语句块中,上面语句所赋的变量值不能立即就为下面的语句所用。块结束后才能完成这次赋值操作,而所赋的变量值是上一次赋值得到的。在编写可综合的时序逻辑块时,这是最常用的赋值方法,这种方式的赋值并不是马上执行的,也就是说,always块内的下一条语句执行后,b并不等于a,而是保持原来的值,always块结束后,才进行赋值。
为方便解释阻塞赋值和非阻塞赋值在这里定义两个缩写:
- RHS:赋值等号右边的表达式或变量可以写作 RHS 表达式或 RHS 变量;
- LHS:赋值等号左边的表达式或变量可以写作 LHS 表达式或 LHS 变量;
4.2.1 阻塞赋值(blocking)
阻塞赋值可以认为只有一个步骤的操作: 即计算 RHS 并更新 LHS 。
所谓阻塞的概念是指,在同一个always块中,后面的赋值语句是在前一句赋值语句结束后才开始赋值的。
阻塞赋值,顾名思义,即在一个 always 块中,后面的语句会受到前语句的影响,具体来说,在同一个always 中,一条阻塞赋值语句如果没有执行结束,那么该语句后面的语句就不能被执行,即被"阻塞"。也就是说 always 块内的语句是一种顺序关系,这里和 C 语言很类似。符号"="用于阻塞的赋值(如:b = a;),阻塞赋值"="在 begin 和 end 之间的语句是顺序执行,属于串行语句。
阻塞赋值的执行可以认为是只有一个步骤的操作,即计算 RHS 的值并更新 LHS,此时不允许任何其他语句的干扰,所谓的阻塞的概念就是值在同一个 always 块中,其后面的赋值语句从概念上来讲(即使不设定延时)是在前面一条赋值语句完成后再执行赋值的。
如果在一个过程块中阻塞赋值的RHS变量正好是另外一个过程块中阻塞赋值的LHS变量,这两个过程块又用同一个时钟沿触发,这时阻塞赋值操作会出现问题,即如果阻塞赋值的顺序安排不好,就会出现竞争。若这两个阻塞赋值操作用同一个时钟沿触发,则执行的顺序是无法确定的。
为了便于理解阻塞赋值的概念以及阻塞赋值和非阻塞赋值的区别,这里以在时序逻辑下使用阻塞赋值为例来实现这样一个功能:在复位的时候,a=1,b=2,c=3;而在没有复位的时候,a 的值清零,同时将 a 的值赋值给 b,b 的值赋值给 c,代码以及信号波形图如下图所示:
代码实例:
always @(posedge clk or negedge rst_n) begin
if (rst_n == 1'b0)begin
a = 1;
b = 2;
c = 3;
end
else begin
a = 0;
b = a;
c = b;
end
end
波形图:
![](https://i-blog.csdnimg.cn/blog_migrate/d2ce832526d447e6e9d2de208410e00b.png)
代码中使用的是阻塞赋值语句,从波形图中可以看到,在复位的时候(rst_n=0),a=1,b=2,c=3;而结束复位之后(波形图中的 0 时刻),当 clk 的上升沿到来时(波形图中的 2 时刻),a=0,b=0,c=0。这是因为阻塞赋值是在当前语句执行完成之后,才会执行后面的赋值语句,因此首先执行的是 a=0,赋值完成后将 a 的值赋值给 b,由于此时 a 的值已经为 0,所以 b=a=0,最后执行的是将 b 的值赋值给 c,而 b的值已经赋值为 0,所以 c 的值同样等于 0。
4.2.2 非阻塞赋值(Non-Blocking)
符号"<="用于非阻塞赋值(如:b <= a;),非阻塞赋值是由时钟节拍决定,在时钟上升到来时,执行赋值语句右边,然后将 begin-end 之间的所有赋值语句同时赋值到赋值语句的左边,注意:是 begin---end之间的所有语句,一起执行,且一个时钟只执行一次,属于并行执行语句。这个是和 C 语言最大的一个差异点。
非阻塞赋值的操作过程可以看作两个步骤:
- 赋值开始的时候,计算 RHS ;
- 赋值结束的时候,更新 LHS 。
所谓非阻塞的概念是指,在计算非阻塞赋值的RHS以及更新LHS期间,允许其他的非阻塞赋值语句同时计算RHS和更新LHS。
非阻塞赋值只能用于对寄存器类型的变量进行赋值,因此只能用在initial块和always块等过程块中。
下面使用非阻塞赋值同样来实现这样一个功能:在复位的时候,a=1,b=2,c=3;而在没有复位的时候,a 的值清零,同时将 a 的值赋值给 b,b 的值赋值给 c,代码以及信号波形图如下图所示:
代码示例:
always @(posedge clk or negedge rst_n) begin
if (rst_n == 1'b0)begin
a <= 1;
b <= 2;
c <= 3;
end
else begin
a <= 0;
b <= a;
c <= b;
end
end
波形图:
![](https://i-blog.csdnimg.cn/blog_migrate/596771c0dfbbe15336461f111584a04a.png)
代码中使用的是非阻塞赋值语句,从波形图中可以看到,在复位的时候(rst_n=0),a=1,b=2,c=3;而结束复位之后(波形图中的 0 时刻),当 clk 的上升沿到来时(波形图中的 2 时刻),a=0,b=1,c=2。这是因为非阻塞赋值在计算 RHS 和更新 LHS 期间,允许其它的非阻塞赋值语句同时计算 RHS 和更新 LHS。在波形图中的 2 时刻,RHS 的表达是 0、a、b,分别等于 0、1、2,这三条语句是同时更新LHS,所以 a、b、c 的值分别等于 0、1、2。
- 在描述组合逻辑的 always 块中用阻塞赋值 = ,综合成组合逻辑的电路结构;这种电路结构只与输入电平的变化有关系。
- 在描述时序逻辑的 always 块中用非阻塞赋值 <=,综合成时序逻辑的电路结构;这种电路结构往往与触发沿有关系,只有在触发沿时才可能发生赋值的变化。
注意:
- 在同一个always块中不要既用非阻塞赋值又用阻塞赋值
- 不允许在多个always块中对同一个变量进行赋值!
4.2.3 赋值语句的使用
什么时候使用阻塞赋值,什么时候使用非阻塞赋值的情况总结如下。
(1)赋值语句的选择
在描述组合逻辑电路的时候,使用阻塞赋值,比如 assign 赋值语句和不带时钟的 always 赋值语句,这种电路结构只与输入电平的变化有关系,代码如下:
代码示例1:assign赋值语句
assign data = (data_en == 1'b1) ? 8'd255 : 8'd0;
代码示例2:不带时钟的always语句
always @(*) begin
if (en) begin
a = a0;
b = b0;
end
else begin
a = a1;
b = b1;
end
end
在描述时序逻辑的时候,使用非阻塞赋值,综合成时序逻辑的电路结构,比如带时钟的 always 语句;
这种电路结构往往与触发沿有关系,只有在触发沿时才可能发生赋值的变化,代码如下:
代码示例 3:
always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n) begin
a <= 1'b0;
b <= 1'b0;
end
else begin
a <= c;
b <= d;
end
end
(2)assign 和 always 区别:
assign 语句使用时不能带时钟。
always 语句可以带时钟,也可以不带时钟。在 always 不带时钟时,逻辑功能和 assign 完全一致,都是只产生组合逻辑。比较简单的组合逻辑推荐使用 assign 语句,比较复杂的组合逻辑推荐使用 always 语句。
(3)带时钟和不带时钟的 always
always 语句可以带时钟,也可以不带时钟。在 always 不带时钟时,逻辑功能和 assign 完全一致,虽然产生的信号定义还是 reg 类型,但是该语句产生的还是组合逻辑。
代码示例:
reg [3:0] led;
always @(*) 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'b0000;
endcase
end
在 always 带时钟信号时,这个逻辑语句才能产生真正的寄存器,如下示例 counter 就是真正的寄存器。
用于产生 0.5 秒使能信号的计数器
代码示例:
always @(posedge sys_clk or negedge sys_rst_n) begin
if (sys_rst_n == 1'b0)
counter <= 1'b0;
else if (counter_en)
counter <= 1'b0;
else
counter <= counter + 1'b1;
end
阻塞与非阻塞赋值使用的8个要点:
- 时序电路建模时,用非阻塞赋值。
- 锁存器电路建模时,用非阻塞赋值。
- 用always块建立组合逻辑模型时,用阻塞赋值。
- 在同一个always块中建立时序和组合逻辑电路时,用非阻塞赋值。
- 在同一个always块中不要既用非阻塞赋值又用阻塞赋值。
- 不要在一个以上的always块中为同一个变量赋值。
- 用$strobe系统任务来显示用非阻塞赋值的变量值。
- 在赋值时不要使用#0延迟。
4.3 条件语句
条件语句必须在过程块中使用。过程块语句是指由initial和always语句引导的块语句。
4.3.1 if_elas语句
Verilog语言提供了3种形式的if语句:
(1)
if(a > b)
out = data_1;
(2)
if(a > b)
out = data_1;
else
out = data_2;
(3)
if(表达式1)
语句1;
else if(表达式2)
语句2;
else if(表达式3)
语句3;
else
语句4;
if语句使用注意事项:
1、允许一定形式的简写,如:
if(a) 等同于 if(a == 1)
if(!a)等同于 if(a != 1)
2、if语句对表达式的值进行判断,若为0,x,z,则按假处理;若为1,按真处理。
3、if和else后面的操作语句可以用begin和end包含多个语句。
4、允许if语句的嵌套。
代码示例:
if(a) begin
语句1;
语句2;
end
else begin
语句3;
if(!b)
语句4;
else
语句5;
end
4.3.2 case语句(多分支选择语句)
case语句和C语言中的switch_case语句相比有不同之处,写代码时需要注意其中的区别。
case语句的三种形式:
case(控制表达式) <case分支项> endcase
casez(控制表达式) <case分支项> endcase
casex(控制表达式) <case分支项> endcase
case语句语法格式:
case(控制表达式)
分支表达式1: 语句1;
分支表达式2: 语句2;
分支表达式3: 语句3;
......
分支表达式n: 语句n;
default: 语句;
endcase
运行时会判断控制表达式的变量是不是等于下面的分支表达式,当控制表达式等于其中一个分支表达式,就执行这个分支表达式后的语句。如果所有的分支表达式的值都没有与控制表达式的值相匹配,就执行default后面的语句。
一个case语句里只能有一个default项。
case语句使用注意事项:
1、分支表达式的值互不相同;
2、所有表达式的位宽必须相等;不能用 'bx 来代替 n'bx
3、casez比较时,不考虑表达式中的高阻值z
4、casex比较时,不考虑高阻值z 和 不定值x
reg [7:0] sel; //1100_0011
casez(sel)
8'b1100_zzzz: 语句1;
8'b1100_xxzz: 语句2;
endcase
示例中定义了一个reg型的8位变量,给变量注释中的值1100_0011。
用casez来做条件语句的话,不用比较高阻值,
- 语句 8'b1100_zzzz: 语句1; 就可以不考虑分支表达式后四位的高阻值z,比较时就不用比较后四位z,只要前四位相等就可以了。
- 语句 8'b1100_xxzz: 语句2; 只能不考虑分支表达式后两位的高阻值z,还有两位的不定值x就必须要考虑了,因为xx和00是不相等的,所以控制表达式和第二个表达式相比是不相等的,语句2也不会执行。
用casex来做条件语句的话,既不用比较高阻值也不用比较不定值。
- 语句 8'b1100_xxzz: 语句2; 就可以不考虑分支表达式后四位的高阻值z和不定值x,比较时就不用比较后四位z,只要前四位相等就可以了。
使用条件语句时注意:
如果用到if语句,最好写上else项;
如果用case语句,最好写上default项。
五、状态机
Verilog 是硬件描述语言,硬件电路是并行执行的,当需要按照流程或者步骤来完成某个功能时,代码中通常会使用很多个 if 嵌套语句来实现,这样就增加了代码的复杂度,以及降低了代码的可读性,这个时候就可以使用状态机来编写代码。状态机相当于一个控制器,它将一项功能的完成分解为若干步,每一步对应于二进制的一个状态,通过预先设计的顺序在各状态之间进行转换,状态转换的过程就是实现逻辑功能的过程。
5.1 状态机概念
状态机,全称是有限状态机(Finite State Machine,缩写为 FSM),是一种在有限个状态之间按一定规律转换的时序电路,可以认为是组合逻辑和时序逻辑的一种组合。
状态机通过控制各个状态的跳转来控制流程,使得整个代码看上去更加清晰易懂,在控制复杂流程的时候,状态机优势明显,因此基本上都会用到状态机,如 SDRAM 控制器等。
5.2 状态机模型
状态寄存器由一组触发器组成,用来记忆状态机当前所处的状态,状态的改变只发生在时钟的跳变沿。
![](https://i-blog.csdnimg.cn/blog_migrate/139d1a8fd259dd6e7648f0252dbeb738.png)
如果状态寄存器由n个触发器组成,这个状态机最多可以记忆2^n个状态。
所有的触发器的时钟端都连接在一个共同的时钟信号上,所以状态机的改变只可能发生在时钟的跳变沿上。
可能发生的状态的改变由正跳变还是由负跳变触发,取决于触发的类型。
状态是否改变、如何改变,取决于产生下一个状态的组合逻辑F的输出,F是当前状态和输入信号的函数。
状态机的输出是由输出组合逻辑G提供的,G也是当前状态和输入信号的函数。
F和G两部分都是纯组合逻辑,它们的逻辑函数表达式如下:
- 下一个状态 = F(当前状态,输入信号);
- 输出信号 = G(当前状态,输入信号);
根据状态机的输出是否与输入条件相关,可将状态机分为两大类,即摩尔(Moore)型状态机和米勒(Mealy)型状态机。
- Mealy 状态机:组合逻辑的输出不仅取决于当前状态,还取决于输入状态。
- Moore 状态机:组合逻辑的输出只取决于当前状态。
5.2.1 Mealy 状态机
米勒状态机的模型如下图所示,
![](https://i-blog.csdnimg.cn/blog_migrate/185a954ed87dafc0aa4c4acd3231c402.png)
模型中:
第一个方框是指产生下一状态的组合逻辑 F,F 是当前状态和输入信号的函数,状态是否改变、如何改变,取决于组合逻辑 F 的输出;
第二框图是指状态寄存器,其由一组触发器组成,用来记忆状态机当前所处的状态,状态的改变只发生在时钟的跳边沿;
第三个框图是指产生输出的组合逻辑 G,状态机的输出是由输出组合逻辑 G 提供的,G 也是当前状态和输入信号的函数。
5.2.2 Moore 状态机
摩尔状态机的模型如下图所示,对比米勒状态机的模型可以发现,其区别在于米勒状态机的输出由当前状态和输入条件决定的,而摩尔状态机的输出只取决于当前状态。
![](https://i-blog.csdnimg.cn/blog_migrate/74986044abcf75040706c5bf1b4ea969.png)
5.3 状态机设计
状态机的设计可参考四段论的流程来写出一个完整的状态机。
四段论:
- 状态空间定义
- 状态跳转
- 下个状态判断
- 各个状态下的动作
5.3.1 状态空间定义
代码示例:
//parameter define
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 define
reg [6:0] curr_st ; //当前状态
reg [6:0] next_st ; //下一个状态
用parameter关键词来定义一个参数,一个参数就代表一个特定的状态空间,参数名最好使用有实际意义的名字,这样可以提高代码的可读性。在参数定义的右边是二进制数,二进制数就相当于对不同状态的一个编码。
独热码 :每个状态只有一个寄存器位置位,译码逻辑简单。
对于用FPGA实现的有限状态机建议采用独热码,因为虽然独热码多用了几个触发器,但所用组合电路可省一些,因而使电路的速度和可靠性有显著提高,而总单元数并无显著增加。
采用独热码后有了多余的状态,就有一些不可到达的状态。为此,在case语句的最后需要增加default分支项,一般综合器都可以通过综合指令的控制来合理地处理默认项。
reg define这里定义的是两个状态寄存器,状态寄存器里用来存储状态空间的编码,因此状态寄存器的位宽需要和状态空间定义的位宽保持一致。
总结:状态空间定义需要定义状态空间参数和状态寄存器。
5.3.2 状态跳转
代码示例:
//状态机的第一段采用同步时序描述状态转移
always @(posedge sys_clk or negedge sys_rst_n) begin//敏感列表:时钟信号以及复位信号边沿的组合
if (!sys_rst_n)
curr_st <= S0;//使用非阻塞赋值
else
curr_st <= next_st;//使用非阻塞赋值
end
时序逻辑里使用非阻塞赋值;组合逻辑里使用阻塞赋值。
5.3.3 下个状态判断
代码示例:
//状态机的第二段采用组合逻辑判断状态转移条件
always @(*) begin//敏感信号表:所有的右边表达式中的变量以及if、 case条件中的变量
case (curr_st)
S0: next_st = S1;
S1: next_st = S2;
S2: next_st = S3;
S3: next_st = S4;
S4: next_st = S5;
S5: next_st = S6;
S6: next_st = S0;
default: next_st = S0;
endcase
end
5.3.4 各个状态下的动作
代码示例:
//状态机的第三段描述状态输出(这里采用时序电路输出)
always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n)
clk_divide_7 <= 1'b0;
else if ((curr_st == S0) | (curr_st == S1) | (curr_st == S2) | (curr_st == S3))
clk_divide_7 <= 1'b0;
else if ((curr_st == S4) | (curr_st == S5) | (curr_st == S6))
clk_divide_7 <= 1'b1;
else
;
end
5.3.5 三段式状态机
用四段论的方法写出的状态机叫做三段式状态机。
状态跳转图:
![](https://i-blog.csdnimg.cn/blog_migrate/0ba736f4aafcfff0b82af347e299f9fc.png)
总代码示例:
// 7 分频
module divider7_fsm (
input sys_clk ,//系统时钟
input sys_rst_n ,//复位
output reg clk_divide_7 //输出时钟
);
//状态空间定义
//parameter define
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 define
reg [6:0] curr_st ; //当前状态
reg [6:0] next_st ; //下一个状态
//状态机的第一段采用同步时序描述状态转移
always @(posedge sys_clk or negedge sys_rst_n) begin//敏感列表:时钟信号以及复位信号边沿的组合
if (!sys_rst_n)
curr_st <= S0;//使用非阻塞赋值
else
curr_st <= next_st;//使用非阻塞赋值
end
//状态机的第二段采用组合逻辑判断状态转移条件
always @(*) begin//敏感信号表:所有的右边表达式中的变量以及if、 case条件中的变量
case (curr_st)
S0: next_st = S1;
S1: next_st = S2;
S2: next_st = S3;
S3: next_st = S4;
S4: next_st = S5;
S5: next_st = S6;
S6: next_st = S0;
default: next_st = S0;
endcase
end
//状态机的第三段描述状态输出(这里采用时序电路输出)
always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n)
clk_divide_7 <= 1'b0;
else if ((curr_st == S0) | (curr_st == S1) | (curr_st == S2) | (curr_st == S3))
clk_divide_7 <= 1'b0;
else if ((curr_st == S4) | (curr_st == S5) | (curr_st == S6))
clk_divide_7 <= 1'b1;
else
;
end
endmodule
三段式可以在组合逻辑后再增加一级寄存器来实现时序逻辑输出:
1、可以有效地滤去组合逻辑输出的毛刺;
2、可以有效地进行时序计算与约束;
3、另外对于总线形式的输出信号来说,容易使总线数据对齐,从而减小总线数据间的偏移,减小接收端数据采样出错的频率。
总线形式的输出信号指输出信号的位宽大于1.如上述例子中输出信号clk_divide_7只有1位的位宽,就不算总线形式的输出信号;若位宽为8位就是总线形式的输出信号。
其他方法设计的状态机:
module fsm(
Clock,
Reset,
state,
A,
K1,
K2
);
input Clock ;
input Reset ;
input A ;
output reg K1 ;
output reg K2 ;
output reg [1:0] state ;
parameter Idle = 2'b00;
parameter Start = 2'b01;
parameter Stop = 2'b10;
parameter Clear = 2'b11;
/*
使用独热码定义状态空间
parameter Idle = 4'b1000;
parameter Start = 4'b0100;
parameter Stop = 4'b0010;
parameter Clear = 4'b0001;
reg [3:0] state ;//使用独热码定义状态空间时需注意状态寄存器的位宽与独热码定义状态空间的位宽保持一致,上方代码中reg [1:0] state 需要定义为reg [3:0] state
*/
always @(posedge Clock)
if(!Reset) begin
state<=Idle;
K2<=0;
K1<=0;
end
else begin
case(state)
Idle:
begin
if(A)begin
state<=Start;
K1<=0;
end
else begin
state<=Idle;
K2<=0;
K1<=0;
end
end
Start:
begin
if(!A)
state<=Stop;
else
state<=Start;
end
Stop:
begin
if(A)begin
state<=Clear;
K2<=1;
end
else begin
state<=Stop;
K2<=0;
K1<=0;
end
end
Clear:
begin
if(A)begin
state<=Idle;
K2<=0;
K1<=0;
end
else begin
state<=Clear;
K2<=0;
K1<=0;
end
end
default:
state<=2'bxx;
endcase
end
endmodule
总结
以上就是今天要讲的内容,本文仅仅简单介绍了Verilog语言的基础语法。