3、状态机设计方法:一段式、二段式、三段式状态机写法对比(Verilog示例)

状态机这东西,说白了就是数字电路里的"大脑"。它决定你的模块什么时候该干什么事。我见过不少新手,一上来就写一段式,结果代码改到崩溃。今天咱们就把三种写法掰开揉碎,看看它们到底差在哪。

核心观点:三段式是工程实践中最稳妥的选择。但理解一段式和二段式,能帮你更深刻地理解时序逻辑。

3.1 一段式状态机:所有逻辑写在一个always块里

一段式,就是把状态跳转和输出逻辑全塞进一个always块。代码看起来最紧凑,但坑也最多。

// 一段式状态机示例
module fsm_onehot(
    input clk,
    input rst_n,
    input [1:0] in,
    output reg out
);

    reg [2:0] state;
    localparam IDLE = 3'b001,
               S1   = 3'b010,
               S2   = 3'b100;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            state <= IDLE;
            out    <= 1'b0;
        end else begin
            case (state)
                IDLE: begin
                    if (in == 2'b01) begin
                        state <= S1;
                        out   <= 1'b0;
                    end else begin
                        state <= IDLE;
                        out   <= 1'b0;
                    end
                end
                S1: begin
                    if (in == 2'b10) begin
                        state <= S2;
                        out   <= 1'b1;
                    end else begin
                        state <= S1;
                        out   <= 1'b0;
                    end
                end
                S2: begin
                    state <= IDLE;
                    out   <= 1'b0;
                end
                default: begin
                    state <= IDLE;
                    out   <= 1'b0;
                end
            endcase
        end
    end

endmodule

⚠ 我曾经踩过的坑:一段式里,输出和状态跳转混在一起。如果你在某个状态里忘了给输出赋值,综合工具会给你生成一个锁存器。有一次我排查了整整两天,才发现是输出条件没写全。从那以后,我再也不敢用一段式做复杂设计。

一段式的优点?嗯,代码短,适合超简单的状态机。比如只有两三个状态,输出也简单。但一旦状态超过5个,维护起来就是噩梦。

3.2 二段式状态机:状态跳转和输出分开

二段式把状态跳转逻辑和输出逻辑拆成两个always块。一个负责时序,一个负责组合逻辑。这样代码清晰多了。

// 二段式状态机示例
module fsm_twostage(
    input clk,
    input rst_n,
    input [1:0] in,
    output reg out
);

    reg [2:0] state, next_state;
    localparam IDLE = 3'b001,
               S1   = 3'b010,
               S2   = 3'b100;

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

    // 第二段:次态逻辑 + 输出逻辑(组合逻辑)
    always @(*) begin
        next_state = state;
        out = 1'b0;
        case (state)
            IDLE: begin
                if (in == 2'b01)
                    next_state = S1;
                else
                    next_state = IDLE;
            end
            S1: begin
                if (in == 2'b10) begin
                    next_state = S2;
                    out = 1'b1;
                end else begin
                    next_state = S1;
                end
            end
            S2: begin
                next_state = IDLE;
            end
            default: begin
                next_state = IDLE;
            end
        endcase
    end

endmodule

💡 我个人习惯:二段式里,组合逻辑块一定要用阻塞赋值(=)。为什么?因为组合逻辑需要立即生效,非阻塞赋值会引入一个时钟周期的延迟。这个细节,面试的时候经常有人答错。

二段式比一段式好在哪里?你想想看,状态跳转和输出分开了,调试的时候一眼就能看出问题。但二段式有个小毛病——输出是组合逻辑,容易产生毛刺。如果你的输出要直接驱动外部设备,最好加一级寄存器打一拍。

3.3 三段式状态机:最推荐的工程实践

三段式把状态机拆成三部分:状态跳转、次态逻辑、输出逻辑。每个部分各司其职,互不干扰。我在项目中几乎都用这种写法。

// 三段式状态机示例
module fsm_threestage(
    input clk,
    input rst_n,
    input [1:0] in,
    output reg out
);

    reg [2:0] state, next_state;
    localparam IDLE = 3'b001,
               S1   = 3'b010,
               S2   = 3'b100;

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

    // 第二段:次态逻辑(组合逻辑)
    always @(*) begin
        next_state = state;
        case (state)
            IDLE: begin
                if (in == 2'b01)
                    next_state = S1;
                else
                    next_state = IDLE;
            end
            S1: begin
                if (in == 2'b10)
                    next_state = S2;
                else
                    next_state = S1;
            end
            S2: begin
                next_state = IDLE;
            end
            default: begin
                next_state = IDLE;
            end
        endcase
    end

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

endmodule

为什么三段式最稳?

  • 输出是寄存器输出,没有毛刺
  • 状态跳转和输出逻辑完全解耦
  • 修改输出逻辑不会影响状态跳转
  • 综合工具优化效果最好

3.4 三种写法对比总结

对比项 一段式 二段式 三段式
代码量 最少 中等 最多
可读性 差 中等 好
输出类型 寄存器/组合 组合逻辑 寄存器输出
毛刺风险 高 高 低
维护难度 高 中等 低
工程推荐度 不推荐 谨慎使用 强烈推荐

⚠ 避坑指南:我曾经在一个项目中,接手了别人写的一段式状态机,足足有20多个状态。每次加功能都要把整个always块翻一遍,改完一个地方还得担心会不会影响其他地方。最后我花了一周时间,全部重写成三段式。嗯,从那以后,我在团队里定了个规矩——状态机超过5个状态,必须用三段式。

3.5 核心知识体系

下面这张图,把三种写法的核心区别和适用场景画出来了。你一看就明白。

状态机三种写法对比 一段式 1个always块 状态跳转 + 输出 全部混在一起 ❌ 维护困难 二段式 2个always块 状态跳转(时序) 输出(组合) ⚠ 有毛刺风险 三段式 ✅ 3个always块 状态跳转(时序) 次态逻辑(组合) 输出(时序) 工程实践建议 🔹 简单状态机(≤3个状态):一段式可以用,但我不推荐 🔹 中等复杂度(4-8个状态):二段式勉强能用,注意输出加寄存器 🔹 复杂状态机(>8个状态):必须用三段式,这是行业标准 🔹 我的铁律:所有新项目统一用三段式,没有例外

💡 最后说一句:你可能会觉得三段式代码写起来麻烦。但你想过没有,调试的时候省下的时间,远比写代码多花的那几分钟值钱。我在项目里见过太多因为状态机写得太随意,导致时序告警查了三天三夜的案例。嗯,别让自己成为下一个。

可可精品资料库