3. 状态机设计:Moore与Mealy状态机区别、三段式状态机写法、状态机编码与常见错误

状态机这东西,说白了就是数字逻辑里的“大脑”。你想想看,一个复杂的时序逻辑,如果没有状态机来调度,那代码写出来就是一锅粥。我面试的时候,状态机几乎是必考题,而且问得特别细。今天咱们就把这块彻底聊透。

3.1 Moore与Mealy状态机:核心区别

很多新手分不清Moore和Mealy,其实记住一句话就行:输出跟谁有关

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

我举个例子你就明白了。假设我们要设计一个“检测连续三个1”的电路:

  • Moore型:需要4个状态(S0~S3)。检测到三个1后,进入S3状态,此时输出为1。下一个时钟沿才能看到输出变化。
  • Mealy型:只需要3个状态(S0~S2)。在S2状态下,如果输入又是1,输出立刻变为1(组合逻辑输出),不需要等下一个时钟。

关键区别:Mealy型输出快一拍,但容易产生毛刺。Moore型输出稳定,但延迟一拍。

我在项目中遇到过一个问题:用Mealy型状态机做高速数据流控制,结果输出毛刺导致后级电路误触发。后来改成Moore型,加了一级寄存器输出,问题就解决了。所以,对时序要求严苛的场景,我建议优先考虑Moore型

3.2 三段式状态机写法

写状态机,我个人习惯用三段式。为什么?因为代码清晰,综合工具也好优化。三段式分别是:

  1. 第一段:时序逻辑,描述状态跳转(always @(posedge clk or negedge rst_n))
  2. 第二段:组合逻辑,描述次态逻辑(always @(*))
  3. 第三段:时序逻辑,描述输出逻辑(always @(posedge clk or negedge rst_n))

给你看个标准模板:

// 第一段:状态寄存器
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:   if (start) next_state = WORK;
                else       next_state = IDLE;
        WORK:   if (done)  next_state = DONE;
                else       next_state = WORK;
        DONE:   next_state = IDLE;
        default: next_state = IDLE;
    endcase
end

// 第三段:输出逻辑(时序逻辑)
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        out <= 1'b0;
    else begin
        case (current_state)
            WORK: out <= 1'b1;
            default: out <= 1'b0;
        endcase
    end
end

小技巧:第二段用组合逻辑,赋值用阻塞赋值(=)。第一段和第三段用时序逻辑,赋值用非阻塞赋值(<=)。这个习惯一定要养成,不然仿真会出各种奇怪问题。

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

编码方式直接影响面积和功耗。我整理了一个对比表:

编码方式 状态数N所需位数 优点 缺点 适用场景
二进制 log2(N) 最省寄存器 译码逻辑复杂,功耗高 状态数多(>16)
格雷码 log2(N) 相邻状态跳变时只有1位变化,功耗低 译码逻辑依然复杂 状态连续跳转(如计数器)
独热码 N 译码逻辑简单,速度快 最费寄存器 状态数少(<8)

我记得有一次面试,我问候选人:“状态数有20个,你选什么编码?”他脱口而出“独热码”。我说你想想看,20个状态用独热码,寄存器要20个,而二进制只要5个。FPGA里寄存器资源虽然多,但也不是这么浪费的。所以,状态数少(比如8个以内)用独热码,状态数多用二进制或格雷码

3.4 常见错误与避坑

错误一:组合逻辑环路

我曾经见过一个同事,在第二段组合逻辑里把输出又反馈回输入,结果综合出来一个组合环路。仿真时看着没问题,一上板子就死机。记住:组合逻辑里不要出现反馈

错误二:漏掉default分支

case语句不写default,综合工具会生成锁存器。我刚开始做设计时就吃过这个亏,仿真没问题,综合报告里一堆latch warning。后来养成了习惯,每个case都带上default,哪怕只是赋个初始值。

错误三:状态跳转条件不完整

比如你写了“if (a) next_state = S1; else next_state = S2;”,但忘了考虑a和b同时为真的情况。这种问题在仿真时很难发现,因为仿真器会保持当前状态。但实际电路中,状态可能跳到未知态。我的建议是:每个状态都明确写出所有跳转条件,不要依赖else兜底

避坑指南:我曾经在调试一个UART接收状态机时,发现偶尔会丢数据。查了两天才发现,是状态跳转条件里用了组合逻辑的边沿检测,导致毛刺误触发。后来改成同步边沿检测,问题就解决了。所以,状态机的跳转条件一定要用同步信号

嗯,状态机这块内容其实挺多的,但核心就是这些。你把这些吃透了,面试时基本不会栽跟头。下一章咱们聊聊跨时钟域处理,那个坑更多,到时候我再给你细讲。