ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

按键消抖原理与Verilog实现:计数器法RTL设计及仿真验证

按键消抖原理与Verilog实现:计数器法RTL设计及仿真验证 写FPGA的人十有八九第一个实战模块就是按键消抖。这东西看起来简单不就是等电平稳定再采样嘛可真到自己写RTL的时候各种细节全冒出来了计数器位宽怎么选、边沿检测放哪一级、长按怎么处理、仿真里怎么快速验证。我前前后后在不同项目里写过不下五个版本的消抖模块从最粗暴的延时采样到带状态机的完整方案都踩过坑。这篇就把我实际工程里最常用、也最稳的一个按键消抖模块拆开揉碎讲清楚从原理到代码到仿真一次说明白。不管是刚开始学Verilog的入门者还是想在项目里直接抄一个可靠模块的工程师这篇都能给你省不少事。1. 按键为什么会抖不消抖到底会怎样按键消抖不是一个“实现问题”而是一个“物理问题”。机械按键和触摸按键不一样它的核心是一个金属弹片按下时弹片接触松开时弹片分离。问题就出在这个机械接触上——弹片不是瞬间贴合或瞬间分离的而是在接触点附近来回弹跳好几次每次弹跳只有几微秒到几十微秒整个抖动过程会持续5到20毫秒。1.1 抖动的本质是电平在短时间内反复跳变假设按键一端接GND另一端接FPGA的IO引脚引脚内部用上拉电阻拉高。按键未按下时引脚读到高电平1按下后理想的电平应该立刻变成0。但实际情况是引脚电平在按下后的几毫秒内会在0和1之间来回震荡最后才稳定到0。松开时同理会在1和0之间抖动一会儿才稳定到1。这个抖动波形如果不处理直接给FPGA的逻辑用会带来很实际的问题。比如你做一个按键计数器的功能本来想按一下加1结果因为抖动了十几下计数器一次加了十几按钮完全不听话。再比如按键用于触发状态机跳转抖动会在一个状态上反复跳来跳去导致逻辑紊乱。对于快速响应的系统抖动还会产生亚稳态的风险这个后面会细说。1.2 抖动时间宽度和“越快越容易抖”的规律按键抖动的时间并不是固定的它跟按键的机械结构、材质、使用时间都有关系。新按键弹片弹性好抖动时间短一些用久了的按键触点氧化或弹片疲劳抖动时间变长。我见过最夸张的案例一个老旧的轻触开关抖动时间接近30毫秒用默认20毫秒消抖时间的模块都压不住。还有一个规律容易被忽略抖动不只发生在按下那一下松开的瞬间同样会抖。很多同学写消抖模块只处理了“按下沿”松手时不做处理结果在快速连续按动时出现误触。好的消抖模块必须对上升沿和下降沿都做对称处理。注意消抖时间的下限要看按键的抖动参数一般建议取10~20ms上限要考虑操作手感如果超过50ms用户会觉得按键“反应迟钝”。工程上最常见的选择是10ms和20ms这两个档位按键资料里标称5ms抖动的用10ms标称不明的保守起见用20ms。2. 常见消抖方案横向对比为什么最终选计数器法消抖方案五花八门从硬件到软件到FPGA逻辑各有各的使用场景。实际做FPGA设计的时候不能网上随便抄一段代码就用得先搞清楚每种方案的原理和边界再结合自己项目的资源、时序和手感要求来选。2.1 硬件方案RC滤波和RS触发器硬件消抖的核心思路是把不稳定的机械信号变成稳定的模拟信号。最常见的是RC低通滤波按键信号经过一个电阻和电容组成的积分电路高频抖动被电容吸收波形变成缓慢上升/下降的模拟电平再经过施密特触发器整形输出干净的方波。RC时间常数的经验值是10ms到100ms电阻取10kΩ到100kΩ电容取0.1μF到10μF。硬件方案的优点是逻辑上完全不影响FPGA内部不用做任何处理引脚采到的就是稳定信号。缺点是增加了外围器件PCB布局要占面积RC参数选不好反而会引入新的问题。而且RC滤波只能滤抖动不能滤异步信号跨时钟域的亚稳态问题进了FPGA还是最好再打两拍。用RS触发器构建锁存电路也能消抖但只适用于单刀双掷的按键普通轻触开关是单刀单掷用不上这个方案。2.2 软件方案延时重采样和状态机单片机里最常用的消抖方式就是检测到边沿后延时10~20ms再读一次引脚如果电平还是稳定的目标值就确认按键有效。这种方式简单直接但延时期间CPU被占住做不了别的事情实时性差。高级一点的做法是放在定时器中断里做扫描每隔1ms读一次连续读到某个状态N次才确认这样不阻塞主流程但逻辑复杂度上去了。软件方案对FPGA的参考价值在于“连续N次采样确认”这个思路在硬件里也很好用它等价于计数器连续累加到一个阈值。在实时性要求高的系统里纯延时方案会漏掉其他信号的处理这个坑在软件里很常见在FPGA里其实也存在如果消抖期间把其他信号都忽略了一样会导致数据丢失。2.3 FPGA方案延时采样法、计数器法和状态机法FPGA里做消抖最常用的有三种写法。延时采样法最接近软件思路检测到按键电平变化后启动一个定时器延时20ms延时结束再采一次键值。代码量小容易理解但有两个问题。一是延时期间按键电平再变化不响应连续性操作会丢事件二是它只响应了“首次变化”如果延时结束采到的是抖动结束后的正确电平还好万一恰恰卡在抖动的中间态就会采错。计数器法是目前工程主流用系统时钟一直采样按键输入如果键值和稳定值不一样计数器清零如果一样计数器累加计数器达到设定的消抖时间所对应的计数值就判定按键稳定输出有效电平。这种方法代码量适中对抖动的过滤能力比延时法更强而且连续操作时不会丢事件。状态机法是把消抖过程拆成空闲、按下确认、按下稳定、释放确认、释放稳定这几个状态每次采样结果驱动状态转移。它的逻辑最严谨代码量也最大适合对时序要求特别高的大项目。对于多数按键场景状态机法是“杀鸡用了牛刀”但理解了它对写其它复杂状态机很有帮助。2.4 我在工程里的选择逻辑综合对比后我自己的项目里默认用的就是计数器法。选它的原因有两个一是过滤能力强它本质上是一个“连续N次采样都相同才算稳定”的滤波器比简单的延时重采样可靠得多二是代码结构清晰可读性好新人接手也能很快看懂方便团队维护。实操心得真正做产品的时候消抖模块最好做成参数化设计——消抖所需时间、系统时钟频率、按键有效电平这些都用parameter或者宏定义配置同一个模块既能用在100MHz的板卡上也能用在50MHz的板卡上换平台的时候只改参数不用改逻辑。这个习惯能帮你省掉大量重复劳动。3. 按键消抖模块RTL实现与参数设计主题内容上我要给出一个实际可用的Verilog消抖模块。代码不是我随手编的是在多个项目里验证过的版本兼顾了功能完整性和代码简洁度。代码之外参数怎么配、位宽怎么算、为什么这么写我会逐一展开。3.1 顶层模块接口定义与参数解析module key_debounce #( parameter CLK_FREQ 50_000_000, // 输入时钟频率单位Hz parameter DEBOUNCE_TIME 20, // 消抖时间单位ms parameter KEY_ACTIVE_LOW 1 // 按键有效电平1表示低电平有效 )( input wire clk, // 系统时钟 input wire rst_n, // 异步复位低电平有效 input wire key_in, // 按键原始输入未消抖 output reg key_out, // 消抖后的稳定电平 output reg key_flag // 按键有效事件脉冲(单周期高电平) );这个模块用了两个parameter。CLK_FREQ是时钟频率DEBOUNCE_TIME是消抖时间。为什么要把它们做成参数而不是直接写死一个数字因为同样的按键模块在50MHz的系统里跑和在100MHz里跑要达到同样20ms的消抖效果计数器的上限不一样。直接写死换平台就得改代码还容易因为忘记改而埋雷。参数化之后例化的时候配一下就行逻辑本身不用动。参数KEY_ACTIVE_LOW用来指示按键的有效电平。多数开发板上的按键按下接地所以是低电平有效也有一些特殊设计的板子是高电平有效。有了这个参数即使板子电平逻辑不一样例化时传参即可模块内部可以自动判断。3.2 内部信号设计与两级同步打拍按键输入是外部异步信号直接连接到FPGA内部逻辑存在亚稳态风险。所谓亚稳态就是触发器的建立时间和保持时间不满足输出端会停留在一个不确定的中间状态可能维持一段时间后才稳定到0或1。如果这个不确定的值被后面的逻辑采到行为就不可预测。解决的经典办法就是两级同步器用两个寄存器级联打两拍。第一拍采到的是可能有亚稳态的信号第二拍采到的是经过一个时钟周期后已经稳定的信号。两级同步不能100%消除亚稳态的传播但能把它的概率降到几乎为0的正常工程水平。reg [1:0] key_sync; always (posedge clk or negedge rst_n) begin if (!rst_n) key_sync 2b11; // 默认高电平假设未按下为高 else begin key_sync[0] key_in; key_sync[1] key_sync[0]; end end wire key_in_sync key_sync[1];这里要注意key_sync[0]直接采外部信号key_sync[1]是同步后的信号之后所有的消抖逻辑都用key_in_sync不能再碰key_in了。这是FPGA设计的一个铁律所有异步输入先过同步器再进逻辑。跳过这一步后面出了问题会非常难排查。3.3 边沿检测逻辑的两种写法消抖模块需要知道按键电平什么时候发生了变化所以边沿检测是核心基础。检测的基本思路是用当前拍和前一拍的信号做比较。上沿rising edge是当前为1前一拍为0下沿falling edge是当前为0前一拍为1。reg key_sync_prev; always (posedge clk or negedge rst_n) begin if (!rst_n) key_sync_prev 1b1; else key_sync_prev key_in_sync; end wire key_rise key_in_sync ~key_sync_prev; // 上升沿 wire key_fall ~key_in_sync key_sync_prev; // 下降沿 wire key_change key_rise | key_fall; // 任意变化这段代码有几个细节。key_sync_prev必须收在always块里让它成为一个寄存器不能直接写成wire的组合逻辑比较不然会产生毛刺。两个边沿都用单周期脉冲的形式表示好处是方便后续逻辑统一处理也不需要考虑电平状态。所有异步输入过两级同步后再做边沿检测顺序不能反。3.4 计数器消抖核心逻辑与阈值计算计数器消抖的思路我在前面已经概括过展开来讲是这样系统时钟每个周期采样一次同步后的键值把这个键值和一个稳定的参考值比较。如果相同计数器加1如果不同说明此刻信号还在抖动计数器清零重来。当计数器累加到设定的消抖阈值时说明键值已经连续稳定了足够长的时间可以认定它已经稳定此时输出新的键值。关键点是计数器的阈值怎么算。假设系统时钟频率是50MHz时钟周期是20ns需要消抖20ms。那么阈值 20ms / 20ns 1_000_000。也就是计数器需要累加到1百万。二进制表示需要多少位2^195242882^201048576所以需要20位。实际工程里为了避免刚好卡边界一般会留一个余量用21位但我下面代码里还是用精确计算的方式由系统自动算出位宽这一点我会详细说明。localparam CNT_MAX CLK_FREQ / 1000 * DEBOUNCE_TIME - 1; // 计数阈值 localparam CNT_WIDTH $clog2(CNT_MAX 1); // 自动计算位宽 reg [CNT_WIDTH-1:0] cnt; reg key_stable; always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt {CNT_WIDTH{1b0}}; key_stable ~KEY_ACTIVE_LOW; // 复位为无效电平 end else if (key_in_sync ! key_stable) begin cnt 0; // 键值和稳定值不一致清零 end else if (cnt CNT_MAX) begin key_stable key_in_sync; // 连续稳定时间够长更新稳定值 end else begin cnt cnt 1b1; end end这个逻辑是消抖模块的核心我重点解释几个写法的用意。第一CNT_WIDTH用$clog2自动计算这是个很实用的系统函数返回括号内表达式以2为底的对数向上取整等价于二进制的位宽。这样无论CLK_FREQ和DEBOUNCE_TIME怎么配位宽都自动匹配不用手算也绝不会算错。当初我开始也是手算CNT_WIDTH的后来有一次改时钟频率忘了同步改位宽仿真跑飞了查了半天才定位到是计数器溢出从那以后新代码一律用$clog2。第二key_stable变量相当于一个“当前公认的稳定电平”。每当采样值和它不一致就认为有抖动计数器清零只有当连续采样值都和它一致且持续足够长的时间才把它更新。这个设计的好处是抖动期间输出值保持不变不会跟随抖动变化松开按键时上升沿经过同样的处理对称性很好。3.5 输出信号生成与单周期脉冲有了key_stable这个稳定电平之后消抖后的电平输出直接就是它key_out key_stable。但实际使用中很多场景需要的不是一个稳定的电平而是一个“按键被按了一下”的事件脉冲这样后续逻辑直接用这个脉冲做触发就行不用自己再判断边沿。reg key_stable_prev; always (posedge clk or negedge rst_n) begin if (!rst_n) key_stable_prev ~KEY_ACTIVE_LOW; else key_stable_prev key_stable; end wire key_press; generate if (KEY_ACTIVE_LOW 1) assign key_press key_stable_prev ~key_stable; // 低有效高→低为按下 else assign key_press ~key_stable_prev key_stable; // 高有效低→高为按下 endgenerate always (posedge clk or negedge rst_n) begin if (!rst_n) key_flag 1b0; else key_flag key_press; endkey_flag是一个单时钟周期的高电平脉冲表示“检测到一次有效的按键按下”。这里使用generate语句根据有效电平参数内部生成对应逻辑代码更通用。同时注意key_flag是打了一拍寄存器输出不要用组合逻辑直接输出脉冲否则可能因为竞争冒险产生毛刺。寄存器输出虽然会晚一个周期但绝不会有毛刺在安全性和质量上是明显更优的选择。3.6 完整代码汇总与例化示例把上面所有片段汇总成一个完整模块代码一共就几十行却承载了消除亚稳态、抖动过滤、有效按键事件检测三个功能。下面的代码是工程可直接使用的版本。module key_debounce #( parameter CLK_FREQ 50_000_000, // 输入时钟频率单位Hz parameter DEBOUNCE_TIME 20, // 消抖时间单位ms parameter KEY_ACTIVE_LOW 1 // 按键有效电平1表示低电平有效 )( input wire clk, input wire rst_n, input wire key_in, output reg key_out, output reg key_flag ); // 计数阈值与位宽自动计算 localparam CNT_MAX CLK_FREQ / 1000 * DEBOUNCE_TIME - 1; localparam CNT_WIDTH $clog2(CNT_MAX 1); // 两级同步器消除亚稳态 reg [1:0] key_sync; always (posedge clk or negedge rst_n) begin if (!rst_n) key_sync 2b11; else begin key_sync[0] key_in; key_sync[1] key_sync[0]; end end wire key_in_sync key_sync[1]; // 边沿检测 reg key_sync_prev; always (posedge clk or negedge rst_n) begin if (!rst_n) key_sync_prev 1b1; else key_sync_prev key_in_sync; end // 消抖计数与稳定电平 reg [CNT_WIDTH-1:0] cnt; reg key_stable; always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt {CNT_WIDTH{1b0}}; key_stable ~KEY_ACTIVE_LOW; end else if (key_in_sync ! key_stable) begin cnt {CNT_WIDTH{1b0}}; end else if (cnt CNT_MAX) begin key_stable key_in_sync; end else begin cnt cnt 1b1; end end // 输出稳定电平 assign key_out key_stable; // 按键事件单周期脉冲 wire key_press; generate if (KEY_ACTIVE_LOW 1) assign key_press key_stable_prev ~key_stable; else assign key_press ~key_stable_prev key_stable; endgenerate reg key_stable_prev; always (posedge clk or negedge rst_n) begin if (!rst_n) key_stable_prev ~KEY_ACTIVE_LOW; else key_stable_prev key_stable; end always (posedge clk or negedge rst_n) begin if (!rst_n) key_flag 1b0; else key_flag key_press; end endmodule顶层例化方法也很简单实例化时指定时钟频率、消抖时间即可。key_debounce #( .CLK_FREQ(50_000_000), .DEBOUNCE_TIME(20), .KEY_ACTIVE_LOW(1) ) u_key_debounce ( .clk (clk), .rst_n (rst_n), .key_in (key), .key_out (key_out), .key_flag (key_flag) );4. 仿真验证的关键要点与测试心得RTL写完了仿真这关必须过。很多初学Verilog的同学遇到过“代码看起来没问题下载到板上就不听话”的情况十有八九是仿真做得不够细。消抖模块的仿真有一个特殊性消抖时间本身是20ms如果用真实的50MHz时钟去仿真需要等1百万个时钟周期才能看到一次按键确认仿真时间会非常长。如果仿真器没有针对这种情况做加速可能跑一次仿真要花掉几个小时甚至更久调试效率低到没法忍。这种场景下仿真环境的设计就显得格外重要。4.1 仿真加速与参数重定义仿真的时候我再例化一个缩减参数版模块把消抖时间改成比如1ms甚至0.1ms用真实50MHz时钟跑仿真时间就大幅缩短了。代码如下key_debounce #( .CLK_FREQ(50_000_000), .DEBOUNCE_TIME(1), // 仿真时缩短为1ms .KEY_ACTIVE_LOW(1) ) u_key_debounce_sim ( .clk (clk), .rst_n (rst_n), .key_in (key_in), .key_out (key_out), .key_flag (key_flag) );要注意这只是为了仿真加速不是改设计功能。参数化在这里就是最大的价值——生产代码和仿真代码共用同一个模块文件只是例化参数不同不会出现“仿真过了上板用的却是另一个版本”的低级错误。4.2 模拟真实抖动的testbench写法好的testbench要造出接近真实的抖动波形而不是简单地给一个干净的高低电平。我一般会用下面这种方式模拟抖动timescale 1ns / 1ps module tb_key_debounce; reg clk; reg rst_n; reg key_in; wire key_out; wire key_flag; initial begin clk 0; forever #10 clk ~clk; // 50MHz时钟 end initial begin rst_n 0; key_in 1b1; // 未按下 #100; rst_n 1; #100; // 模拟按下抖动 key_in 0; #50000; // 按下 key_in 1; #30000; // 反弹 key_in 0; #200000; // 持续按下远超过消抖时间 #100000; // 模拟松开抖动 key_in 1; #40000; key_in 0; #50000; key_in 1; #300000; $finish; end key_debounce #( .CLK_FREQ(50_000_000), .DEBOUNCE_TIME(1), .KEY_ACTIVE_LOW(1) ) dut ( .clk(clk), .rst_n(rst_n), .key_in(key_in), .key_out(key_out), .key_flag(key_flag) ); endmodule关键点在按键变化的时间间隔上。抖动持续总时间一定不能大于消抖时间否则模块会误判为有效电平。我仿真时会刻意设计成抖动时间略小于消抖时间确认输出不翻转然后又设计一个“长抖动”的用例故意让抖动持续时间超过消抖阈值观察模块是否会误判这能帮你发现参数配置错误的隐患。4.3 观察波形的三个关键点仿真跑完以后打开波形窗口重点看三个东西。第一个是key_sync[1]也就是同步后的键值它应该比key_in延迟1~2个时钟周期这是同步器的正常延迟注意不要误以为是bug。第二个是cnt计数器。每次key_in_sync抖动变化cnt应该清零重新计数只有连续稳定时cnt才线性增长到CNT_MAX。如果看到cnt在没有抖动时反复清零说明比较逻辑写反了。第三个是key_flag脉冲它应该只在key_stable变化后的“当前”端口拉出一个单周期的高脉冲而且key_out稳定输出新电平。如果key_flag变成多周期的高电平说明你把它直接连到了key_press组合逻辑上而不是寄存器输出。4.4 我实际用过的一段“速度校验”方法还有一个细节是工程中我认为很有价值的技巧仿真通过后可以加一段“扫参数”的代码把DEBOUNCE_TIME依次设为参数值序列每个值跑一遍同样的抖动输入检查模块是否都在预期的时间点输出。这样能一次性验证模块在不同消抖时间下的行为都正确避免因为参数组合问题导致量产时出踩坑。实操心得做数字逻辑验证不能只看“最终结果对”还要看“中间信号链条是否正确”。单纯盯着key_out看就算模块功能错了也很难直接看出是哪儿的问题。养成观察cnt、key_sync、key_out一级一级往上查的好习惯调试能力会提升得很快。5. 工程化实践经验与常见坑点代码正确、仿真通过只是在软件上到位了。真正把消抖模块用进工程还有不少板级设计、代码走查、综合实现相关的坑这些坑不在RTL里在“工程”二字里。我挑几个踩过或者看别人踩过的典型问题值得拿出来单说。5.1 复位状态选择复位时key_stable被赋为~KEY_ACTIVE_LOW即保持无效电平。这个设计是合理的。如果复位时把key_stable赋为0但按键实际是高电平有效那复位后的第一个比较就会触发清零虽然最终能稳定但白白浪费了启动时间行为也比较别扭。另外一个容易忽略的地方是复位信号本身。如果复位信号也是异步的外部按键或拨码开关产生的它同样会抖动这时候消抖模块的复位端也会受影响。好的工程做法是统一用一个“复位管理模块”处理系统复位把异步复位信号打两拍后再分发到各个子模块不要直接连原始复位按键。5.2 长按和重复触发的需求差异不同产品对按键的行为定义不同。有的产品要求按一下只触发一次长按也是一次有的产品要求长按时周期性地重复触发比如长按音量键连续加减。对于前者用key_flag脉冲就够了——它只在稳定后的当前端口输出一个脉冲长按期间不再产生新脉冲。对于后者需要在消抖基础上扩展一个门槛阈值稳定后继续计数每达到一段时间就产生一个脉冲。千万不要在消抖模块内部混入重复触发逻辑建议另写一个独立模块通过key_flag或key_stable串联实现各模块职责清晰出了问题也容易隔离排查。5.3 多按键场景的资源与共性问题如果你的系统有8个、16个按键最简单的做法是例化多个消抖模块每个按键一个实例。这样代码清晰互不干扰但寄存器消耗会随按键数线性增长。综合来看大多数情况下这种资源开销完全可以接受不必为了省这一点资源去设计复杂的共享消抖结构那样做反而会降低代码可维护性。不过如果按键数量特别多比如矩阵键盘情况就不同了。矩阵键盘通常不是每个按键独立消抖而是先按行扫描列扫描的方式读回一个“按下的行码/列码”再对扫描结果做整体去抖。这种场景下把消抖时间放在扫描周期层面控制更合适也就是“每轮扫描间隔大于消抖时间”而不是对每个按键单独消抖。这个思路对做键盘类产品的同学特别有用。5.4 综合与上板的几个小坑代码在仿真器里跑得欢不代表综合后上板就一定没问题。第一个常见坑是省略了default分支的三段式状态机或计数器逻辑综合工具会生成锁存器。锁存器在FPGA里是不推荐的占用资源多时序行为也难预测。消除方法是在所有组合逻辑块里赋默认值。第二个坑是跨时钟域。如果按键接在100MHz的系统时钟域而我给某个外设发的是50MHz时钟消抖逻辑只处理系统时钟域的信号外设收到的按键事件要再打一拍跨时钟域。可能有人会直接把key_flag跨时钟域使用这也不行单脉冲跨时钟域容易丢失。正确做法是用握手或脉冲同步器或者干脆将消抖后的key_stable电平跨时钟域再在目标时钟域做边沿检测。第三个坑是引脚约束。很多FPGA开发板的按键引脚默认是弱上拉如果你的按键是高电平有效但约束文件里勾选了内部上拉实际电平就变成了按键按下时低电平逻辑判断就反了。每次上板之前先认真检查引脚属性和上下拉设置。5.5 常见问题速查表问题现象可能原因排查方法上板后计数不准按一次加好几次消抖时间太短没有覆盖抖动全部时段用示波器抓按键波形测实际抖动时间DEBOUNCE_TIME适当加大key_flag输出不是单脉冲直接赋值了组合逻辑key_press没有打一拍加一级寄存器输出仿真中cnt永远不累加比较逻辑的按键无效电平写反检查复位后key_stable初值用波形观察key_in_sync比较结果模块在低温环境出现偶发误触抖动时间随温度变化变长把DEBOUNCE_TIME放宽到30ms重新温度测试大量例化后时序恶化复位信号共用导致扇出过大用复位管理模块分频扇出或在综合时设复位树约束实操心得按键消抖模块虽然小但它很有代表性。它涉及异步信号同步、边沿检测、计数器设计、参数化重用、仿真验证、跨时钟域处理几乎把FPGA入门的核心知识点串起来了。把它彻底吃透对后面写UART、SPI、I2C这些常用接口模块的帮助非常大它们底层用的是同一套思维方法。最后再分享一个小技巧我在工程里有一个习惯所有通用模块不只是消抖都会配套一个自检用的testbench模板每换一次平台或每调整一次参数先在仿真里过一遍自检再上板。这个习惯帮我挡下了无数个“看起来没问题但上板就翻车”的意外。按键消抖模块作为“常用模块系列”的第一篇这个测试模板的思路在后续模块里也完全通用。
返回列表