4. 逻辑设计基础(二):状态机设计、FSM编码风格与三态门

好,咱们接着聊。上一章我们把组合逻辑和时序逻辑理了一遍,这一章要啃的,是FPGA逻辑设计里真正见功夫的地方——状态机。

说实话,我见过太多工程师,写代码一把好手,一到状态机就翻车。不是跑飞了,就是资源爆了。为什么?因为状态机这东西,看着简单,门道其实很深。

4.1 Moore与Mealy状态机:选哪个?

先搞清楚两个基本概念。Moore机和Mealy机,区别就一句话:输出跟谁有关

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

我个人的习惯是:能用Moore就用Moore。为什么?因为Moore机的输出和时钟沿同步,时序上干净利落,不容易出毛刺。我在做星载数传模块时,就吃过Mealy机的亏——输入信号抖动了一下,输出直接冒了个短脉冲,差点把后级电路搞崩。

但Mealy机也不是一无是处。它响应快,同样的功能,Mealy机可能比Moore机少用几个状态。比如你要做一个序列检测器,检测“1011”,用Mealy机可能4个状态就够了,Moore机得5个。

核心原则

  • 对输出时序要求严苛的场合(比如控制信号),优先选Moore
  • 对延迟敏感、状态数受限的场合,可以考虑Mealy
  • 宇航级设计,我建议默认用Moore,除非有明确理由

来看个简单的Moore机例子——一个两状态的反转电路:

// Moore型状态机示例
module moore_fsm (
    input  clk,
    input  rst_n,
    input  en,
    output reg out
);

    typedef enum logic {S0, S1} state_t;
    state_t state, next_state;

    // 状态寄存器
    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            state <= S0;
        else
            state <= next_state;
    end

    // 次态逻辑
    always_comb begin
        next_state = state;
        case (state)
            S0: if (en) next_state = S1;
            S1: if (en) next_state = S0;
        endcase
    end

    // 输出逻辑(只与当前状态有关)
    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            out <= 1'b0;
        else
            case (state)
                S0: out <= 1'b0;
                S1: out <= 1'b1;
            endcase
    end

endmodule

注意看,输出out是在时钟沿下更新的,这就是Moore机的典型特征。

4.2 FSM编码风格:二进制、格雷码、独热码

状态编码这事儿,很多新手不重视。觉得“反正能跑就行”。嗯,我以前也这么想,直到有一次项目里状态机跑飞了,查了三天才发现是编码方式选错了。

三种常见编码,各有各的脾气:

编码方式 状态数N所需触发器数 速度 功耗 可靠性
二进制编码 log₂(N) 中等 一般
格雷码 log₂(N) 中等 极低 较高
独热码 N

二进制编码:最省触发器,但组合逻辑复杂。状态跳转时可能有多位同时变化,容易产生毛刺。我一般只在状态数很少(比如4个以下)时用。

格雷码:相邻状态只有一位变化。这在跨时钟域传输时特别有用。我记得做某个星载接口时,状态机需要跨时钟域传递状态信息,用格雷码编码,直接省掉了一堆同步器。

独热码:每个状态一个触发器,译码逻辑极简单。速度最快,但触发器用得最多。FPGA里触发器资源相对丰富,所以独热码在FPGA设计中很常见。我曾经在某个高速SerDes控制器里,状态机跑了500MHz,不用独热码根本跑不动。

我的建议

  • 宇航级设计,优先考虑独热码格雷码
  • 如果状态数超过16个,独热码的触发器开销太大,可以考虑格雷码
  • 如果状态机需要跨时钟域,必须用格雷码

来看个独热码的例子:

// 独热码状态机示例
module onehot_fsm (
    input  clk,
    input  rst_n,
    input  start,
    output reg done
);

    localparam IDLE  = 4'b0001;
    localparam BUSY  = 4'b0010;
    localparam DONE  = 4'b0100;
    localparam ERROR = 4'b1000;

    reg [3:0] state, next_state;

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            state <= IDLE;
        else
            state <= next_state;
    end

    always_comb begin
        next_state = state;
        case (1'b1)
            state[0]: if (start) next_state = BUSY;  // IDLE
            state[1]: next_state = DONE;              // BUSY
            state[2]: next_state = IDLE;              // DONE
            state[3]: next_state = IDLE;              // ERROR
            default: next_state = IDLE;
        endcase
    end

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            done <= 1'b0;
        else
            done <= (state == DONE);
    end

endmodule

看到没?case (1'b1)这种写法,就是独热码的典型风格。每个状态只检查对应的那一位,组合逻辑极简单。

注意:独热码状态机一定要写default分支!否则如果因为单粒子翻转导致状态跑飞,状态机可能永远回不来。我曾经在测试中遇到过,一个独热码状态机没写default,辐照试验时状态直接变成了全0,卡死了。从那以后,我的所有状态机都加了default和非法状态检测。

4.3 三态门与双向IO

三态门,说白了就是输出可以有三个状态:0、1、高阻(Z)。高阻态意味着输出引脚被“断开”了,不驱动总线。

在FPGA里,三态门主要用在两个地方:

  • 双向IO:比如数据总线,同一根线既当输入又当输出
  • 共享总线:多个设备挂在同一根线上,同一时间只能有一个设备驱动

FPGA内部其实没有真正的三态门——内部走的是多路选择器。三态门只存在于IO单元里。所以,不要在FPGA内部逻辑里写三态,那是仿真能过、综合会报错的东西。

来看一个标准的三态双向IO模型:

// 三态双向IO示例
module bidir_io (
    inout wire data,      // 双向数据线
    input  dir,           // 方向控制:1=输出,0=输入
    input  data_out,      // 要输出的数据
    output data_in        // 读入的数据
);

    // 三态输出缓冲器
    assign data = dir ? data_out : 1'bz;

    // 输入缓冲器(始终读取)
    assign data_in = data;

endmodule

这里的关键是inout wire1'bz。dir为1时,data输出data_out的值;dir为0时,data变成高阻,此时外部可以驱动这根线,我们通过data_in读到它的值。

宇航级设计中的三态门注意事项

  • 三态门在辐照环境下容易出问题。高阻态可能被辐射干扰成弱0或弱1
  • 我建议:能不用的地方尽量不用三态。实在要用,加上弱上拉或弱下拉
  • 双向IO的方向切换时,要加死区时间——先让输出变高阻,等一个时钟周期再切换方向
  • FPGA内部不要用三态逻辑,综合工具会把它转成MUX,反而浪费资源

我曾经在一个星载数据采集项目中,用了双向IO来复用地址和数据线。第一次流片回来,发现总线冲突——两个设备同时驱动了同一根线。查了半天,是方向控制信号的时序没对齐。后来加了一个周期的死区,问题就解决了。

嗯,这一章的内容就到这儿。状态机设计是FPGA逻辑的核心中的核心,编码方式的选择直接影响可靠性。三态门虽然简单,但用不好就是坑。下一章我们讲同步设计与跨时钟域处理,那才是真正考验功力的地方。