蓝桥杯单片机学习笔记(二)按键

蓝桥杯入门培训(2):按键控制入门

一、认识按键:单片机的“感受器官”

如果把单片机(微控制器)看作一个机器人的大脑,那么LED就像是它的嘴巴——用来对外表达(输出信息),而按键就像是它的耳朵和触觉——用来感知外界的操作(输入信息)。

与LED不同,按键操作是单片机读取外部状态,而不是写入数据。

硬件原理简析

以常见的单片机为例,按键通常这样连接:

  • 按键一端接地(GND,0V),另一端连接到单片机的引脚

  • 当按键按下时,引脚直接连接到地,电压变为0(低电平)

  • 当按键松开时,引脚通过上拉电阻连接到电源,电压为1(高电平)

简单比喻:想象P3口是一个有8个孔的水管(P3_0到P3_7),按键像一个个水坝开关。当按下按键时,对应水管被打开,水流(电流)通向地面,我们就能检测到"有水流出"(低电平0);松开时水管关闭,我们就检测到"没有水流"(高电平1)。

二、独立按键读取

基础按键读取函数

 /* 按键读取函数 */
 unsigned char Key_Read()
 {
     unsigned char temp = 0;
     
     if(P3_4 == 0) temp = 1;  // 如果P3_4引脚为0(低电平),说明按键1按下
     if(P3_5 == 0) temp = 2;  // 如果P3_5引脚为0(低电平),说明按键2按下
     if(P3_6 == 0) temp = 3;  // 如果P3_6引脚为0(低电平),说明按键3按下
     if(P3_7 == 0) temp = 4;  // 如果P3_7引脚为0(低电平),说明按键4按下
     
     return temp;  // 返回按键编号,如果没有按键按下则返回0
 }

工作方式

  • 没有按键按下时,所有引脚都是1(高电平),函数返回0

  • 有按键按下时,对应引脚变为0(低电平),函数返回对应的按键编号

三、高级技巧:按键边沿检测

在实际应用中,我们经常需要检测按键的按下瞬间松开瞬间,而不仅仅是按键是否被按住。这就需要用到"边沿检测"技术。

核心变量解释

 /* 变量声明区域 */
 unsigned char Key_Val;  // 当前时刻读取的按键值(0-4)
 unsigned char Key_Down; // 按下瞬间的按键(只有按下那一刻不为0)
 unsigned char Key_Up;   // 松开瞬间的按键(只有松开那一刻不为0)
 unsigned char Key_Old;  // 上一次的按键值(用于比较)

生动比喻

  • Key_Val:现在谁在敲门?(1-4表示不同人,0表示没人)

  • Key_Old:刚才谁在敲门?(记住上一次的状态)

  • Key_Down:刚刚开始敲门的那一刻(门铃响起)

  • Key_Up:敲门声刚刚停止的那一刻(门铃停止)

边沿检测算法

 void main()
 {
     while(1)  // 单片机主循环,不断重复执行
     {
         Key_Val = Key_Read();  // 读取当前的按键状态
         
         // 魔法公式分解:
         // 1. Key_Val ^ Key_Old:找出状态变化的部分(异或运算)
         // 2. Key_Val & (变化部分):只有在当前为1,之前为0时,结果才为1(下降沿检测)
         // 3. ~Key_Val & (变化部分):只有在当前为0,之前为1时,结果才为1(上升沿检测)
         Key_Down = Key_Val & (Key_Val ^ Key_Old);  // 检测下降沿(按下瞬间)
         Key_Up = ~Key_Val & (Key_Val ^ Key_Old);    // 检测上升沿(松开瞬间)
         
         Key_Old = Key_Val;  // 更新"上一次"的状态,为下一次比较做准备
     }
 }

真值表理解(以按键1为例):

Key_Old Key_Val Key_Down Key_Up 说明
0 0 0 0 持续没有按下
0 1 1 0 刚刚按下(下降沿)
1 1 0 0 持续按住
1 0 0 1 刚刚松开(上升沿)

四、应用实例:不同按键触发方式

要求实现:

  1. 按键1按下瞬间,灯0点亮

  2. 按键2松开瞬间,灯0熄灭

  3. 按键3按住期间,灯2点亮;松开则熄灭

 // 在主循环的边沿检测代码之后添加:
 ​
 if(Key_Down == 1)  // 按键1按下瞬间
     P1_0 = 0;       // 灯0点亮(假设低电平点亮)
 ​
 if(Key_Up == 2)    // 按键2松开瞬间
     P1_0 = 1;       // 灯0熄灭
 ​
 if(Key_Old == 3)   // 按键3持续按住(Key_Old保存着持续状态)
     P1_1 = 0;       // 灯2点亮
 else
     P1_1 = 1;       // 灯2熄灭

实际应用场景

  • Key_Down:用于触发一次性动作,如计数器加1、切换模式

  • Key_Up:用于确认动作,如长按后的确认释放

  • Key_Old:用于检测持续状态,如长按加速、连续发射

五、标志位:状态记忆的"开关"

当某种事物只有两种状态时,我们可以使用bit(位)类型的标志位。

 bit led_state = 0;  // 定义一个位变量,只有0或1两种值
 ​
 // 使用标志位控制LED状态切换
 if(Key_Down == 1)
 {
     led_state = !led_state;  // 取反:0变1,1变0
     P1_0 = led_state;        // 根据标志位控制LED
 }

比喻:标志位就像电灯开关,只有"开"和"关"两种状态,按一下切换一次。

六、矩阵键盘:节省引脚的设计

独立按键每个按键占用一个引脚,当需要多个按键时,引脚不够用。矩阵键盘通过行列交叉的方式,用较少的引脚控制更多的按键。

4×4矩阵键盘原理

工作方式(扫描法):

  1. 选行:将某一行的引脚设置为0(低电平),其他行设置为1(高电平)

  2. 查列:读取所有列引脚的状态

  3. 判断:如果某列为0,说明该行该列的交叉点按键被按下

  4. 轮询:按顺序切换选中的行,实现所有按键的检测

生动比喻
想象一个4层楼(4行)每层4个房间(4列)的酒店:

  • 你首先站在1楼大喊(设置P3_0=0)

  • 听每个房间是否有人回应(检查P3_4到P3_7)

  • 然后去2楼、3楼、4楼重复这个过程

  • 通过"楼层+房间号"就能确定是哪个按键被按下

矩阵键盘读取函数

 /* 4×4矩阵键盘读取函数 */
 unsigned char Key_Read()
 {
     unsigned char temp = 0;
     
     // 第一行扫描(P3_0=0,选中第一行)
     P3_0 = 0; P3_1 = 1; P3_2 = 1; P3_3 = 1;
     if(P3_4 == 0) temp = 1;   // 第1行第1列 -> 按键1
     if(P3_5 == 0) temp = 2;   // 第1行第2列 -> 按键2
     if(P3_6 == 0) temp = 3;   // 第1行第3列 -> 按键3
     if(P3_7 == 0) temp = 4;   // 第1行第4列 -> 按键4
     
     // 第二行扫描(P3_1=0,选中第二行)
     P3_0 = 1; P3_1 = 0; P3_2 = 1; P3_3 = 1;
     if(P3_4 == 0) temp = 5;   // 第2行第1列 -> 按键5
     if(P3_5 == 0) temp = 6;   // 第2行第2列 -> 按键6
     if(P3_6 == 0) temp = 7;   // 第2行第3列 -> 按键7
     if(P3_7 == 0) temp = 8;   // 第2行第4列 -> 按键8
     
     // 第三行扫描(P3_2=0,选中第三行)
     P3_0 = 1; P3_1 = 1; P3_2 = 0; P3_3 = 1;
     if(P3_4 == 0) temp = 9;   // 第3行第1列 -> 按键9
     if(P3_5 == 0) temp = 10;  // 第3行第2列 -> 按键10
     if(P3_6 == 0) temp = 11;  // 第3行第3列 -> 按键11
     if(P3_7 == 0) temp = 12;  // 第3行第4列 -> 按键12
     
     // 第四行扫描(P3_3=0,选中第四行)
     P3_0 = 1; P3_1 = 1; P3_2 = 1; P3_3 = 0;
     if(P3_4 == 0) temp = 13;  // 第4行第1列 -> 按键13
     if(P3_5 == 0) temp = 14;  // 第4行第2列 -> 按键14
     if(P3_6 == 0) temp = 15;  // 第4行第3列 -> 按键15
     if(P3_7 == 0) temp = 16;  // 第4行第4列 -> 按键16
     
     // 恢复所有行为高电平(释放所有行)
     P3_0 = 1; P3_1 = 1; P3_2 = 1; P3_3 = 1;
     
     return temp;  // 返回按键编号(1-16)
 }

七、实际编程注意事项

1. 按键消抖

实际按键按下时会有机械抖动,可能被误判为多次按下:

 // 简单的软件消抖延时
 if(Key_Val != 0)  // 检测到按键按下
 {
     Delay_ms(10);  // 延时10ms跳过抖动期
     if(Key_Val == Key_Read())  // 再次确认按键状态
     {
         // 确认按键真的按下了
     }
 }

2. 扫描频率

  • 按键扫描需要放在主循环中不断执行

  • 扫描频率不能太低(否则会漏检),也不能太高(浪费CPU资源)

  • 通常10-50ms扫描一次比较合适

3. 矩阵键盘的优化

上面的示例代码是基础版本,实际应用中可以优化:

  • 使用循环简化代码

  • 添加消抖处理

  • 处理多个按键同时按下的情况

总结

按键是单片机与用户交互的重要方式,掌握按键编程是嵌入式开发的基础:

  1. 独立按键:简单直接,适合按键数量少的场景

  2. 边沿检测:区分按下瞬间、持续按住、松开瞬间,实现更精细的控制

  3. 矩阵键盘:用较少的引脚控制更多的按键,适合复杂输入需求

  4. 标志位:管理二进制状态,简化程序逻辑