按键扫描是并不难实现的功能,但是想要优雅的处理按键消抖,以及实现按键的各种事件(按下响应应,松手响应,单击/双击响应,长按响应等),仍然需要花费一定的心思来实现.
本篇文章以第一视角解读使用开源按键库MultiButton,轻松实现上述按键事件。
开源地址放在了文章的最下方
在开始之前,要了解如何定义函数指针,以及如何调用函数指针
//定义了一个新的类型 BtnCallback,该类型的变量指向 返回值为void,成员变量为void*的函数
//后面讲到的回调函数基于此
typedef void (*BtnCallback)(void*);
//在核心逻辑函数中,有调用到一行代码
#define EVENT_CB(ev) if(handle->cb[ev])handle->cb[ev]((Button*)handle)
//简化 if( handle->cb[ev](handle) ) 当handle->cb[ev](handle) 不为空时,会调用该变量指向的函数
接下来正式开始讲解几个简单的函数:
MultiButton有两个核心文件multibutton_master.c和multibutton.h 我们以面向对象的思想来将按键用结构体表示
typedef struct Button {
uint16_t ticks;
uint8_t repeat : 4; //冒号后面的数字表示该变量占用几位空间
uint8_t event : 4;
uint8_t state : 3;
uint8_t debounce_cnt : 3; //去抖动次数
uint8_t active_level : 1; //有效点平(按下时点平状态)
uint8_t button_level : 1; //按键电平
uint8_t button_id; //按键id
uint8_t (*hal_button_Level)(uint8_t button_id_); //一个函数指针
BtnCallback cb[number_of_event]; //按键事件
struct Button* next; //按键使用链表存储
}Button;
按键事件
typedef enum {
PRESS_DOWN = 0, //按下
PRESS_UP, //松开
PRESS_REPEAT, //重复按下触发,可以记下按键连击数量
SINGLE_CLICK, //单击
DOUBLE_CLICK, //双击
LONG_PRESS_START, //长按达到阈值时触发
LONG_PRESS_HOLD, //长按一直触发
number_of_event, //事件
NONE_PRESS //无操作
}PressEvent;
正文开始,按键结构体看起来定义的很复杂,但是使用起来却很简单,我们只需要实现三个函数即可
/**
* @brief 初始化按键结构体变量handle
* @param handle: 按键结构体变量
* @param pin_level: 读取按键电平的函数(这里需要定义一个指定类型的函数来获取按键电平)
* @param active_level: 按键按下时的电平
* @param button_id: 自定义的按键ID
* @retval None
*/
void button_init(struct Button* handle, uint8_t(*pin_level)(uint8_t), uint8_t active_level, uint8_t button_id)
{
memset(handle, 0, sizeof(struct Button));
handle->event = (uint8_t)NONE_PRESS;
handle->hal_button_Level = pin_level;
handle->button_level = handle->hal_button_Level(button_id);
handle->active_level = active_level;
handle->button_id = button_id;
}
第二步:将当前按键与按键事件相关联
/**
* @brief Attach the button event callback function.
* @param handle: 定义的按键结构体
* @param event: 按键事件,想要一个按键触发多个事件时,可以多次调用该函数
* @param cb: 回调函数(需要人为定义)
* @retval None
*/
void button_attach(struct Button* handle, PressEvent event, BtnCallback cb)
{
handle->cb[event] = cb;
}
第三步:该按键已经初始化完成,我们需要将该按键加入到按键链表之中
/**
* @brief 将(Button*)handle 添加到链表头部
* @param handle: target handle struct.
* @retval 0: succeed. -1: already exist.
*/
int button_start(struct Button* handle)
{
struct Button* target = head_handle;
while(target) { //如果当前结构体已经在链表中存在,返回-1
if(target == handle) return -1; //already exist.
target = target->next;
}
handle->next = head_handle;
head_handle = handle;
return 0;
}
到这里,基本的步骤我们已经完成,只需要定时扫描链表即可
/**
* @brief background ticks, timer repeat invoking interval 5ms.
* 按键扫描,扫描时长由用户实现
* @param None.
* @retval None
*/
void button_ticks(void)
{
struct Button* target;
for(target=head_handle; target; target=target->next) {
button_handler(target); //核心逻辑实现,按键回调事件在该函数中调用(满足条件)
//实现逻辑并不复杂,我就不贴我写的注释了.只需要关注回调函数是如何实现的即可.
}
}
接下来看用户代码如何调用:
#define kb1_id 0 //按键ID
struct Button kb1; //定义按键结构体
/* 定义的按键电平读取函数 */
uint8_t read_button_GPIO(uint8_t button_id)
{
switch(button_id){
case kb1_id:
return HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0);
default:
return 1;
}
}
/* 按键注册,重点关注这里 */
void button_register()
{
button_init(&kb1,read_button_GPIO,0,kb1_id);
/* 注册单击回调事件 */
button_attach(&kb1,(PressEvent)SINGLE_CLICK,(BtnCallback)KB1_SingleHandler);
button_start(&kb1); //将该按键条件到按键链表之中
}
/* 按键回调事件 */
void KB1_SingleHandler(void *btn)
{
printf("按下kb1\r\n");
}
// 钩子函数,用于定时,每5ms进入一次按键扫描函数
/* 我在定时器中进行计时,每1ms调用1次该函数 */
void vbtn_timer_hook(void)
{
static u8 cnt = 0;
if(cnt++ == 5)
{
button_ticks();
cnt = 0;
}
}
/* Timer的定时中断回调 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
/*1ms定时器,每1ms执行一次 */
if(htim->Instance == TIM8)
{
extern void vbtn_timer_hook(void); //引用和调用钩子函数,这么写主要是为了接耦合
//但是不建议将具体的逻辑实现在中断中编写,这样会影响运行效率!!!!!!
//可以尝试,只在这里设置标志位,在while()中进行判断执行
vbtn_timer_hook();
}
/* USER CODE END Callback 1 */
}
到这里,每当点击一次KB1,就会执行一次KB1_SingleHandler()函数,耦合性很低,非常适合移植,感谢大大:https://github.com/0x1abin/MultiButton
点击直达
如果访问不了,备用链接百度网盘
提取码:3s5r