简介:面向数字电路设计与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 用的是4'b0000到4'b0101,注释写着「独热码」,但实际赋值是二进制递增,并不是真正的独热编码(独热码应该是 000001、000010 这种一位有效)。这是个典型的文档与代码不一致的点,新手照着注释理解会绕进去。
parameter S0 = 4'b0000; parameter S1 = 4'b0001; parameter S2 = 4'b0010; parameter S3 = 4'b0011; parameter S4 = 4'b0100; parameter S5 = 4'b0101;这段参数定义决定了状态位宽。六个状态用 3 位就够(0 到 5),源码给 4 位是留了余量,综合工具会自动优化掉未用编码。如果你要改成真正的独热码,六个状态需要 6 位,写成6'b000001到6'b100000,好处是次态译码只需比较一位,速度快,代价是寄存器多。常见做法是:状态数少于 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 = (a==1)?S1:S0; S1: nextstate = (a==0)?S2:S1; S2: nextstate = (a==0)?S3:S1; S3: nextstate = (a==1)?S4:S0; S4: nextstate = (a==0)?S5:S1; S5: nextstate = (a==0)?S3:S1; default: nextstate = S0; endcase end这里用阻塞赋值=,因为它是组合逻辑。逐条读转移条件:S0 收到 1 去 S1,否则留在 S0;S1 收到 0 去 S2,收到 1 留在 S1(因为要检测的是 10010,第二个 1 可能是新序列的开头);S2 收到 0 去 S3,收到 1 回 S1;S3 收到 1 去 S4,收到 0 回 S0;S4 收到 0 去 S5,收到 1 回 S1;S5 收到 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 = 4'b0000; parameter S1 = 4'b0001; parameter S2 = 4'b0010; parameter S3 = 4'b0011; parameter S4 = 4'b0100;转移逻辑里 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 = (currentstate==S4 && a==0)),要么就明确这是「寄存输出 Mealy」,别在文档里含糊。
3.3 两套 testbench 的激励序列与时钟参数
两个 testbench 结构几乎一样,实例化对应模块,产生clk、rst、a:
initial 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 rst=0; #3 rst=1;只保持了 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决定时钟周期 100,send_bit每调用一次推进一个周期,激励序列 10010 重复出现,覆盖了重叠匹配。跑完看波形,正常结果是 Mealy 的z比 Moore 早一个周期拉高,且两者在重叠序列处都能连续命中。如果 Mealy 出现毛刺而 Moore 干净,说明输出段的组合路径有问题,回去查 4.5 那条。
对拍还有个隐藏价值:它能暴露复位释放的时序问题。两套状态机共用rst,如果复位释放时刻不对,两个模块会同时从非法态启动,波形上一眼就能看出来,比单独仿真一套更容易定位。我现在的习惯是,任何序列检测类作业或项目,先把 Moore 和 Mealy 并排放进一个 testbench,用对齐到下降沿的 task 送激励,跑一遍对拍再谈综合。从那以后我每次写状态机都强制走一遍「对拍 + 下降沿驱动」这套流程,省下的调试时间比写代码本身还多。希望帮到你。
本文还有配套的精品资源,点击获取