
简介面向数字电路设计与FPGA初学者及进阶者的Verilog状态机学习文档聚焦三段式状态机这一经典设计范式帮助读者理清当前状态、下一状态与输出逻辑的分层写法解决状态转移与输出时序易混淆的问题。文档以序列10010检测为实例完整给出Moore型与Mealy型两种状态机的实现思路Moore型采用独热编码划分S0至S5六个状态输出仅由当前状态决定Mealy型则让输出同时受当前输入影响并配有testbench激励波形用于仿真验证便于对照理解两者在响应速度与输出稳定性上的取舍。资源包共1个docx文件约124KB内容紧凑、代码与说明并重适合作为课程实验或项目前的参考模板。目前已有1260人学习下载可帮助读者快速掌握三段式状态机的编码结构与验证方法。1. 三段式状态机不是模板是能救你仿真波形的那套写法序列检测是数字电路面试和课程设计里出现频率极高的题目10010 这个串更是常客。很多人第一次写状态机把状态转移、输出、寄存器全塞进一个 always 块综合能过仿真也能跑但波形一放大就发现输出比预期晚一拍或者毛刺跟着输入乱跳查半天查不出问题。这份资源给的是 Moore 型和 Mealy 型两套完整可综合的 Verilog 源码外加对应的 testbench核心卖点就是三段式写法当前状态寄存器一段、次态组合逻辑一段、输出逻辑一段。它适合正在学 Verilog 状态机、要做序列检测实验、或者想把「能跑」升级成「时序干净」的从业者和学生。下面我按源码逐段拆把参数、编码方式、testbench 的坑都摆出来。2. 拆 Moore 型源码独热码、次态逻辑与输出为什么必须分开2.1 状态编码与端口定义的实际取舍先看端口和状态定义。模块moorefsm的输入是clk、rst、a输出是z状态寄存器用reg [3:0] currentstate, nextstate。源码里六个状态 S0 到 S5 用的是4b0000到4b0101注释写着「独热码」但实际赋值是二进制递增并不是真正的独热编码独热码应该是 000001、000010 这种一位有效。这是个典型的文档与代码不一致的点新手照着注释理解会绕进去。parameter S0 4b0000; parameter S1 4b0001; parameter S2 4b0010; parameter S3 4b0011; parameter S4 4b0100; parameter S5 4b0101;这段参数定义决定了状态位宽。六个状态用 3 位就够0 到 5源码给 4 位是留了余量综合工具会自动优化掉未用编码。如果你要改成真正的独热码六个状态需要 6 位写成6b000001到6b100000好处是次态译码只需比较一位速度快代价是寄存器多。常见做法是状态数少于 8 用二进制省资源状态数多、速度要求高才上独热码。这里保持 4 位二进制完全能用别被注释带偏。2.2 三段式第一段当前状态寄存器的时序逻辑第一段 always 块只做一件事——把次态打进当前状态寄存器always(posedge clk or negedge rst) begin if(!rst) currentstate S0; else currentstate nextstate; end这是标准的三段式第一段异步复位、低电平有效。rst拉低时当前状态强制回 S0否则每个时钟上升沿把nextstate锁进currentstate。注意这里用的是非阻塞赋值时序逻辑必须这样写否则仿真和综合结果会对不上。参数上唯一要盯的是复位极性源码是negedge rst配if(!rst)如果你的板子复位是高有效这两处要同时改成posedge rst和if(rst)漏改一处就是复位失效的血泪经验。2.3 第二段次态组合逻辑与 case 全覆盖第二段是纯组合逻辑敏感列表是currentstate or a or rst用 case 根据当前状态和输入决定次态always(currentstate or a or rst) begin if(!rst) nextstate S0; else case(currentstate) S0: nextstate (a1)?S1:S0; S1: nextstate (a0)?S2:S1; S2: nextstate (a0)?S3:S1; S3: nextstate (a1)?S4:S0; S4: nextstate (a0)?S5:S1; S5: nextstate (a0)?S3:S1; default: nextstate S0; endcase end这里用阻塞赋值因为它是组合逻辑。逐条读转移条件S0 收到 1 去 S1否则留在 S0S1 收到 0 去 S2收到 1 留在 S1因为要检测的是 10010第二个 1 可能是新序列的开头S2 收到 0 去 S3收到 1 回 S1S3 收到 1 去 S4收到 0 回 S0S4 收到 0 去 S5收到 1 回 S1S5 收到 0 回 S3收到 1 回 S1。S5 回 S3 这个转移容易被忽略——检测完 10010 后末尾的 0 可以当作下一个序列的第二个 0 复用这是序列检测重叠匹配的标准处理。default分支必须有防止状态机跑飞进非法编码后锁死这是综合工具和 lint 检查都会盯的点。2.4 第三段Moore 输出只认当前状态第三段输出逻辑Moore 型的输出只跟当前状态有关always(rst or currentstate) begin if(!rst) z 0; else case(currentstate) S0: z 0; S1: z 0; S2: z 0; S3: z 0; S4: z 0; S5: z 1; default: z 0; endcase end只有 S5 时z为 1其余全 0。敏感列表里没有a这正是 Moore 型的特征——输出不随输入即时变化只在状态跳变后的下一个时钟沿更新。好处是输出干净、无组合毛刺代价是比 Mealy 型晚一个周期出结果。这段用阻塞赋值综合出来是纯组合译码。如果你把这段也写成时序逻辑加时钟沿输出会再晚一拍波形上就是「明明检测到了却迟迟不拉高」这是新手最常见的翻车点之一。3. 拆 Mealy 型源码输出提前一拍代价是组合路径变长3.1 状态数减少与转移条件的差异Mealy 型模块mealyfsm只有五个状态 S0 到 S4比 Moore 少一个因为输出可以在转移边上直接给出不需要专门停在 S5 等输出。状态定义同样是 4 位二进制parameter S0 4b0000; parameter S1 4b0001; parameter S2 4b0010; parameter S3 4b0011; parameter S4 4b0100;转移逻辑里 S4 收到 0 回 S2 而不是像 Moore 那样去 S5收到 1 回 S0。少一个状态意味着少一组寄存器面积略省但输出逻辑变复杂了下面会看到。3.2 输出为什么拆成 temp_z 和 z 两级Mealy 型的输出段是这份源码里最值得细看的地方它没有直接把z写成当前状态和输入的函数而是先用组合逻辑算出temp_z再用一个时序块打一拍输出always(rst or currentstate or a) begin if(!rst) temp_z 0; else case(currentstate) S0: temp_z 0; S1: temp_z 0; S2: temp_z 0; S3: temp_z 0; S4: temp_z (a 0)? 1 : 0; default: temp_z 0; endcase end always(posedge clk or negedge rst) begin if(!rst) z 0; else begin if((temp_z 1)(nextstate S2)) z 1; else z 0; end endtemp_z在 S4 且输入为 0 时置 1表示「当前状态 S4 收到 0即将完成 10010 检测」。第二个时序块在时钟沿判断temp_z为 1 且次态是 S2 时把z拉高。这种写法本质上是把 Mealy 的组合输出又打了一拍好处是输出无毛刺、时序好收敛坏处是它已经不是教科书意义上「输出随输入即时变化」的纯 Mealy 了而是带寄存输出的变体。参数上要盯的是nextstate S2这个条件——它依赖次态组合逻辑的稳定如果次态逻辑有竞争冒险这里会跟着抖。我一般会建议要么老老实实写纯组合 Mealy 输出z (currentstateS4 a0)要么就明确这是「寄存输出 Mealy」别在文档里含糊。3.3 两套 testbench 的激励序列与时钟参数两个 testbench 结构几乎一样实例化对应模块产生clk、rst、ainitial begin clk 0; rst 1; #5 rst 0; #3 rst 1; #20 a 1; #100 a 1; #100 a 0; // ... 后续激励 end always #50 clk ~clk;时钟周期是 100 个时间单位#50翻转两次复位在 5 到 8 之间拉低再释放之后每 100 单位改一次输入。这里有个明显的坑输入a的每次变化间隔 100正好等于一个时钟周期但变化时刻没有和时钟沿对齐仿真波形上输入跳变可能落在时钟沿附近产生建立保持时间的假象。做功能仿真问题不大但如果要跑门级仿真或看时序激励必须和时钟沿错开半个周期。常见做法是把输入变化放在#50这种时钟低电平中间或者用(negedge clk)驱动输入。另外激励序列里连续多个 1 和 0 的排列是为了覆盖重叠匹配的情况读波形时要对着状态跳转逐拍核对别只看z拉没拉高。4. 避坑与排查这五处翻车我见过太多次4.1 输出晚一拍误以为是状态机写错现象仿真里序列明明检测到了z却比预期晚一个或两个周期才拉高。原因Moore 型输出本来就比 Mealy 晚一拍如果第三段又误写成时序逻辑会再晚一拍。解决先确认输出段是组合逻辑敏感列表含currentstate、无posedge clk再对照 Moore/Mealy 的固有延迟差异别把正常延迟当 bug 改。4.2 复位释放时刻不对状态机从非法态启动现象仿真开头currentstate是 X 或乱码跑几个周期才恢复正常。原因testbench 里rst拉低时间太短或者复位释放和时钟沿撞在一起。解决复位至少保持一个完整时钟周期以上释放时刻放在时钟低电平中间源码里#5 rst0; #3 rst1;只保持了 3 个时间单位偏短建议改成保持 20 以上。4.3 独热码注释与赋值不符位宽算错现象照着注释以为是独热码改状态时位宽对不上综合报错或状态译码错误。原因源码注释写「独热码」实际是 4 位二进制递增。解决以赋值语句为准六个状态 3 位足够给 4 位是余量真要独热码就改成 6 位一位有效别混着来。4.4 case 漏 default状态跑飞后锁死现象仿真跑久了状态机卡在某个非法编码输出恒定不变。原因次态或输出 case 没有default分支非法状态无法回到 S0。解决两段 case 都补default次态回 S0、输出回 0这是综合和 lint 的硬要求。4.5 Mealy 输出条件依赖次态竞争冒险被放大现象z在时钟沿附近出现窄脉冲毛刺。原因z的赋值条件里用了nextstate S2而nextstate是组合逻辑输出输入变化时可能瞬时抖动。解决要么改成纯组合 Mealy 输出只依赖currentstate和a要么保留寄存输出但把条件改成只依赖temp_z和当前状态别跨到次态上。5. 进阶验证把两套状态机放进同一个 testbench 对拍单看一套波形容易漏掉边界我习惯把 Moore 和 Mealy 实例化到同一个 testbench 里用同一组激励驱动对比两者输出的时序差。这样既能验证功能又能直观看到「Mealy 早一拍、Moore 稳一拍」的差别。下面是对拍骨架module tb_compare; reg clk, rst, a; wire z_moore, z_mealy; moorefsm u_moore(.clk(clk), .rst(rst), .a(a), .z(z_moore)); mealyfsm u_mealy(.clk(clk), .rst(rst), .a(a), .z(z_mealy)); initial begin clk 0; rst 1; a 0; #20 rst 0; #40 rst 1; // 用 task 逐位送激励和时钟沿错开 send_bit(1); send_bit(0); send_bit(0); send_bit(1); send_bit(0); send_bit(0); send_bit(1); send_bit(0); send_bit(0); send_bit(1); send_bit(0); send_bit(0); send_bit(1); send_bit(0); #200 $finish; end task send_bit(input b); begin (negedge clk); a b; // 在时钟低电平中间改输入 end endtask always #50 clk ~clk; endmodule这段的关键在send_bit任务用(negedge clk)把输入变化对齐到时钟下降沿彻底避开上升沿的建立保持窗口比源码里裸写#100靠谱得多。参数上#50决定时钟周期 100send_bit每调用一次推进一个周期激励序列 10010 重复出现覆盖了重叠匹配。跑完看波形正常结果是 Mealy 的z比 Moore 早一个周期拉高且两者在重叠序列处都能连续命中。如果 Mealy 出现毛刺而 Moore 干净说明输出段的组合路径有问题回去查 4.5 那条。对拍还有个隐藏价值它能暴露复位释放的时序问题。两套状态机共用rst如果复位释放时刻不对两个模块会同时从非法态启动波形上一眼就能看出来比单独仿真一套更容易定位。我现在的习惯是任何序列检测类作业或项目先把 Moore 和 Mealy 并排放进一个 testbench用对齐到下降沿的 task 送激励跑一遍对拍再谈综合。从那以后我每次写状态机都强制走一遍「对拍 下降沿驱动」这套流程省下的调试时间比写代码本身还多。希望帮到你。本文还有配套的精品资源点击获取