简介:面向数字电路与FPGA初学者及需要巩固状态机写法的工程师,这份文档围绕Verilog经典三段式状态机展开,聚焦当前状态、下一状态与输出逻辑分离的实现思路。正文以Moore型序列10010检测为例,给出独热编码状态定义、currentstate/nextstate寄存器、case状态转移及输出赋值,状态在S0至S5间按输入跳转,检测到目标序列后输出置1,并附tb_fsm测试模块的时钟、复位和输入激励,便于对照仿真验证。文档还比较Mealy型输出受当前输入影响的特点,说明Moore输出稳定、Mealy可减少状态数但可能随输入抖动,选型需结合响应速度与稳定性要求,通过对比可加深对两种状态机适用场景的理解。资源包仅含1个docx文档,约124KB,篇幅紧凑,适合课程实验或项目设计前查阅。已有1259人学习,可帮助读者理解三段式写法、测试平台搭建与常见排错要点。
1. 从一份 10010 序列检测代码说起:三段式状态机到底解决了什么
手里这份讲 verilog 三段式状态机的 docx,第一眼看过去很朴素:一个 Moore 型 10010 序列检测器、一个 Mealy 型版本,各自配一个 testbench,代码加起来不到两百行。真正让我停下来的是状态编码那几行——注释写着“独热码”,可parameter给的是4'b0000到4'b0101,这是四位二进制编码,六个状态只用了三个 bit 就能表示。这种注释和实现对不上的细节,恰恰是状态机从仿真走到上板最容易翻车的地方,也是我拿它当例子的原因。
这份资源能解决的事情很具体:给你两套能直接编译、能改序列、能对照波形看时序的模板,顺带把“输出段到底该组合还是寄存”“重叠检测的回退边怎么画”这两个新手绕不开的分歧点摆到台面上。适合刚学完 Verilog 语法、手上有仿真器却不知道从哪下手的同学;也适合已经写过一段式状态机、被综合告警和上板漏检折腾过、想换一套更规矩写法的从业者。下面按我拆代码的顺序走,先 Moore,再 Mealy,最后落到仿真和避坑。
2. Moore 机落地:6 个状态、3 段 always 和一张必须自己画一遍的转移表
2.1 三段式分的到底是哪三段
一段式状态机把所有逻辑塞进一个always块,状态寄存器、次态判断、输出赋值混在一起,代码短,但综合工具读起来费劲,人读起来更费劲。两段式把状态寄存器和输出拆开,次态逻辑还和输出纠缠。三段式的分法是:第一段只写状态寄存器,时钟沿到来时把nextstate打进currentstate;第二段纯组合逻辑,根据currentstate和输入算出nextstate;第三段单独处理输出。
这么拆的好处不只是“好看”。第一段和第二段分离之后,综合工具能明确识别出一组触发器加一张组合查找表,时序分析时路径清晰;第二段写成纯组合的case,覆盖不全时综合会报 latch,等于免费送你一个检查器;第三段独立出来之后,输出要组合还是要寄存,改一个块就行,不用动状态转移。我一般会要求团队里所有状态机都按这个骨架写,理由很现实——出问题时能一眼定位是转移错了还是输出错了。
需要提醒一句:三段式的“第三段”不一定是时序逻辑。Moore 机的输出只跟当前状态有关,用组合写完全合法;Mealy 机输出跟输入有关,用组合写就带毛刺风险。这两种选择后面都会碰到,先把结构立住。
2.2 从状态转移图到 case 表:10010 的六个状态怎么来的
检测串行输入里的10010,Moore 机需要记住“已经匹配到哪一步”。六个状态的含义是这样:S0 什么都没匹配上;S1 收到一个1;S2 收到10;S3 收到100;S4 收到1001;S5 收到完整的10010。每来一个 bit,就沿着“当前已匹配的最长前缀”往前走一步或者回退。
难点在回退。比如已经在 S3(匹配了100),这时候来一个0,序列变成1000,它的后缀里没有任何一个是10010的前缀(1、10、100、1001),所以只能回 S0。再比如在 S5(已经匹配10010),再来一个0,序列尾部是100100,后缀100正好等于前缀100,于是回 S3 而不是 S0——这就是重叠检测。回退边画错,波形上表现为“该出的脉冲没出”,而且往往只在特定输入组合下复现,很难查。
把六个状态在两个输入下的去向列成表,代码就只是抄表:
| 状态 | 含义 | a=0 次态 | a=1 次态 | 输出 z |
|---|---|---|---|---|
| S0 | 无匹配 | S0 | S1 | 0 |
| S1 | 已匹配 “1” | S2 | S1 | 0 |
| S2 | 已匹配 “10” | S3 | S1 | 0 |
| S3 | 已匹配 “100” | S0 | S4 | 0 |
| S4 | 已匹配 “1001” | S5 | S1 | 0 |
| S5 | 已匹配 “10010” | S3 | S1 | 1 |
表里 S1 在 a=1 时留在 S1,是因为11的后缀1仍然是前缀1;S2 在 a=1 时去 S1,是因为101的后缀1匹配前缀。这两条是最容易被写成“回 S0”的地方,写错不会报错,只会少检测。
照着表写出来的 Moore 版本是这样:
module moore_fsm ( input wire clk, input wire rst_n, // 低电平异步复位 input wire a, output reg z ); // 6 个状态,3 bit 二进制编码足够;独热码需要 6 bit,见第 5 章 localparam [2:0] S0 = 3'd0, // 初始 S1 = 3'd1, // 已收到 1 S2 = 3'd2, // 已收到 10 S3 = 3'd3, // 已收到 100 S4 = 3'd4, // 已收到 1001 S5 = 3'd5; // 已收到 10010 reg [2:0] cur_state, nxt_state; // 第一段:状态寄存器,只做一件事 always @(posedge clk or negedge rst_n) begin if (!rst_n) cur_state <= S0; else cur_state <= nxt_state; end // 第二段:次态组合逻辑,穷举所有状态与输入组合 always @(*) begin case (cur_state) S0: nxt_state = a ? S1 : S0; S1: nxt_state = a ? S1 : S2; S2: nxt_state = a ? S1 : S3; S3: nxt_state = a ? S4 : S0; S4: nxt_state = a ? S1 : S5; S5: nxt_state = a ? S1 : S3; default: nxt_state = S0; // 防止综合出 latch endcase end // 第三段:输出与状态同拍,采样次态避免组合毛刺 always @(posedge clk or negedge rst_n) begin if (!rst_n) z <= 1'b0; else z <= (nxt_state == S5); end endmodule这段代码有三处和原始文档不同,都是有意为之。状态编码从parameter改成localparam,避免被上层例化时误覆盖;敏感列表从手写的always@(currentstate or a or rst)改成always @(*),少写一个信号就是一次血泪教训;输出段用nxt_state == S5而不是cur_state == S5,这样 z 拉高的那一拍正好是cur_state变成 S5 的那一拍,不会晚一拍。
输出段的写法值得单独展开。如果写成组合的z = (cur_state == S5),功能上完全正确,仿真波形也漂亮,但六个状态的译码在硬件上是一堆与门或门,状态位同时翻转时输出可能出窄脉冲。如果写成z <= (cur_state == S5),毛刺没了,代价是 z 比状态晚一拍,检测结果滞后一个时钟周期。而z <= (nxt_state == S5)是把“下一拍会不会进 S5”提前算出来寄存,既没有组合毛刺,又和状态同拍。三种写法我都用过,综合面积差别不大,关键是要和下游模块的采样时刻对齐。
参数上唯一需要留意的是复位极性。原始代码用rst高有效之外的写法容易混,我这里统一成rst_n低有效,第一段和第三段都带异步复位。如果项目规范要求同步复位,把negedge rst_n去掉、复位判断挪进时钟沿即可,但要注意同步复位对复位信号宽度的要求。
2.3 输出段用组合还是寄存:一个必须提前做的决定
这个问题在不同项目里答案不一样,判断依据是下游怎么采样。如果状态机的输出直接驱动另一个同步模块的输入,那个模块在时钟沿采样,那么组合输出的毛刺在采样时刻之前早就稳定了,用组合没问题,还省一个触发器。如果输出要送到芯片引脚、或者送到异步的 combinatorial 逻辑、或者要跨时钟域,组合输出就是给自己埋雷。
我一般会问三个问题再决定:输出有没有直接出片?下游是不是纯同步采样?时序余量紧不紧?三个都是“是”以外的答案,就老老实实寄存。寄存带来的延迟可以通过在协议上约定“检测有效信号晚一拍”来消化,比上板后随机误触发好处理得多。
还有一个折中做法是组合输出加一个由时钟打拍的 valid 信号,下游用 valid 采样数据。这种写法在总线类设计里很常见,代价是多一根线,好处是数据路径没有额外延迟。这份资源的例子里没有 valid,所以我在上面的代码里直接选了寄存次态,算是用最少的改动换最稳的行为。
2.4 测试平台怎么写才不是“看波形猜”
原始 testbench 用一串#100 a = ...手动给激励,时钟always #50 clk = ~clk,也就是 100ns 周期。问题出在激励和时钟沿的相对位置上:a在任意时刻变化,很可能正好落在posedge clk的建立保持窗口里,仿真器按调度顺序给个值,波形看着“差不多对”,换一个仿真器或者换timescale就变了。更麻烦的是没有自动比对,只能靠眼睛数 z 的脉冲个数。
我习惯的写法是用 task 在下降沿驱动激励,上升沿采样比较,再加一个移位寄存器做参考模型:
`timescale 1ns/1ps module tb_moore; reg clk, rst_n, a; wire z; reg [4:0] shift; // 参考模型:最近 5 个输入 bit integer err_cnt; moore_fsm dut(.clk(clk), .rst_n(rst_n), .a(a), .z(z)); initial clk = 1'b0; always #5 clk = ~clk; // 10ns 周期,100MHz // 下降沿改变输入,远离建立保持窗口 task send_bit(input logic b); begin @(negedge clk); a = b; end endtask // 参考模型:输入移位,命中 10010 即期望输出 1 always @(posedge clk or negedge rst_n) begin if (!rst_n) shift <= 5'b0; else shift <= {shift[3:0], a}; end // 上升沿后采样比较,err_cnt 为 0 才算通过 always @(posedge clk) begin if (rst_n && (z !== (shift == 5'b10010))) begin err_cnt = err_cnt + 1; $display("MISMATCH @%0t: a=%b z=%b expect=%b", $time, a, z, (shift == 5'b10010)); end end initial begin $dumpfile("tb_moore.vcd"); $dumpvars(0, tb_moore); err_cnt = 0; rst_n = 1'b0; a = 1'b0; repeat (3) @(negedge clk); // 复位保持 3 个周期 rst_n = 1'b1; // 10010 10010 中间夹一个干扰段 send_bit(1); send_bit(0); send_bit(0); send_bit(1); send_bit(0); send_bit(0); send_bit(0); send_bit(1); send_bit(1); send_bit(1); send_bit(1); send_bit(0); send_bit(0); send_bit(1); send_bit(0); repeat (2) @(negedge clk); if (err_cnt == 0) $display("PASS: all samples matched"); else $display("FAIL: %0d mismatches", err_cnt); $finish; end endmodule逻辑说明:send_bit在下降沿赋值,保证上升沿采样时a已经稳定了半个周期;参考模型shift和 DUT 在同一个时钟沿更新,两者处于同一拍,可以直接比较;比较语句放在posedge clk的always里,仿真器会在同一个时间步内先执行完所有非阻塞赋值再执行它,因此z和shift都是更新后的值。!==用来把 X 态也判成错误,避免复位没生效却显示 PASS。
参数上,shift的位宽等于待检测序列长度,换序列时这个位宽和比较常量一起改。repeat (3) @(negedge clk)是我习惯的复位长度,同步复位至少两个周期,异步复位也建议给足,仿真里省这几纳秒没意义。$dumpvars(0, tb_moore)的 0 表示 dump 整棵层次,波形文件会大一些,调状态机时这点体积不算什么。
3. Mealy 机改写:输出提前一拍,回退边和打拍逻辑最容易翻车
3.1 Mealy 与 Moore 的差别不止是“少一个状态”
Moore 的输出只由状态决定,序列检测完成那一刻要先进入一个“完成态”,输出在完成态里为 1。Mealy 的输出由状态和当前输入共同决定,当状态表示“已匹配前四位 1001”、当前输入是 0 时,输出立刻为 1,不需要额外的完成态。所以同样检测10010,Mealy 只需要五个状态,输出比 Moore 早一个时钟周期。
提前这一拍有实际价值。在高速串行协议里,早一拍意味着后面组合逻辑的余量多一拍;在需要快速响应的控制器里,Mealy 的状态数少、译码简单。代价是输出是组合的,输入上的任何毛刺都会直接传到输出,而且输出脉冲的宽度只有一个输入 bit 的持续时间,下游如果采样不及时就会丢。
原文档里的 Mealy 代码有五个状态 S0 到 S4,但输出部分用了一个temp_z再打一拍,这个写法值得单独说,因为它把 Mealy 的时序优势又还回去了。
3.2 重叠检测下的回退路径
Mealy 的转移表比 Moore 少一行,但回退边同样要按“最长后缀匹配前缀”来推:
| 状态 | 含义 | a=0(次态/输出) | a=1(次态/输出) |
|---|---|---|---|
| S0 | 无匹配 | S0 / 0 | S1 / 0 |
| S1 | 已匹配 “1” | S2 / 0 | S1 / 0 |
| S2 | 已匹配 “10” | S3 / 0 | S1 / 0 |
| S3 | 已匹配 “100” | S0 / 0 | S4 / 0 |
| S4 | 已匹配 “1001” | S2 / 1 | S1 / 0 |
关键两行是 S4。a=0 时,序列变成10010,命中,输出 1;同时因为后缀10等于前缀10,下一状态应该是 S2 而不是 S0,这样紧接着再来010就能再命中一次,实现重叠检测。a=1 时,序列变成10011,没命中,但后缀1等于前缀1,下一状态应该是 S1 而不是 S0。
原始代码在 S4 的 a=1 分支写的是nextstate = S0,这就是一个会漏检的 bug。举个能复现的输入:1 0 0 1 1 0 0 1 0。按正确逻辑,S4 收到 1 回 S1,接着 0 进 S2、0 进 S3、1 进 S4、0 命中;按原始代码,S4 收到 1 回 S0,后面的0010从 S0 重新开始,永远凑不齐10010,输出一直是 0。这种错误在随机激励下命中率不高,但用定向用例一测就现形。
3.3 原代码的 temp_z 打拍写法为什么绕
原始 Mealy 模块的输出路径是这么走的:先用组合逻辑算temp_z,只在 S4 且 a=0 时为 1;然后在时钟沿判断(temp_z == 1) && (nextstate == S2),成立则z <= 1。这个写法的意图大概是“想要 Mealy 的状态数,又想要寄存输出”,但实际效果是输出比标准 Mealy 晚一拍,而且和状态的对应关系变得隐晦。
更麻烦的是nextstate == S2这个附加条件。它依赖次态组合逻辑的当前值,等于把输出判断和转移判断耦合在一起。一旦有人改了 S4 在 a=0 时的次态(比如为了别的重叠规则改成 S3),输出条件就同时失效,而改代码的人未必意识到这一点。这种“一个信号承担两个语义”的写法,是状态机里最典型的可维护性陷阱。
标准写法有两种。要组合输出就直接assign z = (cur_state == S4) && (a == 0);,一行搞定,代价是毛刺。要寄存输出就写成下面这样,用时钟沿采样当前状态和输入:
module mealy_fsm ( input wire clk, input wire rst_n, input wire a, output reg z ); localparam [2:0] S0 = 3'd0, S1 = 3'd1, S2 = 3'd2, S3 = 3'd3, S4 = 3'd4; reg [2:0] cur_state, nxt_state; // 第一段:状态寄存器 always @(posedge clk or negedge rst_n) begin if (!rst_n) cur_state <= S0; else cur_state <= nxt_state; end // 第二段:次态组合逻辑,S4 的两条回退边是重点 always @(*) begin case (cur_state) S0: nxt_state = a ? S1 : S0; S1: nxt_state = a ? S1 : S2; S2: nxt_state = a ? S1 : S3; S3: nxt_state = a ? S4 : S0; S4: nxt_state = a ? S1 : S2; // 1 回 S1,0 重叠回 S2 default: nxt_state = S0; endcase end // 第三段:寄存输出,与状态转移同拍 always @(posedge clk or negedge rst_n) begin if (!rst_n) z <= 1'b0; else z <= (cur_state == S4) && (a == 1'b0); end endmodule逻辑说明:第三段在时钟沿采样的是“沿前的cur_state和a”,这正好是 Mealy 定义里“当前状态 + 当前输入”的组合;采样结果在沿后输出,与cur_state跳到 S2 发生在同一拍,所以下游看到z=1时状态已经完成了转移。这比原始代码晚一拍,但语义清晰:z 就是“这一拍发生了 10010 命中”。
参数上,Mealy 的z宽度是 1 bit,如果要做多序列检测,可以把z扩成向量,每一位对应一个模式,第三段改成对每个模式分别判断。状态编码用 3 bit 二进制,五个状态实际只用到 0 到 4,综合工具会自动优化掉不用的编码,不必手动改成独热。
3.4 自检测试平台:用 task 和参考模型代替肉眼
Mealy 的 testbench 可以复用 Moore 那套骨架,只改参考模型和期望值。参考模型要模拟 Mealy 的“提前一拍”特性:在输入 bit 到来的那个沿就判断,而不是等移位寄存器凑满五位之后再判断。简单做法是用“前四位 + 当前位”拼成五位移位寄存器,在时钟沿比较,和 DUT 的采样时刻对齐:
// Mealy 参考模型:沿前 cur 状态对应的最近 4 位 + 当前 a reg [3:0] hist; always @(posedge clk or negedge rst_n) begin if (!rst_n) hist <= 4'b0; else hist <= {hist[2:0], a}; end wire ref_hit = (hist == 4'b1001) && (a == 1'b0);注意hist和a在posedge clk时刻的值构成“前四位是 1001、当前位是 0”,和 DUT 第三段采样的是同一组信号,可以直接比较z !== ref_hit。这个参考模型的写法不依赖状态编码,换序列时只改常量,比对着状态机再写一遍更容易发现设计错误。
覆盖率方面,定向用例至少要覆盖:正常命中一次、连续重叠命中(10010010)、命中后接干扰位(100101...)、复位中途打断、以及在 S4 处来 1 的回退路径。随机激励可以加,但随机激励跑一万个周期没报错,不代表定向用例能过,这两者是互补关系。
4. 仿真跑通与自动比对:iverilog 命令、波形抓手和综合前自检
4.1 编译与运行的最小命令集
拿到两个.v文件之后,最先要确认的是它们能编译。开源工具链里 iverilog 够用,命令不复杂:
# 编译:-g2012 打开 SystemVerilog 子集,-o 指定可执行文件名 iverilog -g2012 -o sim_moore tb_moore.v moore_fsm.v # 运行:当前目录会生成 tb_moore.vcd vvp sim_moore # 看波形(装了 gtkwave 的话) gtkwave tb_moore.vcd &-g2012这个参数值得解释。默认的 iverilog 按 Verilog-2005 解析,遇到logic、always_comb这类 SystemVerilog 关键字会报错。上面的 testbench 里用了logic作为 task 参数类型,所以需要打开 2012 模式。如果坚持纯 Verilog-2005,把logic换成reg也能编译,但always @(*)和$dumpvars这些用法在 2005 里同样合法,不必为了兼容性退回更老的写法。
vvp执行的是 iverilog 生成的中间代码,不是原生二进制,所以换机器时要重新编译。波形文件默认写在当前工作目录,$dumpfile里给的相对路径相对于vvp的执行目录,不是源文件目录,这一点在脚本里跑仿真时经常搞混。
4.2 波形之外,先把自动比对跑起来
波形是给眼睛看的,回归是给 CI 看的。上面两个 testbench 都带了err_cnt和$display,跑完看最后一行是 PASS 还是 FAIL 就行。如果要把仿真接进脚本,可以用$fatal让失败时返回非零退出码:
if (err_cnt != 0) begin $display("FAIL: %0d mismatches", err_cnt); $fatal(1); // 让 vvp 以非零状态退出,方便 CI 判断 end $display("PASS"); $finish;$fatal(1)的参数是退出码,iverilog 支持这个用法。用它替代$finish之后,Makefile 里写vvp sim_moore || exit 1就能卡住失败。$display的输出建议带上$time和关键信号,失败时不用重新跑一遍就知道是哪一拍出的问题。
波形方面,看状态机重点看三个信号:cur_state、a、z。把cur_state的显示格式改成二进制或者符号名,gtkwave 里可以右键选择枚举显示,把3'd0显示成S0,对着转移表看就快得多。如果波形里cur_state出现 X,先查复位有没有生效;如果z一直是 0 而cur_state正常跳,查第三段的比较常量是不是写错了状态名。
4.3 综合前自检:latch、位宽与敏感列表
仿真通过只是第一步,综合工具会从另一个角度挑毛病。没装商业工具的话,yosys 可以做一轮快速检查:
# 读入设计,展开层次,跑通用综合流程并打印资源统计 yosys -p "read_verilog moore_fsm.v; synth -top moore_fsm; stat"输出里重点看两类信息。一是Warning: Latch inferred,出现就说明某个组合always块存在未覆盖的分支,状态机的case一定要写default,输出段的case如果写成组合也要写全。二是触发器数量,六个状态的 Moore 机应该是 3 个状态触发器加 1 个输出触发器,如果数字明显不对,多半是编码或者复位写法让工具推断出了额外寄存器。
下面这张表是我交付状态机代码前会逐项过的检查项:
| 检查项 | 期望结果 | 不满足时的典型现象 |
|---|---|---|
| 组合块敏感列表 | always @(*) | 仿真与综合不一致 |
| 组合块赋值符 | 阻塞= | 仿真出现意外锁存 |
| 时序块赋值符 | 非阻塞<= | 波形出现竞争,状态跳错 |
case覆盖 | 有default | 综合报 latch |
| 状态编码位宽 | 能容纳全部状态 | 工具报位宽截断 |
| 复位极性 | 全模块统一 | 上电后状态不定 |
| 输出采样时刻 | 与下游对齐 | 上板偶尔漏检或重复触发 |
这张表不复杂,但每一条我都见过真实的翻车案例,尤其是敏感列表和赋值符这两条,在小模块里看不出问题,模块一大就开始随机出错。
5. 避坑实录:五个让“仿真通过”变成“上板漏检”的细节
5.1 注释写“独热码”,编码却是四位二进制
现象:综合报告里状态寄存器只有 4 个触发器,和注释里说的独热码对不上;换一个综合工具后资源数字又变了。
原因:原始代码parameter S0 = 4'b0000到S5 = 4'b0101,六个状态用四个 bit 表示,这是二进制编码,不是独热码。真正的独热码是六个 bit,每个状态只有一位为 1,比如6'b000001、6'b000010。注释和实现不一致,后面维护的人会按注释去理解,改错是迟早的事。
解决:要么把编码改成真正的独热码并把位宽改成 6,要么把注释改成“二进制编码”。我一般选后者,把编码交给综合工具去优化,代码里只保留localparam和状态含义注释。如果时序紧张、需要手动控制编码,再显式写独热并同步改注释。
5.2 Mealy 的 S4 在 a=1 时回 S0
现象:输入100110010,期望在最后的 0 处输出一次,实际输出一直是 0;换成10010010却能正常检测。
原因:S4 表示已匹配1001,来 1 之后序列是10011,最长后缀1等于前缀1,正确次态是 S1。写成 S0 就把这个 1 丢了,后面的 0、0、1、0 从初始状态重新开始,凑不齐完整序列。
解决:把 S4 的 a=1 分支改成 S1,并在 testbench 里加一条专门走这条回退边的定向用例。重叠检测的回退边不能凭感觉写,按“最长后缀匹配前缀”逐个状态推一遍,推完对着表抄。
5.3 复位时序和激励不对齐
现象:第一次仿真时z前几十纳秒是 X,或者第一个脉冲比预期早一拍;换一个仿真器结果又不一样。
原因:原始 testbench 里rst = 1; #5 rst = 0; #3 rst = 1;,复位只拉低了 3ns,而时钟周期是 100ns,复位宽度还不到一个周期;同时a在#20就开始变化,和时钟沿的相对位置是任意的,可能正好落在建立保持窗口里。
解决:复位至少保持两个时钟周期,所有激励改到下降沿驱动,比较放到上升沿之后。testbench 里用repeat (N) @(negedge clk)控制节奏,不要用绝对延时堆时间,换时钟频率时绝对延时要全部重算。
5.4 组合输出有毛刺,仿真对、上板错
现象:仿真波形上z干净得很,上板之后下游计数器偶尔多计一次;用示波器抓z能看到纳秒级窄脉冲。
原因:第三段用组合逻辑直接对多位状态译码,状态位在时钟沿同时翻转,不同位的传播延迟不同,译码输出会出现瞬间的错误组合。仿真器是零延迟模型,看不到这个过程。
解决:输出改寄存,用z <= (nxt_state == S5)这类写法保持与状态同拍;或者在下游加一级时钟采样。如果必须组合输出,至少保证下游是同步采样的,并且译码只依赖一位状态(独热码在这个场景下反而有优势)。
5.5 阻塞赋值和非阻塞赋值混用
现象:ModelSim 里行为正确,综合后状态跳转错乱;或者加了一个$display之后结果又对了。
原因:时序always块里用了=,组合块里用了<=。阻塞赋值按顺序立即生效,非阻塞赋值在时间步末尾统一更新,混用会让仿真结果依赖块的执行顺序和激励的调度顺序,和硬件的并行行为对不上。
解决:定一条死规矩——时序块一律<=,组合块一律=,assign用于纯连线。代码 review 时先扫这两条,比看逻辑本身更快发现问题。工具方面,Verilator 的 lint 模式对这类问题会直接报错,有条件的话在提交前跑一遍。
6. 把它变成可复用模板:参数化序列检测器与交付前检查清单
上面两个模块都是针对固定序列10010写的,实际项目里序列会变。我的做法是把序列长度和模式做成参数,状态数由长度推出来,回退边则用一个小的函数在编译期算好,避免每次手推。思路不复杂:状态编号k表示已匹配前k位,来一位新输入后,从k+1开始往下找最长的、既是模式前缀又是当前后缀的长度,找到几就跳到几。
// 参数化 Moore 序列检测器的骨架,PATTERN 低位先入 module seq_detect #( parameter [7:0] PATTERN = 8'b0001_0010, // 10010,低位先到 parameter LEN = 5 ) ( input wire clk, input wire rst_n, input wire a, output reg hit ); localparam ST_NUM = LEN + 1; localparam CW = $clog2(ST_NUM); // 状态位宽由状态数决定 reg [CW-1:0] cur_state, nxt_state; // 计算“当前已匹配 k 位,来 bit b 之后”应回到的状态 function [CW-1:0] fallback(input [CW-1:0] k, input b); integer i; reg [LEN:0] cand; // 候选后缀 begin cand = {cur_state_history(k), b}; // 伪代码:取最近 k 位加 b fallback = 0; for (i = LEN; i > 0; i = i - 1) if (cand[i-1:0] == PATTERN[i-1:0]) begin fallback = i; i = 0; // 找到最长即退出 end end endfunction // 第一段:状态寄存器 always @(posedge clk or negedge rst_n) if (!rst_n) cur_state <= 0; else cur_state <= nxt_state; // 第二段:次态,命中后按重叠规则回退 always @(*) begin if (a == PATTERN[cur_state]) // 下一位对得上,前进 nxt_state = cur_state + 1'b1; else nxt_state = fallback(cur_state, a); end // 第三段:寄存输出,与状态同拍 always @(posedge clk or negedge rst_n) if (!rst_n) hit <= 1'b0; else hit <= (nxt_state == ST_NUM - 1); endmodule这段骨架里fallback函数用了伪代码表示“取最近 k 位”,实际实现需要把历史 bit 存进一个移位寄存器,或者用PATTERN的前缀表在编译期展开成常量。工程上更常见的做法是写一个 Python 脚本,按模式生成本文件的case表,再粘贴进 Verilog,这样综合结果最干净,也不依赖函数在综合工具里的支持程度。我一般会选脚本生成,因为状态转移表是纯组合逻辑,生成的case比运行时计算更容易被工具优化,出了问题也能对着表查。
换成 Mealy 时,状态数减一,输出条件改成“当前状态是LEN-1且输入等于模式最后一位”。注意 Mealy 的输出是组合语义,即使你寄存它,判断条件里也要包含当前输入,不能只看状态,否则就退化成 Moore 了。
交付前的清单我会固定跑一遍:状态转移表是否手工推过一遍并且和代码逐行对得上;case是否有default;时序块是否全部<=;组合块是否全部=且敏感列表是@(*);输出采样时刻是否和下游对齐;testbench 是否有参考模型和err_cnt;定向用例是否覆盖了重叠命中和回退边;综合日志里是否有 latch 告警。这八条跑完,状态机基本不会在集成阶段出问题。
说个自己的教训。早些年我交付一个序列检测模块,仿真跑了十万个随机周期全过,直接进了系统联调,结果在现场偶发漏检。查了三天才发现是 S4 的一条回退边写错,随机激励里那个特定组合出现的概率不到万分之一,而定向用例里根本没覆盖。从那以后我每次写状态机,都先把转移表手工推一遍、把回退边单独列成定向用例,再谈随机回归和覆盖率。状态机这东西,玄学的地方不多,翻车基本都翻在“以为推对了”的那几条边上。希望帮到你。
本文还有配套的精品资源,点击获取