三段式状态机规范:第一段、第二段、第三段

说实话,我刚入行那会儿,写状态机都是「一段式」——一个 always 块搞定所有。那时候觉得挺爽的,代码短,看着也简单。直到有一次,我负责一个通信协议的控制模块,状态多了以后,那个一段式的代码我自己都看不懂了。更惨的是,综合后时序分析报告里一堆组合逻辑环路,领导差点让我重写。

从那以后,我就老老实实用三段式了。今天咱们就聊聊这个经典结构,我把它拆成三块来讲。

第一段:时序逻辑状态跳转

这一段的任务很简单:把当前状态更新成下一个状态。说白了,就是每个时钟沿来的时候,把组合逻辑算好的 next_state 赋值给 current_state。

代码长这样:

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

嗯,就这么几行。我见过有人在这里加一堆复杂逻辑,其实没必要。这段代码就干一件事——寄存状态。你想想看,如果这里塞了别的判断,那状态跳转的时序就乱了。

我的习惯:这段 always 块里只放复位和状态更新。复位用异步复位,同步释放,这是业界标准做法。

第二段:组合逻辑次态判断

这一段是状态机的「大脑」。它根据当前状态和输入信号,算出下一个状态是什么。

这里有个坑,我踩过好几次——组合逻辑里不能有锁存器。什么意思呢?就是你的 case 语句必须覆盖所有状态,default 一定要写。我曾经漏了一个状态,结果综合出来一个锁存器,仿真怎么都对,上板就挂。

代码示例:

// 第二段:次态判断(组合逻辑)
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
注意:组合逻辑的敏感列表用 @(*),别手写敏感信号。我见过有人写 @(current_state or start or data_valid),结果加了个信号忘了更新,仿真半天查不出问题。

我个人习惯在 case 前面先给 next_state 赋一个默认值,就是当前状态。这样如果某个分支没写全,它至少不会变成未知态。这个小技巧帮我省了不少调试时间。

第三段:时序逻辑输出

最后一段,输出。这里有两种做法:

  • 摩尔型输出:只跟当前状态有关
  • 米利型输出:跟当前状态和输入都有关

我建议都用时序逻辑输出,也就是 always @(posedge clk)。为什么呢?因为组合逻辑输出容易有毛刺。你想想看,输入信号稍微抖一下,输出就跟着跳,后级电路很容易误触发。

代码示例(摩尔型):

// 第三段:输出(时序逻辑)
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        data_out <= 1'b0;
    else begin
        case (current_state)
            IDLE: data_out <= 1'b0;
            READ: data_out <= 1'b0;
            DONE: data_out <= 1'b1;
            default: data_out <= 1'b0;
        endcase
    end
end
核心要点:三段式的精髓就是「各司其职」。第一段管寄存,第二段管判断,第三段管输出。每一段只做自己的事,互不干扰。

三段式的整体结构图

下面这张图,是我自己画的三段式状态机结构。你看一眼就明白了:

第一段 时序逻辑 状态跳转 current_state <= next_state 第二段 组合逻辑 次态判断 case + 输入信号 第三段 时序逻辑 输出 data_out <= ... 反馈:next_state 给第一段 输入信号(start, data_valid...) 时钟 clk + 复位 rst_n 输出信号(data_out...) 三段式状态机结构 第一段:状态寄存 | 第二段:次态计算 | 第三段:输出寄存

避坑指南

我这些年总结的几个常见问题,你写代码的时候注意一下:

  • 第二段别漏 default:组合逻辑的 case 必须写 default,否则综合出锁存器。我吃过这个亏,仿真没问题,上板就随机出错。
  • 第三段用时序输出:组合逻辑输出虽然省一个寄存器,但毛刺问题很头疼。尤其是驱动后级触发器的使能信号,毛刺可能导致误触发。
  • 状态编码用独热码还是格雷码?我个人建议:状态少于 8 个用独热码,综合后面积小;状态多的话用格雷码,减少跳变时的毛刺。
总结一下:三段式状态机不是花架子,它是经过工程验证的成熟结构。第一段管寄存,第二段管判断,第三段管输出。各司其职,代码清晰,时序可控。你按这个规范写,综合、仿真、调试都会省心很多。

可可精品资料库