2. Verilog实现基础:三段式状态机写法详解
状态机这东西,说白了就是数字电路里的“大脑”。它决定你的设计下一步该干什么。我刚入行那会儿,写状态机都是两段式,后来被一个时序问题折磨了两天,才彻底转向三段式。今天我就把这三段式的门道,掰开了揉碎了讲给你听。
2.1 为什么我偏爱三段式?
两段式状态机,把时序逻辑和组合逻辑混在一起。看起来代码少,但综合后容易出毛刺。我有个血的教训:
我曾经在一个通信协议解析模块里用了两段式,仿真全对,上板后偶尔抓错数据。查了三天,发现是状态跳转时组合逻辑输出的毛刺,刚好被下一级寄存器采到。从那以后,但凡有点复杂度的状态机,我全用三段式。
三段式的好处很明显:
- 时序清晰:每个always块只干一件事
- 无毛刺:输出由寄存器打一拍,干净利落
- 好维护:改状态编码不影响输出逻辑,改输出逻辑不影响状态跳转
2.2 三段式的三段,到底分什么?
你想想看,一个状态机无非就三件事:
- 当前状态是什么——用寄存器存着
- 下一步要去哪——根据输入和当前状态算出来
- 当前该输出什么——根据当前状态决定
三段式就是把这三件事,分别放到三个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 三段式的知识体系
我把这三段式的核心逻辑画了张图,你一看就明白:
这张图你看懂了吗?说白了就是:第一段存状态,第二段算下一步,第三段给输出。各管各的,互不干扰。我在实际项目中,哪怕只有两三个状态,也坚持用这个结构。为什么?因为后期加功能、改逻辑,三段式改起来最省心。
一个小技巧:写第二段组合逻辑时,我习惯先写一句
next_state = current_state 做默认赋值,这样case里只需要写跳转条件,没覆盖到的状态自动保持。既省代码,又防latch。
好了,三段式状态机的基本写法就这些。记住:状态编码选独热码,三段各司其职,输出用current_state。你按这个套路写,时序问题至少少一半。
公众号:蓝海资料掘金营,微信deep3321