本文记录Verilog计数器、按键消抖、流水灯以及状态机设计思路。博文尾部为参考自顶向下设计思路的简单状态机模块设计。
计数器
计数器是FPGA中常用的单位,例如希望a状态持续X时间,b状态持续Y时间。在时钟做时间源的情况下,通过累计时钟计数得出较大打秒级、分钟级单位,得出flag标志可用到各个模块的判断语句中。
假设有sys_clk =
F,则单个周期T=1/F,A秒钟的计数器所需的计数值为M=A/T=AF。
- 为节省资源,考虑找到所有时间值的最小公倍数
- 单个always块仅操作单个变量
- 提前1拍,打出flag信号,可用于所有变量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| module Counter( input wire sys_clk, input wire sys_rst_n, output reg led_out );
parameter CNT_MAX = 25'd24_999_999;
reg [24:0] cnt;
reg [1:0] half_s_cnt;
always@(posedge sys_clk or negedge sys_rst_n) if(!sys_rst_n) cnt <= 1'b0; else if(cnt == CNT_MAX) cnt <= 1'b0; else cnt <= cnt + 1'b1;
always@(posedge sys_clk or negedge sys_rst_n) if(!sys_rst_n) half_s_cnt <= 2'd0; else if(cnt == CNT_MAX - 1) half_s_cnt <= half_s_cnt + 1'b1; else half_s_cnt <= half_s_cnt;
always@(posedge sys_clk or negedge sys_rst_n) if(!sys_rst_n) led_out <= 1'b0; else if(half_s_cnt == 2'd0)
led_out <= 1'b1; else led_out <= 1'b0;
endmodule
|
half_s_cnt == 2’d0 复位后首次丢失一拍
half_s_cnt == 2’d3,复位后时间正常
两张图的差异在于,led_out默认输出与第一次状态不同。对第一张图,在rst
= 1后,cnt和led同时翻转,因此出现一拍丢失。若希望使用half_s_cnt ==
2’d0,而不出现丢拍,则需要把复位后的led状态拉高。
分频器
Vivado自带的IP核为锁相环,通过PLL由一个输入时钟得到多个不同频率的子时钟。分频器细分为奇分频与偶分频。分频器本质与计数器相同,即计数到某一值输出翻转或输出Flag标志。
偶分频
偶分频可以sys_clk上升沿为为基准,计数刚好对应的分频系数的一半时,对信号翻转。
偶分频(双边沿输出flag标志)
若为低频时钟,则可直接对信号翻转:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
|
module uPLL( input wire sys_clk, input wire sys_rst_n, output reg clk_out, output reg stat_flag ); parameter DIVIDER = 4'd6; localparam DIV_IDX = (DIVIDER > 15 || DIVIDER < 2)? (DIVIDER > 15)?14: (DIVIDER <= 1)?2: DIVIDER: DIVIDER; localparam DIV_MAX = 4'd15; localparam DIV_MIN = 4'd2;
reg [3:0] clk_cnt; always@(posedge sys_clk or negedge sys_rst_n) if(!sys_rst_n) stat_flag <= 1'b0; else if(DIVIDER > DIV_MAX || DIVIDER < DIV_MIN) stat_flag <= 1'b0; else stat_flag <= 1'b1; always@(posedge sys_clk or negedge sys_rst_n) if(!sys_rst_n) clk_cnt <= 4'd0; else if(clk_cnt == DIV_IDX/2-1) clk_cnt <= 4'd0; else clk_cnt <= clk_cnt + 1; always@(posedge sys_clk or negedge sys_rst_n) if(!sys_rst_n) clk_out <= 1'b0; else if(clk_cnt == DIV_IDX/2-1) clk_out <= ~clk_out; else clk_out <= clk_out;
endmodule
|
以此生成的clk:
为解决高速时钟下不精确问题,将clk_out作为输出标识配合同步时钟,思路同计数器。将上方代码块的clk_out代码更换为以下代码:
1 2 3 4 5 6 7
| always@(posedge sys_clk or negedge sys_rst_n) if(!sys_rst_n) clk_out <= 1'b0; else if(clk_cnt == DIV_IDX/2-2) clk_out <= 1'b1; else clk_out <= 1'b0;
|
仿真结果如下:
偶分频仿真结果
产生flag标识后可以配合sys_clk使用:
1 2 3 4 5 6 7 8
| reg [1:0] a; always@(posedge sys_clk or posedge sys_rst_n) if(!sys_rst_n) a <= 2'b0; else if(flag) a <= a + 1'b1; else a <= a;
|
上方的flag == 1
时代表分频后时钟的边沿(上升沿+下降沿),调整计数清零时的值可调整为单边沿或双边沿:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
|
module eveDivider( input wire sys_clk, input wire sys_rst_n, output reg clk_out, output reg clk_flag );
parameter DIVIDER = 32'd6; parameter CLK_EDGE_SEL = 1'b0;
localparam DIV_IDX = (CLK_EDGE_SEL == 1'b0)? DIVIDER -1 : DIVIDER/2 -1 ;
reg [31:0] clk_cnt;
always@(posedge sys_clk or negedge sys_rst_n) if(sys_rst_n == 1'b0) clk_cnt <= 32'd0; else if(clk_cnt == DIV_IDX) clk_cnt <= 32'd0; else clk_cnt <= clk_cnt + 1'd1;
always@(posedge sys_clk or negedge sys_rst_n) if(sys_rst_n == 1'b0) clk_out <= 1'b0; else if(clk_cnt == DIVIDER/2-1 || clk_cnt == DIVIDER-1) clk_out <= ~clk_out; else clk_out <= clk_out;
always@(posedge sys_clk or negedge sys_rst_n) if(sys_rst_n == 1'b0) clk_flag <= 1'b0; else if(clk_cnt == DIV_IDX - 1) clk_flag <= 1'b1; else clk_flag <= 1'b0; endmodule
|
偶分频(双边沿标志)仿真结果
奇分频
偶分频因为分频系数为偶数,最小为二分频,恰好对齐ys_clk上升沿(本节末尾放置使用下降沿的通用分频器)。若对时钟进行奇分频,欲求找到系数一半m/2-1的上升沿是不可能的,但是可引入下降沿为标准,波形如下:
奇分频波形
因此,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| module uPLL( input wire sys_clk, input wire sys_rst_n, output reg clk_out, output reg stat_flag ); parameter DIVIDER = 4'd6; localparam DIV_IDX = (DIVIDER > 15 || DIVIDER < 2)? (DIVIDER > 15)?14: (DIVIDER <= 1)?2: DIVIDER: DIVIDER; localparam DIV_MAX = 4'd15; localparam DIV_MIN = 4'd2; reg [3:0] clk_cnt; always@(posedge sys_clk or negedge sys_clk or negedge sys_rst_n) if(!sys_rst_n) clk_cnt <= 4'b0; else if(clk_cnt == DIV_IDX-1) clk_cnt <= 4'b0 ; else clk_cnt <= clk_cnt + 1'b1; always@(posedge sys_clk or negedge sys_clk or negedge sys_rst_n) if(!sys_rst_n) stat_flag <= 1'b0; else if(DIVIDER > DIV_MAX || DIVIDER < DIV_MIN) stat_flag <= 1'b1; else stat_flag <= 1'b0; always@(posedge sys_clk or negedge sys_clk or negedge sys_rst_n) if(!sys_rst_n) clk_out <= 1'b0; else if(clk_cnt == DIV_IDX-2) clk_out <= 1'b1; else clk_out <= 1'b0; endmodule
|
奇数分频仿真结果
显然always块中增加对下降沿的判断可以快速精准的找到分频结果,但是:
- 双边沿检测综合更复杂、时序约束复杂
- 不适合合并到仅使用上升沿判断的通用分频模块中
因此可将上升沿下降沿分别放在两个块内,计数到分频值的一半时翻转,将输出信号相或即可得到输出:
奇分频波形
代码块如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| module oddDivider( input wire sys_clk, input wire sys_rst_n, output wire clk_out ); parameter DIVIDER = 32'd5; localparam DIV_HIDX = DIVIDER/2; localparam DIV_IDX = DIVIDER - 1;
reg [31:0] clk_cnt; reg clk_pos; reg clk_neg; always@(posedge sys_clk or negedge sys_rst_n) if(!sys_rst_n) clk_cnt <= 32'd0; else if(clk_cnt == DIV_IDX) clk_cnt <= 32'd0 ; else clk_cnt <= clk_cnt + 1'd1; always@(posedge sys_clk or negedge sys_rst_n) if(!sys_rst_n) clk_pos <= 1'b0; else if(clk_cnt == DIV_HIDX || clk_cnt == DIV_IDX) clk_pos <= ~clk_pos; else clk_pos <= clk_pos; always@(negedge sys_clk or negedge sys_rst_n) if(!sys_rst_n) clk_neg <= 1'b0; else if(clk_cnt == DIV_HIDX || clk_cnt == DIV_IDX) clk_neg <= ~clk_neg; else clk_neg <= clk_neg; assign clk_out = clk_pos | clk_neg; endmodule
|
奇分频仿真波形
通用分频
既然上方两种分频方式可产生flag信号,那么可通过对sys_clk基准时钟单上升沿计数封装出通用分频模块,到达分频时输出flag,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| module uPLL( input wire sys_clk, input wire sys_rst_n, output reg clk_flag ); parameter DIVIDER = 32'd5; localparam DIV_REV_CNT = DIVIDER - 2; localparam DIV_REF_CNT = DIVIDER - 1; reg [31:0] clk_cnt; always@(posedge sys_clk or negedge sys_rst_n) if(!sys_rst_n) clk_cnt <= 32'd0; else if(clk_cnt == DIV_REF_CNT) clk_cnt <= 32'd0; else clk_cnt <= clk_cnt + 1'd1; always@(posedge sys_clk or negedge sys_rst_n) if(!sys_rst_n) clk_flag <= 1'd0; else if(clk_cnt == DIV_REV_CNT) clk_flag <= 1'd1; else clk_flag <= 1'd0; endmodule
|
通用分频仿真波形
按键消抖
按键是电路板上常用的给出特殊高低电平的器件,单一使用按键时可能出现按下/弹起时的波形抖动,称为前抖动/后抖动。常用的解决办法是:
- 硬件上采用RS触发器、外接滤波电容
- 软件上采用延时算法
按键抖动模型
按键按下的状态分为5个状态:
- A、E:按下/弹起后的稳定状态
- B:按下时的不稳定状态,持续时间不定
- C:按下后的稳定状态
- D:弹起时的不稳定状态,持续时间不定
功能需求
针对指定引脚,检测到连续20ms低电平时,输出1拍低电平
详细设计
针对输入信号,当检测到低电平时计数器开始计数。若遇到高电平,则立刻清0。当达到给定值时输出低电平脉冲,随后计数值保持,直到按键释放。
方案设计
上图3种方案设计时,依次分析可能出现的问题,直到优化出最终设计。
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| module key_filter( input wire sys_clk, input wire sys_rst_n, input wire key_in, output reg key_out ); parameter FILTER_MAX = 20'd1_000_000; reg [19:0] hold_cnt; always@(posedge sys_clk or negedge sys_rst_n) if(!sys_rst_n) hold_cnt <= 20'd0; else if(key_in == 1'b1) hold_cnt <= 20'd0; else if(hold_cnt == FILTER_MAX - 1) hold_cnt <= hold_cnt; else hold_cnt <= hold_cnt + 1'd1; always@(posedge sys_clk or negedge sys_rst_n) if(!sys_rst_n) key_out <= 1'b1; else if(hold_cnt == FILTER_MAX - 2) key_out <= 1'b0; else key_out <= 1'b1; endmodule
|
tb仿真文件中,也可以使用always块加计数器在不同时间段产生不同的输入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| reg [7:0] clk_cnt; always@(posedge sys_clk or negedge sys_rst_n) if(!sys_rst_n) clk_cnt <= 8'd0; else if(clk_cnt == 199) clk_cnt <= 8'd0; else clk_cnt <= clk_cnt + 1'd1;
always@(posedge sys_clk or negedge sys_rst_n) if(!sys_rst_n) key_in <= 1'd1; else if(clk_cnt <19 || clk_cnt>=150) key_in <= 1'd1; else if(clk_cnt >=50 && clk_cnt <100) key_in <= 1'd0; else key_in <= $random%2;
|
仿真结果
仿真结果
流水灯
功能需求
根据参数,调整4路LED流水灯输出,每间隔固定时间切换下一个灯亮。
详细设计
在参数给定的情况下,只需设定计数器,计数到给定值时刷新输出,即方案A。此种情况无法保证复位按下时的初始状态全灭/全亮。
进一步,如果希望初始状态为4’b0000/4’b1111,和运行时仅1bit为1或0的状态并不一致。因此需要提前打1拍增加flag标志(参考分频器一节,提前1拍打出flag),在检测到flag拉高时根据当前led状态决定是移位或是刷新初值(led_out
== 4b1000)。
如果采用位拼接,只需要在flag拉高时刷新初值。
方案设计
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| module flow_led( input wire sys_clk, input wire sys_rst_n, output reg [3:0] led_out ); parameter G_LIGHT_HD_TIME = 25'd25_000_000; localparam L_LIGHT_HD_TIME = G_LIGHT_HD_TIME - 1; localparam L_LED_INIT = 4'b0000; reg [24:0] clk_cnt; always@(posedge sys_clk or negedge sys_rst_n) if(!sys_rst_n) clk_cnt <= 25'd0; else if(clk_cnt == L_LIGHT_HD_TIME) clk_cnt <= 25'd0; else clk_cnt <= clk_cnt + 1'd1; reg time500ms_flag; always@(posedge sys_clk or negedge sys_rst_n) if(!sys_rst_n) time500ms_flag <= 1'b0; else if(clk_cnt == L_LIGHT_HD_TIME - 1) time500ms_flag <= 1'b1; else time500ms_flag <= 1'b0; always@(posedge sys_clk or negedge sys_rst_n) if(!sys_rst_n) led_out <= L_LED_INIT; else if(led_out == L_LED_INIT) led_out <= L_LED_INIT + 4'b0001; else if(time500ms_flag)begin if( led_out == 4'b1000) led_out <= L_LED_INIT + 4'b0001; else led_out <= led_out<<1'b1; end else led_out <= led_out;
endmodule
|
仿真结果
方案A的不再赘述。对上述方案B,复位后的第一个状态会丢1拍,在计数器一节尾部已经做了分析。
呼吸灯
流水灯是在指定时间间隔是切换状态,呼吸灯为达到呼吸效果则通过控制占空比(PWM)达到不同程度的亮度效果(引脚直接控制电压显然比较困难)。
功能需求
给定参数T下,实现均匀的亮,灭效果,整个周期为2T。
详细设计
假设有1个较短的时间片,根据PWM的原理,此时间片内,高电平持续时间越长,呈现的亮度越亮。通过对相同长度的时间片设置递增的高电平时长,可以“呼吸”式亮起/灭下。
参照其他已有设计,可以1us为基准时间单位,以N个单位设置上一级/上上一级时间片。
若如此设置,单个亮/灭状态的周期 T = N2 us。
将两级时间片的单位设置为相同,程序内方便以当前循环周期设定亮灭。
呼吸灯波形时序
上方是整体的逻辑,N =
1000,标准的1s亮/灭。单独以其中一个为例,将计数值简化t1 = t2 = t3 =
4。
四个led_out从上至下为连续4个cnt_t3下的输出,呈现如下态势:
单周期波形
可发现:cnt_t2<cnt_t3时灯亮,可实现亮度时间均匀的增长。(暗下同理)
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
| module breathe_led #( parameter G_US_CNT_MAX = 6'd50, parameter G_MS_CNT_MAX = 10'd1000, parameter G_S_CNT_MAX = 10'd1000 ) ( input wire sys_clk, input wire sys_rst_n, output reg pwm_out );
localparam L_US_CNT_MAX = G_US_CNT_MAX - 1; localparam L_MS_CNT_MAX = G_MS_CNT_MAX - 1; localparam L_S_CNT_MAX = G_S_CNT_MAX - 1; reg [5:0] us_cnt; reg [10:0] ms_cnt; reg [10:0] s_cnt; always@(posedge sys_clk or negedge sys_rst_n) if(!sys_rst_n) us_cnt <= 6'd0; else if(us_cnt == L_US_CNT_MAX) us_cnt <= 6'd0; else us_cnt <= us_cnt + 1'd1; always@(posedge sys_clk or negedge sys_rst_n) if(!sys_rst_n) ms_cnt <= 10'd0; else if(us_cnt == L_US_CNT_MAX && ms_cnt == L_MS_CNT_MAX) ms_cnt <= 10'd0; else if(us_cnt == L_US_CNT_MAX) ms_cnt <= ms_cnt + 1'd1; else ms_cnt <= ms_cnt; always@(posedge sys_clk or negedge sys_rst_n) if(!sys_rst_n) s_cnt <= 10'd0; else if(us_cnt == L_US_CNT_MAX && ms_cnt == L_MS_CNT_MAX && s_cnt == L_S_CNT_MAX) s_cnt <= 10'd0; else if(us_cnt == L_US_CNT_MAX && ms_cnt == L_MS_CNT_MAX ) s_cnt <= s_cnt + 1'd1; else s_cnt <= s_cnt; reg breathe_status; always@(posedge sys_clk or negedge sys_rst_n) if(!sys_rst_n) breathe_status <= 1'd0; else if(us_cnt == L_US_CNT_MAX && ms_cnt == L_MS_CNT_MAX && s_cnt == L_S_CNT_MAX) breathe_status <= ~breathe_status; else breathe_status <= breathe_status; always@(posedge sys_clk or negedge sys_rst_n) if(!sys_rst_n) pwm_out <= 1'd0; else if((breathe_status == 1'd0 && ms_cnt > s_cnt) || (breathe_status == 1'd1 && ms_cnt < s_cnt)) pwm_out <= 1'd0; else pwm_out <= 1'd1; endmodule
|
上方代码为single channel的输出,再建立顶层模块即可实现4
chl的不同频率输出(参考ip核设计思路,不改动内部代码):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| module breathe_4chl_led( input wire sys_clk, input wire sys_rst_n, output wire [3:0] led_out ); breathe_led #( .G_US_CNT_MAX(6'd50), .G_MS_CNT_MAX(10'd1000), .G_S_CNT_MAX(10'd1000) ) breathe_led_inst0( .sys_clk(sys_clk), .sys_rst_n(sys_rst_n), .pwm_out(led_out[0]) ); breathe_led #( .G_US_CNT_MAX(6'd50), .G_MS_CNT_MAX(10'd700), .G_S_CNT_MAX (10'd700) ) breathe_led_inst1( .sys_clk(sys_clk), .sys_rst_n(sys_rst_n), .pwm_out(led_out[1]) ); breathe_led #( .G_US_CNT_MAX (6'd50), .G_MS_CNT_MAX (10'd500), .G_S_CNT_MAX (10'd500) ) breathe_led_inst2( .sys_clk(sys_clk), .sys_rst_n(sys_rst_n), .pwm_out(led_out[2]) ); breathe_led #( .G_US_CNT_MAX (6'd50), .G_MS_CNT_MAX (11'd2000), .G_S_CNT_MAX (11'd2000) ) breathe_led_inst3( .sys_clk(sys_clk), .sys_rst_n(sys_rst_n), .pwm_out(led_out[3]) ); endmodule
|
仿真结果
pwm_out的输出高电平时间均匀递增
呼吸灯仿真输出
状态机
状态机缩写FSM,又叫有限状态机,指状态的个数有限。根据是否受限于输入分为
- Moore型:最后输出状态至于当前状态有关,与输入无关
- Mealy型:最后输出状态至于当前状态有关,同时与输入有关
状态机的状态意为“事件”,A事件到B或C事件称为状态转移。完整的状态机需要具备:
- 输入:根据输入可以确定是否需要进行状态跳转以及输出,是影响状态机系统执行过程的重要驱动力;
- 输出:根据当前时刻的状态以及输入,是状态机系统最终要执行的动作;
- 状态:根据输入和上一状态决定当前时刻所处的状态,是状态机系统执行的一个稳定的过程。
状态图模型
单输入单输出状态机
功能需求
可乐机每次只能投入1枚1元硬币,且每瓶可乐卖3元钱,即投入3个硬币就可以让可乐机出可乐,如果投币不够3元想放弃投币需要按复位键,否则之前投入的钱不能退回。
详细设计
对该需求需要设计状态转移图,即IDLE、ONE、TWO三个投币状态,状态间的切换靠输入驱动,与当前状态和输入有关,如下Mealy状态机。
Mealy状态机模型
若再增加一个状态THREE,输出则可完全依照当前状态,即检测到为THREE状态后直接输出可乐,输入不影响输出。如下Moore型状态机。

Moore状态机模型
波形分析
此节的需求中,输入只有一个,状态根据输入变化。
Mealy状态机波形
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| module simple_FSM( input wire sys_clk, input wire sys_rst_n, input wire pi_money, output reg po_cola ); parameter IDLE = 3'b001; parameter ONE = 3'b010; parameter TWO = 3'b100; reg [2:0] state; always@(posedge sys_clk or negedge sys_rst_n) if(!sys_rst_n) state <= IDLE; else case (state) IDLE: if(pi_money == 1'b1) state <= ONE; else state <= IDLE; ONE: if(pi_money == 1'b1) state <= TWO; else state <= ONE; TWO: if(pi_money == 1'b1) state <= IDLE; else state <= TWO; default: state <= IDLE; endcase always@(posedge sys_clk or negedge sys_rst_n) if(!sys_rst_n) po_cola <= 1'b0; else if(state == TWO && pi_money == 1'b1) po_cola <= 1'b1; else po_cola <= 1'b0; endmodule
|
仿真分析
三状态仿真波形
多输入多输出状态机
功能需求
可乐定价为2.5元一瓶,可投入0.5元、1元硬币,投币不够2.5元需要按复位键退回钱款,投币超过2.5元需找零。
详细设计
先设计状态转移图,即IDLE、HALF、ONE、ONE_HALF、TWO、三个投币状态,状态间的切换靠输入驱动,与当前状态和输入有关,如下Mealy状态机。
确定输入:
确定输出:
Mealy状态转移图
波形分析
波形分析图
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
| module complex_FSM( input wire sys_clk, input wire sys_rst_n, input wire pi_money_one, input wire pi_money_half, output reg po_money, output reg po_cola ); wire [1:0] pi_money; assign pi_money = {pi_money_one,pi_money_half}; localparam IDLE = 5'b00001; localparam HALF = 5'b00010; localparam ONE = 5'b00100; localparam ONE_HALF = 5'b01000; localparam TWO = 5'b10000; localparam INPUT_HALF = 2'b01; localparam INPUT_ONE = 2'b10; reg [4:0] state; always@(posedge sys_clk or negedge sys_rst_n) if(!sys_rst_n) state <= IDLE; else case(state) IDLE: if(pi_money == INPUT_HALF) state <= HALF; else if(pi_money == INPUT_ONE) state <= ONE; else state <= IDLE; HALF: if(pi_money == INPUT_HALF) state <= ONE; else if(pi_money ==INPUT_ONE) state <= ONE_HALF; else state <= HALF; ONE: if(pi_money == INPUT_HALF) state <= ONE_HALF; else if(pi_money == INPUT_ONE) state <= TWO; else state <= ONE; ONE_HALF:if(pi_money == INPUT_HALF) state <= TWO; else if(pi_money ==INPUT_ONE) state <= IDLE; else state <= ONE_HALF; TWO: if(pi_money[1] != pi_money[0]) state <= IDLE; else state <= TWO; default: state <= IDLE; endcase always@(posedge sys_clk or negedge sys_rst_n) if(!sys_rst_n) po_cola <= 1'b0; else if((state == ONE_HALF && pi_money == INPUT_ONE) || (state == TWO && pi_money[1] != pi_money[0])) po_cola <= 1'b1; else po_cola <= 1'b0; always@(posedge sys_clk or negedge sys_rst_n) if(!sys_rst_n) po_money <= 1'b0; else if(state == TWO && pi_money == INPUT_ONE) po_money <= 1'b1; else po_money <= 1'b0;
endmodule
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| `timescale 1ns / 1ns
module tb_complex_FSM(
); reg sys_clk; reg sys_rst_n; reg [1:0] pi_money; wire po_cola; wire po_money; initial begin sys_clk = 1'b1; sys_rst_n <= 1'b0; #20 sys_rst_n <= 1'b1; end wire [4:0] state = complex_FSM_inst0.state; initial begin $timeformat(-9,0,"ns",6); $monitor("@time %t: pi_money_half = %b, pi_money_one = %b, state = %b,po_cola = %b, po_money = %b", $time ,pi_money[0],pi_money[1],state,po_cola,po_money); end always #10 sys_clk = ~sys_clk; always@(posedge sys_clk or negedge sys_rst_n) if(!sys_rst_n) pi_money <= 2'b00; else pi_money <= {$random}%3; complex_FSM complex_FSM_inst0( .sys_clk(sys_clk), .sys_rst_n(sys_rst_n), .pi_money_one(pi_money[1]), .pi_money_half(pi_money[0]), .po_money(po_money), .po_cola(po_cola) ); endmodule
|
仿真分析
仿真波形
最初在做仿真时,pi_money_half和pi_money_one在独立的always块内赋值,导致出现同时为1的意外情况。后续修正为pi_money
<= {$random}%3; 包含00、01、10三种情况。
综合应用
结合本篇博文的消抖、流水灯、状态机,可以搭建简单的小功能。
功能需求
实现可乐机,投币为0.5元、1元,达到2.5时出可乐,达到3时出可乐并找零。每次投币10s后无操作则回到初始态。每个投币状态由LED反馈,0-2元,分别亮起0-4个LED,2.5元为单向流水,3元时为双向流水,流水间隔0.5s。
详细设计
如果继续按照模块化设计思路,根据需求可拆解以下模块:
- 输入检测:确定是否有输入,输入是什么,并进行滤波
- 状态切换:确定什么时间复位,输出包含所有状态
- LED控制:根据当前状态进行LED亮灭

模块划分
波形分析
[1]输入检测:参考按键消抖一节,当监测到连续20ms输入时,输出1拍高电平
[2]状态机:相对上一节内容
- 取消cola和money的输出
- 新增FSM的state状态量输出,供后续模块调用
- 新增切换状态后10s复位计数器
[3]LED控制:
LED控制状态包括两种,固定排列常亮 +
指定间隔/方向闪烁,对两种分别进行波形逻辑绘制:

基本状态:N个LED常亮,红色虚线表示进入新状态
每uint_cnt,检测并切换LED状态。若在state !=
prev_state时切换状态,则uint_cnt复位,重新对新状态计时。
流水状态:间隔0.5s流水,红色虚线表示进入新状态
由于流水状态包含流水方向,需要1bit寄存器表示什么时候翻转。由于后续状态依靠flow_dir,因此需要提前打一拍翻转。
代码设计
代码查阅:点击此处
总结发现,真正耗时的时确定模块间的数据传递关系,比如:
- Key按键到FSM检测时输入的究竟是1拍高电平还是低电平:单模块仿真时未发现此问题,整体仿真异常才发现
- 状态参数究竟由谁控制:led的state仅前级传递,还是内部定义一个10s回到空闲态的计数器,应在划分模块时提前构思好
仿真分析
此处只放top层文件仿真结果:

仿真结果:模块间传输延时1拍
注意事项
仿真时必须修改tb文件中传递parameter参数的时,底层模块的局部参数才会变化,否则仿真时继续使用上一次的值,导致结果异常。
状态量设置
状态量类似C语言中的宏定义,常见的0、1、2….区分不同状态,在Verilog中也类似,以下三种形式都可以表示四种状态:
- 二进制值:2’b00,2’b01,2’b10,2’b11
- 独热码:4’b0001,4’b0010,4’b0100,4’b1000
- 格雷码:3’b000,3’b001,3’b011,3b’110,相邻两个编码只有一个bit不同
由于Verilog代码最后综合出的是电路,一个bit可以视为一个比较器。由于布局布线的差异多输入比较器可以会存在传输延迟,可能导致A输入在clk上升沿前,而B输出延迟2ns出现在clk上升沿之后,进而出现状态不稳的情况。格雷码相邻编码只有1bit不同,解决多输入传输延迟问题,同时节省部分资源。
FPGA寄存器资源较多,CPLD组合逻辑资源多,通常采用以下配置方式。

编码方式表
————————— End —————————
君子终日乾乾,夕惕若厉。–《周易·乾卦》