2. Verilog实现基础:三段式状态机写法详解

状态机这东西,说白了就是数字电路里的“大脑”。它决定你的设计下一步该干什么。我刚入行那会儿,写状态机都是两段式,后来被一个时序问题折磨了两天,才彻底转向三段式。今天我就把这三段式的门道,掰开了揉碎了讲给你听。

2.1 为什么我偏爱三段式?

两段式状态机,把时序逻辑和组合逻辑混在一起。看起来代码少,但综合后容易出毛刺。我有个血的教训:

我曾经在一个通信协议解析模块里用了两段式,仿真全对,上板后偶尔抓错数据。查了三天,发现是状态跳转时组合逻辑输出的毛刺,刚好被下一级寄存器采到。从那以后,但凡有点复杂度的状态机,我全用三段式。

三段式的好处很明显:

  • 时序清晰:每个always块只干一件事
  • 无毛刺:输出由寄存器打一拍,干净利落
  • 好维护:改状态编码不影响输出逻辑,改输出逻辑不影响状态跳转

2.2 三段式的三段,到底分什么?

你想想看,一个状态机无非就三件事:

  1. 当前状态是什么——用寄存器存着
  2. 下一步要去哪——根据输入和当前状态算出来
  3. 当前该输出什么——根据当前状态决定

三段式就是把这三件事,分别放到三个always块里。我习惯这么分:

段落 always块类型 负责内容 敏感列表
第一段 时序逻辑 状态寄存器更新 posedge clk or negedge rst_n
第二段 组合逻辑 次态逻辑(下一步去哪) current_state, 输入信号
第三段 时序逻辑 输出逻辑(当前输出什么) posedge clk or negedge rst_n

嗯,这里要注意:第二段是组合逻辑,千万别加时钟沿触发。我见过有人把次态逻辑写成时序的,结果状态机慢了一拍,整个逻辑全乱套。

2.3 状态编码怎么选?

状态编码这事,我建议你根据资源情况来定。常用的有三种:

  • 二进制编码:状态数少时用,比如4个状态用2位。省寄存器,但组合逻辑复杂。
  • 独热码:每个状态一个bit。FPGA里寄存器多,LUT少,独热码最合适。我90%的项目都用它。
  • 格雷码:相邻状态只变1bit。跨时钟域或低功耗场景下用得多。
我个人习惯:状态数少于8个用二进制,8到16个用独热码,超过16个?我建议你重新审视一下状态机设计,是不是该拆分了。

2.4 完整的三段式状态机示例

光说不练假把式。我给你写一个简单的例子:一个交通灯控制器,红灯5秒,绿灯4秒,黄灯1秒。咱们用独热码:

// 状态编码 - 独热码
parameter IDLE    = 3'b001;
parameter RED     = 3'b010;
parameter GREEN   = 3'b100;
// 注意:黄灯状态我合并到GREEN的计数里了,实际项目看需求

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

// 第二段:次态逻辑(组合逻辑)
always @(*) begin
    next_state = current_state;  // 默认保持
    case (current_state)
        IDLE:   next_state = RED;
        RED:    if (cnt_red_done) next_state = GREEN;
        GREEN:  if (cnt_green_done) next_state = RED;
        default: next_state = IDLE;
    endcase
end

// 第三段:输出逻辑(时序逻辑)
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        light_out <= 3'b000;
        cnt_en    <= 1'b0;
    end else begin
        case (current_state)
            IDLE:   light_out <= 3'b001;  // 全灭
            RED:    light_out <= 3'b010;  // 红灯亮
            GREEN:  light_out <= 3'b100;  // 绿灯亮
            default: light_out <= 3'b000;
        endcase
        // 计数使能也根据状态来
        cnt_en <= (current_state == RED) || (current_state == GREEN);
    end
end
关键点:注意第三段用的是current_state,不是next_state。输出逻辑只关心当前状态,不关心下一步去哪。这就是三段式“输出与次态分离”的精髓。

2.5 避坑指南:我踩过的三个坑

写三段式状态机,有几个地方特别容易翻车:

  • 坑一:第二段忘了写default。组合逻辑不写default,综合出来就是latch。我刚开始学的时候,仿真没问题,综合报告里一堆latch warning,吓得我赶紧改了。
  • 坑二:第三段用了next_state。输出逻辑用next_state,相当于输出提前了一拍。这在某些场景下是故意的,但如果你没意识到,debug时会很痛苦。
  • 坑三:状态编码冲突。独热码要保证每个状态独占一个bit。我见过有人写parameter A=2'b01, B=2'b10, C=2'b11,这哪是独热码?这是二进制!

2.6 三段式的知识体系

我把这三段式的核心逻辑画了张图,你一看就明白:

三段式状态机结构图 第一段:状态寄存器 时序逻辑 always @(posedge clk) current_state <= next_state 第二段:次态逻辑 组合逻辑 always @(*) next_state = f(input, cs) 第三段:输出逻辑 时序逻辑 always @(posedge clk) output = g(current_state) next_state 反馈给状态寄存器 核心原则 • 第一段只负责更新状态,不参与任何逻辑计算 • 第二段只算下一步去哪,不产生任何寄存器输出 • 第三段只根据当前状态输出,不关心怎么跳转 • 三段之间通过 current_state 和 next_state 两个信号连接

这张图你看懂了吗?说白了就是:第一段存状态,第二段算下一步,第三段给输出。各管各的,互不干扰。我在实际项目中,哪怕只有两三个状态,也坚持用这个结构。为什么?因为后期加功能、改逻辑,三段式改起来最省心。

一个小技巧:写第二段组合逻辑时,我习惯先写一句 next_state = current_state 做默认赋值,这样case里只需要写跳转条件,没覆盖到的状态自动保持。既省代码,又防latch。

好了,三段式状态机的基本写法就这些。记住:状态编码选独热码,三段各司其职,输出用current_state。你按这个套路写,时序问题至少少一半。


公众号:蓝海资料掘金营,微信deep3321