C C51 | 按键的单击、双击和长按的按键动作检测

本文目标

写一个 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 构建状态机

以下是一个简单的状态转换图

graph LR A[状态0: 空闲] -->|按键按下| B[状态1: 按下检测] B -->|按键释放| C[状态2: 等待] B -->|长按超时| D[状态3: 确认为长按] C -->|超时判定| A D -->|按键释放| A

这个状态机描述了按键检测的基本流程:

  1. 从空闲状态开始,等待按键按下
  2. 按键按下后进入按下检测状态,开始计时
  3. 如果按键很快释放,进入释放等待状态,准备检测是否还有第二次按下
  4. 如果按键持续按下超过阈值,进入长按确认状态
  5. 最终都会回到空闲状态,准备下一次检测

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 核心状态机逻辑详解

以状态转换图来理解检测逻辑

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 代码

现在让我们来看完整的代码实现,我会逐部分进行详细解释:

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 即可。

相关推荐
v***88561 小时前
Springboot项目:使用MockMvc测试get和post接口(含单个和多个请求参数场景)
java·spring boot·后端
IMPYLH1 小时前
Lua 的 require 函数
java·开发语言·笔记·后端·junit·lua
爱找乐子的李寻欢2 小时前
线上批量导出 1000 个文件触发 OOM?扒开代码看本质,我是这样根治的
后端
大鸡腿同学2 小时前
大量频繁记录有效击球方式
后端
稚辉君3 小时前
Gemini永久会员 01不等概率随机到01等概率随机
后端
z***56563 小时前
springboot整合mybatis-plus(保姆教学) 及搭建项目
spring boot·后端·mybatis
q***98523 小时前
Spring Boot:Java开发的神奇加速器(二)
java·spring boot·后端
小蒜学长3 小时前
基于spring boot的汽车4s店管理系统(代码+数据库+LW)
java·数据库·spring boot·后端·汽车
q***42053 小时前
Spring Data 什么是Spring Data 理解
java·后端·spring