4. 状态机设计基础:Moore型与Mealy型状态机、三段式状态机写法、状态编码

状态机这东西,说白了就是数字电路里的"大脑"。你想想看,串口通信要处理起始位、数据位、校验位、停止位,没有状态机来管着,数据早就乱套了。我刚开始做FPGA时,第一个实战项目就是UART收发器,那时候对状态机理解不深,写出来的代码自己都看不懂。后来踩了不少坑,才慢慢摸出门道。

4.1 Moore型与Mealy型状态机

这两种状态机的区别,其实就一句话:输出跟谁有关。

  • Moore型:输出只取决于当前状态。说白了,状态变了,输出才变。
  • Mealy型:输出取决于当前状态 和 输入。输入一变,输出可能立刻跟着变。

我在项目中遇到过这样一个场景:设计一个简单的数据包检测器。用Moore型做,状态多两个,但时序好分析;用Mealy型做,状态少,但组合逻辑容易出毛刺。嗯,这里要注意——Mealy型对输入信号的时序要求更严格。

核心区别速记:

  • Moore型:状态决定输出,稳,但可能慢一拍
  • Mealy型:输入+状态决定输出,快,但组合逻辑多

举个例子。假设我们要检测串口数据中是否出现了"101"这个序列。

Moore型状态机:需要4个状态(S0~S3),每个状态对应一个输出值。检测到"101"时,进入S3,输出1。

Mealy型状态机:只需要3个状态(S0~S2),在S2状态下如果输入为1,立刻输出1。少了一个状态,但输出是组合逻辑产生的。

我的建议:新手先从Moore型入手。逻辑清晰,不容易出错。等你把Moore型玩熟了,再尝试Mealy型去优化面积和延迟。

4.2 三段式状态机写法

写状态机,我个人最推荐三段式。为什么?因为可读性强,综合结果好,而且不容易出锁存器。

三段式是哪三段?

  1. 第一段:时序逻辑,描述状态跳转(当前态→次态)
  2. 第二段:组合逻辑,描述次态如何产生(根据输入和当前态)
  3. 第三段:时序逻辑或组合逻辑,描述输出

我曾经见过不少同事用一段式写状态机,一个always块里又管状态跳转又管输出,代码看着像意大利面。后来项目出了问题,定位bug花了三天。从那以后,我坚持用三段式。

来看一个UART接收器的状态机片段,Moore型,三段式写法:

// 第一段:状态跳转
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        current_state <= IDLE;
    else
        current_state <= next_state;
end

// 第二段:次态逻辑(组合逻辑)
always @(*) begin
    case (current_state)
        IDLE: begin
            if (rx_start)      // 检测到起始位
                next_state = START;
            else
                next_state = IDLE;
        end
        START: begin
            if (bit_done)      // 起始位接收完成
                next_state = DATA;
            else
                next_state = START;
        end
        DATA: begin
            if (bit_cnt == 3'd7 && bit_done)  // 8位数据收完
                next_state = STOP;
            else
                next_state = DATA;
        end
        STOP: begin
            if (bit_done)      // 停止位接收完成
                next_state = IDLE;
            else
                next_state = STOP;
        end
        default: next_state = IDLE;
    endcase
end

// 第三段:输出逻辑(Moore型,只与当前状态有关)
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        rx_data_valid <= 1'b0;
        rx_data       <= 8'd0;
    end else begin
        case (current_state)
            IDLE: rx_data_valid <= 1'b0;
            STOP: begin
                rx_data_valid <= 1'b1;  // 停止位到来时,数据有效
                rx_data       <= shift_reg;
            end
            default: rx_data_valid <= 1'b0;
        endcase
    end
end

避坑指南:第二段是组合逻辑,一定要用 always @(*) 或 always_comb。我曾经见过有人在这里用了 posedge clk,结果综合出来多了一级寄存器,时序全乱了。

4.3 状态编码:二进制、格雷码、独热码

状态编码这事儿,很多人觉得随便选就行。其实不然。编码方式直接影响资源消耗、速度、功耗。我根据项目经验,给你总结一下:

编码方式 资源消耗 速度 功耗 适用场景
二进制 最少(log₂N个触发器) 中等 中等 状态数多,资源紧张时
格雷码 最少(log₂N个触发器) 中等 低(相邻状态只变1位) 跨时钟域、低功耗设计
独热码 最多(N个触发器) 快(译码逻辑简单) 高(翻转频繁) 状态数少(≤16),追求速度

二进制编码:最省触发器。比如4个状态,用2位二进制就够了:00, 01, 10, 11。但译码逻辑复杂,组合逻辑路径长。

格雷码:相邻状态之间只有1位不同。我在做跨时钟域同步时特别喜欢用格雷码——因为每次只有1位变化,亚稳态概率大大降低。比如:00→01→11→10。

独热码:每个状态对应一个触发器,任何时候只有1位为1。比如4个状态:0001, 0010, 0100, 1000。译码逻辑就是简单的"检查某一位是否为1",速度极快。但状态数一多,触发器消耗就上去了。

我的经验:FPGA中,如果状态数不超过16个,我一般用独热码。因为FPGA的触发器资源相对丰富,而且独热码的译码逻辑简单,能跑更高的频率。如果是ASIC设计,资源紧张,那就用二进制或格雷码。

来看一个实际对比。假设有8个状态:

  • 二进制:3个触发器,译码需要3输入查找表
  • 格雷码:3个触发器,译码同样需要3输入查找表,但相邻状态切换功耗低
  • 独热码:8个触发器,译码只需要检查对应位,不需要查找表

你想想看,在FPGA中,查找表资源往往比触发器更紧张。所以独热码虽然用了更多触发器,但节省了查找表,整体资源可能更优。

总结一下我的选择原则:

  • 状态数 ≤ 16 → 独热码(速度优先)
  • 状态数 16~32 → 二进制码(资源优先)
  • 跨时钟域 → 格雷码(安全优先)

4.4 本章知识体系

下面这张图,是我自己梳理的状态机设计知识结构。你看一眼,心里就有谱了。

状态机设计知识体系 状态机类型 Moore型 Mealy型 输出只与当前状态有关 输出与当前状态+输入有关 三段式写法 第一段 第二段 第三段 状态跳转 次态逻辑 输出逻辑 时序逻辑 组合逻辑 时序/组合 状态编码 二进制 格雷码 独热码 资源最少 低功耗 速度最快 核心原则:根据状态数、速度要求、功耗目标选择合适的编码方式 新手建议:先掌握Moore型 + 三段式 + 独热码,这是最稳妥的起点

嗯,状态机这部分内容,说难不难,说简单也不简单。关键是多写、多练、多仿真。我刚开始学的时候,一个简单的UART状态机写了七八遍才彻底搞明白。别急,慢慢来。

一个小技巧:写状态机之前,先在纸上画出状态转移图。把每个状态、每个转移条件、每个输出都标清楚。画清楚了再写代码,事半功倍。

可可精品资料库