单片机状态机实现多个按键同时检测单击、多击、长按等操作
1.背景
在之前有个项目需要一个或多个按键检测:单击、双击、长按等操作
于是写了一份基于状态机的按键检测,分享一下思路
2.实现效果
单击翻转绿灯电平
双击翻转红灯电平
长按反转红绿灯电平
实现状态机检测按键单击,双击,长按等状态
3.代码实现
本代码是基于正点原子STM32F407ZGT6探索者开发板 HAL库写的
关于按键的代码可以直接移植,与芯片和HAL库没有多大联系,主要就是引脚定义是使用CubeMX生成的在main.h中,如下
#define BUTTON3_Pin GPIO_PIN_2
#define BUTTON3_GPIO_Port GPIOE
#define BUTTON2_Pin GPIO_PIN_3
#define BUTTON2_GPIO_Port GPIOE
#define BUTTON1_Pin GPIO_PIN_4
#define BUTTON1_GPIO_Port GPIOE
#define LED0_Pin GPIO_PIN_9
#define LED0_GPIO_Port GPIOF
#define LED1_Pin GPIO_PIN_10
#define LED1_GPIO_Port GPIOF
3.1 driver_button.c文件
#include "main.h"
#include "driver_boutton.h"
#define NUM_BUTTONS 3
#define DOUBLE_CLICK_TIME 200 // 双击最大间隔时间(ms)
#define LONG_PRESS_TIME 300 // 长按最小持续时间(ms)
void button_scan(void);
void button_init(void);
ButtonNum button_get_number(void);
// GPIO端口和PIN引脚数组
const GPIO_TypeDef* button_GPIO_Ports[NUM_BUTTONS] =
{
BUTTON1_GPIO_Port,BUTTON2_GPIO_Port, BUTTON3_GPIO_Port,
};
const uint16_t button_GPIO_Pins[NUM_BUTTONS] =
{
BUTTON1_Pin,BUTTON2_Pin, BUTTON3_Pin,
};
// 按键状态定义
typedef enum
{
BUTTON_RELEASED, //松开
BUTTON_PRESSED, //按下
BUTTON_SINGLE_CLICK, //单击
BUTTON_DOUBLE_CLICK, //双击
BUTTON_LONG_PRESS //长按
} Button_State;
// 按键结构体定义
typedef struct
{
GPIO_TypeDef *GPIOx;
uint16_t GPIO_PIN; // 按键连接的GPIO引脚
Button_State state; // 按键状态
uint32_t press_time; // 按下时间
uint32_t release_time; // 释放时间
uint8_t click_count; // 连续点击次数
uint32_t num; // 按键键值
} Button_TypeDef;
//按键函数指针
const Button_Handler *button = &(const Button_Handler)
{
.get_tick = HAL_GetTick, //获取系统时间滴答
.init = button_init, //按键初始化
.callback = button_scan, //按键扫描回调函数
.get_number = button_get_number, //获取键值
};
static Button_TypeDef buttons[NUM_BUTTONS];
static ButtonNum button_num = {0,0,0};
/**
* @简要 初始化按键配置
* @说明 该函数对每个按键的GPIO端口和引脚进行初始化,并将按键状态设置为未按下
* @参数 无
* @返回值 无
*/
void button_init(void)
{
for (int i = 0; i < NUM_BUTTONS; i++)
{
buttons[i].GPIOx = (GPIO_TypeDef*)button_GPIO_Ports[i];
buttons[i].GPIO_PIN = button_GPIO_Pins[i];
buttons[i].state = BUTTON_RELEASED;
buttons[i].click_count = 0;
buttons[i].num = 0x01 << i;
}
}
/**
* @简要 定时器扫描按键
* @说明 定时器消抖扫描并检测按键状态
* @参数 无
* @返回值 无
*/
void button_scan(void) {
uint32_t current_time = button->get_tick(); // 获取当前时间
for (int i = 0; i < NUM_BUTTONS; i++) //遍历所有按键
{
Button_TypeDef *button = &buttons[i];
uint8_t current_state = HAL_GPIO_ReadPin(button->GPIOx, button->GPIO_PIN); // 读取按键状态
if (current_state == 0) // 按键按下
{
if (button->state == BUTTON_RELEASED) // 如果之前是松开状态
{
button->press_time = current_time; // 记录按下时间
button->state = BUTTON_PRESSED; //更新按键状态为按下
}
}
else // 按键释放
{
if (button->state == BUTTON_PRESSED) // 如果之前是按下状态
{
button->release_time = current_time; // 记录释放时间
uint32_t press_duration = button->release_time - button->press_time; // 计算按下持续时间
if (press_duration >= LONG_PRESS_TIME) // 如果按下时间超过长按阈值
{
button->state = BUTTON_LONG_PRESS; // 更新状态为长按
button_num.more |= buttons[i].num; // 标记长按事件
}
else //如果按下时间在长按阈值范围内
{
button->click_count++; // 增加点击计数
}
// 复位按键状态
button->state = BUTTON_RELEASED;
}
}
if (button->click_count) // 如果有点击计数
{
// 距离下一次按下时间大于 DOUBLE_CLICK_TIME 可认为是单击
if (button->click_count == 1 && current_time - button->release_time > DOUBLE_CLICK_TIME)
{
button->click_count = 0; // 重置点击计数
button_num.once |= buttons[i].num; // 标记单击事件
}
// 否则 在 DOUBLE_CLICK_TIME 时间段内按几下算几连击
else if (button->click_count >= 2 && current_time - button->release_time > DOUBLE_CLICK_TIME)
{
button->click_count = 0; // 重置点击计数
button_num.twice |= buttons[i].num; // 标记双击事件
}
}
}
}
/**
* @简要 获取按键状态
* @说明 返回当前各类按键的键值
* @参数 无
* @返回值 按键的键值
*/
ButtonNum button_get_number(void)
{
ButtonNum temp = button_num;
button_num.once = 0;
button_num.twice = 0;
button_num.more = 0;
return temp;
}
3.2 driver_button.h文件
#ifndef __driver_button__
#define __driver_button__
#include <stdint.h>
#define BUTTON1_ONCE (0x01 << 0)
#define BUTTON2_ONCE (0x01 << 1)
#define BUTTON3_ONCE (0x01 << 2)
#define BUTTON1_TWICE (0x01 << 0)
#define BUTTON2_TWICE (0x01 << 1)
#define BUTTON3_TWICE (0x01 << 2)
#define BUTTON1_MORE (0x01 << 0)
#define BUTTON2_MORE (0x01 << 1)
#define BUTTON3_MORE (0x01 << 2)
typedef struct{
uint32_t once; //单击
uint32_t twice; //双击
uint32_t more; //长按
}ButtonNum;
extern ButtonNum button_num;
// 按键处理函数结构体定义
typedef struct {
uint32_t (*get_tick)(void); // 获取系统时间的函数指针
void (*init)(void); // 初始化函数指针
void (*callback)(void); // 回调函数指针
ButtonNum (*get_number)(void);
} Button_Handler;
extern const Button_Handler *button;
#endif
3.3 在定时器中断中 检测按键
这里我使用的是TIM6,每10ms扫描一次
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
static uint32_t timerCount_key = 0;
if(htim->Instance == TIM6)
{
timerCount_key++;
if(timerCount_key == 10)
{
timerCount_key = 0;
button->callback();
}
}
}
3.4 主函数中使用方法
这里使用按键控制led灯演示
/* USER CODE BEGIN 2 */
HAL_TIM_Base_Start_IT(&htim6);
button->init();
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
ButtonNum num = button->get_number();
if(num.twice == BUTTON1_TWICE) HAL_GPIO_TogglePin(LED0_GPIO_Port,LED0_Pin);
if(num.twice == BUTTON2_TWICE) HAL_GPIO_TogglePin(LED0_GPIO_Port,LED0_Pin);
if(num.twice == BUTTON3_TWICE) HAL_GPIO_TogglePin(LED0_GPIO_Port,LED0_Pin);
if(num.more == BUTTON1_MORE) HAL_GPIO_TogglePin(LED1_GPIO_Port,LED1_Pin),HAL_GPIO_TogglePin(LED0_GPIO_Port,LED0_Pin);
if(num.more == BUTTON2_MORE) HAL_GPIO_TogglePin(LED1_GPIO_Port,LED1_Pin),HAL_GPIO_TogglePin(LED0_GPIO_Port,LED0_Pin);
if(num.more == BUTTON3_MORE) HAL_GPIO_TogglePin(LED1_GPIO_Port,LED1_Pin),HAL_GPIO_TogglePin(LED0_GPIO_Port,LED0_Pin);
if(num.once == BUTTON1_ONCE) HAL_GPIO_TogglePin(LED1_GPIO_Port,LED1_Pin);
if(num.once == BUTTON2_ONCE) HAL_GPIO_TogglePin(LED1_GPIO_Port,LED1_Pin);
if(num.once == BUTTON3_ONCE) HAL_GPIO_TogglePin(LED1_GPIO_Port,LED1_Pin);
}
/* USER CODE END 3 */
4.按键状态机思路
void button_scan(void)
主要思路是这样:
我每次定时器执行这个按键扫描的回调函数,都会轮询判断一下所有的按键状态。
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
static uint32_t timerCount_key = 0;
if(htim->Instance == TIM6)
{
timerCount_key++;
if(timerCount_key == 10)
{
timerCount_key = 0;
button->callback();
}
}
}
例如在此之前我从来没按下过按键,当我的按键1按下的时刻,
uint8_t current_state = HAL_GPIO_ReadPin(button->GPIOx, button->GPIO_PIN); // 读取按键状态
current_state被返回了低电平(取决于你的电路设计,我这里按键按下接地)
然后就会进入到
if (current_state == 0) // 按键按下
{
if (button->state == BUTTON_RELEASED) // 如果之前是松开状态
{
button->press_time = current_time; // 记录按下时间
button->state = BUTTON_PRESSED; // 更新按键状态为按下
}
}
在这里由于我们是第一次按下会被标记为状态为按下,然后将你的结构体中的按下时间记录为这一次扫描按键时的HAL_GetTick();
然后你按下按键是需要松手的吧
现在你松手了,接上面的if语句:
else // 按键释放
{
if (button->state == BUTTON_PRESSED) // 如果之前是按下状态
{
button->release_time = current_time; // 记录释放时间
uint32_t press_duration = button->release_time - button->press_time; // 计算按下持续时间
if (press_duration >= LONG_PRESS_TIME) // 如果按下时间超过长按阈值
{
button->state = BUTTON_LONG_PRESS; // 更新状态为长按
button_num.more |= buttons[i].num; // 标记长按事件
}
else // 如果按下时间在长按阈值范围内
{
button->click_count++; // 增加点击计数
}
// 复位按键状态
button->state = BUTTON_RELEASED;
}
}
松手之后(按键释放,那么按键又被上拉到高电平了),这里先判断一下你之前的状态,必须要判断一下这个按键之前是不是被按下了,要不然就会一直进入这个if语句。
由于每次进入这个按键扫描函数都会记录一下HAL_GetTick();,
uint32_t press_duration = button->release_time - button->press_time; // 计算按下持续时间
所以记下了你上次按下按键与这次松开按键的时间间隔,那么这就可以得出你的按下时间,如果超过了长按阈值那么肯定就是长按状态了,就执行对应的长按操作。
如果你的时间间隔少于长按的时间阈值,那么就会给你增加一次点击计数。
之后你松开了按键那么可能要把按键的状态恢复到初始化的情况。
这时这个函数还没有结束,接下来会进入到这个if语句:
if (button->click_count) // 如果有点击计数
{
// 距离下一次按下时间大于 DOUBLE_CLICK_TIME 可认为是单击
if (button->click_count == 1 && current_time - button->release_time > DOUBLE_CLICK_TIME)
{
button->click_count = 0; // 重置点击计数
button_num.once |= buttons[i].num; // 标记单击事件
}
// 否则 在 DOUBLE_CLICK_TIME 时间段内按几下算几连击
else if (button->click_count >= 2 && current_time - button->release_time > DOUBLE_CLICK_TIME)
{
button->click_count = 0; // 重置点击计数
button_num.twice |= buttons[i].num; // 标记双击事件
}
}
如果你按下按键的时间低于长按的时间阈值的话,那么就会进入这个函数,否则直接跳过这个if语句。
例如,这个时候从头到尾,你只按了一次低于长按时间阈值的操作,暂停时间分析:
再进入这个if语句:
if (button->click_count == 1 && current_time - button->release_time > DOUBLE_CLICK_TIME)
{
button->click_count = 0; // 重置点击计数
button_num.once |= buttons[i].num; // 标记单击事件
}
这里判断你的点击次数为1,但是当前你按下到松手后时间还没有超过双击的时间阈值,那么
current_time - button->release_time > DOUBLE_CLICK_TIME
就是false,if语句就进不去,但是如果时间再过去一点,
current_time - button->release_time > DOUBLE_CLICK_TIME
就是true,时间超过了双击的阈值,所以直接判断为单击。
再回到:例如,这个时候从头到尾,你只按了一次低于长按时间阈值的操作,时间暂停分析
接着上面的if判断:
if (button->click_count == 1 && current_time - button->release_time > DOUBLE_CLICK_TIME)
{
button->click_count = 0; // 重置点击计数
button_num.once |= buttons[i].num; // 标记单击事件
}
目前你还没有超过双击的时间阈值
紧接着你又按下了一次按键,并且这一次按下时间同样低于双击的阈值,那么就会继续增加的点击计数
直到本次按键的时间间隔大于双击的阈值,则判断结束,可以返回按键的点击次数了
5.结束
目前代码能够正常检测单击,双击,长按等操作,如果读者使用此代码发现有什么bug,或者值得优化的地方,欢迎评论区留言!