FPGA按键消抖程序剖析 对于学习 FPGA 的爱好者来说 , 在我们做的许许多多的系统和项目中都会用到 按键 , 但是在我们所涉及的按键输入的数据 , 是不能够直接利用的,而是要经过消 抖,唉,为了初级菜鸟,说简单点吧 , 什么叫消抖?消抖说白点就是消除抖动引起的 按键不确定性!如果不消抖处理的话,就会按一次可能将相当于按几次出现的结果。相 信玩过单片机的人都清楚 , 我们在读出按键的时候都会用到一个程序: If(!key) { Delay(x); If(!key) {...} } 这个程序在此就不再延伸解释 , 我所要说的是 , 在 FPGA 里面按键的消抖与单片 机的消抖原理都是一样的!或许大家都自己写过或者在网上找到过许多消抖的程序 , 但是 我所接触的当中 , 我觉得特权前辈所写的那个是最为经典的 , 一下就是我盗版特权前辈 的一个程序: module keyscan_module ( input clk, // 外部输入时钟,我选择 50M input reset, // 复位 , 但是在此不建议使用此种复位 // 形式,建议用异步复位同步释放 input key_in, // 按键输入( 1bit ) output key_ready); // 按键值输出 /*********************************************/ reg key1,key2; 定义两个寄存器变量 wire key_en; 定义一个线性变量 always @ ( posedge clk or negedge reset) if ( !reset ) begin key1 <= 1'b1; key2 <= 1'b1; end else begin key1 <= key_in; // 学习过非阻塞式语句的同志,应该不能理解这里 // 第一个时钟读取按键进入第一个寄存器 , 第二个时钟 key2 <= key1; 将第一个寄存器中的值赋给第二个寄存器 end assign key_en = key2 & ( ~key1 ); // 当检测到下降沿的时候, key_en 保持一个时钟的高 电平 /**********************************************/ reg[19:0] cnt; 定义一个计数器 always @ ( posedge clk or negedge reset ) if ( !reset )cnt <= 20'd0; else if ( key_en ) cnt <= 20'd0; else cnt <= cnt + 1'b1; /********************************************/ reg key3,key4; always @ ( posedge clk or negedge reset ) if ( !reset ) key3 <= 1'b1; else if ( cnt == 20'hfffff) // 当计数到 20ms 的时候 key3 <= key_in; always @ (posedge clk or negedge reset) // 下一个时钟把 key3 的按键值赋给 key4 if(!reset)key4 <= 1'b1; else key4 <= key3;assign key_ready = key4 & ( ~key3); // 当有按键按下时 , 输出有效 , 保 持一个时钟周期 ; endmodule 下面我们来好好研究下这个程序: 整个程序的基本思路是这样的 : 系统上电后 , 计数器就开始计数 , (注意啊 , 不管有没有按键按下都在计数 , 每次计数 20MS ) 于此同 时,系统也在不断采集 key_in 的电平,假设在一个时钟上升沿的时 候,检测到 key_in 为高电平 , (那么 key1<=1,key2<=1 )在下一个时 钟上升沿的时候检测到低电平 (key1<=0;key2<=1) 那么执行这个语句 assign key_en = key2 & ( ~key1 ) 那么 key_en 就会得到一个高电 平 , 当第三个时钟上升沿的时候 ( key1<=0,key2<=0 ) 由此可知 , key_e n 只保持一个时钟周期的高电平 , 而且仅是当检测到有下降沿的时候才 会变为高电平 。 说到计数器 , 前面也说了 , 上电之后计数器就一直在 计数,当时当 key_en 为高电平的时候,计数清零,也就是说,当前 面检测到下降沿之后就重新开始计数, else if ( key_en ) cnt <= 20'd0; else cnt <= cnt + 1'b1; 然后到这个程序 else if ( cnt == 20'hfffff) // 当计数到 20ms 的时候 key3 <= key_in; 这个程序的作用是计数到了 20ms 之后再一次读取 key_in 的值 , 换句话说,就是从前面检测到下降沿之后, 20ms 后再去检测!我们 都知道,抖动所产生的毛刺都是在 us 级别的,哎呀,再怎么大也大 不过 20ms 的,而我们按按键的话那肯定就不止 20ms 啦,再怎么快也要 500ms 以上吧! 再结合下段程序: always @ (posedge clk or negedge reset) // 下一个时钟把 key3 的按 键值赋给 key4 if(!reset)key4 <= 1'b1; else key4 <= key3; assign key_ready = key4 & ( ~key3); // 假设第一次检测到的下降沿是由于抖动产生的毛刺 , 那么 20M S 后 , 过 滤 掉 毛 刺 , 检 测 到 的 应 该 是 一 个 高 电 平 ( key3<=1,key4<=1,key_ready=0 ) , 假如前面检测到的下降沿不是由于 毛刺,而是按键按下的,那么 20MS 后, key_in 肯定还会是低电平 , 所以 ( key3<=0,key4<=1,key_ready=1 ) , 而在 20MS 之后的下一个时钟 ( key3<=0,key4<=0, 那么 key_ready=0 ) key_read 只保持一个时钟周期的时间! 为什么这个消抖程序被列为经典呢?大家神人研究后不妨从 时序和资源方面去考虑一下,昨天我在一本黑金的教程上面弄了一 个,一个按键就消耗 80 多个 lut ,而这个只要 30 个,试想一下, 做 16 个按键都上千呢,那其他模块怎么办?还有我觉得很好的一点就 是它的输出 ( key_ready ) 只保持一个时钟周期 , 有利于上层模块采集 ! 哎呀 , 没有对比 , 看不出差距 , 以下是我们老师教我写的一个消抖程 序:他的思路是:把系统时钟( 50M )分频 100K ( 10ms )后再去读 取按键值 Always @ (posedge clk_100K or negedge reset) If(!reset)begin key1<=1;key2<=1; end Else begin key1<=key_in;key2<=key1;end Assign key_ready=key2 & !key1; 大家看一下这个程序 , 原理和特权的那个差不多 , 看似更加简单 , 的确,这个程序可以消抖,但是存在一个问题,就是 key_ready 也会 保持 10MS 的一个高电平 , 有些人会问 , 高电平持续就一点不是方便 上层采集吗?其实不然 , 举个例子 : 我们用一个按键 , 控制一个二极 管,按一下亮,再按一下灭,程序大概这样:Always @ (posedge clk or negedge ) .........................................( 省略 ) If(key_ready) led<=~led 我们使用的系统时钟是很高的,假如 key_ready 保持不是一个时 钟周期 , 而是 10ms, 那么我们按一次 , led 就会一亮一灭很多次 ! 碰到 这种情况,一般要用组合逻辑解决,比如 Always @ (key_read or negedge) ................... If(key_reaf)...... 这样也可以实现按一次变化一次 , 但是大家都知道 , 组合逻辑往 往会给我们带来许多时序上的问题,能用同步就同步 ! 在此也特别提 醒诸位,在 if(x) 判断电平的时候多考虑一下,免得出错! 对于消抖程序忠告如下 reg [3:0]key1_reg; reg [3:0]key2_reg; always @(posedge clk,negedge rest)begin if(!rest)begin key1_reg<=4'b1111; key2_reg<=4’b1111; end else if(counter==5'hfffff)begin key1_reg<=key; key2_reg<=key1_reg end assign key_ctr=key2_reg&(~key1_reg); 这段程序,20ms后采样 key的值给 key1_reg,而 key2_reg值为 key1_reg前一时刻的值,有按键按下时 key_ctr 为 1,可是程序中 key2_reg 值 20ms 才刷新一次,如果在做我们用一个按 键 , 控制一个二极 管,按一下亮,再按一下灭,程序大概这样:Always @ (posedge clk or negedge ) .........................................( 省略 ) If(key_ready) led<=~led 这段 20ms 时间内每一晶振脉冲下 led 都翻转,这样灯就一闪一闪,不能达到要求,而我们 程序改成这样: reg [3:0]key1_reg; reg [3:0]key2_reg; always @(posedge clk,negedge rest)begin if(!rest) key1_reg<=4'b1111; else if(counter==5'hfffff) key1_reg<=key; end always @(posedge clk,negedge rest)begin if(!rest) key2_reg<=4'b1111; else key2_reg<=key1_reg; end assign key_ctr=key2_reg&(~key1_reg); Always @ (posedge clk or negedge rest ) .........................................( 省略 ) If(key_ready) led<=~led; 这样 led 就只是翻转一次才能达到控制效果。
|