3、状态机设计方法:一段式、二段式、三段式状态机写法对比(Verilog示例)
状态机这东西,说白了就是数字电路里的"大脑"。它决定你的模块什么时候该干什么事。我见过不少新手,一上来就写一段式,结果代码改到崩溃。今天咱们就把三种写法掰开揉碎,看看它们到底差在哪。
核心观点:三段式是工程实践中最稳妥的选择。但理解一段式和二段式,能帮你更深刻地理解时序逻辑。
3.1 一段式状态机:所有逻辑写在一个always块里
一段式,就是把状态跳转和输出逻辑全塞进一个always块。代码看起来最紧凑,但坑也最多。
// 一段式状态机示例
module fsm_onehot(
input clk,
input rst_n,
input [1:0] in,
output reg out
);
reg [2:0] state;
localparam IDLE = 3'b001,
S1 = 3'b010,
S2 = 3'b100;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= IDLE;
out <= 1'b0;
end else begin
case (state)
IDLE: begin
if (in == 2'b01) begin
state <= S1;
out <= 1'b0;
end else begin
state <= IDLE;
out <= 1'b0;
end
end
S1: begin
if (in == 2'b10) begin
state <= S2;
out <= 1'b1;
end else begin
state <= S1;
out <= 1'b0;
end
end
S2: begin
state <= IDLE;
out <= 1'b0;
end
default: begin
state <= IDLE;
out <= 1'b0;
end
endcase
end
end
endmodule
⚠ 我曾经踩过的坑:一段式里,输出和状态跳转混在一起。如果你在某个状态里忘了给输出赋值,综合工具会给你生成一个锁存器。有一次我排查了整整两天,才发现是输出条件没写全。从那以后,我再也不敢用一段式做复杂设计。
一段式的优点?嗯,代码短,适合超简单的状态机。比如只有两三个状态,输出也简单。但一旦状态超过5个,维护起来就是噩梦。
3.2 二段式状态机:状态跳转和输出分开
二段式把状态跳转逻辑和输出逻辑拆成两个always块。一个负责时序,一个负责组合逻辑。这样代码清晰多了。
// 二段式状态机示例
module fsm_twostage(
input clk,
input rst_n,
input [1:0] in,
output reg out
);
reg [2:0] state, next_state;
localparam IDLE = 3'b001,
S1 = 3'b010,
S2 = 3'b100;
// 第一段:状态跳转(时序逻辑)
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
state <= IDLE;
else
state <= next_state;
end
// 第二段:次态逻辑 + 输出逻辑(组合逻辑)
always @(*) begin
next_state = state;
out = 1'b0;
case (state)
IDLE: begin
if (in == 2'b01)
next_state = S1;
else
next_state = IDLE;
end
S1: begin
if (in == 2'b10) begin
next_state = S2;
out = 1'b1;
end else begin
next_state = S1;
end
end
S2: begin
next_state = IDLE;
end
default: begin
next_state = IDLE;
end
endcase
end
endmodule
💡 我个人习惯:二段式里,组合逻辑块一定要用阻塞赋值(=)。为什么?因为组合逻辑需要立即生效,非阻塞赋值会引入一个时钟周期的延迟。这个细节,面试的时候经常有人答错。
二段式比一段式好在哪里?你想想看,状态跳转和输出分开了,调试的时候一眼就能看出问题。但二段式有个小毛病——输出是组合逻辑,容易产生毛刺。如果你的输出要直接驱动外部设备,最好加一级寄存器打一拍。
3.3 三段式状态机:最推荐的工程实践
三段式把状态机拆成三部分:状态跳转、次态逻辑、输出逻辑。每个部分各司其职,互不干扰。我在项目中几乎都用这种写法。
// 三段式状态机示例
module fsm_threestage(
input clk,
input rst_n,
input [1:0] in,
output reg out
);
reg [2:0] state, next_state;
localparam IDLE = 3'b001,
S1 = 3'b010,
S2 = 3'b100;
// 第一段:状态跳转(时序逻辑)
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
state <= IDLE;
else
state <= next_state;
end
// 第二段:次态逻辑(组合逻辑)
always @(*) begin
next_state = state;
case (state)
IDLE: begin
if (in == 2'b01)
next_state = S1;
else
next_state = IDLE;
end
S1: begin
if (in == 2'b10)
next_state = S2;
else
next_state = S1;
end
S2: begin
next_state = IDLE;
end
default: begin
next_state = IDLE;
end
endcase
end
// 第三段:输出逻辑(时序逻辑)
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
out <= 1'b0;
else begin
case (next_state)
S1: out <= 1'b0;
S2: out <= 1'b1;
default: out <= 1'b0;
endcase
end
end
endmodule
为什么三段式最稳?
- 输出是寄存器输出,没有毛刺
- 状态跳转和输出逻辑完全解耦
- 修改输出逻辑不会影响状态跳转
- 综合工具优化效果最好
3.4 三种写法对比总结
| 对比项 | 一段式 | 二段式 | 三段式 |
|---|---|---|---|
| 代码量 | 最少 | 中等 | 最多 |
| 可读性 | 差 | 中等 | 好 |
| 输出类型 | 寄存器/组合 | 组合逻辑 | 寄存器输出 |
| 毛刺风险 | 高 | 高 | 低 |
| 维护难度 | 高 | 中等 | 低 |
| 工程推荐度 | 不推荐 | 谨慎使用 | 强烈推荐 |
⚠ 避坑指南:我曾经在一个项目中,接手了别人写的一段式状态机,足足有20多个状态。每次加功能都要把整个always块翻一遍,改完一个地方还得担心会不会影响其他地方。最后我花了一周时间,全部重写成三段式。嗯,从那以后,我在团队里定了个规矩——状态机超过5个状态,必须用三段式。
3.5 核心知识体系
下面这张图,把三种写法的核心区别和适用场景画出来了。你一看就明白。
💡 最后说一句:你可能会觉得三段式代码写起来麻烦。但你想过没有,调试的时候省下的时间,远比写代码多花的那几分钟值钱。我在项目里见过太多因为状态机写得太随意,导致时序告警查了三天三夜的案例。嗯,别让自己成为下一个。