ebtn课后作业

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 状态就乱了

所以这道题真正要解决的是:

  1. 正确识别组合键

  2. 组合键触发后,屏蔽这次按压过程中的单键动作

  3. 把 LED 状态保存到一个缓冲区,后续再恢复

先记住这道题的处理顺序

这一题最关键的不是代码细节,而是处理顺序:

  1. 当检测到有按键按下时,先判断是不是组合键

  2. 如果这一轮已经触发了组合键,就不要再执行单键逻辑

  3. 只有当所有按键都松开后,才开始下一轮判断

也可以直接记成一句话:

先判组合键,后判单键;组合键一旦成立,就屏蔽本轮单键。

方案一:使用 ebtn 库处理组合键

这一版的思路是:

  • 先定义普通按键和组合键

  • ebtn_combo_btn_add_btn() 指定哪些键组成组合键

  • 在事件回调里分别处理复制、粘贴、剪切

  • 组合键触发后,用 last_combo_tick 在短时间内屏蔽普通单键事件

组合键对应关系是:

  • USER_BUTTON_COMBO_0KEY1 + KEY2 → 复制

  • USER_BUTTON_COMBO_1KEY1 + KEY3 → 粘贴

  • USER_BUTTON_COMBO_2KEY1 + 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;
     }
 }

这一版怎么理解

结论:这版更适合解释“为什么组合键不会误触发单键”。

处理顺序是:

  1. 先看有没有组合键

  2. 如果有,就把 combo_flag 置 1

  3. 后面即使检测到松手,也不执行单键逻辑

  4. 只有所有按键全松开,才认为本轮操作结束,再把标志位清零

为什么用位运算判断双键不会误触

先看这一句:

 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

为什么这套逻辑不会把双键误当成单键

核心原因是:处理顺序对了。

这套代码不是一检测到按键就立刻执行单键,而是分成两步:

  1. 按下过程中,先判断当前是不是组合键

  2. 松手时,再判断这一轮能不能当成单键处理

如果这一轮中间已经触发过组合键,就会把:

 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()