状态机基础:从概念到实战

大家好,我是你们的FPGA讲师。今天咱们聊聊状态机——这个在数字设计里绕不开的核心话题。说实话,我刚开始学FPGA那会儿,觉得状态机就是个花架子,直到第一次做串口协议解析,被时序折腾得够呛,才真正体会到它的重要性。

状态机的基本概念

状态机,说白了就是一个有记忆的逻辑系统。它不像组合逻辑那样,输入一变输出立马跟着变。状态机有内部状态,会根据当前状态和输入来决定下一步怎么走。

我习惯把状态机想象成一个自动售货机:你投币(输入),机器内部记录你投了多少钱(状态),然后决定能不能出货(输出)。这个比喻虽然简单,但核心思想全在里面了。

一个标准的状态机包含三个要素:

  • 状态寄存器:存储当前状态,通常是D触发器阵列
  • 次态逻辑:根据当前状态和输入,计算下一个状态
  • 输出逻辑:根据当前状态(和输入),产生输出信号

核心要点:状态机本质上是时序逻辑 + 组合逻辑的组合体。时序部分负责记忆,组合部分负责决策。

Moore型与Mealy型状态机

这两种类型,我当年面试时被问过不下十次。它们的区别其实就一句话:输出跟谁有关

Moore型状态机

输出只取决于当前状态。输入变化不会立刻影响输出,必须等到下一个时钟沿状态更新后,输出才会变。

举个例子,我做过一个交通灯控制器,用的就是Moore型。红灯、绿灯、黄灯各是一个状态,输出直接由状态决定。这样设计的好处是输出稳定,不会因为输入毛刺而抖动。

我的经验:Moore型适合输出需要干净、无毛刺的场景。比如控制LED灯、驱动外部设备,我一般优先选Moore型。

Mealy型状态机

输出同时取决于当前状态和输入。输入一变,输出可能立刻跟着变,不需要等时钟沿。

我曾经做过一个SPI从机接收模块,用的就是Mealy型。因为需要在接收数据的同一拍就产生响应信号,等下一个时钟沿就来不及了。

对比项 Moore型 Mealy型
输出依赖 仅当前状态 当前状态 + 输入
输出时序 时钟沿后变化 输入变化即变
状态数量 通常较多 通常较少
抗毛刺能力
典型应用 控制类、显示类 通信协议、高速接口

避坑指南:我曾经在一个项目里用Mealy型做按键消抖,结果输出毛刺满天飞。后来改成Moore型,问题立刻解决。记住:输入有毛刺风险时,慎用Mealy型。

状态转移图绘制方法

画状态转移图,是设计状态机的第一步。我见过不少新手,上来就写代码,结果写到一半发现状态跳转逻辑混乱,回头再改图,浪费大量时间。

我的习惯是:先画图,再写代码。图画清楚了,代码就是翻译工作。

绘制步骤

  1. 确定状态:列出所有可能的状态,给每个状态起个有意义的名字
  2. 确定转移条件:什么情况下从A状态跳到B状态
  3. 确定输出:每个状态下输出什么信号
  4. 画图:用圆圈表示状态,箭头表示转移,标注条件和输出

下面是我用SVG画的一个简单状态转移图,展示了一个四状态Moore型状态机的结构:

S0 IDLE 输出: 00 S1 START 输出: 01 S2 RUN 输出: 10 S3 DONE 输出: 11 start=1 ready=1 done=1 reset=1 start=0 ready=0 done=0 reset=0 图例 状态节点 转移箭头 条件标注 输出标注

画图时有个小技巧:先画主干,再补细节。先把主要的状态跳转画出来,比如上电复位后进哪个状态,正常流程怎么走。然后再补异常情况,比如超时、错误处理等。

我的习惯:用不同颜色区分状态类型。比如空闲状态用蓝色,工作状态用绿色,错误状态用红色。这样一眼就能看出状态机的整体结构。

一个简单的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