news 2026/10/29 12:45:31

Verilog三段式状态机与10010序列检测器设计

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Verilog三段式状态机与10010序列检测器设计

简介:面向数字电路、FPGA与Verilog初学者的三段式状态机设计实例文档,聚焦控制逻辑与序列检测中常见的状态机建模问题,帮助读者理清Moore型与Mealy型状态机的结构差异、编码方式与输出逻辑。压缩包共1个docx文件,约124KB,内容以文字讲解配合源代码片段呈现,包含状态转移图、模块定义、测试激励与仿真观察要点。文档以检测序列10010为主线,给出Moore型状态机的完整实现:采用独热编码定义S0至S5六个状态,用currentstate与nextstate分离时序与组合逻辑,并通过case语句描述状态跳转和输出赋值;同时附带testbench,模拟时钟、复位与输入波形,便于在仿真中核对输出z是否在正确节拍拉高。Mealy型部分则说明输出与当前输入相关的写法,对比两者在状态数、响应速度与输出稳定性上的取舍。已有1259人学习,适合课程实验、笔试复习与工程入门时对照参考。

1. 三段式不是“写三个 always”,而是把时序和组合切开

写过 verilog 三段式状态机的人,大多有过这样一种体验:代码照着模板抄,综合出来的波形却差一拍。10010 序列检测器是特别典型的例子——Moore 版本用六个状态,Mealy 版本用五个状态,两段 case 语句长得几乎一样,可一旦把输出打一拍,两者的时序差别就被抹平了。三段式真正解决的从来不是“代码好看”这件事:它把状态寄存器从组合逻辑里单独摘出来,让状态只在时钟沿更新,让次态和输出各自成为纯组合函数。只有这样,仿真、综合、时序分析这三件事才会对同一份代码给出同一个结论。搞懂这三段各自守的边界,比背模板有用得多。

2. 10010 检测器的状态图:六个状态里藏着一次 KMP 回溯

2.1 状态的定义是“已匹配多长前缀”,不是“输入了哪几位”

第一次看 S0 到 S5 这六个状态,很容易把它们理解成“已经收到了 1、10、100、1001、10010”这几个字符缓冲区。这种理解在输入序列刚好一路正确的时候没问题,一旦中间断掉就开始糊涂:收到 100 之后来一个 0,为什么不是回到 S1 继续等,而是干脆回 S0?

正确的状态定义方式只有一句:当前状态 = 已输入序列的某个后缀中,能作为模式串 10010 前缀的最长长度。按这个定义:

  • S0 表示最长匹配前缀长度为 0;
  • S1 表示后缀里有“1”,长度 1;
  • S2 表示后缀里有“10”,长度 2;
  • S3 表示“100”,长度 3;
  • S4 表示“1001”,长度 4;
  • S5 表示“10010”,长度 5,也就是检测成功。

这个定义和字符串匹配里的 KMP 部分匹配表(failure function)是同一个东西,只是换了个硬件说法。之所以必须这么定义,是因为序列检测器要支持重叠检测:输入 100100 里,第 1 到第 5 位构成一次命中,第 2 到第 6 位是 00100 不构成命中,但如果输入 10010010,中间那次命中的尾部又能拼出新的命中。如果状态只记“当前走到模式的第几位”,一遇到不匹配就清零,重叠情况就会被漏掉。

2.2 失败回跳:S3 收到 0 回 S0、S4 收到 1 回 S1 是怎么推出来的

用“最长真后缀等于模式前缀”这个判据,所有转移都能手推出来,不需要背。以 S3 为例,它代表已匹配“100”,此时输入 0,新序列尾部变成“1000”。它的真后缀依次是“00”“0”,都不是 10010 的前缀,所以最长匹配长度退到 0,回 S0。再看 S4,已匹配“1001”,输入 1 后尾部是“10011”,真后缀“1”正好是模式串的第一个字符,最长匹配长度变成 1,所以回 S1,而不是回 S0。

S5 更值得单独说。它已经完整命中“10010”,此时输入 0,尾部变成“100100”,真后缀“100”是模式串的前三位,所以应该回 S3;输入 1,尾部“100101”,真后缀“1”匹配,回 S1。这两条正是重叠检测的关键——检测成功后不归零,而是退回一个仍可能继续匹配的位置。

把六行转移整理成表,就是 Moore 版本那段 case 的全部内容:

当前状态已匹配前缀输入 0输入 1
S0空S0S1
S1“1”S2S1
S2“10”S3S1
S3“100”S0S4
S4“1001”S5S1
S5“10010”S3S1

对照原始代码里的次态段,可以看到每一行都严格对应:

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" -> "10" 或保持 "1" S2: nextstate = (a==0)?S3:S1; // "10" 收到 1 变成 "101",回跳到 S1 S3: nextstate = (a==1)?S4:S0; // "100" 收到 0 变成 "1000",无可用后缀,回 S0 S4: nextstate = (a==0)?S5:S1; // "1001" 收到 1 变成 "10011",后缀 "1" 可用,回 S1 S5: nextstate = (a==0)?S3:S1; // 命中后仍保留重叠可能,回 S3 或 S1 default: nextstate = S0; // 非法编码兜底,防止综合出锁存器 endcase end

这段代码里,currentstate是 4 位寄存器,a是 1 位输入,rst是低电平有效的异步复位。三个信号都出现在敏感列表里,赋值一律用阻塞赋值=,因为它描述的是纯组合逻辑,必须在同一个仿真时间步内把值算完。default分支看着是废话,实际是防锁存的保险丝:综合工具遇到不完整的 case 会推断出电平敏感锁存器,那基本等于埋了个雷。

2.3 Moore 六态和 Mealy 五态不是功能相同,而是输出时机不同

Mealy 版本没有 S5,状态只到 S4。原因很直接:Moore 的输出只看状态,所以必须有一个“已经检测成功”的状态来承载 z=1;Mealy 的输出看状态加输入,站在 S4 上看到 a=0 的瞬间就能判断出完整模式出现,不需要再走一个状态。状态数少一个,是输出时机提前带来的红利,不是功能差异。

但“少一个状态”这件事有个前提:失败回跳必须和 Moore 版本完全一致。原始代码里,Mealy 版本在 S4 收到 a=1 时写的是回 S0,而按 2.2 的推导应该回 S1。这个差别在普通输入下看不出来,喂一串 100110010 就会翻车:Moore 版本在第 5 位到第 9 位之间能检出第二次命中,Mealy 版本在 S4 收到那个 1 之后直接掉回 S0,把“1”这个可用后缀丢了,后续的 10010 再也拼不起来。

这类问题不会报错,仿真波形也不会有红色,只有在对比两个版本的检测结果时才会露出来。我一般会这么理解:状态图是一个推导出来的对象,不是抄出来的对象,凡是代码里的转移和“最长后缀匹配前缀”推出来的不一致,都应该当 bug 处理,而不是当写法差异放过。

3. 三段式各管什么:状态寄存器、次态函数、输出译码

3.1 第一段只做一件事:在时钟沿把次态搬进现态

always@(posedge clk or negedge rst) begin if(!rst) currentstate <= S0; // 复位值必须是一个确定状态,不能省 else currentstate <= nextstate; // 非阻塞:本拍算出,下一沿生效 end

这一段是整个状态机里唯一带时钟的时序逻辑,职责只有“搬运”。currentstate和nextstate都声明为reg [3:0],前者综合成 4 个触发器,后者本可以不声明成 reg,写成 reg 只是为了在 always 块里能赋值。

非阻塞赋值<=在这里不是风格问题,而是语义问题:如果写成阻塞赋值,同一个 always 块里的其他语句会立刻看到新值,多个寄存器之间的交换关系就会错乱;更麻烦的是,一旦后续有人在同一个沿上再加一级逻辑,阻塞赋值会让仿真和综合的调度顺序对不上。三段式的第一段用非阻塞,是这个结构能成立的前提。

异步复位用的是negedge rst加if(!rst),意思是复位不依赖时钟,只要 rst 拉低立刻回 S0。它的代价是复位信号的毛刺会直接穿透到状态寄存器,实际工程里常见的补法是“异步复位、同步释放”,用一个两级触发器把复位撤销沿对齐到时钟。这份示例代码里没做这一步,属于教学简化,真上板时值得补。

3.2 第二段是纯组合函数:输入是现态和激励,输出是次态

第二段就是 2.2 里贴过的那段 case。它没有任何时钟沿,本质是一张查找表:给定currentstate和a,算出nextstate。综合工具看到这种结构,通常会生成一组比较器加多路选择器,或者直接把 case 项映射成 LUT。位宽越大、状态越多,这段的级联就越深,它是整个状态机组合路径的主要来源。

敏感列表写成always@(currentstate or a or rst)是老写法,等价于always@(*)。手写列表的问题是漏信号不会报错,仿真时表现为“某些输入变化后输出不跟着变”,跟综合结果对不上,排查起来很费劲。所以第二段我一般直接写always @(*),让工具自己去推。

nextstate在复位分支里被赋成 S0,这个做法在仿真里是必要的:复位期间currentstate已经被强制成 S0,如果nextstate停在 X,波形上会看到一片红。但它也是多余的,因为第一段在复位期间根本不会采样nextstate。保留它是为了让波形可读,不是什么功能要求。

3.3 第三段是状态译码:Moore 看现态,Mealy 看现态加输入

Moore 的输出段短得几乎没什么可讲:

always@(rst or currentstate) begin if(!rst) z = 0; // 复位期间输出强制为 0 else case(currentstate) S0: z = 0; S1: z = 0; S2: z = 0; S3: z = 0; S4: z = 0; S5: z = 1; // 只有命中态才输出 1,与输入无关 default: z = 0; endcase end

注意这里的z是reg类型,但它综合出来是一根组合线,不是触发器。Moore 的这一段等价于一根“状态等于 S5”的比较器,路径极短,输出稳定,代价是必须在 S5 上多待一个完整时钟周期。

Mealy 的第三段则必须把a拉进来,因为“检测成功”这个事件在 Mealy 里是由 S4 加 a=0 这两个条件共同定义的,缺一个都不成立。更细的实现差异在 4.1 展开。

3.4 独热码还是二进制:编码方式直接改写综合结果

原始代码的注释写着“独热码”,但参数定义是 4'b0000 到 4'b0101,这是标准的二进制顺序编码,六个状态用了 4 位、浪费两个编码。真正的独热码应该是 6'b000001、6'b000010 一直到 6'b100000,一位对应一个状态。

编码方式6 状态所需位宽触发器数次态译码典型适用场景
二进制33需要多级比较,路径较深状态多、面积敏感
格雷码33比较逻辑略小,相邻态只翻一位状态按顺序迁移
独热码66每位直接判断,译码几乎为零触发器便宜、速度优先

独热码的取舍很直白:用更多的触发器换更浅的组合逻辑。FPGA 里触发器资源相对富余,状态机又常常落在关键路径上,所以独热很常见;反过来,状态数上百的时候再用独热,触发器数量就撑不住了。注释和代码对不上这件事看着小,实际影响不小——有人按注释去理解时序报告,会得出完全错误的结论。血泪经验是:永远以参数定义为准,注释只当参考。

4. Mealy 版本多出来的 temp_z:打一拍到底图什么

4.1 temp_z 和 nextstate==S2 这个冗余条件

Mealy 版本的输出部分被拆成了两段,比 Moore 多出一个中间变量:

always@(rst or currentstate or a) 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; // 只有站在 S4 且输入 0 才算命中 default: temp_z = 0; endcase always@(posedge clk or negedge rst) if(!rst) z <= 0; else if((temp_z == 1)&&(nextstate == S2)) z <= 1; // 命中结果打一拍再输出 else z <= 0;

第一段是纯组合,temp_z就是“当前是否处于命中瞬间”的即时判断,它的路径是currentstate的比较器加一个与门。第二段把这个判断在时钟沿上锁进z,于是z比temp_z晚一拍。

那个nextstate == S2的条件是冗余的:当temp_z == 1时,currentstate必然是 S4、a必然是 0,按 2.2 的表,此时nextstate只能等于 S2。写上去不改变功能,但会让读代码的人误以为存在“temp_z 为 1 但次态不是 S2”的情况,属于给自己挖理解成本。冗余条件在综合时会被优化掉,代价只在可读性上。

4.2 打一拍把 Mealy 的“提前一拍”吃掉了

Mealy 相对 Moore 的核心优势是响应快:输出不需要等状态迁移完成,在输入到位的那一拍就能出来。但这个优势成立的前提是输出不打拍。原始代码打了一拍,于是这个优势消失了,而且消失得很隐蔽。

用 testbench 的时间轴算一遍最清楚。时钟周期是 100ns,always #50 clk = ~clk,上升沿落在 50、150、250……输入a的变化点按#100递增,从 28ns 开始,因此每个输入值都在上升沿前约 22ns 就稳定了,采样是安全的。命中的那一次输入序列是 1、1、0、0、1、0,第 6 位 0 在第 528ns 被驱动。

  • 组合版 Mealy:temp_z在 528ns 之后几纳秒内就拉高,输出领先时钟沿约 22ns。
  • 打拍版 Mealy:z要等 550ns 的上升沿才变高,和 Moore 版本在同一时刻出结果。
  • Moore 版:550ns 上升沿把currentstate推进到 S5,组合译码后z几乎同时变高。

结论是,打拍后的 Mealy 在时序上退化成了 Moore,只省下了一个状态,响应速度的优势一点没剩下。这不是说打拍错了,而是说打拍和“用 Mealy”这两个决定必须一起做:如果下游需要一个干净、无毛刺、与时钟对齐的脉冲,那打拍是对的,此时选 Mealy 的理由只剩“状态少一个”;如果确实需要提前响应,就不能打拍,得接受组合输出的毛刺风险。

4.3 毛刺从哪来,什么时候必须打拍

组合输出的毛刺来源很具体:currentstate是多位信号,从 S4 跳到别的状态时,各位不可能同时翻转,中间会短暂出现一个既不是 S4 也不是目标态的编码。如果输出译码恰好对这个中间编码敏感,就会吐出一根窄脉冲。Moore 版本同样有这个问题,只是它的译码只依赖currentstate,毛刺只发生在状态切换的瞬间;Mealy 还额外依赖a,输入端的任何抖动都会直接传到输出。

判断要不要打拍,我一般看三件事:下游是把它当时钟、复位、写使能这类“边沿敏感”的信号用,还是当数据用;组合路径能不能在一個时钟周期内收敛,加一级寄存器会不会破坏时序预算;以及仿真里有没有出现过窄脉冲被下游采到的情况。前两条只要有一条踩中,就老老实实打拍,用一拍延迟换确定性,这笔账通常划算。

5. 常见问题排查:复位极性、激励相位、注释说谎

5.1 现象:上电后前几个周期状态是 X,波形一片红

原因在 testbench 的复位时序。rst初值给的是 1,也就是高电平,而模块里写的是if(!rst),低电平才复位。所以从 0ns 到 5ns 这段时间复位根本没生效,currentstate保持未初始化值 X。虽然 5ns 之后rst被拉低又拉高、状态被正确复位,最终结果没受影响,但这属于“侥幸能跑”。

解决办法是把初值改成rst = 0,复位有效电平从头就开始,或者直接在initial里给currentstate一个初值。更重要的是反过来检查设计:复位极性是低有效还是高有效,必须和 testbench、板级复位电路三方对齐,任何一方反了,上板就是随机状态。

5.2 现象:改了个延时,仿真结果突然全错

testbench 里用#100推进激励,时钟周期正好也是 100ns,输入变化点落在 28、128、228……这些位置都在时钟低电平的中间,距离上升沿有 22ns 余量,所以采样稳定。但如果有人为了“跑快点”把#100改成#50,输入变化点就会正好压在上升沿上,采样到旧值还是新值取决于仿真器的调度顺序,结果就是玄学。

解决办法是让激励变化永远避开时钟有效沿,常规做法是把输入变化对齐到时钟下降沿之后一小段,或者在@(negedge clk)之后再赋值。改延时之前先算一遍相位,比改完再看波形省时间。

5.3 现象:按注释理解代码,时序报告对不上

代码注释写“独热码”,参数却是 4'b0000 到 4'b0101 的二进制;摘要说状态机有五个状态,实际 Moore 版本有 S0 到 S5 六个。这类不一致不会让仿真失败,但会让人按错误的位宽估算资源、按错误的状态数画状态图。

解决办法只有一个:以parameter定义和case分支为准,把注释当成过期文档。看到“独热”“格雷”这类编码声明时,直接数参数里 1 的个数,一位为 1 才是独热。

5.4 现象:仿真对,综合后行为变了

八成是敏感列表漏信号。手写always@(currentstate or a or rst)这种列表,加一个新信号忘了补进去,仿真器不会警告,综合工具却按“块内读到的所有信号”去推,两边语义就不一样了。输出段如果漏了a,组合逻辑在仿真里不响应输入变化,综合后却会响应。

解决办法是把所有组合 always 块统一改成always @(*),让工具推导敏感列表。这一条几乎没有副作用,唯一要注意的是块内不能出现对自身的读后写,否则会推出意外锁存。

5.5 现象:输出偶尔多一个周期的高电平

多半是阻塞和非阻塞混用。三段式里,第一段必须用非阻塞,第二段、第三段用阻塞,这条界线一旦破掉,仿真调度就会出现“本拍看到下一拍的值”或者“下一拍还看到本拍的值”。比如把第一段写成currentstate = nextstate,后面的组合块在同一时刻读到的就是已经更新过的现态,波形会整体错开一拍。

解决办法是按段固定赋值方式:带posedge clk的一律<=,纯组合的一律=,不要在一个块里混着写,也不要在时序块里顺手算组合结果。

6. 设计取舍:Moore 的稳和 Mealy 的快,选哪个

把两个版本并排放在一起看,会发现它们的分歧点其实只有一个:“检测成功”这件事,是由状态承载,还是由状态加输入共同承载。Moore 选择用状态承载,代价是多一个状态、多一拍延迟,换来的是输出与输入解耦——z只跟currentstate有关,输入端的任何抖动都传不到输出上,波形干净,时序分析也简单。Mealy 选择用状态加输入承载,状态少一个、响应早一拍,代价是输出路径上串进了输入,组合逻辑变长,毛刺风险上升。

我的判断标准大概是这样:如果输出要被下游当作事件去触发某个动作,比如写使能、握手请求、中断标志,那它必须干净且与时钟对齐,这种情况我会选 Moore,或者选 Mealy 但把输出打一拍;如果输出只是数据通路上的一个选择信号,晚一拍无所谓,或者状态数多到每省一个状态都能明显减资源,那 Mealy 更划算。序列检测这类“最后一位决定结果”的场合,Mealy 天然省一个状态,优势是实打实的;而控制类状态机里状态迁移本身就复杂,再让输出跟着输入乱跳,调试成本会翻倍。

还有一个容易被忽略的维度是验证成本。Moore 的第三段是纯译码,可以单独拿出来做等价性检查;Mealy 的输出混了输入,testbench 必须把所有输入组合都覆盖到才能确认没有误触发。状态少一个省下的资源,很可能在验证阶段又还回去。

我早年做过一个序列检测的小模块,图省状态选了 Mealy,输出直接接到下游的使能端,仿真一路绿,上板后偶发多触发一次。查了两天才发现是输入信号经过一级组合逻辑带进来的毛刺,正好在状态译码的窗口里被采到。从那以后我每次选 Mealy 都强制走一遍“输出是否打拍”的检查,只要下游是边沿敏感的信号,先打一拍再说,宁可慢一拍也不留这个隐患。

这份资料把 Moore 和 Mealy 两套完整源码连同 testbench 放在一起,价值不在于代码本身有多复杂,而在于它把两种输出时机的差异摆在同一个 10010 检测场景里,可以直接对着波形逐拍比对——包括 Mealy 版本在 S4 收到 1 时回 S0 这处和 Moore 不一致的地方,也只有在两版对照时才会显形。把状态图按“最长后缀匹配前缀”自己推一遍,再拿代码去核对每一行转移,比读十遍模板都管用。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/29 12:44:07

高级词汇 impeccable 用法全解析:词根、搭配与场景实战

在跟英文打交道的这些年里&#xff0c;我逐渐发现一个有趣的现象&#xff1a;真正能把一个高级词用得恰到好处的人&#xff0c;往往不是词汇量最大的那个人&#xff0c;而是对词的“脾气”特别敏感的人。今天想跟大家好好聊的这个词&#xff0c;就是 impeccable 。它常常被翻…

作者头像 李华
网站建设 2026/10/29 12:43:55

impeccable:为AI coding agent前端产出加上设计质量校验闭环

1. 从"impeccable"这个词说起&#xff1a;它到底想解决什么问题第一次看到"impeccable"这个项目名&#xff0c;我脑子里蹦出来的不是词典释义&#xff0c;而是一个很具体的场景&#xff1a;你让 AI coding agent 帮你改一个前端页面&#xff0c;它噼里啪啦…

作者头像 李华
网站建设 2026/10/29 12:43:33

ponytail:一种提升跨职能协作效率的轻量级状态标识符

1. 项目概述&#xff1a;从“ponytail”这个词出发&#xff0c;我们到底在聊什么&#xff1f;“ponytail”这个词最近频繁出现在社交平台的评论区、短视频字幕和年轻用户的日常对话里&#xff0c;但它早已不是字典里那个简单的“马尾辫”释义。如果你刷到一条穿搭视频底下有人留…

作者头像 李华
网站建设 2026/10/29 12:43:02

沃利斯公式推导详解:从积分递推到π的无穷乘积

沃利斯公式是高等数学里一个很特别的存在。它不像牛顿-莱布尼茨公式那样天天用&#xff0c;也不像洛必达法则那样考试必考&#xff0c;但每次在积分表里翻到它&#xff0c;或者在题目里撞见那个带着双阶乘的式子&#xff0c;总会有种“这玩意儿到底怎么来的”的困惑。我当年第一…

作者头像 李华
网站建设 2026/10/29 12:42:37

PostGIS ST_Aspect函数在DEM坡向分析中的应用与实践

1. 项目概述PostGIS作为PostgreSQL的空间数据库扩展&#xff0c;在GIS领域有着广泛的应用。其中ST_Aspect函数是专门用于数字高程模型&#xff08;DEM&#xff09;坡向分析的核心工具。坡向作为地形分析的基本参数之一&#xff0c;反映了地表坡面的朝向&#xff0c;直接影响着太…

作者头像 李华
网站建设 2026/10/29 12:41:12

2026新年PPT封面设计:从定调到模板化的完整实操

1. 新年PPT为什么必须先解决封面每年十二月上旬开始&#xff0c;我的聊天窗口就会被同一种问题刷屏&#xff1a;新年PPT到底怎么做出“年味”又不显得土。问的人里有做年度复盘的运营&#xff0c;有要给客户做答谢方案的销售&#xff0c;也有临时被抓去做部门新年启动会的行政。…

作者头像 李华