Flexible Button 组件移植
本文参考 Flexible Button 官方说明:hellozimo/FlexibleButton: 灵活的按键处理库(Flexible Button)| 按键驱动 | 支持单击、双击、连击、长按、自动消抖 | 灵活适配中断和低功耗 | 按需实现组合按键
Flexible Button 简介
FlexibleButton 是一个基于标准 C 语言的小巧灵活的按键处理库,支持单击、连击、短按、长按、自动消抖,可以自由设置组合按键,可用于中断和低功耗场景。
该按键库解耦了具体的按键硬件结构,理论上支持轻触按键与自锁按键,并可以无限扩展按键数量。另外,FlexibleButton 使用扫描的方式一次性读取所有所有的按键状态,然后通过事件回调机制上报按键事件。核心的按键扫描代码仅有三行,没错,就是经典的 三行按键扫描算法。使用 C 语言标准库 API 编写,也使得该按键库可以无缝兼容任意的处理器平台,并且支持任意 OS 和 non-OS(裸机编程)。
移植
将FlexibleButton移植到STM32、ESP32等芯片上,核心就是为这个库提供“硬件读取”和“定时扫描”这两个基础服务。它本身是标准C语言写的,与硬件解耦得很好,所以移植过程非常清晰,可以归纳为四个步骤:
1 获取源码
直接从GitHub仓库克隆或下载,或者通过RT-Thread的包管理器获取。工程中只需要flexible_button.c和flexible_button.h这两个核心文件即可。
Git 方式,可以看read me 文档,介绍的很详细了
git clone https://github.com/murphyzhao/FlexibleButton.git
RT-Thread menuconfig 方式
RT-Thread online packages --->
miscellaneous packages --->
[*] FlexibleButton: Small and flexible button driver --->
[*] Enable flexible button demo
version (latest) --->
配置完成后,输入 pkgs --update 下载软件包。

2 实现“硬件读取函数”
这是最关键的一步。需要编写一个函数,它的唯一任务就是读取指定STM32等芯片引脚的当前电平状态(高/低) ,并将这个状态返回给库。
函数原型是:uint8_t user_button_read(void *usr_data)
-
使用HAL库:函数内部调用HAL_GPIO_ReadPin()。
-
使用标准库:函数内部直接操作寄存器,比如GPIO_ReadInputDataBit()。
-
根据按键硬件连接(是上拉还是下拉),确保返回的“按下”和“释放”状态逻辑正确。
最简单的返回函数示例:
uint8_t user_button_read(void *usr_data) { return HAL_GPIO_ReadPin(KEY_PORT,KEY_PIN); }
3 创建“按键扫描任务”
FlexibleButton需要被周期性地调用,来检测按键状态的变化。需要提供一个固定的时间基准(tick)来驱动它。
- 在裸机工程中:将flex_button_scan()函数放在一个定时器中断里,或者直接放在主循环的while(1)中(配合delay)。
- 在RT-Thread等RTOS中:可以创建一个独立的线程,在线程的while(1)循环中,以固定周期(如10ms、20ms)调用flex_button_scan()函数。
flex_button_scan()里包含了经典的三行按键扫描算法,非常高效,不用担心它会拖慢你的系统。
注意改变调用周期要同步更新配置:比如调用周期20ms,对应频率就是 50Hz

void user_button_task(void *arg)
{
while (1)
{
flex_button_scan();
vTaskDelay(pdMS_TO_TICKS(20));
}
}
4 初始化与注册
在你的应用初始化代码中,完成最后的配置工作:
- 定义一个flex_button_t结构体数组,用来管理所有的按键。
- 配置每个按键的参数:按键ID(0开始)、编写的硬件读取函数、回调函数、按下的逻辑电平、以及短按/长按/长按不放的触发时间(tick数)。
- 实现事件回调函数:这个函数会在按键事件(如单击、双击、长按)发生时被库调用,你在这里编写具体的应用逻辑(例如,单击点亮LED)。
- 对每个按键调用flex_button_register()函数,将它注册到库中。
代码示例框架
#include "flexible_button.h"
// 1. 定义按键枚举
typedef enum {
USER_BUTTON_0,
USER_BUTTON_MAX
} user_button_t;
// 2. 定义按键数据结构
static flex_button_t user_button[USER_BUTTON_MAX];
// 3. 硬件读取函数
static uint8_t common_btn_read(void *usr_data)
{
// 这里根据传入的usr_data(可以指向GPIO句柄或引脚号),
// 调用HAL库或标准库函数读取引脚电平并返回。
// 返回值: 0 或 1
return HAL_GPIO_ReadPin(GPIOx, GPIO_PIN_x);
}
// 4. 按键事件回调函数
static void common_btn_evt_cb(flex_button_t *btn)
{
switch (btn->event) {
case FLEX_BTN_PRESS_CLICK:
// 单击处理
break;
case FLEX_BTN_PRESS_LONG_START:
// 长按开始处理
break;
// ... 处理其他事件
default:
break;
}
}
// 5. 按键初始化函数
static void user_button_init(void)
{
// 初始化硬件引脚为输入模式...
// ...
// 配置每个按键
for (int i = 0; i < USER_BUTTON_MAX; i++) {
user_button[i].id = i;
user_button[i].usr_button_read = common_btn_read;
user_button[i].cb = common_btn_evt_cb;
user_button[i].pressed_logic_level = 0; // 低电平有效
user_button[i].short_press_start_tick = FLEX_MS_TO_SCAN_CNT(1500);
user_button[i].long_press_start_tick = FLEX_MS_TO_SCAN_CNT(3000);
// 调用注册函数
flex_button_register(&user_button[i]);
}
}
// 6. 在main函数或某个任务中,周期性调用
// while (1) {
// flex_button_scan();
// rt_thread_mdelay(10); // 如果使用RTOS
// // 或者 HAL_Delay(10); // 如果在裸机主循环
// }
Flexible Button组件移植到ESP32-IDF
前置步骤
下载源码,复制到esp32,将源文件编译进工程

移植步骤
1、实现硬件读取函数,不要在初始化函数完成按键 GPIO初始化配置
static uint8_t user_button_read(void *usr_data)
{
return gpio_get_level(BUTTON_PIN_1);
}
2、创建按键扫描任务,周期调用 flex_button_scan();
void user_button_task(void *arg)
{
while (1)
{
flex_button_scan();
vTaskDelay(pdMS_TO_TICKS(20));
}
}
3、初始化按键与注册:
完成定义前置变量:
/*flexible组件移植*/
#include "flexible_button.h"
/*-----------------------------------------typedef---------------------------------------*/
typedef enum
{
USER_BUTTON_0 = 0,
USER_BUTTON_MAX
} user_button_t; //定义按键id枚举,每个按键对应一个编号,这里只有一个button 0
/*-----------------------------------------define---------------------------------------*/
#define BUTTON_PIN_1 GPIO_NUM_39 //按键输入引脚
/*-----------------------------------------static变量---------------------------------------*/
static flex_button_t user_button[USER_BUTTON_MAX]; //定义一个flex_button_t结构体数组,用来管理所有的按键。
/*-----------------------------------------函数声明---------------------------------------*/
static void user_button_task(void *arg);
static void user_button_init(void);
static void user_btn_evt_cb(void *arg);
static uint8_t user_button_read(void *usr_data);
/*不是必需,用于打印信息*/
static char *enum_event_string[] = {
ENUM_TO_STR(FLEX_BTN_PRESS_DOWN),
ENUM_TO_STR(FLEX_BTN_PRESS_CLICK),
ENUM_TO_STR(FLEX_BTN_PRESS_DOUBLE_CLICK),
ENUM_TO_STR(FLEX_BTN_PRESS_REPEAT_CLICK),
ENUM_TO_STR(FLEX_BTN_PRESS_SHORT_START),
ENUM_TO_STR(FLEX_BTN_PRESS_SHORT_UP),
ENUM_TO_STR(FLEX_BTN_PRESS_LONG_START),
ENUM_TO_STR(FLEX_BTN_PRESS_LONG_UP),
ENUM_TO_STR(FLEX_BTN_PRESS_LONG_HOLD),
ENUM_TO_STR(FLEX_BTN_PRESS_LONG_HOLD_UP),
ENUM_TO_STR(FLEX_BTN_PRESS_MAX),
ENUM_TO_STR(FLEX_BTN_PRESS_NONE),
};
static char *enum_btn_id_string[] = {
ENUM_TO_STR(USER_BUTTON_0),
ENUM_TO_STR(USER_BUTTON_MAX),
};
实现事件回调处理函数:
static void user_btn_evt_cb(void *arg)
{
flex_button_t *btn = (flex_button_t *)arg;
ESP_LOGI("BUTTON","id: [%d - %s] event: [%d - %30s] repeat: %d\n",
btn->id, enum_btn_id_string[btn->id],
btn->event, enum_event_string[btn->event],
btn->click_cnt); //打印当前按键触发事件的信息
//使用flex_button_event_read获取当前按键触发的事件,处理对应逻辑
switch (flex_button_event_read(&user_button[USER_BUTTON_0]))
{
case FLEX_BTN_PRESS_CLICK: //点击事件
ESP_LOGI("BUTTON_EVENT","button 0 press click\n");
break;
case FLEX_BTN_PRESS_SHORT_UP: //短按释放事件
ESP_LOGI("BUTTON_EVENT","button 0 press short click up\n");
break;
case FLEX_BTN_PRESS_LONG_UP: //长按释放事件
ESP_LOGI("BUTTON_EVENT","button 0 press long click up\n");
break;
default:
break;
}
}
实现初始化函数:
static void user_button_init(void)
{
/*初始化GPIO*/
gpio_config_t button_cfg = {
.pin_bit_mask = (1ULL<<BUTTON_PIN_1),
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE
};
gpio_config(&button_cfg);
/*flexible button 组件初始化*/
//注册button结构体,这里定义了USER_BUTTON_MAX 个按键类型
for(int i = 0; i < USER_BUTTON_MAX; i++) //依次注册每个按键
{
user_button[0].id = i; //按键编号
user_button[0].usr_button_read = user_button_read; //按键读取函数
user_button[0].cb = &user_btn_evt_cb; //事件回调函数
user_button[i].pressed_logic_level = 0; //按下按键时的逻辑电平
user_button[i].short_press_start_tick = FLEX_MS_TO_SCAN_CNT(1000); //按键识别为短按开始的时间 ms
user_button[i].long_press_start_tick = FLEX_MS_TO_SCAN_CNT(3000); //按键识别为长按开始的时间 ms
user_button[i].long_hold_start_tick = FLEX_MS_TO_SCAN_CNT(4500); //按键识别为超长按的时间 ms
flex_button_register(&user_button[i]); //按键注册函数
}
}
完整源码
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "esp_log.h"
/*flexible组件移植*/
#include "flexible_button.h"
/*-----------------------------------------typedef---------------------------------------*/
typedef enum
{
USER_BUTTON_0 = 0,
USER_BUTTON_MAX
} user_button_t;
/*-----------------------------------------define---------------------------------------*/
#define BUTTON_PIN_1 GPIO_NUM_39
#define ENUM_TO_STR(e) (#e) // 是预处理器的字符串化运算符,它会将宏参数 e 替换为对应的字符串字面量
/*-----------------------------------------static变量---------------------------------------*/
static flex_button_t user_button[USER_BUTTON_MAX];
//非必需,打印信息用
static char *enum_event_string[] = {
ENUM_TO_STR(FLEX_BTN_PRESS_DOWN),
ENUM_TO_STR(FLEX_BTN_PRESS_CLICK),
ENUM_TO_STR(FLEX_BTN_PRESS_DOUBLE_CLICK),
ENUM_TO_STR(FLEX_BTN_PRESS_REPEAT_CLICK),
ENUM_TO_STR(FLEX_BTN_PRESS_SHORT_START),
ENUM_TO_STR(FLEX_BTN_PRESS_SHORT_UP),
ENUM_TO_STR(FLEX_BTN_PRESS_LONG_START),
ENUM_TO_STR(FLEX_BTN_PRESS_LONG_UP),
ENUM_TO_STR(FLEX_BTN_PRESS_LONG_HOLD),
ENUM_TO_STR(FLEX_BTN_PRESS_LONG_HOLD_UP),
ENUM_TO_STR(FLEX_BTN_PRESS_MAX),
ENUM_TO_STR(FLEX_BTN_PRESS_NONE),
};
//非必需,打印信息用
static char *enum_btn_id_string[] = {
ENUM_TO_STR(USER_BUTTON_0),
ENUM_TO_STR(USER_BUTTON_MAX),
};
/*-----------------------------------------函数声明---------------------------------------*/
static void user_button_task(void *arg);
static void user_button_init(void);
static void user_btn_evt_cb(void *arg);
static uint8_t user_button_read(void *usr_data);
void app_main(void)
{
xTaskCreatePinnedToCore(user_button_task,"button_task",2048,NULL,1,NULL,1);
while(1)
{
vTaskDelay(pdMS_TO_TICKS(50));//让出CPU使用权
}
}
static void user_button_init(void)
{
/*初始化GPIO*/
gpio_config_t button_cfg = {
.pin_bit_mask = (1ULL<<BUTTON_PIN_1),
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE
};
gpio_config(&button_cfg);
/*flexible button 组件初始化*/
//注册button结构体,这里定义了USER_BUTTON_MAX 个按键类型
for(int i = 0; i < USER_BUTTON_MAX; i++)
{
user_button[0].id = i;
user_button[0].usr_button_read = user_button_read;
user_button[0].cb = &user_btn_evt_cb;
user_button[i].pressed_logic_level = 0;
user_button[i].short_press_start_tick = FLEX_MS_TO_SCAN_CNT(1000);
user_button[i].long_press_start_tick = FLEX_MS_TO_SCAN_CNT(3000);
user_button[i].long_hold_start_tick = FLEX_MS_TO_SCAN_CNT(4500);
flex_button_register(&user_button[i]);
}
}
static void user_btn_evt_cb(void *arg)
{
flex_button_t *btn = (flex_button_t *)arg;
ESP_LOGI("BUTTON","id: [%d - %s] event: [%d - %30s] repeat: %d\n",
btn->id, enum_btn_id_string[btn->id],
btn->event, enum_event_string[btn->event],
btn->click_cnt);
switch (flex_button_event_read(&user_button[USER_BUTTON_0]))
{
case FLEX_BTN_PRESS_CLICK:
ESP_LOGI("BUTTON_EVENT","button 0 press click\n");
break;
case FLEX_BTN_PRESS_SHORT_UP:
ESP_LOGI("BUTTON_EVENT","button 0 press short click up\n");
break;
case FLEX_BTN_PRESS_LONG_UP:
ESP_LOGI("BUTTON_EVENT","button 0 press long click up\n");
break;
default:
break;
}
}
static uint8_t user_button_read(void *usr_data)
{
return gpio_get_level(BUTTON_PIN_1);
}
void user_button_task(void *arg)
{
user_button_init();
while (1)
{
flex_button_scan();
vTaskDelay(pdMS_TO_TICKS(20));
}
}
注意事项
1、关于 tick 数
short_press_start_tick、long_press_start_tick 和 long_hold_start_tick 必须使用 FLEX_MS_TO_SCAN_CNT 将毫秒时间转化为扫描次数。
user_button[i].short_press_start_tick = FLEX_MS_TO_SCAN_CNT(1500); 表示按键按下开始计时,1500 ms 后按键依旧是按下状态的话,就断定为短按开始。
2、关于组合按键 逻辑实现:判断对应按键事件是否同时触发
原理:该组件一次扫描可以确定所有的按键状态,并上报对应的按键事件
if ((flex_button_event_read(&user_button[USER_BUTTON_0]) == FLEX_BTN_PRESS_CLICK) &&
(flex_button_event_read(&user_button[USER_BUTTON_1]) == FLEX_BTN_PRESS_CLICK))
{
rt_kprintf("[combination]: button 0 and button 1\n");
}
3、事件类型
typedef enum
{
FLEX_BTN_PRESS_DOWN = 0, // 按下事件
FLEX_BTN_PRESS_CLICK, // 单击事件
FLEX_BTN_PRESS_DOUBLE_CLICK, // 双击事件
FLEX_BTN_PRESS_REPEAT_CLICK, // 连击事件,使用 flex_button_t 中的 click_cnt 断定连击次数
FLEX_BTN_PRESS_SHORT_START, // 短按开始事件
FLEX_BTN_PRESS_SHORT_UP, // 短按抬起事件
FLEX_BTN_PRESS_LONG_START, // 长按开始事件
FLEX_BTN_PRESS_LONG_UP, // 长按抬起事件
FLEX_BTN_PRESS_LONG_HOLD, // 长按保持事件
FLEX_BTN_PRESS_LONG_HOLD_UP, // 长按保持的抬起事件
FLEX_BTN_PRESS_MAX,
FLEX_BTN_PRESS_NONE,
} flex_button_event_t;
4、按键结构体:

最终效果

以下 FexibleButton 代码说明来自官方的readme文档说明,链接:
hellozimo/FlexibleButton: 灵活的按键处理库(Flexible Button)| 按键驱动 | 支持单击、双击、连击、长按、自动消抖 | 灵活适配中断和低功耗 | 按需实现组合按键
FexibleButton 代码说明
按键事件定义
按键事件的定义并没有使用 Windows 驱动上的定义,主要是方便嵌入式设备中的应用场景(也可能是我理解的偏差),按键事件定义如下:
typedef enum
{
FLEX_BTN_PRESS_DOWN = 0, // 按下事件
FLEX_BTN_PRESS_CLICK, // 单击事件
FLEX_BTN_PRESS_DOUBLE_CLICK, // 双击事件
FLEX_BTN_PRESS_REPEAT_CLICK, // 连击事件,使用 flex_button_t 中的 click_cnt 断定连击次数
FLEX_BTN_PRESS_SHORT_START, // 短按开始事件
FLEX_BTN_PRESS_SHORT_UP, // 短按抬起事件
FLEX_BTN_PRESS_LONG_START, // 长按开始事件
FLEX_BTN_PRESS_LONG_UP, // 长按抬起事件
FLEX_BTN_PRESS_LONG_HOLD, // 长按保持事件
FLEX_BTN_PRESS_LONG_HOLD_UP, // 长按保持的抬起事件
FLEX_BTN_PRESS_MAX,
FLEX_BTN_PRESS_NONE,
} flex_button_event_t;
其中 FLEX_BTN_PRESS_LONG_HOLD 事件可以用来实现长按累加的应用场景。
按键数据结构
typedef struct flex_button
{
struct flex_button* next;
uint8_t (*usr_button_read)(void *);
flex_button_response_callback cb;
uint16_t scan_cnt;
uint16_t click_cnt;
uint16_t max_multiple_clicks_interval;
uint16_t debounce_tick;
uint16_t short_press_start_tick;
uint16_t long_press_start_tick;
uint16_t long_hold_start_tick;
uint8_t id;
uint8_t pressed_logic_level : 1;
uint8_t event : 4;
uint8_t status : 3;
} flex_button_t;
| 序号 | 数据成员 | 是否需要用户初始化 | 说明 |
|---|---|---|---|
| 1 | next | 否 | 按键库使用单向链表串起所有的按键 |
| 2 | usr_button_read | 是 | 用户设备的按键引脚电平读取函数,重要 |
| 3 | cb | 是 | 设置按键事件回调,用于应用层对按键事件的分类处理 |
| 4 | scan_cnt | 否 | 用于记录扫描次数,按键按下是开始从零计数 |
| 5 | click_cnt | 否 | 记录单击次数,用于判定单击、连击 |
| 6 | max_multiple_clicks_interval | 是 | 连击间隙,用于判定是否结束连击计数,有默认值 MAX_MULTIPLE_CLICKS_INTERVAL |
| 7 | debounce_tick | 否 | 消抖时间,暂未使用,依靠扫描间隙进行消抖 |
| 8 | short_press_start_tick | 是 | 设置短按事件触发的起始 tick |
| 9 | long_press_start_tick | 是 | 设置长按事件触发的起始 tick |
| 10 | long_hold_start_tick | 是 | 设置长按保持事件触发的起始 tick |
| 11 | id | 是 | 当多个按键使用同一个回调函数时,用于断定属于哪个按键 |
| 12 | pressed_logic_level | 是 | 设置按键按下的逻辑电平。1:标识按键按下的时候为高电平;0:标识按键按下的时候未低电平,重要 |
| 13 | event | 否 | 用于记录当前按键事件 |
| 14 | status | 否 | 用于记录当前按键的状态,用于内部状态机 |
注意,在使用 max_multiple_clicks_interval、debounce_tick、short_press_start_tick、long_press_start_tick、long_hold_start_tick 的时候,注意需要使用宏 **FLEX_MS_TO_SCAN_CNT(ms)** 将毫秒值转换为扫描次数。因为按键库基于扫描次数运转。示例如下:
user_button[1].short_press_start_tick = FLEX_MS_TO_SCAN_CNT(1500); // 1500 毫秒
上述代码表示:表示按键按下后开始计时,1500ms 的时候,按键依旧按下,则断定为短按开始,并上报 FLEX_BTN_PRESS_SHORT_START 事件。
按键注册接口
使用该接口注册一个用户按键,入参为一个 flex_button_t 结构体实例的地址。
int8_t flex_button_register(flex_button_t *button);
按键事件读取接口
使用该接口获取指定按键的事件。
flex_button_event_t flex_button_event_read(flex_button_t* button);
按键扫描接口
按键扫描的核心函数,需要放到应用程序中定时扫描,扫描间隔建议 20 毫秒。
void flex_button_scan(void);
注意事项
- 阻塞问题
因为按键事件回调函数以及按键键值读取函数是在按键扫描的过程中执行的,因此请不要在这类函数中使用阻塞接口,不要进行延时操作。 - 按键扫描函数栈需求
按键扫描函数本身对栈的需求小于 300 字节,但是按键事件回调函数和按键键值读取函数都是在按键扫描函数的上下文中执行的,请格外关心按键事件回调函数与按键键值读取函数对栈空间的需求。
其它
关于低功耗
本按键库是通过不间断扫描的方式来检查按键状态,因此会一直占用 CPU 资源,这对低功耗应用场景是不友好的。为了降低正常工作模式下的功耗,建议合理配置扫描周期(5ms - 20ms),扫描间隙里 CPU 可以进入轻度睡眠。
该按键库不在底层实现低功耗处理,应用层可以根据自己的功耗模式灵活处理,通常会有以下两种方式:
- 进入低功耗前,挂起按键扫描线程;退出低功耗后,唤醒按键扫描。
- 增加按键中断模式,所有的按键中断来,就触发一次按键扫描,以确认所有的按键状态。
低功耗相关的探讨参考 issue 1 中的讨论。
关于按键中断模式
由于该按键库一次扫描可以确定所有的按键状态,因此可以将所有的按键中断通过 “或” 的方式转化为一个中断,然后在中断处理函数中执行一次按键扫描。
中断 “或” 的方式可以通过硬件来完成,也可以通过软件来完成。
硬件方式,需要使用一个 或门 芯片,多个输入条件转化为一个输出条件,然后通过一个外部中断即可完成所有按键的中断方式检测。
软件方式,需要为每一个按键配置为中断触发模式,然后在每一个按键中断的中断处理函数中执行按键扫描。
为了在降低中断处理函数中执行按键扫描带来的时延,可以通过信号量的方式来异步处理,仅在中断处理函数中释放一个按键扫描的信号量,然后在按键扫描线程中监测该信号量。
关于组合按键
该按键库仅做了底层的按键扫描处理,一次扫描可以确定所有的按键状态,并上报对应的按键事件,如果需要支持组合按键,请再封一层,根据按键库返回的事件封装需要的组合按键。示例程序提供了简单的实现。
关于矩阵键盘
不管你的矩阵键盘是通过什么通信方式获取按键状态的,只要你将读取按键状态的函数对接到 Flexible_button 数据结构中的 uint8_t (*usr_button_read)(void*); 函数上即可。
更多推荐


所有评论(0)