状态机是FPGA编程必学内容之一,因为状态机在项目用的特别多。
那为什么状态机这么重要呢? 在写这篇blog之前,搜到CSDN一位大佬的博客,有一句话令我醍醐灌顶:
“FPGA不同于CPU的一点特点就是CPU是顺序执行的,而FPGA是同步执行(并行)的。那么FPGA如何处理明显具有时间上先后顺序的事件呢?这个时候我们就需要使用到状态机了。” —— https://blog.csdn.net/wuzhikaidetb/article/details/119421783
以前总是用C语言编程,顺序执行的逻辑已深入人心,而状态机就能够实现顺序执行的逻辑。
状态机的每一个状态代表一个事件,从执行当前事件到执行另一事件我们称之为状态的跳转或状态的转移,状态机通过控制各个状态跳转来控制流程,使得整个代码看上去更加清晰易懂,在控制复杂流程的时候,状态机优势明显。
文章来源地址https://uudwc.com/A/RxNq8
1 状态机分类及我的选择
1.1 根据状态机的输出与输入条件相关与否,可分为Moore和Mealy型。
Mealy 状态机:输出不仅取决于当前状态,还取决于输入状态。
Moore 状态机:组合逻辑的输出只取决于当前状态,而与输入状态无关。
Mealy 状态机比Moore状态机的状态个数要少
Mealy 状态机比Moore状态机的输出要早一个时钟周期
可见,从代码的理解,可读性来讲, Moore的状态要全一点。从程序的运行效率来讲,Mealy的输出要比Moore早一个时钟周期,效率更高。
1.2 根据状态机的代码写法,可分为一段式、两段式、三段式。
一段式状态机:整个状态机写到一个 always模块里面,在该模块中既描述状态转移,又描述状态的输入和输出。
二段式状态机:用两个 always 模块来描述状态机,其中一个 always 模块采用同步时序描述状态转移;另一个模块采用组合逻辑判断状态转移条件,描述状态转移规律以及输出。不同于一段式状态机的是,它需要定义两个状态,现态和次态,然后通过现态和次态的转换来实现时序逻辑。
三段式状态机:在两个 always 模块描述方法基础上,使用三个always 模块,一个always 模块采用同步时序描述状态转移,一个 always 采用组合逻辑判断状态转移条件,描述状态转移规律,另一个 always 模块描述状态输出。
1.3 我的选择
一段式的状态机不好维护,我接手项目看到别人的一段式状态机头都大了,所以我不会选择写一段式。 二段式的状态机有点低不成高不就感觉,除非输出和状态的关系特别简单(比如本文的例子就是,经典的3块钱可乐问题也是),否则不建议使用。因此一般来讲,还是用三段式状态机,好维护。综上,我后续的代码开发主打一手 Moore三段式 状态机, 暂不考虑效率问题,主要还是要让代码写的易读,好维护。
2 样例介绍
开发板上有两个按钮,两个led灯。 一个按钮做复位按钮。 另一个按钮作为控制两个LED灯的输入。 按钮每按下一次,LED的值加一, 00 01 10 11 00 如此循环。按钮按下释放的过程,有5个状态, 空闲(复位状态)→按钮未按下 → 按下的抖动 → 按钮已按下 → 释放抖动 → 按钮未按下。
在开关按下或者释放的时候,都会发生抖动,比如按钮按下的时候,我们可以对按键处于低电平做一个累加计数。 比如当我一直处于低电平累计计数20ms了,那我认为你已经按键按下了,稳定了。 那在这个还没有累积到20ms的阶段呢,我们认为它还在不稳定的状态、抖动状态,JITIER1。释放按钮同理。
我们的输出是什么? 当我的按钮按下一次, 我的LED灯就会加一。 复位状态下是两个灯都不亮。 按第一次,低位灯亮,高位灯灭。 按第二次,高位灯亮,低位灯灭。 按第三次,高位灯和地位灯都亮,按第四次,两个灯都灭。如此循环。
3 样例代码1
以下是我根据样例的理解,写的相关代码
`timescale 1ns / 1ps
module key_fsm(
input wire clk , // 时钟
input wire rst_n , // 复位信号
input wire key_in , // 按键输入
output wire [1:0] led // LED灯输出
);
//==================================================================
// Parameter define
//==================================================================
localparam IDLE = 5'b00001;
localparam BUTTON_UP = 5'b00010;
localparam JUTTER1 = 5'b00100;
localparam BUTTON_DOWN = 5'b01000;
localparam JUTTER2 = 5'b10000;
localparam CNT_MAX = 1_000_000 - 1; // 20ms
//==================================================================
// Internal Signals
//==================================================================
reg [4:0] key_state ; // 按钮状态
reg [4:0] next_state ; // 下一个状态
reg key_d ; // 按钮输入延迟,打一拍 消抖
reg key_dd ; // 按钮输入延迟,打两拍 消抖
reg [31:0] cnt_jutter1 ; // 抖动1计数
reg [31:0] cnt_jutter2 ; // 抖动2计数
reg key_flag ; // 按钮按下标志
reg [1:0] led_r ; // LED灯输出
always @(posedge clk or negedge rst_n) begin
if (rst_n == 1'b0) begin
key_state <= IDLE;
end
else begin
key_state <= next_state;
end
end
always @(*) begin
case(key_state)
IDLE:begin
if(rst_n == 1'b0) begin
next_state <= IDLE;
end
else begin
next_state <= BUTTON_UP;
end
end
BUTTON_UP:begin
if (key_dd == 1'b0) begin
next_state <= JUTTER1;
end
else begin
next_state <= BUTTON_UP;
end
end
JUTTER1:begin
if (key_dd == 1'b1) begin
next_state <= BUTTON_UP;
end
else if (cnt_jutter1 == CNT_MAX) begin
next_state <= BUTTON_DOWN;
end
else begin
next_state <= JUTTER1;
end
end
BUTTON_DOWN:begin
if (key_dd == 1'b1) begin
next_state <= JUTTER2;
end
else begin
next_state <= BUTTON_DOWN;
end
end
JUTTER2:begin
if (key_dd == 1'b0) begin
next_state <= BUTTON_DOWN;
end
else if (cnt_jutter2 == CNT_MAX) begin
next_state <= BUTTON_UP;
end
else begin
next_state <= JUTTER2;
end
end
default:begin // 避免锁存器
next_state <= IDLE;
end
endcase
end
//----------------------------- key_d -----------------------------
always @(posedge clk or negedge rst_n) begin
if (rst_n == 1'b0) begin
key_d <= 1'b1;
end
else begin
key_d <= key_in;
end
end
//----------------------------- key_dd -----------------------------
always @(posedge clk or negedge rst_n) begin
if (rst_n == 1'b0) begin
key_dd <= 1'b1;
end
else begin
key_dd <= key_d;
end
end
//----------------------------- cnt_jutter1 -----------------------------
always @(posedge clk or negedge rst_n) begin
if (rst_n == 1'b0) begin
cnt_jutter1 <= 'd0;
end
else if (key_state == JUTTER1 && key_dd == 1'b0) begin
if (cnt_jutter1 == CNT_MAX) begin
cnt_jutter1 <= 'd0;
end
else begin
cnt_jutter1 <= cnt_jutter1 + 1'b1;
end
end
else begin
cnt_jutter1 <= 'd0;
end
end
//----------------------------- cnt_jutter2 -----------------------------
always @(posedge clk or negedge rst_n) begin
if (rst_n == 1'b0) begin
cnt_jutter2 <= 'd0;
end
else if (key_state == JUTTER2 && key_dd == 1'b1) begin
if(cnt_jutter2 == CNT_MAX) begin
cnt_jutter2 <= 'd0;
end
else begin
cnt_jutter2 <= cnt_jutter2 + 1'b1;
end
end
else begin
cnt_jutter2 <= 'd0;
end
end
//----------------------------- key_flag -----------------------------
always @(posedge clk or negedge rst_n) begin
if (rst_n == 1'b0) begin
key_flag <= 1'b0;
end
else if(cnt_jutter1 == CNT_MAX && key_state == JUTTER1) begin
key_flag <= 1'b1;
end
else begin
key_flag <= 1'b0;
end
end
//----------------------------- led_r -----------------------------
always @(posedge clk or negedge rst_n) begin
if (rst_n == 1'b0) begin
led_r <= 2'b00;
end
else if (key_flag == 1'b1) begin
led_r <= led_r + 1'b1;
end
else begin
led_r <= led_r;
end
end
assign led = ~led_r;
// 这个状态机, 没有第三段,输出只和当前的状态有关,和当前的输入无关。
endmodule
写完运行也成功了,但是却更迷茫了,这个功能用状态机来实现, 输出仅仅和其中一个状态有关联。 没有专门的always模块来描述各种状态系统的输出,这使得我心里那一关过不去, 真的,有这个必要吗? 这一定是我的打开方式不对。
4 样例代码2
苦思冥想后,我决定重新定义这个功能模块状态机的状态。 IDLE(VALUE0) → VALUE1 → VALUE2 → VALUE3。 每个状态都对应一种输出。 复位和默认状态为IDLE,当按键按下一次,触发状态向下一个状态转移。如果没有按下,则保持当前状态。
代码如下:
`timescale 1ns / 1ps
module key_fsm(
input wire clk , // 时钟
input wire rst_n , // 复位信号
input wire key_in , // 按键输入
output wire [1:0] led // LED输出
);
//==================================================================
// Parameter define
//==================================================================
localparam IDLE = 4'b0001;
localparam VALUE1 = 4'b0010;
localparam VALUE2 = 4'b0100;
localparam VALUE3 = 4'b1000;
localparam CNT_MAX = 1000_000 - 1; // 20ms
//==================================================================
// Internal Signals
//==================================================================
reg [3:0] state ;
reg [3:0] next_state ;
reg [31:0] cnt ;
reg key_d ;
reg key_dd ;
reg flag ;
reg key_flag ;
reg [1:0] led_r ;
assign led = ~led_r ;
//----------------------------- FSM_Part1 -----------------------------
always @(posedge clk or negedge rst_n) begin
if (rst_n == 1'b0) begin
state <= IDLE;
end
else begin
state <= next_state;
end
end
//----------------------------- FSM_Part2 -----------------------------
always @(*) begin
case(state)
IDLE:begin
if (key_flag == 1'b1) begin
next_state <= VALUE1;
end
else begin
next_state <= IDLE;
end
end
VALUE1:begin
if (key_flag == 1'b1) begin
next_state <= VALUE2;
end
else begin
next_state <= VALUE1;
end
end
VALUE2:begin
if (key_flag == 1'b1) begin
next_state <= VALUE3;
end
else begin
next_state <= VALUE2;
end
end
VALUE3:begin
if (key_flag == 1'b1) begin
next_state <= IDLE;
end
else begin
next_state <= VALUE3;
end
end
default:begin // 避免锁存器
next_state <= IDLE;
end
endcase
end
//----------------------------- FSM_Part3 -----------------------------
always @(posedge clk or negedge rst_n) begin
if (rst_n == 1'b0) begin
led_r <= 2'b00;
end
else begin
case(state)
IDLE:begin
led_r <= 2'b00;
end
VALUE1:begin
led_r <= 2'b01;
end
VALUE2:begin
led_r <= 2'b10;
end
VALUE3:begin
led_r <= 2'b11;
end
default:begin
led_r <= 2'b00;
end
endcase
end
end
//----------------------------- key_d -----------------------------
always @(posedge clk or negedge rst_n) begin
if (rst_n == 1'b0) begin
key_d <= 1'b1;
end
else begin
key_d <= key_in;
end
end
//----------------------------- key_dd -----------------------------
always @(posedge clk or negedge rst_n) begin
if (rst_n == 1'b0) begin
key_dd <= 1'b1;
end
else begin
key_dd <= key_d;
end
end
//----------------------------- cnt -----------------------------
always @(posedge clk or negedge rst_n) begin
if (rst_n == 1'b0) begin
cnt <= 'd0;
end
else if( key_dd == 1'b0) begin
if (cnt == CNT_MAX) begin
cnt <= 'd0;
end
else begin
cnt <= cnt + 1'b1;
end
end
else begin
cnt <= 'd0;
end
end
//----------------------------- flag -----------------------------
always @(posedge clk or negedge rst_n) begin
if (rst_n == 1'b0) begin
flag <= 1'b0;
end
else if(cnt == CNT_MAX) begin
flag <= 1'b1;
end
else if(key_dd == 1'b1) begin
flag <= 1'b0;
end
else begin
flag <= flag;
end
end
//----------------------------- key_flag -----------------------------
always @(posedge clk or negedge rst_n) begin
if (rst_n == 1'b0) begin
key_flag <= 1'b0;
end
else if (cnt == CNT_MAX && flag == 1'b0) begin
key_flag <= 1'b1;
end
else begin
key_flag <= 1'b0;
end
end
endmodule
可以看到,修改后,代码的逻辑变得更加清晰, 三段式的状态机,三个部分都有。 剩余的所有代码,我都是为了去得到状态机中,用于转移状态的key_flag变量的计算。
例子其实很简单,同样的功能,我的上一篇文章:FPGA_学习_07_按键消抖也实现了,代码量更少,更清爽。 本文主要是为了去探讨三段式状态机。文章来源:https://uudwc.com/A/RxNq8