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 就只是翻转一次才能达到控制效果。