吐血整理ebtn 按键库使用指南

ebtn 按键库使用指南

ebtn 是一个嵌入式 C 按键库,帮你处理消抖、单击、双击、长按、组合键。
核心文件:ebtn.h / ebtn.c / bit_array.h,复制到项目即可使用。


一、你和库之间只有 3 个接触点

你 提供 → ① get_state()    告诉库:某个按键现在有没有被按下
你 提供 → ② on_event()     库通知你:某个按键触发了某个事件
你 调用 → ③ ebtn_process() 每 5ms 调一次,驱动库运转

其他所有代码(结构体、宏、初始化)都是为这 3 个接触点做准备。


二、移植 4 步走

第 1 步:复制文件

ebtn/ 目录下 3 个文件复制到项目,加入编译路径:

你的项目/
├── ebtn/
│   ├── ebtn.h
│   ├── ebtn.c
│   └── bit_array.h
└── main.c

第 2 步:定义按键参数和按键列表

#include "ebtn.h"

// 参数:7 个时间值(单位 ms),先用这个默认值
static const ebtn_btn_param_t param =
    EBTN_PARAMS_INIT(20, 20, 30, 500, 250, 500, 2);
//                   ↑   ↑   ↑   ↑    ↑    ↑   ↑
//           按下消抖 释放消抖 最短点击 最长点击 多击间隔 长按周期 最大连击数

// 按键列表:给每个按键起一个编号(key_id,你自己定)
static ebtn_btn_t btns[] = {
    EBTN_BUTTON_INIT(1, &param),   // 按键1
    EBTN_BUTTON_INIT(2, &param),   // 按键2
};

第 3 步:实现 2 个回调函数

// 回调① :库来问你"这个按键现在按下了吗"
// 返回 1 = 按下,返回 0 = 未按下
uint8_t get_state(struct ebtn_btn *btn)
{
    if (btn->key_id == 1) return HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET;
    if (btn->key_id == 2) return HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET;
    return 0;
}

// 回调②:库通知你发生了什么事
void on_event(struct ebtn_btn *btn, ebtn_evt_t evt)
{
    switch (evt)
    {
    case EBTN_EVT_ONPRESS:    // 按下(消抖后)
        break;

    case EBTN_EVT_ONRELEASE:  // 释放(消抖后)
        break;

    case EBTN_EVT_ONCLICK:    // 点击结束(单击/双击/多击结算后触发)
        // ebtn_click_get_count(btn) 返回点了几下
        if (btn->key_id == 1 && ebtn_click_get_count(btn) == 2) {
            // 按键1 双击
        }
        break;

    case EBTN_EVT_KEEPALIVE:  // 长按中,每隔 time_keepalive_period 触发一次
        // ebtn_keepalive_get_count(btn) 返回已触发了多少次
        break;
    }
}

第 4 步:初始化 + 主循环

void app_init(void)
{
    ebtn_init(
        btns, 2,          // 按键数组 + 数量
        NULL, 0,          // 组合键数组 + 数量(暂不用填 NULL, 0)
        get_state,        // 回调①
        on_event          // 回调②
    );
}

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    GPIO_Init();
    app_init();

    while (1)
    {
        ebtn_process(HAL_GetTick());  // 驱动库,每次传入当前毫秒时间戳
        HAL_Delay(5);                 // 5~10ms 扫描一次
    }
}

三、参数说明

EBTN_PARAMS_INIT(
    time_debounce,          // 按下消抖:按下后需稳定多久才算有效按下,推荐 20ms
    time_debounce_release,  // 释放消抖:释放后需稳定多久才算有效释放,一般填 0 或 20
    time_click_pressed_min, // 最短点击:按下时间低于此值不算点击,推荐 20~30ms
    time_click_pressed_max, // 最长点击:按下时间超过此值算长按(不触发 ONCLICK),推荐 300~500ms
    time_click_multi_max,   // 多击间隔:两次点击之间最长等多久,超过则结算,推荐 200~250ms
    time_keepalive_period,  // 长按周期:长按时每隔多久触发一次 KEEPALIVE,推荐 500ms
    max_consecutive         // 最大连击:连击达到此次数立即结算,推荐 2~5
)

时序示意:

按下────────────────────────────────────────释放
  |← 消抖20ms →| ONPRESS                ONRELEASE
  |←────── 按下时长 ──────────────────────→|
  |                                          |
  如果按下时长 < time_click_pressed_max      ↓
                                      等待 time_click_multi_max
                                      没有新点击 → 触发 ONCLICK
  如果按下时长 > time_click_pressed_max
                                      → 算长按,触发 KEEPALIVE(不触发 ONCLICK)

四、4 种事件速查

事件 触发时机 常用查询
EBTN_EVT_ONPRESS 按下稳定后(消抖通过)
EBTN_EVT_ONRELEASE 释放稳定后(消抖通过)
EBTN_EVT_ONCLICK 点击序列结束后结算 ebtn_click_get_count(btn) 拿点击次数
EBTN_EVT_KEEPALIVE 长按期间周期触发 ebtn_keepalive_get_count(btn) 拿已触发次数

五、你需要用的函数(只有这几个)

函数/宏 在哪用 作用
ebtn_init(...) 初始化 启动库
ebtn_process(ms) 主循环 驱动库运转
btn->key_id 回调里 判断是哪个按键
ebtn_click_get_count(btn) ONCLICK 回调里 拿点击次数
ebtn_keepalive_get_count(btn) KEEPALIVE 回调里 拿长按计数
ebtn_combo_btn_add_btn(combo, key_id) 初始化(用组合键时) 绑定组合键成员

六、进阶:组合键(两个键同时按)

// 额外定义一个组合键,给它一个独立的 key_id(如 100)
static ebtn_btn_combo_t combos[] = {
    EBTN_BUTTON_COMBO_INIT(100, &param),
};

void app_init(void)
{
    ebtn_init(btns, 2, combos, 1, get_state, on_event);

    // 绑定:key_id=1 和 key_id=2 同时按下 → 触发 key_id=100
    ebtn_combo_btn_add_btn(&combos[0], 1);
    ebtn_combo_btn_add_btn(&combos[0], 2);
}

// on_event 里照常处理,key_id==100 就是组合键触发
void on_event(struct ebtn_btn *btn, ebtn_evt_t evt)
{
    if (btn->key_id == 100 && evt == EBTN_EVT_ONCLICK) {
        // 1+2 同时按
    }
}

七、进阶:组合键(多键同时按)

三步,缺一不可:

第1步:定义组合键数组

// 普通键(不变)
static ebtn_btn_t btns[] = {
    EBTN_BUTTON_INIT(USER_BUTTON_0, &param),
    EBTN_BUTTON_INIT(USER_BUTTON_1, &param),
};

// 组合键(新增):给它一个独立的 key_id,不和普通键冲突
static ebtn_btn_combo_t combos[] = {
    EBTN_BUTTON_COMBO_INIT(100, &param),  // combos[0]
    EBTN_BUTTON_COMBO_INIT(101, &param),  // combos[1]
};

第2步:初始化时绑定成员

void app_init(void)
{
    //                      ↓普通键数组  ↓数量  ↓组合键数组  ↓数量
    ebtn_init(btns, 2, combos, 2, get_state, on_event);

    // 组合键1:USER_BUTTON_0 + USER_BUTTON_1 同时按 → 触发 key_id=100
    ebtn_combo_btn_add_btn(&combos[0], USER_BUTTON_0);
    ebtn_combo_btn_add_btn(&combos[0], USER_BUTTON_1);

    // 组合键2:USER_BUTTON_2 + USER_BUTTON_3 同时按 → 触发 key_id=101
    ebtn_combo_btn_add_btn(&combos[1], USER_BUTTON_2);
    ebtn_combo_btn_add_btn(&combos[1], USER_BUTTON_3);
}

add_btn 的作用:把对应键的位图置1,库检测时所有成员同时按下才触发。

第3步:on_event 里多加 case

void on_event(struct ebtn_btn *btn, ebtn_evt_t evt)
{
    if (evt == EBTN_EVT_ONCLICK)
    {
        switch (btn->key_id)
        {
        case USER_BUTTON_0: /* 单独按0 */   break;
        case USER_BUTTON_1: /* 单独按1 */   break;
        case 100:           /* 0+1 同时按 */ break;
        case 101:           /* 2+3 同时按 */ break;
        }
    }
}

三个概念对应关系

定义 btns[]   → 有哪些物理键
定义 combos[] → 有哪些组合(每个组合一个独立 key_id)
add_btn       → 哪些物理键构成这个组合

八、进阶:组合键与普通键冲突处理

问题根源

库在同一帧内顺序独立处理普通键和组合键,且普通键先于组合键遍历:

同一帧触发顺序:
ONCLICK → key_id=1   ← 已发出,无法撤回
ONCLICK → key_id=2   ← 已发出,无法撤回
ONCLICK → key_id=100 ← 这时才知道是组合键

所以在回调里加标志位来不及拦截普通键。

解决方案:事件暂存 + 事后派发

不在 on_event 里直接执行动作,先暂存,等 ebtn_process() 整帧结束后统一派发:

// 暂存结构
static struct {
    uint8_t  fired;
    uint16_t click_cnt;
} pending[3]; // 0=btn1, 1=btn2, 2=combo(100)

void on_event(struct ebtn_btn *btn, ebtn_evt_t evt)
{
    if (evt != EBTN_EVT_ONCLICK) return; // ONPRESS/ONRELEASE 不受影响,正常处理

    if      (btn->key_id == 1)   { pending[0].fired = 1; pending[0].click_cnt = ebtn_click_get_count(btn); }
    else if (btn->key_id == 2)   { pending[1].fired = 1; pending[1].click_cnt = ebtn_click_get_count(btn); }
    else if (btn->key_id == 100) { pending[2].fired = 1; pending[2].click_cnt = ebtn_click_get_count(btn); }
}

void dispatch_events(void)
{
    if (pending[2].fired) {
        // 组合键触发 → 忽略本帧普通键
        handle_combo(pending[2].click_cnt);
        pending[0].fired = pending[1].fired = pending[2].fired = 0;
        return;
    }
    if (pending[0].fired) { handle_btn1(pending[0].click_cnt); pending[0].fired = 0; }
    if (pending[1].fired) { handle_btn2(pending[1].click_cnt); pending[1].fired = 0; }
}

// 主循环
while (1)
{
    ebtn_process(HAL_GetTick());
    dispatch_events();            // 每次 process 后立即派发
    HAL_Delay(5);
}

工作原理

ebtn_process() 一帧内:
  on_event(btn1,  ONCLICK) → pending[0].fired=1  ← 只存,不执行
  on_event(btn2,  ONCLICK) → pending[1].fired=1  ← 只存,不执行
  on_event(combo, ONCLICK) → pending[2].fired=1  ← 只存,不执行

dispatch_events():
  发现 pending[2].fired → 只执行组合键逻辑,清空 pending[0/1]

前提条件

此方案依赖三个 ONCLICK 在同一帧(同一次 ebtn_process() 调用)内触发,满足以下条件时成立:

条件 说明
两键同时释放(时差 < 扫描周期 5ms) 实际手按几乎满足
使用相同的 param 参数 time_click_multi_max 一致,超时同帧到期

九、进阶:长按

长按使用 EBTN_EVT_KEEPALIVE 事件,按住不放时每隔 time_keepalive_period 触发一次。

参数控制

EBTN_PARAMS_INIT(20, 20, 30, 500, 250, 1000, 2)
//                              ↑         ↑
//                    time_click_pressed_max  time_keepalive_period
//                    超过此值算长按           长按时每隔多久触发一次

点击和长按的分界线就是 time_click_pressed_max

按下时长 < time_click_pressed_max → 算点击,最终触发 ONCLICK
按下时长 > time_click_pressed_max → 算长按,触发 KEEPALIVE,不触发 ONCLICK

两种常见需求

需求1:按住超过 1 秒触发一次

case EBTN_EVT_KEEPALIVE:
    if (btn->key_id == USER_BUTTON_0)
    {
        if (ebtn_keepalive_get_count(btn) == 1) {
            // time_keepalive_period=1000ms
            // count==1 说明刚好按住满 1 秒,只执行一次
            do_something();
        }
    }
    break;

需求2:按住期间持续触发(如长按音量键一直加)

case EBTN_EVT_KEEPALIVE:
    if (btn->key_id == USER_BUTTON_0)
    {
        volume_up();  // 每隔 500ms 自动调一次,不判断 count
    }
    break;

ebtn_keepalive_get_count 的含义

按住 0~500ms   → count = 0(还没触发过)
按住 500ms     → count = 1(第1次触发)
按住 1000ms    → count = 2(第2次触发)
按住 1500ms    → count = 3(第3次触发)
...
松手后 count 自动清零

十、其余函数一览(暂时不用管)

函数 什么时候才需要
ebtn_is_in_process() 做低功耗休眠判断时
ebtn_is_btn_active(btn) 需要实时查询按键状态时
ebtn_register(dyn_btn) 运行时动态添加按键时
ebtn_get_btn_by_key_id(key_id) 需要在回调外查找按键时
ebtn_get_btn_index_by_*(...) 库内部使用,基本不需要直接调用

十一、移植核心总结

你负责的(平台相关)          库负责的(平台无关)
──────────────────────        ──────────────────────
① 读 GPIO → get_state()      • 消抖
② 处理事件 → on_event()      • 单击 / 双击 / 多击检测
③ 提供时间戳 → HAL_GetTick() • 长按 + KEEPALIVE 周期事件
④ 周期调用 → ebtn_process()  • 组合键检测
1 个赞