一、按键抖动
按键抖动是电子设备中常见的问题,尤其是在机械开关如按键中。抖动发生是因为当按键被按下或释放时,接触点之间的快速接触和断开导致信号线上产生快速的电平变化,这可能导致微控制器错误地识别多次按键动作。
按键抖动的时间一般为10~50ms
二、硬件消抖
硬件消抖主要是通过两种方式实现:RS触发器和电容滤波器。
-
RS触发器:
- RS触发器利用其特性来吸收按键的抖动。当按键被按下时,触发器会立即翻转状态,而按键的抖动不会影响输出,因为触发器的状态改变是瞬间的,不受后续抖动的影响
-
电容滤波:
- 电容滤波器通过并联一个电容器在按键两端,利用电容的充放电特性来消除抖动。当按键产生抖动时,电容器会吸收这些快速变化的电平,从而减少抖动对电路的影响。
这两种硬件消抖方法可以有效减少按键抖动对电路的影响,确保信号的稳定性。在没有MCU的情况下,硬件消抖电路是常用的消抖手段。而在嵌入式开发中,通常通过软件消抖来实现,因为这种方法成本较低。
硬件消抖会额外增加硬件设施,会有更多的成本,并且不能保证完全可靠,因此更多时候需要添加软件消抖,常见的软件消抖有延时消抖和定时器消抖。
三、延时消抖
延时消抖是一种常用的软件消抖方法,其基本原理是在检测到按键状态变化后,通过引入一定的延时来等待抖动消失,然后再进行一次状态检测以确认按键的真实状态。这种方法简单易实现,但也存在一些缺点,比如会占用CPU资源,影响程序的实时性,以及不够精准。以下是具体的实现步骤和代码示例:
1.延时消抖原理:
当检测到按键按下时,先等待一个固定的延时时间(通常为10ms~50ms),这段时间足以让大部分由接触不良引起的抖动消失。延时过后,再次检测按键状态,如果按键仍然处于按下状态,则认为是有效的按键操作。
2.代码实现:
#include "stm32f4xx.h"
static GPIO_InitTypeDef GPIO_InitStructure;
static EXTI_InitTypeDef EXTI_InitStructure;
static NVIC_InitTypeDef NVIC_InitStructure;
void delay_ms(uint32_t ms)
{
while(ms --)
{
SysTick->CTRL = 0; // 关闭系统定时器后才能配置寄存器
SysTick->LOAD = 21000; // 设置计数值,用于设置定时的时间
SysTick->VAL = 0; // 清空当前值还有计数标志位
SysTick->CTRL = 1; // 使能系统定时器工作,且时钟源为系统时钟的8分频(168MHz/8=21MHz)
while ((SysTick->CTRL & (1<<16))==0); // 等待系统定时器计数完毕
SysTick->CTRL = 0; // 关闭系统定时器
}
}
#define PEout(n) (*(volatile uint32_t *)(0x42000000+(GPIOE_BASE+0x14-0x40000000)*32+n*4))
#define PFout(n) (*(volatile uint32_t *)(0x42000000+(GPIOF_BASE+0x14-0x40000000)*32+n*4))
#define PAin(n) (*(volatile uint32_t *)(0x42000000+(GPIOA_BASE+0x10-0x40000000)*32+n*4))
static uint32_t g_key_event=0;//按键标志位
int main(void)
{
/* 打开端口A的硬件时钟(就是对硬件供电),默认状态下,所有时钟都是关闭 */
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA,ENABLE);
/* 打开端口F的硬件时钟(就是对硬件供电),默认状态下,所有时钟都是关闭 */
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF,ENABLE);
/* 打开系统配置syscfg时钟(就是对硬件供电),默认状态下,所有时钟都是关闭 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG,ENABLE);
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_9|GPIO_Pin_10;//指定9 10号引脚
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_OUT;//引脚工作在输出模式
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_100MHz;//速度越高,响应时间越短,但是功耗就越高,电磁干扰也越高
GPIO_InitStructure.GPIO_OType=GPIO_OType_PP;//如果外部没有上拉电阻,就配置推挽输出模式
GPIO_InitStructure.GPIO_PuPd=GPIO_PuPd_NOPULL;//不需要使能上下拉电阻
GPIO_Init(GPIOF,&GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_0;//指定0号引脚
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IN;//引脚工作在输入模式
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_100MHz;//速度越高,响应时间越短,但是功耗就越高,电磁干扰也越高
GPIO_InitStructure.GPIO_PuPd=GPIO_PuPd_NOPULL;//不需要使能上下拉电阻
GPIO_Init(GPIOA,&GPIO_InitStructure);
/* 将外部中断连接到指定的引脚,特别说明:引脚编号决定了使用哪个外部中断 */
SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOA, EXTI_PinSource0);
/* 配置外部中断0 */
EXTI_InitStructure.EXTI_Line = EXTI_Line0; //指定外部中断0
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;//工作在中断模式
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;//下降沿触发:就是发现有从高电平到低电平的跳变,就向CPU中断请求处理
EXTI_InitStructure.EXTI_LineCmd = ENABLE;//允许外部中断0工作
EXTI_Init(&EXTI_InitStructure);
/* 通过NVIC管理外部中断0的中断请求:中断号、优先级、中断打开/关闭 */
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;//中断号
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x0F;//抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x0F;//响应优先级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;//中断打开
NVIC_Init(&NVIC_InitStructure);
PFout(9) =1;
PFout(10)=1;
while(1)
{
if(g_key_event)
{
delay_ms(50);
if(PAin(0)==0)
PFout(9)^=1;
//等待按键的释放
while(PAin(0)==0);
//释放时也消抖
delay_ms(50);
//等待按键的释放
while(PAin(0)==0);
g_key_event=0;
/* 打开EXTI0外部中断请求 */
NVIC_EnableIRQ(EXTI0_IRQn);
}
}
}
/* 中断服务函数 */
void EXTI0_IRQHandler(void)
{
/* 检测中断是否有触发 */
if(EXTI_GetITStatus(EXTI_Line0) == SET)
{
g_key_event=1;
/* 关闭EXTI0外部中断请求 */
NVIC_DisableIRQ(EXTI0_IRQn);
//清空标志位
EXTI_ClearITPendingBit(EXTI_Line0);
}
}
按键消抖简单方便,但是在裸机编程中,延时会占用CPU。在RTOS(实时操作系统)中vTaskDelay()可以通过睡眠延时进行任务调度,几乎不会占用CPU。
四、定时器消抖
定时器消抖的原理是基于时间延迟来确保按键状态的稳定性。当按键被按下或释放时,由于机械接触的不稳定性或电气噪声,可能会在短时间内产生多次快速的电平变化,这种现象称为抖动。定时器消抖的目的是等待这段不稳定的时间过去,然后确认按键的真实状态。以下是定时器消抖的详细步骤和原理:
-
检测按键状态:
在程序中,首先检测按键的当前状态,例如,检查GPIO引脚的电平是否表示按键被按下。 -
启动定时器:
如果检测到按键状态发生变化(例如,从未按下变为按下),则启动一个定时器,并设置一个适当的延时时间,通常这个时间足够长,可以覆盖大多数抖动事件,但又要足够短,以免影响用户体验,常见的延时时间为几十毫秒。 -
等待定时器超时:
程序等待定时器超时。在这个期间,CPU可以继续执行其他任务,或者进入低功耗模式等待定时器超时。 -
确认按键状态:
定时器超时后,再次检查按键状态。如果按键状态与初始检测时相同(例如,仍然是按下状态),则认为这是一个有效的按键动作,可以执行相应的处理逻辑。 -
处理按键事件:
如果确认按键动作有效,执行相应的按键处理程序,如切换LED状态、发送命令等。 -
清除定时器:
完成按键处理后,清除定时器中断标志,准备下一次按键检测。
通过这种方法,定时器消抖可以有效滤除由于按键抖动引起的错误信号,确保只有在按键状态稳定后,才将其视为有效的用户输入。这种方法简单、可靠,且不会占用过多的CPU资源,因为它允许CPU在等待定时器超时期间执行其他任务。
代码实现
void key_init(void)
{
//使能AHB1总线上指定外设的硬件时钟,其实就是对外设进行供电 (如果该外设不使用,可以关闭其硬件时钟,降低功耗)
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG,ENABLE);//打开中断控制时钟
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IN;//输出模式
// GPIO_InitStructure.GPIO_OType=GPIO_OType_PP;//推挽类型,Push Pull
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_2MHz;//引脚工作速度
GPIO_InitStructure.GPIO_PuPd=GPIO_PuPd_NOPULL;//不使能上下拉电阻
//对GPIOF初始化
GPIO_Init(GPIOA,&GPIO_InitStructure);
/* 将外部中断连接到指定的引脚,特别说明:引脚编号决定了使用哪个外部中断 */
SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOA, EXTI_PinSource0);
/* 配置外部中断0 */
EXTI_InitStructure.EXTI_Line = EXTI_Line0; //指定外部中断0
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;//工作在中断模式
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;//下降沿触发:就是发现有从高电平到低电平的跳变,就向CPU中断请求处理
EXTI_InitStructure.EXTI_LineCmd = ENABLE;//允许外部中断0工作
EXTI_Init(&EXTI_InitStructure);
/* 通过NVIC管理外部中断0的中断请求:中断号、优先级、中断打开/关闭 */
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;//中断号
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x0F;//抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x0F;//响应优先级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;//中断打开
NVIC_Init(&NVIC_InitStructure);
}
void tim2_init(void)
{
//使能TIM2的硬件时钟,就是对它供电
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
//配置TIM2的定时时间
TIM_TimeBaseStructure.TIM_Period = 10000/40-1;//用于设置自动重载寄存器的值,决定了定时时间,当前定时为1秒
TIM_TimeBaseStructure.TIM_Prescaler = 8400-1;//预分频值,将该值传递给预分频的寄存器,会自动加1,实际就是除以16800
//TIM_TimeBaseStructure.TIM_ClockDivision = 2;//当前stm32f407是不支持的,它是属于时钟分频,也是二次分频
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;//向上计数,0 ->9999 就是1秒时间的到达
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
TIM_ITConfig(TIM2,TIM_IT_Update,ENABLE);//配置TIM8的中断触发条件:更新中断(UI)
//配置TIM2的中断:优先级、IRQ通道要使能
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
//使能TIM8工作
TIM_Cmd(TIM2,ENABLE);
}
int main()
{
led_init();
key_init();
tim2_init();
while(1)
{
}
}
void TIM2_IRQHandler(void)
{
//检测是否有更新中断
if(TIM_GetITStatus(TIM2,TIM_IT_Update)==SET)
{
if(PAin(0)==0)
{
PFout(9)^= 1;
}
/* 关闭定时器2 */
TIM_Cmd(TIM2,DISABLE);
//清空标志位,告诉CPU当前中断不需要再服务,已经完成处理
TIM_ClearITPendingBit(TIM2,TIM_IT_Update);
}
}
void EXTI0_IRQHandler(void)
{
/* 检测中断是否有触发 */
if(EXTI_GetITStatus(EXTI_Line0) == SET)
{
//清空标志位
TIM_ClearITPendingBit(TIM2,TIM_IT_Update);
//关闭定时器
TIM_Cmd(TIM2,DISABLE);
//清空当前计数值
TIM_SetCounter(TIM2,0);
//重新启动定时器
TIM_Cmd(TIM2,ENABLE);
//清空标志位
EXTI_ClearITPendingBit(EXTI_Line0);
}
}
定时器消抖不会占用CPU,但是在使用时会占用多一个定时器资源,因此,在实际应用中,根据实际需求进行取舍