1. 话不多说直接上代码
  2. 尝试理解
    1. 取反
    2. 单击按下
    3. 按下后还没有完全松开(引申到长按)
    4. 完全松开
    5. 笔者的思考
  3. 在STM32中使用
  4. 关于消抖问题

三行代码实现按键的长短按检测

版权信息

本文章遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。


话不多说直接上代码

1
2
3
4
5
6
7
uchar cont,trg;    //triger触发 continue连续
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;
    //读取B1的电平状态并把他左移到bit0位
    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赋值

1
Cont = ReadData = 0x01

结果就是:

  • 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。按下判断的条件是:TrgCont均为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;

    //读取B1的电平状态并把他左移到bit0位
    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;       // 取反:在没有按键按下的情况下,其始终为0x00

    Trg = ReadData & (ReadData ^ Cont);             // 2

    Cont = ReadData;                                // 3                        

    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;
}
}