嵌入式中一个轻量级的按键管理库!
大家在做嵌入式项目的时候,是不是经常被按键处理搞得头疼?
今天我们来看看一个按键开源库——lwbtn,看看它是怎么优雅地处理按键管理的。
lwbtn简介
lwbtn(Lightweight Button)是一个专为嵌入式系统设计的轻量级按键管理库,能够自动处理各种复杂的按键逻辑。整个库就几个文件,源码不到500行,但功能很全面;通过宏定义可以精确控制功能开关,避免资源浪费;接口简单清晰,几分钟就能集成到项目中。
https://github.com/MaJerle/lwbtn
基本特性
lwbtn的特性可以说是相当丰富:
- 纯C语言实现:兼容C11标准,在各种嵌入式平台上都能跑
- 平台无关性:只需要你提供毫秒级时间源,其他的它都帮你搞定
- 零动态内存分配:对于内存紧张的单片机来说,这点特别重要
- 事件驱动架构:通过回调函数处理各种按键事件
- 丰富的事件类型:支持按下、释放、单击、多击、长按保活等事件
- 软件防抖:内置防抖算法,告别硬件防抖电路的烦恼
STM32环境使用
咱们先来看看一个stm32的demo。这个demo虽然简单,但是把lwbtn的核心功能都展示出来了。
Demo代码解析
demo的核心思路是把键盘按键映射成硬件按键,通过终端输入来模拟按键的按下和释放。
让我们看看关键的几个函数:
按键状态获取函数:
/**
* @brief 获取按键状态
* @param lw LwBTN实例
* @param btn 按钮实例
* @return 1表示按下,0表示释放
*/
uint8_t get_key_state(struct lwbtn* lw, struct lwbtn_btn* btn) {
(void)lw;
// 从按钮的arg参数中获取按键配置
btn_config_t* config = (btn_config_t*)btn->arg;
// 读取GPIO状态,假设按键按下时为低电平
GPIO_PinState pin_state = HAL_GPIO_ReadPin(config->port, config->pin);
// 返回1表示按下(低电平),0表示释放(高电平)
return (pin_state == GPIO_PIN_RESET) ? 1 : 0;
}
这个函数就是lwbtn和硬件之间的桥梁。
事件处理函数:
/**
* @brief 按钮事件处理函数
* @param lw LwBTN实例
* @param btn 按钮实例
* @param evt 事件类型
*/
void button_event_handler(struct lwbtn* lw, struct lwbtn_btn* btn, lwbtn_evt_t evt) {
(void)lw;
btn_config_t* config = (btn_config_t*)btn->arg;
char msg[100];
switch(evt) {
case LWBTN_EVT_ONPRESS:
snprintf(msg, sizeof(msg), "[%lu] %s 被按下\r\n", system_tick_ms, config->name);
HAL_UART_Transmit(&huart1, (uint8_t*)msg, strlen(msg), 100);
LED_Toggle(); // 按键按下时切换LED状态
break;
case LWBTN_EVT_ONRELEASE:
snprintf(msg, sizeof(msg), "[%lu] %s 被释放\r\n", system_tick_ms, config->name);
HAL_UART_Transmit(&huart1, (uint8_t*)msg, strlen(msg), 100);
break;
#if LWBTN_CFG_USE_CLICK
case LWBTN_EVT_ONCLICK:
snprintf(msg, sizeof(msg), "[%lu] %s 点击事件 (连续点击次数: %d)\r\n",
system_tick_ms, config->name, btn->click.cnt);
HAL_UART_Transmit(&huart1, (uint8_t*)msg, strlen(msg), 100);
// 根据点击次数执行不同动作
if (config->id == BTN_ID_1) {
if (btn->click.cnt == 1) {
// 单击:短闪LED
for(int i = 0; i < 3; i++) {
HAL_GPIO_WritePin(LED_GPIO_PORT, LED_GPIO_PIN, GPIO_PIN_RESET);
HAL_Delay(50);
HAL_GPIO_WritePin(LED_GPIO_PORT, LED_GPIO_PIN, GPIO_PIN_SET);
HAL_Delay(50);
}
} else if (btn->click.cnt == 2) {
// 双击:长闪LED
for(int i = 0; i < 3; i++) {
HAL_GPIO_WritePin(LED_GPIO_PORT, LED_GPIO_PIN, GPIO_PIN_RESET);
HAL_Delay(200);
HAL_GPIO_WritePin(LED_GPIO_PORT, LED_GPIO_PIN, GPIO_PIN_SET);
HAL_Delay(200);
}
}
}
break;
#endif
default:
break;
}
}
这个回调函数会在不同的按键事件发生时被调用,你可以在这里实现具体的业务逻辑。
主循环处理:
/**
* @brief 主函数
*/
int main(void) {
char welcome_msg[] = "\r\n=== LwBTN STM32F103 Demo ===\r\n"
"按键说明:\r\n"
" BTN1(PA0) - 测试点击事件\r\n"
" BTN2(PA1) - 测试长按保活\r\n"
" BTN3(PA2) - 普通按键\r\n"
" BTN4(PA3) - 普通按键\r\n"
"LED: PC13\r\n"
"开始监听按键事件...\r\n\r\n";
/* 系统初始化 */
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
/* 发送欢迎信息 */
HAL_UART_Transmit(&huart1, (uint8_t*)welcome_msg, strlen(welcome_msg), 1000);
/* 初始化按钮结构体 */
for (int i = 0; i < BTN_COUNT; i++) {
memset(&btns[i], 0, sizeof(lwbtn_btn_t));
btns[i].arg = (void*)&btn_configs[i];
}
/* 初始化LwBTN */
if (!lwbtn_init_ex(NULL, btns, BTN_COUNT, get_key_state, button_event_handler)) {
char error_msg[] = "LwBTN初始化失败!\r\n";
HAL_UART_Transmit(&huart1, (uint8_t*)error_msg, strlen(error_msg), 100);
while(1);
}
char init_msg[] = "LwBTN初始化成功!\r\n";
HAL_UART_Transmit(&huart1, (uint8_t*)init_msg, strlen(init_msg), 100);
uint32_t last_process_time = get_system_time_ms();
/* 主循环 */
while (1) {
uint32_t current_time = get_system_time_ms();
/* 每5ms处理一次按钮状态 */
if (current_time - last_process_time >= 5) {
lwbtn_process_ex(NULL, current_time);
last_process_time = current_time;
}
/* 可以在这里添加其他任务 */
/* 简单的任务调度延时 */
HAL_Delay(1);
}
}
这里展示了lwbtn的典型使用模式:定期调用处理函数,让库来检测按键状态变化。
源码框架深度解析
现在我们深入lwbtn的源码,看看它内部是怎么工作的。
整体架构
lwbtn的架构设计相当精巧,主要包含几个核心组件:
核心数据结构:
- lwbtn_t:管理器结构,负责统一管理所有按键
- lwbtn_btn_t:单个按键结构,存储按键的所有状态信息
- 各种配置宏:控制功能开关和参数设置
状态机处理逻辑
lwbtn的核心是一个精心设计的状态机,让我们看看它是怎么处理按键状态变化的:
核心处理函数分析
prv_process_btn函数是整个库的心脏,让我们看看它的处理逻辑:
第一步:状态获取
new_state = LWBTN_BTN_GET_STATE(lwobj, btn);
这里通过宏定义,支持三种不同的状态获取方式:回调函数、手动设置、或者两者结合。
第二步:首次非激活状态检查
if (!(btn->flags & LWBTN_FLAG_FIRST_INACTIVE_RCVD)) {
if (new_state) {
return; // 如果首次检测就是激活状态,直接返回
}
btn->flags = LWBTN_FLAG_FIRST_INACTIVE_RCVD;
}
防止了系统启动时按键已经处于按下状态的情况。
第三步:状态变化检测
if (new_state != btn->last_state) {
btn->time_state_change = mstime; // 记录状态变化时间
}
第四步:按键按下处理 当按键保持按下状态时,会进行防抖处理和事件发送:
if (!(btn->flags & LWBTN_FLAG_ONPRESS_SENT)) {
if ((mstime - btn->time_state_change) >= DEBOUNCE_TIME) {
btn->flags |= LWBTN_FLAG_ONPRESS_SENT;
lwobj->evt_fn(lwobj, btn, LWBTN_EVT_ONPRESS);
}
}
第五步:保活事件处理 对于长按情况,库会定期发送保活事件:
while ((mstime - btn->keepalive.last_time) >= KEEPALIVE_PERIOD) {
btn->keepalive.last_time += KEEPALIVE_PERIOD;
++btn->keepalive.cnt;
lwobj->evt_fn(lwobj, btn, LWBTN_EVT_KEEPALIVE);
}
点击事件检测算法
点击事件的检测是lwbtn最复杂的部分,它需要判断按键的按下时间是否在有效范围内:
这个算法考虑了多种情况:
- 按下时间太短的误触
- 按下时间太长的长按
- 连续多击的时间间隔判断
- 最大连击数限制
配置选项深度解析
lwbtn通过大量的宏定义来控制功能,这种设计让库既强大又节省资源。主要的配置选项包括:
时间相关配置:
- LWBTN_CFG_TIME_DEBOUNCE_PRESS:按下防抖时间(默认20ms)
- LWBTN_CFG_TIME_DEBOUNCE_RELEASE:释放防抖时间(默认0ms)
- LWBTN_CFG_TIME_CLICK_MIN:有效点击最小时间(默认20ms)
- LWBTN_CFG_TIME_CLICK_MAX:有效点击最大时间(默认300ms)
功能开关配置:
- LWBTN_CFG_USE_CLICK:是否启用点击事件检测
- LWBTN_CFG_USE_KEEPALIVE:是否启用保活事件
- LWBTN_CFG_CLICK_MAX_CONSECUTIVE:最大连续点击次数
这种配置方式的好处是,你可以根据具体需求裁剪功能,比如只需要简单的按下/释放事件,就可以关闭点击检测功能,节省内存和CPU资源。
实际应用建议
1. 合理设置防抖时间 不同的按键机械特性不同,需要根据实际情况调整防抖时间。一般机械按键20ms足够,薄膜按键可能需要更短的时间。
2. 注意时间源的精度 lwbtn依赖准确的毫秒时间戳,如果你的系统时间源精度不够,可能会影响按键检测的准确性。
3. 合理配置处理频率 demo中是5ms调用一次处理函数,这个频率对大多数应用来说足够了。频率太高浪费CPU,太低可能错过按键事件。
4. 内存使用优化 每个按键结构体大约占用几十字节内存,如果按键数量很多,需要考虑内存使用情况。可以通过关闭不需要的功能来减少内存占用。
总结
lwbtn作为一个轻量级的按键管理库,设计思路非常值得学习。它通过事件驱动的方式,把复杂的按键处理逻辑封装得很好,让我们在使用时只需要关注业务逻辑,而不用纠结于底层的状态机处理。
特别是它的状态机设计和点击检测算法,基本覆盖了实际应用中可能遇到的各种情况。对于嵌入式开发者来说,这个库既可以直接使用,也可以作为学习按键处理算法的好资料。
如果你的项目中需要处理按键输入,不妨试试lwbtn,相信它会让你的按键处理代码变得更加优雅和可靠。当然,任何库都不是万能的,使用前还是要根据自己的具体需求来评估是否合适。