前言
独立按键虽然简单,但当产品需要十几个按键时,每个按键独占一个GPIO的接法就变得很不经济。矩阵按键通过"行×列"的交叉结构,仅用N+M个GPIO即可驱动N×M个按键。以最常见的4×4矩阵为例,16个按键仅需8个GPIO,引脚利用率提升整整一倍。
本文将从硬件电路 出发,深入讲解行列扫描算法,给出完整的标准库驱动代码,并实现非阻塞消抖 和可靠的事件标记机制。所有代码基于STM32F103C8T6,可直接在工程中编译运行。

一、矩阵按键的硬件结构
1.1 物理连接
矩阵按键由行线(Row)与列线(Column)交叉构成。每个按键位于某一行线与某一列线的交点处,按下时使该行与该列导通。
C0 C1 C2 C3
│ │ │ │
R0 ──┼────┼────┼────┼──
╹ ╹ ╹ ╹
R1 ──┼────┼────┼────┼──
╹ ╹ ╹ ╹
R2 ──┼────┼────┼────┼──
╹ ╹ ╹ ╹
R3 ──┼────┼────┼────┼──
- 行线(R0~R3):配置为推挽输出,扫描时依次拉低。
- 列线(C0~C3):配置为上拉输入(内部上拉或外部上拉电阻),默认读高电平。
当某一行被拉低、该行和某列交叉点的按键闭合时,列线通过闭合触点被拉低,程序即可检测到低电平。
1.2 上拉电阻与引脚配置
列线必须接上拉电阻,以保证悬空时读到确定的高电平。推荐使用外部10kΩ上拉电阻。STM32也可直接配置为GPIO_Mode_IPU,利用内部约40kΩ弱上拉,但抗干扰能力较弱。
本文引脚分配如下(使用PA0~PA7):
| 功能 | 引脚 | 说明 |
|---|---|---|
| 行0 | PA0 | 推挽输出 |
| 行1 | PA1 | 推挽输出 |
| 行2 | PA2 | 推挽输出 |
| 行3 | PA3 | 推挽输出 |
| 列0 | PA4 | 上拉输入 |
| 列1 | PA5 | 上拉输入 |
| 列2 | PA6 | 上拉输入 |
| 列3 | PA7 | 上拉输入 |
1.3 安全注意事项
任何时刻只能拉低一行,其余行必须输出高电平。 如果同时有两行分别输出高和低,当同一列上的两个不同行按键同时按下时,高电平的行将与低电平的行发生短路,可能损坏GPIO。这是软件必须保证的约束。推挽输出只要遵守此规则,就完全安全。
1.4 幽灵键问题(Ghost Key)
多键同时按下时,电流可能通过已闭合的触点形成反向通路,导致未按下的按键被误判为按下。对于常规应用,可在软件中检测到多于2个键同时按下时直接丢弃本次扫描结果;若要求绝对可靠,需在每个按键上串联二极管(如1N4148)。
二、行列扫描算法
2.1 基本流程
- 将所有行线置高电平。
- 逐行扫描:依次将每一行拉低,其余行保持高电平,同时读取所有列线的状态。
- 若某列读到低电平,说明被拉低的这一行与该列的交叉点上的按键被按下。
- 扫描完所有行后,综合结果可获知全部被按下的按键。
2.2 消抖策略
机械按键存在5~20ms的抖动。我们采用固定周期扫描+状态机消抖:
- 通过SysTick产生10ms定时,在
MatrixKey_Scan()中自动限制扫描间隔。 - 为每个按键维护一个消抖计数器,只有连续两次扫描检测到电平与当前稳定状态不同时,才更新稳定状态。
- 稳定状态变化时,产生"按下"或"释放"事件,并标记待消费。
三、标准库完整实现(可直接使用)
3.1 头文件与宏
c
#include "stm32f10x.h"
#include <stdbool.h>
/* 引脚定义 */
#define KEY_PORT GPIOA
#define KEY_ROW0_PIN GPIO_Pin_0
#define KEY_ROW1_PIN GPIO_Pin_1
#define KEY_ROW2_PIN GPIO_Pin_2
#define KEY_ROW3_PIN GPIO_Pin_3
#define KEY_COL0_PIN GPIO_Pin_4
#define KEY_COL1_PIN GPIO_Pin_5
#define KEY_COL2_PIN GPIO_Pin_6
#define KEY_COL3_PIN GPIO_Pin_7
#define KEY_ROWS 4
#define KEY_COLS 4
#define KEY_NUM (KEY_ROWS * KEY_COLS) /* 16 */
/* 消抖参数 */
#define DEBOUNCE_MS 20
#define SCAN_INTERVAL_MS 10 // 扫描间隔10ms,消抖需2次确认
/* LED */
#define LED_GPIO GPIOB
#define LED_PIN GPIO_Pin_0
3.2 按键状态结构
c
typedef enum {
KEY_STATE_IDLE = 0,
KEY_STATE_PRESS,
KEY_STATE_RELEASE
} KeyState;
typedef struct {
uint8_t debounce_cnt; // 消抖计数
bool current_raw; // 当前原始电平(true=未按下)
bool stable; // 消抖后的稳定状态(true=未按下)
KeyState state; // 按键状态
bool event_consumed; // 事件是否已被消费
} KeyInfo;
static KeyInfo key_info[KEY_NUM];
/* 字符映射表 */
static const char key_map[KEY_ROWS][KEY_COLS] = {
{'1', '2', '3', 'A'},
{'4', '5', '6', 'B'},
{'7', '8', '9', 'C'},
{'*', '0', '#', 'D'}
};
3.3 时基(SysTick)
c
volatile uint32_t sysTickUptime = 0;
void SysTick_Init(void) {
if (SysTick_Config(SystemCoreClock / 1000)) {
while (1);
}
NVIC_SetPriority(SysTick_IRQn, 0x0F);
}
void SysTick_Handler(void) {
sysTickUptime++;
}
3.4 GPIO初始化
c
void MatrixKey_GPIO_Init(void) {
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
/* 行线 PA0~PA3 推挽输出,初始全高 */
GPIO_InitStructure.GPIO_Pin = KEY_ROW0_PIN | KEY_ROW1_PIN |
KEY_ROW2_PIN | KEY_ROW3_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(KEY_PORT, &GPIO_InitStructure);
GPIO_SetBits(KEY_PORT, KEY_ROW0_PIN | KEY_ROW1_PIN |
KEY_ROW2_PIN | KEY_ROW3_PIN);
/* 列线 PA4~PA7 上拉输入 */
GPIO_InitStructure.GPIO_Pin = KEY_COL0_PIN | KEY_COL1_PIN |
KEY_COL2_PIN | KEY_COL3_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 内部上拉
GPIO_Init(KEY_PORT, &GPIO_InitStructure);
}
/**
* @brief 初始化按键状态数组,确保上电时状态为"未按下"
*/
void MatrixKey_State_Init(void) {
for (uint8_t i = 0; i < KEY_NUM; i++) {
key_info[i].stable = true; // 初始化为未按下
key_info[i].state = KEY_STATE_IDLE;
key_info[i].event_consumed = true;
key_info[i].debounce_cnt = 0;
}
}
3.5 底层扫描函数
c
/**
* @brief 读取指定行列按键的原始电平
* @param row 行号 (0~3), col 列号 (0~3)
* @retval true: 未按下(高电平) false: 按下(低电平)
*/
static bool MatrixKey_ReadRaw(uint8_t row, uint8_t col) {
const uint16_t row_pin[KEY_ROWS] = {KEY_ROW0_PIN, KEY_ROW1_PIN,
KEY_ROW2_PIN, KEY_ROW3_PIN};
const uint16_t col_pin[KEY_COLS] = {KEY_COL0_PIN, KEY_COL1_PIN,
KEY_COL2_PIN, KEY_COL3_PIN};
/* 全部行先拉高,再拉低目标行 */
GPIO_SetBits(KEY_PORT, KEY_ROW0_PIN | KEY_ROW1_PIN |
KEY_ROW2_PIN | KEY_ROW3_PIN);
GPIO_ResetBits(KEY_PORT, row_pin[row]);
/* 极短延时等待电平稳定 */
for (volatile uint8_t d = 0; d < 5; d++);
/* 读取列状态 */
return (GPIO_ReadInputDataBit(KEY_PORT, col_pin[col]) != Bit_RESET);
}
3.6 消抖与扫描状态机
c
/**
* @brief 矩阵按键扫描函数(每10ms调用一次)
* 内部完成消抖和状态迁移,为每个按键产生一次性事件
*/
void MatrixKey_Scan(void) {
static uint32_t last_scan = 0;
if (sysTickUptime - last_scan < SCAN_INTERVAL_MS) return;
last_scan = sysTickUptime;
for (uint8_t row = 0; row < KEY_ROWS; row++) {
for (uint8_t col = 0; col < KEY_COLS; col++) {
uint8_t idx = row * KEY_COLS + col;
KeyInfo *k = &key_info[idx];
k->current_raw = MatrixKey_ReadRaw(row, col);
/* 消抖计数器:与稳定状态不同则累加,相同则清零 */
if (k->current_raw == k->stable) {
k->debounce_cnt = 0;
} else {
k->debounce_cnt++;
if (k->debounce_cnt >= (DEBOUNCE_MS / SCAN_INTERVAL_MS)) {
// 电平连续2次(20ms)与当前stable不同,更新stable
k->stable = k->current_raw;
k->debounce_cnt = 0;
if (k->stable == false) {
/* 确认按下 */
if (k->state != KEY_STATE_PRESS) {
k->state = KEY_STATE_PRESS;
k->event_consumed = false; // 新事件待消费
}
} else {
/* 确认释放 */
k->state = KEY_STATE_RELEASE;
k->event_consumed = false;
}
}
}
}
}
}
3.7 应用层API
c
/**
* @brief 检测指定按键是否刚被按下(一次性事件,调用后即清除)
* @param row, col 按键位置
* @retval true: 有新的按下事件 false: 无
*/
bool MatrixKey_IsPressed(uint8_t row, uint8_t col) {
uint8_t idx = row * KEY_COLS + col;
KeyInfo *k = &key_info[idx];
if (k->state == KEY_STATE_PRESS && !k->event_consumed) {
k->event_consumed = true;
return true;
}
return false;
}
/**
* @brief 检查指定按键是否处于按住状态(可用于长按连发)
* @retval true: 按键处于按下状态 false: 未按下
*/
bool MatrixKey_IsDown(uint8_t row, uint8_t col) {
uint8_t idx = row * KEY_COLS + col;
return (key_info[idx].state == KEY_STATE_PRESS);
}
/**
* @brief 获取按键对应的字符
*/
char MatrixKey_GetChar(uint8_t row, uint8_t col) {
return key_map[row][col];
}
3.8 主函数示例
c
int main(void) {
GPIO_InitTypeDef GPIO_InitStructure;
/* LED PB0 推挽输出 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitStructure.GPIO_Pin = LED_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(LED_GPIO, &GPIO_InitStructure);
GPIO_ResetBits(LED_GPIO, LED_PIN); // 初始熄灭
/* 矩阵按键初始化 */
MatrixKey_GPIO_Init();
MatrixKey_State_Init(); // 关键:清空状态
SysTick_Init();
while (1) {
MatrixKey_Scan(); // 每循环都调用,内部自动限速
/* 处理所有按键事件 */
for (uint8_t r = 0; r < KEY_ROWS; r++) {
for (uint8_t c = 0; c < KEY_COLS; c++) {
if (MatrixKey_IsPressed(r, c)) {
char ch = MatrixKey_GetChar(r, c);
/* 在此处理按键事件,例如翻转LED */
GPIO_WriteBit(LED_GPIO, LED_PIN,
(BitAction)(1 - GPIO_ReadOutputDataBit(LED_GPIO, LED_PIN)));
// 也可通过串口打印: printf("Key: %c\r\n", ch);
}
}
}
/* 示例:检查"*"键是否按住(连续动作) */
if (MatrixKey_IsDown(3, 0)) { // 第3行第0列,即'*'
// 执行连续操作,如持续调亮度
}
}
}
代码说明:
MatrixKey_State_Init()将全部按键的稳定状态初始化为未按下,防止上电误触发。MatrixKey_Scan()内部由sysTickUptime控制10ms间隔,即使主循环调用再快也不会频繁扫描。- 每个按键的
event_consumed保证一次按下只产生一次IsPressed事件,长按期间不会重复触发。 IsDown()提供持续按住的状态,可用于实现长按加速等逻辑。
四、扩展建议
- 长按识别 :可在每个按键上增加按下时间戳,当
IsDown()为真且持续时间超过阈值时,触发长按事件(需自行扩展状态机)。 - 组合键 :同时检查多个按键的
IsDown()状态即可。 - 低功耗 :将
MatrixKey_Scan()放入定时中断,主循环空闲时调用__WFI(),可大幅降低功耗。
五、常见问题排查
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 按键无反应 | 行线未输出、列线上拉未使能 | 检查GPIO_Mode_Out_PP和GPIO_Mode_IPU |
| 单次按下触发多次 | 事件未消费、消抖不足 | 确认event_consumed机制,检查扫描间隔 |
| 多键同时按下误判 | 幽灵键效应 | 软件丢弃>2键同时按下的结果,或硬件加二极管 |
| 上电后自动触发一次 | 初始状态未校准 | 调用MatrixKey_State_Init() |
| 按键响应慢 | 扫描间隔太长 | 减小SCAN_INTERVAL_MS(建议10ms) |
六、总结
本文从矩阵按键的硬件原理出发,深入讲解了行列扫描算法,并给出了一套完整、可直接使用的标准库驱动。通过固定周期扫描配合消抖状态机,实现了精准、非阻塞的按键识别,且事件消费机制严谨可靠。
这套代码与之前文章中的状态机、调度器及低功耗方案完全兼容,稍作整合即可构建出复杂而稳定的裸机交互系统。
若有任何疑问,欢迎在评论区留言交流!