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, ¶m), // 按键1
EBTN_BUTTON_INIT(2, ¶m), // 按键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, ¶m),
};
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, ¶m),
EBTN_BUTTON_INIT(USER_BUTTON_1, ¶m),
};
// 组合键(新增):给它一个独立的 key_id,不和普通键冲突
static ebtn_btn_combo_t combos[] = {
EBTN_BUTTON_COMBO_INIT(100, ¶m), // combos[0]
EBTN_BUTTON_COMBO_INIT(101, ¶m), // 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() • 组合键检测