
N3 时采样序列1,1,0,1里虽然出现了三个 1却没有连续三个 1。如果去抖计数器在中间那个 0 到来时只是保持最后一个 1 就可能提前触发业务事件。只测“拉高以后一直保持”的正常波形很容易漏掉这个错误。按键硬件与基础实现可先看已有的《实战按键消抖——从硬件原理到 Verilog 实现》。本篇围绕连续样本计数的中断、N−1/N 阈值、N1 和位宽边界设计自检给出一个同步输入滤波器、六种参数配置、固定种子的激励和五个单缺陷变体检查测试是否真能发现提前提交、累计中断样本和事件粘连。1 先把复现方式和验证范围放在前面依赖是 Vivado Simulator 的xvlog、xelab、xsim。正文提供完整 RTL 和 SystemVerilog 自检测试复制为debounce_stable.v、tb_debounce_stable.sv在已经配置好 Vivado 环境的终端执行xvlog --sv debounce_stable.v tb_debounce_stable.sv xelab tb_debounce_stable -s debounce_snapshot -debug typical xsim debounce_snapshot -runall三个阶段分别负责编译、展开生成快照、运行仿真。即使 RTL 文件后缀是.v这里也明确使用--sv因为参数保护使用了$fatal测试平台还使用了 SystemVerilog 语法。独立命令行流程可对照 AMD UG900 2022.1 批处理仿真。批量测试另有run_fpga_tests.py需要 Python 3.8 及以上仅使用标准库。它为每个正例、变异反例和参数反例创建独立目录保存三阶段日志、源文件快照、SHA-256 和 JSON 结果防止误用上一轮快照。工具在 PATH 中时运行python run_fpga_tests.py --out results否则增加--vivado-bin并填写本机安装目录。输出目录必须尚不存在。2026 年 10 月 16 日使用 Windows 11构建 26200、Python 3.14.5、Vivado Simulator 2022.1 64 位XSim SW Build 3526262 完成实测。实际工具目录是D:/tools/Xilinx/Vivado/2022.1/bin。本篇主回归的 1 个正例、5 个变异反例、2 个参数反例全部符合各自预期六种正常配置共检查 3839 个驱动/采样周期故意错误全部由指定检查发现。CASE_PASS debounce N1 cycles275 commits55 rise_events31 CASE_PASS debounce N2 cycles364 commits47 rise_events27 CASE_PASS debounce N3 cycles385 commits42 rise_events24 CASE_PASS debounce N5 cycles523 commits37 rise_events21 CASE_PASS debounce N16 cycles1053 commits18 rise_events11 CASE_PASS debounce N17 cycles1239 commits23 rise_events14 TEST_PASS debounce_stable six parameter configurationscycles统计每个配置受检的周期数每周期还检查边沿前保持和上升沿后输出3839 不是 SVA 覆盖率或独立标量断言总数。commits不计复位造成的电平清零rise_events统计整段测试中的上升事件。首轮仿真发现 Vivado 2022.1 对 TB 端口声明中的 done 初始化发出兼容性警告。将初始化移入initial块后完整双模块套件重新运行17/17 项符合预期两个 RTL 没有更改。本篇给出的是重跑后的 TB。另用整段样本窗口参考模型补测 N1、2、17、32、33通过记录及完整补充代码见后文。本文验证的是 RTL 行为没有进行综合、布局布线、时序收敛或上板。上面的命令可独立复现正文主正例批量运行时快照统一命名为sim_snapshot不改变设计内容。2 接口只接收同步输入先明确五条规则接口包括时钟clk、高有效同步复位rst、同步输入din以及寄存输出level、rise。复位在上升沿生效level0、rise0候选计数清零。din与level相同说明候选变化被打断计数立即清零。din与level不同连续统计这样的采样第 N 次时提交leveldin。提交上升变化时rise1下一拍恢复为 0提交下降变化不产生rise。N 是运行期间不变的正整数启动时必须使同步复位覆盖至少一个上升沿。本文只定义din、rst为确定的 0/1 时的功能。未初始化、X/Z 注入和板级电气异常不在这组测试的输入契约中。N1时第一次相反采样立即提交没有额外的稳定确认区间仍保留寄存输出与同步复位语义。外部按键不能直接连到这里的din。应先根据器件和系统要求完成输入同步再将同步后的单比特电平送入本模块。AMD 的单比特同步宏文档也单独规定了目标时钟的采样条件把后级去抖计数增大并不能替代同步器设计。UG953 2022.1XPM_CDC_SINGLE3 连续三拍与累计三拍会得到不同结果取 N3复位后的level0。给入序列1,1,0,1,1,1,1每个数代表一个上升沿采样值。下面均为该沿非阻塞赋值完成后的状态采样沿dincountlevelrise解释E11100第一个候选采样E21200第二个候选采样E30000返回原电平前两次作废E41100重新累计E51200尚未提交E61011第三次连续采样提交上升E71010保持高电平不重复发事件若 E3 只是保持 count那么 E4 就会把两段高电平拼成“三次”失去连续稳定的含义。这类错误在“输入直接从 0 变为 1 并一直保持”的测试里不会暴露所以短于阈值的候选段和中断后的重新计数必须单独测。下降方向同样确认 N 次。假设level1采样0,0,1,0,0,0前两个 0 因中间的 1 作废最后三个 0 才提交下降整个过程都不产生上升事件。4 N 是采样数不能直接等同于连续物理时间计数器统计采样结果。若输入在第一有效采样前刚刚变化第 N 次采样与第一次采样之间只相隔 N−1 个完整周期。因此“N 个连续样本”与“从第一次观察到变化后再等待 N 个完整周期”是两个接口。例如 50 MHz 时钟周期为 20 nsN500000 对应常见的约 10 ms 确认尺度计数器需要 19 位。若需求严格规定“首次观察到变化后至少再等待 10 ms”应按该时间定义重新确定阈值不能照搬 N500000 的说法。时钟频率误差和前级同步延迟也应进入完整的响应时间预算。更重要的是采样点之间发生又恢复的变化可能根本没有被本模块看见。这份 RTL 保证的是连续采样判定不证明引脚在整个区间里没有任何模拟毛刺。涉及安全联锁、复位源或严格脉宽测量时应单独设计接口和验证方法。5 完整 RTL确认时提交普通拍先清事件保存为debounce_stable.v。计数宽度至少为 1避免 N1 时出现零宽向量其他情况下只需表示 0 到 N−1。N 使用有符号integer参数集成时要保证正整数且不超过该类型的表示范围。timescale 1ns/1ps // din and rst must already belong to clks domain. This is not a CDC block. module debounce_stable #( parameter integer N 3 ) ( input wire clk, input wire rst, input wire din, output reg level, output reg rise ); function integer count_width; input integer n; integer v; begin count_width 0; v n - 1; while (v 0) begin count_width count_width 1; v v 1; end if (count_width 1) count_width 1; end endfunction localparam integer CW count_width(N); localparam [CW-1:0] LAST N - 1; reg [CW-1:0] count; // synthesis translate_off initial begin if (N 1) $fatal(1, PARAM_GUARD: debounce_stable requires N 1, N%0d, N); end // synthesis translate_on always (posedge clk) begin if (rst) begin count {CW{1b0}}; level 1b0; rise 1b0; end else begin rise 1b0; // clear event every ordinary cycle if (din level) begin count {CW{1b0}}; // cancel interrupted candidate end else if ((N 1) || (count LAST)) begin count {CW{1b0}}; level din; rise din; // one event only when committing 0 - 1 end else begin count count 1b1; end end end endmodule在第 N 次候选采样到来前寄存器中已经保存了 N−1所以比较的是旧count N-1。满足条件的这一拍提交新电平并清计数不能先从“数到 N”这个口头描述推导出LASTN。如果这个值能被计数位宽容纳会延迟提交若 N 恰好是二次幂N 在当前位宽内还会截断成 0反而提前提交。反例应统一按“终值错误”理解再结合实际失败配置定位。rise每个普通拍先写 0只有确认上升的分支再覆盖为 1。两个赋值都在同一个时序块内明确表达了本拍的优先关系。长按只保持level1不会让rise一直为 1。如果另一个always (posedge clk)用if (rise)更新业务寄存器它在确认沿仍看到旧的rise会在下一上升沿消费事件。把rise当作时钟使用会改变整个时序结构不应这样连接。对低有效按键可以先把同步后的逻辑电平转换为“按下1”再使用这个接口。还有一个常被忽略的产品语义复位后level固定从 0 开始。如果按键在复位释放时已经按住持续 N 次高采样后仍会产生一次rise。如果系统要求“上电已经按住不算一次新按键”应增加独立的启动抑制或先释放再使能逻辑并补相应测试。6 完整自检 TB参考模型使用连续样本数保存为tb_debounce_stable.sv。一共实例化 N1、2、3、5、16、17 六种配置覆盖最小值、普通值、二次幂和跨越二次幂的计数位宽边界。参考模型先累计本拍连续样本数再检查是否达到 N与 RTL 的“比较上一拍 count 与 N−1”写法不同。每拍使用四态严格比较!检查输出避免未知值被当作普通相等。激励在下降沿改变上升沿后#1比较另外检查下降沿改变输入后输出不能提前变化。timescale 1ns/1ps module debounce_case #(parameter integer N3) (output reg done); reg clk0, rst1, din0; wire level, rise; integer run_length0, cycles0, commits0, events0; reg model_level0, model_rise0; reg [31:0] prng32h47a1c39d; reg previous_level, previous_rise; integer i, length; always #5 clk~clk; debounce_stable #(.N(N)) dut(.*); // Independent model: number of successive samples disagreeing with level. task step(input bit reset_value, input bit data_value); begin (negedge clk); previous_levellevel; previous_riserise; rstreset_value; dindata_value; #1; if ({level,rise} ! {previous_level,previous_rise}) $fatal(1,DEBOUNCE_SYNC_ASSERT N%0d cycle%0d,N,cycles); (posedge clk); model_rise0; if (rst) begin model_level0; run_length0; end else if (din model_level) begin run_length0; end else begin run_lengthrun_length1; if (run_length N) begin model_leveldin; model_risedin; run_length0; commitscommits1; end end #1; cyclescycles1; if ({level,rise} ! {model_level,model_rise}) $fatal(1,DEBOUNCE_ASSERT N%0d cycle%0d din%b rst%b actual%b%b expected%b%b,N,cycles,din,rst,level,rise,model_level,model_rise); if (rise) eventsevents1; end endtask task hold_samples(input bit value, input integer n); integer j; begin for (j0;jn;jj1) step(0,value); end endtask initial begin done0; // Establish a known state before the first off-edge check. (posedge clk); #1; step(1,0); hold_samples(0,3); // N-1 samples must not commit; returning to the old level cancels them. hold_samples(1,N-1); hold_samples(0,1); hold_samples(1,N); hold_samples(1,N3); // rise must clear even if level remains high hold_samples(0,N-1); hold_samples(1,1); hold_samples(0,N); // commit a falling transition without a rise event // Reset midway through a candidate, then demand a complete fresh run. hold_samples(1,N-1); step(1,1); hold_samples(1,N); // Reset a committed high output strictly between positive edges. step(1,0); hold_samples(0,2); // Deterministic bursts, including plenty of runs N. for (i0;i96;ii1) begin prng(prng1) ^ (prng[31] ? 32h04c11db7 : 32h00000000); length1(prng[15:8] % (N3)); hold_samples(prng[0],length); if ((i%17)0) step(1,prng[1]); end hold_samples(0,N1); hold_samples(1,N1); if ((events 2) || (commits 4)) $fatal(1,DEBOUNCE_COVERAGE_ASSERT N%0d events%0d commits%0d,N,events,commits); $display(CASE_PASS debounce N%0d cycles%0d commits%0d rise_events%0d,N,cycles,commits,events); done1; end endmodule module tb_debounce_stable; wire [5:0] done; debounce_case #(.N(1)) c1(done[0]); debounce_case #(.N(2)) c2(done[1]); debounce_case #(.N(3)) c3(done[2]); debounce_case #(.N(5)) c5(done[3]); debounce_case #(.N(16)) c16(done[4]); debounce_case #(.N(17)) c17(done[5]); initial begin wait (done); $display(TEST_PASS debounce_stable six parameter configurations); $finish; end initial begin #200000; $fatal(1,DEBOUNCE_TIMEOUT_ASSERT); end endmodule这里的伪随机序列由固定初值和固定递推产生方便复现。它用于扩展长短交替段的组合关键的 N−1、N、长按、下降确认和中途复位仍由明确的定向测试负责。#1只用于测试平台避开 NBA 更新区不会变成综合电路里的延时。7 反例要验证“能抓到错误”为避免正常测试只是在重复实现批量脚本一次只注入一处错误再使用同一份 TB。每次重新编译、展开不能只修改源码后继续运行旧快照。变体单点修改应被检查发现的行为debounce_earlyLAST N - 1改为N - 2提前一拍确认debounce_lateLAST N - 1改为N延迟确认或特定位宽下阈值截断debounce_accumulate中断候选分支清零改为保持 count将不连续的样本累计debounce_sticky普通拍rise0改为保持旧 rise长按期间事件未及时撤销debounce_async_reset敏感表加入posedge rst同步复位被误改为异步复位实际仿真首先捕获的错误如下时间取自 XSim 的Time:行变体首次停止时间实际失败debounce_early56 nsN2第5受检周期提前提交level/rise为11期望00debounce_late56 nsN2终值N在1位中截成0同样提前提交debounce_accumulate76 nsN2第7受检周期把被中断样本累计进来debounce_sticky76 nsN1level/rise为11期望10事件没有清零debounce_async_reset151 nsN1非上升沿改变了输出触发同步性检查debounce_late是脚本中的变体名称不能据此把观察到的错误写成“晚一拍”。本次最先失败的 N2 配置实际表现为提前提交恰好说明参数边界测试的重要性。另外用 N0、N−3 各建一个非法实例两者实际均在 0 ps 命中PARAM_GUARD。参数检查中的$fatal位于综合忽略段它是仿真入口保护不能代替工程参数审查。反例判定要求编译和展开先成功随后命中特定的DEBOUNCE_ASSERT、DEBOUNCE_SYNC_ASSERT或参数保护且没有正常通过标志。编译语法错、工具没启动、测试超时都不算成功检出这个缺陷。本次 XSim 在这些故意失败的仿真后也返回退出码 0因此判定使用实际 Fatal 标志及没有 TEST_PASS只看退出码会漏掉反例。只复现某个反例时复制正文中的两个文件到新目录按上表修改一处再运行第 1 节三条命令。参数反例可以把测试平台替换为下面的独立顶层分别设置 0 和 −3timescale 1ns/1ps module tb_guard; wire level, rise; debounce_stable #(.N(0)) dut( .clk(1b0), .rst(1b0), .din(1b0), .level(level), .rise(rise) ); initial begin #20; $display(GUARD_MISSING); $finish; end endmodule这时展开顶层改为tb_guard。如果看见GUARD_MISSING应视为参数保护没有按预期工作。8 补充复核换一个参考模型再测位宽边界主 TB 使用连续样本计数。补充 TB 改成保存整个 N 样本窗口窗口全 1 才期望高电平窗口全 0 才期望低电平避免参考模型和 DUT 共享同一种计数写法。它遍历长度 0…N1 的候选段、候选中断和不同阶段的复位并扩展到 N32、33。保存为tb_debounce_review.sv与同一份debounce_stable.v一起编译xvlog --sv debounce_stable.v tb_debounce_review.sv xelab tb_debounce_review -s debounce_review_snapshot -debug typical xsim debounce_review_snapshot -runalltimescale 1ns/1ps module debounce_review_case #(parameter integer N1)(output bit done); logic clk0,rst1,din0; wire level,rise; logic [N-1:0] history0; bit expected_level0, expected_rise0, old_level; integer checks0,events0; always #5 clk~clk; debounce_stable #(.N(N)) dut(.*); // Independent model: whole N-sample window, not a run-length counter. task automatic step(input bit reset_i, input bit value_i); (negedge clk); rstreset_i; dinvalue_i; old_levelexpected_level; if(reset_i) begin history0;expected_level0;expected_rise0;end else begin history(history1)|value_i; if(history) expected_level1; else if(~|history) expected_level0; expected_riseexpected_level !old_level; end (posedge clk); #1;checks; if({level,rise}!{expected_level,expected_rise}) $fatal(1,REVIEW_DEBOUNCE_ASSERT N%0d check%0d got%b%b expected%b%b,N,checks,level,rise,expected_level,expected_rise); if(rise)events; endtask initial begin done0;step(1,0); for(int length0;lengthN1;length) begin step(1,0); repeat(length)step(0,1); repeat(N1)step(0,0); end // Interrupted candidates and reset at every phase before acceptance. for(int phase_i0;phase_iN;phase_i) begin repeat(phase_i)step(0,1); step(0,0); repeat(N)step(0,1); repeat(phase_i)step(0,0); step(1,1); repeat(N)step(0,1); repeat(N)step(0,0); end repeat(100)begin step(0,1);step(0,0);end $display(REVIEW_PASS debounce N%0d checks%0d rises%0d,N,checks,events);done1; end endmodule module tb_debounce_review; wire [4:0] done; debounce_review_case #(1) c1(done[0]); debounce_review_case #(2) c2(done[1]); debounce_review_case #(17) c17(done[2]); debounce_review_case #(32) c32(done[3]); debounce_review_case #(33) c33(done[4]); initial begin wait(done);$display(TEST_PASS independent debounce boundary review);$finish;end initial begin #200000;$fatal(1,REVIEW_TIMEOUT_DEBOUNCE);end endmodule实际输出REVIEW_PASS debounce N1 checks218 rises104 REVIEW_PASS debounce N2 checks241 rises6 REVIEW_PASS debounce N17 checks1906 rises36 REVIEW_PASS debounce N32 checks6046 rises66 REVIEW_PASS debounce N33 checks6410 rises68 TEST_PASS independent debounce boundary review这组补充检查与主 TB 分开编译、展开和运行检查次数不并入前面的 3839。9 波形检查可以按这四个位置走在成功编译并展开后执行xsim debounce_snapshot -gui打开仿真界面在 Scope 中选择tb_debounce_stable/c3把clk、rst、din、level、rise、dut/count加入波形。先运行到约 400 ns再按需要继续。找到最初的两拍高输入count 依次为 1、2level 仍是 0。检查随后的低样本count 必须清零再来高样本时从 1 重新开始。找到第三次连续高样本level 与 rise 同拍置高下一拍 level 保持、rise 清零。找到候选中途复位和高电平状态下复位两者都应在上升沿生效下一段候选重新等待完整 N 次。TB 的cycles是受检上升沿数commits是非复位造成的电平提交次数rise_events是观察到上升事件的次数。这些字段统计整个测试流程不能拿它们直接计算某一段按键的触发频率。10 接入业务前把一次按键的定义说清楚这个模块只给出“已确认电平”和“已确认上升事件”。长按连发、双击、最短间隔、上电按住抑制都属于后续业务规则适合在稳定电平之上另写状态机。原理和测试边界明确后最值得问调用方的是如果设备复位时按键已经按住系统应在稳定 N 次后触发一次还是必须等用户先松开再按下才允许触发延伸阅读《实战按键消抖——从硬件原理到 Verilog 实现》。接入前请明确上电时按键已按住是否应产生一次业务事件
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。