这次 ebtn 讲评作业到底要先搞懂什么?
来源:基于
note/5.2 讲评后的ebtn作业.md原稿整理,结合GD32_DEMO_04/GD32_DEMO_05例程、课程提供的优化版ebtn库源码,以及这几轮对话里的补充解释。
## 这节在讲什么
这节的重点不是单纯做出 copy / paste / cut 这三个功能,而是搞懂一件事:
- 当一个按键既有单键功能,又参与组合键功能时,怎么避免单键和组合键互相冲突。
围绕这个问题,这节实际上分成了三层:
-
应用层功能:
BTN0 + BTN1复制,BTN0 + BTN2粘贴,BTN0 + BTN3剪切。 -
应用层写法对比:例程把冲突交给
ebtn库处理,我自己的版本用pending延后执行单键来补偿冲突。 -
库层实现:
ebtn_set_config(EBTN_CFG_COMBO_PRIORITY)到底做了什么,为什么能拦住单键误触发。
先看这份例程到底做了什么
业务目标
这份例程把 6 个普通按键和 3 个组合键一起注册进 ebtn:
-
USER_BUTTON_0 ~ USER_BUTTON_5:普通按键 -
USER_BUTTON_COMBO_COPY = BTN0 + BTN1 -
USER_BUTTON_COMBO_PASTE = BTN0 + BTN2 -
USER_BUTTON_COMBO_CUT = BTN0 + BTN3
普通键负责控制对应 LED,组合键负责做:
-
复制当前 6 个 LED 状态到剪贴板
-
从剪贴板恢复 LED 状态
-
复制后再全部熄灭
例程完整代码
下面这份是整理后的“去掉串口输出版”例程代码,保留原来的功能和整体结构:
#include "ebtn_app.h"
#include "ebtn.h"
#include "gpio.h"
#include <string.h> // memcpy / memset
extern uint8_t ucLed[6];
static uint8_t led_clipboard[6] = {0};
typedef enum
{
USER_BUTTON_0 = 0,
USER_BUTTON_1,
USER_BUTTON_2,
USER_BUTTON_3,
USER_BUTTON_4,
USER_BUTTON_5,
USER_BUTTON_MAX,
USER_BUTTON_COMBO_COPY = 0x100, // BTN0 + BTN1
USER_BUTTON_COMBO_PASTE, // BTN0 + BTN2
USER_BUTTON_COMBO_CUT, // BTN0 + BTN3
USER_BUTTON_COMBO_MAX
} user_button_t;
static const ebtn_btn_param_t btn_param_normal =
EBTN_PARAMS_INIT(20, 20, 50, 500, 300, 200, 2);
static const ebtn_btn_param_t btn_param_combo =
EBTN_PARAMS_INIT(30, 30, 50, 500, 200, 0, 1);
static ebtn_btn_t btns[] =
{
EBTN_BUTTON_INIT(USER_BUTTON_0, &btn_param_normal),
EBTN_BUTTON_INIT(USER_BUTTON_1, &btn_param_normal),
EBTN_BUTTON_INIT(USER_BUTTON_2, &btn_param_normal),
EBTN_BUTTON_INIT(USER_BUTTON_3, &btn_param_normal),
EBTN_BUTTON_INIT(USER_BUTTON_4, &btn_param_normal),
EBTN_BUTTON_INIT(USER_BUTTON_5, &btn_param_normal),
};
static ebtn_btn_combo_t btns_combo[] =
{
EBTN_BUTTON_COMBO_INIT_RAW(USER_BUTTON_COMBO_COPY, &btn_param_combo, EBTN_EVT_MASK_ONCLICK),
EBTN_BUTTON_COMBO_INIT_RAW(USER_BUTTON_COMBO_PASTE, &btn_param_combo, EBTN_EVT_MASK_ONCLICK),
EBTN_BUTTON_COMBO_INIT_RAW(USER_BUTTON_COMBO_CUT, &btn_param_combo, EBTN_EVT_MASK_ONCLICK),
};
uint8_t prv_btn_get_state(struct ebtn_btn *btn)
{
switch (btn->key_id)
{
case USER_BUTTON_0:
return !HAL_GPIO_ReadPin(GPIOE, GPIO_PIN_2);
case USER_BUTTON_1:
return !HAL_GPIO_ReadPin(GPIOE, GPIO_PIN_3);
case USER_BUTTON_2:
return !HAL_GPIO_ReadPin(GPIOE, GPIO_PIN_4);
case USER_BUTTON_3:
return !HAL_GPIO_ReadPin(GPIOE, GPIO_PIN_5);
case USER_BUTTON_4:
return !HAL_GPIO_ReadPin(GPIOE, GPIO_PIN_6);
case USER_BUTTON_5:
return !HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13);
default:
return 0;
}
}
void prv_btn_event(struct ebtn_btn *btn, ebtn_evt_t evt)
{
if (evt != EBTN_EVT_ONCLICK)
{
return;
}
uint16_t click_cnt = ebtn_click_get_count(btn);
switch (btn->key_id)
{
case USER_BUTTON_0:
if (click_cnt == 1)
{
ucLed[0] = 1;
}
else if (click_cnt == 2)
{
ucLed[0] = 0;
}
break;
case USER_BUTTON_1:
if (click_cnt == 1)
{
ucLed[1] = 1;
}
else if (click_cnt == 2)
{
ucLed[1] = 0;
}
break;
case USER_BUTTON_2:
if (click_cnt == 1)
{
ucLed[2] = 1;
}
else if (click_cnt == 2)
{
ucLed[2] = 0;
}
break;
case USER_BUTTON_3:
if (click_cnt == 1)
{
ucLed[3] = 1;
}
else if (click_cnt == 2)
{
ucLed[3] = 0;
}
break;
case USER_BUTTON_4:
if (click_cnt == 1)
{
ucLed[4] = 1;
}
else if (click_cnt == 2)
{
ucLed[4] = 0;
}
break;
case USER_BUTTON_5:
if (click_cnt == 1)
{
ucLed[5] = 1;
}
else if (click_cnt == 2)
{
ucLed[5] = 0;
}
break;
case USER_BUTTON_COMBO_COPY:
if (click_cnt == 1)
{
memcpy(led_clipboard, ucLed, sizeof(ucLed));
}
break;
case USER_BUTTON_COMBO_PASTE:
if (click_cnt == 1)
{
memcpy(ucLed, led_clipboard, sizeof(ucLed));
}
break;
case USER_BUTTON_COMBO_CUT:
if (click_cnt == 1)
{
memcpy(led_clipboard, ucLed, sizeof(ucLed));
memset(ucLed, 0, sizeof(ucLed));
}
break;
default:
break;
}
}
void app_ebtn_init(void)
{
int init_ok;
init_ok = ebtn_init(btns,
EBTN_ARRAY_SIZE(btns),
btns_combo,
EBTN_ARRAY_SIZE(btns_combo),
prv_btn_get_state,
prv_btn_event);
if (!init_ok)
{
return;
}
ebtn_set_config(EBTN_CFG_COMBO_PRIORITY);
ebtn_combo_btn_add_btn(&btns_combo[0], USER_BUTTON_0);
ebtn_combo_btn_add_btn(&btns_combo[0], USER_BUTTON_1);
ebtn_combo_btn_add_btn(&btns_combo[1], USER_BUTTON_0);
ebtn_combo_btn_add_btn(&btns_combo[1], USER_BUTTON_2);
ebtn_combo_btn_add_btn(&btns_combo[2], USER_BUTTON_0);
ebtn_combo_btn_add_btn(&btns_combo[2], USER_BUTTON_3);
}
void btn_task(void)
{
ebtn_process(HAL_GetTick());
}
这份例程的学习顺序要怎么抓
这份代码建议按下面顺序看,不要一上来就钻回调细节:
-
先看普通键和组合键是怎么注册的。
-
再看
app_ebtn_init()里怎么初始化、开配置、绑定组合成员。 -
再看
prv_btn_event()里每个键最后触发了什么功能。 -
最后再回到底层库里,看
EBTN_CFG_COMBO_PRIORITY为什么能解决冲突。
我自己的写法和例程,真正差在哪
核心差异一句话
两份代码都能实现 copy / paste / cut,但真正差异不在业务功能,而在:
- 单键和组合键冲突时,到底谁来裁决。
一句话记:
例程是“库层解决冲突”,我的写法是“应用层补偿冲突”。
对比表
| 对比点 | 例程写法 | 我的写法 |
|---|---|---|
| 冲突裁决位置 | 主要交给 ebtn 库 |
主要由应用层自己处理 |
| 关键机制 | EBTN_CFG_COMBO_PRIORITY |
single_click_pending[] 延后执行 |
| 单键执行时机 | 在回调里直接执行 | 先挂起,btn_task() 里再补执行 |
| 组合键初始化 | EBTN_BUTTON_COMBO_INIT_RAW(..., EBTN_EVT_MASK_ONCLICK) |
EBTN_BUTTON_COMBO_INIT(...) 默认方式 |
| 参数策略 | 普通键和组合键分开配参数 | 单键和组合键共用一套参数 |
| 扩展性 | 组合键变多时更稳 | 组合键一多,清 pending 容易漏 |
| 对库内部时序依赖 | 相对更少 | 相对更多 |
例程为什么更稳
例程最关键的不是 copy / paste / cut 那几行,而是这两件事:
- 打开组合键优先
ebtn_set_config(EBTN_CFG_COMBO_PRIORITY);
这表示:如果 USER_BUTTON_0 既有单键功能,又参与 0+1、0+2、0+3 这些组合键,那么库会优先尝试把这次操作判成组合键,而不是先触发 USER_BUTTON_0 的单键逻辑。
- 组合键只监听单击
EBTN_BUTTON_COMBO_INIT_RAW(USER_BUTTON_COMBO_COPY, &btn_param_combo, EBTN_EVT_MASK_ONCLICK)
这表示组合键对象只关心 ONCLICK,不去处理按下、释放、长按这些额外事件。
我的写法为什么也能工作
我的代码没有把冲突裁决完全交给库,所以自己做了一个延后执行层:
static uint8_t single_click_pending[USER_BUTTON_MAX];
思路是:
-
USER_BUTTON_0 ~ USER_BUTTON_3在单击时先不立刻改 LED。 -
先记成
pending,表示这次可能要执行单键动作。 -
如果这一轮后面又识别出
0+1、0+2、0+3是组合键,就把对应的pending清零。 -
btn_task()末尾再统一检查pending,只执行那些没有被组合键取消的单键动作。
所以我这版的重点不是“库优先判组合键”,而是:
-
即使单键事件先来了
-
我也先忍住不执行
-
等确认没有组合键再说
这部分最该记住什么
-
例程更像正式工程写法:能把冲突交给库,就尽量别在应用层补太多状态机。
-
我的写法适合理解原理:它把“为什么会冲突、冲突后怎么补救”展示得很直白。
-
真正要长期维护时,优先选“组合键优先 + 组合键只响应单击 + 单独参数”这一套更稳。
ebtn_set_config(EBTN_CFG_COMBO_PRIORITY) 到底做了什么
先记一句最重要的话
ebtn_set_config(EBTN_CFG_COMBO_PRIORITY); 本身并不会直接产生组合键事件,它只是给 ebtn 库打开一个处理规则:
当某几个按键已经组成组合键时,这几个成员键这一轮先不要按单键处理,而是优先把它们当成组合键处理。
所以它做的不是“新增组合键”,而是“决定单键和组合键冲突时谁优先”。
先看这句代码本身
#define EBTN_CFG_COMBO_PRIORITY (1 << 0)
这只是定义了一个配置位,意思是:
-
第 0 位如果是
1 -
就表示开启“组合键优先模式”
ebtn_set_config() 本体也很短:
void ebtn_set_config(uint8_t cfg_flags)
{
ebtn_t *ebtobj = &ebtn_default;
ebtobj->config = cfg_flags;
}
它的意思是:
-
把你传进来的配置保存到
ebtn_default.config -
后面真正扫描按键时,再根据这个配置决定怎么处理
所以这一步还只是“存配置”,真正的逻辑不在这里。
组合键在库里是怎么存的
先看组合键结构体:
typedef struct ebtn_btn_combo
{
BIT_ARRAY_DEFINE(comb_key, EBTN_MAX_KEYNUM);
ebtn_btn_t btn;
} ebtn_btn_combo_t;
这里可以这样理解:
-
comb_key:记录这个组合键由哪些普通按键组成 -
btn:把这个组合键也当成一个按钮对象来管理
也就是说,在这个库里,组合键不是临时 if (btn0 && btn1) 拼出来的,而是先注册成一个正式对象。
comb_key 是什么
comb_key 可以理解成一排标记位。
假设一共有 6 个普通按键:
-
第 0 位对应
USER_BUTTON_0 -
第 1 位对应
USER_BUTTON_1 -
第 2 位对应
USER_BUTTON_2 -
第 3 位对应
USER_BUTTON_3 -
第 4 位对应
USER_BUTTON_4 -
第 5 位对应
USER_BUTTON_5
如果组合键 COPY = BTN0 + BTN1,它的 comb_key 可以理解成:
[1, 1, 0, 0, 0, 0]
意思是:
-
这个组合需要
BTN0 -
也需要
BTN1 -
其他键不是这个组合的成员
组合键成员是怎么绑进去的
例程里最后有这种写法:
ebtn_combo_btn_add_btn_by_idx(&btns_combo[0], btn0_idx);
ebtn_combo_btn_add_btn_by_idx(&btns_combo[0], btn1_idx);
底层实现是:
void ebtn_combo_btn_add_btn_by_idx(ebtn_btn_combo_t *btn, int idx)
{
bit_array_set(btn->comb_key, idx);
}
这句的意思很简单:
-
idx指向哪个普通按键 -
就把
comb_key里对应那一位设成1
所以对 COPY 来说:
-
先把
BTN0对应位置设成1 -
再把
BTN1对应位置设成1
这样 COPY 这个组合键就知道:自己是由 BTN0 + BTN1 组成的。
如果你用的是按 key_id 绑定,底层其实也只是多做了一步“先查索引,再设位”:
void ebtn_combo_btn_add_btn(ebtn_btn_combo_t *btn, uint16_t key_id)
{
int idx = ebtn_get_btn_index_by_key_id(key_id);
if (idx < 0)
{
return;
}
ebtn_combo_btn_add_btn_by_idx(btn, idx);
}
作者为什么又额外加了 combo_active
这是理解 EBTN_CFG_COMBO_PRIORITY 的关键。
在 ebtn_t 结构体里,作者专门加了这个成员:
BIT_ARRAY_DEFINE(combo_active, EBTN_MAX_KEYNUM);
uint8_t config;
你可以这样区分:
-
comb_key:某个组合键理论上需要哪些成员 -
combo_active:这一轮扫描里,哪些普通按键已经被某个组合键占用了
比如:
-
COPY的comb_key永远是BTN0 + BTN1 -
但
combo_active是动态变化的
当你没有按组合键时:
combo_active = [0, 0, 0, 0, 0, 0]
当你这一轮刚好在按 BTN0 + BTN1 时:
combo_active = [1, 1, 0, 0, 0, 0]
这表示:
-
BTN0这一轮已经被组合键占用 -
BTN1这一轮已经被组合键占用 -
这两个键先不要再按单键逻辑处理
真正的逻辑从哪里开始生效
核心函数在 ebtn_process_with_curr_state() 里,先看开头:
uint8_t combo_priority = ebtobj->config & EBTN_CFG_COMBO_PRIORITY;
这句的意思是:
-
如果你之前调用了
ebtn_set_config(EBTN_CFG_COMBO_PRIORITY); -
那么
combo_priority就会非 0 -
后面就进入“组合键优先”的处理分支
接着,库会先清空本轮的 combo_active:
if (combo_priority)
{
bit_array_clear_all(ebtobj->combo_active, EBTN_MAX_KEYNUM);
}
这一步的意思是:
-
每一轮都重新统计当前有哪些键属于活动组合
-
不能把上一轮的结果带到下一轮
第一步:先扫描所有组合键
这是最关键的预处理:
for (i = 0; i < ebtobj->btns_combo_cnt; ++i)
{
BIT_ARRAY_DEFINE(tmp_data, EBTN_MAX_KEYNUM) = {0};
bit_array_t *comb_key = ebtobj->btns_combo[i].comb_key;
bit_array_and(tmp_data, curr_state, comb_key, EBTN_MAX_KEYNUM);
uint8_t curr = bit_array_cmp(tmp_data, comb_key, EBTN_MAX_KEYNUM) == 0;
if (curr)
{
bit_array_or(ebtobj->combo_active, ebtobj->combo_active, comb_key, EBTN_MAX_KEYNUM);
}
}
拆开理解就行。
curr_state 是什么
curr_state 表示:
- 当前这一刻,所有普通按键的真实按下情况
比如当前你按下了 BTN0 和 BTN1:
curr_state = [1, 1, 0, 0, 0, 0]
comb_key 是什么
假设当前检查的是 COPY,那么:
comb_key = [1, 1, 0, 0, 0, 0]
bit_array_and(tmp_data, curr_state, comb_key, ...) 在干什么
这一句是在取交集:
-
当前真实按下的键里
-
有哪些是这个组合键关心的成员
如果现在确实按着 BTN0 + BTN1,交集结果就是:
tmp_data = [1, 1, 0, 0, 0, 0]
bit_array_cmp(tmp_data, comb_key, ...) == 0 在判断什么
它在判断:
-
当前交集结果
-
是否和这个组合键要求的成员完全一样
如果一样,说明:
-
这个组合键要求的成员都按下了
-
所以这个组合当前成立
也就是说:
uint8_t curr = 1;
组合成立后为什么要 or 到 combo_active
bit_array_or(ebtobj->combo_active, ebtobj->combo_active, comb_key, EBTN_MAX_KEYNUM);
这句的意思是:
-
既然这个组合成立了
-
那就把这个组合涉及的所有成员键都标记为当前处于活动组合中
如果当前识别出 COPY = BTN0 + BTN1,那么:
combo_active = [1, 1, 0, 0, 0, 0]
第二步:处理普通按键前,先看它是不是组合成员
接下来库才开始处理普通按键:
for (i = 0; i < ebtobj->btns_cnt; ++i)
{
if (combo_priority && bit_array_get(ebtobj->combo_active, i))
{
continue;
}
ebtn_process_btn(&ebtobj->btns[i], ebtobj->old_state, curr_state, i, mstime);
}
这里最关键的是:
if (combo_priority && bit_array_get(ebtobj->combo_active, i))
{
continue;
}
翻成人话就是:
-
如果开了组合键优先
-
并且这个普通按键已经被标记为某个活动组合的一部分
-
那么这一轮直接跳过它的单键处理
注意,它不是“单键先执行,后面再撤回”,而是:
一开始就不让这个单键进入处理流程。
这也是为什么它比我之前那个 single_click_pending[] 方案更干净。
第三步:最后再处理组合键本身
普通按键处理完后,库才去处理组合键:
for (i = 0; i < ebtobj->btns_combo_cnt; ++i)
{
ebtn_process_btn_combo(&ebtobj->btns_combo[i].btn,
ebtobj->old_state,
curr_state,
ebtobj->btns_combo[i].comb_key,
mstime);
}
意思是:
-
前面已经把会冲突的成员单键拦住了
-
现在只需要让组合键对象自己走完整个状态判断流程
所以最后剩下的就是组合键事件,不会再被单键抢走。
组合键为什么也能复用普通按钮的逻辑
ebtn_process_btn_combo() 这一段很重要:
static void ebtn_process_btn_combo(ebtn_btn_t *btn, bit_array_t *old_state, bit_array_t *curr_state, bit_array_t *comb_key, ebtn_time_t mstime)
{
BIT_ARRAY_DEFINE(tmp_data, EBTN_MAX_KEYNUM) = {0};
if (bit_array_num_bits_set(comb_key, EBTN_MAX_KEYNUM) == 0)
{
return;
}
bit_array_and(tmp_data, curr_state, comb_key, EBTN_MAX_KEYNUM);
uint8_t curr = bit_array_cmp(tmp_data, comb_key, EBTN_MAX_KEYNUM) == 0;
bit_array_and(tmp_data, old_state, comb_key, EBTN_MAX_KEYNUM);
uint8_t old = bit_array_cmp(tmp_data, comb_key, EBTN_MAX_KEYNUM) == 0;
prv_process_btn(btn, old, curr, mstime);
}
这里最关键的是最后一句:
prv_process_btn(btn, old, curr, mstime);
它说明:
-
组合键最终也会被转换成一个
old / curr的按钮状态 -
然后直接交给普通按钮的内部状态机处理
也就是说,组合键在底层其实被看成了一个虚拟按钮。
这个虚拟按钮:
-
当前成员都按下时,等价于按下
-
成员不全时,等价于松开
一旦变成这样,它就能复用普通按钮那套:
-
消抖
-
单击
-
双击
-
长按
结合例程,完整走一遍 BTN0 + BTN1
假设你当前按的是 COPY = BTN0 + BTN1,整个流程如下:
app_ebtn_init()里先调用:
ebtn_set_config(EBTN_CFG_COMBO_PRIORITY);
这表示后面扫描时按“组合键优先”来。
-
btns_combo[0]之前已经绑定了BTN0和BTN1,所以它的comb_key已经准备好了。 -
运行到
ebtn_process()时,库先得到当前真实状态:
curr_state = [1, 1, 0, 0, 0, 0]
- 库先扫描组合键,发现
COPY的comb_key也是:
comb_key = [1, 1, 0, 0, 0, 0]
- 两者匹配,说明
COPY当前成立,于是:
combo_active = [1, 1, 0, 0, 0, 0]
- 接下来处理普通按键时:
-
BTN0发现自己在combo_active里,跳过 -
BTN1发现自己也在combo_active里,跳过
-
最后处理组合键对象,
COPY这个虚拟按钮完成自己的单击判断。 -
回调里最终只触发:
case USER_BUTTON_COMBO_COPY:
而不会再触发:
case USER_BUTTON_0:
case USER_BUTTON_1:
如果不开这个配置,会怎样
如果不写:
ebtn_set_config(EBTN_CFG_COMBO_PRIORITY);
那后果就是:
-
库不会先建立
combo_active -
普通按键不会提前跳过
-
BTN0和BTN1可能按单键各自处理 -
最后组合键
COPY又可能再处理一次
这就是你之前担心的“单键和组合键冲突”。
也就是:
-
单键先动了
-
组合键后面又动了
-
同一次操作触发了两套逻辑
它和我之前 pending 方案的关系
你之前自己写的思路是:
-
单键事件先来
-
但先不立刻执行
-
先挂到
single_click_pending[] -
如果后面判断出其实是组合键,就把对应的
pending清掉
这是一种应用层补偿。
例程现在做的是库层预处理:
-
先扫组合键
-
先把组合成员标出来
-
单键这轮直接跳过
-
根本不让冲突单键进入后续处理
所以两者目标一样,但层级不一样:
-
我的方案:先挂起,再撤销
-
例程方案:先识别,再跳过
这部分最该记住的结论
-
ebtn_set_config(EBTN_CFG_COMBO_PRIORITY);只是开关,真正逻辑在扫描函数里。 -
comb_key记录一个组合键需要哪些成员。 -
combo_active记录这一轮哪些普通键已经被组合键占用了。 -
开启组合键优先后,库会先扫描组合,再跳过相关单键,最后只处理组合键事件。
-
这套写法本质上就是把你之前手动做的冲突裁决下沉到库内部了。
优化版 ebtn 库还值得补充记住的点
ebtn_set_config() 一定要放在 ebtn_init() 后面
优化版库在初始化时,会把整个默认对象清零:
memset(ebtobj, 0x00, sizeof(*ebtobj));
...
ebtobj->config = 0;
这表示:
-
只要重新调用一次
ebtn_init() -
前面设置过的配置就会被清掉
所以正确顺序必须是:
ebtn_init(...);
ebtn_set_config(EBTN_CFG_COMBO_PRIORITY);
不能反过来写。
ebtn_set_config() 是覆盖,不是累加
底层实现是:
void ebtn_set_config(uint8_t cfg_flags)
{
ebtobj->config = cfg_flags;
}
注意这里是直接赋值,不是:
ebtobj->config |= cfg_flags;
这意味着:
-
如果以后这个库再扩展出别的
EBTN_CFG_xxx -
你想同时打开多个配置,就要一次性组合好再传进去
例如:
ebtn_set_config(FLAG_A | FLAG_B);
而不是分两次调,不然前一次会被后一次覆盖掉。
组合键判断是“包含即可成立”,不是“必须刚好这几个键”
底层判断核心是:
bit_array_and(tmp_data, curr_state, comb_key, EBTN_MAX_KEYNUM);
uint8_t curr = bit_array_cmp(tmp_data, comb_key, EBTN_MAX_KEYNUM) == 0;
这段逻辑的含义是:
-
当前按下状态里
-
只要已经包含了这个组合键要求的所有成员
-
就算这个组合成立
所以如果 COPY = BTN0 + BTN1,下面两种情况都会成立:
-
BTN0 + BTN1 -
BTN0 + BTN1 + BTN4
这个点非常值得记住,因为它不是严格精确匹配,而是至少包含这些成员。
如果自己定义了重叠组合键,可能会同时触发
比如你以后自己写:
-
A = BTN0 + BTN1 -
B = BTN0 + BTN1 + BTN2
那么当你按下 BTN0 + BTN1 + BTN2 时:
-
A会成立 -
B也会成立
原因不是库写错了,而是这个库目前没有做“最长组合优先”或“更具体组合覆盖更短组合”的裁决逻辑。
所以后面自己设计组合键时,最好记住:
-
优先避免写出彼此重叠的组合键
-
如果必须重叠,就要自己在回调层再补一层优先级判断
这个优化版库不只支持静态组合键,还支持动态组合键
除了这种静态数组:
static ebtn_btn_combo_t btns_combo[] = { ... };
它还提供了动态组合键结构体和注册接口:
typedef struct ebtn_btn_combo_dyn
int ebtn_combo_register(ebtn_btn_combo_dyn_t *button)
这说明作者做这个库时,不只是给这一个例程用,而是希望它能支持:
-
编译期写死的组合键
-
运行时再动态挂进去的组合键
组合键也会被算进“当前是否还在处理中”
库里有一段会检查:
if (ebtn_is_btn_in_process(&ebtobj->btns_combo[i].btn))
{
return 1;
}
这表示:
-
普通按键如果还在连击窗口、消抖窗口、状态机处理中,会被算进去
-
组合键也一样
也就是说,在这个库作者的设计里:
-
组合键不是附属品
-
它和普通键一样,也是完整的按钮状态机对象
这部分最终该记住什么
-
ebtn_set_config(EBTN_CFG_COMBO_PRIORITY)要在ebtn_init()之后调用。 -
ebtn_set_config()是覆盖赋值,不是按位累加。 -
组合键成立条件是“包含所需成员”,不是“只能有这些成员”。
-
重叠组合键可能同时成立,库本身没有帮你做更高级的裁决。
-
这个优化版库已经具备动态组合键能力,说明它是按通用库思路设计的。
-
组合键在这个库里本质上就是一个完整的虚拟按钮对象。
本节结论
这节最该真正吃透的不是 copy / paste / cut 业务本身,而是下面这条主线:
- 组合键冲突不是靠“侥幸事件顺序”解决的,而是要明确谁优先、谁让路。
如果你只站在应用层写功能:
- 可以像我之前那样,用
pending先挂起单键,再手动取消冲突。
如果你往正式工程思路走:
-
更推荐把冲突裁决下沉到按键库里。
-
也就是:先扫组合键,再屏蔽组合成员的单键事件,最后只放组合键事件出来。
当前工程经验
-
以后只要一个按键既有单键功能、又参与组合键,就先想到“会不会冲突”。
-
只要库已经提供了组合键优先机制,优先用库能力,不要急着在应用层堆状态补丁。
-
组合键最好单独配参数,尤其是只做单击时,不要和普通键完全共用一套配置。
-
自己设计组合键时,尽量避免重叠组合,不然后面还要自己补优先级。
后续学习建议
-
下一步可以再自己手推一遍:按下
BTN0 + BTN1时,curr_state、comb_key、combo_active分别是什么。 -
如果后面要把这套思路迁到比赛工程,优先先把 HAL 相关部分替换成你自己的 GPIO 读状态和毫秒时基接口。
-
真正上板前,再检查一遍按键映射、消抖参数、组合键是否重叠,不要只看逻辑对不对。