刚开始用 Verilog 写状态机,老师讲的是「三段式」写法,但我自己写的时候要么状态跳转有漏项,要么综合后波形跟预期完全不一样。尤其是带计数器、握手信号(ready/valid)的时候,很容易写出「看着能运行,实际一堆隐含锁存器和时序违例」的代码。想请教:在实际 FPGA 工程里,大家写状态机会遵循什么固定模板?比如状态枚举、状态转移、输出逻辑分别怎么组织?有没有一些典型示例可以照着改,比如「串口接收」「简单总线握手」「数据包解析」这几类场景?
Verilog 写状态机总是乱套,有没有一套「模板」?
提问
回答 4

兄弟,你这问题太真实了,我刚学的时候也这样。别慌,工业界确实有套万能模板。核心就是三段式加枚举,别自己瞎编二进制状态。第一段用always @(posedge clk)直接给next_state赋值给cur_state,这步最简单。第二段用always @()写组合逻辑,里面就一个大case(cur_state),每个case里用if-else把所有输入组合全覆盖,最后一定要加default: next_state = IDLE; 不然综合出锁存器你哭都来不及。第三段输出逻辑也写组合always,别混时序。对于握手信号,你先画个图,valid和ready谁先拉高、谁等谁,想清楚了再写。推荐你先抄个串口接收模板,就那几个状态:空闲、起始、数据位、停止位,跑通了再改。

作为一个踩过无数坑的老油条,我建议你先把状态枚举用localparam定义好,别用`define,调试方便。然后记住一条铁律:状态跳转的always块里只写cur_state <= next_state,别的啥也别干。输出逻辑和跳转逻辑严格分开。对于ready/valid场景,我习惯把输出也寄存器化,就是输出再打一拍,这样时序好控制,虽然多一个时钟延迟但不会出违例。给你个具体例子:数据包解析状态机,先设IDLE、HEADER、DATA、DONE几个状态,握手时IDLE里等valid进来才跳到HEADER,HEADER里把数据收完再跳。default分支一定给next_state赋IDLE,输出全赋0,这样综合出来干干净净。

你描述的问题我太懂了,特别是带计数器的时候最容易乱。我的模板是:把计数器单独拿出来,别和状态机混在一个always块里。状态机只用三段式,计数器用另一个always计数,状态跳转时判断计数器的值。比如串口接收,空闲状态等start信号,检测到后启动计数器并跳到数据位状态,数据位里计数器每加一次收一个bit,收完8个就跳停止位。这样逻辑清晰,仿真看波形一眼就知道哪出错了。枚举状态名用大写加下划线,比如S_IDLE、S_START,仿真时显示直观。另外强烈建议你用仿真工具先跑边界情况,比如valid突然拉低、ready还没准备好,看看状态机卡不卡死。卡了就是组合逻辑漏条件,回去补default和else。

写状态机确实需要一套固定的模板,否则很容易出bug。我的经验是:先画状态转移图,然后用三段式结构实现。第一段是时序逻辑,用always @(posedge clk or negedge rst_n)更新当前状态和next_state;第二段是组合逻辑,用always @()写状态转移,case语句必须包含所有状态和default分支,default里把next_state赋为IDLE或某个安全状态,避免锁存器;第三段是输出逻辑,也是组合逻辑,根据当前状态直接赋值输出,或者用always @(posedge clk)寄存输出以改善时序。对于握手信号,我习惯在状态机里单独用两个寄存器控制ready和valid,并在组合逻辑里根据状态和输入条件更新它们,这样波形调试时一目了然。串口接收是我的第一个模板:空闲态等待start_bit,检测到下降沿进入数据位状态,用计数器逐位采样,最后进入停止位状态并输出数据。你可以先从这个练起,仿真时故意加一些干扰信号,看状态机能否正确恢复。
发表回答
登录后可在本页底部提交回答
