安全状态机 Verilog 模板:两段式、默认分支与非法态恢复
更新时间:2026-10-31。这篇安全状态机 Verilog 模板属于 芯片设计入门系列,适合控制链路已经想清楚、准备把状态图落成 RTL 的读者。模板只负责三件事:正常转移不锁死、每个输出都有确定值、未使用编码回到入口态。触发器和复位的基础概念见 锁存器与触发器 与 时钟与复位概念。
首屏答案:用一个时序 always 保存当前状态,再用一个组合 always 计算下一状态和输出。组合块开头先写 next_state = state、安全输出默认值,case 末尾用 default 把非法编码送回 IDLE。这样即使两位状态寄存器出现未使用的 2'b11,busy 和 valid 也立即为 0,下一拍回到 IDLE,而不是维持一个可能危险的旧输出。
本文要回答的问题
- 两段式状态机为什么比把转移和输出揉在一起更容易检查?
default分支、块首默认赋值分别挡住哪类问题?- 三态模板怎样保留一个非法编码,用来验证恢复路径?
- 仿真怎样证明所有正常分支、非法态安全输出和下一拍恢复?
一、安全状态机先定义失效时的行为
这里的“安全”不是功能正确就完事,而是明确两件事:状态编码超出设计集合时,外部执行器看到什么;状态机随后回到哪里。下面用空闲、工作、完成三个状态演示。BUSY 拉高 busy,DONE 拉高 valid;其余情况两个输出都为 0。两位编码留下 2'b11,正好可作为测试中的非法态。
| 当前状态 | busy / valid | 条件为 0 | 条件为 1 |
|---|---|---|---|
IDLE=00 | 0 / 0 | 保持 IDLE | start=1 到 BUSY |
BUSY=01 | 1 / 0 | 保持 BUSY | done=1 到 DONE |
DONE=10 | 0 / 1 | 保持 DONE | ack=1 到 IDLE |
| 其他编码 | 0 / 0 | 无条件到 IDLE | 同左 |
表中“条件为 0”不是可以省略的旁支,而是状态驻留语义。写代码时由块首的 next_state = state 统一实现,就不必在每个分支重复写自环,也不会因为漏掉一种输入组合而意外推出锁存器。
特意保留一个未使用编码,还能让恢复路径成为可执行、可回归的设计的一部分。如果三个状态改用两位 one-hot 之外的特殊编码,或后续扩展到四个合法状态,原来的 2'b11 就可能不再非法;这时要重新挑选注入口,不能照旧复制测试。同理,STATE_WIDTH 只是模板参数,并非任意宽度都能自动形成合理编码:宽度必须容下全部合法状态,恢复目标也必须是已经定义的入口态。
读这张图抓两条线:蓝色正常状态构成 IDLE → BUSY → DONE → IDLE 闭环,每条自环对应输入条件不成立时的驻留;红色非法态不接入正常业务环,只保留一条回到 IDLE 的恢复边。输出直接标在状态内,便于核对“状态异常时执行器必须松开”的约束。
二、可复制的两段式参数化模板
完整 RTL 如下。STATE_WIDTH 是状态寄存器宽度,示例取 2;增加状态时应同步调整宽度和本地编码,不能只加 case 项。输出采用 Moore 型写法,即只由当前状态决定,输入毛刺不会绕过状态寄存器直接改变 busy 或 valid。
`timescale 1ns/1ps
module safe_fsm #(
parameter STATE_WIDTH = 2
) (
input wire clk,
input wire rst_n,
input wire start,
input wire done,
input wire ack,
output reg busy,
output reg valid
);
localparam [STATE_WIDTH-1:0] IDLE = 0;
localparam [STATE_WIDTH-1:0] BUSY = 1;
localparam [STATE_WIDTH-1:0] DONE = 2;
reg [STATE_WIDTH-1:0] state;
reg [STATE_WIDTH-1:0] next_state;
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
state <= IDLE;
else
state <= next_state;
end
always @* begin
next_state = state;
busy = 1'b0;
valid = 1'b0;
case (state)
IDLE: begin
if (start)
next_state = BUSY;
end
BUSY: begin
busy = 1'b1;
if (done)
next_state = DONE;
end
DONE: begin
valid = 1'b1;
if (ack)
next_state = IDLE;
end
default: begin
next_state = IDLE;
busy = 1'b0;
valid = 1'b0;
end
endcase
end
endmodule第一段时序逻辑只做状态保存,复位时进入 IDLE,其余时钟沿接收 next_state。第二段组合逻辑同时负责转移和输出,两个职责共享同一个 case,所以不会出现“转移已经改到新规则,输出还停在旧分支”的两张皮。
块首三个赋值是防锁存的关键:任何未被后续分支覆盖的路径,都沿用当前状态并输出安全值。组合逻辑若在某些输入组合下没有给 busy 赋值,综合器就需要用存储元件记住它之前的值,原本想要的状态译码器便混入锁存器。先写默认值再按状态覆盖,可以让每个变量在每条路径上都有唯一、直观的来源;它也比在各个 if 的两侧反复补 else 更不容易漏项。
末尾 default 则处理显式列出的三个编码之外的一切情况。它对非法态的输出立即生效,不需要等时钟;next_state = IDLE 则在下一个有效时钟沿完成恢复。两者时间尺度不同,验证时也应分别检查。
恢复目标也不必永远是无条件 IDLE。有些控制器需要回到专门的 ERROR 状态,等待软件读错误码并确认,而不是自动重新接收 start;有些系统要求保持输出关闭,直到重新复位。选择 IDLE 是因为本例入口态空闲且无副作用。若业务需要错误可见性,应增加 error 寄存器或错误态,并补上对应的测试,不要一边自动恢复、一边把唯一故障痕迹清掉。
复制后不要机械保留 busy=0、valid=0。电机、阀门或写入使能的安全值要由系统约束决定,有些场合安全动作甚至是主动拉高一条停机信号。还要避免给这个 case 加 full_case、parallel_case 一类综合指令:它们可能把仿真中的覆盖承诺变成硬件里的错误假设,反而绕过刚刚建立的恢复语义。
三、测试必须走到非法态,而不只是编译通过
正常仿真覆盖六个分支:三个状态的驻留自环,以及 start、done、ack 分别触发的三条转移。随后让状态机重新进入 BUSY,用 testbench 的层次引用强制状态为 2'b11。force 和 release 只用于仿真,不能综合进 RTL。
force dut.state = 2'b11;
#1;
if (busy !== 1'b0 || valid !== 1'b0)
$fatal(1, "illegal state produced unsafe output");
release dut.state;
@(posedge clk);
#1;
if (dut.state !== 2'b00)
$fatal(1, "FSM did not recover to IDLE");这里故意先检查输出、再释放状态、最后等一个时钟沿。若只检查最终回到 IDLE,一个“下一拍能恢复,但非法态期间仍拉高执行信号”的设计也会漏过。自检 testbench 对每个状态、输出都使用四态不等比较 !==,遇到 X 同样判失败;12 项检查任一失败都会以非零状态结束。
这段记录先走完正常闭环,再进入 BUSY 准备注入。start moves IDLE to BUSY、done moves BUSY to DONE、ack moves DONE to IDLE 对应三条业务边,相邻的 holds 行对应三条自环。也就是说,测试不是只走一条幸运路径,六个正常分支都有断言结果。
PASS: illegal state forces safe outputs state=11 busy=0 valid=0
PASS: released illegal state stays safe state=11 busy=0 valid=0
PASS: next clock recovers to IDLE state=00 busy=0 valid=0
PASS: recovered FSM obeys IDLE hold state=00 busy=0 valid=0
RESULT: PASS checks=12; all branches and recovery verified
tb_safe_fsm.v:105: $finish called at 97000 (1ps)非法态部分给出了时间顺序:state=11 时两个输出已经为 0;释放强制值后、下一时钟前仍保持安全;下一拍状态变为 00;再停一拍还能遵守 IDLE 的驻留规则。最终 checks=12 与全部 PASS 对上,-Wall 编译没有告警。这个结论只覆盖单非法编码及组合恢复逻辑,不代表模板能抵抗任意多位翻转、时钟丢失或持续的硬件故障。
实验环境
- 机器/系统:本机 macOS,纯命令行仿真
- 工具版本:Icarus Verilog 13.0(stable,v13_0)
- 日期:2026-10-31
- 完整命令:
iverilog -g2012 -Wall -s tb_safe_fsm -o safe_fsm_sim safe_fsm.v tb_safe_fsm.v && vvp safe_fsm_sim
代码获取
参数化 RTL、自检 testbench 和运行脚本在公开仓 https://github.com/derekzhuo/geek-doc.cn/tree/main/experiments/tpl-verilog-fsm-safe。进入目录后执行 bash run.sh 即可复现;脚本结束后自动删除仿真可执行文件。
这一篇在知识体系里的位置
本篇把状态图落成可验证的 RTL 模板。时序元件和阻塞、非阻塞赋值的基础见 锁存器与触发器;异步复位的接入方式见 时钟与复位概念。若状态来自另一个时钟域,应先完成信号同步,可参考 两级同步寄存器 Verilog 模板,不要让异步输入直接破坏本模板的时序前提。
总结
安全状态机 Verilog 模板的核心是“默认安全、显式恢复、逐分支验证”:时序块只保存状态,组合块先给驻留值和安全输出,再用完整 case 描述业务,default 让非法编码输出归零并在下一拍回到 IDLE。配套仿真覆盖三个驻留分支、三个正常转移及非法态注入,Icarus Verilog 13.0 下 12 项检查全部通过;真正复制到工程时,还应按执行器风险重新定义安全输出,而不是把 0 当成万能答案。