一.定时器按键消抖的原理:
按键消抖的原因:
当我们按下按键的后, 端口从高电平变成低电平, 理想的情况是, 按下, 只发生一次中断, 中断程序只记录一个数据.
但是我们使用的是金属弹片, 实际的情况就是如上图所示, 可能会发生多次中断,难道我们要记录3/4次数据吗?
答:按键按下的时候, 会有机械振动, 这个震荡也会引起单片机io口变化, 从而实现多次触发按键中断,实际我们只需要一次按键中断, 通过科学家的研究, 按下按键到按键平稳, 这个时长是10ms
处理方法(1)
所以我们一般情况下, 是当检测到按键电平变化的时候, 延时10ms后, 然后才真正判断, 按键是否真正按下, 从而把这个按键震荡周期躲了过去
分析第一种方法
第一种延时躲避按键震荡周期的方法, 通过强行让单片机死循环10ms, 来进行实现的。有一个弊端就是延时的这10ms,我们是做不了事情的, 单片机只能傻傻的停留在那里, 如果我们多按下几次, 那系统不就卡爆了。一般按键的事件, 都不是特别紧急的,开发者开发系统的时候, 留给用户的按键, 都是保证绝对安全的, 所以按键的优先级不能太高。程序的运行应该留给更重要并且能够保证系统安全的功能。
这个时候再来看, 我们按下按键程序竟然卡死了10ms, 这个不是很严重的事情吗? 所以我们要节省这10ms, 必须用定时器来做,意思就是系统该干什么就干什么, 我买了一个秒表,按下按键后开始计时, 到点了(震荡周期过了)提醒系统处理按键中断就行了。
处理方法(2)定时器优化
现实因素:
我们按下按键, 不可避免的会产生, 按键机械振动
实际需求:
当按下按键, 按键平稳的时候, 我们才认定是按下了按键, 我们就进入中断去处理。
分析因素和需求之间的困难
因素:按键振动的不可避免
需求: 按键平稳才处理按键事件
困难: 常用方法解决的弊端(ctrl 加鼠标左键,快速跳转)
确定真正软件层面的需求
了解了这中间的矛盾, 我们仔细分析按键的整个过程
我们需要的是按键平稳的时候, 再处理按键, 所以如果我们精准的找到最后一次按键按下的时机,那么不就是可以了吗?
所以现在我们的需求就变成了,如何精准的找到,按键彻底按下的时机,也就是按键按下到按键彻底闭合, 这之间虽然会产生按键振动,也就是上图所示的①②③④, 这些电平都会被我们的单片机捕捉到,识别到io口电平变化,但这只是蜻蜓点水,不是真正的按下, 我们要的是稳稳地幸福。
解决方案
(1)逃避法(ctrl 加鼠标左键,快速跳转)
(2)按键中断+定时器实时扫描法(喂狗模型)
通过了解方案(1), 我们不能进行逃避, 所以要精准的识别到最后一次按键,然后触发事件就行了.
那如何判断, 最后一次按键,就是最后一次呢?
观察最后一次按键抖动的特点:
最后一次按键按下后, 就是彻底的闭合了,因为按键抖动的时长最长是10ms,所以按键按下,然后电平保持稳定10ms,就可以认定是此次按键事件触发。
但是按键按下的信息都一样, 都是电平变化, 不管电平变化间隔的时间长短,都会触发if(电平变化){代表按键按下}
所以我们就需要, 在每一次按键按下后, 都开始计时10ms,
按键识别分成两种情况:
(1)这个按键是抖动
虽然是抖动, 我们一视同仁, 也开始用定时器计时从零计时10ms,然后还没来得及计时到 10ms
下次抖动就触发了, 然后就把这个计时抛弃了, 再次刷新定时器从头开始计时,判断下次的抖动是否是最后一次按键
(2)最后一次按键来临
通过过滤上面的抖动按键(每次新的按键来临,并且这个按键定时器计时不超过10ms),终于等到你(最后一次稳稳地幸福),那么我们还是开始计时, 此时定时器就计时到了10ms,然后我们就去处理按键事件就行了。
二.代码执行方案
测试真的存在按键抖动问题
1.首先解压打开0602_key_isr_oled.7z,然后改名为 0603_key_timer
0602工程
密码:5bpw
2.打开工程
3.找到中断函数
4.当发生按键中断的时候, 按键被调用
5.我们会看到中断调用了回调函数, 我们接着f12进入
6.这里就是我们之前自己写的中断回调函数,之前我们是点亮led,现在我们开始记录按键次数, 看看是否存在按键抖动现象
int g_key_cnt = 0;
void HAL_GPIO_EXTI_Callback()
{
g_key_cnt++;
}
7.我们现在开始测试, 那现在我们就按下按键, 查看这个g_key的值, 怎么显示呢? 我们就用OLED函数吧, 我们用之前的OLED函数
8.复制初始化函数, 拿到main函数里面
9.先显示 一串字符串, 表明这个是我们的按键次数, 复制示例代码里面的函数
10.然后在while循环里面, 重复的进行, 显示按键次数
11.编译运行, 发现没有oled函数头文件, 所以我们把路径, 加入到环境变量里面(只指定目录即可)
12.然后我们烧录, 按下按键, 观察现象(我们只是按下松开一次, 数值就增加了好多次)
定时器解决按键抖动问题
1.我们进入启动文件, 来定位到我们的滴答定时器计数中断,然后f12进入
2.每一毫秒运行一次
3.f12接着进入, 我们再看看, 他真的是只增加一个计数值而已
4.那么如何获得计数值呢?
5.当我们按下或者松开按键的时候, 我们回调函数被调用
6.每当有按键按下(即使这个按键是按键抖动), 我们都要去刷新修改定时器的计数值, 从而实现上文所说的(精确的检测到最后一次按键按下)
7.当最后一次按键按下的时候, 然后等待10ms, 定时器超时, 我们就要进行按键事件的处理
8.什么是timer, 所以我们就需要在main.c里面声明一个结构体.
我们想一下, 我们计时需要哪些变量, 我们把他们放在一个结构体里面就行了
① 超时时间: 在我们SysTick里面, 我们每次过1ms, 都会增加一个计数值,
uint32_t timeout; //当前uwTick + 某一个数值
② 想做的通用一点的话, 来个参数
void *arg;
③ 调用什么函数
void (*func)(void *);
9.我们在main.c里面定义一个结构体
struct soft_timer{
uint32_t timeout;
void * args;
void (*func)(void *);
};
10.我们再根据这个结构体,来定义一个按键的结构体:
第一个超时时间,我们对0取反,就相当于一个巨大的数
第二个我们定义成引脚吧, 先设置成NULL
第三个,就是我们按键事件触发,调用的函数, 我们还没有写, 先欠着
struct soft_timer key_timer = {~0, NULL, key_timeout_func};
11.之前我们发生按键操作的时候, 我们是修改led, 现在是来修改这个结构体里面的超时时间, 也就是我们之前说的, 按键来了(不管这个按键是抖动还是最后一次按键), 我们都进行刷新定时器计数(也就是结构体里面的超时时间)
我们首先传入此时的滴答计数器的数值 , 再传入10ms这个参数, 把超时时间设置成 10ms后,
后面我们定时器里面就每毫秒查询这个 超时时间和滴答定时器的数值, 如果到达,则代表按键平稳是最后一次按键, 如果没达到, 就会被下次按键刷新
mod_timer(&key_timer, 10);
12.我们按键中断函数里面, 已经设置好了超时时间, 那么检测是否超时的任务,就交给定时器中断了, 我们进入此定时器中断函数,进行设置检测是否超时函数
extern void check_timer(void);//声明一下这是外部函数
check_timer(); //每毫秒都调用检测是否超时, 从而实现按键抖动过滤
13.下面我们来完善这些代码
mod_timer(&key_timer, 10);//修改超时时间
void mod_timer(struct soft_timer *pTimer, uint32_t timeout) { pTimer->timeout = HAL_GetTick() + timeout; }
超时时间等于 10ms, 我们传入的是当前的按键结构体, 和设置的超时时长(10ms)
我们结构体的超时时间, 需要和定时器的滴答计数器对比,
所以我们的超时时间 = 当前Tick时间 + 超时时长
也就是 pTimer->timeout = HAL_GetTick() + timeout;
14.接下来我们写check_timer();
void check_timer(void)
{
if(key_timer.timeout <= HAL_GetTick())
{
key_timer.func(key_timer.args);
}
}
我们定时器, 一直在检测, 滴答定时器是否达到超时时间, 在按下按键的时候, 刚开始会产生抖动, 也就是我们还没达到超时时间, 就又触发按键中断, 那么按键中断里面的
mod_timer(&key_timer, 10); 函数,就会刷新超时时间, 从而使定时器无法达到超时时间
也就进入不了 if(key_timer.timeout <= HAL_GetTick())
只有最后一次按键触发,电平稳定,也就是达到超时时间, 才能够触发我们的按键处理事件
也就是 我们调用了结构体里面的按键函数事件 key_timer.func(key_timer.args);
15.定时器通过调用 check_timer() 函数检测uwTick是否达到超时时间, 如果达到, 就代表着是最后一次按键事件, 我们就调用结构体里的key_timer.func(key_timer.args);
我们在这里处理的就是 , 对按键计数值加一
一次按键中断, 累加一次, 并不会重复触发中断,从而实现了消抖
当然, 需要注意的一点是, 我们一次按键事件触发后, 在key处理函数中, 要记得清除超时时间, 因为定时器是一直工作的, 要么按键事件处理完后,我们就把key_timer.timeout设置成一个很大的值.定时器就不会再超时触发按键事件了, 直到下次按键再次按下, 重新设置超时时间.
void key_timeout_func(void *args);
void key_timeout_func(void *args)
{
g_key_cnt++;
key_timer.timeout = ~0;
}
16.烧录运行, 发现按键按下一次, 触发一次
测试成功的工程