.外设:一个io口、一个定时器。
为了降低上手的门槛,本文仅使用一个IO口作演示。
实现思路
使用定时器,定时20ms来读取简化的按键状态机。
这里简化了状态机,大家只需明白三个概念。
状态:数量为有限个,记录按键状态。可根据条件切换。对应的代码中,用switch case来匹配不同的状态
条件:就是一些简单的判断。对应的代码,用if来判断按键按下或释放,判断时间计数的长短
事件:在特定的状态和条件下,产生的事件。代码只有三个事件:Null(空闲)SingleClick(短按)LongPress(长按)
思路图解:
3. 长按、单击 定义
长按事件:按下时间大于1s,释放后响应。(不支持连按,若需要连按,进行简单修改即可)
单击事件:按下时间小于1s, 释放后响应。
4.代码分析
4.1 外设准备
4.2 类型、变量定义
4.2.1按键事件
枚举型名称:KEY_EventList_TypeDef
本文实现短按长按功能,故只有三种事件:无事件、短按事件、长按事件
对应代码,即:空闲、单击、长按
typedef enum _KEY_EventList_TypeDef
{
KEY_Event_Null = 0x00, // 空闲
KEY_Event_SingleClick = 0x01, // 单击
KEY_Event_LongPress = 0x02 // 长按
}KEY_EventList_TypeDef;
4.2.2 按键电平、动作
因为有的电路按下时,引脚为低电平;有的按下时,引脚却为高电平。这里将电平、动作分开,更方便移植。
枚举型名称:KEY_PinLevel_TypeDef
即:高、低电平。
// 按键引脚的电平
typedef enum
{
KKEY_PinLevel_Low = 0,
KEY_PinLevel_High
}KEY_PinLevel_TypeDef;
枚举型名称:KEY_Action_TypeDef
按键只有按下和没按下俩个动作:
即:按下、释放
// 按键动作,
typedef enum
{
KEY_Action_Press = 0,
KEY_Action_Release
}KEY_Action_TypeDef;
4.2.3 按键状态
枚举型名称:KEY_StatusList_TypeDef
思路图解的分析,分为如下几个状态。
即:空闲、消抖、确认按下、确认长按
// 按键状态
typedef enum _KEY_StatusList_TypeDef
{
KEY_Status_Idle = 0, // 空闲
KEY_Status_Debounce , // 消抖
KEY_Status_ConfirmPress , // 确认按下
KEY_Status_ConfirmPressLong, // 确认长按
}KEY_StatusList_TypeDef;
4.2.4 按键配置结构体
按键配置信息的结构体名称:KEY_Configure_TypeDef
打包好按键的基本属性。
typedef struct _KEY_Configure_TypeDef
{
uint16_t KEY_Count; // 按键长按时长计数
KEY_Action_TypeDef KEY_Action; // 按键动作,按下或释放
KEY_StatusList_TypeDef KEY_Status; // 按键状态
KEY_EventList_TypeDef KEY_Event; // 按键事件
KEY_PinLevel_TypeDef (*KEY_ReadPin_Fcn)(void); // 读引脚电平函数
}KEY_Configure_TypeDef;
成员解释:
KEY_Count:计数,记一个数为20ms。
KEY_Action:按键动作,按下或者释放。
KEY_Status:记录按键的状态值。
KEY_Event:记录按键触发的事件。
KEY_ReadPin_Fcn:读取按键电平值的函数指针。方便移植。
4.3 变量、函数、宏定义
4.3.1宏定义
KEY_LONG_PRESS_TIME :
短按、长按的时长分界线。大于–>长按,小于–>短按。
KEY_PRESSED_LEVEL:
按键被按下的实际电平,我的电路里,按键按下引脚接地。所以为低电平。
/****************************************************************************************************
* 长按、单击 定义
* 长按事件:按下时间大于 KEY_LONG_PRESS_TIME,释放后响应。(不支持连按,需要连按响应可自己配置)
* 单击事件:按下时间小于 KEY_LONG_PRESS_TIME 释放后响应。
****************************************************************************************************/
// 长按时长的宏定义
#define KEY_LONG_PRESS_TIME 50 // 20ms*50 = 1s
#define KEY_PRESSED_LEVEL 0 // 按键被按下时的电平
4.3.2 变量定义
KeyCfg:全局变量,打包了按键的各个属性。
/****************************************************************************************************
* 按键配置信息的全局结构体变量
****************************************************************************************************/
KEY_Configure_TypeDef KeyCfg={
0, // 按键长按时长计数
KEY_Action_Release, // 按键动作,按下或释放
KEY_Status_Idle, // 按键状态
KEY_Event_Null, // 按键事件
KEY_ReadPin // 读引脚电平函数
};
4.3.3函数定义
局部函数:
//读取引脚的电平
static KEY_Action_TypeDef KEY_ReadPin(void)
{
return (KEY_Action_TypeDef)GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0);
}
// 获取按键动作,按下或释放,保存到结构体
static void KEY_GetAction(void)
{
if(KeyCfg.KEY_ReadPin_Fcn() == KEY_PRESSED_LEVEL)
{
KeyCfg.KEY_Action = KEY_Action_Press;
}
else
{
KeyCfg.KEY_Action = KEY_Action_Release;
}
}
KEY_ReadPin:读取PA_0的电平状态。
KEY_GetAction:将读取到的电平状态转换为实际的按下或释放的动作。
状态处理函数
KEY_ReadStateMachine:编写,思路完全按照前文手绘的思路图像
首先读取按键的动作,再在switch case 里面匹配引脚的状态,case下用if判断按键动作或按下的时长,对状态、事件进行赋值。
void KEY_ReadStateMachine(void)
{
KEY_GetAction();
switch(KeyCfg.KEY_Status)
{
//状态:没有按键按下
case KEY_Status_Idle:
if(KeyCfg.KEY_Action == KEY_Action_Press)
{
KeyCfg.KEY_Status = KEY_Status_Debounce;
KeyCfg.KEY_Event = KEY_Event_Null;
}
else
{
KeyCfg.KEY_Status = KEY_Status_Idle;
KeyCfg.KEY_Event = KEY_Event_Null;
}
break;
//状态:消抖
case KEY_Status_Debounce:
if(KeyCfg.KEY_Action == KEY_Action_Press)
{
KeyCfg.KEY_Status = KEY_Status_ConfirmPress;
KeyCfg.KEY_Event = KEY_Event_Null;
}
else
{
KeyCfg.KEY_Status = KEY_Status_Idle;
KeyCfg.KEY_Event = KEY_Event_Null;
}
break;
//状态:确认按下
case KEY_Status_ConfirmPress:
if( (KeyCfg.KEY_Action == KEY_Action_Press) && ( KeyCfg.KEY_Count >= KEY_LONG_PRESS_TIME))
{
printf("KEY_Status_ConfirmPressLong\r\n");
KeyCfg.KEY_Count = 0;
KeyCfg.KEY_Status = KEY_Status_ConfirmPressLong;
KeyCfg.KEY_Event = KEY_Event_Null;
}
else if( (KeyCfg.KEY_Action == KEY_Action_Press) && (KeyCfg.KEY_Count < KEY_LONG_PRESS_TIME))
{
printf("继续按下 %d\r\n",KeyCfg.KEY_Count);
KeyCfg.KEY_Count++;
KeyCfg.KEY_Status = KEY_Status_ConfirmPress;
KeyCfg.KEY_Event = KEY_Event_Null;
}
else
{
printf("突然gg,按短了 %d\r\n",KeyCfg.KEY_Count);
KeyCfg.KEY_Count = 0;
KeyCfg.KEY_Status = KEY_Status_Idle;
KeyCfg.KEY_Event = KEY_Event_SingleClick;
}
break;
//状态:确认长按
case KEY_Status_ConfirmPressLong:
printf("KEY_Status_ConfirmPressLong\r\n");
if(KeyCfg.KEY_Action == KEY_Action_Press)
{ // 一直等待其放开
printf("一直按着 KEY_Status_ConfirmPressLong\r\n");
KeyCfg.KEY_Status = KEY_Status_ConfirmPressLong;
KeyCfg.KEY_Event = KEY_Event_Null;
KeyCfg.KEY_Count = 0;
}
else
{
KeyCfg.KEY_Status = KEY_Status_Idle;
KeyCfg.KEY_Event = KEY_Event_LongPress;
KeyCfg.KEY_Count = 0;
}
break;
default:
break;
}
}
5.1定时器中断:
直接调用KEY_ReadStateMachine()函数即可,将读取到的事件保存到KeyCfg.KEY_Event变量。
extern KEY_Configure_TypeDef KeyCfg;
//定时器3中断服务程序
void TIM3_IRQHandler(void) //TIM3中断
{
if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) //检查TIM3更新中断发生与否
{
TIM_ClearITPendingBit(TIM3, TIM_IT_Update ); //清除TIMx更新中断标志
KEY_ReadStateMachine(); //调用状态机
if(KeyCfg.KEY_Event == KEY_Event_SingleClick)
{
printf("单击\r\n");//事件处理
}
if(KeyCfg.KEY_Event == KEY_Event_LongPress)
{
printf("长按\r\n");//事件处理
}
}
}
5.2 main函数
这里main函数十分简洁。初始化即可。
实际应用时,事件处理的代码不建议放在定时器里面。
int main(void)
{
uart_init(115200); // 用于查看输出
TIM3_Int_Init(200-1,7200-1); //调用定时器使得20ms产生一个中断
//按键初始化函数
KEY_Init();
while(1);
}