本帖最后由 正点原子运营 于 2023-10-27 11:18 编辑
1)实验平台:正点原子 ATK-DFPGL22G开发板
2) 章节摘自【正点原子】ATK-DFPGL22G之FPGA开发指南_V1.0
6)FPGA技术交流QQ群:435699340
1.1.1 状态机Verilog是硬件描述语言,硬件电路是并行执行的,当需要按照流程或者步骤来完成某个功能时,代码中通常会使用很多个if嵌套语句来实现,这样就增加了代码的复杂度,以及降低了代码的可读性,这个时候就可以使用状态机来编写代码。状态机相当于一个控制器,它将一项功能的完成分解为若干步,每一步对应于二进制的一个状态,通过预先设计的顺序在各状态之间进行转换,状态转换的过程就是实现逻辑功能的过程。
状态机,全称是有限状态机(FiniteState 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,因为采用时序电路输出有很大的优势,所以这里第三段状态机是由时序电路输出的。
状态机采用时序逻辑输出的状态机模型如下图所示: 采用这种描述方法虽然代码结构复杂了一些,但是这样做的好处是可以有效地滤去组合逻辑输出的毛刺,同时也可以更好的进行时序计算与约束,另外对于总线形式的输出信号来说,容易使总线数据对齐,减小总线数据间的偏移,从而降低接收端数据采样出错的频率。
1.1.2 模块化设计模块化设计是FPGA设计中一个很重要的技巧,它能够使一个大型设计的分工协作、仿真测试更加容易,代码维护或升级更加便利,当更改某个子模块时,不会影响其它模块的实现结果。进行模块化、标准化设计的最终目的就是提高设计的通用性,减少不同项目中同一功能设计和验证引入的工作量。划分模块的基本原则是子模块功能相对独立、模块内部联系尽量紧密、模块间的连接尽量简单。
在进行模块化设计中,对于复杂的数字系统,我们一般采用自顶向下的设计方式。可以把系统划分成几个功能模块,每个功能模块再划分成下一层的子模块;每个模块的设计对应一个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 moduleseg_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_staticu_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定义的参数做例化。
1.2 Verilog编程规范本节主要给大家介绍下编程规范,良好的编程规范是一个FPGA工程师必备的素质。
1.2.1 编程规范重要性当前数字电路设计越来越复杂,一个项目需要的人越来越多,当几十号设计同事完成同一个项目时候,大家需要互相检视对方代码,如果没有一个统一的编程规范,那么是不可想象的。大家的风格都不一样,如果不统一的话,后续维护、重用等会有很大的困难,即使是自己写的代码,几个月后再看也会变的很陌生,也会看不懂(您可能不相信,不过笔者和同事交流发现大家都是这样的,时间长不看就忘记了),所以编程规范的重要性显而易见。
另外养成良好的编程规范,对于个人的工作习惯、思路等都有非常大的好处。可以让新人尽快融入项目中,让大家更容易看懂您的代码。
1.2.2 工程组织形式工程的组织形式一般包括如下几个部分,分别是doc、par、rtl和sim四个部分。 XX工程名 |--doc |--par |--rtl |--sim |
doc:一般存放工程相关的文档,包括该项目用到的datasheet(数据手册)、设计方案等。不过为了便于大家查看,我们开发板文档是统一汇总存放在资料盘下的; par:主要存放工程文件和使用到的一些IP文件; rtl:主要存放工程的rtl代码,这是工程的核心,文件名与module名称应当一致,建议按照模块的层次分开存放; sim:主要存放工程的仿真代码,复杂的工程里面,仿真也是不可或缺的部分,可以极大减少调试的工作量。
1.2.3 文件头声明每一个Verilog文件的开头,都必须有一段声明的文字。包括文件的版权,作者,创建日期以及内容介绍等,如下表所示。 - //****************************************Copyright(c)***********************************//
- //原子哥在线教学平台:www.yuanzige.com
- //技术支持:www.openedv.com
- //淘宝店铺:http://openedv.taobao.com
- //关注微信公众平台微信号:"正点原子",免费获取ZYNQ & FPGA & STM32 & LINUX资料。
- //版权所有,盗版必究。
- //Copyright(C) 正点原子 2018-2028
- //All rights reserved
- //----------------------------------------------------------------------------------------
- // File name: led_twinkle
- // Last modified Date: 2019/4/14 10:55:56
- // Last Version: V1.0
- // Descriptions: LED灯闪烁
- //----------------------------------------------------------------------------------------
- // Created by: 正点原子
- // Created date: 2019/4/14 10:55:56
- // Version: V1.0
- // Descriptions: The original version
- //
- //----------------------------------------------------------------------------------------
- //****************************************************************************************//
复制代码我们建议一个.V只包括一个module,这样模块会比较清晰易懂。
1.2.4 输入输出定义端口的输入输出有Verilog95和2001两种格式,推荐大家采用Verilog 2001语法格式。下面是Verilog 2001语法的一个例子,包括module名字、输入输出、信号名字、输出类型、注释。 - 1 module led(
- 2 input sys_clk , //系统时钟
- 3 input sys_rst_n, //系统复位,低电平有效
- 4 output reg [3:0] led //4位LED灯
- 5 );
复制代码我们建议如下几点: ① 一行只定义一个信号; ② 信号全部对齐; ③ 同一组的信号放在一起。
1.2.5 parameter定义我们建议如下几点: ① module中的parameter声明,不建议随处乱放; ② 将parameter定义放在紧跟着module的输入输出定义之后; ③ parameter等常量命名全部使用大写。 - 7 //parameter define
- 8 parameter WIDTH = 25 ;
- 9 parameter COUNT_MAX = 25_000_000; //板载50M时钟=20ns,0.5s/20ns=25000000,需要25bit
- 10 //位宽
复制代码
1.2.6 wire/reg定义一个module中的wire/reg变量声明需要集中放在一起,不建议随处乱放。
因此,我们建议如下: ① 将reg与wire的定义放在紧跟着parameter之后; ② 建议具有相同功能的信号集中放在一起; ③ 信号需要对齐,reg和位宽需要空2格,位宽和信号名字至少空四格; ④ 位宽使用降序描述,[6:0]; ⑤ 时钟使用前缀clk,复位使用后缀rst; ⑥ 不能使用Verilog关键字作为信号名字; ⑦ 一行只定义一个信号。 - 12 //reg define
- 13 reg [WIDTH-1:0] counter ;
- 14 reg [1:0] led_ctrl_cnt;
- 15
- 16 //wire define
- 17 wire counter_en ;
复制代码
1.2.7 信号命名大家对信号命名可能都有不同的喜好,我们建议如下: ① 信号命名需要体现其意义,比如fifo_wr代表FIFO读写使能; ② 可以使用“_”隔开信号,比如sys_clk; ③ 内部信号不要使用大写,也不要使用大小写混合,建议全部使用小写; ④ 模块名字使用小写; ⑤ 低电平有效的信号,使用_n作为信号后缀; ⑥ 异步信号,使用_a作为信号后缀; ⑦ 纯延迟打拍信号使用_dly作为后缀。
1.2.8 always块描述方式always块的编程规范,我们建议如下: ① if需要空四格; ② 一个always需要配一个begin和end; ③ always前面需要有注释; ④ beign建议和always放在同一行; ⑤ 一个always和下一个always空一行即可,不要空多行; ⑥ 时钟复位触发描述使用posedgesys_clk和negedge sys_rst_n ⑦ 一个always块只包含一个时钟和复位; ⑧ 时序逻辑使用非阻塞赋值。 - 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.2.9 assign块描述方式assign块的编程规范,我们建议如下: ① assign的逻辑不能太复杂,否则易读性不好; ② assign前面需要有注释; ③ 组合逻辑使用阻塞赋值。 - 23 //计数到最大值时产生高电平使能信号
- 24 assign counter_en = (counter == (COUNT_MAX - 1'b1)) ? 1'b1 : 1'b0;
复制代码
1.2.10 空格和TAB由于不同的解释器对于TAB翻译不一致,所以建议不使用TAB,全部使用空格。
1.2.11 注释添加注释可以增加代码的可读性,易于维护。我们建议规范如下: ① 注释描述需要清晰、简洁; ② 注释描述不要废话,冗余; ③ 注释描述需要使用“//”; ④ 注释描述需要对齐; ⑤ 核心代码和信号定义之间需要增加注释。 - 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) // counter_en为1时,counter清0
- 31 counter <= 1'b0;
- 32 else
- 33 counter <= counter + 1'b1;
- 34 end
复制代码
1.2.12 模块例化模块例化我们建议规范如下: ① moudle模块例化使用u_xx表示。 - 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_staticu_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 );
复制代码
1.2.13 其他注意事项其他注意事项如下: ① 代码写的越简单越好,方便他人阅读和理解; ② 不使用repeat等循环语句; ③ RTL级别代码里面不使用initial语句,仿真代码除外; ④ 避免产生Latch锁存器,比如组合逻辑里面的if不带else分支、case缺少default语句; ⑤ 避免使用太复杂和少见的语法,可能造成语法综合器优化力度较低。 良好的编程规范是大家走向专业FPGA工程师的必备素质,希望大家都能养成良好的编程规范。 |