跳转到内容
新建笔记

4×4 矩阵按键:高阻扫描、鬼键与单键消抖

4×4 矩阵把 16 个开关布置在 4 行与 4 列之间,共用 8 根信号线。与每个开关独占一个输入相比,它节省 GPIO,但需要扫描、消抖,并明确多键处理方式。电源和地等连接不包含在“8 根信号线”这个计数里。

开关连接的是一行和一列

跳转到“开关连接的是一行和一列”

每个常开瞬时按键在按下时连接对应行、列,松开时断开。下表是键位映射,不是表示所有交叉点永久相连的电气连线:

行 / 列C0C1C2C3
R0123A
R1456B
R2789C
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];
}
#endif

16 位帧中,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 计时;观察到多键后则必须先稳定全释放,不能直接退回单键继续触发。若产品要求组合键、长按或上电已按住也生效,应另行定义并验证规则。

根据实际原理图填写四个行引脚和四个列引脚,不能直接套用别人的开发板编号。板级回调遵守以下约定:

回调必须实现的行为
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 电流、波形、线路建立时间或机械抖动。接到目标板后仍需完成引脚映射、回调、时基和电气验证。