状态机基础:从概念到实战
大家好,我是你们的FPGA讲师。今天咱们聊聊状态机——这个在数字设计里绕不开的核心话题。说实话,我刚开始学FPGA那会儿,觉得状态机就是个花架子,直到第一次做串口协议解析,被时序折腾得够呛,才真正体会到它的重要性。
状态机的基本概念
状态机,说白了就是一个有记忆的逻辑系统。它不像组合逻辑那样,输入一变输出立马跟着变。状态机有内部状态,会根据当前状态和输入来决定下一步怎么走。
我习惯把状态机想象成一个自动售货机:你投币(输入),机器内部记录你投了多少钱(状态),然后决定能不能出货(输出)。这个比喻虽然简单,但核心思想全在里面了。
一个标准的状态机包含三个要素:
- 状态寄存器:存储当前状态,通常是D触发器阵列
- 次态逻辑:根据当前状态和输入,计算下一个状态
- 输出逻辑:根据当前状态(和输入),产生输出信号
核心要点:状态机本质上是时序逻辑 + 组合逻辑的组合体。时序部分负责记忆,组合部分负责决策。
Moore型与Mealy型状态机
这两种类型,我当年面试时被问过不下十次。它们的区别其实就一句话:输出跟谁有关。
Moore型状态机
输出只取决于当前状态。输入变化不会立刻影响输出,必须等到下一个时钟沿状态更新后,输出才会变。
举个例子,我做过一个交通灯控制器,用的就是Moore型。红灯、绿灯、黄灯各是一个状态,输出直接由状态决定。这样设计的好处是输出稳定,不会因为输入毛刺而抖动。
我的经验:Moore型适合输出需要干净、无毛刺的场景。比如控制LED灯、驱动外部设备,我一般优先选Moore型。
Mealy型状态机
输出同时取决于当前状态和输入。输入一变,输出可能立刻跟着变,不需要等时钟沿。
我曾经做过一个SPI从机接收模块,用的就是Mealy型。因为需要在接收数据的同一拍就产生响应信号,等下一个时钟沿就来不及了。
| 对比项 | Moore型 | Mealy型 |
|---|---|---|
| 输出依赖 | 仅当前状态 | 当前状态 + 输入 |
| 输出时序 | 时钟沿后变化 | 输入变化即变 |
| 状态数量 | 通常较多 | 通常较少 |
| 抗毛刺能力 | 强 | 弱 |
| 典型应用 | 控制类、显示类 | 通信协议、高速接口 |
避坑指南:我曾经在一个项目里用Mealy型做按键消抖,结果输出毛刺满天飞。后来改成Moore型,问题立刻解决。记住:输入有毛刺风险时,慎用Mealy型。
状态转移图绘制方法
画状态转移图,是设计状态机的第一步。我见过不少新手,上来就写代码,结果写到一半发现状态跳转逻辑混乱,回头再改图,浪费大量时间。
我的习惯是:先画图,再写代码。图画清楚了,代码就是翻译工作。
绘制步骤
- 确定状态:列出所有可能的状态,给每个状态起个有意义的名字
- 确定转移条件:什么情况下从A状态跳到B状态
- 确定输出:每个状态下输出什么信号
- 画图:用圆圈表示状态,箭头表示转移,标注条件和输出
下面是我用SVG画的一个简单状态转移图,展示了一个四状态Moore型状态机的结构:
画图时有个小技巧:先画主干,再补细节。先把主要的状态跳转画出来,比如上电复位后进哪个状态,正常流程怎么走。然后再补异常情况,比如超时、错误处理等。
我的习惯:用不同颜色区分状态类型。比如空闲状态用蓝色,工作状态用绿色,错误状态用红色。这样一眼就能看出状态机的整体结构。
一个简单的Verilog示例
光说不练假把式。下面是一个Moore型状态机的Verilog代码,实现了上面那个四状态的状态机:
module fsm_example(
input clk,
input rst_n,
input start,
input ready,
input done,
output reg [1:0] out
);
// 状态编码
localparam S0 = 2'b00; // IDLE
localparam S1 = 2'b01; // START
localparam S2 = 2'b10; // RUN
localparam S3 = 2'b11; // DONE
reg [1:0] state, next_state;
// 状态寄存器
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
state <= S0;
else
state <= next_state;
end
// 次态逻辑
always @(*) begin
case (state)
S0: next_state = start ? S1 : S0;
S1: next_state = ready ? S2 : S1;
S2: next_state = done ? S3 : S2;
S3: next_state = S0;
default: next_state = S0;
endcase
end
// 输出逻辑(Moore型)
always @(*) begin
case (state)
S0: out = 2'b00;
S1: out = 2'b01;
S2: out = 2'b10;
S3: out = 2'b11;
default: out = 2'b00;
endcase
end
endmodule
这段代码我用了三段式写法。为什么用三段式?因为组合逻辑和时序逻辑分开,仿真和调试都方便。我早期写状态机喜欢用两段式,后来发现三段式更清晰,就改过来了。
注意:次态逻辑和输出逻辑一定要用组合逻辑(always @(*)),别写成时序逻辑。否则状态机会慢一拍,时序就乱了。我曾经在这个坑里栽过跟头,查了两天才发现是输出逻辑用了posedge clk。
好了,状态机的基础知识就聊到这儿。记住三个核心:状态、转移、输出。画好图,选对类型,写代码就是水到渠成的事。
公众号:蓝海资料掘金营,微信deep3321