3. 三段式状态机

状态机的写法,圈里人争论了好多年。我刚开始做RTL设计那会儿,前辈们大多用一段式,一个always块搞定所有逻辑。后来项目越做越大,一段式的维护成本实在太高了。我个人习惯用三段式,说白了就是把状态机的三个核心功能拆开写——状态跳转、下一状态判断、输出产生,各管各的。

你想想看,这样写有什么好处?代码清晰,分工明确。综合工具也喜欢这种结构,时序收敛更容易。我带过几个新人,一开始都觉得三段式麻烦,但上手之后都说真香。

第一段:时序逻辑——状态寄存器的更新

第一段负责状态寄存器的更新。它是个时序逻辑,只在时钟上升沿或复位时动作。代码很简单,但容易写错的地方也不少。

// 第一段:状态寄存器更新
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        current_state <= IDLE;
    else
        current_state <= next_state;
end

嗯,这里要注意:复位信号我用的是异步复位、同步释放的方式。我在项目中遇到过因为复位信号毛刺导致状态机误触发的情况,后来统一改成异步复位,问题就解决了。

个人经验:状态编码建议用独热码(One-Hot)或格雷码。独热码适合状态数少于16个的场景,综合后面积小、速度也快。状态数多的话,格雷码更省寄存器。

第二段:组合逻辑——下一状态的判断

第二段是组合逻辑,根据当前状态和输入信号,决定下一状态是什么。这里最容易出问题的是——组合逻辑的敏感列表没写全,或者产生了锁存器。

// 第二段:下一状态组合逻辑
always @(*) begin
    next_state = current_state;  // 默认保持
    case (current_state)
        IDLE: begin
            if (start)
                next_state = READ;
            else
                next_state = IDLE;
        end
        READ: begin
            if (data_valid)
                next_state = DONE;
            else
                next_state = READ;
        end
        DONE: begin
            next_state = IDLE;
        end
        default: next_state = IDLE;
    endcase
end

看到没?我习惯在case外面先给next_state赋一个默认值。这样做的好处是——就算某个分支漏写了,也不会产生锁存器。我曾经因为少写一个分支,仿真跑了两天才发现锁存器问题,从那以后这个习惯就养成了。

避坑指南:组合逻辑中不要用非阻塞赋值(<=)。非阻塞赋值在组合逻辑中会产生仿真和综合不一致的问题。我见过有人因为这个bug调了三天,最后发现是赋值方式用错了。

第三段:输出逻辑——产生控制信号

第三段负责输出。这里有两种选择:用组合逻辑输出,或者用时序逻辑输出。我个人建议用时序逻辑输出,虽然多了一拍延迟,但能有效避免毛刺。

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

你可能会问:为什么不用组合逻辑输出?组合逻辑输出确实省一拍延迟,但信号容易有毛刺。尤其是当多个输出同时变化时,毛刺问题更明显。我在一个高速接口项目中吃过这个亏,后来全部改成时序输出,问题就解决了。

核心要点:三段式状态机的精髓在于——第一段管状态跳转,第二段管下一状态判断,第三段管输出。三段各司其职,互不干扰。这样写出来的代码,可读性强,综合结果好,调试也方便。

三段式状态机的完整结构

把三段拼在一起,就是一个完整的状态机。我画了个图,帮你理解这三段之间的关系:

第一段:时序逻辑 状态寄存器更新 current_state <= next_state 时钟上升沿触发 第二段:组合逻辑 下一状态判断 case (current_state) 根据输入决定next_state 第三段:输出逻辑 产生控制信号 时序输出或组合输出 data_out, valid_out current_state next_state current_state 输入信号(start, data_valid等) 输出信号(data_out, valid_out) 三段式状态机结构图 第一段:状态跳转 → 第二段:下一状态判断 → 第三段:输出产生 输入信号影响第二段,第三段根据当前状态产生输出

从图上你能看到,三段之间通过current_state和next_state这两个信号连接。第一段把next_state锁存到current_state,第二段根据current_state和输入算出新的next_state,第三段根据current_state产生输出。这个闭环结构,就是状态机的核心。

三种写法的对比

对比项 一段式 两段式 三段式
代码结构 一个always块 两个always块 三个always块
可读性 差,逻辑混在一起 中等 好,分工明确
输出毛刺 容易产生 组合输出有毛刺 时序输出无毛刺
调试难度 高 中等 低
综合结果 一般 较好 最优
适用场景 简单状态机(<4个状态) 中等复杂度 复杂状态机(推荐)

从表格能看出来,三段式在可读性、调试、综合结果上都占优势。唯一的代价是多写几行代码,但这点代价换来的维护便利性,值。

我的建议:新项目一律用三段式。别图省事用一段式,等代码量上去了,改一个状态就要翻半天。我见过一个项目,状态机有30多个状态,用一段式写的,后来加功能时没人敢动那段代码,最后只能重写。

好了,三段式状态机的写法就讲到这里。记住三个段各司其职,写出来的代码既漂亮又可靠。下一节我们聊聊状态机编码方式的选择,到时候我会分享一些实际项目中的编码技巧。

可可精品资料库