2. Verilog状态机编码风格:一段式、两段式、三段式状态机的区别与选择
状态机这东西,说白了就是数字逻辑里的“大脑”。它决定你的电路在什么时候该干什么事。我刚入行那会儿,写状态机全凭感觉,代码写得跟流水账似的。后来被老工程师骂过几次,才老老实实把编码风格给捋顺了。
今天咱们就聊聊状态机的三种主流写法:一段式、两段式、三段式。它们各有各的脾气,选对了事半功倍,选错了……嗯,调试的时候有你受的。
2.1 一段式状态机:简单粗暴,但别滥用
一段式,就是把状态跳转和输出逻辑全塞进一个always块里。代码看起来最紧凑,新手最爱用。
// 一段式状态机示例
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= IDLE;
out <= 0;
end else begin
case (state)
IDLE: begin
if (start) begin
state <= WORK;
out <= 1;
end else begin
state <= IDLE;
out <= 0;
end
end
WORK: begin
if (done) begin
state <= IDLE;
out <= 0;
end else begin
state <= WORK;
out <= 1;
end
end
default: begin
state <= IDLE;
out <= 0;
end
endcase
end
end
你看,一个always块搞定所有。但这里有个坑——输出是寄存器输出,比组合逻辑输出晚一个时钟周期。我在项目中遇到过这种情况:明明状态已经跳过去了,输出却还赖着不走,导致下游逻辑误判。
我个人习惯是:只有非常简单的状态机(比如3-4个状态,输出逻辑简单)才用一段式。复杂一点?别偷懒,换两段式或三段式。
2.2 两段式状态机:经典实用,我推荐新手入门
两段式把状态跳转和输出逻辑分开。第一个always块负责状态跳转(时序逻辑),第二个always块负责输出逻辑(组合逻辑)。
// 两段式状态机示例
// 第一段:状态跳转
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
state <= IDLE;
else
state <= next_state;
end
// 第二段:组合逻辑输出
always @(*) begin
next_state = state;
out = 0;
case (state)
IDLE: begin
if (start) begin
next_state = WORK;
out = 1;
end
end
WORK: begin
if (done) begin
next_state = IDLE;
out = 0;
end else begin
out = 1;
end
end
default: begin
next_state = IDLE;
out = 0;
end
endcase
end
两段式的优势很明显:代码结构清晰,状态跳转和输出逻辑分离。调试的时候,你一眼就能看出状态是怎么变的,输出是怎么来的。
我曾经在一个通信协议解析模块里用过两段式。那个状态机有十几个状态,输出信号也复杂。如果用一段式,代码会乱成一锅粥。两段式让我能快速定位问题——状态跳转不对?查第一段。输出不对?查第二段。分工明确,省心不少。
2.3 三段式状态机:工程首选,大型项目必备
三段式把状态机拆成三个部分:状态跳转(时序)、次态逻辑(组合)、输出逻辑(时序)。说白了,就是两段式的基础上,把输出也做成寄存器输出。
// 三段式状态机示例
// 第一段:状态跳转
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 (start) next_state = WORK;
end
WORK: begin
if (done) next_state = IDLE;
end
default: next_state = IDLE;
endcase
end
// 第三段:输出逻辑(时序)
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
out <= 0;
else begin
case (next_state)
IDLE: out <= 0;
WORK: out <= 1;
default: out <= 0;
endcase
end
end
你想想看,三段式的好处是什么?输出是寄存器输出,没有毛刺,时序干净。而且状态跳转和输出逻辑完全解耦,想改输出?只动第三段就行,不影响状态机核心逻辑。
我记得有一次做Lattice的FPGA项目,芯片资源比较紧张。我用三段式状态机,综合工具能自动优化寄存器的位置,时序收敛得特别快。换成两段式,组合逻辑输出路径太长,setup time老是过不了。从那以后,但凡状态机超过5个状态,我无脑选三段式。
- 一段式:状态+输出全在一个always块,输出是时序的,延迟一拍。适合极简场景。
- 两段式:状态跳转时序,输出组合。结构清晰,但输出有毛刺风险。适合中等复杂度。
- 三段式:状态跳转时序,次态组合,输出时序。无毛刺,时序好,可维护性强。工程首选。
2.4 怎么选?我的实战建议
别纠结,直接看场景:
| 场景 | 推荐风格 | 理由 |
|---|---|---|
| 状态数 <= 4,输出简单 | 一段式 | 代码少,够用就行 |
| 状态数 5-10,输出组合逻辑 | 两段式 | 结构清晰,调试方便 |
| 状态数 > 10,或输出对时序敏感 | 三段式 | 时序好,可维护性强 |
| Lattice/低端FPGA | 三段式 | 资源利用率高,时序易收敛 |
| 团队协作项目 | 三段式 | 代码规范,别人容易看懂 |
我个人习惯是:新项目一律三段式起步。为什么?因为后期需求变更太常见了。你今天觉得状态机简单,明天产品经理就给你加两个状态、三个输出。一段式改起来?改到想骂人。三段式?加个case分支就行,稳得很。
2.5 知识体系一览
下面这张图,把三种风格的核心逻辑和适用场景串起来了。你保存下来,写代码前瞄一眼,心里就有数了。
好了,三种风格的区别和选择就聊到这儿。说白了,没有绝对的好坏,只有合不合适。你写代码的时候多想想:这个状态机以后会不会改?输出有没有毛刺风险?团队其他人能不能看懂?想清楚了,选哪种都行。