第二周第二讲笔记

单片机按键控制LED:从机械开关到智能交互的进阶之路

1. 认识KEY原理图:你的单片机"遥控器"

可以把按键想象成单片机的"遥控器按钮",就像电视遥控器一样,按下不同的键,单片机就会执行不同的命令。

按键工作原理

  • 默认状态:单片机的GPIO引脚默认是高电平(1),相当于按钮"等待被按下"的状态
  • 按下状态:当按键被按下时,引脚直接接地,变为低电平(0),就像按下遥控器按钮发出信号一样

简单比喻:每个按键都是单片机的一个"门铃",按下门铃(按键),单片机就会知道:“哦,有人找我!”

2. 按键检测基础:学会"倾听"按键的声音

2.1 最简单的按键检测

if (P3_4 == 0)  // 如果检测到按键按下(低电平)
{
    P1_0 = 0;   // 点亮LED灯
}

比喻:这就像 constantly 问:“按钮按下了吗?按下了吗?” —— 简单但效率不高

2.2 按键抖动问题:机械开关的"口吃"现象

机械按键在按下和弹起时会有5-10ms的抖动,就像人说话前会"口吃"一样。

消抖解决方案

if (P3_4 == 0)        // 第一次检测到按下
{
    Delay(20);         // 等待"口吃"结束(20ms消抖)
    if (P3_4 == 0)     // 再次确认真的按下了
    {
        // 真正的按键处理逻辑
        while (P3_4 == 0);  // 等待松手(防止重复触发)
        Delay(20);          // 松手消抖
    }
}

3. 高级按键技巧:边沿检测 - 捕捉"按下瞬间"的魔法

3.1 边沿检测的原理

你代码中的这段逻辑非常精妙:

Key_Val = Key_Read();                    // 读取当前按键状态
Key_Down = Key_Val & (Key_Val ^ Key_Old);  // 检测下降沿(按下瞬间)
Key_Up = ~Key_Val & (Key_Val ^ Key_Old);   // 检测上升沿(松开瞬间)
Key_Old = Key_Val;                       // 保存当前状态供下次比较

生活比喻

  • Key_Old就像记忆:记住按键上一刻的状态
  • Key_Val就像现实:按键当前的实际状态
  • Key_Down从没按到按下的瞬间(记忆中是1,现实中是0)
  • Key_Up从按下到松开的瞬间(记忆中是0,现实中是1)

3.2 边沿检测的优势

传统方式只能知道"按键是否被按住",而边沿检测可以精确知道按键刚刚被按下刚刚被松开的瞬间,这是实现高级交互的基础。

4. 矩阵键盘:用少数引脚控制多数按键的"密码术"

你的第三个代码示例展示了矩阵键盘扫描,这是一种高效的按键扩展技术:

4.1 矩阵键盘工作原理

P3_0 = 0; P3_1 = 1; P3_2 = 1; P3_3 = 1;  // 只让第一列为低电平
if (P3_4 == 0) temp = 1;  // 检测第一行
if (P3_5 == 0) temp = 2;  // 检测第二行
// ... 以此类推

比喻:就像在教室里点名——先让第一列同学举手(设置低电平),然后老师依次问每一行的同学:“你在吗?”(检测按键)

4.2 矩阵键盘的优势

  • 4×4矩阵只用8个引脚控制16个按键(节省引脚资源)
  • 通过行列扫描确定具体哪个按键被按下

5. 阻塞延时的问题:单片机为什么会"卡住"

你提到的"delay函数导致的阻塞"是一个非常重要的概念!

5.1 阻塞延时的困境

void delay(unsigned int xms)
{
    // 延时函数内部:CPU在这里"空转"等待
    while(xms--) {
        // 复杂的循环计算,什么都不做,就是等待
    }
}

// 使用时的问题:
while(1) {
    if (按键按下) {
        // 处理按键
    }
  
    delay(500);  // ⚠️ 在这500ms内,单片机无法检测其他按键!
                 // 这就是"卡灯"的原因
}

生动比喻:阻塞延时就像让管家在门口等快递,在快递到达前,管家不能做任何其他事情,即使有客人敲门也听不见。

5.2 阻塞延时的危害

  • :cross_mark: 无法及时响应其他按键
  • :cross_mark: LED动画会卡顿(你遇到的"卡灯"问题)
  • :cross_mark: CPU利用率极低(大部分时间在空等)

6. 解决方案:非阻塞编程 - 让单片机"一心多用"

6.1 状态机思维:从"等待"到"检查"

这是解决阻塞问题的核心思想:

// 非阻塞方式:记录开始时间,然后定期检查
unsigned long previousTime = 0;
unsigned long interval = 500;  // 500ms间隔

while(1) {
    unsigned long currentTime = getCurrentTime();  // 获取当前时间
  
    // 检查是否到了该执行的时间
    if (currentTime - previousTime >= interval) {
        previousTime = currentTime;  // 重置计时
        LED_TOGGLE();                // 执行任务
    }
  
    // 同时可以检测按键,不会互相阻塞!
    if (按键按下) {
        处理按键();
    }
}

比喻:这就像智能管家定期查看门口,而不是一直守着。平时可以打扫房间、接电话,只是偶尔看一眼快递到了没。

6.2 你的代码优化建议

基于非阻塞思想,可以这样改进你的流水灯代码:

// 全局变量记录状态
unsigned long lastLEDTime = 0;
unsigned long LEDInterval = time;  // 可变的间隔时间

void main() {
    while(1) {
        // 1. 非阻塞检测LED更新时间
        if (getCurrentTime() - lastLEDTime >= LEDInterval) {
            lastLEDTime = getCurrentTime();
            ucled = _crol_(ucled, 1);
            P1 = ucled;
        }
    
        // 2. 按键检测(永远不会被阻塞)
        Key_Val = Key_Read();
        Key_Down = Key_Val & (Key_Val ^ Key_Old);
        Key_Old = Key_Val;
    
        // 3. 按键处理
        switch(Key_Down) {
            case 1: System_Flag = 1; break;
            case 2: System_Flag = 0; break;
            case 3: if(time > 100) time -= 100; break;  // 加速
            case 4: if(time < 1000) time += 100; break; // 减速
        }
    }
}

7. 实战技巧与常见问题排查

7.1 按键检测的"最佳实践"

  1. 消抖是必须的:硬件消抖(电容)或软件消抖(延时)
  2. 松手检测:避免一次按下被识别为多次
  3. 状态保存:使用static变量或全局变量记录状态

7.2 调试技巧

当按键不响应时,按照以下顺序排查:

  1. :white_check_mark: 检查硬件连接:按键是否真的接通?引脚定义是否正确?
  2. :white_check_mark: 检查电平逻辑:按下时是否是低电平?松开时是否是高电平?
  3. :white_check_mark: 检查消抖:是否因为抖动导致检测不稳定?
  4. :white_check_mark: 检查阻塞:是否有延时函数阻碍了按键检测?