【ZYNQ】从入门到秃头04 Verilog HDL语法

引言

FPGA跟着Xilinx来到了新时代,在这个阶段,Verilog不再像过去是唯一的工具,有了matlab和hls,对于Verilog的掌握程度也可以放松一些。

在我的应用场景中,Verilog更多的会出现在计数器、DDS等场景中,需要通过Verilog语言对AXI-Sterming进行接口的自定义设计

即服务与AXI总线

Verilog 和 VHDL 区别

这两种语言都是用于数字电路系统设计的硬件描述语言,而且都已经是IEEE的标准。 VHDL 1987年成为标准,而 Verilog是 1995年才成为标准的。这是因为 VHDL是美国军方组织开发的,而 Verilog是由一个公司的私有财产转化而来。为什么 Verilog能成为 IEEE标准呢?它一定有其独特的优越性才行,所以说 Verilog有更强的生命力。

这两者有其共同的特点:

  1. 能形式化地抽象表示电路的行为和结构;
  2. 支持逻辑设计中层次与范 围地描述;
  3. 可借用高级语言地精巧结构来简化电路行为和结构;
  4. 支持电路描述由高层到低层的综合转换;
  5. 硬件描述和实现工艺无关。

但是两者也各有特点。Verilog推出已经有 20年了,拥有广泛的设计群体,成熟的资源,且 Verilog容易掌握,只要有 C语言的编程基础,通过比较短的时间,经过一些实际的操作,可以在 1个月左右掌握这种语言。而VHDL设计相对要难一点,这个是因为 VHDL不是很直观,一般认为至少要半年以上的专业培训才能掌握。

Verilog和 C的区别

Verilog是硬件描述语言,在编译下载到 FPGA之后,会生成电路,所以 Verilog全部是并行处理与运行的; C语言是软件语言,编译下载到 单片机 /CPU之后,还是软件指令,而不会根据你的代码生成相应的硬件电路,而 单片机 /CPU处理 软件指令 需要 取 址 、译码、执行, 是 串行执行的。

Verilog 和 C 的 区别也是 FPGA 和 单片机 /CPU 的 区别, 由于 FPGA全部 并行处理, 所以处理 速度非常快 ,这个 是 FPGA的 最大优势,这一点是单片机 /CPU 替代不了的。

Verilog基础知识

我们先看下
逻辑 电路 中有四种值,即四种状态:

逻辑0:表示低电平,也就是对应我们电路的 GND

逻辑1:表示高电平,也就是对应我们电路的 VCC

逻辑X:表示未知,有可能是高电平,也有可能是低电平

逻辑Z:表示高阻态,外部没有激励信号是一个悬空状态。

如下图所示:
在这里插入图片描述

Verilog的标识符

标识符(identifier)用于定义模块名、端口名和信号名等。 Verilog的标识符可以是任意一组字母、数字、 $和 _(下划线 )符号的组合,但标识符的第一个字符必须是字母或者下划线。另外,标识符是区分大小写的。

以下是一些书写规范的要求:

1、用有意义的有效的名字如 sum、 cpu_addr等。

2、用下划线区分词语组合,如 cpu_addr。

3、采用一些前缀或后缀,比如:时钟采用 clk前缀: clk_50m clk_cpu;低电平采用 _n后缀:enable_n

4、统一缩写,如全局复位信号 rst。

5、同一信号在不同层次保持一致性,如同一时钟信号必须在各模块保持一致。

6、自定义的标识符不能与保留字(关键词)同名。

7、参数统一采用大写,如定义参数 使用 SIZE。

Verilog的数字进制格式

Verilog数字 进制格式包括 二 进制、 八进制 、 十进制 和 十六进制 ,一般常用的为 二 进制、 十进制 和 十六进制。

二进制表示如下: 4’b0101表示 4位 二进制数字 0101)

十进制表示 如下: 4’d2表示 4位十 进制数字 2(二进制 0010)

十六 进制表示 如下: 4’ha表示 4位十六 进制数字 a(二进制 1010),十六进制 的计数方式为 0 12…9 a b c d e f 最大计数为 f f:十 进制 表示为 15)。

当代码中没有指定数字的位宽与进制时,默认为32位的十进制,比如 100,实际上表示的值为32’d100。

Verilog的数据类型

在Verilog语法中,主要有三大类数据类型,即寄存器类型、线网类型和参数类型。从名称中,我们可以看出,真正在数字电路中起作用的数据类型应该是寄存器类型和线网类型。

1) 寄存器类型

**寄存器类型表示一个抽象的数据存储单元,它只能在always语句和 initial语句中被赋值,并且它的值从一个赋值到另一个赋值过程中被保存下来。**如果该过程语句描述的是时序逻辑,即 always语句 带有时钟信号 ,则该寄存器变量对应为寄存器;如果该过程语句描述的是组合逻辑 即 always语句不 带有时钟信号 ,则该寄存器变量对应为硬件连线;寄存器类型的缺省值是 x(未知状态)。

寄存器数据类型有很多种,如reg、 integer、 real等,其中最常用的就是 reg类型,它的使用方法如下:

//reg define 
reg [31:0] delay_cnt; //延时计数器 
reg key_flag ; //按键标志

2) 线网类型

**线网表示Verilog结构化元件间的物理连线。它的值由驱动元件的值决定,例如连续赋值或门的输出。**如果没有驱动元件连接到线网,线网的缺省值为 z(高阻态)。线网类型同寄存器类型一样也是有很多种,如 tri和 wire等,其中最常用的就是 wire类型,它的使用方法如下:

//wire define 
wire data_en; //数据使能信号 
wire [7:0] data ; //数据

3) 参数类型

我们再来看下参数类型 ,参数其实就是一个常量,常被用于定义状态机的状态、数据位宽和延迟大小等,由于它可以在编译时修改参数的值,因此它又常被用于一些参数可调的模块中,使用户在实例化模块时,可以根据需要配置参数。在定义参数时,我们可以一次定义多个参数,参数与参数之间需要用逗号隔开。这里我们需要注意的是参数的定义是局部的,只在当前模块中有效。它的使用方法如下:

//parameter define 
parameter DATA_WIDTH = 8; //数据位宽为8位

Verilog的运算符

大家看完了 Verilog的 数据类型, 我们再 来 介绍 下 Verilog的运算符。 Verilog中的运算符按照 功能 可以分为下述类型:

1、算术运算符、 2、关系运算符、 3、逻辑运算符、 4、条件运算符、 5、位运算符、6、移位运算符、 7、拼接运算符。

下面我们分别 对 这些运算符进行介绍。

1) 算术运算符

算术运算符,简单来说,就是 数学运算 里面的加减乘除 ,数字逻辑处理有 时候也需要进行数字运算,所以 需要 算术 运算 符。常用的算术运算符主要包括 加减乘除 和模除(模除运算也叫取余运算) 如下表 所示
在这里插入图片描述
大家要注意下, Verilog实现 乘除比较浪费组合 逻辑 资源,尤其 是除法

一般 2的指数次幂 的乘 除 法使用 移位 运算 来 完成运算 ,详情 可以看 移位 运算 符章节 。

非 2的指数次幂 的乘除法一般是 调用 现成的 IPQUARTUS/ISE等 工具 软件会有提供, 不过这些 工具 软件提供的 IP也是 由 最 底层的 组合逻辑 (与 或非门等 )搭建而成的

2) 关系 运算符

关系运算符主要是用来做一些条件判断用的,在进行关系运算符时,如果声明的关系是假的,则返回值是 0,如果声明的关系是真的,则返回值是 1;所有的关系运算符有着相同的优先级别,关系运算符的优先级别低于算术运算符的优先级别 如下表所示。
在这里插入图片描述

3) 逻辑 运算符

逻辑运算符是连接多个关系表达式用的,可实现更加复杂的判断,一般不单独使用,都需要配合具体语句来实现完整的意思,如 下表 所示。
在这里插入图片描述

4) 条件 运算符

条件操作符一般来构建从两个输入中选择一个作为输出的条件选择结构,功能等同于always中的 if-else语句,如 下表 所示。
在这里插入图片描述

5) 位 运算符

位运算符是一类最基本的运算符,可以认为它们直接对应数字逻辑中的与、或、非门等逻辑门。常用的位运算符如 下表 所示 。
在这里插入图片描述在这里插入图片描述
位运算符的与、或、非与逻辑运算符逻辑与、逻辑或、逻辑非使用 时候容易混淆,逻辑运算符 一般 用在条件判断上,位运算符 一般 用在 信号 赋值上。

6) 移位 运算符

移位运算符包括 左移位运算符和右移位运算符,这两种移位运算符都用 0来填补移出的空位。如 下表所示 。
在这里插入图片描述
假设a有 8bit数据 位宽 ,那么 a<<2,表示 a左 移 2bit a还是 8bit数据位宽 a的最高 2bit数据被移位丢弃了,最低 2bit数据 固定 补 0。 如果 a是 3(二进制 00000011 那么 3左 移 2bit 3<<2 就是 12(二进制 00001100)。 一般 使用 左移位 运算代替乘法 ,右移位 运算 代替 除法 但是这种也只能表示 2的指数次幂 的乘 除 法。

7) 拼接 运算符

Verilog中有一个特殊的运算符 是 C语言 中没有的,就是位拼接运算符。用这个运算符可以把两个或多个信号的某些位拼接起来进行运算操作。如 下表 所示 。
在这里插入图片描述

8) 运算符 的优先级

介绍完了 这么多运算符,大家可能 会 想到究竟哪个运算符高,哪个运算符低。为了便于大家查看这些运算符的优先级,我们将它们制作成了表格,如 下表 所示。
在这里插入图片描述
在这里插入图片描述

关键字

Verilog和 C语言 类似 ,都因编写 需要 定义了一系列保留字,叫做关键字(或关键词)。 这些 保留字 是识别 语法的关键 。我们 给大家列出了 Verilog中 的 关键字 ,如下表 所示。
在这里插入图片描述
在这里插入图片描述
虽然上表列了很多,但是实际 经常 使用的不是很多,实际 经常 使用的主要 如下 表 所示。
在这里插入图片描述
注意只有小写的关键字才是保留字。例如,标识符always(这是个关键词 )与标识符 ALWAYS(非关键词 )是不同的。

程序框架

我们以 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 代表 计数到最大值时产生高电平使能, 本质上是 一个 硬件连线 ,其实代表的是 一些计数 器 /寄存器 做逻辑 判断 的 结果 。

 //*********************************************************************************** 
 //** main code 
 //***********************************************************************************

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

Verilog高级知识点

阻塞赋值( 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,代码以及信号波形图如下图所示
在这里插入图片描述
在这里插入图片描述
代码中使用的是阻塞赋值语句,从波形图中可以看到,在复位的时候(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。

非阻塞赋值( 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,代码以及信号波形图如下图所示
在这里插入图片描述
在这里插入图片描述
代码中使用的是非阻塞赋值语句,从波形图中可以看到,在复位的时候( rst_n=0) a=1 b=2c=3;而结束复位之后(波形图中的 0时刻),当 clk的上升沿到来时(波形图中的 2时刻), a=0 b=1c=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

assign和 always区别

assign语句 使用 时不能带时钟 。

always语句可以 带时钟 也可以不带时钟 。 在 always不带 时钟时 ,逻辑 功能和 assign完全 一致,都是只 产生 组合 逻辑 。比较简单的 组合逻辑推荐使用 assign语句,比较复杂的组合逻辑推荐使用 always语句

示例如下

assign counter_en = (counter == (COUNT_MAX - 1'b1)) ? 1'b1 : 1'b0; 
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

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 

什么是 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的 ,第 一个仍然 是普通的 带 有时钟的寄存器。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

状态机

Verilog是硬件描述语言,硬件电路是并行执行的,当需要按照流程或者步骤来完成某个功能时,代码中通常会使用很多个 if嵌套语句来实现,这样就增加了代码的复杂度,以及降低了代码的可读性,这个时候就可以使用状态机来编写代码。 状态机 相当于一个控制器,它将一项功能的完成分解为若干步,每一步对应于二进制的一个状态,通过预先设计的顺序在各状态之间进行转换,状态转换的过程就是实现逻辑功能的过程。

状态机,全称是有限 状态机( Finite State Machine,缩写为 FSM),是一种在有限个状态之间按一定规律转换的时序电路,可以认为 是组合逻辑和 时序 逻辑的一种组合。 状态机通过控制各个状态的跳转来控制流程,使得整个代码看上去更加清晰易懂,在控制复杂流程的时候,状态机优势明显,因此基本上都会用到状态机,如 SDRAM控制器等。在本手册提供的例程中,会有多个用到状态机设计的例子,希望大家能够慢慢体会和理解,并且能够熟练掌握。

根据状态机的输出是否与输入条件相关,可将状态机分为两大类,即摩尔(Moore)型状态机和米勒(Mealy)型状态机。

➢ Mealy状态机:组合逻辑的输出不仅取决于当前状态,还取决于输入状态。
➢ Moore状态机:组合逻辑的输出 只取决于当前状态。

1) Mealy状态机

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

2) Moore状态机

摩尔状态机的模型如下图所示,对比米勒状态机的模型可以发现,其区别在于米勒状态机的输出由当前状态和输入条件决定的,而摩尔状态机的输出只取决于当前状态。
在这里插入图片描述

3) 三段式状态机

根据状态机的实际写法,状态机还可以分为一段式、二段式和三段式状态机 。

一段式 :整个状态机写到一个 always模块里面,在该模块中既描述状态转移,又描述状态的输入和输出 。不推荐采用这种状态机,因为从代码风格方面来讲,一般都会要求把组合逻辑和时序逻辑分开;从代码维护和升级来说,组合逻辑和时序逻辑混合在一起不利于代码维护和修改,也不利于约束。

二段式:用两个 always模块来描述状态机,其中一个 always模块采用同步时序描述状态转移;另一个模块采用组合逻辑判断状态转移条件,描述状态转移规律以及输出 。 不同于一段式状态机的是 它需要定义两个状态 现态和次态,然后通过现态和次态的转换来实现时序逻辑。

三段式:在两个always模块描述方法基础上,使用三个 always模块,一个 always模块采用同步时序描述状态转移,一个 always采用组合逻辑判断状态转移条件,描述状态转移规律,另一个 always模块描述状态输出 (可以用组合电路输出,也可以时序电路输出 )。

实际应用中三段式状态机 使用最多, 因为三段式状态机将组合逻辑和时序分开,有利于综合器分析优化以及程序的维护;并且三段式状态机将状态转移与状态输出分开,使代码看上去更加清晰易懂,提高了代码的可读性, 推荐大家使用 三段式状态机, 本文也着重讲解三段式 。

三段式状态机的基本格式是:

第一个always语句实现同步状态跳转;
第二个always语句 采用组合逻辑判断状态转移条件
第三个always语句描述状态输出 (可以用组合电路输出,也可以时序电路输出 )。

在开始编写状态机代码之前,一般先画出状态跳转图,这样在编写代码时思路会比较清晰,下面以一个 7分频 为例(对于分频等较简单的功能,可以不使用状态机,这里只是演示状态机编写的方法),状态跳转图如下图所示:
在这里插入图片描述
状态跳转图画完之后,接下来通过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 
47 end 
48 
49 //状态机的第三段描述状态输出(这里采用时序电路输出) 
50 always @(posedge sys_clk or negedge sys_rst_n) begin 
51 	if (!sys_rst_n) 
52 		clk_divide_7 <= 1'b0;
53 	else if ((curr_st == S0) | (curr_st == S1) | (curr_st == S2) | (curr_st == S3)) 
54 		clk_divide_7 <= 1'b0; 
55 	else if ((curr_st == S4) | (curr_st == S5) | (curr_st == S6)) 
56 		clk_divide_7 <= 1'b1; 
57 	else 
58 		; 
59 end 
60 
61 endmodule

在编写状态机代码时首先要定义状态变量(代码中的参数S0~S6)与状态寄存器 curr_st、next_st),如代码中第 10行至第 21行所示;
在这里插入图片描述

接下来使用三个 always语句来实现三段状态机, 第一个always语句实现同步状态跳转 (如代码的第 27至第 33行所示),
在这里插入图片描述

在复位的时候,当前状态处在 S0状态,否则将下一个状态赋值给当前状态;第二个 always采用组合逻辑判断状态转移条件(如代码的第 35行至第 47行代码所示),
在这里插入图片描述

这里每一个状态只保持一个时钟周期,也就是直接跳转到下一个状态,在实际应用中,一般根据输入的条件来判断是否跳转到其它状态或者停留在当前转态,最后在 case语句后面增加一个 default语句,来防止状态机处在异常的状态;

第三 个 always输出分频后的时钟(如代码的第 49至第59行代码所示),
在这里插入图片描述

状态机的第三段可以使用组合逻辑电路输出,也可以使用时序逻辑电路输出,一般推荐使用时序电路输出,因为状态机的设计和其它设计一样,最好使用同步时序方式设计,以提高设计的稳定性,消除毛刺。

从代码中可以看出,输出的分频时钟clk_divide_7只与当前状态( curr_st)有关,而与输入状态无关,所以属于摩尔型状态机。

状态机的第一段对应摩尔状态机模型的状态寄存器,用来记忆状态机当前所处的状态;

状态机的第二段对应摩尔状态机模型产生下一状态的组合逻辑 F;

状态机的第三段对应摩尔状态机产生输出的组合逻辑 G,因为采用时序电路输出有很大的优势,所以这里第三段状态机是由时序电路输出的。

状态机采用时序逻辑输出的状态机模型如下图所示:
在这里插入图片描述
采用这种描述方法虽然代码结构复杂了一些,但是 这样做的好处是可以有效地滤去 组合 逻辑输出的毛刺,同时 也 可以 更好的 进行时序计算与约束,另外对于总线形式的输出信号来说,容易使总线数据 对齐减小总线数据间的偏移,从而 降低 接收端数据采样出错的频率。

模块 化设计

模块化设计是FPGA设计中一个很重要的技巧,它能够使一个大型设计的分工协作、仿真测试更加容易,代码维护或升级更加便利 ,当更改某个子模块时,不会影响其它模块的实现结果。进行模块化、标准产生下一状态的组合逻辑F状态寄存器clk输入输出组合逻辑G时钟信号 clk激励信号当前状态输出输入输出寄存器clk输入化设计的最终目的就是提高设计的通用性,减少不同项目中同一功能设计和验证引入的工作量。划分模块的基本原则是子模块功能相对独立、模块内部联系尽量紧密、模块间的连接尽量简单。

在进行模块化设计中,对于复杂的数字系统,我们一般采用自顶向下的设计方式。可以把系统划分成几个功能模块,每个功能模块再划分成下一层的子模块;每个模块的设计对应一个module,一个module设计成一个Verilog 程序文件。因此,对一个系统的顶层模块,我们采用结构化的设计,即顶层模块分别调用了各个功能模块。

下图是模块化设计的功能框图,一般整个设计的顶层模块只做例化(调用其它模块),不做逻辑。顶层下面会有模块A、模块B、模块C 等,模块A/B/C 又可以分多个子模块实现。
在这里插入图片描述
在这里我们补充一个概念,就是Verilog 语法中的模块例化。FPGA 逻辑设计中通常是一个大的模块中包含了一个或多个功能子模块,Verilog 通过模块调用或称为模块实例化的方式来实现这些子模块与高层模块的连接,有利于简化每一个模块的代码,易于维护和修改。

下面以一个实例(静态数码管显示实验)来说明模块和模块之间的例化方法。在静态数码管显示实验中,我们根据功能将FPGA 顶层例化了以下两个模块:计时模块(time_count)和数码管静态显示模块(seg_led_static),如下图所示:
在这里插入图片描述
计时模块部分代码如下所示:

1 module time_count(
2 	input clk , // 时钟信号
3 	input rst_n , // 复位信号
4 
5 output reg flag // 一个时钟周期的脉冲信号 
6 ); 
7 
8 //parameter define 
9 parameter MAX_NUM = 25000_000; // 计数器最大计数值
……
34 endmodule

数码管静态显示模块部分代码如下所示:

1 module seg_led_static ( 
2 	input clk , // 时钟信号 
3 	input rst_n , // 复位信号(低有效) 
4 
5 	input add_flag, // 数码管变化的通知信号 
6 	output reg [5:0] sel , // 数码管位选 
7 	output reg [7:0] seg_led // 数码管段选 
8 );
…… 
66 endmodule

顶层模块代码如下所示:

1 module seg_led_static_top ( 
2 input sys_clk , // 系统时钟 
3 input sys_rst_n, // 系统复位信号(低有效) 
4 
5 output [5:0] sel , // 数码管位选 
6 output [7:0] seg_led // 数码管段选 
7 
8 ); 
9 
10 //parameter define 
11 parameter TIME_SHOW = 25'd25000_000; // 数码管变化的时间间隔0.5s 
12 
13 //wire define 
14 wire add_flag; // 数码管变化的通知信号 
15 
16 //***************************************************** 
17 //** main code 
18 //***************************************************** 
19 
20 //例化计时模块 
21 time_count #(
22 .MAX_NUM (TIME_SHOW) 
23 ) u_time_count( 
24 .clk (sys_clk ), 
25 .rst_n (sys_rst_n), 
26 
27 .flag (add_flag ) 
28 ); 
29 
30 //例化数码管静态显示模块 
31 seg_led_static u_seg_led_static ( 
32 .clk (sys_clk ), 
33 .rst_n (sys_rst_n), 
34 
35 .add_flag (add_flag ), 
36 .sel (sel ), 
37 .seg_led (seg_led ) 
38 ); 
39 
40 endmodule

我们上面贴出了顶层模块的完整代码,子模块只贴出了模块的端口和参数定义的代码。这是因为顶层模块对子模块做例化时,只需要知道子模块的端口信号名,而不用关心子模块内部具体是如何实现的。如果子模块内部使用 parameter定义了一些参数, Verilog也支持对参数的例化(也叫参数的传递),即顶层模块可以通过例化参数来修改子模块内定义的参数。

我们先来看一下顶层模块是如何例化子模块的,例化方法如下图所示:
在这里插入图片描述
上图右侧是例化的数码管静态显示模块,子模块名是指被例化模块的模块名,而例化模块名相当于标识,当例化多个相同模块时,可以通过例化名来识别哪一个例化,我们一般命名为 “u_”+“子模块名 ”。

信号列表中 “.”之后的信号是数码管静态显示模块定义的端口信号,括号内的信号则是顶层模块声明的信号,这样就将顶层模块的信号与子模块的信号一一对应起来,同时需要注意信号的位宽要保持一致。

接下来再来介绍一下参数的例化,参数的例化是在模块例化的基础上,增加了对参数的信号定义,如下图所示:
在这里插入图片描述
在对参数进行例化时,在模块名的后面加上““#”,表示后面跟着的是参数列表。计时模块定义的MAX_NUM和顶层模块的 TIME_SHOW都是等于 25000_000,当在顶层模块定义TIME_SHOW=12500_000时,那么子模块的 MAX_NUM的值实际上是也等于 12500_000。当然即使子模块包含参数,在做模块的例化时也可以不添加对参数的例化,这样的话,子模块的参数值等于该 模块内部实际定义的值。

值得一提的是,Verilog语法中的 localparam代表的意思同样是参数定义,用法和 parameter基本一致,区别在于 parameter定义的参数可以做例化,而 localparam定义的参数是指本地参数,上层模块不可以对 localparam定义的参数做例化。

Verilog编程规范

工程组织形式

工程的组织形式一般包括如下几个部分 ,分别 是 doc、 par、 rtl和 sim四个 部分。
在这里插入图片描述
doc:一般存放工程相关的文档,包括该项目用到的 datasheet(数据手册)、设计方案等。不过 为了便于大家查看, 我们开发板文档 是 统一汇总 存放 在 资料盘下的

par:主要存放工程文件和使用 到的一些 IP文件;

rtl:主要存放工程的 rtl代码,这是工程的核心,文件名与 module名称应当一致,建议按照模块的层次分开存放;

sim:主要存放工程的仿真代码 复杂的工程里面,仿真也是不可或缺的部分 可以极大减少 调试的工作量 。

文件头声明

每一个Verilog文件的开头,都必须有一段声明的文字。包括文件的版权,作者,创建日期以及内容介绍等,如下表所示。
在这里插入图片描述
我们建议一个.V只包括一个 module 这样 模块会比较 清晰 易懂。

输入输出定义

端口的 输入输出有 Verilog 95和 2001两种格式,推荐大家采用 Verilog 2001语法 格式。下面是 Verilog 2001语法 的 一个例子,包括module名字、输入输出、信号名字、输出类型、注释。
在这里插入图片描述
我们建议如下几点

① 一行只定义一个信号;
② 信号全部对齐;
③ 同一组的信号放在一起。

parameter定义

我们建议如下几点

① module中的 parameter声明,不建议随处乱放;
② 将 parameter定义放在紧跟着 module的输入输出定义之后;
③ parameter等常量命名全部使用大写。
在这里插入图片描述

wire/reg定义

一个module中的 wire/reg变量声明需要集中放在一起,不建议随处乱放。

因此,我们建议如下

① 将 reg与 wire的定义放在紧跟着 parameter之后;
② 建议具有相同功能的信号集中放在一起;
③ 信号需要对齐, reg和位宽需要空 2格,位宽和信号名字至少空四格;
④ 位宽使用降序描述, ,[6:
⑤ 时钟使用前缀 clk,复位使用后缀 rst
⑥ 不能使用 Verilog关键字作为信号名字;
⑦ 一行只定义一个信号。
在这里插入图片描述

信号命名

大家对 信号命名可能都有不同的喜好,我们建议 如下

① 信号命名需要体现其意义,比如 fifo_wr代表 FIFO读写使能;
② 可以使用“ “_”隔开信号,比如 sys_clk
③ 内部信号不要使用大写,也不要使用大小写混合,建议全部使用小写;
④ 模块名字使用小写;
⑤ 低电平有效的信号,使用 _n作为信号后缀;
⑥ 异步信号,使用 _a作为信号后缀;
⑦ 纯延迟打拍信号使用 _dly作为后缀。

always块描述方式

always块 的编程规范, 我们建议 如下

① if需要空四格;
② 一个 always需要配一个 begin和 end
③ always前面需要有注释;
④ beign建议和 always放在同一行;
⑤ 一个 always和下一个 always空一行即可,不要空多行;
⑥ 时钟复位触发描述使用 posedge sys_clk和 negedge sys_rst_n
⑦ 一个 always块只包含一个时钟和复位;
⑧ 时序逻辑使用非阻塞赋值。

assign块描述方式

assign块 的编程规范, 我们建议 如下

① assign的逻辑不能太复杂,否则易读性不好;
② assign前面需要有注释;
③ 组合逻辑使用阻塞赋值。

空格和 TAB

由于不同的解释器对于TAB翻译不一致,所以建议不使用 TAB,全部使用空格。

注释

添加注释可以增加代码的可读性,易于维护。我们建议规范如下

① 注释描述需要清晰、简洁;
② 注释描述不要废话,冗余;
③ 注释描述需要使用“
④ 注释描述需要对齐;
⑤ 核心代码和信号定义之间需要增加注释。

模块例化

模块例化我们建议规范如下
① moudle模块例化使用 u_xx表示。
在这里插入图片描述

其他注意 事项

其他注意事项如下

① 代码写的越简单越好, 方便 他人 阅读 和 理解
② 不使用 repeat等循环语句;
③ RTL级别代码里面不使用 initial语句,仿真代码除外;
④ 避免产生 Latch锁存器, 比如组合逻辑里面 的 if不带 else分支 、 case缺少 default语句
⑤ 避免使用太复杂和少见的语法,可能造成语法综合器优化力度较低。

良好的编程规范是 大家 走向专业 FPGA工程师 的 必备 素质 希望大家都能养成良好的编程规范。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值