两段式状态机:状态跳转逻辑与输出逻辑分离

说实话,我刚入行那会儿,写状态机都是三段式。后来有个项目,资源紧张得要命,我才开始认真琢磨两段式。你想想看,两段式其实就是把状态跳转和输出逻辑拆开,各管各的。这样做的好处很明显——代码结构清晰,综合工具也好优化。

我个人习惯把两段式叫做「跳转+输出」模式。第一段管状态怎么跳,第二段管输出是什么。说白了,就是把状态寄存器和输出逻辑彻底分开。

两段式的核心结构

两段式状态机由两个always块组成:

  • 第一段:时序逻辑,负责状态寄存器的更新。也就是根据当前状态和输入,决定下一个状态是什么。
  • 第二段:组合逻辑,负责输出信号的生成。根据当前状态(或者当前状态+输入),产生对应的输出。

嗯,这里要注意:第二段可以是组合逻辑,也可以是时序逻辑。如果输出需要寄存,那就用时序逻辑;如果不需要,组合逻辑就够了。我在项目中遇到过,有些同事把输出也做成时序,结果多了一级流水延迟,调试了半天才发现。

代码模板:一个实际例子

咱们拿一个简单的序列检测器来说。检测到"101"就输出高电平。状态定义如下:

// 状态编码
localparam IDLE = 2'b00;
localparam S1   = 2'b01;
localparam S10  = 2'b10;
localparam S101 = 2'b11;

reg [1:0] current_state, next_state;

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

// 第二段:输出逻辑(组合)
always @(*) begin
    // 默认值
    next_state = IDLE;
    out = 1'b0;

    case (current_state)
        IDLE: begin
            if (data_in == 1'b1)
                next_state = S1;
            else
                next_state = IDLE;
        end
        S1: begin
            if (data_in == 1'b0)
                next_state = S10;
            else
                next_state = S1;
        end
        S10: begin
            if (data_in == 1'b1)
                next_state = S101;
            else
                next_state = IDLE;
        end
        S101: begin
            out = 1'b1;  // 检测到序列,输出高
            if (data_in == 1'b1)
                next_state = S1;
            else
                next_state = IDLE;
        end
        default: begin
            next_state = IDLE;
            out = 1'b0;
        end
    endcase
end

你看,第一段就三行代码,干净利落。第二段把状态跳转和输出放在一起,但注意——输出是组合逻辑,不寄存。我曾经犯过一个错,把输出写在时序块里,结果仿真波形总是晚一个周期,查了半天才发现。

两段式 vs 三段式:怎么选?

对比项 两段式 三段式
代码量 少,约2/3 多,结构更清晰
输出时序 组合输出,可能有毛刺 寄存输出,干净
综合效率 高,资源少 稍高,但多一级寄存器
调试难度 中等,输出需注意 低,波形直观

我个人建议:如果输出信号后续还要接寄存器(比如数据总线),用两段式就够了。如果输出直接驱动外部引脚,最好用三段式,避免毛刺。我在一个高速接口项目里,就因为两段式的组合输出导致误触发,后来改成三段式才解决。

仿真验证方法

两段式的仿真,核心要验证两件事:状态跳转对不对,输出对不对。

第一步:写testbench

我习惯用任务(task)来封装输入序列。比如检测"101":

task test_seq;
    input [2:0] seq;
    begin
        data_in = seq[2]; #10;
        data_in = seq[1]; #10;
        data_in = seq[0]; #10;
    end
endtask

initial begin
    rst_n = 0; #20; rst_n = 1;
    test_seq(3'b101);  // 应该输出1
    test_seq(3'b100);  // 应该输出0
    test_seq(3'b111);  // 应该输出0
    #100 $finish;
end

第二步:看波形

打开仿真工具,重点关注:

  • current_state 是否按预期跳转
  • out 信号是否在 S101 状态时拉高
  • 有没有毛刺(组合输出常见问题)

我曾经遇到一个坑:仿真时输出正常,但综合后时序分析报错。原因是组合逻辑路径太长,导致建立时间违例。解决办法是在输出后面加一级寄存器,或者改用三段式。

核心要点:两段式状态机的精髓在于「跳转逻辑」和「输出逻辑」分离。第一段只管状态更新,第二段管状态跳转和输出。这样写出来的代码,综合工具能更好地优化组合逻辑。

小技巧:写第二段时,记得给 next_state 和 out 赋默认值。这样能避免综合出锁存器。我习惯在 case 前面先写 next_state = IDLE; out = 1'b0;,然后再写 case 分支。

注意:两段式的输出是组合逻辑,如果驱动多个模块,要注意扇出问题。我曾经在一个项目中,输出信号扇出太大,导致时序收敛困难。后来加了一级缓冲寄存器才解决。

知识体系图

两段式状态机知识体系 两段式状态机 第一段:状态跳转 时序逻辑 always块 第二段:输出逻辑 组合逻辑 always块 current_state更新 next_state计算 组合输出生成 默认值赋值 核心:跳转逻辑与输出逻辑分离,代码简洁,综合高效

这张图把两段式的结构讲清楚了。左边是状态跳转,右边是输出逻辑,中间通过 current_state 连接。你写代码时,脑子里要有这张图,就不会把逻辑写混了。

好了,两段式状态机就聊到这儿。记住:代码简洁是好事,但别为了简洁牺牲可读性。该加注释的地方加注释,该赋默认值的地方别偷懒。这样,不管是自己调试还是别人接手,都能省不少时间。

可可精品资料库