5.1 ebtn作业

ebtn作业

来源:

  • 原稿:D:\竞赛\32_learning\西门子嵌入式\note\5.1 ebtn作业.md

  • 上位笔记:D:\竞赛\32_learning\西门子嵌入式\note_codex\5 easy_button.md

  • 当前工程实现:D:\GD32F4_demo\etbn_test\APP\ebtn_app.c

说明:这篇不是把对话记录原样贴过来,而是把这次作业里真正形成的思路整理成可复习的版本。

这节在讲什么

这节不是单纯在做 copy / paste / cut

真正要解决的是:当单键功能和组合键功能共用同一批按键时,怎么避免单键先误触发。

这次作业一共走了两条路:

  • 手写四行代码版:自己维护当前状态、上一拍状态、新按下、新松开

  • ebtn 版:让库负责识别事件,自己只管业务动作和冲突处理

最后形成的结论是:

  • 组合键判断要么在底层状态位上做

  • 要么在事件层做“先挂起,后确认”

作业目标

目标是实现这 3 个组合键功能:

  • 按键0 + 按键1:复制 ucLeducLed_buf

  • 按键0 + 按键2:把 ucLed_buf 粘贴回 ucLed

  • 按键0 + 按键3:先复制,再把 ucLed 全部清零

同时普通按键还要能单独控制 LED。

这就是后面会出现冲突的原因:同一个按键,既参与单键动作,又参与组合键动作。

四行代码版先解决了什么

为什么原来的 paste 不对

原来 key_read() 返回的是普通编号:

 1 2 3 4 5 6 12 13 14

但边沿判断却用了按位思路:

 key_down = key_val & (key_old ^ key_val);
 key_up = ~key_val & (key_old ^ key_val);

这里的问题是:

  • 这套写法更适合“每个键占 1bit”

  • 不适合 12 13 14 这种普通组合编号

所以 13 这种值一旦参与按位运算,结果就可能不是 13paste 自然就不稳定。

后来为什么改成位标志

后来的改法不是简单“换几个数字”,而是把按键编号改成按键状态位:

 #define KEY_PE2 0x01
 #define KEY_PE3 0x02
 #define KEY_PE4 0x04
 #define KEY_PE5 0x08
 #define KEY_PE6 0x10
 #define KEY_PC13 0x20

这样组合键就能直接写成:

 #define KEY_COPY  (KEY_PE2 | KEY_PE3)
 #define KEY_PASTE (KEY_PE2 | KEY_PE4)
 #define KEY_CUT   (KEY_PE2 | KEY_PE5)

这时再判断边沿才顺:

 key_down = key_val & (uint8_t)(~key_old);
 key_up = key_old & (uint8_t)(~key_val);

一句话记住:

前者是“普通编号”,后者是“状态位”。只有状态位才适合后面的按位判断。

四行代码版的优点和边界

优点:

  • 简单直接

  • 能自己完全看懂

  • 很适合先把“新按下 / 新松开”练明白

边界:

  • 组合键和单键冲突处理要自己写

  • 多击、长按、复杂时序会越来越乱

所以它更像“组合键入门版”,不是最终工程版。

从四行代码版切到 ebtn 版,本质换了什么

四行代码版自己维护:

  • 当前键值

  • 上一拍键值

  • 新按下

  • 新松开

ebtn 版把这些底层事交给库:

  • 消抖

  • 单击时序

  • 组合键识别

  • 回调通知

自己只需要关心 3 件事:

  1. 定义普通按键和组合键对象

  2. 把逻辑按键 ID 映射到 GPIO

  3. 在事件回调里写业务动作

可以把 ebtn_app.c 顺着理解成这条链:

  1. btns[]:注册普通按键

  2. btns_combo[]:注册组合键对象

  3. prv_btn_get_state():按键 ID 对应哪个 GPIO

  4. prv_btn_event():收到事件后做什么

  5. app_ebtn_init():把组合键真正绑定起来

  6. btn_task():周期调用 ebtn_process()

这次作业真正的核心坑

坑不在“识别不出组合键”。

坑在:组合键还没来得及最终确认,单键动作可能已经先执行了。

例如:

  • USER_BUTTON_0 + USER_BUTTON_1copy

  • USER_BUTTON_1 本身又有单键翻转 LED 的功能

如果一收到单击事件就立刻:

 ucLed[1] ^= 1;

那用户本来是想做 copy,结果 LED1 也被顺手翻了。

这就是为什么后面一定要多写一层确认。

single_click_pending 到底在干什么

single_click_pending 不是为了“多此一举”,而是为了把单键动作从“立即执行”改成“延后执行”。

思路分两步:

  1. 单键事件先只记下来,不马上改 ucLed

  2. 等本轮 ebtn_process() 把组合键也判断完,再决定这次单键动作要不要真的执行

也就是:

  • 如果后面发现这轮其实是组合键,就取消对应的单键动作

  • 如果后面发现不是组合键,再真正执行单键动作

可以把它理解成“先记账,后结算”。

关键骨架是:

 if (evt == EBTN_EVT_ONCLICK)
 {
     single_click_pending[USER_BUTTON_1] = 1;
 }

如果后面识别出了组合键:

 single_click_pending[USER_BUTTON_0] = 0;
 single_click_pending[USER_BUTTON_1] = 0;

最后在 btn_task() 末尾统一结算:

 ebtn_process(HAL_GetTick());
 ​
 if (single_click_pending[USER_BUTTON_1])
 {
     single_click_pending[USER_BUTTON_1] = 0;
     ucLed[1] ^= 1;
 }

一句话总结:

不是先执行单键,而是先挂起单键,等确认不是组合键后再执行。

USER_BUTTON_0 的理解变化

一开始的想法

一开始的想法是:USER_BUTTON_0 像键盘里的 Ctrl,只做组合功能键,不参与单键动作。

这个想法没错,而且很稳:

  • 写法简单

  • 冲突最少

  • 最不容易出 bug

后来的变化

后来想清楚了一点:

  • USER_BUTTON_0 不是不能有单键功能

  • 而是不能太早执行它的单键功能

只要也把 USER_BUTTON_0 纳入 single_click_pending 这一套:

  • 单独按 USER_BUTTON_0 时,最后再执行 ucLed[0] ^= 1

  • 参与 0+1 / 0+2 / 0+3 时,就把它的单键挂起取消

这样它就既能:

  • 像普通键一样单独控制 ucLed[0]

  • 又能像功能键一样参与组合键

最后形成的结论是:

不是 USER_BUTTON_0 不能有单键功能,而是不能在组合键还没判清之前就先执行。

当前 ebtn 版实现思路

普通按键与组合键对象

普通按键用 btns[] 注册,组合键对象用 btns_combo[] 注册。

组合键 ID 单独用:

 USER_BUTTON_COMBO_COPY
 USER_BUTTON_COMBO_PASTE
 USER_BUTTON_COMBO_CUT

这样业务层读代码时,能明确区分:

  • 这是物理按键

  • 还是逻辑上的组合键

组合键绑定

真正的组合关系不是在 btns_combo[] 里写死,而是在初始化里绑定:

 ebtn_combo_btn_add_btn(&btns_combo[0], USER_BUTTON_0);
 ebtn_combo_btn_add_btn(&btns_combo[0], USER_BUTTON_1);

它的意思是:

  • btns_combo[0] 这个组合键

  • USER_BUTTON_0USER_BUTTON_1 组成

后面两组同理:

  • btns_combo[1] = 0 + 2

  • btns_combo[2] = 0 + 3

事件回调怎么分工

prv_btn_event() 里只处理 EBTN_EVT_ONCLICK

这里的分工是:

  • 单键 0/1/2/3:先挂起

  • 单键 4/5:没有组合冲突,可以直接翻转

  • 组合键 copy / paste / cut:一边执行业务,一边取消本轮相关单键

这也是为什么 ebtn 版虽然用了库,但应用层逻辑还是不能偷懒。

库能帮你判断事件,不会替你决定“单键和组合键冲突时谁优先”。

手写版和 ebtn 版怎么选

场景 更适合的做法 原因
想先搞懂边沿检测 四行代码版 最直接,最容易看懂
只有简单单键翻转 四行代码版 成本低
要做组合键、多击、长按 ebtn 底层事件管理更稳
多个按键共用单键和组合键功能 ebtn 版 + 挂起确认 更容易把冲突理顺

本节结论

  • 组合键问题不只是“怎么识别”,更是“识别完成前,单键会不会先执行”。

  • 手写四行代码版的核心收获是:按键一旦进组合逻辑,就要把普通编号改成位标志。

  • ebtn 版的核心收获是:库只负责判断事件,单键和组合键的优先级仍然要自己定。

  • single_click_pending 这层的意义,就是把“立即执行单键”改成“确认后执行单键”。

  • USER_BUTTON_0 不一定非得像 Ctrl 一样只能做功能键,只要挂起再确认,它也可以兼顾单键功能。

后续学习建议

  • 再看一次 D:\GD32F4_demo\etbn_test\APP\ebtn_app.c,顺着“注册 → 映射 → 回调 → 初始化 → 任务”读一遍。

  • 自己尝试再加一个组合键,例如 0+4,验证这套“挂起再确认”的逻辑是否还能复用。

  • 如果后面要做长按或双击,不要急着改业务层,先确认 ebtn 事件本身什么时候上报,再决定业务动作放在哪一层。