news 2026/9/8 8:15:56

Verilog状态机入门:从一段式到三段式及串口接收实例

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Verilog状态机入门:从一段式到三段式及串口接收实例

1. 从组合逻辑到状态机:为什么你的FPGA迟早要用它

我记得第一次接触Verilog状态机,是在一个按键控制的流水灯项目里。当时刚学会always块和assign,觉得写逻辑没什么大不了的——LED从左往右亮,用一个计数器打拍子就行,简单。结果产品经理轻飘飘加了一句话:“按一下按键,流水方向反过来。”现场就凉了半截。计数器还在那跑,我临时加了一个方向标志位,然后发现按键按下去那一下方向是变了,可计数值也在变,流水灯跳了好几格。再往下折腾“暂停”“变速”“复位到初始态”这些需求,代码里的标志位越堆越多,状态之间互相影响,查一个bug要来回翻几个always块。

那个项目最终被我推倒,用状态机重写了一遍,才算是真正消停了。后来带过不少做数字IC和FPGA的应届生,很多人遇到的第一个分水岭也是“什么时候意识到自己该用状态机了”。Verilog状态机不是一种语法糖,更不是考试八股,它是把“一堆复杂的、带时序的控制逻辑”整理成一张可以被人类理解、被工具综合、被波形验证的图纸。凡是控制为主、时序纠缠的逻辑,用计数器加标志位堆,代码量翻一倍,bug量翻两倍;用状态机,逻辑清晰,边界清楚,仿真也好写。

这篇内容不打算讲FIFO、不聊握手协议,就只讲最核心的Verilog状态机基础:它是什么、有哪几种写法、怎么选编码、怎么写不会跑飞、以及一个完整的串口接收案例。适合刚学Verilog想搞懂状态机的学生,也适合已经写了几个小模块但总是“跑飞”“出现latch警告”“波形乱跳”的开发者。

1.1 状态机的三个要素,和一台自动售货机

有限状态机(FSM)的核心思想是:系统在任意时刻处于有限个状态中的一个,当满足某些条件时,从一个状态跳转到另一个状态,并在过程中产生输出。很多人第一次接触这个概念觉得抽象,换个说法就通了——自动售货机。

一台只卖可乐的自动售货机,投币5元出一罐,投币10元呢?出两罐再找零也行,我们简化成:它始终处在两个状态里,一个是待机,一个是出货。你投了5元硬币,这个事件就是状态转移条件,机器从待机跳到出货,出了可乐再回到待机。如果机器卡住了,停在出货状态不动,那就是“状态机卡死”,对应到FPGA里就是状态跳到了未定义态再也回不来。

所以状态机三要素就三样:

  • 状态集合:系统当前可能处在的各个稳定阶段,对应Verilog里的localparam枚举常量。
  • 转移条件:从当前状态跳到下一状态需要满足的条件,对应组合逻辑或时序逻辑里的case分支判断。
  • 输出:在某个状态下或某次转移时对外产生的信号,对应摩尔型(Moore)和米利型(Mealy)两种输出方式。

理解了这三要素,再去看代码就不会晕。状态机的代码再怎么变,无非是把“状态如何转移”和“输出如何产生”这两件事用不同的风格写出来。

1.2 状态机到底解决什么问题

再回想一下那个流水灯反方向的需求。不用状态机时,你的脑子里要同时维护“计数器走到哪了”和“方向是哪个”两个变量;再加上“暂停”“复位”之后,你还得考虑“计数器和方向之间有没有时序耦合”。这种失控感,本质上是逻辑的先后顺序没有被显式表达出来——方向翻转和计数器归零到底哪个先发生,代码里没有任何一条约束来保证。

状态机把这种先后顺序写死了:每一个状态是一个明确的时间切片,只有状态跳转发生时,内部变量才允许大范围变化;在同一个状态里,计数器哪怕清零,也是这个状态自身的行为。这等于给时序逻辑加了一堵墙,把混乱关在里面。

1.3 手写状态机的通用骨架

虽然我后面会讲一段式、两段式、三段式的区别,但先给一个最通用的三段式模板(这是工业项目里最常见、也最不容易出问题的写法),先把“骨架”立起来。

// 状态定义 localparam S_IDLE = 2'd0; localparam S_WORK = 2'd1; localparam S_DONE = 2'd2; reg [1:0] state_c; // 组合逻辑里计算出的下一状态 reg [1:0] state_n; // 时序逻辑里寄存后的当前状态 // 第一段:状态寄存器,时钟沿打拍 always @(posedge clk or negedge rst_n) begin if (!rst_n) state_n <= S_IDLE; else state_n <= state_c; end // 第二段:组合逻辑,计算下一状态和输出 always @(*) begin state_c = state_n; case (state_n) S_IDLE: begin if (start_i) state_c = S_WORK; else state_c = S_IDLE; end S_WORK: begin if (done_cnt) state_c = S_DONE; else state_c = S_WORK; end S_DONE: state_c = S_IDLE; default: state_c = S_IDLE; endcase end // 第三段:时序逻辑,寄存器输出 always @(posedge clk or negedge rst_n) begin if (!rst_n) work_done_o <= 1'b0; else if (state_n == S_DONE) work_done_o <= 1'b1; else work_done_o <= 1'b0; end

先别急着理解每一段的细节,后面会拆开讲。记住一个结论:能只用计数器加if解决的简单逻辑可以用计数器,一旦涉及到“多阶段流程控制、分支条件较多”,直接用状态机,别挣扎。

2. 状态机的三种经典写法:一段式、两段式、三段式怎么选

拿到一个新的状态机需求,新人问得最多的问题是:“用一段式两段式还是三段式?”网上答案各有偏向,有些教程说三段式好,有些说两段式简单。我这些年写下来,结论是:没有绝对好坏,但不同写法的可维护性、出bug的难易程度确实差异很大。

2.1 一段式:全部塞进一个always块,写起来爽,查起来哭

一段式把所有逻辑——状态转移、状态寄存、输出产生——全部写在一个always @(posedge clk)块里。

always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= S_IDLE; work_done_o <= 1'b0; end else begin case (state) S_IDLE: begin work_done_o <= 1'b0; if (start_i) state <= S_WORK; end S_WORK: begin if (done_cnt) begin state <= S_DONE; work_done_o <= 1'b1; end end S_DONE: begin state <= S_IDLE; work_done_o <= 1'b0; end endcase end end

一段式的优点是直观,所有行为都写在同一个时钟节拍里;缺点也很明显:当状态机和输出逻辑交织在一起,你想要单独看“输出在什么条件下有效”时,必须在case的各个分支里翻来翻去找。而且一段式很容易漏掉对输出的默认赋值——就像上面代码里,S_WORK状态里work_done_o保持上一次的值,综合后可能生成锁存器(latch),报一堆警告。工程上一个状态机动辄七八个状态、十几个输出信号,一段式根本维护不住。

2.2 两段式:状态寄存器一段,状态转移与输出一段

两段式是把一段式拆开:第一段时序逻辑负责状态寄存,第二段组合逻辑负责计算下一状态和产生输出。

reg [1:0] state; reg [1:0] next_state; always @(posedge clk or negedge rst_n) begin if (!rst_n) state <= S_IDLE; else state <= next_state; end always @(*) begin next_state = state; work_done_o = 1'b0; // 组合逻辑直接用阻塞赋值 case (state) S_IDLE: begin if (start_i) next_state = S_WORK; end S_WORK: begin if (done_cnt) begin next_state = S_DONE; work_done_o = 1'b1; end end S_DONE: next_state = S_IDLE; endcase end

两段式比一段式清晰不少,状态转移和输出都在组合逻辑里集中描述,不容易漏默认输出。但它有一个坑:组合逻辑产生的输出可能有毛刺。如果你的work_done_o要被外部电路采样,而外部电路对毛刺敏感(比如接异步复位、接跨时钟域逻辑),组合输出直接驱动外部就不太稳。另外,组合逻辑里的case如果不用default兜底,或者漏掉某个状态分支,综合器可能生成意想不到的锁存器。

2.3 三段式:状态转移、下一状态计算、输出寄存分开

三段式在前面模板里已经见过,这里重点说说它解决了两段式什么问题。

三段式把输出单独放到一个时序逻辑always块里,通过判断state_n来寄存输出。这样做有几个直接好处:

  • 输出信号是寄存器输出,经过时钟打拍,消除了组合毛刺。
  • 组合逻辑块只管算下一状态,case语句里写转移条件即可,不需要考虑输出。
  • 状态寄存、转移计算、输出寄存三个部分彼此独立,任何一个环节出问题,定位速度极快。

代价是代码量稍微多一点。但对工程维护来说,这点代价完全值得。尤其在状态机后期加入状态扩展(比如从8个状态变成12个),三段式只需要在组合逻辑块里加case分支,输出块里加对应的判断,不会牵一发动全身。

always @(posedge clk or negedge rst_n) begin if (!rst_n) work_done_o <= 1'b0; else if (state_n == S_DONE) work_done_o <= 1'b1; else work_done_o <= 1'b0; end

有人会问:“输出寄存一拍,会不会导致时序上慢了一个周期?”确实会,但绝大多数控制信号对一拍延迟不敏感。如果实在对时序敏感,可以在设计时把状态转移条件和输出逻辑的时序在文档里标清楚,别等到综合之后再去补。

2.4 三种写法的选型对照

写法代码量可读性输出毛刺适用场景
一段式最少差,逻辑混杂极简单的时序控制,教学演示
两段式中等较好有,组合输出功能简单、输出不受毛刺影响的模块
三段式稍多最好,三部分职责清晰无,寄存输出工程主流,推荐所有新项目采用

个人经验:除非你只是想快速跑通一个仿真,否则直接上三段式。我见过很多开发者一开始图省事用一段式,后来加需求加到崩溃再翻写成三段式,中间的返工成本远大于一开始多写10行代码。

2.5 摩尔型与米利型:输出到底跟谁有关

状态机还有一个绕不开的分类——摩尔型(Moore)和米利型(Mealy)。

  • 摩尔型:输出只取决于当前状态,与输入无关。好处是输出稳定,不会随输入瞬间抖动;坏处是“反应慢半拍”,因为要等状态跳转完成后才能改变输出。
  • 米利型:输出同时取决于当前状态和输入。好处是响应快,输入一变化,组合逻辑立刻反映到输出;坏处是容易出毛刺,输入的一个毛刺会直接穿到输出端。

实际工程里,三段式状态机配合寄存器输出,很多人写的是摩尔型思路;米利型的输出通常在组合逻辑块里直接产生,或者单独再包一层寄存器。选型建议:控制类逻辑优先用摩尔型,因为稳定;对响应速度要求极端的场景(比如总线仲裁)才用米利型,并且一定要在关键输出路径上加时序约束。

3. 编码方式与跳转细节:状态机跑飞的全部可能原因

状态机写好了,仿真一跑,波形乱跳,状态从S_IDLE一下蹦到了一个没定义的数,从此再也回不来。相信每个写FPGA的人都见过这种场面。要么是复位没做干净,要么是case漏分支,要么是状态编码和状态位宽不匹配。这里把跑飞的原因一个个拆开。

3.1 二进制编码、独热码、格雷码:什么时候选哪个

状态编码是三段式状态机里最先要决定的事。常见三种:

编码方式状态数N需要宽度特点适用场景
二进制编码(Sequential)clog2(N)位位宽最小,状态转换逻辑较多状态少、逻辑资源紧张
独热码(One-hot)N位,每状态只有1位为1译码简单,速度快,功耗高状态多、时序要求高,FPGA推荐
格雷码(Gray)clog2(N)位相邻状态只有1位变化跨时钟域时降低毛刺风险,状态顺序跳转场景

FPGA里为什么常用独热码?因为FPGA的寄存器资源丰富,而且独热码的状态判断就是一个位的判断:if (state[2])就代表在第2个状态,根本不需要复杂的译码组合逻辑。状态数一多,独热码的时序往往比二进制编码更好收敛。代价就是位宽变宽,但FPGA不在乎这点寄存器。

二进制编码更适合资源紧张或状态数非常少(3~4个)的场景。格雷码一般用在状态连续递增/递减的计数器式状态机,或者需要跨时钟域传递状态值的场景。

3.2 case语句的default分支与状态变量位宽:两个最基础的坑

第一个坑:漏写default。Verilog里case语句如果没有匹配项,组合逻辑输出的state_c会保持原值,这等于给状态锁存器提供条件,综合器会给你一个latch警告。有了default,未定义的状态会跳回复位态,状态机才具备自恢复能力。

case (state_n) S_IDLE: ... S_WORK: ... S_DONE: ... default: state_c = S_IDLE; // 必须有,兜底自恢复 endcase

第二个坑:状态枚举和位宽不匹配。比如定义了8个状态,用独热码需要8位,你写成reg [2:0] state,8个状态根本装不下,状态直接重叠,综合时工具会报warning甚至error。这个错误在仿真阶段很容易被漏掉,因为状态数量少、波形看着“好像还能跑”,但上板后行为完全不对。写完状态定义后,先自己算一遍位宽够不够,别等综合报warning。

3.3 未定义状态与自恢复机制

跑飞的本质是:状态寄存器里出现了一个既不对应任何localparam、也不在case任何分支里的值。可能是上电时复位没完全清干净、跨时钟域毛刺打进了寄存器、也可能是状态位宽有冗余但没处理。

解决思路是双保险:

  1. case里写default,把未知状态强制引导到复位态。
  2. 在状态机里加一个“状态非法检测”:用组合逻辑判断当前状态是否落在合法集合里,如果不在,直接触发复位或跳到安全状态。

代码示意(检测非法状态并告警):

wire state_valid = (state_n == S_IDLE) || (state_n == S_WORK) || (state_n == S_DONE); always @(posedge clk or negedge rst_n) begin if (!rst_n) state_error_o <= 1'b0; else if (!state_valid) state_error_o <= 1'b1; // 拉高告警 else state_error_o <= 1'b0; end

这条告警信号建议接到测试口或者LED上,在调试阶段极有用。状态机为什么跑飞、什么时候跑飞,从这个信号上一眼就能看到。

4. 串口接收状态机实战:从协议到RTL的完整推导

前面讲了一堆理论,这里用一个非常典型的案例——UART串口接收模块,把状态机的设计流程完整过一遍。选串口接收做例子,是因为它的状态划分足够典型,既有空闲等待、又有中间采样、还有结束判断,非常贴合“状态机是控制协议翻译器”这个本质。

4.1 协议层拆解:状态从哪里来

看一眼UART一帧数据的时序,就明白状态是怎么来的了。以最常见的8N1格式为例:空闲时线路为高电平,发送方先拉低1位时间作为起始位,接着发送8个数据位(LSB在前),最后拉高1位时间作为停止位。

也就是说,接收端从头到尾经历的过程是:空闲(等起始位)→ 检测到起始位下降沿 → 采样8个数据位 → 等停止位 → 回到空闲。这不就是四个状态吗?

状态含义跳转条件
IDLE空闲,线路为高,等待起始位检测到rx_line拉低,进入START
START已检测到起始位,等待采样中点计数到中点,进入DATA
DATA依次采样8个数据位8位采完,进入STOP
STOP停止位阶段,确认线路拉高计数结束,回IDLE

状态划分遵循一个原则:每个状态对应协议里的一个明确阶段,状态的迁移对应协议阶段之间的明确事件。有的同学会把DATA按bit再拆成8个子状态,写起来很烦也没必要——用一个计数器在DATA状态里数8次就够了。

4.2 波特率分频与采样点计算

UART常用波特率9600,意味着每秒传输9600个bit,每个bit的持续时间是1/9600 ≈ 104.167us。FPGA系统时钟假设50MHz(周期20ns),那么每个bit持续50MHz / 9600 ≈ 5208个时钟周期。

采样时机要注意,为了避开数据位的边沿抖动,接收端通常在数据位的中点采样。所以START状态里计数器计到中点(约2604)时确认起始位有效,DATA状态里计数器每计满5208采样一次,一共采8次。

这个“中点采样”的细节非常能体现状态机的价值:如果不用状态,你得同时维护“现在是第几个bit”和“当前的采样时间窗”,两个计数逻辑缠在一起;用了状态机,DATA状态只需要管“采满8次没有”,每次采样的间隔由内部的cnt统一维护。

4.3 完整RTL设计:三段式串口接收状态机

模块io定义:clk系统时钟,rst_n低复位,rx_line是串口输入,rx_data_o是接收到的并行8bit输出,rx_done_o是接收完成脉冲。

module uart_rx_fsm ( input wire clk, input wire rst_n, input wire rx_line, output reg [7:0] rx_data_o, output reg rx_done_o ); // 参数定义 localparam CLK_FREQ = 50_000_000; localparam BAUD_RATE = 9_600; localparam BAUD_CNT_MAX = CLK_FREQ / BAUD_RATE - 1; // 5207 localparam BAUD_CNT_MID = CLK_FREQ / BAUD_RATE / 2 - 1; // 采样中点 // 状态定义,用独热码 localparam S_IDLE = 4'b0001; localparam S_START = 4'b0010; localparam S_DATA = 4'b0100; localparam S_STOP = 4'b1000; reg [3:0] state_c; reg [3:0] state_n; reg [12:0] baud_cnt; // 波特率计数,2^13=8192 够用 reg [2:0] bit_cnt; // 数据位计数,0~7 // 第一段:状态寄存器 always @(posedge clk or negedge rst_n) begin if (!rst_n) state_n <= S_IDLE; else state_n <= state_c; end // 第二段:组合逻辑计算下一状态和关键控制信号 always @(*) begin state_c = state_n; case (state_n) S_IDLE: begin if (rx_line == 1'b0) state_c = S_START; // 检测到起始位下降沿 else state_c = S_IDLE; end S_START: begin if (baud_cnt == BAUD_CNT_MID) state_c = S_DATA; // 到达起始位中点,确认起始有效 else state_c = S_START; end S_DATA: begin if (bit_cnt == 3'd7 && baud_cnt == BAUD_CNT_MAX) state_c = S_STOP; // 8位采完 else state_c = S_DATA; end S_STOP: begin if (baud_cnt == BAUD_CNT_MAX) state_c = S_IDLE; // 停止位结束,回空闲 else state_c = S_STOP; end default: state_c = S_IDLE; endcase end // 第三段:时序逻辑,分别产生波特率计数、位计数和输出 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin baud_cnt <= 13'd0; bit_cnt <= 3'd0; end else begin case (state_n) S_IDLE: begin baud_cnt <= 13'd0; bit_cnt <= 3'd0; end S_START: begin if (baud_cnt == BAUD_CNT_MID) baud_cnt <= 13'd0; else baud_cnt <= baud_cnt + 1'b1; end S_DATA: begin if (baud_cnt == BAUD_CNT_MAX) begin baud_cnt <= 13'd0; bit_cnt <= bit_cnt + 1'b1; end else baud_cnt <= baud_cnt + 1'b1; end S_STOP: begin if (baud_cnt == BAUD_CNT_MAX) begin baud_cnt <= 13'd0; bit_cnt <= 3'd0; end else baud_cnt <= baud_cnt + 1'b1; end default: begin baud_cnt <= 13'd0; bit_cnt <= 3'd0; end endcase end end // 输出寄存:DATA状态下,在采样点把rx_line存入移位寄存器 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin rx_data_o <= 8'd0; end else if (state_n == S_DATA && baud_cnt == BAUD_CNT_MAX) begin rx_data_o <= {rx_line, rx_data_o[7:1]}; // 右移,先收到的LSB排最高位?注意顺序 end end // 停止位结束产生接收完成脉冲 always @(posedge clk or negedge rst_n) begin if (!rst_n) rx_done_o <= 1'b0; else if (state_n == S_STOP && baud_cnt == BAUD_CNT_MAX) rx_done_o <= 1'b1; else rx_done_o <= 1'b0; end endmodule

这里有个很细节、也很容易搞错的地方:串口数据LSB先发。假设发送方依次发bit0、bit1……bit7到rx_line,接收端移位寄存器的拼接方向要保证最后rx_data_o[0]是bit0。上面代码里用{rx_line, rx_data_o[7:1]},第一次采样rx_line进入最高位,第二次采样第二次的rx_line进入最高位,同时原来的最高位移到次高位。这样8次之后,第1次采到的bit0被挤到了最低位,刚好符合预期。这个方向反了,整个串口收到的字节就是按位镜像的,查bug时非常隐蔽。

4.4 仿真验证的几个关键点

写testbench时,不要只盯着最终rx_done_o是否拉高,中间三个地方一定要看:

  1. 起始位检测:发送端把rx_line拉低后,状态是否在下一个时钟沿从S_IDLE跳到了S_START
  2. 采样时刻DATA状态下,baud_cnt计数到BAUD_CNT_MAX产生采样脉冲时,rx_data_o是否逐位更新。
  3. 停止位窗口S_STOP状态里,如果线路是低电平(发送端出错,没拉回高),状态机是否还坚持等完整个时间片。这验证了状态机对协议异常的基本容忍。

仿真命令示例(Vivado或ModelSim大同小异):

$dumpfile("uart_rx_fsm.vcd"); $dumpvars(0, uart_rx_fsm_tb);

testbench里用任务发送一帧数据,比如0xA5:

task uart_send_byte(input [7:0] data); integer i; begin rx_line = 1'b1; #104167; rx_line = 1'b0; // 起始位 #104167; for (i = 0; i < 8; i = i + 1) begin rx_line = data[i]; // LSB在前 #104167; end rx_line = 1'b1; // 停止位 #104167; end endtask

我实操的经验是:仿真里把发送端的10个bit(起始位+8数据+停止位)用等间隔的2个#104167隔开,但实际中如果时钟频率和波特率分频不匹配,采样点在每个bit里的位置会偏移。夸张一点,把主时钟频率改成50.001MHz,你就能看到采样点逐渐向bit边界偏移,最后在第7个bit上采到错误值。这个仿真手法能直观验证状态机的采样窗口设计是否留了裕量。

5. 我在状态机上踩过的五个经典坑

理论和案例都讲完了,最后分享一些实践里反复踩出来的经验。这些坑在教科书里很少被提到,但每一个我都真实遇到过,有些甚至带来过整板联调时半夜排查的经历。

5.1 不画状态转移图直接写代码

这是我见过最多新手的通病,包括当年的自己。拿到需求,脑子还没把状态梳理清楚,就直接打开编辑器敲localparam。结果写到一半发现还缺一个状态,又回头改枚举定义,改完case分支,又开始纠结输出是哪个状态下拉高,整个模块越写越乱。

建议是:任何状态机,动手前先在纸上画气泡图,或者用文本列一个“状态-转移条件-输出”三列表。状态少于5个的,画图10分钟;状态多于10个的,画图30分钟,但可以帮你省掉的返工时间是以天计的。这个习惯我一直用到现在,简单的小模块也会先列个表。

5.2 状态变量既有组合逻辑也有时序逻辑,代码里写混了

上面三段式模板里我特意用了state_n(寄存的当前状态)和state_c(组合算出的下一状态)两个变量。有同学会问:能不能只用state一个变量?可以,但代价是你得在一个always块里同时处理寄存和跳转,回到一段式的老路上,输出又容易出锁存器。

更隐蔽的问题是:在组合逻辑块里对state_n赋值时漏了某些路径。比如:

always @(*) begin state_c = state_n; case (state_n) S_A: if (cond) state_c = S_B; // 如果cond不满足,走默认保持 S_B: state_c = S_A; endcase end

看起来没问题,因为state_c = state_n已经做了默认保持。但如果我把这个默认赋值写在case之后而非之前,某些分支里又提前return式地赋值(Verilog不支持return,但容易用if-else嵌套出漏路径),就可能出现某个输入条件下state_c没有被赋值,组合逻辑的输出开始依赖历史,生成锁存器。写组合逻辑块时,第一行就先写默认保持+默认输出,后面case里只写需要改变的部分,这是最稳的写法。

5.3 状态枚举和信号位宽长短不匹配

用独热码时,4个状态写reg [3:0]是正确的;但如果是二进制编码,4个状态只需要reg [1:0]。很多新手搞混,或者干脆统一用reg [7:0]图省事。位宽写宽了不会报错,但浪费资源还容易让仿真波形里出现一堆冗余的高位;位宽写窄了直接爆。

建议:状态变量统一用宏定义或者localparam,位宽单独算好,不要凭感觉。独热码状态数就是位宽,二进制编码向上取log2加1(比如6个状态需要3位,8个状态需要3位)。

5.4 输出信号没有寄存,导致外部电路莫名其妙误触发

两段式的组合输出毛刺问题,前面反复提过。这里给一个真实案例:我做过一个ADC采样控制模块,用两段式状态机产生片选信号,结果示波器上一看,片选线下行沿有一串几十纳秒的毛刺,ADC直接多触发了好几次采样。排查了很久,最终把片选输出改成寄存器输出,毛刺消失。

规约:状态机的对外控制输出,一律加一级寄存器。除非你能确认下游是组合逻辑且对毛刺不敏感,否则别省这一个always块。三段式的第三段就是干这个用的。

5.5 复位风格不统一,异步复位和同步复位混用

写状态机必然涉及复位,但新手常常一会儿用negedge rst_n做异步复位,一会儿又依赖同步复位,整个模块里风格不统一。这在功能上不一定会立刻出错,但综合时容易产生复位树问题,而且在仿真时复位时序稍微没对好,状态机就可能拿着一个未复位的初值开始跑。

建议:一个模块统一用一种复位风格。FPGA工程里最常用的是异步复位、同步释放(异步复位信号先打两拍,再用作同步复位),既避免了异步复位的亚稳态问题,也保持复位风格的统一。低电平复位就用negedge rst_n,每个时序always块都要带复位分支,别留裸奔的寄存器。

我把这五个坑对照整理成了表格,调试时可逐一排查:

现象排查方法
没画状态图硬写模块越改越乱停下来画气泡图,梳理状态-转移-输出表
状态变量赋值漏路径出现latch警告,状态跳转随机组合逻辑块首行写默认赋值
状态枚举位宽不匹配综合warning,状态重叠核对编码方式与位宽计算
输出未寄存外部信号毛刺、误触发输出改为三段式寄存器输出
复位风格不统一仿真复位异常,上板状态不明统一异步复位+同步释放

最后分享一个调试小技巧

写这篇文章时,我想起一个几乎每次调试状态机都会用到的小工具:把状态编码映射成reg然后接到测试口或LED上,同时用localparam里的名字在仿真波形里显示。比如Vivado仿真里,右键信号,选择“Radix”再改成“ASCII”,或者在testbench里用$display打印状态名:

always @(posedge clk) begin if (state_n != state_c) $display("Time=%0t FSM: %s -> %s", $time, state_name(state_c), state_name(state_n)); end

配合一个把状态值转字符串的函数,跑完仿真直接看状态跳转日志,几分钟就能定位是哪一步跳错了,比盯着波形猜快太多。这个技巧陪伴了我好几年,每次写新状态机都会第一时间把这个打印逻辑挂上,等模块稳定了再摘掉。希望你也能借着这篇文章,把Verilog状态机这个基本功打扎实。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 8:13:07

Spring Boot + JDBC多数据源配置实战:从双JdbcTemplate到动态路由

简介&#xff1a;Spring Boot与JdbcTemplate结合实现多数据源管理&#xff0c;是一份面向Java后端开发者的实用工程示例。资源围绕主、从两个数据源的创建与使用展开&#xff0c;演示了通过DataSourceBuilder构建Bean、在application配置中绑定不同prefix参数&#xff0c;并利用…

作者头像 李华
网站建设 2026/9/8 8:12:56

Rust重写微服务通信与数据同步:高可用异步架构实战

去年我们团队用RUST重写了一套微服务间的通信与数据同步系统&#xff0c;从最初的gRPC接口、异步任务调度&#xff0c;到连接池管理、增量数据同步、高可用切换&#xff0c;前前后后折腾了大半年。这期间踩了不少坑&#xff0c;也沉淀了不少经验&#xff0c;今天把整条技术链路…

作者头像 李华
网站建设 2026/9/8 8:12:08

HTML+CSS制作“我的家乡”网页模板:从零到完整页面的前端基本功

简介&#xff1a;HTMLCSS模板『我的家乡』是一套以地域文化为主题的响应式网页制作模板&#xff0c;适合网页设计初学者、前端学习者以及需要快速搭建家乡主题展示页的开发者使用。模板通过HTML定义页面结构、CSS控制视觉样式&#xff0c;完整覆盖首页、文化、历史、特产、名人…

作者头像 李华
网站建设 2026/9/8 8:11:32

Altium Designer许可证选型与部署:单机版和网络版怎么选

前阵子帮一家客户做Altium Designer的许可证规划&#xff0c;对方采购开口就问&#xff1a;"我们8个工程师&#xff0c;买5个网络版够不够&#xff1f;"这个问题听起来简单&#xff0c;背后其实牵扯到并发建模、网络拓扑、出差场景和预算分摊&#xff0c;答不好要么买…

作者头像 李华
网站建设 2026/9/8 8:06:17

Linux网卡配置全攻略:从入门到生产环境稳定调优

1. 项目概述与核心需求解析1.1 网卡配置到底在配什么干过Linux服务器运维的人都知道&#xff0c;网卡配置这件事看着简单&#xff0c;真上手到处是坑。搜索“linux 网卡配置”的&#xff0c;多半不是找不到配置文件&#xff0c;就是改了配置起不来&#xff0c;再要不就是好不容…

作者头像 李华
网站建设 2026/9/8 8:05:59

PyTorch从零实现BERT文本分类:完整指南与踩坑记录

简介&#xff1a;面向 PyTorch 与自然语言处理开发者&#xff0c;这套代码资源完整实现了基于谷歌 BERT 模型的自然语言处理流程&#xff0c;侧重从理论到工程的过渡环节&#xff0c;可帮助读者快速理解双向 Transformer 结构、预训练与微调思想&#xff0c;并顺利跑通文本编码…

作者头像 李华