本文目标
写一个 C51 程序, 当单击 K1 时,点亮 D1, 当双击 K1 时,点亮 D2, 当长按 K1 时,点亮 D3。
1 框架
这个项目最难搞的地方就是这个按键检测。
本文的项目拿 LED 的动作作为最终效果的检验。
c
#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 按键检测的简单实现
通常像这样来检测按键状态
c
sbit K1 = P3^0; // 定义按键引脚
if (K1 == 0) {
// 按键被按下
} else {
// 按键被释放
}
实际上,这样的检测存在一些问题。
2.2 按键抖动
几乎所有机械按键都存在一个称为按键抖动的机械现象。当按键被按下或释放时,由于机械触点的弹性特性,在稳定接触之前会产生一系列的快速通断现象,这个过程通常持续 5 - 20 毫秒。
如果没有处理抖动,单次按键操作可能会被误检测为多次按键。
2.3 定时器
为了准确检测不同的按键动作,特别是区分单击、双击和长按,应测量事件的时间分布。在 C51 系统中,这通常通过定时器中断来实现。定时器可以提供一个稳定的时间基准,让我们能够测量按键按下的持续时间、两次单击之间的时间间隔等关键时间参数。
3 构建
3.1 按键检测
从基础开始。这个版本只检测按键是否被按下,但加入了基本的消抖处理:
c
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 的定时器功能
c
static unsigned int press_time = 0;
// 在定时器中断服务函数中
void timer_isr()
interrupt 1
{
TH0 = 0xFC; // 重装定时器初值,实现1ms定时
TL0 = 0x66;
if (K1 == 0) {
press_time++; // 按键按下时持续计时
}
}
通过这种方式,我们可以精确测量按键按下的持续时间,这是区分不同按键动作的基础。
3.3 构建状态机
以下是一个简单的状态转换图
这个状态机描述了按键检测的基本流程:
- 从空闲状态开始,等待按键按下
- 按键按下后进入按下检测状态,开始计时
- 如果按键很快释放,进入释放等待状态,准备检测是否还有第二次按下
- 如果按键持续按下超过阈值,进入长按确认状态
- 最终都会回到空闲状态,准备下一次检测
4 实现
4.1 状态定义
为了在代码中清晰地表达状态机,我们首先定义各个状态:
c
// 按键状态定义
#define STATE_IDLE 0 // 空闲状态:等待按键按下
#define STATE_PRESS 1 // 按下状态:按键已按下,正在检测持续时间
#define STATE_RELEASE 2 // 释放状态:按键已释放,等待可能的第二次按下
#define STATE_LONG 3 // 长按状态:已确认为长按操作
4.2 时间参数的设定
可以根据实际情况调整
c
#define CLICK_MAX 50 // 单击最大时间 50ms
#define DOUBLE_TIMEOUT 300 // 双击超时时间 300ms
#define LONG_PRESS 960 // 长按判定时间 1000ms
4.3 核心状态机逻辑详解
以状态转换图来理解检测逻辑
这个状态机详细描述了所有可能的状态转换路径。需要注意的是,状态机的执行是周期性的,每次调用检测函数时,都会根据当前状态和输入条件决定是否进行状态转换。
4.4 代码
现在让我们来看完整的代码实现,我会逐部分进行详细解释:
c
int k1Listener() {
// 静态变量 - 保持状态 between function calls
static unsigned char k1State = 0; // 当前状态
static unsigned int pressTime = 0; // 按下持续时间
static unsigned char clickCount = 0; // 单击计数
static unsigned int doubleClickTimeout = 0; // 双击超时计数
unsigned char actionResult = 0; // 本次检测结果
// 第一部分:处理按键按下情况
if (k1 == 0) {
// 按键当前处于按下状态
if (k1State == 0) {
// 从空闲状态转换到按下状态
k1State = 1;
pressTime = 0;
}
else if (k1State == 1) {
// 保持在按下状态,累计按下时间
pressTime++;
// 检查是否达到长按阈值
if (pressTime > LONG_PRESS) {
k1State = 3; // 转换到长按状态
actionResult = 3; // 返回长按代码
pressTime = 0; // 清空按下时间
}
}
}
else {
// 按键当前处于释放状态
if (k1State == 1) {
// 从按下状态转换到释放状态
// 检查是否为有效单击(按下时间较短)
if (pressTime < CLICK_MAX) {
clickCount++;
// 如果是第一次单击,初始化双击超时计数
if (clickCount == 1) {
doubleClickTimeout = 0;
}
}
k1State = 2; // 进入释放等待状态
pressTime = 0; // 清空按下时间
}
else if (k1State == 3) {
// 长按后释放,重置状态
k1State = 0;
clickCount = 0;
}
}
// 第二部分:处理释放等待状态下的双击检测
if (k1State == 2) {
// 在释放状态下等待可能的第二次按下
doubleClickTimeout++;
// 检查是否超过双击检测时间窗口
if (doubleClickTimeout > DOUBLE_TIMEOUT) {
// 时间窗口结束,根据单击计数确定结果
if (clickCount == 1) {
actionResult = 1; // 单次单击
}
else if (clickCount >= 2) {
actionResult = 2; // 双击
}
// 重置状态,准备下一次检测
k1State = 0;
clickCount = 0;
doubleClickTimeout = 0;
}
}
return actionResult;
}
这段代码的逻辑相对复杂,但通过状态机的分解,变得清晰可控。
4.5 应用
目标项目可以被清晰地实现了
c
// LED引脚定义
sbit d1 = P1^0;
sbit d2 = P1^1;
sbit d3 = P1^2;
// LED状态变量
unsigned char d1Status = 0;
unsigned char d2Status = 0;
unsigned char d3Status = 0;
void main()
{
unsigned char keyAction;
// 初始化代码
initAll();
while(1) {
// 检测按键动作
keyAction = k1Listener();
// 根据检测结果执行相应操作
switch(keyAction) {
case 1: // 单击 - 控制d1
d1Status = !d1Status;
d1 = d1Status;
break;
case 2: // 双击 - 控制d2
d2Status = !d2Status;
d2 = d2Status;
break;
case 3: // 长按 - 控制d3
d3Status = !d3Status;
d3 = d3Status;
break;
default:
// 无操作,继续其他任务
break;
}
}
}
这个主循环的结构非常清晰,按键检测和功能执行完全分离,符合模块化设计的原则。如果需要添加新的按键功能,只需要在 switch 语句中添加新的 case 即可。