☰
Verilog三段式状态机:Moore与Mealy序列检测器设计避坑指南
2026/11/3 6:10:12 网站建设 项目流程

简介:面向数字电路设计与FPGA入门者的Verilog状态机学习文档,聚焦三段式状态机这一经典设计范式,帮助读者理清当前状态、下一状态与输出逻辑的分层写法,解决状态转移与输出耦合、代码可读性差等常见问题。压缩包内仅含1个docx文档,约124KB,以文字讲解配合源代码片段的形式组织,便于边看边对照理解。文档以序列10010检测为例,完整给出Moore型状态机的状态转移图、独热编码参数定义、三段式always块实现及testbench测试模块,并延伸讨论Mealy型状态机的输出与输入关联特性,同时对比两类状态机在输出稳定性与响应速度上的取舍。已有1260人学习,适合需要掌握状态机编码风格、仿真验证思路与模块复用方法的读者参考。

1. 三段式状态机:为什么“多写两个 always 块”反而更稳

很多人第一次写 Verilog 状态机,都是一个always块里把状态跳转、次态计算、输出赋值全塞进去,仿真波形看着也对,于是就觉得三段式是“学院派讲究”。直到把代码综合进 FPGA,时序报告里冒出一条又长又别扭的组合路径,或者输出端出现一个时钟周期的毛刺,才回头怀疑人生。三段式状态机解决的正是这个问题:它把“状态寄存器更新”“次态组合逻辑”“输出逻辑”三件事物理隔离,让每一段职责单一、时序可预测。本文围绕一份 Moore 型与 Mealy 型序列检测器(检测 10010)的经典实例,把三段式为什么这么切、切完之后综合出什么、以及那些仿真对但上板翻车的坑,一层层拆开。搞懂它,你再看任何状态机代码,都能一眼判断哪段该进时序块、哪段该进组合块。

2. 状态机拆三段:每一段到底在描述什么硬件

2.1 第一段:状态寄存器的“唯一时序边界”

三段式的第一段永远长这样,它只做一件事——在时钟沿把次态打进当前态:

// 第一段:状态寄存器,纯时序逻辑,综合出触发器 always @(posedge clk or negedge rst) begin if (!rst) currentstate <= S0; // 异步复位到初始状态 else currentstate <= nextstate; // 时钟沿采样次态 end

这段代码的物理含义非常明确:currentstate是一组触发器(flip-flop)的输出,nextstate是触发器输入端前面的组合逻辑结果。把状态更新单独拎出来,最大的好处是全设计只有这一处对状态做时序赋值,不会出现两个 always 块同时驱动同一个状态寄存器的情况——那在综合时会直接报多驱动错误,或者更隐蔽地综合出锁存器。

注意这里用的是非阻塞赋值<=。状态寄存器之间是并行更新的,如果用阻塞赋值=,仿真时多个状态位的更新顺序会依赖代码书写顺序,出现“仿真通过、综合后行为不一致”的经典血泪问题。这条规则不是风格偏好,是仿真语义和硬件语义对齐的硬约束。

2.2 第二段:次态组合逻辑,状态转移图的直译

第二段是纯组合逻辑,输入是当前态和外部输入,输出是次态:

// 第二段:次态组合逻辑,综合出多路选择/查找逻辑 always @(currentstate or a or rst) begin if (!rst) nextstate = S0; else case (currentstate) S0: nextstate = (a == 1) ? S1 : S0; // 等第一个1 S1: nextstate = (a == 0) ? S2 : S1; // 1后面等0 S2: nextstate = (a == 0) ? S3 : S1; // 继续等0 S3: nextstate = (a == 1) ? S4 : S0; // 等到1,序列1001 S4: nextstate = (a == 0) ? S5 : S1; // 等到0,序列10010 S5: nextstate = (a == 0) ? S3 : S1; // 重叠检测的关键回退 default: nextstate = S0; endcase end

这段case就是状态转移图的逐行翻译。以检测10010为例,S0 到 S4 一路匹配前缀,到 S5 时已经完整看到10010,输出拉高。真正体现设计功力的是 S5 的转移:如果再来一个 0,序列变成100100,末尾的00可以复用为下一次匹配的前两位,所以回退到 S3 而不是 S0;如果来的是 1,则回退到 S1。这种重叠序列检测的回退设计,是序列检测器最容易写错的地方,写成回 S0 功能也能跑,但会漏掉重叠出现的目标串。

这里用阻塞赋值=是对的,因为组合逻辑块内部是顺序求值,且不跨时钟沿保存。敏感列表写成currentstate or a or rst是 Verilog-2001 的写法,现代工具更推荐always @(*),让综合器自动推导敏感列表,避免漏写信号导致仿真与综合不一致。

2.3 第三段:输出逻辑,Moore 与 Mealy 的分水岭

第三段决定输出。Moore 型的输出只跟当前态有关:

// 第三段(Moore):输出仅由当前状态决定 always @(rst or currentstate) begin if (!rst) z = 0; else case (currentstate) S5: z = 1; // 只有完整匹配后才输出 default: z = 0; endcase end

而 Mealy 型的输出同时依赖当前态和输入,实例里用了一个中间量temp_z再打一拍:

// 第三段(Mealy):输出依赖状态与输入,再寄存一拍 always @(rst or currentstate or a) begin if (!rst) temp_z = 0; else case (currentstate) S4: temp_z = (a == 0) ? 1 : 0; // 在S4看到0即判定匹配 default: temp_z = 0; endcase end always @(posedge clk or negedge rst) begin if (!rst) z <= 0; else if (temp_z == 1 && nextstate == S2) z <= 1; // 把组合输出再寄存,消除毛刺 else z <= 0; end

Mealy 型的状态数更少(5 个状态就够,Moore 要 6 个),响应更快——在 S4 看到输入 0 的当拍就能判定,而 Moore 必须等状态真正跳到 S5。代价是输出是组合逻辑产物,输入一抖输出就抖。实例里额外加了一级寄存器把z打拍,本质是把 Mealy 的“快”和寄存输出的“稳”折中,这也是实际工程里常见的做法。

提示:Moore 输出若直接由组合逻辑产生,同样可能有毛刺,对输出质量要求高时也应寄存一拍,代价是一个周期的延迟。

3. 独热码与状态编码:为什么 6 个状态却用了 4 位

3.1 独热码在这里其实“名不副实”

实例里状态定义是S0=4'b0000, S1=4'b0001, S2=4'b0010 ... S5=4'b0101,注释写着“独热码”,但真正的独热编码应该是每个状态只有一位为 1(000001、000010、000100……)。这里用的是普通二进制编码,4 位刚好装下 6 个状态。这是一个很典型的“注释与实现不符”的坑,读代码时不能只看注释。

编码方式的选择直接影响综合结果:

编码方式位数(6 状态)特点适用场景
二进制3 位触发器少,次态译码逻辑复杂状态多、资源紧张
独热码6 位触发器多,次态逻辑简单、速度快FPGA 中触发器充裕
格雷码3 位相邻状态只变一位,降低翻转功耗低功耗、顺序状态机

FPGA 里触发器资源通常很富余,而组合逻辑层级是速度瓶颈,所以工具往往自动把状态机重编码成独热码。也就是说,你写二进制,综合器可能偷偷给你换成独热,仿真波形里的状态值和综合后的物理状态值对不上,这是调试时“波形和代码对不上号”的常见来源。

3.2 状态位宽与 default 分支的防御价值

实例里reg [3:0] currentstate, nextstate用了 4 位,而 6 个状态只用到 0 到 5,剩下 6 到 15 是非法状态。case末尾的default: nextstate = S0;就是防御这些非法编码。如果状态机因为复位不彻底、单粒子翻转等原因跑进非法态,default能把它拉回 S0,避免死锁。没有default的case在综合时可能被推断出锁存器(latch),因为工具认为“某些输入组合下输出未定义,需要保持原值”,这在时序逻辑里是灾难。

注意:组合逻辑always块里,如果case没覆盖所有分支且没写default,综合器会推断出锁存器。锁存器对毛刺敏感、时序分析困难,是数字设计里要极力避免的结构。

4. 测试平台与仿真:波形对了不代表设计对了

4.1 testbench 的时钟与激励节奏

实例的 testbench 用always #50 clk = ~clk;产生周期 100 个时间单位的时钟,激励在initial块里按#100的间隔逐拍改变输入a。这个节奏和时钟周期对齐,保证每个输入值稳定保持一个完整时钟周期,状态机在每个上升沿采样到确定的输入。

initial begin clk = 0; rst = 1; #5 rst = 0; // 复位有效 #3 rst = 1; // 释放复位 #20 a = 1; // 开始喂激励 #100 a = 1; #100 a = 0; // ... 后续按 100 的整数倍推进 end always #50 clk = ~clk;

这里有个细节值得注意:复位释放的时刻(#8)和时钟沿不对齐,复位是异步的,所以释放时刻不影响状态。但如果换成同步复位,复位释放必须满足建立/保持时间,否则可能出现复位释放的亚稳态。testbench 里这种“随便找个时刻释放复位”的写法,在仿真里没事,上板时如果复位信号来自按键,不加同步器就可能翻车。

4.2 怎么从波形判断状态机真的对了

看波形不能只看z有没有拉高,要按序列逐拍核对状态轨迹。喂入1 1 0 0 1 0 0 1 0 0 0 0 1 0 0 1 0 1 0这串激励时,正确的状态轨迹应该在第一次出现10010时走到 S5 并输出 1,之后的重叠序列也要能再次命中。如果只在第一次命中、后面漏检,八成是 S5 的回退写成了 S0 而不是 S3。

另一个验证点是复位行为:复位期间currentstate必须是 S0,z必须是 0。如果复位释放后第一个时钟沿状态就乱跳,检查第一段是不是误用了阻塞赋值,或者复位极性写反(!rst对应低有效复位,接高有效复位信号就永远不复位)。

5. 避坑与排查:那些仿真通过、上板翻车的瞬间

5.1 坑一:组合逻辑块漏写敏感信号,仿真对综合错

现象:仿真波形完全正确,综合后上板行为错乱,某个输入变化后状态不跳转。原因:第二段always @(currentstate or a or rst)如果漏写了a,仿真时因为 testbench 里其他信号变化碰巧触发了块执行,看起来正常;但综合出的硬件只在敏感列表里的信号变化时才重新计算,漏掉的信号变化不会触发次态更新。解决:统一用always @(*)让工具自动推导敏感列表,或者用always_comb(SystemVerilog)。改完重新跑一遍 lint,确认没有“incomplete sensitivity list”警告。

5.2 坑二:同一状态被两个 always 块赋值

现象:综合报“multiple drivers”错误,或者更隐蔽地综合出线与逻辑,行为诡异。原因:把输出z在第一段和第三段都赋值,或者把currentstate在次态块里也写了<=。解决:严格划分职责——状态寄存器只在第一段赋值,次态只在第二段赋值,输出只在第三段赋值。用reg声明的变量只能在一个always块里被赋值,这是硬规则。

5.3 坑三:Mealy 输出毛刺被下游当成有效脉冲

现象:Mealy 型状态机的z接到计数器或中断控制器,计数偶尔多一个,或者中断误触发。原因:Mealy 输出是组合逻辑,输入a的任何抖动都会直接传到z,产生窄脉冲。下游若对脉冲宽度敏感,就会误判。解决:像实例那样把 Mealy 输出再寄存一拍,或者下游加同步器和边沿检测。代价是一个周期延迟,换来输出干净。

5.4 坑四:状态编码被工具重编码,波形对不上

现象:仿真里currentstate是 0、1、2、3,上板抓波形却是独热码的 000001、000010。原因:综合工具对状态机做了自动重编码优化,物理状态值和 RTL 里的参数值不再一一对应。解决:调试时不要依赖状态数值,改用综合工具输出的状态名映射,或者在综合约束里关闭 FSM 重编码(不推荐,通常重编码是好事)。理解这一点,就不会对着波形怀疑代码写错了。

5.5 坑五:复位释放不同步导致首拍状态不确定

现象:上电后状态机偶尔从非法态开始,要等下一次复位才正常。原因:异步复位释放时刻与时钟沿太近,触发器的复位恢复时间(recovery time)不满足,进入亚稳态。解决:复位释放做同步化处理,即异步复位、同步释放:复位信号先打两拍再送给状态机。这是复位设计的基本功,和状态机本身无关,但状态机是最先暴露这个问题的模块。

6. 设计取舍:三段式不是教条,是让时序可推理

三段式的真正价值,不在于“三个 always 块”这个形式,而在于它把状态机的时序路径切成了可独立推理的几段:第一段是触发器到触发器,时序分析直接;第二段是纯组合,延迟可估算;第三段是输出逻辑,Moore 型天然稳定,Mealy 型加一级寄存就能稳定。每一段的输入输出边界清晰,综合工具也好、人也好,都能分段验证。

但三段式也有代价。状态少、逻辑简单的场合,一段式写起来更紧凑,综合结果未必差;Mealy 型为了输出稳定加的那级寄存器,本质上把 Mealy 的“快”吃掉了一部分,这时候要重新权衡:到底是要少一个状态、快半个周期,还是要输出绝对干净。我一般的判断标准是——输出要进中断、要驱动使能、要被别的时钟域采样,就寄存;输出只是内部数据通路的选择信号,组合输出可以接受。

还有一个常被忽略的点:三段式里第二段和第三段都是组合逻辑,如果状态多、输入宽,这两段的组合延迟会叠加。这时候可以把输出逻辑也改成查表式(用 case 直接映射),或者把部分次态计算提前寄存,用流水换频率。状态机设计从来不是套模板,而是围绕“这条路径多长、这个输出能不能抖”做取舍。

从那以后我每次写完状态机,都会强制走一遍三件事:lint 查敏感列表和多驱动、仿真里逐拍核对状态轨迹而不只看输出、综合后看一眼时序报告里状态机相关的路径。这三步花不了多少时间,但能挡掉绝大多数上板才暴露的坑。这份 Moore 与 Mealy 对照的实例,正好把三段式的骨架和序列检测的回退逻辑都摆在一起,对着代码把状态转移图重画一遍,比读十篇教程都管用。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询