5.2 讲评的ebtn作业

这次 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());
 }

这份例程的学习顺序要怎么抓

这份代码建议按下面顺序看,不要一上来就钻回调细节:

  1. 先看普通键和组合键是怎么注册的。

  2. 再看 app_ebtn_init() 里怎么初始化、开配置、绑定组合成员。

  3. 再看 prv_btn_event() 里每个键最后触发了什么功能。

  4. 最后再回到底层库里,看 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 那几行,而是这两件事:

  1. 打开组合键优先
 ebtn_set_config(EBTN_CFG_COMBO_PRIORITY);

这表示:如果 USER_BUTTON_0 既有单键功能,又参与 0+10+20+3 这些组合键,那么库会优先尝试把这次操作判成组合键,而不是先触发 USER_BUTTON_0 的单键逻辑。

  1. 组合键只监听单击
 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];

思路是:

  1. USER_BUTTON_0 ~ USER_BUTTON_3 在单击时先不立刻改 LED。

  2. 先记成 pending,表示这次可能要执行单键动作。

  3. 如果这一轮后面又识别出 0+10+20+3 是组合键,就把对应的 pending 清零。

  4. 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 来说:

  1. 先把 BTN0 对应位置设成 1

  2. 再把 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:这一轮扫描里,哪些普通按键已经被某个组合键占用了

比如:

  • COPYcomb_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 表示:

  • 当前这一刻,所有普通按键的真实按下情况

比如当前你按下了 BTN0BTN1

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;

组合成立后为什么要 orcombo_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,整个流程如下:

  1. app_ebtn_init() 里先调用:
ebtn_set_config(EBTN_CFG_COMBO_PRIORITY);

这表示后面扫描时按“组合键优先”来。

  1. btns_combo[0] 之前已经绑定了 BTN0BTN1,所以它的 comb_key 已经准备好了。

  2. 运行到 ebtn_process() 时,库先得到当前真实状态:

curr_state = [1, 1, 0, 0, 0, 0]
  1. 库先扫描组合键,发现 COPYcomb_key 也是:
comb_key = [1, 1, 0, 0, 0, 0]
  1. 两者匹配,说明 COPY 当前成立,于是:
combo_active = [1, 1, 0, 0, 0, 0]
  1. 接下来处理普通按键时:
  • BTN0 发现自己在 combo_active 里,跳过

  • BTN1 发现自己也在 combo_active 里,跳过

  1. 最后处理组合键对象,COPY 这个虚拟按钮完成自己的单击判断。

  2. 回调里最终只触发:

case USER_BUTTON_COMBO_COPY:

而不会再触发:

case USER_BUTTON_0:
case USER_BUTTON_1:

如果不开这个配置,会怎样

如果不写:

ebtn_set_config(EBTN_CFG_COMBO_PRIORITY);

那后果就是:

  • 库不会先建立 combo_active

  • 普通按键不会提前跳过

  • BTN0BTN1 可能按单键各自处理

  • 最后组合键 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_statecomb_keycombo_active 分别是什么。

  • 如果后面要把这套思路迁到比赛工程,优先先把 HAL 相关部分替换成你自己的 GPIO 读状态和毫秒时基接口。

  • 真正上板前,再检查一遍按键映射、消抖参数、组合键是否重叠,不要只看逻辑对不对。