怎么能把按键处理玩出花?按键处理作为一个基础入门实验,大部分人在刚接触单片机的时候都会自己写一份,开始我们利用延时消抖,后来发现在大的工程当中,延时消抖在没有加入操作系统来调度的情况下,无疑是一种很浪费资源的做法。再后来我们开了定时器去扫描,确实比较靠谱,但是一但设计到复杂的组合按键,长按短按双击等,就需要我们去费很大的功夫去进行逻辑判断。
在网上看到了很多很棒的方法,即把底层寄存器的配置抽离出来,采用状态机思想去进行逻辑判断,可以有效地实现各种复杂的按键处理。借鉴这种思想,完成了自己的按键处理函数。这里直接上代码,再讲解。
#ifndef __KEY_H
#define __KEY_H
#include "sys.h"
/**********************************************************************/
#define KEY0_RCCclock RCC_AHB1Periph_GPIOE
#define KEY0_PinPort GPIOE
#define KEY0_WhichPin GPIO_Pin_2
#define KEY0_PinStatus GPIO_PuPd_UP //上拉
#define KEY0_shortPress Key0_ShortCallback
#define KEY0_longPress Key0_LongCallback
#define KEY1_RCCclock RCC_AHB1Periph_GPIOE
#define KEY1_PinPort GPIOE
#define KEY1_WhichPin GPIO_Pin_3
#define KEY1_PinStatus GPIO_PuPd_UP //上拉
#define KEY1_shortPress Key1_ShortCallback
#define KEY1_longPress Key1_LongCallback
/**********************************************************************/
#define KEY_MAXNUM 4 //最大按键数
#define KEY_TIMER_MS 2 //扫描时间间隔
#define KEY_DELAY_MS 10 //消抖完成标志位
#define KEY_PRESS_STATUS 0 //即按下标志位
#define KEY_LONG_STATUS (1000/KEY_DELAY_MS*3) //即按下3s及判定为长按
#define KEY_DOUBLE_HIT_MAX (100/KEY_DELAY_MS*3) //连击判定时间最大值为300ms
//#define KEY_DOUBLE_HIT_MIN (100/KEY_DELAY_MS*1) //连击判定事件最小值为100ms
/**********************************************************************/
#define KEY_NODOWN 0x0000 //无按键按下
#define KEY_DOWN 0x1000 //有按键按下
#define KEY_UP 0x2000 //按键短按标志位
#define KEY_LIAN 0x4000 //按键连按标志位
#define KEY_LONG 0x8000 //按键长按标志位
/**********************************************************************/
/*
这三个函数的作用分别是:
1、设置a某一位的值 G_SET_BIT
2、清楚a某一位的值 G_CLEAR_BIT
3、获得a某一位的值 G_IS_BIT_SET
*/
#define G_SET_BIT(a,b) (a |= (1 << b))
#define G_CLEAR_BIT(a,b) (a &= ~(1 << b))
#define G_IS_BIT_SET(a,b) (a & (1 << b))
/**********************************************************************/
//定义了一个回调指针,即根据发生的事件,
typedef void (*KeyCallback_Pointer) (void);
/**********************************************************************/
//单个按键对象结构体
__packed typedef struct
{
uint8_t Key_Num;//共有多少个按键对象
uint32_t Key_RccPeriphConfig;//按键对象时钟
GPIO_TypeDef* KeyPort;//按键所在IO口组
uint32_t Key_WhichPin;//第几个IO引脚
GPIOPuPd_TypeDef Key_PinStatus;//IO引脚的状态
KeyCallback_Pointer shortPress;//定义一个函数指针指向短按回调函数
KeyCallback_Pointer longPress;//定义一个函数指针指向长按回调函数
}keyTypeDef_t;//单个按键对象结构体!
/**********************************************************************/
//多个按键对象结构体(总)
__packed typedef struct
{
u8 KeyTotolNum; //按键总数累计
keyTypeDef_t* singleKey;//按键对象的指针!
}keysTypeDef_t;//多个按键对象结构体!
/**********************************************************************/
//双击枚举!
typedef enum {Keyd_Wait_Flag = 0,Keyd_End_Flag = 1,Keyd_IDLE_Flag = 2}keyd_Status;
//双击结构体!
typedef struct
{
keyd_Status Keyd_Flag;
uint16_t First_KeyVal;
uint16_t Key_Double_Hit_Count;
}Keyd_t;
/**********************************************************************/
extern u32 key_down;//声明外部变量,表示按键长短按及哪一个按键
extern keysTypeDef_t keys;
extern Keyd_t keyd;
void Key_Init(void);
void Key0_ShortCallback(void);
void Key0_LongCallback(void);
void Key1_ShortCallback(void);
void Key1_LongCallback(void);
void Key_Scan_TimeConfig(void);
uint16_t keyGet(keysTypeDef_t* keys_t);
void Key_Handle_Task(void);
#endif
最上面的和KEY0/KEY1处理有关的宏定义即是把底层抽离的方式之一,在移植的过程中,只需要修改宏定义即可完成对不同的IO口的初始化。而有关按键处理的函数我们并不需要去做处理,初始化按键处理后,只需要去判断 u32 key_down的不同的位,即可获得当前按键的各种状态。这里说一下key_down这个变量,我假定最多按键处理为4个,那么32位便以4分区,可以分成8各区域,那么每个区域即可标识不同的状态位,这里可以根据项目需求去做更改这个变量的不同位。
/
key_down 共有32位,这里把它分割成不同的区域:
0-3 : 预留区域,这里最多定义4个按键,哪个为1表示状态“绑定”在哪个按键上面
4-7 : 短按判断区,这里最多判断4个,哪个按键在触发短按事件,哪个位置1
8-11 : 长按判断区,这里最多判断4个,哪个按键在触发长按事件,哪个位置1
12-15 : 连击判断区,这里最多判断4个,哪个按键在触发连击事件,哪个位置1
/
从宏定义可以看出,这里是把按键这个事件当成一个类,类对应到单片机上每个按键IO口时,即为实例化了一个按键对象。我们需要几个按键,就去实例化几个IO口即可。每个对象都有两个函数指针,当对应状态产生时,即可调用初始化过程中的函数指针指向的函数。这里编程思想比较类似于C++,只是没有区分共有私有之类的数据部分。函数指针即为公共接口,需要自己去编写。这里我虽然在初始化时规定了指向,但是并没有编写接口(回调)函数,而是直接判断key_down 的相应位去判断按键状态。
#include "key.h"
#include "delay.h"
#include "stdio.h"
/*
key_down 共有32位,这里把它分割成不同的区域:
0-3 : 预留区域,这里最多定义4个按键,哪个为1表示状态“绑定”在哪个按键上面
4-7 : 短按判断区,这里最多判断4个,哪个按键在触发短按事件,哪个位置1
8-11 : 长按判断区,这里最多判断4个,哪个按键在触发长按事件,哪个位置1
12-15 : 连击判断区,这里最多判断4个,哪个按键在触发连击事件,哪个位置1
*/
u32 key_down = 0;//按键状态标志位,所以的操作都是为了改变这个全局变量
#define GPIO_KEY_NUM 2 //定义按键成员个数
keyTypeDef_t singKey[GPIO_KEY_NUM]; //定义单个按键成员数组指针
keysTypeDef_t keys; //定义总的按键模块结构
Keyd_t keyd; //双击结构体
uint8_t keyCountTime = 0;
uint16_t keyGet(keysTypeDef_t* keys_t)
{
uint8_t i = 0;
uint16_t readKey = 0;
//循环读取判断键值
for(i = 0;i < keys_t->KeyTotolNum ; i++) //初始化了几个按键,则扫描几次
{
if(KEY_PRESS_STATUS == GPIO_ReadInputDataBit(keys_t->singleKey[i].KeyPort,keys_t->singleKey[i].Key_WhichPin))
{
G_SET_BIT(readKey,keys_t->singleKey[i].Key_Num);//Key_Num即为每个按键对象的“序号”
}
}
return readKey;
}
//采用状态机思想,将按键形态分割成不同状态
uint16_t readKeyValue(keysTypeDef_t* keys_t)
{
static uint8_t keyCheck = 0;
static uint8_t keyState = 0;
static uint8_t keydCheck = 0; //双击补偿量,每点击一次按键,则重置为0,这个值过大则表示
static uint16_t keyLongCheck = 0; //长按事件检测标志位
static uint16_t keyPrev = 0; //上一次的键值
uint16_t keyPress = 0;
uint16_t keyReturn = 0;
keyCountTime += KEY_TIMER_MS; //每当进入一次中断,便自增2ms
if(keyCountTime >= KEY_DELAY_MS) //消抖完成
{
keyCountTime = 0;
keyCheck = 1;
}
if(1 == keyCheck) //即每10ms 进行一次按键读取,如果这一次判断按键按下,下一次按键同样为按下状态,则表示确实按下!
{
keyCheck = 0;
keyPress = keyGet(keys_t);//当对应按键按下时,16位对应位置,这里只用了两个,即仅判断第0位和第1位
switch(keyState)
{
case 0://按键未按下态
if(keyPress != 0)//表示有按键按下
{
keyPrev = keyPress; //记录当前按键状态
keyState = 1;
}
break;
case 1://表示有按键按下,判断当前值和上一次的值是否一样,若不一样则为抖动!
if(keyPress == keyPrev)//不是抖动
{
keydCheck = 0;
keyState = 2;
keyReturn = keyPrev | KEY_DOWN;
}else{
keyState = 0; //是抖动!返回上一层
}
case 2:
if(keyPress != keyPrev)//表示按键已松开,触发一次短按操作!
{
keyd.Keyd_Flag = Keyd_Wait_Flag;//开启双击检测标志位
}else{
keyLongCheck++;
if(keyLongCheck >= KEY_LONG_STATUS)//按下时间超过3s
{
keyLongCheck = 0;
keyState = 3;
keyReturn = keyPress | KEY_LONG;//返回值标记哪个按键
return keyReturn;
}
}
keydCheck += 1;
keyState = 1;//加入这个是为了当有按键按下时清零
break;
case 3:
if(keyPress != keyPrev)//一次按键扫描已经完成,等待按键松开
{
keyState = 0;
keydCheck = 0;
keyd.Keyd_Flag = Keyd_End_Flag;
keyd.Key_Double_Hit_Count = 0;
}
break;
}
if(keyd.Keyd_Flag == Keyd_Wait_Flag)
{
keyd.Key_Double_Hit_Count++;
if(keyd.Key_Double_Hit_Count >= KEY_DOUBLE_HIT_MAX) //超过300ms 出发了双击事件
{
keydCheck = 0;//重新清零双击计数
keyd.Key_Double_Hit_Count = 0;
keyd.Keyd_Flag = Keyd_End_Flag;
//超时则返回短按
keyState = 0;
keyLongCheck = 0;
keyReturn = keyPrev | KEY_LIAN; //标记一次短按成功
return keyReturn;
}
//这个判断即小于300ms区间内没有发生双击事件,keydCheck的值是一直在增加的而没有被清零
else if((keydCheck > 20) && (keyd.Key_Double_Hit_Count < KEY_DOUBLE_HIT_MAX))//表示触发了短按事件
{
keydCheck = 0;//重新清零双击计数
keyd.Keyd_Flag = Keyd_End_Flag;
keyd.Key_Double_Hit_Count = 0;
//超时则返回短按
keyState = 0;
keyLongCheck = 0;
keyReturn = keyPrev | KEY_UP; //标记一次短按成功
return keyReturn;
}
keyd.First_KeyVal = keyPrev;
}
}
return KEY_NODOWN;
}
//读取按键返回值,并作出相应的判断
void Key_Scan_2ms(keysTypeDef_t* keys_t)
{
uint8_t i = 0;
uint16_t key_value = 0;
key_value = readKeyValue(keys_t);
if(!key_value) return;
//短按事件触发
if(key_value & KEY_UP)
{
for(i = 0;i < keys_t->KeyTotolNum;i++)//循环扫描看是哪个按键按下
{
if(G_IS_BIT_SET(key_value,keys_t->singleKey[i].Key_Num))
{
G_SET_BIT(key_down,(keys_t->singleKey[i].Key_Num + 4)); // 4 5 位
//如果初始化指向了回调函数,
// if(keys_t->singleKey[i].shortPress)
// {
// keys_t->singleKey[i].shortPress();//执行相应的回调函数
// }
}
}
}
//长按事件触发
if(key_value & KEY_LONG)
{
for(i = 0;i < keys_t->KeyTotolNum;i++)
{ //判断是否产生长按事件
if(G_IS_BIT_SET(key_value,keys_t->singleKey[i].Key_Num))
{
G_SET_BIT(key_down,(keys_t->singleKey[i].Key_Num + 8)); // 8 9 位
// if(keys_t->singleKey[i].longPress)
// {
// keys_t->singleKey[i].longPress();
// }
}
}
}
//双击事件触发
if(key_value & KEY_LIAN)
{
for(i = 0;i < keys_t->KeyTotolNum;i++)
{//判断是否是双击事件
if(G_IS_BIT_SET(key_value,keys_t->singleKey[i].Key_Num))//判断第0位还是第1位是被置1的
{
G_SET_BIT(key_down,(keys_t->singleKey[i].Key_Num + 12)); // 12 13 位
}
}
}
}
/*
该函数为填充单个按键IO口状态函数,入口参数为:
1、按键IO时钟配置参数 Key_RccPeriphConfig
2、选择按键IO引脚组 KeyPort
3、选择第几个IO口 Key_WhichPin
4、选择IO口上下拉状态 Key_PinStatus
5、指向短回调函数 shortPress
6、指向长回调函数 longPress
*/
keyTypeDef_t KeyInit_One(uint32_t Key_RccPeriphConfig,GPIO_TypeDef* KeyPort,uint32_t Key_WhichPin,\
GPIOPuPd_TypeDef Key_PinStatus,KeyCallback_Pointer shortPress,KeyCallback_Pointer longPress)
{
static int8_t key_total = -1;
keyTypeDef_t Key_TemporaryVar;
Key_TemporaryVar.KeyPort = KeyPort;
Key_TemporaryVar.Key_Num = ++key_total;//标记了该组按键IO是第几个!
Key_TemporaryVar.Key_WhichPin = Key_WhichPin;
Key_TemporaryVar.Key_PinStatus = Key_PinStatus;
Key_TemporaryVar.Key_RccPeriphConfig = Key_RccPeriphConfig;
/*指向定义了的长短按回调函数!*/
Key_TemporaryVar.longPress = longPress;
Key_TemporaryVar.shortPress = shortPress;
keys.KeyTotolNum++;//在总按键函数中,记录当前装载按键IO个数!
return Key_TemporaryVar;
}
/*
按键初始化函数
KEY0 <--> PE2 上拉
KEY1 <--> PE3 上拉
KEY2 <--> PE4 上拉
KEY_UP <--> PA0 下拉
*/
void Key_PartInit(keysTypeDef_t *Keys)
{
uint8_t temp;
if(NULL == Keys)
{
return;
}
Keys->KeyTotolNum = (Keys->KeyTotolNum > KEY_MAXNUM) ? KEY_MAXNUM : Keys->KeyTotolNum;//限定个数!
for(temp = 0; temp < Keys->KeyTotolNum ; temp++)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_AHB1PeriphClockCmd(Keys->singleKey[temp].Key_RccPeriphConfig,ENABLE);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN;//普通输入模式
GPIO_InitStructure.GPIO_Pin = Keys->singleKey[temp].Key_WhichPin;
GPIO_InitStructure.GPIO_PuPd = Keys->singleKey[temp].Key_PinStatus;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;//100M
GPIO_Init(Keys->singleKey[temp].KeyPort,&GPIO_InitStructure);
}
Key_Scan_TimeConfig();
}
//2ms进入一次中断
void Key_Scan_TimeConfig(void)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE); ///使能TIM3时钟
TIM_TimeBaseInitStructure.TIM_Period = 2000 - 1; //自动重装载值
TIM_TimeBaseInitStructure.TIM_Prescaler=84 - 1; //定时器分频
TIM_TimeBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up; //向上计数模式
TIM_TimeBaseInitStructure.TIM_ClockDivision=TIM_CKD_DIV1;
TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStructure);//初始化TIM3
TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE); //允许定时器3更新中断
TIM_Cmd(TIM3,ENABLE); //使能定时器3
NVIC_InitStructure.NVIC_IRQChannel=TIM3_IRQn; //定时器3中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=0x01; //抢占优先级1
NVIC_InitStructure.NVIC_IRQChannelSubPriority=0x03; //子优先级3
NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
//每2ms进入一次中断
//定时器3中断服务函数
void TIM3_IRQHandler(void)
{
if(RESET != TIM_GetITStatus(TIM3,TIM_IT_Update)) //溢出中断
{
Key_Scan_2ms((keysTypeDef_t*)&keys);
TIM_ClearITPendingBit(TIM3,TIM_IT_Update); //清除中断标志位
}
}
/**********************************************************************/
//回调函数聚集区!!!~!~!
void Key0_ShortCallback(void)
{
//printf("KEY2短按触发\r\n");
}
void Key0_LongCallback(void)
{
//printf("KEY2长按触发\r\n");
}
void Key1_ShortCallback(void)
{
//printf("KEY1短按触发\r\n");
}
void Key1_LongCallback(void)
{
//printf("KEY1长按触发\r\n");
}
/**********************************************************************/
//就是先填充结构体,然后根据每个数组成员结构体的值去进行相应的初始化函数
void Key_Init(void)
{
singKey[0] = KeyInit_One(KEY0_RCCclock,KEY0_PinPort,KEY0_WhichPin,KEY0_PinStatus,Key0_ShortCallback,Key0_LongCallback);
singKey[1] = KeyInit_One(KEY1_RCCclock,KEY1_PinPort,KEY1_WhichPin,KEY1_PinStatus,Key1_ShortCallback,Key1_LongCallback);
keys.singleKey = (keyTypeDef_t*)&singKey;//指向第一个按键对象
Key_PartInit(&keys);
}
大体的思路是,首先定义了一个结构体数组,keyTypeDef_t singKey[GPIO_KEY_NUM];(GPIO_KEY_NUM的值即为需要初始化的按键个数),然后分别填充这个数组(这里其实直接去改宏定义,函数整体不用动)。最后把总按键处理结构体里的按键对象指针指向这个数组的首地址,即可通过一个结构体,去控制所有的按键对象。然后根据填充的数据对IO口进行初始化,开一个更新中断为2ms的定时器TIM3,以这个时间进行状态机的行为切换(其实是20ms,足够用了)。
有限状态机在逻辑性比较强的程序中非常有用,即通过对按键状态的判断,去切换不同的状态,最后返回相应的按键状态。
怎么确定当前状态是哪个按键呢?即通过调用keyGet()函数,返回一个u16类型的数据,每个按键对应自己的标号位(即KEY0对应第0位,KEY1对应第1位…)。readKeyValue()即根据u16类型的不同位,去进行状态机检测判断,直到返回一个确定的状态。再把一个u16类型的数据(标识了哪个按键和哪个状态)返回给Key_Scan_2ms()这个函数,在这个函数里进行对u16数据的解析,再把上面说的key_down置相应位。至此一个完整的按键处理已经完成,具体的逻辑判断见readKeyValue()这个函数(即状态机处理函数)。我们在外面只需要判断key_down的相应位即可判断是哪个按键发生了哪个事件。
程序其实有点绕,但是到真正移植的时候会发现,只需要改改宏,就能在不同的芯片,不同的设备上,轻松的完成复杂的按键处理。
下面上主程序:
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置系统中断优先级分组2
delay_init(168); //延时初始化
uart_init(115200); //串口初始化波特率为115200
LED_Init(); //初始化与LED连接的硬件接口
//初始化按键底层
Key_Init();
while(1)
{
if(G_IS_BIT_SET(key_down,4))
{
G_CLEAR_BIT(key_down,4);
printf("KEY2触发短按事件\r\n");
}
if(G_IS_BIT_SET(key_down,5))
{
G_CLEAR_BIT(key_down,5);
printf("KEY1触发短按事件\r\n");
}
if(G_IS_BIT_SET(key_down,8))
{
G_CLEAR_BIT(key_down,8);
printf("KEY2触发长按事件\r\n");
}
if(G_IS_BIT_SET(key_down,9))
{
G_CLEAR_BIT(key_down,9);
printf("KEY1触发长按事件\r\n");
}
if(G_IS_BIT_SET(key_down,12))
{
G_CLEAR_BIT(key_down,12);
printf("KEY2触发双击事件\r\n");
}
if(G_IS_BIT_SET(key_down,13))
{
G_CLEAR_BIT(key_down,13);
printf("KEY1触发双击事件\r\n");
}
}
}
当然组合按键也没有问题,只需要判断标志位是否同时存在即可,下面上个截图:
至此,一个按键模块就被做了出来,这个模块可以套用在任何stm32的芯片上,具备了很高的移植性,费点儿时间看明白,以后一劳永逸~