资讯详情

【正点原子FPGA连载】第七章 Verilog HDL语 -摘自【正点原子】领航者ZYNQ之FPGA开发指南_V2.0

1)实验平台:正点原子领导者ZYNQ开发板 2)平台采购地址:https://item.taobao.com/item.htm?&id=606160108761 3)全套实验源码 手册 视频下载地址:http://www.openedv.com/thread-301505-1-1.html 4)正点原子FPGA感兴趣的同学可以加群讨论:99424016 5)关注正点原子微信官方账号,获取最新信息更新 在这里插入图片描述

第七章 Verilog HDL语法

Verilog HDL(Hardware Description Language)硬件描述语言是在使用最广泛的C语言的基础上发展起来的,具有灵活性高、易学易用等特点。Verilog HDL能在短时间内学习和掌握,已经在了FPGA开发/IC设计领域占据绝对领先地位。 本章包括以下几个部分: 1.1 Verilog概述 1.2 Verilog基础知识 1.3 Verilog程序框架 1.4 Verilog高级知识点 1.5 Verilog编程规范

1.1 Verilog概述

本节主要描述Verilog HDL(以下简称Verilog)简介、Verilog和VHDL和C语言的区别。 1.1.1 Verilog简介 Verilog它是一种以文本形式描述数字系统硬件结构和行为的硬件描述语言。它可以表示逻辑电路图、逻辑表达式和数字逻辑系统完成的逻辑功能。 使用这种语言,数字电路设计师可以从顶层到底层逐层描述自己的设计理念,并使用一系列分层模块来表示极其复杂的数字系统。然后利用电子设计自动化(EDA)工具,逐层模拟验证,然后将需要转换为实际电路的模块组合,通过自动综合工具转换为门级电路网表。接下来,再用专用集成电路ASIC或FPGA布线工具的自动布局将网表转换为要实现的具体电路结构。 Verilog语言最初是在1983年开始的Gateway Design Automation公司为其模拟器产品开发的硬件建模语言。由于其模拟和仿真产品的广泛应用,Verilog HDL作为一种易于使用和实用的语言,它逐渐被许多设计师所接受。在努力增加语言普及的活动中,Verilog HDL1990年,语言被推向公众领域。Verilog语言于1995年成为IEEE标准,称为IEEE Std1364-1995,俗称Verilog-95。 使用设计师Verilog-在95的过程中发现了一些改进。为了解决用户使用这个版本的问题Verilog过程中反映的问题,Verilog经过修改和扩展,扩展版后来成为电气电子工程师学会Std1364-2001标准,俗称Verilog-2001。Verilog-2001是对Verilog-95的主要改进版本具有敏感列表、多维数组、生成句块、命名端口连接等一些新的实用功能。目前,Verilog-2001是Verilog大多数商业电子设计自动化软件都支持最主流的版本。 1.1.2 为什么需要Verilog 在FPGA在设计中,我们有多种设计方法,如设计原理图、编写描述语言(代码)等。起初,许多工程师喜欢原理图的设计方法。这种输入方法可以直观地看到电路结构并快速理解。然而,随着电路设计规模的不断扩大,逻辑电路设计变得越来越复杂。这种设计方法越来越不能满足项目的实际需要。这个时候Verilog语言被取代了目前Verilog已经在FPGA开发/IC设计领域占据绝对领先地位。 1.1.3 Verilog和VHDL区别 这两种语言都是用于数字电路系统设计的硬件描述语言,都是IEEE的标准。 VHDL 1987年成为标准,Verilog直到1995年才成为标准。这是因为VHDL是美国军方组织开发的,而Verilog它是由一家公司的私有财产转化而来的。为什么Verilog能成为IEEE标准呢?它必须有其独特的优势,所以Verilog生命力更强。 两者有其共同的特点:

  1. 抽象表示电路的行为和结构;
  2. 支持逻辑设计的层次和范围描述;
  3. 简化电路行为和结构可以采用高级语言的精细结构;
  4. 支持电路描述由高到低的综合转换;
  5. 硬件描述与实现过程无关。 但两者也各有特点。Verilog已经推出20年了,设计群体广泛,资源成熟,资源成熟Verilog很容易掌握,只要有C语言的编程基础,通过相对较短的时间,经过一些实际操作,你就可以在大约一个月内掌握这种语言。而VHDL设计相对要难一点,这个是因为VHDL不太直观,一般认为掌握专业培训至少需要半年。 近10年来,EDA界一直在争论数字逻辑设计中使用哪种硬件描述语言。目前,在美国和高层次数字系统设计领域的应用Verilog和VHDL比例为80%和20%;日本与中国台湾和美国相似;在欧洲VHDL良好的发展;我国许多集成电路设计公司都采用了Verilog。我们推荐你学习Verilog,本教程的所有例程都使用Verilog开发的。 1.1.4 Verilog和C的区别 Verilog在编译和下载中是硬件描述语言FPGA之后,产生电路,所以Verilog所有并行处理和运行;C语言是软件语言,编译下载到单片机/CPU之后,它仍然是软件指令,不会根据您的代码生成相应的硬件电路,而是单片机/CPU处理软件指令需要取址、翻译、执行,并串行执行。 Verilog和C的区别也是FPGA和单片机/CPU因为FPGA所有并行处理,所以处理速度很快,这是FPGA单片机//CPU无法替代的。 1.2 Verilog基础知识 本节主要讲解Verilog基础知识,包括五节,下面我们分别介绍五节的内容。 1.2.1 Verilog的逻辑值 逻辑电路中有四种值,即四种状态: 逻辑 0:表示低电平,即对应我们的电路GND; 逻辑 1:表示高电平,即对应我们的电路VCC; 逻辑 X:表示未知,可能是高电平或低电平; 逻辑 Z:表示高阻态,外部无激励信号为悬挂状态。 如下图所示:

图 7.2.1.1 Verilog逻辑值 1.2.2 Verilog的标识符 定义 标识符(identifier)用于定义模块名、端口名和信号名等。Verilog任何一组字母、数字、 和 ( 下 划 线 ) 符 号 的 组 合 , 但 标 识 符 的 第 一 个 字 符 必 须 是 字 母 或 者 下 划 线 。 另 外 , 标 识 符 是 区 分 大 小 写 的 。 以 下 是 标 识 符 的 几 个 例 子 : C o u n t C O U N T / / 与 C o u n t 不 同 。 R 5 6 6 8 F I V E 和_(下划线)符号的组合,但标识符的第一个字符必须是字母或下划线。另外,标识符是区分大小写的。以下是标识符的几个例子: Count COUNT //与Count不同。 R56_68 FIVE 和(下划线)符号的组合,但标识符的第一个字符必须是字母或者下划线。另外,标识符是区分大小写的。以下是标识符的几个例子:CountCOUNT//与Count不同。R566​8FIVE 虽然标识符写法很多,但是要简洁、清晰、易懂,推荐写法如下: count fifo_wr 不建议大小写混合使用,普通内部信号建议全部小写,参数定义建议大写,另外信号命名最好体现信号的含义。 规范建议 以下是一些书写规范的要求: 1、用有意义的有效的名字如sum、cpu_addr等。 2、用下划线区分词语组合,如cpu_addr。 3、采用一些前缀或后缀,比如:时钟采用clk前缀:clk_50m,clk_cpu;低电平采用_n后缀:enable_n; 4、统一缩写,如全局复位信号rst。 5、同一信号在不同层次保持一致性,如同一时钟信号必须在各模块保持一致。 6、自定义的标识符不能与保留字(关键词)同名。 7、参数统一采用大写,如定义参数使用SIZE。 1.2.3 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。 1.2.4 Verilog的数据类型 在Verilog语法中,主要有三大类数据类型,即寄存器类型、线网类型和参数类型。从名称中,我们可以看出,真正在数字电路中起作用的数据类型应该是寄存器类型和线网类型。

  1. 寄存器类型 寄存器类型表示一个抽象的数据存储单元,它只能在always语句和initial语句中被赋值,并且它的值从一个赋值到另一个赋值过程中被保存下来。如果该过程语句描述的是时序逻辑,即always语句带有时钟信号,则该寄存器变量对应为寄存器;如果该过程语句描述的是组合逻辑,即always语句不带有时钟信号,则该寄存器变量对应为硬件连线;寄存器类型的缺省值是x(未知状态)。 寄存器数据类型有很多种,如reg、integer、real等,其中最常用的就是reg类型,它的使用方法如下:
//reg define
reg  [31:0]  delay_cnt;    //延时计数器
reg          key_flag ;    //按键标志
  1. 线网类型 线网表示Verilog结构化元件间的物理连线。它的值由驱动元件的值决定,例如连续赋值或门的输出。如果没有驱动元件连接到线网,线网的缺省值为z(高阻态)。线网类型同寄存器类型一样也是有很多种,如tri和wire等,其中最常用的就是wire类型,它的使用方法如下: //wire define wire data_en; //数据使能信号 wire [7:0] data ; //数据
  2. 参数类型 我们再来看下参数类型,参数其实就是一个常量,常被用于定义状态机的状态、数据位宽和延迟大小等,由于它可以在编译时修改参数的值,因此它又常被用于一些参数可调的模块中,使用户在实例化模块时,可以根据需要配置参数。在定义参数时,我们可以一次定义多个参数,参数与参数之间需要用逗号隔开。这里我们需要注意的是参数的定义是局部的,只在当前模块中有效。它的使用方法如下: //parameter define parameter DATA_WIDTH = 8; //数据位宽为8位 1.2.5 Verilog的运算符 大家看完了Verilog的数据类型,我们再来介绍下Verilog的运算符。Verilog中的运算符按照功能可以分为下述类型:1、算术运算符、 2、关系运算符、3、逻辑运算符、 4、条件运算符、 5、位运算符、 6、移位运算符、 7、拼接运算符。下面我们分别对这些运算符进行介绍。
  3. 算术运算符 算术运算符,简单来说,就是数学运算里面的加减乘除,数字逻辑处理有时候也需要进行数字运算,所以需要算术运算符。常用的算术运算符主要包括加减乘除和模除(模除运算也叫取余运算)如下表所示: 表 7.2.1 算术运算符 符号 使用方法 说明
  • a + b a 加上 b
  • a - b a 减去 b
  • a * b a 乘以 b / a / b a 除以 b % a % b a 模除 b 大家要注意下,Verilog实现乘除比较浪费组合逻辑资源,尤其是除法。一般2的指数次幂的乘除法使用移位运算来完成运算,详情可以看移位运算符章节。非2的指数次幂的乘除法一般是调用现成的IP,QUARTUS/ISE等工具软件会有提供,不过这些工具软件提供的IP也是由最底层的组合逻辑(与或非门等)搭建而成的。
  1. 关系运算符 关系运算符主要是用来做一些条件判断用的,在进行关系运算符时,如果声明的关系是假的,则返回值是0,如果声明的关系是真的,则返回值是1;所有的关系运算符有着相同的优先级别,关系运算符的优先级别低于算术运算符的优先级别如下表所示。 表 7.2.2 关系运算符 符号 使用方法 说明

a > b a 大于 b < a < b a 小于 b = a >= b a 大于等于 b <= a <= b a 小于等于 b == a == b a 等于 b != a != b a 不等于 b

  1. 逻辑运算符 逻辑运算符是连接多个关系表达式用的,可实现更加复杂的判断,一般不单独使用,都需要配合具体语句来实现完整的意思,如下表所示。 表 7.2.3 逻辑运算符 符号 使用方法 说明 ! !a a的非,如果a为0,那么a的非是1。 && a && b a 与上 b,如果a和b都为1,a&&b结果才为1,表示真。 || a || b a 或上 b,如果a或者b有一个为1,a||b结果为1,表示真。
  2. 条件运算符 条件操作符一般来构建从两个输入中选择一个作为输出的条件选择结构,功能等同于 always中的if-else语句,如下表所示。 表 7.2.4 条件运算符 符号 使用方法 说明 ? : a ? b : c 如果 a 为真,就选择 b,否则选择 c
  3. 位运算符 位运算符是一类最基本的运算符,可以认为它们直接对应数字逻辑中的与、或、非门等逻辑门。常用的位运算符如下表所示。 表 7.2.5 位运算符 符号 使用方法 说明 ~ ~a 将 a 的每个位进行取反 & a & b 将 a 的每个位与 b 相同的位进行相与 | a | b 将 a 的每个位与 b 相同的位进行相或 ^ a ^ b 将 a 的每个位与 b 相同的位进行异或 位运算符的与、或、非与逻辑运算符逻辑与、逻辑或、逻辑非使用时候容易混淆,逻辑运算符一般用在条件判断上,位运算符一般用在信号赋值上。
  4. 移位运算符 移位运算符包括左移位运算符和右移位运算符,这两种移位运算符都用0来填补移出的空位。如下表所示。 表 7.2.6 移位运算符 符号 使用方法 说明 << a << b 将 a 左移 b 位

a >> b 将 a 右移 b 位 假设a有8bit数据位宽,那么a<<2,表示a左移2bit,a还是8bit数据位宽,a的最高2bit数据被移位丢弃了,最低2bit数据固定补0。如果a是3(二进制:00000011),那么3左移2bit,3<<2,就是12(二进制:00001100)。一般使用左移位运算代替乘法,右移位运算代替除法,但是这种也只能表示2的指数次幂的乘除法。

  1. 拼接运算符 Verilog中有一个特殊的运算符是C语言中没有的,就是位拼接运算符。用这个运算符可以把两个或多个信号的某些位拼接起来进行运算操作。如下表所示。 表 7.2.7 位拼接运算符 符号 使用方法 说明 {} {a,b} 将 a 和 b 拼接起来,作为一个新信号
  2. 运算符的优先级 介绍完了这么多运算符,大家可能会想到究竟哪个运算符高,哪个运算符低。为了便于大家查看这些运算符的优先级,我们将它们制作成了表格,如下表所示。 表 7.2.8 运算符的优先级 运算符 优先级 !、 ~ 最高 *、 /、 % 次高 +、 -

<<、 >> <、 <=、 >、 >= ==、 !=、 =、 ! & ^、 ^~ | && || 次低 ? 最低 1.3 Verilog程序框架 在介绍Verilog程序框架之前,我们先来看下Verilog一些基本语法,基础语法主要包括注释和关键字。 1.3.1 注释 Verilog HDL中有两种注释的方式,一种是以“/”符号开始,“/”结束,在两个符号之间的语句都是注释语句,因此可扩展到多行。如: /* statement1 , statement2, … statementn */ 以上n个语句都是注释语句。 另一种是以//开头的语句,它表示以//开始到本行结束都属于注释语句。如: //statement1 我们建议的写法:使用//作为注释。 1.3.2 关键字 Verilog和C语言类似,都因编写需要定义了一系列保留字,叫做关键字(或关键词)。这些保留字是识别语法的关键。我们给大家列出了Verilog中的关键字,如下表所示。 表 7.3.1 Verilog的所有关键字

and	always	assign	begin	buf
bufif0	bufif1	case	casex	casez
cmos	deassign	default	defparam	disable
edge	else	end	endcase	endfunction
endprimitive	endmodule	endspecify	endtable	endtask
event	for	force	forever	fork
function	highz0	highz1	if	ifnone
initial	inout	input	integer	join
large	macromodule	medium	module	nand
negedge	nor	not	notif0	notif1
nmos	or	output	parameter	pmos
posedge	primitive	pulldown	pullup	pull0
pull1	rcmos	real	realtime	reg
release	repeat	rnmos	rpmos	rtran
rtranif0	rtranif1	scalared	small	specify
specparam	strength	label0	label1	supply0
supply1	table	task	tran	tranif0
tranif1	time	tri	triand	trior
trireg	tri0	tri1	vectored	wait
wand	weak0	weak1	while	wire
wor	xnor	xor		

虽然上表列了很多,但是实际经常使用的不是很多,实际经常使用的主要如下表所示。 表 7.3.2 Verilog常用的关键字 关键字 含义 module 模块开始定义 input 输入端口定义 output 输出端口定义 inout 双向端口定义 parameter 信号的参数定义 wire wire信号定义 reg reg信号定义 always 产生reg信号语句的关键字 assign 产生wire信号语句的关键字 begin 语句的起始标志 end 语句的结束标志 posedge/negedge 时序电路的标志 case Case语句起始标记 default Case语句的默认分支标志 endcase Case语句结束标记 if if/else语句标记 else if/else语句标记 for for语句标记 endmodule 模块结束定义 注意只有小写的关键字才是保留字。例如,标识符always(这是个关键词)与标识符ALWAYS(非关键词)是不同的。 1.3.3 程序框架 我们以LED灯闪烁程序为例来给大家展示Verilog的程序框架,代码如下所示(注意:代码中前面的行号只是为了方便大家阅读代码与快速定位到行号的位置,在实际编写代码时不可以添加行号,否则编译代码时会报错)。

1  module led(
2      input               sys_clk  ,  //系统时钟
3      input               sys_rst_n,  //系统复位,低电平有效
4      output  reg  [3:0]  led         //4位LED灯
5      );
6  
7  //parameter define
8  parameter  WIDTH     = 25        ;
9  parameter  COUNT_MAX = 25_000_000;  //板载50M时钟=20ns,0.5s/20ns=25000000,需要25bit
10                                     //位宽
11 
12 //reg define
13 reg    [WIDTH-1:0]  counter     ;
14 reg    [1:0]        led_ctrl_cnt;
15 
16 //wire define
17 wire                counter_en  ;
18 
19 //***********************************************************************************
20 //** main code
21 //***********************************************************************************
22 
23 //计数到最大值时产生高电平使能信号
24 assign  counter_en = (counter == (COUNT_MAX - 1'b1)) ? 1'b1  :  1'b0;  
25                                                                                                                                                                                                                          
26 //用于产生0.5秒使能信号的计数器
27 always @(posedge sys_clk or negedge sys_rst_n) begin
28     if (sys_rst_n == 1'b0)
29         counter <= 1'b0;
30     else if (counter_en)
31         counter <= 1'b0;
32     else
33         counter <= counter + 1'b1;
34 end
35 
36 //led流水控制计数器
37 always @(posedge sys_clk or negedge sys_rst_n) begin
38     if (sys_rst_n == 1'b0)
39         led_ctrl_cnt <= 2'b0;
40     else if (counter_en)
41         led_ctrl_cnt <= led_ctrl_cnt + 2'b1;
42 end
43 
44 //通过控制IO口的高低电平实现发光二极管的亮灭
45 always @(posedge sys_clk or negedge sys_rst_n) begin
46     if (sys_rst_n == 1'b0)
47         led <= 4'b0;
48     else begin
49         case (led_ctrl_cnt)                 
50             2'd0 : led <= 4'b0001;
51             2'd1 : led <= 4'b0010;
52             2'd2 : led <= 4'b0100;
53             2'd3 : led <= 4'b1000;
54             default : ;
55         endcase
56     end
57 end
58 
59 endmodule 

首先//开头的都是注释,这个之前我们讲解过了。下面我们来看下具体的解释。 第1行为模块定义,模块定义以module开始,endmodule结束,如59行所示。 其次2到5行为端口定义,需要定义led模块的输入信号和输出信号,此处输入信号为系统时钟和复位信号,输出为led控制信号。 7到9行为参数parameter定义,语法如7到9行所示,定义parameter的好处是可以灵活改变参数数字就能控制一些计数器最大计数值或者信号位宽的最大位宽。 12到14行为reg信号定义,reg信号一般情况下代表寄存器,比如此处控制0.5秒使能信号的计数器counter。 16到17行为wire信号定义,wire信号就是硬件连线,比如此处的counter_en,代表计数到最大值时产生高电平使能,本质上是一个硬件连线,其实代表的是一些计数器/寄存器做逻辑判断的结果。 19到21行为moudle开始的注释,不添加工具综合也不会报错,但是我们推荐添加,作为一个良好的编程规范。 23到24行为assign语句的样式,条件成立选择1,否则选择0。 26到34行是always语句的样式,27行代表在时钟上升沿或者复位的下降沿进行信号触发。begin/end代表语句的开始和结束。28到33行为if/else语句,和C语言是比较类似的。29行的“<=”标记代表信号是非阻塞赋值,信号赋值有非阻塞赋值和阻塞赋值两个方式,这个我们后面会详细解释。 36和42行也是一个always语句,和26到34行类似。 44和57行也是一个always语句,不过这个always语句中嵌入了一个case语句,case语句的语法如49到55行所示,需要一个case关键字开始,endcase关键字结束,default作为默认分支,和C语言也是类似的。当然case语句也可以用在不带时钟的always语句中,不过本例子的always都是带有时钟的。不带时钟的always和带时钟的always语句的差异这个我们后面也会详细解释。 59行是endmodule标记,代表模块的结束。 在这里需要补充一点的是,一些初学者可能会有这样一个疑问,在always语句中编写if语句或else语句时,后面需要加begin和end吗?其实这个主要看if条件后面跟着几条赋值语句,如果只有一条赋值语句时,if后面可以加begin和end,也可以不加;如果超过一条赋值语句时,就必须加上begin和end。 if条件只有一条赋值语句时,下面两种写法都是可以的,这里更推荐第一种写法,因为第二种写法会占用更多的行号,代码如下所示:

if(en == 1'b1)
	a <= 1'b1;
或者
if(en == 1'b1) begin
	a <= 1'b1;
end
对于if条件超过一条赋值语句的情况,必须添加begin和end,代码如下所示:
if(en == 1'b1) begin
	b <= 1'b1;
	c <= 1'b1;
end

好了,程序框架就讲解完了,大家是不是觉得也很简单呢?这些都是基本的语法规范,希望大家能记住这些基础的知识点。如果有些地方大家还是觉得比较抽象,很难理解,没有关系,相信大家会在后面的学习中,会慢慢理解的。 1.4 Verilog高级知识点 前几节主要介绍了Verilog一些基础的知识点和程序框架,本节给大家介绍一些高级的知识点。高级知识点包括阻塞赋值和非阻塞赋值、assign和always语句差异、什么是存器、状态机、模块化设计等。 1.4.1 阻塞赋值(Blocking) 阻塞赋值,顾名思义,即在一个always块中,后面的语句会受到前语句的影响,具体来说,在同一个always中,一条阻塞赋值语句如果没有执行结束,那么该语句后面的语句就不能被执行,即被“阻塞”。也就是说always块内的语句是一种顺序关系,这里和C语言很类似。符号“=”用于阻塞的赋值(如:b = a;),阻塞赋值“=”在begin和end之间的语句是顺序执行,属于串行语句。 在这里定义两个缩写: RHS:赋值等号右边的表达式或变量可以写作RHS表达式或RHS变量; LHS:赋值等号左边的表达式或变量可以写作LHS表达式或LHS变量; 阻塞赋值的执行可以认为是只有一个步骤的操作,即计算RHS的值并更新LHS,此时不允许任何其他语句的干扰,所谓的阻塞的概念就是值在同一个always块中,其后面的赋值语句从概念上来讲是在前面一条语句赋值完成后才执行的。 为了方便大家理解阻塞赋值的概念以及阻塞赋值和非阻塞赋值的区别,我们这里以在时序逻辑下使用阻塞赋值为例来实现这样一个功能:在复位的时候,a=1,b=2,c=3;而在没有复位的时候,a的值清零,同时将a的值赋值给b,b的值赋值给c,代码以及信号波形图如下图所示:

图 7.4.1.1 阻塞赋值代码

图 7.4.1.2 阻塞赋值的信号波形图 代码中使用的是阻塞赋值语句,从波形图中可以看到,在复位的时候(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。 1.4.2 非阻塞赋值(Non-Blocking) 符号“<=”用于非阻塞赋值(如:b <= a;),非阻塞赋值是由时钟节拍决定,在时钟上升到来时,执行赋值语句右边,然后将begin-end之间的所有赋值语句同时赋值到赋值语句的左边,注意:是begin—end之间的所有语句,一起执行,且一个时钟只执行一次,属于并行执行语句。这个是和C语言最大的一个差异点,大家要逐步理解并行执行的概念。 非阻塞赋值的操作过程可以看作两个步骤: (1)赋值开始的时候,计算RHS; (2)赋值结束的时候,更新LHS。 所谓的非阻塞的概念是指,在计算非阻塞赋值的RHS以及LHS期间,允许其它的非阻塞赋值语句同时计算RHS和更新LHS。 我们下面使用非阻塞赋值同样来实现这样一个功能:在复位的时候,a=1,b=2,c=3;而在没有复位的时候,a的值清零,同时将a的值赋值给b,b的值赋值给c,代码以及信号波形图如下图所示:

图 7.4.2.1 非阻塞赋值代码

图 7.4.2.2 非阻塞赋值的信号波形图 代码中使用的是非阻塞赋值语句,从波形图中可以看到,在复位的时候(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。 在了解了阻塞赋值和非阻塞赋值的区别之后,有些朋友可能还是对什么时候使用阻塞赋值,什么时候使用非阻塞赋值有些疑惑,在这里给大家总结如下。 在描述组合逻辑电路的时候,使用阻塞赋值,比如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

1.4.3 assign和always区别 assign语句和always语句是Verilog中的两个基本语句,这两个都是经常使用的语句。 assign语句使用时不能带时钟。 always语句可以带时钟,也可以不带时钟。在always不带时钟时,逻辑功能和assign完全一致,都是只产生组合逻辑。比较简单的组合逻辑推荐使用assign语句,比较复杂的组合逻辑推荐使用always语句。示例如下: 24 assign counter_en = (counter == (COUNT_MAX - 1’b1)) ? 1’b1 : 1’b0;

45  always @(*) begin
49          case (led_ctrl_cnt)                 
50              2'd0 : led = 4'b0001;
51              2'd1 : led = 4'b0010;
52              2'd2 : led = 4'b0100;
53              2'd3 : led = 4'b1000;
54              default : led = 4'b0000;
55          endcase
57  end

1.4.4 带时钟和不带时钟的always always语句可以带时钟,也可以不带时钟。在always不带时钟时,逻辑功能和assign完全一致,虽然产生的信号定义还是reg类型,但是该语句产生的还是组合逻辑。

44  reg   [3:0] led;
45  always @(*) begin
49          case (led_ctrl_cnt)                 
50              2'd0 : led = 4'b0001;
51              2'd1 : led = 4'b0010;
52              2'd2 : led = 4'b0100;
53              2'd3 : led = 4'b1000;
54              default : led = 4'b0000;
55          endcase
57  end

在always带时钟信号时,这个逻辑语句才能产生真正的寄存器,如下示例counter就是真正的寄存器。

26  //用于产生0.5秒使能信号的计数器
27  always @(posedge sys_clk or negedge sys_rst_n) begin
28      if (sys_rst_n == 1'b0)
29          counter <= 1'b0;
30      else if (counter_en)
31          counter <= 1'b0;
32      else
33          counter <= counter + 1'b1;
34  end

1.4.5 什么是latch latch是指锁存器,是一种对脉冲电平敏感的存储单元电路。锁存器和寄存器都是基本存储单元,锁存器是电平触发的存储器,寄存器是边沿触发的存储器。两者的基本功能是一样的,都可以存储数据。锁存器是组合逻辑产生的,而寄存器是在时序电路中使用,由时钟触发产生的。 latch的主要危害是会产生毛刺(glitch),这种毛刺对下一级电路是很危险的。并且其隐蔽性很强,不易查出。因此,在设计中,应尽量避免latch的使用。 代码里面出现latch的两个原因是在组合逻辑中,if或者case语句不完整的描述,比如if缺少else分支,case缺少default分支,导致代码在综合过程中出现了latch。解决办法就是if必须带else分支,case必须带default分支。 大家需要注意下,只有不带时钟的always语句if或者case语句不完整才会产生latch,带时钟的语句if或者case语句不完整描述不会产生latch。 下面为缺少else分支的带时钟的always语句和不带时钟的always语句,通过实际产生的电路图可以看到第二个是有一个latch的,第一个仍然是普通的带有时钟的寄存器。

图 7.4.5.1 缺少else的带时钟的always语句电路图

图 7.4.5.2 缺少else的不带时钟的always语句电路图 1.4.6 状态机 Verilog是硬件描述语言,硬件电路是并行执行的,当需要按照流程或者步骤来完成某个功能时,代码中通常会使用很多个if嵌套语句来实现,这样就增加了代码的复杂度,以及降低了代码的可读性,这个时候就可以使用状态机来编写代码。状态机相当于一个控制器,它将一项功能的完成分解为若干步,每一步对应于二进制的一个状态,通过预先设计的顺序在各状态之间进行转换,状态转换的过程就是实现逻辑功能的过程。 状态机,全称是有限状态机(Finite State Machine,缩写为FSM),是一种在有限个状态之间按一定规律转换的时序电路,可以认为是组合逻辑和时序逻辑的一种组合。状态机通过控制各个状态的跳转来控制流程,使得整个代码看上去更加清晰易懂,在控制复杂流程的时候,状态机优势明显,因此基本上都会用到状态机,如SDRAM控制器等。在本手册提供的例程中,会有多个用到状态机设计的例子,希望大家能够慢慢体会和理解,并且能够熟练掌握。 根据状态机的输出是否与输入条件相关,可将状态机分为两大类,即摩尔(Moore)型状态机和米勒(Mealy)型状态机。  Mealy状态机:组合逻辑的输出不仅取决于当前状态,还取决于输入状态。  Moore状态机:组合逻辑的输出只取决于当前状态。

  1. Mealy状态机 米勒状态机的模型如下图所示,模型中第一个方框是指产生下一状态的组合逻辑F,F是当前状态和输入信号的函数,状态是否改变、如何改变,取决于组合逻辑F的输出;第二框图是指状态寄存器,其由一组触发器组成,用来记忆状态机当前所处的状态,状态的改变只发生在时钟的跳边沿;第三个框图是指产生输出的组合逻辑G,状态机的输出是由输出组合逻辑G提供的,G也是当前状态和输入信号的函数。

图 7.4.6.1 Mealy状态机模型 2) Moore状态机 摩尔状态机的模型如下图所示,对比米勒状态机的模型可以发现,其区别在于米勒状态机的输出由当前状态和输入条件决定的,而摩尔状态机的输出只取决于当前状态。

图 7.4.6.2 Moore状态机模型 3) 三段式状态机 根据状态机的实际写法,状态机还可以分为一段式、二段式和三段式状态机。 一段式:整个状态机写到一个always模块里面,在该模块中既描述状态转移,又描述状态的输入和输出。不推荐采用这种状态机,因为从代码风格方面来讲,一般都会要求把组合逻辑和时序逻辑分开;从代码维护和升级来说,组合逻辑和时序逻辑混合在一起不利于代码维护和修改,也不利于约束。 二段式:用两个always模块来描述状态机,其中一个always模块采用同步时序描述状态转移;另一个模块采用组合逻辑判断状态转移条件,描述状态转移规律以及输出。不同于一段式状态机的是,它需要定义两个状态,现态和次态,然后通过现态和次态的转换来实现时序逻辑。 三段式:在两个always模块描述方法基础上,使用三个always模块,一个always模块采用同步时序描述状态转移,一个always采用组合逻辑判断状态转移条件,描述状态转移规律,另一个always模块描述状态输出(可以用组合电路输出,也可以时序电路输出)。 实际应用中三段式状态机使用最多,因为三段式状态机将组合逻辑和时序分开,有利于综合器分析优化以及程序的维护;并且三段式状态机将状态转移与状态输出分开,使代码看上去更加清晰易懂,提高了代码的可读性,推荐大家使用三段式状态机,本文也着重讲解三段式。 三段式状态机的基本格式是: 第一个always语句实现同步状态跳转; 第二个always语句采用组合逻辑判断状态转移条件; 第三个always语句描述状态输出(可以用组合电路输出,也可以时序电路输出)。 在开始编写状态机代码之前,一般先画出状态跳转图,这样在编写代码时思路会比较清晰,下面以一个7分频为例(对于分频等较简单的功能,可以不使用状态机,这里只是演示状态机编写的方法),状态跳转图如下图所示:

图 7.4.6.3 七分频状态跳转图 状态跳转图画完之后,接下来通过parameter来定义各个不同状态的参数,如下代码所示:

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; 

这里是使用独热码的方式来定义状态机,每个状态只有一位为1,当然也可以直接定义成十进制的0,1,2……7。 因为我们定义成独热码的方式,每一个状态的位宽为7位,接下来还需要定义两个7位的寄存器,一个用来表示当前状态,另一个用来表示下一个状态,如下所示:

reg  [6:0]   curr_st     ;     //当前状态
reg  [6:0]   next_st     ;     //下一个状态

接下来就可以使用三个always语句来开始编写状态机的代码,第一个always采用同步时序描述状态转移,第二个always采用组合逻辑判断状态转移条件,第三个always是描述状态输出,一个完整的三段式状态机的例子如下代码所示:

1  module divider7_fsm ( 
2      //系统时钟与复位
3      input       sys_clk      , 
4      input       sys_rst_n    ,
5  
6      //输出时钟
7      output reg  clk_divide_7 
8      );
9  
10 //parameter define 
11 parameter S0 = 7'b0000001;     //独热码定义方式
12 parameter S1 = 7'b0000010;
13 parameter S2 = 7'b0000100;
14 parameter S3 = 7'b0001000;
15 parameter S4 = 7'b0010000;
16 parameter S5 = 7'b0100000;
17 parameter S6 = 7'b1000000;    
18     
19 //reg define 
20 reg  [6:0]   curr_st     ;     //当前状态
21 reg  [6:0]   next_st     ;     //下一个状态
22 
23 //*****************************************************
24 //** main code
25 //***************************************************** 
26 
27 //状态机的第一段采用同步时序描述状态转移
28 always @(posedge sys_clk or negedge sys_rst_n) begin 
29         if (!sys_rst_n)
30             curr_st <= S0;
31         else
32             curr_st <= next_st;
33 end
34 
35 //状态机的第二段采用组合逻辑判断状态转移条件
36 always @(*) begin 
37     case (curr_st) 
38         S0: next_st = S1;
39         S1: next_st = S2;
40         S2: next_st = S3;
41         S3: next_st = S4;
42         S4: next_st = S5;
43         S5: next_st = S6;
44         S6: next_st = S0;
45         default: next_st = S0;
46     endcase
 

标签: s1m通用二极管

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

锐单商城 - 一站式电子元器件采购平台