下面这版已经整理成 CSDN Markdown 技术博客格式 ,可以直接复制粘贴发布。内容统一描述为"常规小项目 / 小型控制项目",没有出现你要求避开的词,并且把 raw_level、key_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_RELEASED 和 KEY_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 模式通常由 P3M1 和 P3M0 控制。
高阻输入要求:
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_level 用 bit 类型最合适。
二十六、检测电平是否变化
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 的业务逻辑更清楚
从下一天开始,程序会从"单个按键事件"继续扩展到"模式管理"和"状态机思想",让整个小项目的结构更加清晰。