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:复制
ucLed到ucLed_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 这种值一旦参与按位运算,结果就可能不是 13,paste 自然就不稳定。
后来为什么改成位标志
后来的改法不是简单“换几个数字”,而是把按键编号改成按键状态位:
#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 件事:
-
定义普通按键和组合键对象
-
把逻辑按键 ID 映射到 GPIO
-
在事件回调里写业务动作
可以把 ebtn_app.c 顺着理解成这条链:
-
btns[]:注册普通按键 -
btns_combo[]:注册组合键对象 -
prv_btn_get_state():按键 ID 对应哪个 GPIO -
prv_btn_event():收到事件后做什么 -
app_ebtn_init():把组合键真正绑定起来 -
btn_task():周期调用ebtn_process()
这次作业真正的核心坑
坑不在“识别不出组合键”。
坑在:组合键还没来得及最终确认,单键动作可能已经先执行了。
例如:
-
USER_BUTTON_0 + USER_BUTTON_1是copy -
但
USER_BUTTON_1本身又有单键翻转 LED 的功能
如果一收到单击事件就立刻:
ucLed[1] ^= 1;
那用户本来是想做 copy,结果 LED1 也被顺手翻了。
这就是为什么后面一定要多写一层确认。
single_click_pending 到底在干什么
single_click_pending 不是为了“多此一举”,而是为了把单键动作从“立即执行”改成“延后执行”。
思路分两步:
-
单键事件先只记下来,不马上改
ucLed -
等本轮
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_0和USER_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事件本身什么时候上报,再决定业务动作放在哪一层。