FPGA 入门 14:always 块与 case——两种 always、完备分支
更新时间:2026-09-12。本文是
fpga/入门层第 16 篇。赋值符号区别见下一篇 阻塞与非阻塞,锁存器陷阱见 意外锁存器,FSM 练习见 状态机入门。
本文要回答的问题
always @(*)和always @(posedge clk)在仿真与综合上分别变成什么电路?为什么前者无记忆、后者有记忆?- 为什么组合 always 必须用阻塞赋值、时序 always 必须用非阻塞?混用会怎样(下一篇细讲)?
- 为什么组合块里
if不写else、case不写default就会冒出意外锁存器?「先给默认值」为什么能根治? - 敏感列表手写
@(a or b)为什么危险,@(*)到底自动包含了哪些信号? if-else和case综合出的电路有什么结构差异(优先级 mux 链 vs 并行多路选择)?什么时候用哪个?case/casez/casex、==/===的差别是什么,为什么新手应慎用 casex?- 三段式状态机的三段各管什么、为什么这样最不容易错?状态编码 binary/gray/one-hot 如何选择?
- SystemVerilog 的
always_comb/always_ff比老式 always 多了哪些编译期保护?
读完本文,你应该能判断任意一个 always 块综合后是 LUT 还是触发器,能写出无锁存器的组合逻辑和结构清晰的三段式 FSM,并理解每一条「规矩」背后对应的硬件。
一、两种 always
| 组合 always | 时序 always | |
|---|---|---|
| 敏感列表 | @(*) 输入变即执行 | @(posedge clk) 只在时钟沿 |
| 赋值 | 阻塞 = | 非阻塞 <= |
| 记忆 | 无(输出只看当前输入) | 有(寄存器) |
| 综合为 | LUT/连线 | FF |
| 分支不全 | 推断锁存器(坑) | 合法(寄存器保持) |
二、case 完备性
always @(*) begin
y = 1'b0; // 先给默认值, 防锁存器
case (sel)
2'd0: y = a;
2'd1: y = b;
default: y = 1'b0; // 或靠 default 覆盖
endcase
end- if-else = 优先级链(串行 mux),case = 并行多路选择;
- casez 用
?通配(译码用),慎用 casex(x 也通配掩盖问题)。
三、三段式状态机

状态名用 localparam S_IDLE=...,case 覆盖全部状态+default。
四、敏感列表到底在敏感什么
always @(...) 括号里的内容叫敏感列表(sensitivity list),它告诉仿真器「哪些信号变化时要重新执行这个块」。它只影响仿真行为——综合器根本不读你的敏感列表,而是按块内实际逻辑实现完整电路。这个「仿真看列表、综合看逻辑」的分裂,正是手写敏感列表会导致仿综不一致的根源。
- 组合逻辑:
always @(*)(等价@*)让工具自动把块内等号右边读到的所有信号加入列表,任何输入变化都重算,永不漏触发; - 时序逻辑:
always @(posedge clk)只在时钟上升沿执行;带异步复位时写成@(posedge clk or negedge rst_n),复位沿也进列表(同步复位则复位信号不进列表,因为复位只在时钟沿被采样); - 旧式
always @(a or b)手写列表:漏写 c 时,仿真中 c 变化不触发重算(仿真错),而综合照样把 c 接进逻辑(硬件对),两者行为分叉且极难排查。
记忆法:组合块只用 @(*),时序块只列时钟(+异步复位),其余信号永不手写进列表。
五、组合 always 的标准模板(零锁存器)
组合块最稳的写法是「开头给所有输出赋默认值,后面再按条件覆盖」,这样无论进入哪条分支,每个输出都被赋过值,语义上不可能「保持旧值」,也就没有锁存器:
always @(*) begin
y1 = 1'b0; // 默认值
y2 = 1'b0;
case (sel)
2'd0: begin y1 = a; y2 = b; end
2'd1: y1 = c;
default: ; // 保持默认
endcase
end对照三种危险写法:①if(en) y=a; 没有 else——en 为 0 时 y 保持,推断 latch;②case 只列部分选择值又没 default——未列值保持;③输出只在某个分支里赋值而另一个分支漏掉它。统一用「默认值 + 覆盖」就能一次防住全部三种(系统讲解见 意外锁存器)。
注意:时序 always 里不存在锁存器问题。寄存器在没有新赋值时本来就保持(else 省略等于「该拍不变」),这是合法且常用的,例如 if(en) q <= d; 就是带使能的触发器。判断标准只有一个:这是组合块还是时序块。
六、if-else 与 case 综合出的电路结构
虽然两者有时能互换,但综合出的硬件结构不同:
- if-else 链是有优先级的:先写的条件先判断,综合成一条串行选择链(优先 mux/priority encoder)。条件之间若不互斥,靠前的优先;链越长,组合延迟越大。适合「条件有天然优先级」的场景,如中断仲裁、
valid/ready握手。 - case 针对同一个选择表达式做多路分支,综合器知道分支互斥,通常实现为并行多路选择器,各路径延迟更对称,译码器、状态机、七段码表、ALU 操作码首选。
- 当 case 的分支是常量且较密集时,综合器还可能推断成 ROM/查找表结构(尤其配合
default和寄存器输出时可能进 BRAM,见 BRAM 与分布式 RAM)。
实践建议:同一信号的多路译码用 case,带优先级/复合布尔条件用 if-else;不要把十几层嵌套 if 当译码器用(时序差且难读)。
case / casez / casex 与全等比较
case进行四值比较:分支里出现 x/z 时按字面全等匹配;普通==遇到 x 结果是 x,===(case equality)才把 x/z 也当确定值比较(仿真/testbench 用,不可综合或综合结果依赖工具)。casez:把 z(或写?)当通配位,常用于指令译码(高位确定、低位忽略),推荐用?明确表达「不关心」,可读性最好。casex:x 和 z 都当通配,危险:若输入信号本身在仿真中是 x(未复位/未驱动),它也被当通配,可能选中你意想不到的分支,把真实问题掩盖掉。新手统一用 case,确需掩码译码用 casez +?。
七、把三段式状态机写扎实
三段式把「记住状态、决定去向、产生输出」三件事拆到三个独立块,是 FPGA 控制逻辑最推荐的范式(完整序列检测器与摩尔/米利选择见 状态机入门):
// ① 状态寄存器:唯一的时序块,只负责把 next 存入 state
always @(posedge clk or negedge rst_n)
if (!rst_n) state <= S_IDLE;
else state <= next;
// ② 次态逻辑:纯组合,开头 next = state 防 latch,case 全覆盖 + default
always @(*) begin
next = state; // 默认保持
case (state)
S_IDLE : if (start) next = S_RUN;
S_RUN : if (done) next = S_IDLE;
default: next = S_IDLE; // 回到安全态
endcase
end
// ③ 输出逻辑:推荐寄存一拍,无毛刺、利于时序收敛
always @(posedge clk or negedge rst_n)
if (!rst_n) begin en <= 1'b0; sel <= 2'd0; end
else begin
en <= (state == S_RUN);
sel <= (state == S_RUN) ? 2'd1 : 2'd0;
end为什么这样最稳:①寄存器块极简,复位清晰,综合器一眼识别;②次态块是纯函数(state、输入决定 next),配合默认值/default 不会推断锁存器;③输出寄存后,组合译码的毛刺不会直接到管脚,也切断了「输出绕回影响次态」的长组合环。两段式(寄存器与输出合一)少写代码但职责混杂,复杂后容易出问题,入门直接养成三段式习惯收益最大。
摩尔还是米利,落到输出逻辑上
- 摩尔(Moore):第③段输出只依赖 state(如上例),进到某状态输出即固定,稳、无毛刺;
- 米利(Mealy):输出依赖 state 加输入,省一拍/省状态,但输入抖动会传到输出,需要时也寄存一拍。 新手默认摩尔,只有明确要省周期的紧耦合路径才用米利,并对米利输出加同步。
异步输入先同步再进状态机
按键、外部就绪等不与本时钟同源的信号,不能直接进 if 判断,否则可能亚稳态导致状态跳飞。标准做法是打两拍同步器再使用,详见进阶 CDC 与亚稳态。关键等待态还要有超时计数,防止事件丢失时状态机永久死等。
八、SystemVerilog 的编译期保护
always_comb / always_ff 不是换个名字,它们让工具替你守规矩:
always_comb:工具检查这确实是纯组合逻辑——一旦可能推断锁存器就直接报错,并自动推导敏感列表、保证仿真在零时刻也执行一次(避免初值毛刺);always_ff @(posedge clk):工具检查块内只有一个时钟/复位沿的时序逻辑,若你不小心写了组合行为或加了非时钟到敏感列表会告警;- 配合
logic类型,单驱动冲突也会在编译期暴露。
新写代码建议直接采用;阅读老 Verilog 时能对应回 always @(*) 与 always @(posedge clk) 即可。
一个组合译码器的完整对照
七段数码管译码(显示 0–9,见 数码管与点阵)是练习 case 完备性的绝佳小例:输入 4 位 BCD,输出 8 位段选,组合 always_comb 开头给默认段码(全灭),case 列 0–9,default 回到全灭。这样即使输入是 10–15(非法 BCD)也输出确定值,不产生 latch,且行为可预期。把它写成模块、单独建 testbench 枚举 0–15,能一次练到:组合 always、case + default、参数/端口、按位段码、仿真验证全流程。
九、自测要点与学习地图
自测:① 给一段 always,判断综合成 LUT 还是 FF,并指出依据;② 说明手写敏感列表为何导致仿真与硬件不一致;③ 写出零锁存器组合块的「默认值」模板;④ 比较 if-else 链与 case 的电路结构和延迟特征;⑤ 说出 case/casez/casex、==/=== 的差别与新手纪律;⑥ 默写出三段式 FSM 的三段,并指出每段用阻塞还是非阻塞;⑦ 说明摩尔/米利、为什么输出建议寄存;⑧ 解释 always_comb/always_ff 相比老式 always 多的保护。
学习地图:紧接着攻克赋值时机——阻塞与非阻塞;锁存器与综合警告系统看 意外锁存器;动手写完整 FSM 看 状态机入门,按键/串口等真实控制逻辑看 按键消抖、UART 回环;数据类型配套看 数据类型与位宽;时序与流水线深入见进阶 流水线与资源优化。
十、组合与时序配合的两个经典小电路
理解两种 always 最快的方式是看它们如何配合。下面两个例子几乎在每个入门工程里都会出现。
边沿检测(组合产生条件、时序打拍)
检测信号 sig 的上升沿:把它打一拍存到 sig_d,再用组合比较「当前为 1 且上一拍为 0」。
reg sig_d;
always @(posedge clk or negedge rst_n)
if (!rst_n) sig_d <= 1'b0;
else sig_d <= sig;
wire rise = sig & ~sig_d; // 纯组合:上升沿脉冲, 高 1 拍
wire fall = ~sig & sig_d; // 下降沿脉冲这里时序块负责记忆(存上一拍),assign 负责组合判断(当前与历史的差异)。按键消抖、UART 起始位检测、脉冲同步都用这个套路(见 按键消抖)。注意 sig 若是片外异步信号,要先打两拍同步再做边沿检测,防亚稳态。
计数器产生使能节拍(时序块 + 比较)
reg [15:0] cnt;
reg tick;
always @(posedge clk or negedge rst_n)
if (!rst_n) begin cnt <= 16'd0; tick <= 1'b0; end
else begin
tick <= (cnt == 16'd49999); // tick 比动作早一拍的写法
cnt <= (cnt == 16'd49999) ? 16'd0 : cnt + 16'd1;
end两个赋值都在同一个时序块、都用非阻塞,计数器和 tick 同步更新,无竞争。下游逻辑在 if(tick) 时动作即可(时钟使能思想见 计数器与分频)。这两个例子共同说明:时序块存状态、组合块算结论是 RTL 最基本的分工。
十一、锁存器:为什么 FPGA 不想要它
组合逻辑的本质是「输出 = 当前输入的函数」,任何时刻输入定了输出就该定。若某条路径没给输出赋值,Verilog 语义规定输出「保持原值」,要实现保持就得加一个电平触发的存储元件——锁存器(latch)。它与触发器的区别和问题:
- 触发器是边沿触发:只在时钟沿采样,其余时间输入怎么抖都不影响输出,静态时序分析有成熟模型,FPGA 里有大量现成 FF;
- 锁存器是电平敏感:使能有效期间输出「透明」地跟随输入,使能关断才保持。在使能窗口内输入毛刺会直接传到输出;时序分析要处理透明传播,更复杂;FPGA 内部没有专用 latch 单元,要用通用资源拼,浪费且慢。
99% 的情况,你要的是「漏写分支」而非真要锁存器,所以综合器一报 inferred latch 就应当 bug 处理。彻底规避靠两条:组合块开头给所有输出默认值;用 SystemVerilog always_comb,会推断 latch 时直接编译失败(见 意外锁存器)。
十二、从 always 看资源与时序
写完一个 always,应当能大致预判它在芯片上长什么样、关键路径在哪:
- 纯组合 always_comb:输入到输出是 LUT + 布线,没有时钟约束的寄存器边界,延迟就是传播延迟;if-else 深链/case 宽译码会拉长这条路径,可能成为建立时间瓶颈。
- 时序 always_ff:每个非阻塞赋值的左值对应一个(组)FF,输出相对输入延迟一个时钟周期;组合逻辑夹在前级 FF 的 Q 与后级 FF 的 D 之间,这段必须在一个周期内算完(STA 检查,见进阶 静态时序分析)。
- 减少长组合路径靠切分流水线:在长组合中间插入寄存器,把一拍算不完的事分两拍,吞吐不变、频率能提(见进阶 流水线与资源优化)。
- 扇出过大:一个使能/复位驱动几千个 FF 会因布线电容变慢,工具会复制驱动;写代码时控制超大 fanout 的信号。
- 资源对应:宽 case/查找可映射 ROM(BRAM),乘法进 DSP,寄存器对应 FF,其余组合落 LUT(资源看法见 资源利用率)。
建立这种「一段 always → 一片电路 → 一条时序路径」的直觉,比记住语法更重要:它让你在写的时候就知道哪里会慢、哪里该寄存、哪里会出锁存器。
十三、过程块里其他关键字与可综合性
新手常把软件里的控制流直接搬进 always,需要分清哪些可综合、综合成什么:
- if / else if / else、case:可综合,分别映射优先 mux 链与并行选择器,是 RTL 主力。
- for 循环:仅当循环边界是编译期常量时可综合,综合器把它展开成 N 份并行硬件(如逐位异或算奇偶、generate 式复制)。边界依赖运行时信号的 for 通常不可综合——硬件不能「动态决定有几份电路」。testbench 里 for 可随意用(不综合)。
- while / forever / forever 延时:RTL 里基本不用,多见于 testbench 产生时钟(
always #5 clk = ~clk;),不要写进可综合设计。 - initial:用于仿真给初值/激励;部分 FPGA 支持 initial 给寄存器上电初值,但可靠控制逻辑仍应写复位,不要把 initial 当复位用。
#delay延时、$display等系统任务:仅仿真,不可综合,RTL 里出现延时会被综合器忽略或报错——硬件时序靠时钟周期表达,不靠#10。- function/task:静态 function 可综合(本质是可复用组合表达式),task 多用于仿真,复杂行为的 task 一般不可综合。
一句话:可综合 RTL 描述的是结构和时钟沿上的行为,不是顺序算法;凡是依赖「运行时决定执行次数/时间」的语句,都不属于 RTL。
组合逻辑反馈环:必须避免的结构
组合 always 的输出若未经寄存器又绕回来参与自己的输入,就形成组合环(combinational loop):y 依赖 y,电路没有稳定状态,可能振荡、产生毛刺、让 STA 无法分析。综合器会告警。规则:任何「记住上一次值」的需求都要用时序逻辑(FF),组合逻辑的输出只能由当前输入决定。带状态的反馈(计数器、FSM)回路里一定有寄存器把环切断。
十四、读懂综合日志里的关键警告
always/case 相关的警告通常就那几类,看到了要能立刻对号:
| 警告关键词 | 含义 | 处理 |
|---|---|---|
| inferred latch / inferring latch | 组合块漏分支,推断锁存器 | 开头赋默认值/补 default |
| incomplete sensitivity list | 敏感列表不全(老代码) | 改 @(*) / always_comb |
| multi-driven net / multiple drivers | 同一信号被多处驱动 | 合并为单驱动 |
| width mismatch / truncated | 位宽不匹配被截断 | 核对位宽 |
| signal has no load / removed | 信号没被使用,被优化掉 | 检查是否接错/需保留 |
| combinational loop | 组合反馈环 | 回路插入寄存器 |
| division/modulus | 除法/取模很耗资源 | 用移位(2 的幂)或 IP |
把这些警告清零(或逐条确认可接受)再往下走,是每个工程都该守的质量门(进阶 lint 见 RTL lint)。
十五、一次完整的小实验
建议按下面步骤把本文知识动手验证一遍(约 30–40 分钟):
- 写一个 4 选 1 mux:分别用
if-else if和case实现,综合后在 schematic 里对比结构;故意删掉 default 编译一次,观察 inferred latch 警告,再加回来看警告消失。 - 写边沿检测:给
sig造方波激励,仿真确认rise/fall各高一个时钟周期。 - 写一个 2 状态(IDLE/RUN)三段式 FSM:用按键(经消抖/同步)启动,完成后回 IDLE,输出一个 LED;用 localparam 命名状态,输出寄存一拍。
- 把次态 case 的 default 删掉、把某状态的次态故意留空,分别观察锁存器警告与仿真「卡住」现象。
- 上板用 ILA/SignalTap 抓 state(见 ILA 初用),对照仿真波形。
做完这五步,你会从「记得几条规则」变成「亲眼见过锁存器、组合环、状态跳变和资源对应」,这正是 RTL 入门最需要的经验。
十六、顺手厘清:复位在 always 里怎么写
时序块的敏感列表和复位写法紧密相关,顺带讲清两种复位,避免和 always 语法混淆(系统讨论见 时钟与复位)。
- 异步复位:
always @(posedge clk or negedge rst_n),复位信号的有效沿也进敏感列表;块内第一句先判复位if(!rst_n) ... else ...。复位一拉低,无论有没有时钟沿立刻生效。优点是不依赖时钟也能复位,缺点是复位释放时刻若靠近时钟沿可能产生亚稳态,工程上常用「异步复位、同步释放」。 - 同步复位:
always @(posedge clk),敏感列表里只有时钟;块内if(!rst_n) ...。复位只在时钟沿被采样生效,完全同步、利于时序,但时钟没起来时无法复位,且复位信号要走数据路径、占少量逻辑。 - 选择:多数入门板和厂商模板用异步复位(低有效
rst_n很常见),重点是全设计统一一种风格、一个极性,不要有的模块高有效同步、有的低有效异步。 - 复位要覆盖所有控制/状态寄存器;纯数据流水寄存器可按资源/时序取舍是否复位。
把复位写法、敏感列表、阻塞/非阻塞(下一篇)三者一起记,时序 always 的「壳」就永远不会写错:
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) q <= '0;
else if (en) q <= d;这一个壳能套住 FPGA 里绝大多数时序逻辑:使能、复位、数据都在正确的位置,非阻塞赋值、单一时钟、状态确定。掌握它再去看任何复杂模块,核心时序部分都只是这个壳的组合与扩展。
最后把 always 这条主线收束成一句话:**判断一个过程块,先问它有没有时钟沿——有,就是时序逻辑,输出对应触发器、用非阻塞、天然能保持;没有,就是组合逻辑,输出只由当前输入决定、用阻塞、必须保证每个输出在所有输入组合下都被赋值。**组合块的完备性用「开头默认值 + 完整 case/default」保证,多分支优先用 case、优先级条件用 if-else,需要掩码才用 casez 且避免 casex;带状态的控制逻辑用三段式 FSM,状态寄存、次态组合、输出寄存各司其职;再用 always_ff/always_comb 让工具在你破坏这些规则时立刻报错。把这套判断内化成习惯,Verilog 里最高频的「锁存器、仿综不一致、竞争、毛刺、组合环」五类问题都会大幅减少,你也就能把注意力从语法细节转到真正有价值的事上——数据通路、状态划分与时序收敛。多在仿真和综合报告里对照验证几次,这套规则会从「要背的纪律」变成「下笔即对」的肌肉记忆。
十七、if 与 case 综合出来的电路差别
if-else if-else 和 case 都能做多分支,但它们在综合器眼里并不总是等价,差别集中在"优先级"两个字上。if-else if 天生是一条有优先级的链:前面的条件先判断,命中就不再看后面,综合出来是一串顺序排列的多路选择器(优先级编码器式的级联)。这在两个场景下是好事:一是条件之间确实存在"谁优先"的语义(比如中断仲裁、先到先得),用 if 写既直观又和电路一致;二是希望靠前的条件路径更短、延迟更小。但当分支很多、且条件彼此平等时,这条级联链会变长,最后一个分支要串过所有前级选择逻辑,组合延迟累积,可能压到关键路径上影响频率。
case 在语义上是并行选择:用一个选择信号(状态、操作码)同时索引所有分支,综合器更倾向于推出一个平衡的多路器,甚至在 case 项是连续常量时推断成 ROM/查找表结构,各分支路径延迟更均匀。因此多分支控制、状态机次态逻辑、译码器优先用 case。但要注意三个前提:选择表达式必须是"单一变量",若 case 的判断式本身是复杂表达式,并行优势就打折扣;要补全 default,避免推断锁存器;casez 里的 ?/z 是"不关心"通配,而 casex 会把仿真中的 x 也当不关心,可能掩盖问题,工程上一般避免 casex。实际判断方法很简单:写完后看综合日志和 Schematic,确认工具推断出的是 mux、priority encoder 还是 ROM,再结合时序报告看关键路径落在哪一支。语义上有优先级就用 if,语义上平等多选就用 case,并且始终补默认分支——这条习惯同时解决锁存器、时序和可读性三类问题。
十八、定位意外锁存器的一套标准动作
综合报告里冒出 "inferred latch" 警告,是组合逻辑初学者最常遇到的红灯。锁存器之所以危险,是因为它是电平敏感器件:在使能条件不满足时保持旧值,这不仅引入一条你没设计的反馈通路、容易产生时序和可测性问题,更会造成仿真与综合行为难以对齐。定位它不需要玄学,按一套固定动作走即可。第一步,直接读警告里给出的层级和信号名,综合器通常会明确指出是哪个 always 块、哪个变量被推断成 latch,从这里入手而不是全文瞎找。第二步,判断这个 always 块本该是组合还是时序:带时钟沿却报 latch 多半是笔误,不带时钟沿则是组合块不完备。
第三步,对组合块做"完备性"体检,锁存器的根因永远是同一个:存在某些输入条件下,某个输出没有被赋新值,工具只能让它保持。具体查三处——if 链有没有 else(所有分支是否都给输出赋值)、case 有没有 default、块开头是否给每个输出设了默认值。最省事、最可靠的修法是"块首默认值 + 分支里只写需要改变的输出":在 always 块第一行把所有被赋值变量统一赋一个安全缺省值,后面的 if/case 只覆盖真正需要的情况,这样无论走哪条路径,每个输出在每次执行时都被赋过值,锁存器在结构上就不可能产生。第四步,用 SystemVerilog 的 always_comb 替换 always @(*):前者在工具发现存在未被覆盖的赋值路径时会直接报错,把锁存器从"警告"升级为"编译失败",强制你写完备。第五步,重新综合确认警告消失,并跑一次仿真验证保持逻辑不是被误依赖。养成这套动作后,锁存器问题基本可以在编码阶段就被杜绝,而不必等到上板遇到奇怪的"信号自己保持"现象才回头排查。
常见坑对照
| 坑 | 后果 | 正确做法 |
|---|---|---|
| 组合 always 用 <= | 仿真/综合行为怪异 | 组合 =,时序 <= |
| case 无 default | 推断锁存器 | default 或先赋默认值 |
| 敏感列表手写漏信号 | 仿真与综合不一致 | 一律用 @(*) |
| 状态用魔法数字 | 难读易错 | localparam 定义 |
| if 链过长当译码 | 串行延迟大 | 译码用 case |
相关与延伸
一句话总结
always 块只有两种正确用法:always @(*) 组合逻辑(输入变即重算、阻塞赋值 =、综合成 LUT)和 always @(posedge clk) 时序逻辑(只在时钟沿动作、非阻塞赋值 <=、综合成 FF),两者不要混。组合 always 最大的坑是分支不完备:case 没覆盖所有选择值且没 default 时,未覆盖情况下输出"保持旧值"→推断出意外锁存器,修复方法是 case 前给输出默认值或加 default(时序 always 里寄存器天然保持、不存在此问题)。多路选择用 case(并行 mux,状态机/译码首选),优先级条件用 if-else,casez/casex 新手慎用。状态机推荐三段式:时序 always 存 state、组合 always 按 case 算 next_state、输出再打一拍防毛刺,状态名一律 localparam——职责分开后综合结果可预期、最好维护。