三段式状态机规范:第一段、第二段、第三段
说实话,我刚入行那会儿,写状态机都是「一段式」——一个 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
核心要点:三段式的精髓就是「各司其职」。第一段管寄存,第二段管判断,第三段管输出。每一段只做自己的事,互不干扰。
三段式的整体结构图
下面这张图,是我自己画的三段式状态机结构。你看一眼就明白了:
避坑指南
我这些年总结的几个常见问题,你写代码的时候注意一下:
- 第二段别漏 default:组合逻辑的 case 必须写 default,否则综合出锁存器。我吃过这个亏,仿真没问题,上板就随机出错。
- 第三段用时序输出:组合逻辑输出虽然省一个寄存器,但毛刺问题很头疼。尤其是驱动后级触发器的使能信号,毛刺可能导致误触发。
- 状态编码用独热码还是格雷码?我个人建议:状态少于 8 个用独热码,综合后面积小;状态多的话用格雷码,减少跳变时的毛刺。
总结一下:三段式状态机不是花架子,它是经过工程验证的成熟结构。第一段管寄存,第二段管判断,第三段管输出。各司其职,代码清晰,时序可控。你按这个规范写,综合、仿真、调试都会省心很多。
可可精品资料库