FPGA 入门 19:状态机入门——米利/摩尔、三段式与编码
更新时间:2026-09-12。本文是
fpga/入门层第 21 篇。三段式的 always/case 基础见 always 块与 case,锁存器坑见 意外锁存器,异步输入先同步见进阶 CDC 亚稳态。
本文要回答的问题
- 哪一类逻辑适合用状态机描述?状态机的「两要素」(状态寄存器 + 次态逻辑)分别对应什么硬件?
- 摩尔型(Moore)和米利型(Mealy)的差别到底是什么?输出打不打一拍各自的后果?
- 三段式状态机的三段各写什么、用阻塞还是非阻塞?为什么比一大段 always 更稳?
- 次态组合块为什么开头要先写
next = state、还要有 default?漏了会怎样? - binary、gray、one-hot 三种编码各用多少触发器、组合逻辑复杂度如何?为什么 FPGA 常默认 one-hot?
- 复位、异步输入、超时保护、组合输出毛刺,状态机在真实板上最常见的跑飞原因有哪些?
- 序列检测器「1101」这种例子,次态转移是怎么一步步推出来的?
- 状态机怎么仿真、怎么用 ILA 抓 state 定位「卡在某状态」的问题?
读完本文,你应该能为「按步骤走、遇事件跳转」的控制逻辑(UART、SDRAM 刷新、按键命令、总线仲裁、VGA 时序)独立画出状态转移图,并写出结构清晰、可综合、可调试的三段式 FSM。
一、状态机在描述什么
凡是“有若干个步骤/状态,根据输入事件在状态之间跳,处在不同状态做不同事”的逻辑,都用 FSM:UART 收发、SDRAM 刷新、总线仲裁、按键命令解析、VGA 时序……核心就两要素:
- 状态寄存器:记住“现在在哪个状态”;
- 次态逻辑:看着「当前状态 + 输入」决定“下一拍去哪”。
二、摩尔型 vs 米利型
| 摩尔 Moore | 米利 Mealy | |
|---|---|---|
| 输出取决于 | 只看当前状态 | 当前状态 + 输入 |
| 响应输入 | 慢一拍(等跳转) | 快一拍(不用跳转) |
| 输出毛刺 | 无(状态稳则输出稳) | 输入毛刺会传到输出 |
| 适合 | 控制、对外输出(默认) | 省状态/省一拍的紧耦合 |
新手默认摩尔;米利输出也建议寄存一拍再用。
三、三段式写法(序列检测器:检测 1101)
module seq_detector(
input wire clk, rst_n, din, // din: 每拍来 1 bit 串行数据
output reg found // 检测到 "1101" 拉高 1 拍
);
localparam S_IDLE = 3'd0, S_GOT1 = 3'd1, S_GOT11 = 3'd2,
S_GOT110 = 3'd3;
reg [2:0] state, next;
// ① 状态寄存器(时序)
always @(posedge clk or negedge rst_n)
if (!rst_n) state <= S_IDLE;
else state <= next;
// ② 次态逻辑(组合):判断已匹配到多长
always @(*) begin
next = state; // 默认保持, 防锁存器
case (state)
S_IDLE : next = din ? S_GOT1 : S_IDLE;
S_GOT1 : next = din ? S_GOT11 : S_IDLE;
S_GOT11 : next = din ? S_GOT11 : S_GOT110; // 11 后再来1仍算11
S_GOT110: next = din ? S_GOT1 : S_IDLE; // 110 后到1=命中
default : next = S_IDLE;
endcase
end
// ③ 输出逻辑(时序输出, 无毛刺)
always @(posedge clk or negedge rst_n)
if (!rst_n) found <= 1'b0;
else found <= (state == S_GOT110) && din;
endmodule要点:状态名一律 localparam;组合块开头先 next = state; 给默认值;default 回安全态;输出打一拍。
四、状态编码
| 编码 | FF 数(N 态) | 组合逻辑 | 典型用途 |
|---|---|---|---|
| binary 二进制 | ⌈log₂N⌉ 最省 | 较复杂 | 状态多、省寄存器 |
| gray 格雷 | ⌈log₂N⌉ | 相邻跳变 1 位 | 异步 FIFO 指针、低功耗 |
| one-hot 独热 | N 个 | 最简单(免译码) | FPGA 默认,LUT 多跑得快 |
FPGA 综合器(Vivado)常自动选 one-hot;用符号名写、编码交给工具,需要时在 XDC/属性里设
fsm_encoding。
五、先画状态图,再写代码
写 FSM 最不容易错的流程永远是「先图后码」:
- 列状态:把流程拆成有限个明确阶段(如 UART 发送:IDLE、START、DATA、PARITY、STOP),每个状态对应「正在做的一件事」。
- 找转移条件:在每个状态下,根据什么输入/事件跳到哪;条件不全的先画成「否则留在本态」。
- 定输出:每个状态/条件下要置起哪些控制信号(使能、选择、中断、片选)。
- 定初态与回家路径:复位后进哪个状态、每个等待状态等不到事件怎么办(超时/错误回 IDLE)。
- 再把图机械地翻译成三段式 RTL。
状态图能在写代码前暴露「某个状态有没有出口」「条件是否互斥完备」这类问题,比直接堆 if/else 可靠得多。凡是满足「有有限个阶段、在事件驱动下切换、不同阶段做不同事」的逻辑——通信收发、内存刷新、命令解析、总线仲裁、显示时序、菜单/按键流程——都优先考虑 FSM,而不是用一堆零散标志位(flag)硬拼。标志位一多,状态组合指数增长、互相约束不清,正是 bug 温床;FSM 用显式状态把这些组合收敛成一张可读的图。
六、三段式的时序细节
回顾标准三段式:
- 第一段(时序,状态寄存器):
always_ff里复位置初态,否则state <= next。只有这一段改写 state。 - 第二段(组合,次态):
always_comb开头next = state(默认保持,天然防 latch),再用 case(state)+输入算 next,覆盖所有状态并带 default 回安全态。 - 第三段(输出):可以组合,但推荐再用时序块寄存一拍。
为什么输出建议寄存?组合输出 = 状态/输入经 LUT 译码,状态跳转和输入变化时不同路径延迟不同,可能产生短暂毛刺(glitch);若直接驱动片外使能、时钟或握手,毛刺会误触发。寄存一拍后只在时钟沿更新,输出干净、时序也好收敛,代价是晚一个周期——大多数控制场景都付得起。
时序关系上要意识到:第二段算出的 next 是基于当前 state,第一段在下一个沿才把 next 存入 state;若第三段是时序输出,它响应的又是更新后的 state,因此比状态变化再晚一拍。设计握手时把这一、两拍延迟算进去,就不会出现「仿真对,联调差一拍」的问题(配合 阻塞与非阻塞 理解沿上采样)。
一段式/两段式为什么不推荐
- 一段式:在一个时序 always 里既切状态又算输出,写法看似省事,但容易把组合判断塞进时序块、输出时序关系混乱、加状态时容易出错。
- 两段式:状态寄存器 + 组合输出(或次态输出合一),简单 FSM 可用,但复杂后职责混杂、易生毛刺。
- 三段式用一点样板代码换清晰边界和可维护性,是工业界主流推荐。
七、Moore 与 Mealy 的工程取舍
- Moore 输出只看 state:同一状态输出恒定,天然配合「输出寄存」,最稳、最好测;多花状态或一拍。
- Mealy 输出看 state+输入:能省一拍/省状态,适合紧耦合快速反应(如检测到特定输入立刻拉信号),但输入毛刺直达输出,通常也寄存后使用,或确保输入已同步。
- 实践中常混用:关键对外信号用 Moore(稳),内部紧耦合判定用 Mealy(快),但都让输出经寄存器离开模块。
八、状态编码与资源
N 个状态的编码对比:
| 编码 | 触发器数 | 次态/输出逻辑 | 特点 |
|---|---|---|---|
| binary 二进制 | ⌈log₂N⌉ | 需译码,较复杂 | 省 FF,状态多时合适 |
| gray 格雷 | ⌈log₂N⌉ | 相邻仅 1 位变 | 低功耗/异步 FIFO 指针 |
| one-hot 独热 | N | 几乎免译码,最简单 | FPGA 默认倾向,速度快 |
| Johnson/自定义 | 介于其间 | 视设计 | 特殊场景 |
为什么 FPGA 偏好 one-hot:FPGA 里 FF 相对 LUT 宽裕,独热码「同一时刻只有一位为 1」使次态逻辑变成简单的置位/复位条件、状态判断常是单根线,LUT 级数少、容易跑高频;代价是多几个 FF,而 FF 通常不缺。新手不必手算编码:用 localparam 写符号状态,编码交给综合器(可在属性/设置里指定 fsm_encoding),先保证正确,再依据资源与时序报告调整。要验证综合器认出了 FSM,可在报告中查看状态寄存器编码和「safe FSM」选项(进入非法态时自动回初态的容错)。
九、实战骨架:一个 UART 发送 FSM
UART 发送是状态机的经典练习(完整实验见 UART 回环)。状态与转移大致是:
- IDLE:空闲,总线为高;收到发送请求(有效数据 + 启动脉冲)→ START;
- START:拉一个比特周期的起始位(低)→ DATA;
- DATA:按波特率节拍依次送出 8 个数据位(用一个位计数器 + 一个节拍计数器)→ STOP;
- STOP:发停止位(高)→ IDLE。
用三段式实现:状态寄存器管 IDLE/START/DATA/STOP;次态组合块在各状态根据「一个比特时间到」tick、位计数是否到 8 决定跳转(开头 next=state,default 回 IDLE);输出用时序块按 state 驱动 txd 和发送忙标志。波特率 tick 由计数器时钟使能产生(见 计数器与分频),不要造新时钟。这个例子同时练到:FSM、计数器、时钟使能、并串转换、复位——是入门综合度最高的实验之一。
写这类 FSM 的几个实战细节:①等待 tick 的状态默认留在本态,tick 到才走,避免漏写「否则保持」;②计数到位要清零/归位,最好在退出状态时统一处理;③输入「发送请求」若是异步来的,先打两拍同步并做边沿检测(见 按键消抖 的同步/稳定思路);④关键等待态加超时,防止对方异常导致永久停留。
十、状态机跑飞的六大原因与对策
- 次态 case 不全又没 default:进入未覆盖状态/输入时保持,可能死锁。对策:默认
next=state+ default 回 IDLE,并可开综合的 safe FSM。 - 复位不回初态:上电 state 是随机编码,可能直接落非法状态。对策:异步/同步复位把 state 置 IDLE,必要时用异步复位同步释放(见 时钟与复位)。
- 异步输入直接判断:外部信号与时钟不同相,亚稳态导致偶发跳错。对策:先两拍同步、再边沿检测(进阶 CDC 与亚稳态)。
- 等待事件永远不来:等 ready/应答但对端故障或信号没接对,状态机死等。对策:超时计数兜底回 IDLE 并置错误标志。
- 组合输出直接对外:译码毛刺误触发。对策:输出寄存一拍。
- 状态用魔法数字、条件交叠:难读且易写出互斥不全的条件。对策:localparam 命名、互斥条件写清楚、配合波形逐条走状态。
十一、怎么验证与调试状态机
- 单元仿真:testbench 直接驱动时钟/复位/输入,用断言或打印跟踪 state,覆盖所有转移、非法输入、复位、超时;先在波形里确认状态图逐条实现(见 Testbench、看波形)。
- 观察 state 的技巧:仿真按符号名看状态;综合后确认 FSM 被识别、编码符合预期。
- 上板调试:用 ILA(Xilinx,见 ILA 初用)或 SignalTap(Intel)抓 state、关键输入和输出,设置触发(如进入错误态、超时、某控制信号),复现「卡住/乱跳」时直接看停在哪个状态、哪个条件没满足。这比猜管脚和时钟高效得多(系统方法见进阶 调试方法论)。
- 覆盖率直觉:至少要让每个状态、每条转移线都在仿真里发生一次;只测「快乐路径」会漏掉边界。
十二、自测要点与学习地图
自测:① 给一个控制需求(如自动售货/电梯/UART)列出状态、转移、输出并画图;② 默写三段式模板,标明各段赋值符号和默认值;③ 解释 Moore/Mealy 及输出寄存的原因;④ 算 N 态时 binary/one-hot 各用多少 FF;⑤ 说出 FSM 跑飞的六个原因及对策;⑥ 说明为什么异步输入要先同步;⑦ 描述如何用 ILA 抓 state 定位死锁。
学习地图:语法基础看 always 与 case、意外锁存器;配套时序原语看 计数器与时钟使能、按键消抖;实战看 UART 回环、VGA 显示;深入看进阶 CDC 与亚稳态、调试方法论。
十三、把「1101」序列检测的状态图推出来
序列检测是理解「状态=已匹配的历史」最好的例子。每来 1 位输入,状态代表「截至目前已匹配到模式串的多长前缀」:
- S_IDLE:还没匹配到有效前缀;收到 1 → S_GOT1,收到 0 留 S_IDLE;
- S_GOT1:已匹配「1」;再收 1 → S_GOT11(「11」仍是更长前缀),收 0 退回 S_IDLE;
- S_GOT11:已匹配「11」;收 0 → S_GOT110,收 1 仍停在能匹配「11」的状态(新串以这个 1 作前缀);
- S_GOT110:已匹配「110」;再收 1 → 命中「1101」,拉高 found 一拍,同时这个结尾的 1 又可作为下一匹配的前缀 → 回 S_GOT1;收 0 则回 S_IDLE。
关键思想有两个:①状态编码的是「历史信息的等价类」,不必记住全部原始比特,只记「匹配到哪一步」;②命中跳转要考虑重叠匹配(1101 的最后一个 1 可能是下一个 1101 的开头),所以命中后回 S_GOT1 而不是 S_IDLE。先在纸上走几遍输入序列(如 1101101),确认输出脉冲位置,再写 case——这正是「先图后码」的价值。
十四、可直接套用的 SystemVerilog 三段式模板
module fsm_template #(
parameter int unsigned W = 4
)(
input logic clk, rst_n,
input logic start, tick, done,
output logic en,
output logic [W-1:0] data
);
typedef enum logic [1:0] { S_IDLE, S_RUN, S_WAIT, S_DONE } state_t;
state_t state, next;
// ① 状态寄存器
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) state <= S_IDLE;
else state <= next;
// ② 次态逻辑:默认保持,完整 case + default
always_comb begin
next = state;
unique case (state)
S_IDLE: if (start) next = S_RUN;
S_RUN : if (tick) next = S_WAIT;
S_WAIT: if (done) next = S_DONE;
else next = S_WAIT;
S_DONE: next = S_IDLE;
default: next = S_IDLE;
endcase
end
// ③ 寄存输出(Moore,只看 state,无毛刺)
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) begin en <= 1'b0; data <= '0; end
else begin
en <= (state == S_RUN || state == S_WAIT);
data <= (state == S_DONE) ? {W{1'b1}} : '0;
end
endmodule要点:用 typedef enum 命名字面状态,工具会自动编码并在仿真里显示状态名(比 localparam 更易读,SystemVerilog 工程推荐);unique case 让工具检查分支互斥/完整,缺失会告警;次态块唯一职责是算 next,不驱动输出;输出块用寄存器。综合时仍可指定 one-hot/binary 编码。
状态机设计检查清单
- [ ] 复位后进入明确初态;
- [ ] 每个状态都有出路(含「条件不满足留本态」);
- [ ] 次态 always_comb 开头 next=state,case 完整或有 default;
- [ ] 关键等待态有超时/错误回退,不会死等;
- [ ] 外部/跨时钟输入已先同步;
- [ ] 对外输出寄存,无组合毛刺;
- [ ] 一个状态寄存器只由一个 always_ff 更新;
- [ ] 仿真覆盖每个状态、每条转移、复位和异常输入;
- [ ] 综合报告无 latch,必要时启用 safe FSM。
十五、状态变多怎么办:层次化与表格化
真实控制器常有十几个甚至几十个状态,平铺一个大 case 会难维护。常用两种手段控制复杂度:
- 层次化 FSM(FSM 分层):主 FSM 只管大阶段(INIT、RX、PROC、TX),每个大阶段内部再用一个子 FSM 管细节(如 RX 内再分 START/DATA/STOP)。主从之间用 start/done 握手:主态发启动、等子 FSM 的完成脉冲再进下一阶段。这样每层状态都很少、各自可独立仿真。
- 数据通路与控制分离(FSM+DATAPATH):FSM 只产生控制信号(如 load、shift、clr、mux_sel),计数、比较、移位等数据运算放在独立模块/寄存器里,FSM 根据状态标志(zero、eq、full)做转移。不要把大量算术塞进次态 case,那会让组合路径又长又难收敛。
- 表格驱动:规则、强对称的协议可用「现态+输入 → 次态+输出」查找表描述,综合成 ROM/BRAM(见 BRAM 与分布式 RAM),但入门阶段优先用清晰的三段式。
用 valid/ready 或 start/done 做握手
模块间协作最常见的是脉冲/电平握手:启动方拉一个周期 start(或 valid),执行 FSM 进入忙状态 busy=1,完成后拉一个周期 done(或 ready)。设计要点:①脉冲只高一个时钟周期(边沿检测得到);②busy 期间忽略重复启动或排队;③跨时钟时握手要走 CDC 同步,不能直接采(进阶 CDC 与亚稳态、异步 FIFO)。FIFO 是多状态/解耦速率的终极手段,但其满/空标志背后仍是状态机加格雷码指针(见进阶 FIFO 设计)。
十六、常见外设里的 FSM 影子
把「状态机思维」迁移到后续实验,会发现它无处不在:
- UART 收发:START/DATA/PARITY/STOP 的节拍控制(UART 回环);
- SPI/I2C:按相位拉片选、移位、采样的小 FSM(SPI ADC);
- VGA/显示时序:行/帧计数器其实是两个联动的状态机,控制消隐与同步(VGA 显示);
- SDRAM/DDR:初始化序列、刷新、读写命令切换是典型复杂 FSM;
- 按键命令/菜单:用 FSM 替代一堆标志位(按键消抖);
- DMA/AXI 传输:地址推进、突发、响应处理由控制 FSM 驱动。
写这些模块时都先画状态图、明确每个状态的输出和回家路径,再套三段式模板,复杂协议就被拆成了你已经掌握的小块。状态机不是某个孤立知识点,而是描述一切「按时序步骤推进」的控制逻辑的通用语言,与计数器、时钟使能、握手、复位共同构成 FPGA 时序设计的核心工具箱。
一个可操作的练习路径
建议用递进的四个小练习把本文彻底练熟:①纯仿真的二态灯(按键启动/停止),只验证状态切换和复位;②三态以上的脉冲/命令解析,加入超时和非法输入;③序列检测器,重点推导重叠匹配;④UART 发送,加入位/节拍计数和真实波特率,并与接收端或电脑串口联调。每一步都严格走「画图→三段式→testbench 全覆盖→看综合报告确认无 latch→上板抓 state」的闭环。完成这四个练习后,再面对任何带「步骤、等待、超时、握手」的协议,你的第一反应都会是先画状态图,而不是用一堆相互牵连的标志位硬凑——这正是从「能写 Verilog」到「会设计时序控制器」的标志。把本文的三段式模板和检查清单存成个人代码库的起点,后续无论做多复杂的控制逻辑,骨架都不会变。最后再强调一个容易被新手忽视的心智转换:写状态机不是在写「随时间执行的程序」,而是在描述「一块组合逻辑根据当前状态和输入算出下一状态,一组寄存器在每个时钟沿把下一状态变成当前状态」。正因为如此,状态机天然是同步时序电路:所有跳转只在时钟沿生效、所有记忆都集中在 state 寄存器、所有判断都是当前状态的函数。把这个硬件模型想透,你在仿真波形里看到的就不再是神秘的「程序走到哪一行」,而是清晰的「state 在沿上从一个编码跳到另一个编码」;当系统卡死时,你也会条件反射地去查三件事:当前停在哪个状态、那个状态在等哪个条件、那个条件为什么一直不成立。这种可观测、可推理、可复位到确定起点的特性,正是 FPGA 里几乎所有可靠控制器都建立在状态机之上的根本原因。遇到复杂控制逻辑,与其在脑子里模拟无数标志位组合,不如回到那张状态转移图:它既是你设计时的蓝图,也是仿真、上板调试时与波形逐点对照的基准,能持续帮你把复杂度压到可管理的范围。
十七、状态编码的取舍与安全(默认/恢复)状态
状态机用几个触发器编码当前状态,而"怎么给状态分配二进制码"会影响资源、时序和可靠性,常见有四种编码。**二进制(binary)**用最少的触发器,N 个状态只需 log2(N) 个,但多个比特同时翻转、译码逻辑较复杂;**独热码(one-hot)**给每个状态一个触发器、任意时刻只有一位为 1,N 个状态用 N 个触发器,译码只需简单的位判断、速度快、毛刺少,特别契合 FPGA"触发器多、组合相对宝贵"的结构,是综合器对 FSM 的常用默认;**格雷码(gray)**让相邻状态只差一个比特,适合需要低功耗或跨时钟域传递状态的场合;还有约翰逊码等变体。综合工具通常提供 enum_encoding 一类属性或综合开关让你指定,小状态机交给工具自动选择即可,但要知道它为什么这么选、什么时候该手动干预。
比编码更关键的是非法状态与恢复。用二进制或独热码时,物理上可能出现的编码数往往多于实际状态数(比如 4 个独热位理论上有 16 种组合,合法的只有 4 种),上电扰动或单粒子翻转可能让状态机进入"谁也没定义"的非法态。如果 case 没有 default、或 default 什么都不做,机器就可能永久卡死。规范做法是:用 parameter/localparam 或 SystemVerilog enum 具名定义全部状态;在次态逻辑的 default 分支里明确把状态打回一个安全态(通常是 IDLE);必要时加看门狗——若长时间停在异常状态或未见预期应答,强制复位 FSM。这样即使误入非法态,下一周期也能自动回到已知状态。对高可靠场景,还可对状态寄存器做奇偶校验或三模冗余,但入门阶段先做到"枚举命名 + default 回 IDLE + 关键 FSM 有超时退出"这三条,就足以避免绝大多数状态机死机问题。
十八、状态机与数据通路的分工:别把数据塞进状态
初学者写控制器常犯一个结构性错误:把本应在数据通路里流动的大块数据也搬进状态机,用状态去"记住"整个数据字,导致状态数爆炸、编码混乱、难以维护。正确的分工是把设计切成两部分:**数据通路(datapath)**负责搬数和算数——寄存器堆、ALU、乘法器、FIFO、多路器,数据在这里按字节宽度流动;**控制通路(控制 FSM)**负责决定"何时做什么"——在哪个周期装载、何时启动运算、何时读写下一级、握手成功后跳到哪个状态。状态机记住的应该是"阶段/序号"这样的小颗粒控制信息(几个到几十个状态),而不是数据本身;需要计数时,用独立的计数器模块给出 tick 或 done 信号,FSM 只检测这些标志。
这条边界一旦划清,很多复杂设计都能套用同一个骨架。比如做 UART 发送,波特率计时交给一个分频 tick 计数器,移位位数交给一个 bit 计数器,FSM 只在 IDLE、START、DATA、PARITY、STOP 之间跳转,并根据 tick 和"计满"标志决定下一步;做数据包处理,长度、校验、地址都在数据通路寄存器里,FSM 只走"收头→收载荷→校验→回送"的流程。好处有三:一是状态数与数据宽度解耦,改位宽不用改 FSM;二是便于复用,同一个控制 FSM 可配不同数据通路;三是时序更好收敛,因为重的组合运算留在数据通路并可插入流水线寄存器,FSM 本身只承载轻量的跳转判断。评审时看到一个状态机里出现几十位的"状态数据"、或 case 分支里做大段算术,基本就是分工错位的信号。把"控制走状态、数据走通路、计时交计数器"这条原则养成习惯,是从能写闪烁灯到能写协议控制器之间最重要的一次思维升级,也直接衔接后续 FIFO、DMA、AXI 等更复杂的数据通路设计。
一个判断自己是否真正掌握 FSM 的检验方法,是尝试不看旧代码、从零把同一个控制器(比如 UART 发送或本文的小例子)用"状态寄存器 + 次态组合 + 输出/数据通路"三段重新写一遍,并且做到:所有状态用 localparam 具名、次态逻辑带 default 回 IDLE、计满和完成都由独立信号通知 FSM、寄存器输出用非阻塞。如果你能一次写对、综合无 latch 警告、仿真在边界条件(复位中途、突发连续请求、应答超时)下都能回到已知状态,说明这套控制与数据分离的范式已经内化成习惯。此后再遇到任何带协议、带流程的新外设,你的第一反应都会是先画状态转移图、列清每个状态的输出和跳转条件,而不是直接堆砌 if-else——这正是从"能写出可运行代码"到"能设计可靠控制器"的标志。
常见坑对照
| 坑 | 后果 | 正确做法 |
|---|---|---|
| 次态 case 无 default | 推断锁存器、卡死 | default 回 S_IDLE |
| 复位不回初始态 | 上电状态随机 | 异步复位同步释放 |
| 组合输出直接对外 | 毛刺到片外 | 输出寄存一拍 |
| 等待态无超时 | 事件丢了死等 | 加超时计数兜底 |
| 异步输入直接判断 | 亚稳态 | 先打两拍同步 |
| 状态写魔法数字 | 难维护 | localparam 命名 |
相关与延伸
基础:always 块与 case、意外锁存器、阻塞与非阻塞。 深入:进阶 CDC 亚稳态与同步器、ILA/VIO 高级调试(抓 state 定位)、FPGA 调试方法论。
一句话总结
状态机 = 一个状态寄存器记住“现在在哪” + 一段组合逻辑看着状态和输入决定“下一步去哪”;摩尔型输出只看状态(稳、默认选它),米利型输出还看输入(快一拍但易有毛刺)。标准写法是三段式:时序 always 存 state、组合 always @(*) 用完整 case(先给 next 默认值 + default 回安全态)算 next_state、输出再打一拍成时序输出。状态用 localparam 命名,编码 binary 省 FF、gray 只变一位、one-hot 译码最简单——FPGA 里 LUT 多 FF 宽裕,工具默认常用 one-hot,新手写对即可、编码交工具。最常见的坑是次态 case 不完整出锁存器、复位不回初始态、组合输出毛刺、等待态无超时死等、异步输入没同步——抓 ILA 看 state 值定位最快。