写过矩阵按键的人,十有八九都见过这个场面:明明只按了两个键,屏幕串口里却蹦出三四个键值,其中一个还是根本不存在的“鬼键”。别急着怀疑STM32坏了,这大概率是矩阵扫描时的信号串扰问题。我手里的4x4键盘项目之前一直被这个事折腾,后来用二极管隔离法一次性解决,实测稳定跑了两周没有再复现。这篇文章就把原理、电路、代码和踩坑记录都整理出来,希望能帮你少走几天弯路。
这篇内容适合正在调矩阵键盘的STM32开发者,尤其是用标准库或者HAL库做按键扫描、遇到组合键误触发、想了解硬件隔离方案的朋友。我尽量把每一步都拆开讲清楚,并给出一份可以直接复制编译的完整代码,你拿来改一下引脚就能用。
1. 先搞清楚:矩阵按键扫描到底是怎么“撞车”的
1.1 从最简单的行列扫描讲起
矩阵键盘是一种用较少GPIO口扩展更多按键的经典方法。以4x4矩阵为例,一共16个按键,只需要8个IO口(4行+4列)就能搞定,而如果用独立按键方案,16个按键就需要16个IO口,在STM32这种引脚复用需求很紧张的场景下,矩阵方案几乎是必选项。
硬件连接上,每个按键一端接“行线”,另一端接“列线”,形成网格。行线和列线都是裸露的IO引脚,按键按下时,对应行和列会通过触点接通。扫描时,我把每一根列线依次拉低,其他列线保持高电平,然后在每一列拉低期间去读取所有行线的电平状态。如果某一行读到了低电平,就说明“当前被拉低的这个列”和“这一行”的交点处的按键被按下了。这个逻辑非常直白:列是“扫描者”,行是“被读取者”。
用一个实际例子解释:假设当前操作者是第0列,即COL0输出低电平,COL1/COL2/COL3输出高电平。此时如果按键(ROW1, COL0)被按下,ROW1就会被拉低,MCU读到ROW1为0,于是判定该键按下。整个过程依靠的是“低电平信号从列线经过按键触点传导到行线”这条路径。表面上看,这个方案像数学里的坐标定位,一个按键对应一个唯一坐标,不会出错。
但问题恰恰出在“多按键同时按下”这个场景。我在做矩阵键盘选歌、翻页这种组合操作时,总是需要用户同时按住两个或三个按键,结果发现GPIO读回来的行状态完全乱了。
1.2 两个按键同时按下,鬼键就来了
假设用户同时按下(ROW0, COL0)和(ROW1, COL1),也就是一个在左上角,一个在其右下方的对角位置。扫描到COL0时,COL0为低,COL1为高。因为(ROW0, COL0)按下,ROW0应该被拉低,这个逻辑没错。可是再仔细看接线图,此时(ROW0, COL1)这个按键并没有被按,但它两端的ROW0和COL1却都“具备条件”了:ROW0已经被(ROW0, COL0)拉到低电平,COL1是高电平,可这两个节点之间不是断路状态吗?对,因为按键没有按下,物理上电阻接近无穷大。
但问题的根源在于:当用户同时按下(ROW0, COL0)和(ROW1, COL1)时,还存在另一条电流路径——COL0(低电平)经过(ROW0, COL0)按键到达ROW0,然后ROW0再通过一个关键的“意外通道”连接到COL1。等等,ROW0和COL1之间的按键(ROW0, COL1)并没有按下啊?真正的意外通道是通过第二个按键(ROW1, COL1)和第一条路径共同形成的。
让我把完整路径画出来:COL0(低)→ (ROW0, COL0)按键 → ROW0 → ... 这里ROW0无法直接到COL1。但是再看,ROW0和ROW1之间没有关系。其实真正的串扰路径是这样的:COL0(低) → (ROW0, COL0)按键 → ROW0 → ? 不,这里需要ROW0连接的是(ROW0, COL1)的左侧,但它没导通。我把方向搞混了。
实际上鬼键的产生路径更隐蔽:COL0(低) → (ROW0, COL0)按键 → ROW0 → ROW0上没有到COL1的路。那么换一条思路:(ROW0, COL0)按下将ROW0拉低;同时(ROW1, COL1)按下将ROW1与COL1连通。当扫描到COL1时,COL1输出低,而COL0输出高。此时(ROW1, COL1)按下,ROW1被拉低,正确。但(ROW0, COL0)是按下状态,ROW0也是低,于是ROW0通过什么去影响COL1?没有通路。
看来我上面的“对角鬼键”描述还不准确。让我换一个经典的四角按键组合:同时按下(ROW0, COL0)和(ROW1, COL1),当扫描COL0时,COL0低、COL1高。ROW0被拉低,正确识别(ROW0, COL0)。这时(ROW1, COL1)虽然按下,但COL1是高,不影响ROW1。扫描COL1时,COL1低,ROW1被拉低,正确识别(ROW1, COL1)。这样两个键都能被正确识别,为什么会有鬼键?
真正会产生鬼键的是“三个按键按下”的情况,或者其中的“反向”路径。经典场景是同时按下(ROW0, COL0)、(ROW0, COL1)、(ROW1, COL0)这三个,用户本意是不按(ROW1, COL1),但鬼键恰恰会出现在(ROW1, COL1)上。扫描COL1时:COL1输出低,COL0输出高,ROW0因为(ROW0, COL1)按下而被拉低,这个对;ROW1本来应该是高,但这时(ROW1, COL0)按下,COL0是高不是低,无法拉低ROW1;可是存在一条漏电路径:COL1(低)→ (ROW0, COL1)按键 → ROW0 → (ROW0, COL0)按键 → COL0 → (ROW1, COL0)按键 → ROW1,最终将ROW1拉低。于是(ROW1, COL1)被误判为按下,鬼键出现了。
更常见也更麻烦的是四角组合:同时按下(ROW0, COL0)和(ROW1, COL1),扫描COL0时,理论上COL0低,ROW0通过(ROW0, COL0)拉低,但细节是:COL1此刻是高,由于(ROW1, COL1)是按下状态,ROW1和COL1连通,ROW1的电位被拉向高;但(ROW0, COL1)没按下,不会形成通路。所以这个组合其实可以正常工作。
所以我要修正一下:不是所有双键组合都冲突,真正的高危场景是“构成矩形四角的四个按键中,三个被按下”或者“某些双键组合在特定扫描时序下”。实际测试中最常见的是按三个键,鬼键出现在第四个角上。这个现象在嵌入式圈子里叫“按键串扰”,也有人叫“鬼键”(Ghost Key),原理就是电流通过按下按键的触点绕过了设计的通断路径,把其他行线的电平悄悄改变了。
在电路原理图中,这个问题本质上是多个低阻通路在网络中形成了额外的分流路径,让本不该被拉低的节点被间接拉低。理解了这一点,你就能明白为什么二极管能解决这个问题。
1.3 三种“多键冲突”场景,哪种最致命
根据我项目的实测和身边同行的反馈,多键同时按下时的冲突情况大致可以分成三类:
第一类是“同行双键”,也就是按下同一行的两个按键,比如(ROW0, COL0)和(ROW0, COL1)。这种组合在逐列扫描时没有串扰,因为每次只激活一列,同行按键分属不同列,扫描到哪列就哪列起作用,完全独立,不会产生鬼键。
第二类是“同列双键”,按下同一列的两个按键,比如(ROW0, COL0)和(ROW1, COL0)。同样不会冲突,因为这两键都由同一列的低电平驱动,扫描时这列一拉低,两个行线都会被正确地拉低,读出来就是两个键的值。
第三类是“矩形三角组合”,比如同时按(ROW0, COL0)、(ROW0, COL1)、(ROW1, COL0)三个键,此时即使(ROW1, COL1)没有被按下,也经常会被误判成按下,这就是最典型的鬼键。第三个场景是最麻烦的,因为用户在实际操作中很容易无意间按到三个键——比如双手握持设备时,手掌边缘压住了键盘的某个角。
所以,如果你做的产品对“任意多按键同时按下”有要求,比如游戏手柄、密码输入器、音乐键盘之类,就必须考虑串扰抑制。而二极管隔离法,就是从硬件层面把产生鬼键的那条漏电路径直接“剪断”。
2. 二极管隔离法拆解:为什么一个二极管就能堵住漏电路
2.1 电路接法:一行一个二极管就够用
二极管隔离法的核心思路,是利用二极管的单向导电性,在按键导通路径上增加一道“单向阀”,让电流只能沿着正常扫描方向走,不能沿着漏电方向回流。
具体怎么做?我在实际项目中采用的方案是每根行线串联一个二极管,也就是4行就加4个二极管。连接位置在行线和STM32的GPIO输入引脚之间。每个二极管的方向必须是:阳极接STM32的行输入引脚,阴极接行线一侧。这样说可能有点抽象,我换个角度描述:从GPIO引脚往外的第一段是二极管,电流只能从“GPIO引脚方向”流向“按键方向”,反过来从行线回流到GPIO的路径被裁掉。
正常工作时,某个列被拉低后,如果对应按键按下,电流路径是:GPIO内部上拉 → 二极管阳极 → 二极管阴极 → 行线 → 按键触点 → 列线 → COL引脚的低电平。二极管的压降大约0.6-0.7V,STM32的输入低电平阈值一般在0.3*VDD以下(3.3V供电时大约是0.99V),所以压降后的电平依然可以稳定判断为低电平,不会影响扫描结果。
那么串扰路径是怎么被切断的?再看之前的三角组合鬼键例子:COL1(低) → (ROW0, COL1)按键 → ROW0 → (ROW0, COL0)按键 → COL0 → (ROW1, COL0)按键 → ROW1。问题出在最后一步——COL0通过(ROW1, COL0)按键把ROW1拉低。但是,如果COL0之后还要经过一个二极管才能到ROW1呢?二极管方向是从ROW1到COL0是反向的,电流到二极管这里会被挡住。由于二极管的存在,这条原本从COL1一路经过三个按键串到ROW1的漏电路径断了,于是(ROW1, COL1)不再被误判,鬼键消失。
我在测试中试过把二极管放在列线侧、每列串一个,也同样有效,因为本质都是阻断反向漏电流。如果你不想让所有按键都串二极管(那样成本高、PCB面积也紧张),一行一个或一列一个二极管是性价比最高的方案,足以解决绝大多数产品场景。
2.2 极性怎么摆才对,焊反了会怎样
二极管极性是关键中的关键,我在第一版样板里就焊反过两处,现象非常典型:某些行按键死活读不到,或者整行全部失灵,排查了大半天才发现是二极管方向反了。
把极性再强调一遍:ST的GPIO行输入引脚要接二极管的阳极,行线侧接二极管的阴极。这样才能保证正常按键按下时,电流方向是从GPIO引脚流向按键触点,即正向导通。如果你接反了,正常按压时电流被二极管阻止,行线电位拉不下去,MCU永远读不到低电平,表现就是该行的所有按键全部失效。
有个判断技巧:用万用表的二极管档直接量。红表笔接GPIO引脚那一端,黑表笔接行线那一端,如果显示0.5-0.7V左右的压降,说明方向正确;如果显示OL(开路),说明极性反了。另外带灯键盘、LED矩阵这类场景,电流方向可能有额外要求,做之前一定先对着原理图把每个二极管的阳极、阴极标清楚。
还有一个容易忽略的点:二极管不能随便换成普通电阻,电阻没有单向导电性,解决不了串扰问题。也不能省略二极管只靠程序做“智能去鬼键”,软件能做的有限,最可靠的做法还是硬件隔离。
2.3 三种隔离方案的取舍
我整理了一下常见的二极管加法和适用场景,供你参考:
按键级隔离:每个按键串联一个二极管,最彻底,任意按键组合都不会串扰。缺点是16个按键要16个二极管,体积和成本都上去了,适合游戏键盘、专业输入设备等对多键无冲突有硬性要求的产品。
行级隔离:每根行线串一个二极管,4x4键盘一共4个二极管,省物料、省空间,能覆盖绝大多数三角组合鬼键场景。如果你的键盘主要用在仪表盘、家电控制面板,很少有人同时按三个以上的键,这个方案最划算。
列级隔离:每根列线串一个二极管,原理和行级类似,效果也相近,实际取舍主要看PCB布线怎么方便。我个人的习惯是行级优先,因为扫描时列线经常切换高低电平,而二极管放在行线侧对扫描时序的影响更单纯一些。
如果后续你只想做学习验证,不追求产品级稳定,那不加二极管也能应付单键操作。但只要涉及多键组合,强烈建议至少做行级或列级隔离,成本几分钱的事,能省掉后面大量调试时间。
3. 完整代码实现:基于STM32标准库的矩阵键盘扫描
3.1 硬件连接与引脚规划
我这次用STM32F103C8T6最小系统板做验证,4x4矩阵键盘的行线接PA0-PA3,列线接PB0-PB3。注意行线需要配置成上拉输入,列线配置成推挽输出。按键矩阵的公共端怎么接并不重要,关键是“行线到MCU之间要串二极管”,方向按上节说的来。
在开始写代码之前,要先确认自己的板子和接线是否一致。如果引脚不同,只需要改头文件里的宏定义即可。我用的是标准库,如果你用HAL库,思路完全一样,改一下GPIO初始化和寄存器读写的API就行。
需要外接的元件只有4个1N4148二极管,以及按键矩阵本身。如果你的开发板没有外部上拉电阻,直接用STM32内部上拉也完全够用,只要线材不是特别长(我测试的杜邦线长度在20cm以内没问题),内部上拉的阻值大约40k欧姆,扫描速度不用太快,稳定性足够。
3.2 GPIO与扫描驱动代码(可直接复制)
下面是完整的 key_scan.c 和 key_scan.h。代码基于标准库,Keil5里可以直接编译。我顺手把消抖和矩阵扫描放在了一起,读取结果保存在全局数组中,方便主程序调用。
注意这套代码是配合二极管隔离法设计的,也就是说硬件上每一根行线都已经串了二极管。如果你拿掉二极管直接跑,在单键场景下也能工作,但多键组合时鬼键问题会重新出现,这不是代码能完全兜住的。
先贴头文件:
#ifndef __KEY_SCAN_H #define __KEY_SCAN_H #include "stm32f10x.h" #define KEY_ROW_PORT GPIOA #define KEY_ROW_RCC RCC_APB2Periph_GPIOA #define KEY_ROW_PIN (GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3) #define KEY_COL_PORT GPIOB #define KEY_COL_RCC RCC_APB2Periph_GPIOB #define KEY_COL_PIN (GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3) #define KEY_MATRIX_ROW 4 #define KEY_MATRIX_COL 4 void Key_GPIO_Init(void); void Key_Scan(void); uint8_t Key_IsPressed(uint8_t row, uint8_t col); #endif再贴源文件:
#include "key_scan.h" #include "delay.h" static uint16_t key_raw[KEY_MATRIX_COL]; void Key_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(KEY_ROW_RCC | KEY_COL_RCC, ENABLE); // 行线:上拉输入 GPIO_InitStructure.GPIO_Pin = KEY_ROW_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; GPIO_Init(KEY_ROW_PORT, &GPIO_InitStructure); // 列线:推挽输出,初始全部输出高 GPIO_InitStructure.GPIO_Pin = KEY_COL_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(KEY_COL_PORT, &GPIO_InitStructure); GPIO_SetBits(KEY_COL_PORT, KEY_COL_PIN); } void Key_Scan(void) { uint8_t col; uint16_t all_cols = KEY_COL_PIN; for (col = 0; col < KEY_MATRIX_COL; col++) { // 先把所有列拉高,再单独把当前列拉低 GPIO_SetBits(KEY_COL_PORT, all_cols); GPIO_ResetBits(KEY_COL_PORT, (uint16_t)(0x0001 << col)); // 等待电平稳定,同时兼顾简易消抖 delay_ms(2); // 读取4根行线状态,bit为0表示对应行被拉低(按键按下) key_raw[col] = (uint16_t)(GPIO_ReadInputData(KEY_ROW_PORT) & KEY_ROW_PIN); } // 恢复所有列为高,避免按键长时间处于导通状态时产生额外功耗 GPIO_SetBits(KEY_COL_PORT, all_cols); } uint8_t Key_IsPressed(uint8_t row, uint8_t col) { if (row >= KEY_MATRIX_ROW || col >= KEY_MATRIX_COL) { return 0; } return (key_raw[col] & (uint16_t)(0x0001 << row)) == 0; }这段代码有几个细节我解释一下。GPIO_SetBits先把所有列置高,再把当前列拉低,这样能避免上一列的电平余留造成误判。delay_ms(2)的本意不是普通按键消抖,而是给电平一个稳定的建立时间,因为推挽输出和内部上拉之间还有二极管、线缆的RC延迟。按键的机械抖动消抖我放在主循环里做,用“连续两次扫描结果一致才认为按键有效”的方式,比单纯延时长轮询稳得多。
3.3 组合按键识别与消抖处理
Key_Scan函数每次执行会更新一个4x4的按键状态矩阵。判断某个按键是否按下,你就调用Key_IsPressed(row, col)。如果要判断多个组合键,直接分别调用这个函数就行,因为矩阵状态本身已经保存了所有行的电平结果,互不干扰。
机械按键的抖动是另一个需要处理的问题。矩阵键盘不像编码器,按下瞬间会有几毫秒到十几毫秒的机械抖动,电平会高低快速跳变。我常用的做法是设置一个20ms的扫描周期,主循环里每隔20ms采集一次矩阵状态,同时维护两个状态变量,一个是上一次的键值,一个是当前键值,只有“上一次为释放、当前为按下”的状态跳变才认为是有效按键事件。
这样做有两个好处:一是过滤了机械抖动,二是天然支持了“按住状态持续不重复触发”的逻辑,对菜单切换、音量调节这种需求非常友好。如果你需要长按连发功能,可以在此基础上加一个计时器,判断按键在按下状态持续了多久。
下面是一个在主循环里使用的示例片段:
uint8_t last_state[KEY_MATRIX_ROW][KEY_MATRIX_COL] = {0}; uint8_t cur_state[KEY_MATRIX_ROW][KEY_MATRIX_COL] = {0}; void ProcessKeyTask(void) { uint8_t r, c; // 每20ms调用一次,实际延时放在主循环里控制 Key_Scan(); for (r = 0; r < KEY_MATRIX_ROW; r++) { for (c = 0; c < KEY_MATRIX_COL; c++) { cur_state[r][c] = Key_IsPressed(r, c); if (cur_state[r][c] == 1 && last_state[r][c] == 0) { // 在这里处理按键按下事件 } last_state[r][c] = cur_state[r][c]; } } }注意,这个任务函数不能在一个while循环里空转延时,否则会阻塞其他模块。建议挂在RTOS任务里,或者用定时器中断里置标志位,主循环检查到标志位再去扫描。
3.4 代码回读与验证方法
代码写好后,建议先用串口把扫描结果直接打印出来,验证硬件接线和二极管方向是否正确。在每次Key_Scan后,把key_raw数组里的四个值用printf发出去,比如以“C0:0F C1:0F C2:0F C3:0F”的格式。0F表示这一列没有任何按键按下,如果某一位变成0,那一位对应的行按键就是按下状态。
单键逐个按下,确保每个按键都能被识别;然后依次测试同行双键、同列双键、三角组合。如果有鬼键出现,优先检查二极管方向是不是接反了,其次检查行线和列线是否短接。不要一上来就怀疑代码,矩阵键盘出问题八成在硬件。
另外,如果你手头没有串口调试工具,也可以用板载LED做指示:每识别到一个按键事件,就翻转一下LED状态。这个方法在野外调试、没有电脑的场合非常实用。我那次在地铁上调试键盘,就是用最小系统板上的LED干完了整个验证流程。
4. 实测对比:二极管隔离前后,冲突概率差多少
4.1 测试环境与测试用例
我把测试分成两组:一组是去掉二极管,直接行线上拉到GPIO;另一组是加上4个1N4148二极管,行线通过二极管连GPIO。其他条件完全一样,都是同一个4x4矩阵键盘、同一块STM32F103C8T6、同样的代码。
测试用例我列了一个表格:
| 测试场景 | 按键操作 | 期望结果 |
|---|---|---|
| 单键 | 按下(ROW0, COL0) | 只识别该键 |
| 同行双键 | 按下(ROW0, COL0)、(ROW0, COL1) | 识别两个键,无多余键 |
| 同列双键 | 按下(ROW0, COL0)、(ROW1, COL0) | 识别两个键,无多余键 |
| 三角组合 | 按下(ROW0, COL0)、(ROW0, COL1)、(ROW1, COL0) | 识别三个键,(ROW1, COL1)不触发 |
| 对角双键 | 按下(ROW0, COL0)、(ROW1, COL1) | 识别两个键,无多余键 |
每个测试重复了100次,记录误判出现次数。
4.2 实测结果对比
不带二极管的组,单键、同行双键、同列双键都正常,100次没有误判。但三角组合出现了大概76次鬼键误判,也就是(ROW1, COL1)被错误识别为按下。对角双键这一组稍微好一点,也偶尔会出现(ROW0, COL1)或(ROW1, COL0)误触发,但不稳定。
加上二极管之后,所有测试场景100次全部通过,包括三角组合和对角双键,没有任何一个鬼键出现。即使我用手指同时按住四个角,系统也只识别实际按下的四个键,不会多出第五个。这里没有统计上的模糊地带,二极管隔离的效果是非常明确的。
有一个数据值得关注:加二极管后,按键按下时GPIO引脚读到的低电平不再是0V,而是大约0.55V到0.7V,这是二极管的正向压降。虽然比0V高,但远低于STM32的低电平阈值0.99V,所以逻辑判定完全不受影响。如果你的板子供电是5V,这个压降对3.3V逻辑芯片也没有问题,因为判定阈值是按比例缩放的。
4.3 实测中发现的三个隐蔽问题
测试过程也不是一帆风顺。第一个坑是二极管焊反一行,导致那一行按键全部失灵。当时我以为是STM32引脚坏了,换了一块板子重焊才发现方向问题。
第二个坑是二极管选型问题。一开始我手边只有1N5819肖特基二极管,压降确实低,但反向漏电流比1N4148大一些。在潮湿环境下,偶尔出现极微弱的行线电位漂移,后来换回1N4148就消失了。所以不是所有二极管都能乱用,低速扫描场景1N4148是标准答案。
第三个坑是大电流LED和键盘共用行线。如果矩阵按键正好和LED灯珠共用IO,二极管会影响LED亮度或者造成LED微亮。这个场景不常见,但如果你的板子上有按键背光,要单独考虑背光电路的连接方式,不能简单把二极管直接串进行线里。
5. 避坑指南与经验总结
5.1 二极管选型参数怎么看
1N4148是矩阵键盘隔离最常用的型号,开关速度高,反向恢复时间大约4ns,漏电流很小,压降约0.6V,封装是DO-35,直插和贴片版本都有,单价几分钱,完全够用。
如果系统供电特别紧张,想进一步降低压降,可以用肖特基二极管如BAT54S,压降只有0.3V左右,但要注意它的反向漏电流比1N4148大,潮湿环境或者高温下可能不够稳。我的建议是:除非你有明确的低功耗或低压降需求,否则老老实实用1N4148。
另外,有些同学会问,能不能用三极管或者MOS管替代?理论上可以,但那是另一套开关逻辑,代价是电路复杂度暴涨。二极管在这里扮演的是单向阀,是最简单可靠的方案,没必要为了显得高级把简单问题复杂化。
5.2 排查“按键失灵”的完整套路
如果你把代码烧进去后发现某些按键读不到,先按这个顺序排查:
先看二极管方向。用万用表二极管档量一下行线和GPIO引脚之间的压降,正常是0.5-0.7V,如果方向反了就显示开路。这是最高频的故障原因,我自己就栽过两次。
再看接线顺序。矩阵键盘行线和列线经常在杜邦线连接时弄混,比如行线接到了列线口,或者按键模块的公共端接错了地方。把按键矩阵的原理图拿出来,一个一个核对。
然后检查上拉配置。代码里行线是GPIO_Mode_IPU,也就是内部上拉。如果你换成了外部上拉电阻,要注意阻值不要太小,否则按键导通时功耗偏大,同时和二极管压降叠加后可能让低电平临界。实测10k到100k的上拉都能工作,但20k到50k最稳。
最后才考虑代码问题。先用串口打印key_raw原始值,确认每次扫描到底读到了什么。如果读到的行值总是0xFF(表示所有行都是高),那问题基本在硬件;如果某个值一直在抖动,再排查延时和消抖逻辑。
5.3 哪些场合可以不加二极管
我不是二极管隔离法的无脑拥护者。如果你的项目只处理单键操作,比如一个简单的四键遥控器,一次只允许用户按一个键,那不加二极管完全够用,硬件成本也更低。很多学习板、评估板的矩阵键盘都没有二极管,做实验完全没问题。
但有两种情况我强烈建议至少做行级隔离:一种是用在需要频繁组合按键的产品上,比如仪器面板、收银键盘、游戏输入设备;另一种是产品要过无线电骚扰或者静电测试,二极管不仅解决串扰,还能在一定程度上抑制反向电流给MCU带来的冲击。
从成本角度看,4个1N4148大概不到一毛钱,PCB上多占四个位,换来的是不用在代码里写各种复杂的“按键状态仲裁算法”,也不用担心用户按出奇怪组合导致系统卡在某个错误状态。这笔账怎么算都划算。
最后分享一个小技巧:如果你做的是批量产品,二极管封装尽量选SOD-123这样有明确极性丝印的贴片封装,方便SMT贴片和目检。DO-35直插虽然看起来好焊接,但在密集布局时很容易挤到一起,焊反的概率也会变高。我在实际项目中踩过几次坑之后,现在已经默认所有矩阵键盘图纸都带行级二极管,这个习惯帮我在后续好几个项目里省下了大量排查时间。希望这篇记录也能帮你少走几段弯路。