本文目标
写一个 C51 程序, 当单击 K1 时,点亮 D1, 当双击 K1 时,点亮 D2, 当长按 K1 时,点亮 D3。
1 框架
这个项目最难搞的地方就是这个按键检测。
本文的项目拿 LED 的动作作为最终效果的检验。
#include <REGX52.H>
int k1Listener();
// [entry]
void main() {
if (k1Listener == 1) {
P2_0 = 0;
} else if (k1Listener == 2) {
P2_1 = 0;
} else if (k1Listener == 3) {
P2_2 = 0;
}
}
int k1Listener() {
// TODO
}
2 基础知识参考
2.1 按键检测的简单实现
通常像这样来检测按键状态
sbit K1 = P3^0; // 定义按键引脚
if (K1 == 0) {
// 按键被按下
} else {
// 按键被释放
}
实际上,这样的检测存在一些问题。
2.2 按键抖动
几乎所有机械按键都存在一个称为按键抖动的机械现象。当按键被按下或释放时,由于机械触点的弹性特性,在稳定接触之前会产生一系列的快速通断现象,这个过程通常持续 5 - 20 毫秒。
如果没有处理抖动,单次按键操作可能会被误检测为多次按键。
2.3 定时器
为了准确检测不同的按键动作,特别是区分单击、双击和长按,应测量事件的时间分布。在 C51 系统中,这通常通过定时器中断来实现。定时器可以提供一个稳定的时间基准,让我们能够测量按键按下的持续时间、两次单击之间的时间间隔等关键时间参数。
3 构建
3.1 按键检测
从基础开始。这个版本只检测按键是否被按下,但加入了基本的消抖处理:
int key_scan(void) {
static unsigned char key_state = 0;
// 按下 K1
if (K1 == 0) {
if (key_state == 0) {
delay(10); // 消抖延时
if (K1 == 0) {
key_state = 1;
return 1; // 返回有效按下
}
}
} else {
key_state = 0;
}
return 0;
}
已经解决了按键抖动的问题。它通过一个状态变量来跟踪按键状态,只有在检测到稳定的按下信号后才返回有效按键。
这种方法只能检测到按键被按下,但无法区分这是单击、双击还是长按,这显然是不够的。
3.2 时间测量
不同的按键动作在时间特征上有着明显的区别
单击:短暂的按下和释放,整个过程通常在几十毫秒内完成
双击:两次连续的单击,两次按下之间的间隔通常在几百毫秒内
长按:持续的按下状态,通常需要保持1秒或更长时间
实现时间测量,可以利用 C51 的定时器功能
static unsigned int press_time = 0;
// 在定时器中断服务函数中
void timer_isr()
interrupt 1
{
TH0 = 0xFC; // 重装定时器初值,实现1ms定时
TL0 = 0x66;
if (K1 == 0) {
press_time++; // 按键按下时持续计时
}
}
通过这种方式,我们可以精确测量按键按下的持续时间,这是区分不同按键动作的基础。
3.3 构建状态机
以下是一个简单的状态转换图
graph LR
A[状态0: 空闲] -->|按键按下| B[状态1: 按下检测]
B -->|按键释放| C[状态2: 等待]
B -->|长按超时| D[状态3: 确认为长按]
C -->|超时判定| A
D -->|按键释放| A
这个状态机描述了按键检测的基本流程:
- 从空闲状态开始,等待按键按下
- 按键按下后进入按下检测状态,开始计时
- 如果按键很快释放,进入释放等待状态,准备检测是否还有第二次按下
- 如果按键持续按下超过阈值,进入长按确认状态
- 最终都会回到空闲状态,准备下一次检测
4 实现
4.1 状态定义
为了在代码中清晰地表达状态机,我们首先定义各个状态:
// 按键状态定义
#define STATE_IDLE 0 // 空闲状态:等待按键按下
#define STATE_PRESS 1 // 按下状态:按键已按下,正在检测持续时间
#define STATE_RELEASE 2 // 释放状态:按键已释放,等待可能的第二次按下
#define STATE_LONG 3 // 长按状态:已确认为长按操作
4.2 时间参数的设定
可以根据实际情况调整
#define CLICK_MAX 50 // 单击最大时间 50ms
#define DOUBLE_TIMEOUT 300 // 双击超时时间 300ms
#define LONG_PRESS 960 // 长按判定时间 1000ms
4.3 核心状态机逻辑详解
以状态转换图来理解检测逻辑
graph TD
A[开始按键检测] --> B{K1按下?}
B -->|是| C{状态=0?}
B -->|否| D{状态=1?}
B -->|否| E{状态=3?}
C -->|是| F[状态=1,时间=0]
C -->|否| G
D -->|是| H[时间++]
H --> I{时间>960?}
I -->|是| J[状态=3,返回3,时间=0]
I -->|否| K[结束]
E -->|是| L[状态=0,计数=0]
F --> K
J --> K
L --> K
D -->|否| M
E -->|否| M
M --> N{状态=2?}
N -->|是| O[超时计数++]
O --> P{计数>300?}
P -->|是| Q{单击计数?}
P -->|否| K
Q -->|1| R[返回1]
Q -->|≥2| S[返回2]
R --> T[状态=0,计数=0]
S --> T
T --> K
N -->|否| K
这个状态机详细描述了所有可能的状态转换路径。需要注意的是,状态机的执行是周期性的,每次调用检测函数时,都会根据当前状态和输入条件决定是否进行状态转换。
4.4 代码
现在让我们来看完整的代码实现,我会逐部分进行详细解释:
#include <REGX52.H>
#define CLICK_MAX_TIME 20 // 200ms
#define DOUBLE_CLICK_TIMEOUT 40 // 400ms
#define LONG_PRESS_TIME 100 // 1000ms
// 简单的延时函数
void Delay(unsigned int ms) {
unsigned int i, j;
for(i = ms; i > 0; i--)
for(j = 110; j > 0; j--);
}
unsigned char k1() {
static unsigned char k1State = 0;
static unsigned int pressTime = 0;
static unsigned char clickCount = 0;
static unsigned int doubleClickTimeout = 0;
unsigned char statusCode = 0;
if (P3_1 == 0) { // 按键按下
if (k1State == 0) { // 初始状态
k1State = 1;
pressTime = 0;
} else if (k1State == 1) { // 按下状态
pressTime++;
if (pressTime > LONG_PRESS_TIME) {
k1State = 3;
statusCode = 3; // 长按
clickCount = 0;
}
} else if (k1State == 2) { // 等待第二次点击时再次按下
k1State = 1;
pressTime = 0;
// 这是第二次按下,立即判定为双击
statusCode = 2;
k1State = 0;
clickCount = 0;
doubleClickTimeout = 0;
}
} else { // 按键释放
if (k1State == 1) { // 从按下状态释放
if (pressTime < CLICK_MAX_TIME) {
clickCount = 1; // 记录一次点击
k1State = 2; // 进入等待第二次点击状态
doubleClickTimeout = 0;
} else {
// 按下时间过长,不算点击
k1State = 0;
clickCount = 0;
}
pressTime = 0;
} else if (k1State == 3) { // 长按释放
k1State = 0;
clickCount = 0;
}
}
// 在等待第二次点击状态
if (k1State == 2) {
doubleClickTimeout++;
if (doubleClickTimeout > DOUBLE_CLICK_TIMEOUT) {
// 超时,判定为单击
if (clickCount == 1) {
statusCode = 1;
}
k1State = 0;
clickCount = 0;
doubleClickTimeout = 0;
}
}
return statusCode;
}
这段代码的逻辑相对复杂,但通过状态机的分解,变得清晰可控。
4.5 应用
目标项目可以被清晰地实现了
#include <REGX52.H>
#define CLICK_MAX_TIME 20 // 200ms
#define DOUBLE_CLICK_TIMEOUT 40 // 400ms
#define LONG_PRESS_TIME 100 // 1000ms
// 简单的延时函数
void Delay(unsigned int ms) {
unsigned int i, j;
for(i = ms; i > 0; i--)
for(j = 110; j > 0; j--);
}
unsigned char k1() {
static unsigned char k1State = 0;
static unsigned int pressTime = 0;
static unsigned char clickCount = 0;
static unsigned int doubleClickTimeout = 0;
unsigned char statusCode = 0;
if (P3_1 == 0) { // 按键按下
if (k1State == 0) { // 初始状态
k1State = 1;
pressTime = 0;
} else if (k1State == 1) { // 按下状态
pressTime++;
if (pressTime > LONG_PRESS_TIME) {
k1State = 3;
statusCode = 3; // 长按
clickCount = 0;
}
} else if (k1State == 2) { // 等待第二次点击时再次按下
k1State = 1;
pressTime = 0;
// 这是第二次按下,立即判定为双击
statusCode = 2;
k1State = 0;
clickCount = 0;
doubleClickTimeout = 0;
}
} else { // 按键释放
if (k1State == 1) { // 从按下状态释放
if (pressTime < CLICK_MAX_TIME) {
clickCount = 1; // 记录一次点击
k1State = 2; // 进入等待第二次点击状态
doubleClickTimeout = 0;
} else {
// 按下时间过长,不算点击
k1State = 0;
clickCount = 0;
}
pressTime = 0;
} else if (k1State == 3) { // 长按释放
k1State = 0;
clickCount = 0;
}
}
// 在等待第二次点击状态
if (k1State == 2) {
doubleClickTimeout++;
if (doubleClickTimeout > DOUBLE_CLICK_TIMEOUT) {
// 超时,判定为单击
if (clickCount == 1) {
statusCode = 1;
}
k1State = 0;
clickCount = 0;
doubleClickTimeout = 0;
}
}
return statusCode;
}
void main() {
P2 = 0xFF; // 初始化LED全灭
while(1) {
unsigned char result = k1();
if (result == 1) {
P2_0 = ~P2_0; // 单击切换LED1
Delay(200); // 防止连按
} else if (result == 2) {
P2_1 = ~P2_1; // 双击切换LED2
Delay(200); // 防止连按
} else if (result == 3) {
P2_2 = ~P2_2; // 长按切换LED3
Delay(200); // 防止连按
}
Delay(10); // 主循环延时
}
}
这个主循环的结构非常清晰,按键检测和功能执行完全分离,符合模块化设计的原则。如果需要添加新的按键功能,只需要在 switch 语句中添加新的 case 即可。