版权信息
话不多说直接上代码
1 2 3 4 5 6 7 uchar cont,trg; void KeyScan () { uchar ReadData = PINB^0xff ; trg = ReadData & (ReadData^cont); cont = ReadData; }
就完事了,灰常的amzing啊。
尝试理解
首先我们要理解PINB
,这里的PINB
指的是我们的单片机GPIOB的所有Pin的端口数据,而每个端口的数据(高/低——1/0)对应1个bit位。例如某单片机有八个端口只有端口0为高,其余皆为低,则PINB=0000 0001。在STM32Hal库中,我们可以这样实现:
1 2 3 4 5 6 7 8 9 10 11 uint8_t KeyBit (void ) { uint8_t keybit = 0x00 ; keybit |= HAL_GPIO_ReadPin(B1_GPIO_Port,B1_Pin) << 0 ; keybit |= HAL_GPIO_ReadPin(B2_GPIO_Port,B2_Pin) << 1 ; keybit |= HAL_GPIO_ReadPin(B3_GPIO_Port,B3_Pin) << 2 ; keybit |= HAL_GPIO_ReadPin(B4_GPIO_Port,B4_Pin) << 3 ; return keybit; }
取反
第一行代码就相当于是取反操作,使没有按键按下时data始终为0x00。
注意,不一定就是异或0xff,按照上面的hal库方法,你的PINB变量实际上只有四位有效位,为了让没有按键按下时data为0x00,我们异或0x0f即可,或者将keybit初始化为0xf0然后异或0xff;而有时GPIO输入接了下拉,没有输入的情况本来就是0,这时就不需要再取反。反正就是要让它没有按键按下时等于0x00。
点击即看异或真值表
与0异或不变,与1异或取反
A
B
A ⊕ B
0
0
0
0
1
1
1
0
1
1
1
0
单击按下
第一次PB0按下的情况
端口数据原本为0xfe,ReadData读端口并且取反,变为 0x01 (第一行)。
因为这是第一次按下,所以Cont是上次的值,应为为0。与0异或不改变原有值,那么第二行执行的实际是:
1 Trg = 0x01 & (0x01^0x00) = 0x01
将cont赋值
结果就是:
ReadData = 0x01
Trg = 0x01
Cont = 0x01
按下后还没有完全松开(引申到长按)
此时程序依然在执行。
第一行:不变,依然为0x01。
第二行:操作为——把检测到的按下按钮对应的bit为给置0了,让Trg等于0了
第三行:不变,依然为0x01。
我们可以看到唯一变了的地方就是Trg被置0了。
完全松开
很好理解,肯定全为0x00了。
笔者的思考
该代码的核心思路我认为是用三种不同的数据内容代表不同的三种情况(未按下、单按、长按)按照这种思路,我们甚至还可以扩展。
ReadData
存储原始的端口电平数据,不能改变。后续的两个数据都会基于这个原始数据。
Trg
用于按键触发。ReadData
中有bit位有上升沿时,Trg会记录这个上升沿:只有bit从0变为1,Trg才会置1。按下判断的条件是:Trg
、Cont
均为1。
Cont
用于长按。长按的判断条件是:Trg
为0的情况下Cont
依然为1。
这样的思路让我联想到了3-8译码器的巧妙设计,通过3个值的输入,可以得到8个值的输出,用3个值就可以为8个不同的情况进行处理。
在STM32中使用
附上我的使用方法,我使用的是时间片轮询架构 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 #define B1 0x01 #define B2 0x02 #define B3 0x04 #define B4 0x08 uint8_t KeyBit (void ) { uint8_t keybit = 0x00 ; keybit |= HAL_GPIO_ReadPin(B1_GPIO_Port,B1_Pin) << 0 ; keybit |= HAL_GPIO_ReadPin(B2_GPIO_Port,B2_Pin) << 1 ; keybit |= HAL_GPIO_ReadPin(B3_GPIO_Port,B3_Pin) << 2 ; keybit |= HAL_GPIO_ReadPin(B4_GPIO_Port,B4_Pin) << 3 ; return keybit; } void KeyFuc (void ) { static uint8_t i = 0 ; unsigned char ReadData = KeyBit() ^ 0x0f ; Trg = ReadData & (ReadData ^ Cont); Cont = ReadData; if (Trg && Cont){ switch (Trg){ case B4: Key4_Fuc();break ; case B3: Key3_Fuc();break ; case B2: Key2_Fuc();break ; case B1: Key1_Fuc();break ; default : break ; } } if (Cont && 0 == Trg) i++; else i = 0 ; if (i >= 20 ){ i = 0 ; switch (Cont) { case B3: Key3_Fuc_Preesed();break ; default : break ; } } }
关于消抖问题
不知道为什么,按理来说是需要消抖的,但是实际跑起来的时候没有消抖也非常稳,,,可能是我的按键检测任务执行间隔比较长?
2025.03.28 补充修改:如果使用时间片轮询调度,理论上如果按键扫描任务执行间隔时间较长,是不用消抖的。这就解释了为什么我的程序没有消抖也那么稳!例如,假设你设置的时间片调度间隔是 100ms,如果按键每次扫描时是 100ms 执行一次,假设按键抖动的周期是 10ms,那么每次扫描时按键就已经稳定了。这样,按键的物理状态变化就不太可能被误判为多个状态变化,因为时间间隔本身已经足够长。当然代价是 会牺牲按键响应速度,以及无法应用快速连按的按键逻辑
关于消抖我还没有实际上机跑过,但是也写一下,Deepseek写的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 #define DEBOUNCE_TIME 1 typedef enum { KEY_STATE_IDLE, KEY_STATE_PRESSED, KEY_STATE_RELEASED } KeyState; KeyState key_state = KEY_STATE_IDLE; uint16_t debounce_timer = 0 ;void KeyScan () { uchar ReadData = PINB ^ 0xff ; uchar new_trg = ReadData & (ReadData ^ cont); switch (key_state) { case KEY_STATE_IDLE: if (new_trg != 0 ) { key_state = KEY_STATE_PRESSED; debounce_timer = DEBOUNCE_TIME; } break ; case KEY_STATE_PRESSED: if (debounce_timer > 0 ) { debounce_timer --; } else { if (ReadData == (PINB ^ 0xff )) { trg = new_trg; cont = ReadData; if (trg && cont){ } key_state = KEY_STATE_RELEASED; } else { key_state = KEY_STATE_IDLE; } } break ; case KEY_STATE_RELEASED: if (new_trg == 0 ) { key_state = KEY_STATE_IDLE; } break ; } }