一、概述
矩阵按键是一种通过行列交叉方式连接多个按键的技术,可以显著减少单片机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 扫描原理(逐行扫描法)
-
第一步:选中第一行
-
将P3_0设置为低电平(0V)
-
将P3_1、P3_2、P3_3设置为高电平(5V)
-
此时只有第一行处于"激活"状态
-
-
第二步:检测列线
-
读取P3_4、P3_5、P3_6、P3_7的电平状态
-
如果某列为低电平,说明该列与第一行交叉处的按键被按下
-
记录对应的按键编号
-
-
第三步:循环扫描
-
依次激活第二行、第三行、第四行
-
每次激活一行后检测所有列线状态
-
通过"行号+列号"组合确定唯一按键
-
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口是准双向口,具有以下特点:
-
无方向寄存器:与某些现代单片机不同,51单片机没有专门的输入/输出方向寄存器
-
自动切换:通过读写操作自动切换模式
-
内部上拉:每个引脚都有内部上拉电阻(约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
理想: 高 ──────┐ 低 ──────┐ 高
实际: 高 ──┐ 低 高 低 ──┐ 低 ──┐ 高
↓ 抖动期 ↓
处理方法:
-
硬件消抖:并联电容(0.1μF)
-
软件消抖:检测到按键后延时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口配置问题
现象:无法正确读取列线状态
检查要点:
-
行线是否正确设置为输出
-
列线是否默认被内部上拉
-
是否在读取列线前将行线置为低电平
八、完整示例程序
#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);
}
}
}
九、总结
矩阵按键扫描技术的关键点:
-
行列分离:将按键布置成矩阵,通过行列交叉点识别按键
-
逐行扫描:每次只激活一行,检测所有列的状态
-
双向IO利用:利用51单片机P3口的准双向特性,动态切换输入/输出模式
-
软件消抖:通过延时消除机械按键的抖动影响
-
编号映射:为每个按键分配唯一编号,便于程序处理
通过掌握这些原理和技术,可以高效地实现多个按键的检测,节省单片机IO资源,提高系统集成度。