STC8 单片机矩阵按键控制:从原理到功能实现(附完整代码)
在单片机项目中,当需要多个按键(如 6 个以上)时,传统 “一对一 IO 口” 的独立按键方案会严重浪费 IO 资源。而矩阵按键通过 “行 / 列交叉扫描”,仅需N+M
个 IO 口即可实现N×M
个按键功能(如 4 行 4 列仅需 8 个 IO,实现 16 个按键),是高按键数量场景的最优选择。
一、矩阵按键核心原理(先搞懂再动手)
矩阵按键由 “行线” 和 “列线” 交叉组成,按键两端分别接在一条行线和一条列线上(如 4×4 矩阵:4 条行线 + 4 条列线,共 16 个交叉点,对应 16 个按键)。其检测核心是 **“逐行拉低 + 逐列检测”**:
行线控制:将某一行线拉低(输出低电平),其他行线拉高(输出高电平);
列线检测:读取所有列线的电平,若某一列线为低电平,说明 “当前拉低的行线” 与 “该列线” 交叉处的按键被按下;
循环扫描:依次拉低每一行,重复步骤 2,即可检测所有按键的按下状态。
以 4×4 矩阵为例,若 “行 2 拉低时,列 3 检测到低电平”,则对应按键为 “行 2 列 3”(需提前定义按键编号与行 / 列的对应关系)。
二、项目硬件清单与接线(4×4 矩阵为例)
STC8 单片机 IO 口充足,推荐选择 P1 口(8 个 IO)实现 4×4 矩阵按键(4 行 + 4 列),硬件成本极低,仅需按键、杜邦线和面包板:
硬件模块 | 规格 / 型号 | 作用说明 |
---|---|---|
STC8 单片机 | STC8A8K64U(或同系列) | 主控,实现按键扫描与功能控制 |
矩阵按键 | 4×4 薄膜按键(或独立按键搭建) | 输入设备,提供 16 个按键控制信号 |
面包板 + 杜邦线 | 通用规格 | 搭建电路,连接单片机与按键 |
上拉电阻(可选) | 10kΩ(4 个) | 若行 / 列线无内部上拉,需外接确保电平稳定 |
硬件接线图(关键!按此接线避免错误)
STC8 的 P1 口分为 “行线” 和 “列线”,建议将高 4 位设为行线(输出),低 4 位设为列线(输入),接线如下:
STC8 单片机引脚 | 矩阵按键引脚 | 角色 | 备注 |
---|---|---|---|
P34 | 行 1(Row1) | 输出 | 控制该行电平(拉低 / 拉高) |
P35 | 行 2(Row2) | 输出 | - |
P40 | 行 3(Row3) | 输出 | - |
P41 | 行 4(Row4) | 输出 | - |
P03 | 列 1(Col1) | 输入 | 检测该列电平(判断按键是否按下) |
P06 | 列 2(Col2) | 输入 | - |
P07 | 列 3(Col3) | 输入 | - |
P17 | 列 4(Col4) | 输入 | - |
GND | 矩阵按键 GND | 接地 | 所有按键的公共端需接地 |
注意:STC8 的 IO 口可配置为 “准双向口”(默认),准双向口作输入时需先拉高(避免电平不确定),因此列线无需额外上拉电阻(软件拉高即可)。
三、软件核心实现(从扫描到功能)
矩阵按键的软件核心是 **“消抖 + 扫描 + 按键编码”**:消抖避免按键机械抖动导致误触发,扫描获取按键位置,编码将 “行 / 列坐标” 转为便于使用的按键编号(如 0-15)。以下代码基于 Keil C51 开发,兼容 STC8 全系列单片机。
1. 第一步:基础定义与消抖函数(避免误触发)
按键按下时会有 10-20ms 的机械抖动(电平反复跳变),需通过 “延时消抖” 或 “多次检测” 确保按键状态稳定。这里采用 “延时消抖”(简单易懂,适合新手)。
代码文件:MatrixKey.h
(头文件)
#ifndef __MATRIX_KEYS__
#define __MATRIX_KEYS__
#include "GPIO.h"
#define MK_USE_DOWN 1
#define MK_USE_UP 1
// 只是做声明,用户使用的时候,相应开关打开,函数同时一定要定义
// 按下的回调函数
void MK_on_keydown(u8 row, u8 col);
// 抬起的回调函数
void MK_on_keyup(u8 row, u8 col);
// 初始化
void MK_init();
// 扫描按键
void MK_scan();
#endif
代码文件:MatrixKey.c
(延时消抖函数)//在按键后面加个NOP10();就好
// 扫描按键
void MK_scan() {
u8 r, c;
for (r = 0; r < 4; r++) { // 检查行 r = 0~3
NOP2(); // 可选的延时,可以不写
row_out(r); // 设置行引脚
for (c = 0; c < 4; c++) { // 列
if (IS_KEY_UP(r, c) && col_in(c) == DOWN) { // 上一次抬起,当前按下,按下才有效
SET_KEY_DOWN(r, c); // 保存状态
// printf("第 %d 行第 %d 列 按下\n", (int)(r+1), (int)(c+1));
#if MK_USE_DOWN
MK_on_keydown(r, c);
#endif
} else if (IS_KEY_DOWN(r, c) && col_in(c) == UP) { // 上一次按下,当前抬起,抬起才有效
SET_KEY_UP(r, c);
// printf("第 %d 行第 %d 列 抬起\n", (int)(r+1), (int)(c+1));
#if MK_USE_UP
MK_on_keyup(r, c);
#endif
}
}
}
}
2. 第二步:矩阵按键扫描函数(核心算法)
扫描函数通过 “逐行拉低→逐列检测” 实现按键位置识别,步骤如下:
初始化行线(全部拉高)、列线(全部拉高,确保输入电平稳定);
逐行拉低当前行,其他行保持拉高;
检测所有列线,若某列电平为低,说明对应按键按下,进行消抖后返回按键编号;
若所有行扫描完成无低电平,返回 “无按键”(KEY_NONE)。
代码片段:MatrixKey.c
(扫描函数实现)
#include "MatrixKeys.h"
#define COL1 P03 // 列引脚
#define COL2 P06
#define COL3 P07
#define COL4 P17
#define ROW1 P34 // 行引脚
#define ROW2 P35
#define ROW3 P40
#define ROW4 P41
#define DOWN 0
#define UP 1
u16 states = 0xffff; // 每个按键都是1,都是抬起
#define IS_KEY_UP(r, c) ((states >> (r*4+c) & 1) == 1) // 取出第n位,判断是1
#define IS_KEY_DOWN(r, c) ((states >> (r*4+c) & 1) == 0) // 取出第n位,判断是0
#define SET_KEY_UP(r, c) (states |= (1 << (r*4+c))) // 第n位置1
#define SET_KEY_DOWN(r, c) (states &= ~(1 << (r*4+c))) // 第n位置0
void row_out(u8 r) {
COL4 = COL3 = COL2 = COL1 = 1;
ROW1 = r == 0 ? 0 : 1;
ROW2 = r == 1 ? 0 : 1;
ROW3 = r == 2 ? 0 : 1;
ROW4 = r == 3 ? 0 : 1;
}
u8 col_in(u8 c) {
if (c == 0) return COL1; // 返回列引脚的电平的值
if (c == 1) return COL2;
if (c == 2) return COL3;
if (c == 3) return COL4;
return 0;
}
// 初始化
void MK_init() {
// P03 06 07
P0_MODE_IO_PU(GPIO_Pin_3 | GPIO_Pin_6 | GPIO_Pin_7);
// P17
P1_MODE_IO_PU(GPIO_Pin_7);
// P34 35
P3_MODE_IO_PU(GPIO_Pin_4 | GPIO_Pin_5);
// P40 41
P4_MODE_IO_PU(GPIO_Pin_0 | GPIO_Pin_1);
}
// 扫描按键
void MK_scan() {
u8 r, c;
for (r = 0; r < 4; r++) { // 检查行 r = 0~3
NOP2(); // 可选的延时,可以不写
row_out(r); // 设置行引脚
for (c = 0; c < 4; c++) { // 列
if (IS_KEY_UP(r, c) && col_in(c) == DOWN) { // 上一次抬起,当前按下,按下才有效
SET_KEY_DOWN(r, c); // 保存状态
// printf("第 %d 行第 %d 列 按下\n", (int)(r+1), (int)(c+1));
#if MK_USE_DOWN
MK_on_keydown(r, c);
#endif
} else if (IS_KEY_DOWN(r, c) && col_in(c) == UP) { // 上一次按下,当前抬起,抬起才有效
SET_KEY_UP(r, c);
// printf("第 %d 行第 %d 列 抬起\n", (int)(r+1), (int)(c+1));
#if MK_USE_UP
MK_on_keyup(r, c);
#endif
}
}
}
}
优化说明:代码中 “等待按键释放”(
while(COLx == 0)
)可避免长按导致的重复触发,若需要 “长按连发” 功能,可删除该句,并在主函数中通过定时判断长按时间。
3. 第三步:主函数实现(按键功能控制)
主函数通过循环调用扫描函数获取按键状态,再根据按键编号执行对应功能(这里以 “控制 LED 亮灭” 为例,实际可替换为 “调节屏幕显示、控制电机、设置参数” 等功能)。
代码文件:main.c
(功能实现)
#include "GPIO.h"
#include "Delay.h"
#include "UART.h" // 串口配置 UART_Configuration
#include "NVIC.h" // 中断初始化NVIC_UART1_Init
#include "Switch.h" // 引脚切换 UART1_SW_P30_P31
#include "MatrixKeys.h"
void GPIO_config() {
GPIO_InitTypeDef info;
// ===== UART1 P30 P31 准双向
info.Mode = GPIO_PullUp; // 准双向
info.Pin = GPIO_Pin_0 | GPIO_Pin_1; // 引脚
GPIO_Inilize(GPIO_P3, &info);
}
// 串口配置函数的定义
void UART_config(void) {
// >>> 记得添加 NVIC.c, UART.c, UART_Isr.c <<<
COMx_InitDefine COMx_InitStructure; //结构定义
COMx_InitStructure.UART_Mode = UART_8bit_BRTx; //模式, UART_ShiftRight,UART_8bit_BRTx,UART_9bit,UART_9bit_BRTx
COMx_InitStructure.UART_BRT_Use = BRT_Timer1; //选择波特率发生器, BRT_Timer1, BRT_Timer2 (注意: 串口2固定使用BRT_Timer2)
COMx_InitStructure.UART_BaudRate = 115200ul; //波特率, 一般 110 ~ 115200
COMx_InitStructure.UART_RxEnable = ENABLE; //接收允许, ENABLE或DISABLE
COMx_InitStructure.BaudRateDouble = DISABLE; //波特率加倍, ENABLE或DISABLE
UART_Configuration(UART1, &COMx_InitStructure); //初始化串口1 UART1,UART2,UART3,UART4
NVIC_UART1_Init(ENABLE,Priority_1); //中断使能, ENABLE/DISABLE; 优先级(低到高) Priority_0,Priority_1,Priority_2,Priority_3
UART1_SW(UART1_SW_P30_P31); // 引脚选择, UART1_SW_P30_P31,UART1_SW_P36_P37,UART1_SW_P16_P17,UART1_SW_P43_P44
}
// 按下的回调函数
void MK_on_keydown(u8 r, u8 c) {
// printf("第 %d 行第 %d 列 按下\n", (int)(r+1), (int)(c+1));
if (r == 0 && c == 0) { // 从0开始计算
printf("===========key1按下=========\n");
} else if ((r+1) == 4 && (c+1) == 4) { // 从1开始
printf("===========key16按下=========\n");
}
}
// 抬起的回调函数
void MK_on_keyup(u8 r, u8 c) {
// printf("第 %d 行第 %d 列 抬起\n", (int)(r+1), (int)(c+1));
if (r == 0 && c == 0) {
printf("===========key1抬起=========\n");
} else if ((r+1) == 4 && (c+1) == 4) {
printf("===========key16抬起=========\n");
}
}
void main() {
EA = 1; // 使能中断总开关
GPIO_config(); // GPIO配置
UART_config(); // 串口配置
MK_init(); // 矩阵按键
while (1){
MK_scan();
delay_ms(20);
}
}
四、常见问题排查(避坑指南)
- 所有按键无响应:
检查接线:行线 / 列线是否接反,GND 是否接好;
检查 IO 口方向:行线是否设为输出,列线是否先拉高(准双向口输入需先拉高);
测试延时函数:若延时过短,消抖不彻底,可适当增加消抖时间(如改为 30ms)。
- 按键误触发(按下 A 键触发 B 键):
排查硬件短路:行线与列线是否有短路(面包板接触不良可能导致);
优化扫描逻辑:确保 “扫描完一行后恢复为高电平”(避免相邻行干扰)。
- 按键按下后无释放(一直触发):
检查 “等待按键释放” 代码:若删除了
while(COLx == 0)
,需在主函数中增加 “按键释放检测”;检查按键机械故障:更换按键或清理按键触点(避免触点粘连)。
五、功能拓展(从基础到实用)
长按功能:在扫描函数中记录 “按键按下时间”,若按下时间超过 500ms,判定为长按,执行对应功能(如长按 “上键” 快速调节数值);
组合按键:检测多个按键同时按下(如 “KEY_0+KEY_1”),执行特殊功能(如系统复位);
中断扫描:将列线设为外部中断引脚,仅当列线电平变化时触发中断,再扫描行线,降低 CPU 占用率(适合多任务场景);
结合屏幕显示:将按键功能与前文的 ISP/OLED 屏幕结合,如按下按键切换显示界面、修改时间参数等。