按键长按逻辑技术文档
文档信息
- 项目名称: DS18B20温度传感器控制系统
- 目标平台: STC15F2K60S2 单片机
- 主频: 12.000MHz
- 文档版本: v1.0
- 最后更新: 2026-02-03
1. 概述
1.1 功能描述
长按功能允许用户通过持续按住按键来实现参数的连续调节,无需重复按压。本系统实现了 S14(增加)和 S15(减少)两个按键的长按检测和连续触发功能。
1.2 技术特点
双层计时机制: 中断计时 + 主循环减速
可配置触发参数: 延迟时间、触发频率可调
防抖动设计: 松开立即清零,避免误触发
资源占用低: 仅使用 2 个全局变量
1.3 系统参数
| 参数 | 数值 | 说明 |
|---|---|---|
| 定时器周期 | 1ms | Timer0 中断频率 |
| 按键扫描周期 | 10ms | Key_Proc 调用频率 |
| 数码管刷新周期 | 50ms | Seg_Proc 调用频率 |
| 长按触发延迟 | 250ms | 按住多久后开始连续触发 |
2. 实现原理
2.1 架构设计
┌─────────────────────────────────────────────────────────┐
│ 硬件层 (物理按键) │
│ S14 / S15 │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 中断层 (Timer0: 1ms) │
│ ┌───────────────────────────────────────────────────┐ │
│ │ • 按键状态监测: Key_Old │ │
│ │ • 计时器累加: Key_Long++ (每1ms) │ │
│ │ • 松开清零: Key_Long = 0 │ │
│ └───────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 应用层 (Key_Proc: 10ms) │
│ ┌───────────────────────────────────────────────────┐ │
│ │ • 阈值检测: if(Key_Long >= 250) │ │
│ │ • 减速控制: Key_Long_Slow │ │
│ │ • 动作执行: P_DIS[0]++ / P_DIS[1]++ │ │
│ └───────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
2.2 双计时器机制
第一层:中断计时器 (Key_Long)
作用: 精确测量按键按下的持续时间
// 定时器中断 (1ms执行一次)
if((Key_Old == 14) || (Key_Old == 15))
{
if(++Key_Long == 65535) Key_Long = 0; // 累加,防溢出
}
else
{
Key_Long = 0; // 松开立即归零
}
特性:
- 精度:1ms
- 最大测量时间:65.535秒
- 按键松开后立即清零
第二层:减速计数器 (Key_Long_Slow)
作用: 控制连续触发的频率
// 主循环 (10ms执行一次)
if(Key_Long >= 250) // 已按下超过250ms
{
if(++Key_Long_Slow >= 5) // 减速计数器
{
Key_Long_Slow = 0; // 达到阈值,清零重计
P_DIS[0] ++; // 执行动作
}
}
特性:
- 基准周期:10ms (Key_Proc调用周期)
- 触发间隔:阈值 × 10ms
- 示例:阈值=5 → 每50ms触发一次
2.3 时间轴分析
完整操作流程
时间 (ms) 中断 (1ms周期) 主循环 (10ms周期) 显示
═════════════════════════════════════════════════════════════════
0 按下S14 - P 30-20
Key_Long = 0
1 Key_Long = 1 - P 30-20
2 Key_Long = 2 - P 30-20
... ... ... ...
10 Key_Long = 10 检查: 10 < 250 (未触发) P 30-20
20 Key_Long = 20 检查: 20 < 250 (未触发) P 30-20
... ... ... ...
250 Key_Long = 250 检查: 250 >= 250 ✅ P 30-20
Key_Long_Slow = 1
260 Key_Long = 260 Key_Long_Slow = 2 P 30-20
270 Key_Long = 270 Key_Long_Slow = 3 P 30-20
280 Key_Long = 280 Key_Long_Slow = 4 P 30-20
290 Key_Long = 290 Key_Long_Slow = 5 P 30-20
300 Key_Long = 300 Key_Long_Slow = 0 ✅ P 31-20
执行: P_DIS[0]++
310 Key_Long = 310 Key_Long_Slow = 1 P 31-20
... ... ... ...
350 Key_Long = 350 Key_Long_Slow = 0 ✅ P 32-20
执行: P_DIS[0]++
... ... ... ...
500 松开按键 - P 35-20
501 Key_Long = 0 检查: 0 < 250 (停止) P 35-20
总结:
- 0~250ms: 等待期,判断是否为长按
- 250ms后: 每50ms执行一次增加操作
- 松开时: 立即停止
3. 代码详解
3.1 变量声明
/* 全局变量 */
unsigned int Key_Long; // 长按计时器 (中断累加)
unsigned char Key_Old; // 当前按键状态
unsigned char P_DIS[2]; // 参数显示数组 [TMAX, TMIN]
unsigned char Set_Index; // 当前设置索引 (0=TMAX, 1=TMIN)
unsigned char Display_Mode; // 显示模式 (1=参数设置模式)
3.2 中断服务函数
/* 定时器0中断服务函数 - 1ms周期 */
void Timer0Server() interrupt 1
{
// ... 其他中断任务 ...
/* 长按计时逻辑 */
if((Key_Old == 14) || (Key_Old == 15)) // 检测S14或S15
{
if(++Key_Long == 65535) // 累加计时
Key_Long = 0; // 防止溢出
}
else // 按键松开
{
Key_Long = 0; // 立即清零
}
}
关键点:
- 精确计时: 每1ms累加一次,误差 < 0.1%
- 防溢出保护: 达到65535后归零
- 快速响应: 松开瞬间清零,下次按下重新计时
3.3 按键处理函数
当前实现(存在问题)
void Key_Proc()
{
if(Key_Slow_Down) return;
Key_Slow_Down = 1;
Key_Val = Key_Read();
Key_Down = Key_Val & (Key_Old ^ Key_Val);
Key_Up = ~Key_Val & (Key_Old ^ Key_Val);
Key_Old = Key_Val;
// ... 按键下降沿处理 ...
/* 长按处理 - 当前实现 */
switch(Key_Old)
{
case 14: // S14增加
if(Key_Long >= 250) // ⚠️ 问题:无减速,触发过快
{
if(Display_Mode == 1)
{
switch(Set_Index)
{
case 0:
P_DIS[0] ++;
if(P_DIS[0] >= 70) P_DIS[0] = 70;
break;
case 1:
P_DIS[1] ++;
if(P_DIS[1] >= 70) P_DIS[1] = 70;
break;
}
}
}
break;
case 15: // S15减少 (类似逻辑)
// ... 省略 ...
break;
}
}
问题分析:
触发频率 = 100次/秒(每10ms一次)
参数变化过快,难以精确控制
用户体验差
4. 优化方案
4.1 方案A:固定减速(推荐
)
实现代码
/* 新增全局变量 */
unsigned char Key_Long_Slow; // 长按减速计数器
/* 优化后的按键处理函数 */
void Key_Proc()
{
if(Key_Slow_Down) return;
Key_Slow_Down = 1;
Key_Val = Key_Read();
Key_Down = Key_Val & (Key_Old ^ Key_Val);
Key_Up = ~Key_Val & (Key_Old ^ Key_Val);
Key_Old = Key_Val;
// ... 按键下降沿处理 ...
/* 优化后的长按处理 */
switch(Key_Old)
{
case 14: // S14增加
if(Key_Long >= 250)
{
if(Display_Mode == 1)
{
if(++Key_Long_Slow >= 5) // 减速控制
{
Key_Long_Slow = 0; // 达到阈值后清零
switch(Set_Index)
{
case 0:
P_DIS[0] ++;
if(P_DIS[0] >= 70) P_DIS[0] = 70;
break;
case 1:
P_DIS[1] ++;
if(P_DIS[1] >= 70) P_DIS[1] = 70;
break;
}
}
}
}
break;
case 15: // S15减少
if(Key_Long >= 250)
{
if(Display_Mode == 1)
{
if(++Key_Long_Slow >= 5)
{
Key_Long_Slow = 0;
switch(Set_Index)
{
case 0:
P_DIS[0] --;
if(P_DIS[0] <= 10) P_DIS[0] = 10;
break;
case 1:
P_DIS[1] --;
if(P_DIS[1] <= 10) P_DIS[1] = 10;
break;
}
}
}
}
break;
}
}
/* 中断服务函数修改 */
void Timer0Server() interrupt 1
{
// ... 其他代码 ...
if((Key_Old == 14) || (Key_Old == 15))
{
if(++Key_Long == 65535) Key_Long = 0;
}
else
{
Key_Long = 0;
Key_Long_Slow = 0; // 松开时重置减速计数器
}
}
效果对比
| 项目 | 优化前 | 优化后 |
|---|---|---|
| 触发频率 | 100次/秒 | 20次/秒 |
| 触发间隔 | 10ms | 50ms |
| 调整10→70 | 0.6秒 | 3秒 |
| 用户体验 | ||
| 精确控制 |
4.2 方案B:加速曲线(进阶 
)
实现代码
void Key_Proc()
{
// ... 前置代码 ...
switch(Key_Old)
{
case 14:
if(Key_Long >= 250)
{
if(Display_Mode == 1)
{
unsigned char speed;
/* 渐进加速曲线 */
if(Key_Long < 500) // 0~0.5s: 初期
speed = 10; // 每100ms触发
else if(Key_Long < 1500) // 0.5~1.5s: 中期
speed = 5; // 每50ms触发
else // 1.5s+: 后期
speed = 2; // 每20ms触发
if(++Key_Long_Slow >= speed)
{
Key_Long_Slow = 0;
switch(Set_Index)
{
case 0:
P_DIS[0] ++;
if(P_DIS[0] >= 70) P_DIS[0] = 70;
break;
case 1:
P_DIS[1] ++;
if(P_DIS[1] >= 70) P_DIS[1] = 70;
break;
}
}
}
}
break;
case 15: // 减少逻辑类似
// ... 省略 ...
break;
}
}
速度曲线
触发频率 (次/秒)
↑
50 ┤ ┌──────────
│ │ 快速期
20 ┤ ┌───────────┤
│ │ 中速期 │
10 ┤──────────────┤ │
│ 初速期 │ │
0 └──────────────┴───────────┴──────────→ 按下时长 (秒)
0.5 1.5
效果对比
| 阶段 | 时长 | 速度 | 适用场景 |
|---|---|---|---|
| 初速期 | 0~0.5s | 10次/秒 | 精确定位目标值 |
| 中速期 | 0.5~1.5s | 20次/秒 | 中等范围调整 |
| 快速期 | 1.5s+ | 50次/秒 | 大范围快速调整 |
4.3 方案C:自适应速度(专业 

)
核心思想
根据当前值与边界的距离动态调整速度:
- 离边界远 → 快速调整
- 接近边界 → 自动减速
void Key_Proc()
{
// ... 前置代码 ...
switch(Key_Old)
{
case 14:
if(Key_Long >= 250)
{
if(Display_Mode == 1)
{
unsigned char speed;
unsigned char distance; // 距离边界的距离
/* 自适应速度 */
switch(Set_Index)
{
case 0: // 调整TMAX
distance = 70 - P_DIS[0]; // 距离上限
if(distance > 20) speed = 2; // 远离边界,快速
else if(distance > 5) speed = 5; // 接近边界,中速
else speed = 10; // 临近边界,慢速
break;
case 1: // 调整TMIN
distance = 70 - P_DIS[1];
if(distance > 20) speed = 2;
else if(distance > 5) speed = 5;
else speed = 10;
break;
}
if(++Key_Long_Slow >= speed)
{
Key_Long_Slow = 0;
// 执行增加操作
}
}
}
break;
}
}
5. 参数配置指南
5.1 Key_Long_Slow 阈值选择
计算公式
触发间隔 (ms) = 阈值 × Key_Proc周期
触发频率 (次/秒) = 1000 / 触发间隔
参数对照表
| 阈值 | 触发间隔 | 触发频率 | 10→70耗时 | 适用场景 |
|---|---|---|---|---|
| 1 | 10ms | 100次/秒 | 0.6秒 | |
| 2 | 20ms | 50次/秒 | 1.2秒 | |
| 3 | 30ms | 33次/秒 | 1.8秒 | |
| 5 | 50ms | 20次/秒 | 3秒 | |
| 8 | 80ms | 12.5次/秒 | 4.8秒 | |
| 10 | 100ms | 10次/秒 | 6秒 | |
| 20 | 200ms | 5次/秒 | 12秒 | |
| 50 | 500ms | 2次/秒 | 30秒 | |
| 100 | 1000ms | 1次/秒 | 60秒 |
选择建议
决策树:
参数范围 < 20?
├─ 是 → 阈值 8~10 (精确控制)
└─ 否 ↓
参数范围 20~50?
├─ 是 → 阈值 5 (推荐) ⭐
└─ 否 ↓
参数范围 > 50?
└─ 是 → 阈值 3~5 (快速调整)
5.2 长按触发延迟 (Key_Long >= ?)
标准值对比
| 延迟 | 说明 | 适用场景 |
|---|---|---|
| 150ms | 快速响应 | 游戏、快速操作 |
| 250ms | 标准 | 蓝桥杯推荐 |
| 500ms | 保守 | 防止误触优先 |
| 1000ms | 超保守 |
蓝桥杯建议
#define KEY_LONG_DELAY 250 // 250ms
#define KEY_LONG_SLOW 5 // 50ms间隔
if(Key_Long >= KEY_LONG_DELAY)
{
if(++Key_Long_Slow >= KEY_LONG_SLOW)
{
// 执行动作
}
}
6. 常见问题与解决方案
6.1 问题:长按触发过快,无法精确控制
症状:
想调整到 35,但一松开就跳到 38 了
原因: Key_Long_Slow 阈值过小
解决:
// 修改前
if(++Key_Long_Slow >= 2) // 太快
// 修改后
if(++Key_Long_Slow >= 5) // 合适
6.2 问题:长按后等待很久才开始变化
症状:
按住按键后,等了1秒才看到数字变化
原因: Key_Long 触发延迟过大
解决:
// 修改前
if(Key_Long >= 1000) // 太慢
// 修改后
if(Key_Long >= 250) // 标准
6.3 问题:松开按键后还在继续变化
症状:
松开 S14 后,参数还增加了 2~3 次
原因: 忘记在中断中清零 Key_Long_Slow
解决:
void Timer0Server() interrupt 1
{
if((Key_Old == 14) || (Key_Old == 15))
{
if(++Key_Long == 65535) Key_Long = 0;
}
else
{
Key_Long = 0;
Key_Long_Slow = 0; // ✅ 必须清零
}
}
6.4 问题:调整速度不均匀,有时快有时慢
症状:
有时每秒变化5次,有时每秒变化20次
原因: Key_Long_Slow 没有在执行后清零
解决:
if(++Key_Long_Slow >= 5)
{
Key_Long_Slow = 0; // ✅ 必须清零重计
P_DIS[0] ++;
}
6.5 问题:按键按下瞬间就触发长按
症状:
刚按下 S14,立刻就开始连续增加
原因: 没有检查 Key_Long >= 250 条件
解决:
// 确保有延迟判断
if(Key_Long >= 250) // ✅ 必须先达到阈值
{
if(++Key_Long_Slow >= 5)
{
// 执行动作
}
}
7. 性能分析
7.1 资源占用
| 资源 | 占用量 | 说明 |
|---|---|---|
| RAM | 3字节 | Key_Long(2) + Key_Long_Slow(1) |
| ROM | ~50字节 | 长按判断逻辑 |
| CPU | < 1% | 中断和主循环开销极小 |
7.2 时序精度
| 项目 | 精度 | 误差 |
|---|---|---|
| 计时精度 | 1ms | < 0.01% |
| 触发延迟 | ±10ms | < 4% |
| 触发频率 | ±1次/秒 | < 5% |
7.3 响应时间
最快响应 = 长按延迟 + 减速周期
= 250ms + 50ms
= 300ms
首次触发后 = 每 50ms 触发一次
8. 最佳实践
8.1 蓝桥杯推荐配置
/* 宏定义配置 */
#define KEY_LONG_TRIGGER 250 // 长按触发延迟 (ms)
#define KEY_LONG_INTERVAL 5 // 触发间隔 (×10ms)
/* 全局变量 */
unsigned int Key_Long;
unsigned char Key_Long_Slow;
/* 中断服务函数 */
void Timer0Server() interrupt 1
{
// ... 其他代码 ...
if((Key_Old == 14) || (Key_Old == 15))
{
if(++Key_Long == 65535) Key_Long = 0;
}
else
{
Key_Long = 0;
Key_Long_Slow = 0;
}
}
/* 按键处理函数 */
void Key_Proc()
{
// ... 前置代码 ...
switch(Key_Old)
{
case 14:
if(Key_Long >= KEY_LONG_TRIGGER)
{
if(Display_Mode == 1)
{
if(++Key_Long_Slow >= KEY_LONG_INTERVAL)
{
Key_Long_Slow = 0;
// 执行增加操作
}
}
}
break;
case 15:
// 减少逻辑类似
break;
}
}
8.2 代码规范
推荐写法
// 1. 使用宏定义,便于调整
#define KEY_LONG_SLOW_THRESHOLD 5
// 2. 明确的变量命名
unsigned int Key_Long; // 不要用 kl
unsigned char Key_Long_Slow; // 不要用 kls
// 3. 清晰的注释
if(++Key_Long_Slow >= 5) // 每50ms触发一次
{
Key_Long_Slow = 0; // 清零重计
P_DIS[0] ++; // 执行操作
}
// 4. 防御性编程
if(P_DIS[0] >= 70) P_DIS[0] = 70; // 边界保护
不推荐写法
// 1. 魔法数字
if(++kls >= 5) { } // ❌ 变量名不清晰
// 2. 缺少边界检查
P_DIS[0] ++; // ❌ 可能超出范围
// 3. 忘记清零
if(Key_Long >= 250)
{
P_DIS[0] ++; // ❌ 没有减速控制
}
8.3 调试技巧
技巧1: 串口输出监控
if(Key_Long >= 250)
{
if(++Key_Long_Slow >= 5)
{
Key_Long_Slow = 0;
P_DIS[0] ++;
// 调试输出
printf("Key_Long=%u, Value=%u\n", Key_Long, P_DIS[0]);
}
}
技巧2: LED指示
// 长按触发时点亮 L1
if(Key_Long >= 250)
{
ucLed[0] = 1; // 指示灯亮
}
else
{
ucLed[0] = 0; // 指示灯灭
}
技巧3: 数码管显示计时器
// 临时显示 Key_Long 值,便于调试
Seg_Buf[0] = Key_Long / 1000 % 10;
Seg_Buf[1] = Key_Long / 100 % 10;
Seg_Buf[2] = Key_Long / 10 % 10;
Seg_Buf[3] = Key_Long % 10;
9. 扩展功能
9.1 多按键长按支持
void Key_Proc()
{
// 为每个按键分配独立的计时器
static unsigned char Key_Slow[4] = {0, 0, 0, 0}; // S12~S16
switch(Key_Old)
{
case 12:
if(Key_Long >= 250)
if(++Key_Slow[0] >= 5) { Key_Slow[0] = 0; /* 动作 */ }
break;
case 13:
if(Key_Long >= 250)
if(++Key_Slow[1] >= 5) { Key_Slow[1] = 0; /* 动作 */ }
break;
// ... 其他按键
}
}
9.2 长按/短按区分
void Key_Proc()
{
// 捕捉按键上升沿
Key_Up = ~Key_Val & (Key_Old ^ Key_Val);
if(Key_Up == 14) // S14松开
{
if(Key_Long < 250)
{
// 短按动作:单次增加
P_DIS[0] ++;
}
// 长按动作已在持续过程中执行
}
}
9.3 双键组合长按
// 同时按住 S14 + S15 → 快速复位
if((Key_Old == 14) && (Key_Val == 15)) // 组合按键检测
{
if(Key_Long >= 1000) // 长按1秒
{
// 执行复位操作
P_DIS[0] = 30;
P_DIS[1] = 20;
}
}
10. 附录
10.1 完整示例代码
文件位置: main.c
/* 头文件声明 */
#include <STC15F2K60S2.H>
#include <Init.h>
#include <Key.h>
#include <Seg.h>
/* 宏定义 */
#define KEY_LONG_TRIGGER 250 // 长按触发延迟
#define KEY_LONG_INTERVAL 5 // 连续触发间隔
/* 全局变量 */
unsigned char Key_Val, Key_Down, Key_Old, Key_Up;
unsigned char Key_Slow_Down;
unsigned int Key_Long;
unsigned char Key_Long_Slow;
unsigned char P_DIS[2] = {30, 20};
unsigned char Set_Index;
unsigned char Display_Mode;
/* 按键处理函数 */
void Key_Proc()
{
if(Key_Slow_Down) return;
Key_Slow_Down = 1;
Key_Val = Key_Read();
Key_Down = Key_Val & (Key_Old ^ Key_Val);
Key_Up = ~Key_Val & (Key_Old ^ Key_Val);
Key_Old = Key_Val;
/* 按键下降沿处理 */
switch(Key_Down)
{
case 12: // 模式切换
Display_Mode++;
if(Display_Mode == 2) Display_Mode = 0;
break;
case 13: // 切换设置项
if(Display_Mode == 1)
{
Set_Index++;
if(Set_Index == 2) Set_Index = 0;
}
break;
}
/* 长按处理 */
switch(Key_Old)
{
case 14: // S14增加
if(Key_Long >= KEY_LONG_TRIGGER)
{
if(Display_Mode == 1)
{
if(++Key_Long_Slow >= KEY_LONG_INTERVAL)
{
Key_Long_Slow = 0;
switch(Set_Index)
{
case 0:
P_DIS[0]++;
if(P_DIS[0] >= 70) P_DIS[0] = 70;
break;
case 1:
P_DIS[1]++;
if(P_DIS[1] >= 70) P_DIS[1] = 70;
break;
}
}
}
}
break;
case 15: // S15减少
if(Key_Long >= KEY_LONG_TRIGGER)
{
if(Display_Mode == 1)
{
if(++Key_Long_Slow >= KEY_LONG_INTERVAL)
{
Key_Long_Slow = 0;
switch(Set_Index)
{
case 0:
P_DIS[0]--;
if(P_DIS[0] <= 10) P_DIS[0] = 10;
break;
case 1:
P_DIS[1]--;
if(P_DIS[1] <= 10) P_DIS[1] = 10;
break;
}
}
}
}
break;
}
}
/* 定时器0初始化 */
void Timer0Init(void)
{
AUXR &= 0x7F;
TMOD &= 0xF0;
TL0 = 0x18;
TH0 = 0xFC;
TF0 = 0;
TR0 = 1;
ET0 = 1;
EA = 1;
}
/* 定时器0中断服务函数 */
void Timer0Server() interrupt 1
{
if(++Key_Slow_Down == 10) Key_Slow_Down = 0;
/* 长按计时 */
if((Key_Old == 14) || (Key_Old == 15))
{
if(++Key_Long == 65535) Key_Long = 0;
}
else
{
Key_Long = 0;
Key_Long_Slow = 0;
}
}
/* 主函数 */
void main()
{
System_Init();
Timer0Init();
while(1)
{
Key_Proc();
Seg_Proc();
}
}
10.2 测试用例
| 用例ID | 操作 | 预期结果 | 通过标准 |
|---|---|---|---|
| TC-01 | 短按S14 | 无反应 | |
| TC-02 | 长按S14 (0.3s) | 参数开始连续增加 | |
| TC-03 | 长按S14 (1s) | 增加约20次 | |
| TC-04 | 参数达到70后继续长按 | 停止在70 | |
| TC-05 | 松开S14 | 立即停止增加 | |
| TC-06 | 从10调整到70 | 约3秒完成 |
10.3 参考资料
- STC15F2K60S2 数据手册
- 蓝桥杯单片机竞赛规则
- 《单片机按键检测技术》
- 《嵌入式系统人机交互设计》
版本历史
| 版本 | 日期 | 修改内容 |
|---|---|---|
| v1.0 | 2026-02-03 | 初始版本,完整文档编写 |
文档结束