4. 有限状态机基础:状态机概念、Moore型与Mealy型状态机、状态转移图、状态编码

各位同学,今天我们来聊聊有限状态机。说实话,这是时序控制器设计的灵魂。我做了十几年数字IC,见过太多因为状态机没写好导致芯片翻车的案例。你想想看,一个复杂的控制逻辑,如果没有状态机来管理,代码会乱成一锅粥。

4.1 什么是有限状态机?

有限状态机,简称FSM。说白了,就是一个系统在有限个状态之间跳来跳去。每个状态代表系统当前所处的某种情况,根据输入信号决定下一步去哪。

举个最简单的例子——电梯门。它有“开门中”、“已开门”、“关门中”、“已关门”四个状态。你按开门按钮,它就从“已关门”跑到“开门中”。这不就是状态机吗?

核心三要素:

  • 状态集合:系统所有可能的状态,有限个
  • 转移条件:什么情况下从一个状态跳到另一个
  • 输出:每个状态下系统做什么

我在一个MCU项目中,状态机写了32个状态。刚开始觉得太多了,后来发现每个状态职责清晰,调试起来反而比那些“万能状态”好使。

4.2 Moore型 vs Mealy型状态机

这两种类型,是面试必考题。我当年也被问过,答得磕磕巴巴的。现在给你讲清楚。

4.2.1 Moore型状态机

Moore型的特点是:输出只取决于当前状态。跟输入没关系。

打个比方:你到了一个十字路口,红灯就停,绿灯就走。这个“停”或“走”只取决于当前是红灯还是绿灯状态,跟你之前怎么来的没关系。

// Moore型状态机示例
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        state <= IDLE;
    else
        state <= next_state;
end

always @(*) begin
    case (state)
        IDLE:   next_state = (start) ? RUN : IDLE;
        RUN:    next_state = (done)  ? IDLE : RUN;
        default: next_state = IDLE;
    endcase
end

// 输出只跟当前状态有关
assign out = (state == RUN);

我的经验:Moore型状态机更安全。输出不会因为输入毛刺而抖动。我在做电源管理芯片时,所有状态机都用Moore型,因为输出直接控制功率管,不能乱跳。

4.2.2 Mealy型状态机

Mealy型就不一样了:输出同时取决于当前状态和输入

还是电梯的例子。你按了关门按钮,电梯门开始关。但如果有人挡在中间(输入),门立刻又打开。这个“开门”动作,既取决于当前“关门中”的状态,也取决于“有人挡门”这个输入。

// Mealy型状态机示例
always @(*) begin
    case (state)
        IDLE: begin
            next_state = (start) ? RUN : IDLE;
            out = (start) ? 1'b1 : 1'b0;  // 输出依赖输入
        end
        RUN: begin
            next_state = (done) ? IDLE : RUN;
            out = (done) ? 1'b0 : 1'b1;
        end
    endcase
end

注意:Mealy型状态机输出可能产生毛刺。因为输入变化会立刻影响输出,组合逻辑路径上的竞争可能导致短暂错误。我曾经在一个通信协议控制器里用了Mealy型,结果输出信号被采样到错误值,查了两天才发现是毛刺问题。

4.2.3 两者对比

特性 Moore型 Mealy型
输出依赖 仅当前状态 状态+输入
状态数量 通常较多 通常较少
输出延迟 慢一个时钟周期 组合逻辑直接输出
抗毛刺能力
典型应用 控制类、安全关键 高速数据通路

4.3 状态转移图

写代码之前,先把状态转移图画出来。这是规矩。我见过有人直接上手写代码,结果状态跳转逻辑自己都理不清,最后改得面目全非。

状态转移图用圆圈表示状态,箭头表示转移方向,箭头旁边标注转移条件。Moore型的输出写在圆圈里,Mealy型的输出写在箭头上。

下面我用SVG画一个典型的状态转移图,展示一个简单的串行数据检测器——检测到“101”序列就输出高电平。

序列检测器状态转移图(检测"101") S0 等待"1" out=0 S1 收到"1" out=0 S2 收到"10" out=0 S3 检测到"101" out=1 in=1 in=0 in=0 in=1 in=1 in=0 → 回到S0 任意输入 → S0 图例: in=1转移 in=0转移 条件不满足

画图的时候,我习惯用不同颜色区分不同输入条件。这样一眼就能看出哪些转移是并行的,哪些是互斥的。嗯,这个习惯帮我避免过好几次状态冲突的bug。

4.4 状态编码

状态编码,就是把状态名字翻译成二进制数。这里面门道不少。

4.4.1 二进制编码

用最少的触发器。N个状态需要log2(N)个触发器。比如4个状态用2个触发器。省面积,但组合逻辑复杂,容易出毛刺。

4.4.2 独热码

每个状态用一个触发器。N个状态需要N个触发器。面积大,但组合逻辑简单,速度快。FPGA里特别流行,因为FPGA触发器多,查找表资源有限。

4.4.3 格雷码

相邻状态之间只有1位不同。适合状态转移路径固定的场景,比如计数器。能减少毛刺和功耗。

状态 二进制 独热码 格雷码
IDLE 00 0001 000
S1 01 0010 001
S2 10 0100 011
S3 11 1000 010

我的建议:FPGA项目用独热码,ASIC项目用二进制码。如果状态少于8个,独热码更省事。多于16个状态,二进制码更划算。我曾经在一个128状态的控制器里用了独热码,结果触发器用掉一大半,被领导骂了一顿。

4.5 避坑指南

最后分享几个我踩过的坑:

  • 漏掉default分支:综合工具会生成锁存器,仿真时状态可能跑到未知态。我曾经因为这个,芯片在高温下随机死机。
  • 状态转移条件重叠:两个转移条件同时满足,状态机不知道该去哪。一定要保证互斥。
  • 输出信号未寄存:Mealy型输出直接来自组合逻辑,容易被毛刺干扰。关键输出加一级寄存器。
  • 状态编码未考虑安全:工业级设计要用“三模冗余”或“状态保护”,防止单粒子翻转导致状态跑飞。

好了,状态机的基础就讲到这里。记住一句话:状态机是时序控制器的骨架。骨架歪了,肉长得再好也没用。下次我们讲状态机的Verilog实现模板,到时候我会手把手教你写一个健壮的状态机。


公众号:蓝海资料掘金营,微信deep3321