目录
独立按键简介
轻触按键相当于一种电子开关,按下时开关接通,松开时开关断开,实现原理是通过轻触按键内部的金属弹片受力弹动来实现接通与断开。
按键抖动
由于按键内部使用的是机械式弹簧片来进行通断的,所以在按下和松手的瞬间会伴随有一连串的抖动
当机械触点断开、闭合时,由于机械触点的弹性作用,一个按键开关在闭合时不会马上就稳定的接通,在断开时也不会一下子彻底断开,而是在闭合和断开的瞬间伴随了一连串的抖动。由于单片机检测 IO口速度非常快,超过弹片抖动的频率,所以在检测按键状态时。从而对按键的判断产生一些误操作(比如按一下会产生按多下的效果),因此必须要消除抖动才能正常使用按键。要消除按键抖动的影响。抖动时间的长短由按键的机械特性决定的,一般为5ms 到10ms。
硬件消抖
本节不涉及
软件消抖
延时消抖
定时器消抖
模块接线
按键 | STM32F103C8T6 |
KEY1 | PB11 |
KEY2 | PB1 |
另一端接入GND
延时消抖
按键松手后判定
Key.h
#ifndef _KEY_H_
#define _KEY_H_
void Key_Init(void);
uint8_t Key_GetNum(void);
#endif
Key.c
#include "stm32f10x.h" // Device header
#include "Key.h"
#include "Delay.h"
#define KEY1 GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11)
#define KEY2 GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1)
void Key_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11 | GPIO_Pin_1;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;//输出速度,输入无用
GPIO_Init(GPIOB, &GPIO_InitStructure);
}
uint8_t Key_GetNum(void)
{
uint8_t KeyNum = 0;
if(KEY1 == RESET)
{
Delay_ms(20);
while(KEY1 == RESET)
Delay_ms(20);
KeyNum = 1;
}
if(KEY2 == RESET)
{
Delay_ms(20);
while(KEY2 == RESET)
Delay_ms(20);
KeyNum = 2;
}
return KeyNum;
}
单按松手前按键触发+连按按键扫描函数来源于正点原子:
//按键处理函数 //返回按键值 //mode:0,不支持连续按;1,支持连续按; //0,没有任何按键按下 //1,KEY0按下 //2,KEY1按下 //3,KEY3按下 WK_UP //注意此函数有响应优先级,KEY0>KEY1>KEY_UP!! u8 KEY_Scan(u8 mode) { static u8 key_up=1;//按键按松开标志 if(mode)key_up=1; //支持连按 if(key_up&&(KEY0==0||KEY1==0||WK_UP==1)) { delay_ms(10);//去抖动 key_up=0; if(KEY0==0)return KEY0_PRES; else if(KEY1==0)return KEY1_PRES; else if(WK_UP==1)return WKUP_PRES; }else if(KEY0==1&&KEY1==1&&WK_UP==0)key_up=1; return 0;// 无按键按下 }
连按: key_up每次进入函数都为1,相当于把有关key_up的判断与赋值删除,只要有按键按下,消抖后再次判断是否按下,不等松手就返回对应按键值。
//拆分代码 u8 KEY_Scan(void) { if(KEY0==0||KEY1==0||WK_UP==1) { delay_ms(10);//去抖动 key_up=0; if(KEY0==0)return KEY0_PRES; else if(KEY1==0)return KEY1_PRES; else if(WK_UP==1)return WKUP_PRES; }; return 0;// 无按键按下 }
只单击不连按: key_up为上一次检测按键值,上一次没有按,这一次按了才会返回对应按键值,
u8 KEY_Scan(void) { static u8 key_up=1;//按键按松开标志 if(key_up&&(KEY0==0||KEY1==0||WK_UP==1)) { delay_ms(10);//去抖动 key_up=0; if(KEY0==0)return KEY0_PRES; else if(KEY1==0)return KEY1_PRES; else if(WK_UP==1)return WKUP_PRES; }else if(KEY0==1&&KEY1==1&&WK_UP==0)key_up=1; return 0;// 无按键按下 }
作为这种很简单的演示程序,我们可 以这样来写,但是实际做项目开发的时候,程序量往往很大,各种状态值也很多,while(1) 这个主循环要不停的扫描各种状态值是否有发生变化,及时的进行任务调度,如果程序中间 加了这种 delay 延时操作后,很可能某一事件发生了,但是我们程序还在进行 delay 延时操作 中,当这个事件发生完了,程序还在 delay 操作中,当我们 delay 完事再去检查的时候,已经晚了,已经检测不到那个事件了。为了避免这种情况的发生,我们要尽量缩短 while(1)循环 一次所用的时间,而需要进行长时间延时的操作,必须想其它的办法来处理。 那么消抖操作所需要的延时该怎么处理呢?
定时器扫描按键代码
为了检测按键按下和松开,必须记录按键当前状态和上一次状态。如果当前状态为高电平,上一次状态为低电平,则检测到按键按下后松手;如果当前状态为低电平,上一次为高电平,则检测到按键按下。由于短按按下一次只需返回一次按键值,因此只需检测按键松开动作,即上一次状态低电平,当前状态高电平。
定时1ms(根据项目和按键抖动情况更改,不要频繁进入中断)进入中断,中断服务函数里扫描按键上一次状态低电平,当前状态高电平就认为按下了。主函数获取按键值后就清除按键按下标志位,等待下一次中断扫描更新按键按下标志位。
单击不支持连按
Key.h
#ifndef _KEY_H_
#define _KEY_H_
void Key_Init(void);
uint8_t Key_GetNum(void);
void Key_Scan(void);
#endif
Key.c
#include "stm32f10x.h" // Device header
#include "Key.h"
uint8_t Key_KeyNum;
#define KEY1 GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11)
#define KEY2 GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1)
void Key_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11 | GPIO_Pin_1;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;//输出速度,输入无用
GPIO_Init(GPIOB, &GPIO_InitStructure);
}
uint8_t Key_GetNum(void)
{
uint8_t temp;
temp = Key_KeyNum;
Key_KeyNum = 0;
return temp;
}
static uint8_t Key_Get(void)
{
uint8_t Key_Check = 0;
if(KEY1 == RESET){ Key_Check = 1;}
if(KEY2 == RESET){ Key_Check = 2;}
return Key_Check;
}
void Key_Scan(void)
{
static uint8_t LastNum, KeyNum;
LastNum = KeyNum;
KeyNum = Key_Get();
if(LastNum == 1 && KeyNum == 0)
{
Key_KeyNum = 1;
}
if(LastNum == 2 && KeyNum == 0)
{
Key_KeyNum = 2;
}
}
main.c
#include "stm32f10x.h" // Device header
#include "OLED.h"
#include "Key.h"
#include "Timer.h"
int8_t KeyNum, Num;
int main(void)
{
OLED_Init();
Key_Init();
Timer_Init();
OLED_ShowString(1, 1, "Num:");
while (1)
{
KeyNum = Key_GetNum();
if(KeyNum == 1)
{
Num++;
}
if(KeyNum == 2)
{
Num--;
}
OLED_ShowSignedNum(1, 5, Num,3);
}
}
void TIM2_IRQHandler(void)
{
if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)
{
Key_Scan();
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}
思考
由于定时器每1ms进入一次中断,对于具有IIC和One-Wire通信等具有严格时序要求的项目来说,容易使通信被干扰。 在通信前关闭中断,在通信完成后打开中断。根据此种方法,可以在通过中断提高MCU工作效率的同时,使中断不干扰具有严格时序的通信(如I2C,One-Wire,SPI等)
MultiButton按键驱动
事件 | 说明 |
---|---|
PRESS_DOWN | 按键按下,每次按下都触发 |
PRESS_UP | 按键弹起,每次松开都触发 |
PRESS_REPEAT | 重复按下触发,变量repeat计数连击次数 |
SINGLE_CLICK | 单击按键事件 |
DOUBLE_CLICK | 双击按键事件 |
LONG_PRESS_START | 达到长按时间阈值时触发一次 |
LONG_PRESS_HOLD | 长按期间一直触发 |
开源链接:
0x1abin/MultiButton: Button driver for embedded system (github.com)
使用教程(包含一个回调函数检测多个按键方法):
MultiButton | 一个小巧简单易用的事件驱动型按键驱动模块 - 知乎 (zhihu.com)
//头文件中可根据需要修改参数
//According to your need to modify the constants.
#define TICKS_INTERVAL 5 //ms
#define DEBOUNCE_TICKS 3 //MAX 7 (0 ~ 7)
#define SHORT_TICKS (300 /TICKS_INTERVAL)
#define LONG_TICKS (1000 /TICKS_INTERVAL)