嵌入式中一个轻量级的按键管理库!

嵌入式中一个轻量级的按键管理库!

编码文章call10242025-09-11 15:46:182A+A-

大家在做嵌入式项目的时候,是不是经常被按键处理搞得头疼?

今天我们来看看一个按键开源库——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的架构设计相当精巧,主要包含几个核心组件:

核心数据结构

  1. lwbtn_t:管理器结构,负责统一管理所有按键
  2. lwbtn_btn_t:单个按键结构,存储按键的所有状态信息
  3. 各种配置宏:控制功能开关和参数设置

状态机处理逻辑

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,相信它会让你的按键处理代码变得更加优雅和可靠。当然,任何库都不是万能的,使用前还是要根据自己的具体需求来评估是否合适。

点击这里复制本文地址 以上内容由文彬编程网整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!
qrcode

文彬编程网 © All Rights Reserved.  蜀ICP备2024111239号-4