C51学习-DAY8

下面这版已经整理成 CSDN Markdown 技术博客格式 ,可以直接复制粘贴发布。内容统一描述为"常规小项目 / 小型控制项目",没有出现你要求避开的词,并且把 raw_levelkey_stable_level 优化成了更准确的 bit 类型。

51单片机C语言重新入门:第八天学习非阻塞按键扫描、消抖、短按和长按

前言

这是我重新学习 51 单片机 C 语言的第八天笔记。

前面七天主要学习了:

text 复制代码
第一天:C51 程序结构、main()、while(1)、LED 闪烁
第二天:数据类型、变量、bit、unsigned char、unsigned int
第三天:sfr、sbit、寄存器、位操作
第四天:GPIO 输出控制、高低电平、IO 模式
第五天:GPIO 输入、按键读取、简单消抖
第六天:函数封装、模块化代码结构、.h/.c 文件分离
第七天:定时器、中断、1ms 系统节拍、非阻塞延时

第八天开始,把第五天和第七天的知识结合起来:

text 复制代码
第五天:按键读取 + Delay_ms(20) 消抖
第七天:Timer0 + 1ms 系统节拍
第八天:用定时器节拍实现非阻塞按键消抖

之前的按键消抖方式是:

c 复制代码
if(KEY == 0)
{
    Delay_ms(20);

    if(KEY == 0)
    {
        // 确认按下
    }
}

这种方式简单,但是会阻塞程序。

在常规小项目中,程序可能同时要处理:

text 复制代码
LED 闪烁
按键扫描
状态切换
电池检测
串口通信
参数保存
低功耗判断

如果一直使用 Delay_ms(),主程序就会经常被卡住。

所以第八天开始学习更接近实际项目的写法:

text 复制代码
定时扫描
非阻塞消抖
按键事件
短按识别
长按识别

一、第八天学习目标

第八天的学习目标是:

text 复制代码
理解为什么要升级按键消抖
理解阻塞消抖和非阻塞消抖的区别
理解什么是按键状态
理解什么是按键事件
理解 Key_Task() 的作用
理解 Key_GetEvent() 的作用
能用 10ms 周期扫描按键
能用连续多次稳定判断实现消抖
能识别短按事件
能识别长按事件
能理解 bit、unsigned char、unsigned int 在按键模块中的使用

今天最重要的一句话是:

非阻塞按键扫描的核心,是用定时器节拍固定周期读取按键状态,通过连续稳定判断实现消抖,再把按键动作转换成短按、长按等事件。


二、为什么要升级按键消抖?

第五天学习的按键消抖是这种写法:

c 复制代码
bit Key_IsPressed_Debounce(void)
{
    if(KEY == KEY_PRESS_LEVEL)
    {
        Delay_ms(20);

        if(KEY == KEY_PRESS_LEVEL)
        {
            return 1;
        }
    }

    return 0;
}

它的思路是:

text 复制代码
第一次检测到按下
延时 20ms
再检测一次
如果还是按下,就认为按键真的按下

这种方法非常适合入门学习。

但是它有一个明显问题:

text 复制代码
Delay_ms(20) 会阻塞程序。

也就是说,程序在这 20ms 内基本停住了,不能及时处理其他任务。

如果项目中只有一个 LED 和一个按键,问题不大。

但是如果项目功能稍微复杂一点,例如同时要处理多个周期任务,就不推荐大量使用阻塞式延时。


三、什么是阻塞式按键消抖?

阻塞式按键消抖就是:

text 复制代码
程序发现按键可能按下后,原地等待一段时间,再确认一次。

典型代码:

c 复制代码
if(KEY == 0)
{
    Delay_ms(20);

    if(KEY == 0)
    {
        // 按键确认按下
    }
}

这种方式的特点是:

text 复制代码
简单
容易理解
代码短
适合初学实验

但是缺点也明显:

text 复制代码
Delay_ms() 期间主程序会停住
多个任务不好同时运行
按键多了以后程序会越来越乱
不适合较完整的小项目

四、什么是非阻塞按键消抖?

非阻塞按键消抖的思路是:

不在按键函数里原地等待,而是每隔固定时间读取一次按键状态,根据连续几次读取结果判断按键是否稳定。

例如:

text 复制代码
每 10ms 扫描一次按键
如果连续 2 次都检测到按下
就认为按键真的按下

这样就相当于实现了约 20ms 消抖。

但是程序不会卡在按键函数里。

主循环还可以继续处理其他任务。


五、阻塞消抖和非阻塞消抖对比

1. 阻塞消抖

c 复制代码
if(KEY == 0)
{
    Delay_ms(20);

    if(KEY == 0)
    {
        return 1;
    }
}

特点:

text 复制代码
直接等待 20ms
代码简单
但是会卡住程序

2. 非阻塞消抖

text 复制代码
每 10ms 读取一次按键
连续 2 次读到同样变化
才确认状态改变

特点:

text 复制代码
不会卡住程序
适合多任务
可以扩展短按、长按、双击
更接近实际项目写法

六、什么是按键状态?

按键状态描述的是:

text 复制代码
当前按键处于什么状态

例如:

text 复制代码
当前是松开状态
当前是按下状态

可以用代码表示:

c 复制代码
#define KEY_STATE_RELEASED    0
#define KEY_STATE_PRESSED     1

在今天的代码中,我们没有直接使用 KEY_STATE_RELEASEDKEY_STATE_PRESSED,而是用电平表示状态:

c 复制代码
#define KEY_PRESS_LEVEL      0
#define KEY_RELEASE_LEVEL    1

因为按键硬件接法是:

text 复制代码
松开 = 1
按下 = 0

七、什么是按键事件?

按键事件描述的是:

text 复制代码
刚刚发生了什么按键动作

例如:

text 复制代码
刚刚短按了一次
刚刚长按了一次

在今天的代码中定义为:

c 复制代码
#define KEY_EVENT_NONE     0
#define KEY_EVENT_SHORT    1
#define KEY_EVENT_LONG     2

含义如下:

事件 含义
KEY_EVENT_NONE 没有事件
KEY_EVENT_SHORT 短按事件
KEY_EVENT_LONG 长按事件

八、按键状态和按键事件的区别

这两个概念一定要分清楚。

1. 按键状态

状态表示:

text 复制代码
现在是什么样

例如:

text 复制代码
现在按键按下
现在按键松开

2. 按键事件

事件表示:

text 复制代码
刚刚发生了什么动作

例如:

text 复制代码
刚刚完成一次短按
刚刚触发一次长按

3. 通俗理解

状态像"姿势"。

事件像"动作"。

比如一个人:

text 复制代码
站着:状态
坐着:状态
从站着变成坐着:事件
从坐着变成站着:事件

按键也是一样:

text 复制代码
按下:状态
松开:状态
从松开变成按下:按下变化
从按下变成松开:松开变化
按下后很快松开:短按事件
按住超过设定时间:长按事件

九、第八天小项目目标

今天实现一个更规范的按键模块。

功能目标:

text 复制代码
KEY 接 P3.2
按下为低电平
Timer0 已经提供 1ms 系统节拍
每 10ms 扫描一次按键
实现非阻塞消抖
识别短按事件
识别长按事件
短按:LED 翻转一次
长按:LED 点亮

今天先以一个按键为例。

后续可以扩展为多个按键。


十、按键扫描周期为什么选择10ms?

机械按键在按下或松开时会有抖动。

抖动时间一般在几毫秒到十几毫秒左右。

常见扫描周期可以是:

text 复制代码
5ms
10ms
20ms

今天为了简单,使用:

text 复制代码
10ms 扫描一次

如果连续 2 次都检测到状态变化,就相当于确认了约 20ms 的稳定变化。

计算方式是:

text 复制代码
消抖时间 = 扫描周期 × 消抖次数

今天设置为:

text 复制代码
10ms × 2 = 20ms

十一、按键有效电平定义

假设按键接法是:

text 复制代码
IO 上拉
按键接 GND

也就是:

text 复制代码
松开 = 1
按下 = 0

所以代码中定义:

c 复制代码
#define KEY_PRESS_LEVEL      0
#define KEY_RELEASE_LEVEL    1

这样写的好处是:

text 复制代码
如果以后硬件改成按下为高电平,只需要修改宏定义。

例如:

c 复制代码
#define KEY_PRESS_LEVEL      1
#define KEY_RELEASE_LEVEL    0

其他代码尽量不用改。


十二、最简单的周期扫描框架

第七天已经学习了 Timer_GetTick()

主循环中可以这样写:

c 复制代码
unsigned int now_tick = 0;
unsigned int last_key_tick = 0;

while(1)
{
    now_tick = Timer_GetTick();

    if((unsigned int)(now_tick - last_key_tick) >= 10)
    {
        last_key_tick = now_tick;
        Key_Task();
    }
}

这段代码的意思是:

text 复制代码
每隔 10ms 执行一次 Key_Task()
Key_Task() 负责扫描按键和处理消抖

注意:

text 复制代码
Key_Task() 不应该卡住等待
Key_Task() 每次只执行很短一段逻辑
执行完马上返回主循环

这就是非阻塞按键扫描。


十三、Key_Task() 是什么?

Key_Task() 可以理解为:

按键模块的周期处理函数。

它每 10ms 被调用一次。

里面做的事情包括:

text 复制代码
读取当前按键电平
判断是否和稳定状态相同
进行消抖计数
确认按下或松开
累计按下时间
判断短按或长按
生成按键事件

以后在实际小项目中,类似函数会很多,例如:

c 复制代码
void Key_Task(void);
void LED_Task(void);
void Battery_Task(void);
void Mode_Task(void);

这些都是周期任务函数。


十四、非阻塞消抖的基本思路

我们维护几个变量:

c 复制代码
static bit key_stable_level = KEY_RELEASE_LEVEL;
static unsigned char key_debounce_count = 0;

含义如下:

text 复制代码
key_stable_level:当前已经确认稳定的按键电平
key_debounce_count:消抖计数

每次扫描时读取当前电平:

c 复制代码
bit raw_level;

raw_level = KEY;

然后判断:

text 复制代码
如果 raw_level 和 key_stable_level 不同:
    说明按键可能发生变化
    消抖计数加 1
    如果连续多次都不同:
        确认状态真的变化
        更新 key_stable_level

如果 raw_level 和 key_stable_level 相同:
    说明状态稳定
    消抖计数清零

通俗理解:

text 复制代码
不要因为一次读到变化就立刻相信。
连续几次都读到变化,才认为按键真的变了。

十五、短按和长按怎么判断?

按键被稳定按下后,开始计时。

因为 Key_Task() 每 10ms 调用一次,所以可以这样累计:

c 复制代码
key_press_time += KEY_SCAN_PERIOD_MS;

如果按下时间达到 1000ms,就认为是长按:

c 复制代码
if(key_press_time >= KEY_LONG_TIME_MS)
{
    key_event = KEY_EVENT_LONG;
}

如果没有达到长按时间,按键就松开了,就认为是短按:

c 复制代码
if(key_long_event_sent == 0)
{
    key_event = KEY_EVENT_SHORT;
}

所以:

text 复制代码
按下时间 < 1000ms 后松开:短按
按下时间 >= 1000ms:长按

十六、为什么长按事件要防止重复触发?

如果按键一直按住,程序每 10ms 都会进入 Key_Task()

当时间超过 1000ms 后,如果不做限制,就可能一直产生长按事件。

所以要加一个标志位:

c 复制代码
static bit key_long_event_sent = 0;

含义是:

text 复制代码
这次按下过程中,长按事件是否已经发送过。

第一次达到长按时间时:

c 复制代码
if((key_press_time >= KEY_LONG_TIME_MS) && (key_long_event_sent == 0))
{
    key_event = KEY_EVENT_LONG;
    key_long_event_sent = 1;
}

这样长按按住不放时,只触发一次长按事件。


十七、为什么要用 Key_GetEvent() 读取事件?

Key_Task() 负责产生事件。

Key_GetEvent() 负责读取事件。

c 复制代码
unsigned char Key_GetEvent(void)
{
    unsigned char event;

    event = key_event;
    key_event = KEY_EVENT_NONE;

    return event;
}

这段代码的意思是:

text 复制代码
先保存当前事件
然后清除事件
最后把刚才保存的事件返回给主程序

为什么读取后要清除?

因为事件是一次性的。

如果不清除,主程序会一直读到同一个事件。

例如短按一次,本来只应该处理一次。

如果不清除事件,主程序可能会一直认为有短按事件。

通俗理解:

text 复制代码
按键事件就像一张通知单。
主程序拿走通知单后,通知单就要清空。
否则主程序会一直重复处理同一张通知单。

十八、完整 key.h 设计

c 复制代码
#ifndef __KEY_H__
#define __KEY_H__

#include "STC8G.H"

#define KEY_EVENT_NONE     0
#define KEY_EVENT_SHORT    1
#define KEY_EVENT_LONG     2

void Key_Init(void);
void Key_Task(void);
unsigned char Key_GetEvent(void);

#endif

这个头文件对外提供三个函数:

text 复制代码
Key_Init():初始化按键 IO 和内部变量
Key_Task():每 10ms 调用一次,处理扫描和消抖
Key_GetEvent():获取按键事件

main.c 不需要知道按键如何消抖。

main.c 只需要知道:

text 复制代码
周期性调用 Key_Task()
通过 Key_GetEvent() 获取事件
根据事件执行对应动作

十九、完整 key.c 代码

下面是完整的非阻塞按键模块。

假设:

text 复制代码
KEY 接 P3.2
按下为低电平
10ms 调用一次 Key_Task()
长按时间为 1000ms

代码如下:

c 复制代码
#include "STC8G.H"
#include "key.h"

#define BIT2                 0x04

#define KEY_PRESS_LEVEL      0
#define KEY_RELEASE_LEVEL    1

#define KEY_DEBOUNCE_COUNT   2
#define KEY_SCAN_PERIOD_MS   10
#define KEY_LONG_TIME_MS     1000

sbit KEY = P3^2;

static bit key_stable_level = KEY_RELEASE_LEVEL;
static unsigned char key_debounce_count = 0;
static unsigned int key_press_time = 0;
static unsigned char key_event = KEY_EVENT_NONE;
static bit key_long_event_sent = 0;

void Key_Init(void)
{
    /* P3.2 设置为高阻输入 */
    P3M1 |= BIT2;
    P3M0 &= ~BIT2;

    key_stable_level = KEY_RELEASE_LEVEL;
    key_debounce_count = 0;
    key_press_time = 0;
    key_event = KEY_EVENT_NONE;
    key_long_event_sent = 0;
}

void Key_Task(void)
{
    bit raw_level;

    raw_level = KEY;

    if(raw_level != key_stable_level)
    {
        key_debounce_count++;

        if(key_debounce_count >= KEY_DEBOUNCE_COUNT)
        {
            key_debounce_count = 0;
            key_stable_level = raw_level;

            if(key_stable_level == KEY_PRESS_LEVEL)
            {
                key_press_time = 0;
                key_long_event_sent = 0;
            }
            else
            {
                if(key_long_event_sent == 0)
                {
                    key_event = KEY_EVENT_SHORT;
                }

                key_press_time = 0;
            }
        }
    }
    else
    {
        key_debounce_count = 0;

        if(key_stable_level == KEY_PRESS_LEVEL)
        {
            if(key_press_time < KEY_LONG_TIME_MS)
            {
                key_press_time += KEY_SCAN_PERIOD_MS;
            }

            if((key_press_time >= KEY_LONG_TIME_MS) && (key_long_event_sent == 0))
            {
                key_event = KEY_EVENT_LONG;
                key_long_event_sent = 1;
            }
        }
    }
}

unsigned char Key_GetEvent(void)
{
    unsigned char event;

    event = key_event;
    key_event = KEY_EVENT_NONE;

    return event;
}

二十、key.c 逐段解释

1. 引入头文件

c 复制代码
#include "STC8G.H"
#include "key.h"

STC8G.H 用来让编译器认识:

text 复制代码
P3
P3M0
P3M1
sbit
bit

key.h 是按键模块自己的头文件,里面包含函数声明和事件宏定义。


2. 定义 P3.2 对应的位

c 复制代码
#define BIT2                 0x04

因为 P3.2 是 P3 端口的 bit2。

bit2 对应:

text 复制代码
0000 0100

也就是:

text 复制代码
0x04

3. 定义按键有效电平

c 复制代码
#define KEY_PRESS_LEVEL      0
#define KEY_RELEASE_LEVEL    1

表示:

text 复制代码
按下 = 0
松开 = 1

如果以后硬件改成按下为高电平,只需要改成:

c 复制代码
#define KEY_PRESS_LEVEL      1
#define KEY_RELEASE_LEVEL    0

4. 定义消抖次数

c 复制代码
#define KEY_DEBOUNCE_COUNT   2

表示连续 2 次检测到变化,才确认状态变化。

因为每 10ms 扫描一次,所以消抖时间为:

text 复制代码
10ms × 2 = 20ms

5. 定义扫描周期

c 复制代码
#define KEY_SCAN_PERIOD_MS   10

表示 Key_Task() 应该每 10ms 调用一次。

这个值必须和 main.c 中调用 Key_Task() 的周期一致。


6. 定义长按时间

c 复制代码
#define KEY_LONG_TIME_MS     1000

表示按住 1000ms 后识别为长按。


7. 定义按键引脚

c 复制代码
sbit KEY = P3^2;

表示 KEY 对应 P3.2 这个 IO 引脚。

读取:

c 复制代码
KEY

就相当于读取 P3.2 当前电平。


二十一、按键模块内部变量解释

c 复制代码
static bit key_stable_level = KEY_RELEASE_LEVEL;

表示当前已经确认稳定的按键电平。

因为它只保存 0 或 1,所以使用 bit 类型更合适。


c 复制代码
static unsigned char key_debounce_count = 0;

表示消抖计数。

它要记录 0、1、2 等计数值,所以用 unsigned char


c 复制代码
static unsigned int key_press_time = 0;

表示按键稳定按下的持续时间,单位是 ms。

因为时间可能达到 1000ms、2000ms,所以用 unsigned int


c 复制代码
static unsigned char key_event = KEY_EVENT_NONE;

表示当前按键事件。

可能是:

text 复制代码
KEY_EVENT_NONE
KEY_EVENT_SHORT
KEY_EVENT_LONG

因为事件有 3 种以上,不能用 bit,所以用 unsigned char


c 复制代码
static bit key_long_event_sent = 0;

表示这次按下过程中,长按事件是否已经发送过。

它只有两个状态:

text 复制代码
0:还没发送过
1:已经发送过

所以用 bit


二十二、为什么这些变量要加 static?

例如:

c 复制代码
static bit key_stable_level = KEY_RELEASE_LEVEL;
static unsigned char key_event = KEY_EVENT_NONE;

这里的 static 表示:

text 复制代码
这些变量只在 key.c 文件内部有效。
其他 .c 文件不能直接访问它们。

这是模块化编程的好习惯。

外部文件不应该直接修改:

c 复制代码
key_event = KEY_EVENT_SHORT;

外部文件只应该通过函数读取:

c 复制代码
key_event = Key_GetEvent();

这样模块边界更清楚。


二十三、Key_Init() 函数解释

c 复制代码
void Key_Init(void)
{
    /* P3.2 设置为高阻输入 */
    P3M1 |= BIT2;
    P3M0 &= ~BIT2;

    key_stable_level = KEY_RELEASE_LEVEL;
    key_debounce_count = 0;
    key_press_time = 0;
    key_event = KEY_EVENT_NONE;
    key_long_event_sent = 0;
}

这个函数做两类事情:

text 复制代码
配置按键 IO
初始化按键模块内部变量

1. 配置 P3.2 为高阻输入

c 复制代码
P3M1 |= BIT2;
P3M0 &= ~BIT2;

对于 STC8G 这类单片机,IO 模式通常由 P3M1P3M0 控制。

高阻输入要求:

text 复制代码
P3M1.2 = 1
P3M0.2 = 0

所以:

c 复制代码
P3M1 |= BIT2;

表示把 P3M1 的 bit2 置 1。

c 复制代码
P3M0 &= ~BIT2;

表示把 P3M0 的 bit2 清 0。

因此 P3.2 被配置为高阻输入。


2. 初始化内部变量

c 复制代码
key_stable_level = KEY_RELEASE_LEVEL;

默认认为按键松开。

c 复制代码
key_debounce_count = 0;

消抖计数清零。

c 复制代码
key_press_time = 0;

按下时间清零。

c 复制代码
key_event = KEY_EVENT_NONE;

初始没有任何按键事件。

c 复制代码
key_long_event_sent = 0;

长按事件还没有发送过。


二十四、Key_Task() 函数整体解释

c 复制代码
void Key_Task(void)
{
    bit raw_level;

    raw_level = KEY;

    ...
}

Key_Task() 是按键模块最核心的函数。

它的作用是:

text 复制代码
周期性读取按键电平
进行非阻塞消抖
判断按键是否稳定按下或松开
计算按下持续时间
产生短按或长按事件

这个函数应该固定周期调用,比如每 10ms 调用一次。

它里面不应该再写 Delay_ms()


二十五、raw_level 是什么?

c 复制代码
bit raw_level;

raw_level = KEY;

raw_level 表示:

text 复制代码
当前这一次读取到的按键原始电平。

如果按键松开:

text 复制代码
raw_level = 1

如果按键按下:

text 复制代码
raw_level = 0

因为 KEY 是 P3.2 单个位,所以 raw_levelbit 类型最合适。


二十六、检测电平是否变化

c 复制代码
if(raw_level != key_stable_level)
{
    key_debounce_count++;

    if(key_debounce_count >= KEY_DEBOUNCE_COUNT)
    {
        ...
    }
}

这段代码表示:

text 复制代码
如果当前读取到的原始电平 raw_level
和当前已经确认的稳定电平 key_stable_level 不一样,
说明按键可能正在发生变化。

例如当前稳定状态是松开:

text 复制代码
key_stable_level = 1

现在读到:

text 复制代码
raw_level = 0

说明可能按下了。

但因为机械按键会抖动,所以不能马上确认。

于是先执行:

c 复制代码
key_debounce_count++;

表示连续检测到变化的次数加 1。


二十七、消抖确认逻辑

c 复制代码
if(key_debounce_count >= KEY_DEBOUNCE_COUNT)
{
    key_debounce_count = 0;
    key_stable_level = raw_level;

    ...
}

因为:

c 复制代码
#define KEY_DEBOUNCE_COUNT   2

所以当连续 2 次检测到变化后,才确认状态真的变化。

每 10ms 调用一次:

text 复制代码
第 1 次读到变化:key_debounce_count = 1
第 2 次读到变化:key_debounce_count = 2,确认变化

确认之后:

c 复制代码
key_debounce_count = 0;

消抖计数清零。

c 复制代码
key_stable_level = raw_level;

更新稳定电平。

也就是说,从这一刻开始,新的状态被正式承认了。


二十八、确认稳定按下

c 复制代码
if(key_stable_level == KEY_PRESS_LEVEL)
{
    key_press_time = 0;
    key_long_event_sent = 0;
}

这段表示:

text 复制代码
如果消抖确认后的稳定状态是按下,
说明按键刚刚被稳定按下。

这时候要做两件事:

c 复制代码
key_press_time = 0;

按下计时从 0 开始。

c 复制代码
key_long_event_sent = 0;

长按事件标志清零,表示这次新的按下动作还没有发送过长按事件。


二十九、确认稳定松开

c 复制代码
else
{
    if(key_long_event_sent == 0)
    {
        key_event = KEY_EVENT_SHORT;
    }

    key_press_time = 0;
}

这个 else 表示:

text 复制代码
消抖确认后的稳定状态是松开。

也就是说,按键刚刚从按下变成了松开。

这时要判断它是不是短按。

如果:

c 复制代码
key_long_event_sent == 0

说明这次按下过程中还没有触发过长按。

那么松开时就认为这是一次短按。

所以产生短按事件:

c 复制代码
key_event = KEY_EVENT_SHORT;

如果已经触发过长按,松开时就不再产生短按。

这样可以避免一次长按同时触发长按和短按。


三十、电平没有变化时的处理

c 复制代码
else
{
    key_debounce_count = 0;

    if(key_stable_level == KEY_PRESS_LEVEL)
    {
        ...
    }
}

这个 else 对应:

c 复制代码
if(raw_level != key_stable_level)

所以这里表示:

text 复制代码
当前读取到的原始电平 raw_level
和已经确认的稳定电平 key_stable_level 一样。

说明目前按键状态稳定,没有发生变化。

因此:

c 复制代码
key_debounce_count = 0;

消抖计数清零。


三十一、稳定按下时累计按下时间

c 复制代码
if(key_stable_level == KEY_PRESS_LEVEL)
{
    if(key_press_time < KEY_LONG_TIME_MS)
    {
        key_press_time += KEY_SCAN_PERIOD_MS;
    }

    if((key_press_time >= KEY_LONG_TIME_MS) && (key_long_event_sent == 0))
    {
        key_event = KEY_EVENT_LONG;
        key_long_event_sent = 1;
    }
}

这段只在按键已经稳定按下时执行。


1. 累加按下时间

c 复制代码
if(key_press_time < KEY_LONG_TIME_MS)
{
    key_press_time += KEY_SCAN_PERIOD_MS;
}

意思是:

text 复制代码
如果按下时间还没有达到长按时间,
就继续累加。

因为 Key_Task() 每 10ms 调用一次,所以每次加 10ms。

例如:

text 复制代码
调用 1 次:10ms
调用 2 次:20ms
调用 3 次:30ms
...
调用 100 次:1000ms

2. 判断长按事件

c 复制代码
if((key_press_time >= KEY_LONG_TIME_MS) && (key_long_event_sent == 0))
{
    key_event = KEY_EVENT_LONG;
    key_long_event_sent = 1;
}

这段表示:

text 复制代码
如果按下时间已经达到长按时间,
并且长按事件还没有发送过,
就产生一次长按事件。

产生长按事件:

c 复制代码
key_event = KEY_EVENT_LONG;

然后:

c 复制代码
key_long_event_sent = 1;

表示这次按下已经发送过长按事件。

这样可以防止长按事件重复触发。


三十二、Key_GetEvent() 函数解释

c 复制代码
unsigned char Key_GetEvent(void)
{
    unsigned char event;

    event = key_event;
    key_event = KEY_EVENT_NONE;

    return event;
}

这个函数负责把按键事件交给外部程序。


1. 定义临时变量

c 复制代码
unsigned char event;

用来临时保存当前事件。


2. 读取事件

c 复制代码
event = key_event;

把当前按键事件保存到临时变量。


3. 清除事件

c 复制代码
key_event = KEY_EVENT_NONE;

这一步非常重要。

意思是:

text 复制代码
事件已经被取走了,清空事件。

否则主程序每次调用 Key_GetEvent() 都会读到同一个事件,导致一个短按被处理很多次。


4. 返回事件

c 复制代码
return event;

把刚才保存的事件返回给主程序。

主程序可以这样使用:

c 复制代码
key_event = Key_GetEvent();

if(key_event == KEY_EVENT_SHORT)
{
    LED_Toggle();
}
else if(key_event == KEY_EVENT_LONG)
{
    LED_On();
}

三十三、短按完整流程

假设按键松开时是 1,按下时是 0。

初始状态:

text 复制代码
key_stable_level = 1
key_event = NONE
key_press_time = 0
key_long_event_sent = 0

用户短按一次,例如按下 300ms 后松开。


1. 按下开始

第一次扫描到 0:

text 复制代码
raw_level = 0
key_stable_level = 1
不同,key_debounce_count = 1

第二次扫描还是 0:

text 复制代码
key_debounce_count = 2
确认按下
key_stable_level = 0
key_press_time = 0
key_long_event_sent = 0

2. 按住 300ms

按键稳定按下时,每 10ms:

c 复制代码
key_press_time += 10;

最终大约:

text 复制代码
key_press_time = 300

没有达到:

text 复制代码
KEY_LONG_TIME_MS = 1000

所以不会产生长按事件。


3. 松开

第一次扫描到 1:

text 复制代码
raw_level = 1
key_stable_level = 0
不同,key_debounce_count = 1

第二次扫描还是 1:

text 复制代码
key_debounce_count = 2
确认松开
key_stable_level = 1

因为:

text 复制代码
key_long_event_sent = 0

所以:

c 复制代码
key_event = KEY_EVENT_SHORT;

产生短按事件。


三十四、长按完整流程

用户按住超过 1000ms。


1. 按下确认

经过消抖后:

text 复制代码
key_stable_level = 0
key_press_time = 0
key_long_event_sent = 0

2. 持续按住

每 10ms 累加:

c 复制代码
key_press_time += 10;

达到 1000ms 时:

c 复制代码
if((key_press_time >= KEY_LONG_TIME_MS) && (key_long_event_sent == 0))
{
    key_event = KEY_EVENT_LONG;
    key_long_event_sent = 1;
}

产生长按事件。


3. 继续按住

因为:

text 复制代码
key_long_event_sent = 1

所以不会再重复产生长按事件。


4. 松开

松开确认后进入:

c 复制代码
else
{
    if(key_long_event_sent == 0)
    {
        key_event = KEY_EVENT_SHORT;
    }

    key_press_time = 0;
}

由于:

text 复制代码
key_long_event_sent = 1

所以不会产生短按事件。

这就避免了:

text 复制代码
一次长按同时触发短按

三十五、完整 main.c 示例

假设已经有:

text 复制代码
led.h / led.c
key.h / key.c
timer.h / timer.c

main.c 可以写成:

c 复制代码
#include "STC8G.H"
#include "led.h"
#include "key.h"
#include "timer.h"

void main(void)
{
    unsigned int now_tick = 0;
    unsigned int last_key_tick = 0;
    unsigned char key_event = KEY_EVENT_NONE;

    LED_Init();
    Key_Init();
    Timer0_Init();

    while(1)
    {
        now_tick = Timer_GetTick();

        if((unsigned int)(now_tick - last_key_tick) >= 10)
        {
            last_key_tick = now_tick;
            Key_Task();
        }

        key_event = Key_GetEvent();

        if(key_event == KEY_EVENT_SHORT)
        {
            LED_Toggle();
        }
        else if(key_event == KEY_EVENT_LONG)
        {
            LED_On();
        }
    }
}

这个程序的功能是:

text 复制代码
短按按键:LED 翻转一次
长按按键:LED 点亮

主函数非常清楚:

text 复制代码
每 10ms 调用一次按键任务
读取按键事件
根据短按或长按执行不同动作

三十六、为什么 Key_Task() 要固定10ms调用?

因为 key.c 里有:

c 复制代码
#define KEY_SCAN_PERIOD_MS   10

并且长按时间是靠这个周期累加出来的:

c 复制代码
key_press_time += KEY_SCAN_PERIOD_MS;

如果实际不是 10ms 调用一次,而是 20ms 调用一次,那么时间计算就不准确。

所以必须保证:

text 复制代码
KEY_SCAN_PERIOD_MS 的值
和 main.c 中调用 Key_Task() 的周期一致

例如主函数里是:

c 复制代码
if((unsigned int)(now_tick - last_key_tick) >= 10)
{
    last_key_tick = now_tick;
    Key_Task();
}

那么:

c 复制代码
#define KEY_SCAN_PERIOD_MS   10

这两个要一致。


三十七、如果想改成5ms扫描怎么办?

如果想每 5ms 扫描一次按键,主函数改成:

c 复制代码
if((unsigned int)(now_tick - last_key_tick) >= 5)
{
    last_key_tick = now_tick;
    Key_Task();
}

同时 key.c 里要改:

c 复制代码
#define KEY_SCAN_PERIOD_MS   5

如果还想保持约 20ms 消抖,那么:

c 复制代码
#define KEY_DEBOUNCE_COUNT   4

因为:

text 复制代码
5ms × 4 = 20ms

所以消抖时间的计算方法是:

text 复制代码
消抖时间 = 扫描周期 × 消抖次数

三十八、短按事件是什么时候产生的?

在今天这版代码里,短按事件是在按键松开时产生的。

逻辑是:

text 复制代码
按键稳定按下
还没有达到长按时间
然后按键稳定松开
产生短按事件

也就是说:

text 复制代码
短按不是按下瞬间触发
而是松开时确认

为什么这样设计?

因为只有松开时,才能知道这次按键是不是短按。

如果按下瞬间就触发短按,而用户继续按住变成长按,就会发生冲突。

所以比较常见的设计是:

text 复制代码
短按:松开时确认
长按:达到长按时间时确认

三十九、长按事件是什么时候产生的?

长按事件是在按住超过设定时间时产生的。

例如:

c 复制代码
#define KEY_LONG_TIME_MS     1000

那么:

text 复制代码
按住 1000ms 后,产生长按事件

注意:

text 复制代码
长按事件不是松开时才产生
而是按住达到长按时间时产生

这样适合一些实际功能,例如:

text 复制代码
长按开关功能
长按进入设置
长按恢复默认参数
长按触发特殊动作

四十、短按和长按的执行时机对比

操作 事件产生时机
短按 松开时产生
长按 按住达到设定时间时产生

通俗理解:

text 复制代码
短按要等你松手,才能确定它只是短按。
长按只要按够时间,就可以确定它是长按。

四十一、完整模块化文件结构

经过第八天后,项目结构可以是:

text 复制代码
Project
│
├── main.c
│
├── led.h
├── led.c
│
├── key.h
├── key.c
│
├── timer.h
├── timer.c
│
└── STC8G.H

其中:

text 复制代码
timer 模块:提供 1ms 系统节拍
key 模块:基于 10ms 周期扫描,实现非阻塞消抖
led 模块:提供 LED 控制函数
main.c:只负责调度和业务逻辑

这已经很接近实际小项目的写法。


四十二、第八天常见错误

1. Key_Task() 调用周期和宏定义不一致

如果主函数里是 20ms 调用一次:

c 复制代码
if((unsigned int)(now_tick - last_key_tick) >= 20)

key.c 里写:

c 复制代码
#define KEY_SCAN_PERIOD_MS   10

那么长按时间计算就不准确。

解决方法:

text 复制代码
main.c 调用周期和 KEY_SCAN_PERIOD_MS 必须一致。

2. 短按事件被重复读取

如果 Key_GetEvent() 里不清除事件:

c 复制代码
return key_event;

那么主程序会一直读到同一个事件。

正确写法:

c 复制代码
event = key_event;
key_event = KEY_EVENT_NONE;
return event;

读取后清除事件。


3. 长按事件重复触发

如果没有:

c 复制代码
key_long_event_sent

长按按住不放时,可能每 10ms 都触发一次长按事件。

解决方法:

text 复制代码
一次长按只发送一次事件,松开后再允许下一次长按。

4. 在 Key_Task() 里又使用 Delay_ms()

非阻塞按键扫描的目的就是不用 Delay_ms()

所以不要在 Key_Task() 里写:

c 复制代码
Delay_ms(20);

否则又变回阻塞消抖了。


5. 忘记调用 Key_Task()

如果主循环里没有周期调用:

c 复制代码
Key_Task();

那么按键模块不会更新状态,也不会产生事件。


6. raw_level 错误理解为必须用 unsigned char

KEY 是 P3.2 单个位。

所以:

c 复制代码
bit raw_level;

更准确。

unsigned char raw_level; 也能工作,但从语义上不如 bit 精准。


四十三、第八天必须掌握的重点

今天必须掌握下面这些内容:

text 复制代码
阻塞消抖会卡住程序
非阻塞消抖靠周期扫描实现
Key_Task() 应该固定周期调用
按键状态和按键事件不是一回事
短按通常在松开时确认
长按通常在达到设定时间时确认
Key_GetEvent() 读取事件后要清除事件
长按事件要防止重复触发
扫描周期 × 消抖次数 = 消抖时间
单个 IO 电平建议用 bit 保存
按键事件编号建议用 unsigned char 保存
main.c 只负责调度和处理事件

四十四、第八天练习任务

任务1:解释为什么不用 Delay_ms(20) 消抖

参考答案:

text 复制代码
Delay_ms(20) 会阻塞程序。
在这 20ms 内,主循环不能及时处理其他任务。
非阻塞消抖通过周期扫描实现,不会卡住主程序。

任务2:解释按键状态和按键事件的区别

参考答案:

text 复制代码
按键状态表示当前按键是按下还是松开。
按键事件表示刚刚发生了短按、长按、按下或松开等动作。

任务3:计算消抖时间

如果:

c 复制代码
#define KEY_SCAN_PERIOD_MS   10
#define KEY_DEBOUNCE_COUNT   3

那么消抖时间是多少?

答案:

text 复制代码
10ms × 3 = 30ms

任务4:把长按时间改成2秒

答案:

c 复制代码
#define KEY_LONG_TIME_MS     2000

任务5:写出主循环中每10ms调用Key_Task()的代码

参考答案:

c 复制代码
now_tick = Timer_GetTick();

if((unsigned int)(now_tick - last_key_tick) >= 10)
{
    last_key_tick = now_tick;
    Key_Task();
}

任务6:写出读取按键事件的代码

参考答案:

c 复制代码
key_event = Key_GetEvent();

if(key_event == KEY_EVENT_SHORT)
{
    LED_Toggle();
}
else if(key_event == KEY_EVENT_LONG)
{
    LED_On();
}

任务7:解释为什么 raw_level 用 bit 更合适

参考答案:

text 复制代码
KEY 是 P3.2 单个位,只有 0 和 1 两种状态。
raw_level 只是保存当前 IO 电平,所以用 bit 更贴切。
unsigned char 也能保存 0 和 1,但语义上没有 bit 精准。

任务8:解释 key_event 为什么不能用 bit

参考答案:

text 复制代码
key_event 需要表示 KEY_EVENT_NONE、KEY_EVENT_SHORT、KEY_EVENT_LONG 等多个事件。
bit 只能表示 0 或 1,不能表示 0、1、2 等多个编号。
所以 key_event 应该使用 unsigned char。

四十五、第八天总结

今天学习的是定时器驱动下的非阻塞按键扫描。

可以用一句话总结:

非阻塞按键扫描的核心,是用定时器节拍固定周期读取按键状态,通过连续稳定判断实现消抖,再把按键动作转换成短按、长按等事件。

今天最重要的理解是:

text 复制代码
第五天的按键消抖,是"检测到按下后原地等 20ms"。
第八天的按键消抖,是"每 10ms 看一眼,连续几次稳定才确认"。
前者会阻塞程序,后者不会阻塞程序。

第八天达到下面程度就算合格:

text 复制代码
知道为什么要非阻塞按键消抖
知道 Key_Task() 的作用
知道 Key_GetEvent() 的作用
知道短按事件通常在松开时产生
知道长按事件通常在达到长按时间时产生
能看懂 KEY_DEBOUNCE_COUNT
能看懂 KEY_SCAN_PERIOD_MS
能看懂 KEY_LONG_TIME_MS
能用 Timer_GetTick() 每 10ms 调用一次 Key_Task()
能实现短按 LED 翻转、长按 LED 点亮
知道单个 IO 电平适合用 bit
知道事件编号适合用 unsigned char

后续可以继续学习:

text 复制代码
状态机思想
模式切换
用按键控制多个工作模式
普通模式 / 低功耗模式 / 设置模式
如何让 main.c 的业务逻辑更清楚

从下一天开始,程序会从"单个按键事件"继续扩展到"模式管理"和"状态机思想",让整个小项目的结构更加清晰。

相关推荐
youcans_1 小时前
从零搭建 STM32 VSCode 开发环境
vscode·stm32·单片机·嵌入式硬件
chase。2 小时前
【学习笔记】Dexora:面向高自由度双臂灵巧操作的开源 VLA 系统
笔记·学习
ye150127774552 小时前
220V降5V0.3A电源芯片WT5104
单片机·嵌入式硬件·其他·硬件工程
第二层皮-合肥2 小时前
【数据采集专栏】输入阻抗
单片机·嵌入式硬件
風清掦2 小时前
【STM32学习笔记-15】FLASH 闪存(Claude)
笔记·stm32·单片机·嵌入式硬件·学习
新时代牛马2 小时前
内核调试方法
linux·学习
我想我不够好。2 小时前
贝利亚 扎克
学习
MartinYeung53 小时前
[论文学习]CAMIA:基于上下文感知的成员资格推断攻击:针对预训练大型语言模型的深度分析
人工智能·学习·语言模型
chase。3 小时前
【学习笔记】Unified World Models:基于视频-动作耦合扩散的机器人预训练新范式
笔记·学习·音视频