资讯动态

Verilog状态机实现非重叠序列检测1011:设计与仿真验证

发布时间:2026/10/15 15:37:45 来源:尧图企业网站定制
状态机训练绕不开序列检测这算是Verilog训练笔记里最经典的开题。我这次要总结的是非重叠序列检测简单说就是在串行输入流里识别一个指定的bit序列识别完一遍之后立刻把状态清空重新开始绝不允许跟前一段检测共用任何一个bit。听起来很好懂但真正写代码时不少人在“重叠”两个字上翻车——同样是检测1011输入1011011有人输出一次有人输出两次谁对谁错完全取决于题目里有没有“非重叠”。这篇文章就围绕检测序列1011展开把状态机方案、完整可综合代码、仿真验证、移位寄存器写法都过一遍也记录几个我实际调试时踩过的坑。适合刚学数字逻辑、准备笔试面试或者正在用状态机做协议解析的读者。1. 非重叠检测和重叠检测同样一段1011011输出为什么差一倍1.1 先看一个反直觉的测试序列“检测连续的1011序列”这个需求如果不加上“非重叠”三个字其实是个不完整需求。我给学生上课时经常用同一个输入串演示输入串1 0 1 1 0 1 1目标是检测1011。这个串里藏了两个可以形成1011的地方第1位到第4位1011第4位到第7位1011因为第4位是1第5位是0第6位是1第7位是1所以如果实现的是重叠检测就会输出两次如果实现的是非重叠检测只会输出一次。第一次匹配完成后最后一个bit如果被当作下一次匹配的开头那就是在“重用”旧序列里的bit这就违背了非重叠的定义。1.2 重叠与非重叠的完整时序对照把两种行为放在一张表里差异非常直观。下面这个表里“匹配末尾”那一列写的是状态机在输出后应该去哪里。周期输入重叠检测的累计状态重叠输出非重叠检测的累计状态非重叠输出11已匹配“1”0S1已匹配“1”020已匹配“10”0S2已匹配“10”031已匹配“101”0S3已匹配“101”041完整命中最后一个1保留为新起点1完整命中状态回到S0150从“1”开始继续匹配“10”0S0忽略这个0061匹配到“101”0S1只匹配到“1”071完整命中1仍是S1需要等00这个表基本就是两种检测器的规格说明书。你会发现关键差异在第4拍之后重叠检测会把当前bit的“1”作为新序列的第一个bit留下来所以第5拍输入0时它已经处于“匹配到10”的状态非重叠检测则把已经消费掉的四个bit整体丢弃第5拍的0被当成无效数据忽略掉。1.3 用“单词匹配”理解非重叠的含义可以把串行数据流想象成一连串字母目标序列1011想象成一个单词。非重叠检测的意思是在一段文本里搜索这个单词匹配成功一个之后就从这个单词的最后一个字母后面重新开始找下一个单词绝不允许这个单词的最后一个字母同时充当下一个单词的第一个字母。现实中英文文本没有这种连续bit流的尴尬但数字信号是严格连续的。所以在设计前必须跟需求方确认清楚检测到一遍之后是“彻底清空重新累积”还是“保留最后几个bit继续累积”。这个决策会直接改变状态转移表也会改变最终电路。2. 基于Mealy状态机的1011非重叠检测从状态表到可综合代码2.1 状态定义与状态转移表推导检测固定序列1011用Mealy型状态机最省状态。划分四个状态就够了S0初始态还没有匹配到任何有效bit等待第一个“1”S1已经匹配到序列开头“1”S2已经匹配到“10”S3已经匹配到“101”状态转移表如下现态输入0的次态输入1的次态输出说明S0S0S1无输出S1S2S1无输出S2S0S3无输出S3S0S0仅当S3且输入1时输出1表里最需要解释的是最后一行。状态S3表示已经攒齐了“101”如果这时输入为1那当前这一bit让整个序列变成“1011”检测成功输出1。但非重叠要求清空所以次态是S0而不是S1。可能有读者会问当前这个1为什么不能作为下一个序列的开头因为严格意义上的非重叠不允许同一个bit被前后两个序列共享。它已经被消费掉了下一轮要从下一个新bit开始。S2状态下如果输入0当前累计变成“100”不可能再以“0”开头形成目标序列所以次态也回S0。S1状态下如果输入1累计还是“1”继续停在S1等0。2.2 完整可综合代码按照上面的状态表用三段式状态机写出来的代码比较清晰。三段分别负责状态寄存器更新、次态组合逻辑计算、输出逻辑处理。module seq_detect_1011_nonoverlap( input wire clk, input wire rst_n, input wire data_in, output reg detected ); localparam [1:0] S0 2d0, S1 2d1, S2 2d2, S3 2d3; reg [1:0] state, next_state; // 第一段状态寄存器 always (posedge clk or negedge rst_n) begin if (!rst_n) state S0; else state next_state; end // 第二段次态组合逻辑 always (*) begin next_state S0; // 防止意外综合出 latch case (state) S0: next_state data_in ? S1 : S0; S1: next_state data_in ? S1 : S2; S2: next_state data_in ? S3 : S0; S3: next_state data_in ? S0 : S0; default: next_state S0; endcase end // 第三段输出逻辑寄存器输出 always (posedge clk or negedge rst_n) begin if (!rst_n) detected 1b0; else if (state S3 data_in) detected 1b1; else detected 1b0; end endmodule这个模块只有两个状态寄存器bit加一个时钟周期的输出脉冲综合面积非常小。检测到1011后detected会在下一个时钟上升沿置高并维持一个周期方便下游逻辑采样。2.3 代码里的关键取舍为什么输出要打一拍Mealy状态机的天然输出是组合逻辑也就是assign detected (state S3) data_in;。这样写更快data_in一到就能立刻给出结果。但组合输出有一个工程问题输出直接跟随data_in变化如果data_in上有毛刺detected上也会出现毛刺而且在时序分析里组合输出直接接到下游时钟域容易出现竞争风险。我在这份代码里选择把输出寄存器化让detected稳定一个周期。虽然严格来说输出晚了一个时钟沿但与非重叠序列检测的“检测到之后清空”语义并不冲突。训练项目里优先保证波形干净、逻辑容易验证真正对时序有极致要求的场景再单独考虑组合输出。2.4 状态编码与综合细节四个状态只需要两个bit用二进制编码就够了。如果是几十个状态的大状态机通常用独热码减少组合逻辑深度代价是FF数量更多。这个题目状态太少不需要额外优化。还有一点next_state组合块开头写next_state S0;然后在case里对每个分支都显式赋值。这样万一case有遗漏分支也不会综合出latch。综合工具的警告一般比较烦人养成这个习惯能省去很多检查时间。3. testbench与波形判读用1011011验证非重叠行为3.1 为什么非要用1011011当测试向量很多教材的testbench只给1011_1111这种简单串能验证模块不会输出错误但验证不出“重叠”和“非重叠”的区别。1011011这个串是故意设计的它让第一次成功检测的最后一个bit正好可以与后面的011再拼成一个1011。如果模块实现成了重叠检测这里就会多输出一次。所以我强烈建议凡是做序列检测练习手里至少要备三组输入测试串预期非重叠输出次数主要验证点10111基本功能正常10110111是否误实现了重叠逻辑10110110113连续多次非重叠检测是否正常第三组1011011011可以拆成三段1011_011_011但只有第一段能命中后面全是杂散数据。不过如果你把输入改成连续的101110111011非重叠检测应该每4拍输出一次。3.2 可仿真的testbench写法写testbench时有个经验不要在时钟上升沿的同一边沿改变data_in容易造成仿真竞争。一般做法是在时钟下降沿改变输入让数据在上升沿到来前稳定。timescale 1ns/1ps module tb_seq_detect_1011_nonoverlap; reg clk; reg rst_n; reg data_in; wire detected; seq_detect_1011_nonoverlap u_dut( .clk (clk), .rst_n (rst_n), .data_in (data_in), .detected(detected) ); initial clk 1b0; always #5 clk ~clk; reg [6:0] seq 7b1011011; integer i; initial begin rst_n 1b0; data_in 1b0; #15 rst_n 1b1; for (i 6; i 0; i i - 1) begin (negedge clk); data_in seq[i]; end #10 data_in 1b0; #50 $finish; end initial begin $dumpfile(tb.vcd); $dumpvars(0, tb_seq_detect_1011_nonoverlap); end endmodule这里把序列1011011按位送到了data_in。第4拍开始进入最后一组011之前state已经被清空所以后面无论如何都不会再命中。3.3 仿真波形怎么判读观察波形时重点看两个地方。第一第一次检测输出位置。1011011送入后第4个时钟上升沿应该看到detected拉高并在第5个时钟沿拉低。因为输出是寄存器输出所以严格说detected置1的沿和检测到序列最后一位的沿是同一个但数据在第4位之后state已经被更新成S0下一个时钟沿到来时输出信号被清掉。第二后面第7拍的位置。重叠检测器会在第7个时钟沿再拉高一次非重叠检测器不会。如果看到这里又出现一个高电平脉冲几乎可以断定是状态转移表的最后一行写错了把S3输入1的次态写成了S1。3.4 自动对比仿真结果的小技巧训练项目规模小肉眼盯波形没毛病。但序列多了之后人很容易看花眼建议在testbench里加一个计数器和一行打印reg [3:0] cnt; always (posedge clk or negedge rst_n) begin if (!rst_n) cnt 4d0; else if (detected) cnt cnt 1b1; end仿真结束后用$display打印cnt。如果cnt不是你预期的次数说明实现和需求对不上。这种方法对排查重叠非重叠问题特别有效比自己数波形可靠得多。4. 另一种实现移位寄存器法做非重叠检测的代码与取舍4.1 用“最近4个bit等于1011”来判断状态机不是唯一解法。序列检测的本质是判断“当前以及最近三拍输入组成的4位串”是否等于目标序列。所以也可以用移位寄存器保存最近4拍数据再用比较器判断。module seq_detect_shift_nonoverlap( input wire clk, input wire rst_n, input wire data_in, output reg detected ); reg [3:0] shift; wire [3:0] shift_next {shift[2:0], data_in}; // 把当前输入移入后最新4bit是否等于1011 wire hit (shift_next 4b1011); always (posedge clk or negedge rst_n) begin if (!rst_n) begin shift 4b0; detected 1b0; end else if (hit) begin // 非重叠命中后直接清空当前bit不参与下一轮 shift 4b0; detected 1b1; end else begin shift shift_next; detected 1b0; end end endmodule注意这里用shift_next判断而不是用shift判断。如果用shift判断等于比较的是“上一拍移入之后的4个bit”会慢一个周期逻辑容易绕晕。用shift_next表示“当前data_in进入之后是否能凑成1011”语义更准确。那个hit为真的分支里shift直接清零没有执行移位。这一拍输入其实已经被比较器消费掉了如果不清零下一次就可能检出重叠。4.2 状态机方案和移位寄存器方案怎么选我用一张表把两个方案的区别列出来对比维度状态机方案移位寄存器方案寄存器资源2个bit状态寄存器4个bit移位寄存器判断逻辑状态转移表4位比较器非重叠处理S3来1回S0命中后清空shift适合初学练习训练状态机思维训练时序思维扩展任意序列状态数线性增加表会变大只需增加移位寄存器位数和比较器可配置多条序列难度较高容易并联多个比较器工程上如果只是检测一个固定短序列状态机面积更小写法也更正统。如果是做协议帧头匹配要在一拍内同时判断多个可能模式移位寄存器加比较器会更灵活。比如一个串行数据头可能匹配1011或1101两种状态机要维护两套状态移位寄存器方案则直接并两个比较器。4.3 位移位方案的坑命中后清零的时序用移位寄存器方案时最容易被坑的地方是“命中后清零”会不会丢掉下一拍的有效数据。比如输入1011011第一次命中在“第4拍”那一拍的data_in是1。代码里hit为真shift直接变成0。下一拍data_in变成0此时shift_next是{3b000, 0}相当于从0开始重新积累这是非重叠的正确行为。如果改成重叠检测只需要把else if (hit)分支里的shift 4b0;去掉让数据正常移入hit信号正常拉高。可见两种检测的代码差异就在这一行设计时务必写清楚注释。5. 训练中容易翻车的三个细节与我的复盘习惯5.1 最容易把非重叠做成重叠的地方先说最容易翻车的位置状态转移表的最后一行。检测1011时S3收到1后次态写S1输出也正常看起来检测成功但实际上这个写法是在允许重叠。这也是为什么1011011这个测试串这么重要它能在第一次检测后立刻暴露问题。如果拿到一个任意序列检测模块先喂这个串看输出次数就能快速判断实现语义。更一般的做法是设计完成后从每个可能形成重叠的位置构造测试序列。比如检测1011重点就是验证“1011011”这种后缀重叠检测111则用“1111”测试重叠会输出两次非重叠只输出一次。5.2 组合输出与时序输出真的按需选我见过不少训练代码喜欢写assign detected (state S3) data_in;觉得这是“正宗Mealy”。没错Mealy是组合输出但工程落地要另算。组合输出在数据变化快的接口里很容易被毛刺干扰也让下游逻辑的采样窗口变得不可控。我的建议是训练阶段就把两种写法都试一遍。先写组合输出用1011011测一遍观察窄脉冲再改成寄存器输出波形会干净很多。以后真正做项目如果检测信号要送给其他模块做握手优先寄存器输出如果是纯组合运算路径且有明确的时序预算再考虑组合输出。5.3 复位与default分支一个都不能少异步复位、复位后进S0这个基本操作也要养成习惯。如果上电后状态机进入S1或S2可能会把之前的数据误当成有效序列的一部分导致首次检测位置错位。next_state组合逻辑里的default分支同样重要因为状态寄存器有2bit理论上会存在未定义编码虽然case覆盖了四个状态但default能防止意外行为。具体到FPGA工程复位信号最好统一做异步复位、同步释放。这里不展开细讲但序列检测这种小状态机复位处理好可以省去很多上电后的排查时间。5.4 我自己的训练复盘方法写这种基础训练项目不要只追求能跑通。我的习惯是每写完一个功能就列三件事仿真里用了哪些特殊向量、哪些case是最容易出错的、如果换一种实现方式会怎样。对非重叠序列检测来说我会固定记录自测向量必须包含1011011输出信号默认打一拍状态转移表最后一行明确注释“检测成功回S0非重叠”同时写一版移位寄存器方案做对比这一套做完再把重叠检测也写一遍两个项目对照着看状态机设计的感觉就出来了。这篇笔记主要解决的是“非重叠”三个字背后的状态转移问题。下次遇到检测1101、10101之类的题目先画状态图再写转移表最后用带重叠陷阱的测试串验证基本不会出错。我已经把这一篇的代码和testbench整理好打算继续往下记录重叠检测、以及更多位宽输入下的序列检测实现等积累几个版本再一起分享仿真波形上的对比经验。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑