矩阵按键笔记

一、概述

矩阵按键是一种通过行列交叉方式连接多个按键的技术,可以显著减少单片机IO口的占用。以4×4矩阵键盘为例,只需8个IO口即可检测16个按键。

二、硬件连接原理

2.1 引脚分配

text

行线(输出控制):
P3_0 → 第1行
P3_1 → 第2行
P3_2 → 第3行
P3_3 → 第4行
​
列线(输入检测):
P3_4 → 第1列
P3_5 → 第2列
P3_6 → 第3列
P3_7 → 第4列

2.2 电路连接示意图

text

     列1(P3_4) 列2(P3_5) 列3(P3_6) 列4(P3_7)
        │        │        │        │
行1(P3_0)└────┐  │  ┌────┘        │
        │ 按键1  │  │  按键3       │
行2(P3_1)┌────┘  │  └────┐        │
        │  按键5 │       │  按键7  │
        └────────┴────────┴────────┴

2.3 物理连接方式

每个按键连接在行线和列线的交叉点上:

  • 按键一端连接到某一行线

  • 按键另一端连接到某一列线

  • 行线通过单片机控制输出电平

  • 列线通过上拉电阻连接到VCC,平时保持高电平

三、工作原理

3.1 扫描原理(逐行扫描法)

  1. 第一步:选中第一行

    • 将P3_0设置为低电平(0V)

    • 将P3_1、P3_2、P3_3设置为高电平(5V)

    • 此时只有第一行处于"激活"状态

  2. 第二步:检测列线

    • 读取P3_4、P3_5、P3_6、P3_7的电平状态

    • 如果某列为低电平,说明该列与第一行交叉处的按键被按下

    • 记录对应的按键编号

  3. 第三步:循环扫描

    • 依次激活第二行、第三行、第四行

    • 每次激活一行后检测所有列线状态

    • 通过"行号+列号"组合确定唯一按键

3.2 扫描时序图

text

时间轴:  T1      T2      T3      T4
         ↓       ↓       ↓       ↓
行1:  ──0──────1──────1──────1──
行2:  ──1──────0──────1──────1──
行3:  ──1──────1──────0──────1──
行4:  ──1──────1──────1──────0──
​
列检测: 在T1期间检测4列
        在T2期间检测4列
        在T3期间检测4列
        在T4期间检测4列

四、代码详解

4.1 完整扫描代码

// 按键读取函数
unsigned char Key_Read() {
    unsigned char temp = 0;  // 用于存储按键编号,0表示无按键
​
    // 扫描第一行
    P3_0 = 0;  // 第一行输出低电平(选中)
    P3_1 = 1;  // 其他行输出高电平(不选中)
    P3_2 = 1;
    P3_3 = 1;
    
    // 检测各列状态
    if(P3_4 == 0) temp = 1;   // 第一行第一列
    if(P3_5 == 0) temp = 2;   // 第一行第二列
    if(P3_6 == 0) temp = 3;   // 第一行第三列
    if(P3_7 == 0) temp = 4;   // 第一行第四列
​
    // 扫描第二行
    P3_0 = 1;
    P3_1 = 0;  // 第二行输出低电平
    P3_2 = 1;
    P3_3 = 1;
    
    if(P3_4 == 0) temp = 5;   // 第二行第一列
    if(P3_5 == 0) temp = 6;   // 第二行第二列
    if(P3_6 == 0) temp = 7;   // 第二行第三列
    if(P3_7 == 0) temp = 8;   // 第二行第四列
​
    // 扫描第三行
    P3_0 = 1;
    P3_1 = 1;
    P3_2 = 0;  // 第三行输出低电平
    P3_3 = 1;
    
    if(P3_4 == 0) temp = 9;   // 第三行第一列
    if(P3_5 == 0) temp = 10;  // 第三行第二列
    if(P3_6 == 0) temp = 11;  // 第三行第三列
    if(P3_7 == 0) temp = 12;  // 第三行第四列
​
    // 扫描第四行
    P3_0 = 1;
    P3_1 = 1;
    P3_2 = 1;
    P3_3 = 0;  // 第四行输出低电平
    
    if(P3_4 == 0) temp = 13;  // 第四行第一列
    if(P3_5 == 0) temp = 14;  // 第四行第二列
    if(P3_6 == 0) temp = 15;  // 第四行第三列
    if(P3_7 == 0) temp = 16;  // 第四行第四列
​
    return temp;  // 返回按键编号(1-16)或0(无按键)
}

4.2 代码关键点解析

4.2.1 temp变量的作用

  • 存储按键编号:检测到按键时,将对应的按键编号(1-16)存入temp

  • 初始值为0:表示没有按键按下

  • 返回值意义:主程序通过返回值判断哪个按键被按下

4.2.2 行列对应关系

按键编号与行列对应关系:

按键编号 行号 列号 检测条件
1-4 第1行 1-4列 P3_0=0时检测
5-8 第2行 1-4列 P3_1=0时检测
9-12 第3行 1-4列 P3_2=0时检测
13-16 第4行 1-4列 P3_3=0时检测

4.3 改进版本(带消抖)

c

// 带延时消抖的按键读取函数
unsigned char Key_Read_With_Debounce() {
    unsigned char temp = 0;
    
    // 扫描第一行
    P3_0 = 0; P3_1 = 1; P3_2 = 1; P3_3 = 1;
    Delay_ms(5);  // 延时消抖
    
    if(P3_4 == 0) {
        Delay_ms(10);  // 再次确认
        if(P3_4 == 0) temp = 1;
    }
    // ... 其他按键类似
    
    return temp;
}

五、51单片机P3口特性

5.1 双向IO口特性

51单片机的P3口是准双向口,具有以下特点:

  1. 无方向寄存器:与某些现代单片机不同,51单片机没有专门的输入/输出方向寄存器

  2. 自动切换:通过读写操作自动切换模式

  3. 内部上拉:每个引脚都有内部上拉电阻(约20-50kΩ)

5.2 输入/输出模式切换原理

作为输出时:

P3_0 = 0;  // 向锁存器写入0,引脚输出低电平
  • 数据写入内部锁存器

  • 输出驱动电路工作

  • 引脚输出指定电平

作为输入时:

if(P3_4 == 0)  // 读取引脚实际电平
  • 读取的是引脚的实际电压状态

  • 必须先写1(使内部上拉有效)

  • 外部低电平可将引脚拉低

5.3 矩阵按键中的IO口使用策略

text

扫描阶段1(行输出):
  P3_0~P3_3: 输出模式(赋值语句控制)
  P3_4~P3_7: 输入模式(读取语句检测)
​
扫描阶段2(切换行):
  改变输出行,其他不变
  
关键:同一时间,行线作为输出,列线作为输入

六、实际应用要点

6.1 上拉电阻的重要性

text

         +5V
          │
        10kΩ(上拉电阻)
          │
    列线───┼────→ 到P3_4~P3_7
          │
          └────→ 平时保持高电平
  • 未按键时:列线通过上拉电阻保持高电平

  • 按键按下时:行线的低电平通过按键将列线拉低

  • 如果没有上拉电阻,列线会处于悬浮状态,电平不确定

6.2 扫描频率选择

  • 太快:可能检测不到按键(机械按键有抖动)

  • 太慢:响应延迟,用户体验差

  • 推荐:10-50ms扫描一次

6.3 按键抖动处理

机械按键的抖动现象:

text

理想:   高 ──────┐ 低 ──────┐ 高
实际:   高 ──┐ 低 高 低 ──┐ 低 ──┐ 高
              ↓ 抖动期 ↓

处理方法:

  1. 硬件消抖:并联电容(0.1μF)

  2. 软件消抖:检测到按键后延时10-20ms再次确认

七、常见问题与解决方案

问题1:按键检测不稳定

可能原因

  • 没有消抖处理

  • 扫描频率不合适

  • 电路接触不良

解决方案

// 添加软件消抖
if(P3_4 == 0) {
    Delay_ms(15);  // 延时避开抖动期
    if(P3_4 == 0) {
        // 确认按键按下
        while(P3_4 == 0);  // 等待释放
    }
}

问题2:多个按键同时按下

现象:只能检测一个按键或检测错误

原因:逐行扫描法对多键支持有限

改进方法

// 记录所有按键状态
unsigned char key_state[16] = {0};
​
// 扫描并更新状态矩阵
void Key_Scan_Matrix() {
    for(int row=0; row<4; row++) {
        // 激活当前行
        // 检测所有列
        // 更新key_state数组
    }
}

问题3:IO口配置问题

现象:无法正确读取列线状态

检查要点

  1. 行线是否正确设置为输出

  2. 列线是否默认被内部上拉

  3. 是否在读取列线前将行线置为低电平

八、完整示例程序

#include <reg51.h>
​
// 定义按键引脚
sbit ROW1 = P3^0;
sbit ROW2 = P3^1;
sbit ROW3 = P3^2;
sbit ROW4 = P3^3;
sbit COL1 = P3^4;
sbit COL2 = P3^5;
sbit COL3 = P3^6;
sbit COL4 = P3^7;
​
// 延时函数
void Delay_ms(unsigned int ms) {
    unsigned int i, j;
    for(i=0; i<ms; i++)
        for(j=0; j<123; j++);
}
​
// 按键初始化
void Key_Init() {
    // 所有行线置为高电平(不选中)
    ROW1 = 1;
    ROW2 = 1;
    ROW3 = 1;
    ROW4 = 1;
    
    // 列线默认就是输入状态
    // 内部上拉使其保持高电平
}
​
// 按键扫描函数(带消抖)
unsigned char Key_Scan() {
    unsigned char key_value = 0;
    
    // 扫描第一行
    ROW1 = 0; ROW2 = 1; ROW3 = 1; ROW4 = 1;
    Delay_ms(5);  // 消抖
    
    if(COL1 == 0) { key_value = 1; goto end; }
    if(COL2 == 0) { key_value = 2; goto end; }
    if(COL3 == 0) { key_value = 3; goto end; }
    if(COL4 == 0) { key_value = 4; goto end; }
    
    // 扫描第二行
    ROW1 = 1; ROW2 = 0; ROW3 = 1; ROW4 = 1;
    Delay_ms(5);
    
    if(COL1 == 0) { key_value = 5; goto end; }
    if(COL2 == 0) { key_value = 6; goto end; }
    if(COL3 == 0) { key_value = 7; goto end; }
    if(COL4 == 0) { key_value = 8; goto end; }
    
    // 扫描第三行
    ROW1 = 1; ROW2 = 1; ROW3 = 0; ROW4 = 1;
    Delay_ms(5);
    
    if(COL1 == 0) { key_value = 9; goto end; }
    if(COL2 == 0) { key_value = 10; goto end; }
    if(COL3 == 0) { key_value = 11; goto end; }
    if(COL4 == 0) { key_value = 12; goto end; }
    
    // 扫描第四行
    ROW1 = 1; ROW2 = 1; ROW3 = 1; ROW4 = 0;
    Delay_ms(5);
    
    if(COL1 == 0) { key_value = 13; goto end; }
    if(COL2 == 0) { key_value = 14; goto end; }
    if(COL3 == 0) { key_value = 15; goto end; }
    if(COL4 == 0) { key_value = 16; goto end; }
    
end:
    // 恢复所有行线为高电平
    ROW1 = 1; ROW2 = 1; ROW3 = 1; ROW4 = 1;
    
    return key_value;
}
​
// 主函数示例
void main() {
    unsigned char key_val;
    
    Key_Init();  // 按键初始化
    
    while(1) {
        key_val = Key_Scan();  // 扫描按键
        
        if(key_val != 0) {
            // 根据按键值执行相应操作
            switch(key_val) {
                case 1:
                    // 按键1按下,执行相应操作
                    break;
                case 2:
                    // 按键2按下
                    break;
                // ... 其他按键
                default:
                    break;
            }
            
            // 等待按键释放
            while(Key_Scan() != 0);
        }
    }
}

九、总结

矩阵按键扫描技术的关键点:

  1. 行列分离:将按键布置成矩阵,通过行列交叉点识别按键

  2. 逐行扫描:每次只激活一行,检测所有列的状态

  3. 双向IO利用:利用51单片机P3口的准双向特性,动态切换输入/输出模式

  4. 软件消抖:通过延时消除机械按键的抖动影响

  5. 编号映射:为每个按键分配唯一编号,便于程序处理

通过掌握这些原理和技术,可以高效地实现多个按键的检测,节省单片机IO资源,提高系统集成度。