FPGA-04-Verilog语法

本文记录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
//需求:led 0.5s亮,1.5s灭
module Counter(
input wire sys_clk,
input wire sys_rst_n,

output reg led_out
);

parameter CNT_MAX = 25'd24_999_999; //计数值为0.5s

reg [24:0] cnt; //计数器-0.5s
//reg cnt_flag; //到达0.5s标志
reg [1:0] half_s_cnt; //0.5s个数

//计数块
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;

//计算第几个0.5s,用于做不同时长闪烁
always@(posedge sys_clk or negedge sys_rst_n)
if(!sys_rst_n)
half_s_cnt <= 2'd0; //或者此处复位设置为3
else if(cnt == CNT_MAX - 1)
half_s_cnt <= half_s_cnt + 1'b1;
else
half_s_cnt <= half_s_cnt;

//led控制块
always@(posedge sys_clk or negedge sys_rst_n)
if(!sys_rst_n)
led_out <= 1'b0;
else if(half_s_cnt == 2'd0)
//放在首个亮时,由于half_s_cnt复位后为0,与cnt同一拍开始计数,所以上电后第一个亮会为480ms
//因此,可将亮起的位置放在后侧
led_out <= 1'b1;
else
led_out <= 1'b0;

//parameter CNT_MAX = 25'd24_999_999; //50MHz Clock,计数0.5s
//localparam CNT_MAX = 25'd24_999_999; //局部参数,实例化时不可从外部使用
endmodule
image can't load. half_s_cnt == 2’d0 复位后首次丢失一拍
image can't load. half_s_cnt == 2’d3,复位后时间正常

两张图的差异在于,led_out默认输出与第一次状态不同。对第一张图,在rst = 1后,cnt和led同时翻转,因此出现一拍丢失。若希望使用half_s_cnt == 2’d0,而不出现丢拍,则需要把复位后的led状态拉高。

分频器

Vivado自带的IP核为锁相环,通过PLL由一个输入时钟得到多个不同频率的子时钟。分频器细分为奇分频与偶分频。分频器本质与计数器相同,即计数到某一值输出翻转或输出Flag标志。

偶分频

偶分频可以sys_clk上升沿为为基准,计数刚好对应的分频系数的一半时,对信号翻转。

image can't load. 偶分频(双边沿输出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:

  • 可直接在后续alway块中做参考

    1
    2
    3
    4
    5
    6
    reg [1:0] a;
    always@(posedge clk_out or negedge sys_rst_n)
    if(!sys_rst_n)
    a <= 2'd0;
    else
    a <= a + 1'b1;

  • 不属于FPGA全局时钟,高速时钟下不精确

为解决高速时钟下不精确问题,将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;

仿真结果如下:

image can't load. 偶分频仿真结果

产生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在新时钟的上升沿下降沿均加1
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
//偶分频模块
//采用flag信号
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 : //单边沿flag
DIVIDER/2 -1 ; //双边沿flag
//-------------------分频计数器-------------------
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
image can't load. 偶分频(双边沿标志)仿真结果

奇分频

偶分频因为分频系数为偶数,最小为二分频,恰好对齐ys_clk上升沿(本节末尾放置使用下降沿的通用分频器)。若对时钟进行奇分频,欲求找到系数一半m/2-1的上升沿是不可能的,但是可引入下降沿为标准,波形如下:

image can't load. 奇分频波形

因此,代码如下:

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
image can't load. 奇数分频仿真结果

显然always块中增加对下降沿的判断可以快速精准的找到分频结果,但是:

  • 双边沿检测综合更复杂、时序约束复杂
  • 不适合合并到仅使用上升沿判断的通用分频模块中

因此可将上升沿下降沿分别放在两个块内,计数到分频值的一半时翻转,将输出信号相或即可得到输出:

image can't load. 奇分频波形

代码块如下:

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
image can't load. 奇分频仿真波形

通用分频

既然上方两种分频方式可产生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; //flag reverse count
localparam DIV_REF_CNT = DIVIDER - 1; //reference value to clear the count

//-------------------分频计数器-------------------
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
image can't load. 通用分频仿真波形

按键消抖

按键是电路板上常用的给出特殊高低电平的器件,单一使用按键时可能出现按下/弹起时的波形抖动,称为前抖动/后抖动。常用的解决办法是:

  • 硬件上采用RS触发器、外接滤波电容
  • 软件上采用延时算法
image can't load. 按键抖动模型

按键按下的状态分为5个状态:

  • A、E:按下/弹起后的稳定状态
  • B:按下时的不稳定状态,持续时间不定
  • C:按下后的稳定状态
  • D:弹起时的不稳定状态,持续时间不定

功能需求

针对指定引脚,检测到连续20ms低电平时,输出1拍低电平

详细设计

针对输入信号,当检测到低电平时计数器开始计数。若遇到高电平,则立刻清0。当达到给定值时输出低电平脉冲,随后计数值保持,直到按键释放。

image can't load. 方案设计

上图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) //A/E高电平阶段
key_in <= 1'd1;
else if(clk_cnt >=50 && clk_cnt <100) //C低电平阶段
key_in <= 1'd0;
else //B/D不稳定阶段
key_in <= $random%2;

仿真结果

image can't load. 仿真结果

流水灯

功能需求

根据参数,调整4路LED流水灯输出,每间隔固定时间切换下一个灯亮。

详细设计

在参数给定的情况下,只需设定计数器,计数到给定值时刷新输出,即方案A。此种情况无法保证复位按下时的初始状态全灭/全亮。

进一步,如果希望初始状态为4’b0000/4’b1111,和运行时仅1bit为1或0的状态并不一致。因此需要提前打1拍增加flag标志(参考分频器一节,提前1拍打出flag),在检测到flag拉高时根据当前led状态决定是移位或是刷新初值(led_out == 4b1000)。

如果采用位拼接,只需要在flag拉高时刷新初值。

image can't load. 方案设计

代码实现

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; //LED light hold time

//-------------------局部配置参数-------------------
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;

//方案B-移位
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;
//方案A--位拼接
/*
always@(posedge sys_clk or negedge sys_rst_n)
if(!sys_rst_n)
led_out <= L_LED_INIT;
else if(clk_cnt == L_LIGHT_HD_TIME)
led_out <= {led_out[2:0],led_out[3]}; //位拼接
else
led_out <= led_out;
*/
endmodule

仿真结果

image can't load.

方案A的不再赘述。对上述方案B,复位后的第一个状态会丢1拍,在计数器一节尾部已经做了分析。

呼吸灯

流水灯是在指定时间间隔是切换状态,呼吸灯为达到呼吸效果则通过控制占空比(PWM)达到不同程度的亮度效果(引脚直接控制电压显然比较困难)。

  • 在不同时间段,不同占空比呈现不同亮度。

功能需求

给定参数T下,实现均匀的亮,灭效果,整个周期为2T。

详细设计

假设有1个较短的时间片,根据PWM的原理,此时间片内,高电平持续时间越长,呈现的亮度越亮。通过对相同长度的时间片设置递增的高电平时长,可以“呼吸”式亮起/灭下。

参照其他已有设计,可以1us为基准时间单位,以N个单位设置上一级/上上一级时间片。

若如此设置,单个亮/灭状态的周期 T = N2 us。

将两级时间片的单位设置为相同,程序内方便以当前循环周期设定亮灭。

image can't load. 呼吸灯波形时序

上方是整体的逻辑,N = 1000,标准的1s亮/灭。单独以其中一个为例,将计数值简化t1 = t2 = t3 = 4。

四个led_out从上至下为连续4个cnt_t3下的输出,呈现如下态势:

image can't load. 单周期波形

可发现: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, //cnt_t1
parameter G_MS_CNT_MAX = 10'd1000, //cnt_t2
parameter G_S_CNT_MAX = 10'd1000 //cnt_t3,参数值由外部输入,便于仿真观察
)
(
input wire sys_clk,
input wire sys_rst_n,

output reg pwm_out
);

//-------------- 全局参数 -------------
//parameter G_US_CNT_MAX = 6'd50, //全局参数也可写在内部
//parameter G_MS_CNT_MAX = 10'd1000,
//parameter G_S_CNT_MAX = 10'd1000,

//-------------- 局部参数 -------------
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; //此处注意,位宽需要结合实际值,若希望计数129,则6bit是不够的
reg [10:0] ms_cnt;
reg [10:0] s_cnt;

//-------------- us计数器 -------------
always@(posedge sys_clk or negedge sys_rst_n) //用于产生基准us计数器
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;

//-------------- ms计数器 -------------
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;

//-------------- s计数器 -------------
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;

//-------------- PWM输出 -------------
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的输出高电平时间均匀递增

image can't load. 呼吸灯仿真输出

状态机

状态机缩写FSM,又叫有限状态机,指状态的个数有限。根据是否受限于输入分为

  • Moore型:最后输出状态至于当前状态有关,与输入无关
  • Mealy型:最后输出状态至于当前状态有关,同时与输入有关

状态机的状态意为“事件”,A事件到B或C事件称为状态转移。完整的状态机需要具备:

  • 输入:根据输入可以确定是否需要进行状态跳转以及输出,是影响状态机系统执行过程的重要驱动力;
  • 输出:根据当前时刻的状态以及输入,是状态机系统最终要执行的动作;
  • 状态:根据输入和上一状态决定当前时刻所处的状态,是状态机系统执行的一个稳定的过程。
image can't load. 状态图模型

单输入单输出状态机

功能需求

可乐机每次只能投入1枚1元硬币,且每瓶可乐卖3元钱,即投入3个硬币就可以让可乐机出可乐,如果投币不够3元想放弃投币需要按复位键,否则之前投入的钱不能退回。

详细设计

对该需求需要设计状态转移图,即IDLE、ONE、TWO三个投币状态,状态间的切换靠输入驱动,与当前状态和输入有关,如下Mealy状态机。

image can't load. Mealy状态机模型

若再增加一个状态THREE,输出则可完全依照当前状态,即检测到为THREE状态后直接输出可乐,输入不影响输出。如下Moore型状态机。

image can't load.
Moore状态机模型

波形分析

此节的需求中,输入只有一个,状态根据输入变化。

image can't load. 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

仿真分析

image can't load. 三状态仿真波形

多输入多输出状态机

功能需求

可乐定价为2.5元一瓶,可投入0.5元、1元硬币,投币不够2.5元需要按复位键退回钱款,投币超过2.5元需找零。

详细设计

先设计状态转移图,即IDLE、HALF、ONE、ONE_HALF、TWO、三个投币状态,状态间的切换靠输入驱动,与当前状态和输入有关,如下Mealy状态机。

确定输入:

  • 0.5元
  • 1元

确定输出:

  • 是否找零
  • 是否出可乐
image can't load. Mealy状态转移图

波形分析

image can't load. 波形分析图

代码实现

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

//--------------- 引用仿真实例inst0内的量
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
//--------------- 50MHz时钟
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

仿真分析

image can't load. 仿真波形

最初在做仿真时,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亮灭
image can't load.
模块划分

波形分析

[1]输入检测:参考按键消抖一节,当监测到连续20ms输入时,输出1拍高电平

[2]状态机:相对上一节内容

  • 取消cola和money的输出
  • 新增FSM的state状态量输出,供后续模块调用
  • 新增切换状态后10s复位计数器

[3]LED控制:

LED控制状态包括两种,固定排列常亮 + 指定间隔/方向闪烁,对两种分别进行波形逻辑绘制:

image can't load.
基本状态:N个LED常亮,红色虚线表示进入新状态

每uint_cnt,检测并切换LED状态。若在state != prev_state时切换状态,则uint_cnt复位,重新对新状态计时。

image can't load. 流水状态:间隔0.5s流水,红色虚线表示进入新状态

由于流水状态包含流水方向,需要1bit寄存器表示什么时候翻转。由于后续状态依靠flow_dir,因此需要提前打一拍翻转。

代码设计

代码查阅:点击此处

总结发现,真正耗时的时确定模块间的数据传递关系,比如:

  • Key按键到FSM检测时输入的究竟是1拍高电平还是低电平:单模块仿真时未发现此问题,整体仿真异常才发现
  • 状态参数究竟由谁控制:led的state仅前级传递,还是内部定义一个10s回到空闲态的计数器,应在划分模块时提前构思好

仿真分析

此处只放top层文件仿真结果:

image can't load.
仿真结果:模块间传输延时1拍

注意事项

  1. 仿真时必须修改tb文件中传递parameter参数的时,底层模块的局部参数才会变化,否则仿真时继续使用上一次的值,导致结果异常。

  2. 状态量设置

    状态量类似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组合逻辑资源多,通常采用以下配置方式。

    image can't load.
    编码方式表

————————— End —————————

君子终日乾乾,夕惕若厉。–《周易·乾卦》