ebtn课后作业
这一题主要是练习组合键的识别,以及组合键和单键冲突时该怎么处理。题目本身不难,重点是先明确每个组合键要完成什么功能,再想办法避免“按下组合键时,里面的单键也被误触发”。
题目要求
-
按键1 + 按键2:复制当前 LED 状态
-
按键1 + 按键3:粘贴之前保存的 LED 状态
-
按键1 + 按键4:剪切当前 LED 状态
这里的“复制、粘贴、剪切”操作的对象,不是某一个 LED,而是整组 ucLed[6] 的状态。
可以把下面这个数组理解成“剪贴板”:
static uint8_t temp_old[6];
它的作用分别是:
-
复制:把当前
ucLed[]保存到temp_old[] -
粘贴:把
temp_old[]恢复到ucLed[] -
剪切:先保存到
temp_old[],再把ucLed[]清零
这一题的核心难点
结论:难点不是复制和粘贴本身,而是组合键冲突。
因为 按键1 + 按键2、按键1 + 按键3、按键1 + 按键4 都包含按键1,如果处理不好,就会出现下面这种情况:
-
本来想触发组合键
-
结果系统先把按键1当成普通单键处理了
-
然后又去处理组合键
-
最后 LED 状态就乱了
所以这道题真正要解决的是:
-
正确识别组合键
-
组合键触发后,屏蔽这次按压过程中的单键动作
-
把 LED 状态保存到一个缓冲区,后续再恢复
先记住这道题的处理顺序
这一题最关键的不是代码细节,而是处理顺序:
-
当检测到有按键按下时,先判断是不是组合键
-
如果这一轮已经触发了组合键,就不要再执行单键逻辑
-
只有当所有按键都松开后,才开始下一轮判断
也可以直接记成一句话:
先判组合键,后判单键;组合键一旦成立,就屏蔽本轮单键。
方案一:使用 ebtn 库处理组合键
这一版的思路是:
-
先定义普通按键和组合键
-
用
ebtn_combo_btn_add_btn()指定哪些键组成组合键 -
在事件回调里分别处理复制、粘贴、剪切
-
组合键触发后,用
last_combo_tick在短时间内屏蔽普通单键事件
组合键对应关系是:
-
USER_BUTTON_COMBO_0→KEY1 + KEY2→ 复制 -
USER_BUTTON_COMBO_1→KEY1 + KEY3→ 粘贴 -
USER_BUTTON_COMBO_2→KEY1 + KEY4→ 剪切
#include "btn_app.h"
#include "ebtn.h"
static const ebtn_btn_param_t defaul_ebtn_param = EBTN_PARAMS_INIT(20, 0, 20, 2000, 200, 500, 10);
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_0 = 0x100,
USER_BUTTON_COMBO_1,
USER_BUTTON_COMBO_2,
USER_BUTTON_COMBO_MAX,
} user_button_t;
static ebtn_btn_t btns[] = {
EBTN_BUTTON_INIT(USER_BUTTON_0, &defaul_ebtn_param),
EBTN_BUTTON_INIT(USER_BUTTON_1, &defaul_ebtn_param),
EBTN_BUTTON_INIT(USER_BUTTON_2, &defaul_ebtn_param),
EBTN_BUTTON_INIT(USER_BUTTON_3, &defaul_ebtn_param),
EBTN_BUTTON_INIT(USER_BUTTON_4, &defaul_ebtn_param),
EBTN_BUTTON_INIT(USER_BUTTON_5, &defaul_ebtn_param),
};
static ebtn_btn_combo_t btns_combo[] = {
EBTN_BUTTON_COMBO_INIT(USER_BUTTON_COMBO_0, &defaul_ebtn_param),
EBTN_BUTTON_COMBO_INIT(USER_BUTTON_COMBO_1, &defaul_ebtn_param),
EBTN_BUTTON_COMBO_INIT(USER_BUTTON_COMBO_2, &defaul_ebtn_param),
};
void comb_key_init(void){
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);
}
uint8_t prv_btn_get_state(struct ebtn_btn *btn)
{
switch (btn->key_id)
{
case USER_BUTTON_0:
return !HAL_GPIO_ReadPin(GPIOE, KEY1_Pin);
case USER_BUTTON_1:
return !HAL_GPIO_ReadPin(GPIOE, KEY2_Pin);
case USER_BUTTON_2:
return !HAL_GPIO_ReadPin(GPIOE, KEY3_Pin);
case USER_BUTTON_3:
return !HAL_GPIO_ReadPin(GPIOE, KEY4_Pin);
case USER_BUTTON_4:
return !HAL_GPIO_ReadPin(GPIOE, KEY5_Pin);
case USER_BUTTON_5:
return !HAL_GPIO_ReadPin(KEY6_GPIO_Port, KEY6_Pin);
default:
return 0;
}
}
void prv_btn_event(struct ebtn_btn *btn, ebtn_evt_t evt){
static uint8_t temp_old[6] = {0, 0, 0, 0, 0, 0};
static uint32_t last_combo_tick = 0;
if (evt == EBTN_EVT_ONCLICK)
{
uint16_t click_cnt = ebtn_click_get_count(btn);
if (HAL_GetTick() - last_combo_tick < 100)
return;
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;
default:
break;
}
}
if (btn->key_id == USER_BUTTON_COMBO_0) {
last_combo_tick = HAL_GetTick();
if (evt == EBTN_EVT_ONCLICK) {
temp_old[0] = ucLed[0];
temp_old[1] = ucLed[1];
temp_old[2] = ucLed[2];
temp_old[3] = ucLed[3];
temp_old[4] = ucLed[4];
temp_old[5] = ucLed[5];
}
return;
}
if (btn->key_id == USER_BUTTON_COMBO_1){
last_combo_tick = HAL_GetTick();
if(temp_old[0] == ucLed[0] && temp_old[1] == ucLed[1] &&
temp_old[2] == ucLed[2] && temp_old[3] == ucLed[3] &&
temp_old[4] == ucLed[4] && temp_old[5] == ucLed[5])
return;
else{
if (evt == EBTN_EVT_ONCLICK){
ucLed[0] = temp_old[0];
ucLed[1] = temp_old[1];
ucLed[2] = temp_old[2];
ucLed[3] = temp_old[3];
ucLed[4] = temp_old[4];
ucLed[5] = temp_old[5];
}
}
return;
}
if(btn->key_id == USER_BUTTON_COMBO_2){
last_combo_tick = HAL_GetTick();
if (evt == EBTN_EVT_ONCLICK){
temp_old[0] = ucLed[0];
temp_old[1] = ucLed[1];
temp_old[2] = ucLed[2];
temp_old[3] = ucLed[3];
temp_old[4] = ucLed[4];
temp_old[5] = ucLed[5];
ucLed[0] = ucLed[1] = ucLed[2] = ucLed[3] = ucLed[4] = ucLed[5] = 0;
}
}
}
void app_ebtn_init(void){
ebtn_init(btns, EBTN_ARRAY_SIZE(btns), btns_combo, EBTN_ARRAY_SIZE(btns_combo),
prv_btn_get_state, prv_btn_event);
comb_key_init();
}
void btn_task(void)
{
ebtn_process(HAL_GetTick());
}
这一版怎么理解
-
KEY1 + KEY2触发后,把当前ucLed[]复制到temp_old[] -
KEY1 + KEY3触发后,把temp_old[]粘贴回ucLed[] -
KEY1 + KEY4触发后,先保存,再把全部 LED 清零 -
last_combo_tick的作用是:组合键刚触发完,短时间内不让普通单键再执行
这一版适合已经在用 ebtn 库的情况,思路比较自然。
方案二:手写扫描逻辑,组合键触发后屏蔽单键
这一版的关键变量是:
uint8_t combo_flag = 0;
它表示:
-
0:这次按压过程中还没有触发组合键 -
1:这次按压过程中已经触发过组合键,后面就不要再把它当成普通单键处理
这个方法的优点是逻辑很直接,也很适合解释单双键冲突是怎么被屏蔽掉的。
#include "key_app.h"
uint8_t key_val, key_down, key_up, key_old;
uint8_t combo_flag = 0; // 用来标记这次按压过程是否触发过组合键
uint8_t key_read(void){
uint8_t temp = 0;
if(HAL_GPIO_ReadPin(GPIOE, KEY1_Pin) == GPIO_PIN_RESET) temp |= (1 << 0);
if(HAL_GPIO_ReadPin(GPIOE, KEY2_Pin) == GPIO_PIN_RESET) temp |= (1 << 1);
if(HAL_GPIO_ReadPin(GPIOE, KEY3_Pin) == GPIO_PIN_RESET) temp |= (1 << 2);
if(HAL_GPIO_ReadPin(GPIOE, KEY4_Pin) == GPIO_PIN_RESET) temp |= (1 << 3);
if(HAL_GPIO_ReadPin(GPIOE, KEY5_Pin) == GPIO_PIN_RESET) temp |= (1 << 4);
if(HAL_GPIO_ReadPin(KEY6_GPIO_Port, KEY6_Pin) == GPIO_PIN_RESET) temp |= (1 << 5);
return temp;
}
void key_task(void){
uint8_t i = 0;
static uint8_t temp1[6] = {0, 0, 0, 0, 0, 0};
key_val = key_read();
key_down = key_val & (key_val ^ key_old);
key_up = ~key_val & (key_val ^ key_old);
key_old = key_val;
// 1. 先处理组合键
if (key_val != 0 && key_down != 0) {
if ((key_val & 0x03) == 0x03) { // KEY1 + KEY2,复制
combo_flag = 1;
for(i = 0; i < 6; i++) temp1[i] = ucLed[i];
}
else if ((key_val & 0x05) == 0x05) { // KEY1 + KEY3,粘贴
combo_flag = 1;
for(i = 0; i < 6; i++) ucLed[i] = temp1[i];
}
else if ((key_val & 0x09) == 0x09) { // KEY1 + KEY4,剪切
combo_flag = 1;
for(i = 0; i < 6; i++) {
temp1[i] = ucLed[i];
ucLed[i] = 0;
}
}
}
// 2. 再处理单键
// 只有在本次按压过程中没有触发组合键时,松手后才处理单键
if (key_up != 0 && combo_flag == 0) {
switch(key_up){
case 0x01: ucLed[0] ^= 1; break;
case 0x02: ucLed[1] ^= 1; break;
case 0x04: ucLed[2] ^= 1; break;
case 0x08: ucLed[3] ^= 1; break;
case 0x10: ucLed[4] ^= 1; break;
case 0x20: ucLed[5] ^= 1; break;
}
}
// 3. 所有按键都松开后,再清除组合键标记
if (key_val == 0) {
combo_flag = 0;
}
}
这一版怎么理解
结论:这版更适合解释“为什么组合键不会误触发单键”。
处理顺序是:
-
先看有没有组合键
-
如果有,就把
combo_flag置 1 -
后面即使检测到松手,也不执行单键逻辑
-
只有所有按键全松开,才认为本轮操作结束,再把标志位清零
为什么用位运算判断双键不会误触
先看这一句:
if ((key_val & 0x03) == 0x03)
0x03 的二进制是 00000011,表示按键1和按键2这两位都必须为 1。
所以:
-
只按按键1,
key_val = 0x01,不满足条件 -
只按按键2,
key_val = 0x02,不满足条件 -
同时按按键1和按键2,
key_val = 0x03,满足条件
所以从“组合识别”这一步看,单键不会被误认成双键。
但要注意,真正避免误触的关键不只是位运算,而是下面两点:
-
位运算只负责判断“当前哪些键被按下”
-
后面的流程要保证“先判组合键,再决定要不要处理单键”
也就是说,位运算负责识别,逻辑顺序负责防冲突。
一个容易忽略的小点
现在写法是:
if ((key_val & 0x03) == 0x03)
它表示的是“按键1和按键2都按下了”,但不限制是不是还按了别的键。
比如如果以后出现:
- 按键1 + 按键2 + 按键3
它同样也会满足这个条件。
如果想写得更严格,只允许“恰好这两个键”,可以写成:
if (key_val == 0x03) // 只允许 1 + 2
if (key_val == 0x05) // 只允许 1 + 3
if (key_val == 0x09) // 只允许 1 + 4
为什么这套逻辑不会把双键误当成单键
核心原因是:处理顺序对了。
这套代码不是一检测到按键就立刻执行单键,而是分成两步:
-
按下过程中,先判断当前是不是组合键
-
松手时,再判断这一轮能不能当成单键处理
如果这一轮中间已经触发过组合键,就会把:
combo_flag = 1;
后面即使出现 key_up,也不会进入单键逻辑:
if (key_up != 0 && combo_flag == 0)
所以它的效果就是:
-
本来想按
1 + 2 -
系统先识别出这是组合键
-
然后给这一轮按压打上“已经触发过组合键”的标记
-
后面松手时,不再把它当成单键处理
这就是为什么不会出现“组合键里的按键1也顺手触发了单键功能”。
除了这种逻辑屏蔽法,还有哪些解决办法
除了现在这种“先判组合键,再屏蔽本轮单键”的办法,常见还有下面几种思路。
1. 延时确认单键
思路是:
-
按下单键时先不马上执行
-
先等一个很短的时间
-
如果这段时间里又来了第二个键,就判成组合键
-
如果没有,再把它当成单键
优点是实现比较直观,缺点是单键响应会慢一点。
2. 组合键优先级更高
思路是:
-
只要组合键成立,就直接吞掉本轮单键事件
-
单键只有在没有触发组合键时才允许执行
你现在这份代码本质上就是这一类。
3. 按下和松开分开处理
思路是:
-
按下时只负责判断是不是组合键
-
松开时再决定单键要不要执行
这也是这道题里很实用的一种方式,因为它天然给组合键留出了识别时间。
4. 改交互方式
如果项目不是死规定必须用双键,也可以改成:
-
长按 + 单键
-
功能键 + 普通键
-
进入某个模式后再执行复制粘贴剪切
这种方法能减少冲突,但不一定符合这道题的要求。
5. 硬件辅助
例如:
-
按键消抖
-
矩阵键盘加二极管
-
改善扫描电路
这些方法能减少误判,但主要解决的是信号稳定性问题,不能单独解决单双键功能冲突。
memcpy 的使用
如果只是想保存和恢复整个 LED 数组,用 memcpy() 会比一个一个赋值更简洁。
先包含头文件:
#include <string.h>
1. 复制 LED 状态
memcpy(temp_old, ucLed, sizeof(temp_old));
意思是:把 ucLed 里的 6 个字节,复制到 temp_old。
2. 粘贴 LED 状态
memcpy(ucLed, temp_old, sizeof(temp_old));
意思是:把之前保存的状态,再拷贝回 ucLed。
3. 剪切 LED 状态
memcpy(temp_old, ucLed, sizeof(temp_old));
memset(ucLed, 0, sizeof(ucLed));
意思是:
-
先复制到
temp_old -
再把
ucLed全部清零
适合背的总结
这道题可以直接记成下面几句话:
-
1 + 2是复制,把当前 LED 状态保存起来 -
1 + 3是粘贴,把保存的 LED 状态恢复出来 -
1 + 4是剪切,先保存,再清空当前 LED -
真正难点不是复制粘贴,而是组合键和单键冲突
-
位运算只负责判断哪些键被按下
-
真正避免误触的是“先判组合键,再判单键”的处理顺序
-
常见解决办法有两种:时间窗口屏蔽,或者用标志位记录“本轮已经触发过组合键”
-
如果要复制整个数组,优先想到
memcpy()