4×4 矩阵把 16 个开关布置在 4 行与 4 列之间,共用 8 根信号线。与每个开关独占一个输入相比,它节省 GPIO,但需要扫描、消抖,并明确多键处理方式。电源和地等连接不包含在“8 根信号线”这个计数里。
开关连接的是一行和一列
跳转到“开关连接的是一行和一列”每个常开瞬时按键在按下时连接对应行、列,松开时断开。下表是键位映射,不是表示所有交叉点永久相连的电气连线:
| 行 / 列 | C0 | C1 | C2 | C3 |
|---|---|---|---|---|
| R0 | 1 | 2 | 3 | A |
| R1 | 4 | 5 | 6 | B |
| R2 | 7 | 8 | 9 | C |
| R3 | * | 0 | # | D |
原配图标出的按钮帽、复位弹簧、桥式动触点、静触点和接线柱,可以理解为“人手施力 → 机械位移 → 触点改变连接 → 松手复位”。但这张结构示意不足以确定一个实际四脚按键的引脚导通关系,也不保证所有开关都是同一机械结构或常开配置。接入矩阵前,用器件资料或断电通断测量确认常开触点;四个焊脚不一定是四个独立触点。
逐行扫描时,非选中行应释放
跳转到“逐行扫描时,非选中行应释放”本页采用列上拉、选中行拉低的方式:
- C0~C3 为输入,各自有上拉,未被拉低时读高。
- R0~R3 使用开漏输出:写 0 拉低,写 1 释放;也可采用受控切换输入高阻与输出低的实现。
- 同一时刻只选中一行,其余行均保持高阻。切换前先释放上一行。
原文建议选中行推挽低、其他行推挽高。在没有逐键隔离器件的矩阵里,若同一列上两只按键同时按下,就可能通过按键把两个互相冲突的输出连接起来。不能仅靠软件“通常只按一个键”来消除这种电气问题。
例如只按“2”,扫描 R0 时的回路为:
列上拉电源 → C1 上拉电阻 → C1 → 按键 2 → R0 下拉晶体管 → 地此时 C1 读低,位置为 (row=0, col=1);扫描其他行时 R0 被释放,C1 应重新回高。每行都读完四列后才能形成一个完整扫描帧。
STM32F1 的开漏输出模式没有可单独启用的内部上拉,但列配置为输入时可以使用内部上拉。内部偏置是否足够,仍需结合线长、漏电、抗干扰和扫描建立时间判断。STM32F1 GPIO 电平与模式
高阻释放不等于解决了鬼键
跳转到“高阻释放不等于解决了鬼键”无逐键二极管时,多只闭合开关可能形成绕行路径。例如按下 (R0,C0)、(R1,C0)、(R1,C1),扫描 R0 会沿 R0 → C0 → R1 → C1 把 C1 也拉低,于是看起来 (R0,C1) 也按下了。
开漏避免了高低输出争用,却不能恢复被这种连通关系隐藏的真实按键组合。需要可靠多键同时按下时,应采用正确方向的逐键隔离二极管或合适的键盘控制器,并验证其电压、时序与去抖。不同扫描算法的歧义也不完全相同;不能把“能扫描 16 个位置”理解成“支持 16 键同时按下”。Microchip AN3407 给出了矩阵扫描与鬼键的说明,可用于理解这种边界。AN3407,第 2 节
以下示例明确选择单键输入策略:观察到多个位置时丢弃本次输入,等待所有按键稳定释放后,才接受下一次单键按下。它不推测多键组合里哪一键是用户想输入的,也不尝试从鬼键结果恢复真实键值。
完整扫描与单键事件代码
跳转到“完整扫描与单键事件代码”代码分成两层:Matrix_Scan() 读取 16 位扫描帧;Matrix_Update() 消抖并产生字符事件。板级适配层通过 MatrixIo 提供真实引脚操作,所有回调与 context 必须在使用期间有效,函数应由同一执行者调用。
#ifndef MATRIX_LOGIC_H#define MATRIX_LOGIC_H#include <stdbool.h>#include <stdint.h>
typedef struct { void *context; /* row=-1 releases every row; otherwise exactly one row sinks current. */ void (*select_row)(void *context, int row); void (*settle)(void *context); /* Return column 0..3 logic levels in bit 0..3; 1 means high. */ uint8_t (*read_columns)(void *context);} MatrixIo;
static uint16_t Matrix_Scan(const MatrixIo *io){ uint16_t pressed = 0; io->select_row(io->context, -1); for (unsigned row = 0; row < 4; ++row) { io->select_row(io->context, (int)row); io->settle(io->context); uint8_t low = (uint8_t)(~io->read_columns(io->context) & 0x0Fu); pressed |= (uint16_t)((uint16_t)low << (4u * row)); io->select_row(io->context, -1); } return pressed;}
/* -1: released, -2: multiple observed positions, 0..15: one position. */static int Matrix_Decode(uint16_t pressed){ if (pressed == 0u) return -1; if ((pressed & (uint16_t)(pressed - 1u)) != 0u) return -2; for (int i = 0; i < 16; ++i) if ((pressed & (uint16_t)(1u << i)) != 0u) return i; return -2;}
typedef struct { int candidate; uint32_t since_ms; bool armed;} MatrixKeyState;
static char Matrix_Update(MatrixKeyState *state, uint16_t pressed, uint32_t now_ms){ static const char map[] = "123A456B789C*0#D"; int key = Matrix_Decode(pressed); if (key == -2) { state->armed = false; state->candidate = -2; state->since_ms = now_ms; return '\0'; } if (key != state->candidate) { state->candidate = key; state->since_ms = now_ms; return '\0'; } if ((uint32_t)(now_ms - state->since_ms) < 20u) return '\0'; if (key == -1) { state->armed = true; return '\0'; } if (!state->armed) return '\0'; state->armed = false; return map[key];}#endif16 位帧中,bit 4*row+col 为 1 表示该次扫描观察到低电平。Matrix_Decode() 返回 -1 表示全释放、-2 表示多位置,0~15 对应映射表。Matrix_Update() 返回字符 '0' 时是数字键 0,返回 \0 才是没有新事件。
状态应跨调用保留,初始化为:
static MatrixKeyState key_state = {-2, 0u, false};启动后先观察至少 20 ms 的稳定全释放状态,再允许产生按下事件。按住一键不会连发;已经接受一次单键事件后,从一键滑到另一键需要先稳定全释放。尚未接受事件时,候选键改变会重新开始 20 ms 计时;观察到多键后则必须先稳定全释放,不能直接退回单键继续触发。若产品要求组合键、长按或上电已按住也生效,应另行定义并验证规则。
如何接到 STM32 HAL
跳转到“如何接到 STM32 HAL”根据实际原理图填写四个行引脚和四个列引脚,不能直接套用别人的开发板编号。板级回调遵守以下约定:
| 回调 | 必须实现的行为 |
|---|---|
select_row(context, -1) | 给所有开漏行写 SET,释放所有行 |
select_row(context, r) | 已释放其他行后,仅给第 r 行写 RESET;不得把其他行推挽拉高 |
read_columns(context) | 分别读取四列,把高电平映射到 bit 0~3;不能直接把任意整个 IDR 强转为这四位 |
settle(context) | 等待已验证的建立时间,让线路充放电及输入采样稳定;应有明确的时间上限 |
初始化应先启用端口时钟、预装行输出为 SET,再打开开漏输出;列选择符合硬件条件的上拉输入。Matrix_Scan() 开头和每行结束都会释放所有行,函数返回时不会残留某一行选中。
原文的 for (volatile int i=0; i<100; ++i) 不能定义稳定的微秒时间,它受主频、优化和指令实现影响。建立时间要按上拉、电容和采样要求选择,并用计时器或已验证的板级延时实现。行间短暂建立等待和按键的毫秒消抖是两个不同问题;不要在每行插入 20 ms 的消抖延时。
完整帧可按约 1~5 ms 的周期读取,再使用同一个递增毫秒时基调用 Matrix_Update()。20 ms 为示例门限,不是所有按键的固定特性;两帧之间的变化不可见,连续四行也不是完全同一时刻采样。无符号时间差支持正常的计数回绕,但调用间隔不能跨越完整的 32 位毫秒计数周期。
验证范围
跳转到“验证范围”本页原文和唯一结构图已完整阅读,图中文字已转录;推挽争用、鬼键与单键策略分别解释。纯 C 扫描和事件代码经过主机编译测试,包括全部 16 键映射、逐行释放、三键形成矩形鬼键、无键/多键、抖动、长按、释放后再按、上电抑制及时间回绕。
这些测试使用理想开关连通模型和模拟回调,未测量实际 GPIO 电流、波形、线路建立时间或机械抖动。接到目标板后仍需完成引脚映射、回调、时基和电气验证。