STC8单片机矩阵按键控制的功能实现
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~3NOP2(); // 可选的延时,可以不写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_DOWNMK_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_UPMK_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 1u16 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位置0void 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 07P0_MODE_IO_PU(GPIO_Pin_3 | GPIO_Pin_6 | GPIO_Pin_7);// P17P1_MODE_IO_PU(GPIO_Pin_7);// P34 35P3_MODE_IO_PU(GPIO_Pin_4 | GPIO_Pin_5);// P40 41P4_MODE_IO_PU(GPIO_Pin_0 | GPIO_Pin_1);
}// 扫描按键
void MK_scan() {u8 r, c;for (r = 0; r < 4; r++) { // 检查行 r = 0~3NOP2(); // 可选的延时,可以不写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_DOWNMK_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_UPMK_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_BRTxCOMx_InitStructure.UART_BRT_Use = BRT_Timer1; //选择波特率发生器, BRT_Timer1, BRT_Timer2 (注意: 串口2固定使用BRT_Timer2)COMx_InitStructure.UART_BaudRate = 115200ul; //波特率, 一般 110 ~ 115200COMx_InitStructure.UART_RxEnable = ENABLE; //接收允许, ENABLE或DISABLECOMx_InitStructure.BaudRateDouble = DISABLE; //波特率加倍, ENABLE或DISABLEUART_Configuration(UART1, &COMx_InitStructure); //初始化串口1 UART1,UART2,UART3,UART4NVIC_UART1_Init(ENABLE,Priority_1); //中断使能, ENABLE/DISABLE; 优先级(低到高) Priority_0,Priority_1,Priority_2,Priority_3UART1_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 屏幕结合,如按下按键切换显示界面、修改时间参数等。