拓冰建站拓冰建站
首页 / 资讯中心 / 正文

STM32矩阵键盘串口输入实战:扫描、消抖与printf重定向

简介面向STM32入门与嵌入式开发初学者这是一份基于STM32F103的矩阵键盘串口输入实践资源演示如何通过GPIO行列扫描识别按键并经USART将键值实时发送至上位机。资源包共300个文件约7.43MB以标准外设库的c/h源码、o/crf等编译中间文件为主同时包含uvprojx工程文件、hex烧录文件及axf调试文件可直接用Keil打开工程并烧录运行。目前已有678人学习下载。内容覆盖GPIO输入输出配置、矩阵键盘扫描算法、按键去抖处理以及串口初始化与数据发送等关键环节并提供了完整的工程结构和必要的库文件可帮助读者快速理解硬件输入与串口通信的结合方式适合作为课程设计或自学实验的参考模板。 调试一块带矩阵键盘的STM32开发板时最直观的验证方式就是按下一个按键串口调试助手里立刻跳出对应的键值。很多朋友第一次做这个需求要么卡在矩阵键盘的扫描逻辑上绕不出来要么串口发不出数据要么发出来全是乱码。我前前后后做过几个带键盘输入的采集控制设备把STM32矩阵键盘串口输入这条链路完整跑通之后发现核心其实就三件事矩阵扫描、按键消抖、串口发送。这篇文章就把这三件事拆开讲透。我会从矩阵键盘为什么要扫描讲起给出可以直接抄的标准库例程再把消抖、printf重定向、上位机联调这些环节逐个填坑最后附上我实战中踩过的坑位清单。适合手里有F103开发板、正准备做键盘输入类小项目或者做毕设想快速打通输入通道的同学参考。整套方案做完你拿到的不只是一个能用的按键读取程序而是一套可以复用到其他项目里的输入输出骨架。1. 矩阵键盘为什么非扫不可从引脚账说起1.1 独立按键方案的引脚开销很多初学者第一个想到的方案是像独立按键一样每个按键接一个GPIO按下为低电平。对两三个按键来说这完全没毛病但按键数量一上来直接IO的方案就变得很难看。拿4x4矩阵键盘举例16个按键如果全部用独立IO要占16个引脚而用矩阵接法只需要4根行线加4根列线共8个引脚直接省了一半。换成8x8矩阵64个按键用独立接法需要64个引脚矩阵接法只要16个引脚。芯片引脚资源越紧张矩阵的优势越明显。矩阵键盘之所以叫矩阵是因为按键被安排在行线和列线的交叉点上。4x4矩阵就是4根行线ROW0~ROW3和4根列线COL0~COL3交叉形成16个交叉点每个交叉点放一个按键。按键没按下时行线和列线互不相通按下后二者短接这就是整个矩阵键盘工作的物理基础。1.2 行列扫描的基本原理交叉点上的按键按下后行线和列线短接了但MCU怎么知道是哪一个交叉点这就需要扫描。扫描的思路分两步先把列线全部设为带上拉的输入把行线设为输出然后一次只把其中一行拉低其余行保持高电平。此时去读列线的电平如果哪一列被拉低到0就说明按下的按键正好在当前这一行和那一列的交叉点上。判断逻辑非常简单读到0表示按键按下读到1表示没有按下。扫完第一行再扫第二行四行全部扫完就能确定唯一的按键位置。这里有个关键前提列线必须有一个明确的高电平来源硬件上通常用外部上拉电阻STM32内部也有上拉可以承担这个角色。如果用开漏输出加拉低的方式外部上拉电阻基本是必备的。1.3 逐行扫描法与电平反转法怎么选实际工程里还有另一种常见思路电平反转法也叫行列反转法。原理是先把行设为输出、列设为输入扫描一遍得到行号然后把角色互换列设为输出、行设为输入再扫一遍得到列号。两次结果组合成唯一坐标。这个方法在行列引脚数相等时很对称代码看起来也直观。不过我个人的习惯是优先用逐行扫描法。理由是它不要求行数和列数相等几行几列可以很灵活另外同一行上多个按键同时按下时逐行扫描的判读逻辑更可控后续要加多键优先级或者组合键策略也好扩展。反转法虽然简单但在多键同时按下的场景下容易产生奇怪的误判组合。搞产品、搞毕设我建议直接从逐行扫描入手。对比维度逐行扫描法电平反转法行数列数不相等时完全适配需要额外处理多键同时按下易扩展优先级逻辑可能误判代码直观程度中等较直观实际工程使用频率更高多见于简单示例2. 硬件接线与初始化决定成败的前30秒2.1 引脚分配和最容易被忽略的上拉问题以STM32F103C8T6为例我习惯把PB0~PB3作为行线PB4~PB7作为列线。为什么不优先选PA口因为PA口经常被USART1、SPI、I2C等功能占用调试串口通常就在PA9和PA10上。把矩阵键盘放在PB口能少很多引脚复用冲突这个选择在项目引脚比较满的时候能省下大量排查时间。上拉方式上STM32内部上拉可以直接用省电阻。但内部上拉的阻值通常在30~50kΩ阻值偏大如果行线或列线走线比较长、环境电磁干扰大容易出现误触发。我实测下来正常面包板或洞洞板上用内部上拉完全够用如果做产品、走线长度超过20厘米建议在列线上加10kΩ外部上拉到3.3V稳定度会有肉眼可见的提升。2.2 GPIO初始化代码里的关键细节初始化代码本身不复杂但有几个细节容易翻车。首先要记得开启GPIOB的时钟如果是标准库还要确认重映射相关配置。其次如果你把行线或列线安排在PB3、PB4上必须在初始化最前面调用GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE); 同时开启AFIO时钟把JTAG功能关掉只保留SWD调试。否则PB3、PB4一直被调试功能占用配置成普通IO也不会生效。下面这段是标准库风格的矩阵键盘引脚初始化HAL库思路完全一样换成HAL_GPIO_Init即可RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE); GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE); GPIO_InitTypeDef GPIO_InitStructure; // 行线 PB0~PB3推挽输出默认输出高 GPIO_InitStructure.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); GPIO_SetBits(GPIOB, GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3); // 列线 PB4~PB7上拉输入 GPIO_InitStructure.GPIO_Pin GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; GPIO_Init(GPIOB, GPIO_InitStructure);2.3 硬件层面会提前埋雷的几个地方第一个常见的坑是按键没有做任何抖动抑制。机械按键按下和释放的瞬间触点会来回弹跳几毫秒到十几毫秒如果不消抖串口会一次按下冒出好几个键值。软件消抖是必须的但硬件上如果空间允许每个按键并联一个100nF瓷片电容能从源头压低抖动幅度。小批量验证时可以先不加电容靠软件消抖就够。第二个坑是接线用了乱糟糟的杜邦线。矩阵键盘行线和列线本身相互交叉线材之间会有寄生电容扫描频率高了容易串扰。建议扫描间隔不要小于10ms给信号一个稳定的时间窗口。第三个坑跟电源有关有的开发板上矩阵键盘和OLED、舵机共用一个3.3V稳压按下多个按键时电流变化造成电压跌落扫描结果就会不稳定。遇到这种问题先拿示波器或万用表量一下按键按下瞬间的3.3V波形。3. 扫描与消抖从能按到按得准3.1 完整扫描函数标准库示例与逐段拆解扫描函数是整篇的核心直接贴一版我验证过的标准库实现行线PB0~PB3列线PB4~PB7uint8_t MatrixKey_Scan(void) { uint8_t row 0, col 0; uint16_t rowPin[4] {GPIO_Pin_0, GPIO_Pin_1, GPIO_Pin_2, GPIO_Pin_3}; uint16_t colPin[4] {GPIO_Pin_4, GPIO_Pin_5, GPIO_Pin_6, GPIO_Pin_7}; GPIO_InitTypeDef GPIO_InitStructure; // 行线设为推挽输出并全部输出高电平 GPIO_InitStructure.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); GPIO_SetBits(GPIOB, GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3); // 列线设为上拉输入 GPIO_InitStructure.GPIO_Pin GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; GPIO_Init(GPIOB, GPIO_InitStructure); for (row 0; row 4; row) { GPIO_ResetBits(GPIOB, rowPin[row]); // 当前行拉低 for (col 0; col 4; col) { if (GPIO_ReadInputDataBit(GPIOB, colPin[col]) 0) { return row * 4 col; // 返回按键编号 0~15 } } GPIO_SetBits(GPIOB, rowPin[row]); // 扫完恢复当前行高电平 } return 0xFF; // 无按键按下 }这个函数的逻辑很直接每次都把全部行输出高然后逐行拉低某一行读所有列。只要某一列读到0就返回当前行列对应的编号。0~15对应4x4键盘的16个键位0xFF表示没有任何按键按下。两个细节需要特别讲一下第一每扫完一行要立刻把该行恢复为高电平否则下一轮拉低另一行时上一行还处于低电平会造成多行同时拉低读出来的结果必然是错的。第二这个函数以第一次读到的按键为准立即返回适合单键操作场景多个按键同时按下时会返回行号最小的那个键具体策略见后文。3.2 为什么不能只靠一个delay解决消抖很多教程的做法是检测到低电平后delay 20ms再确认一次代码简单也确实有效。但它在单片机里有一个隐藏成本delay期间CPU被完全占用。如果主循环里同时还有OLED刷新、舵机控制、串口发送这些任务一次20ms的阻塞可能造成显示闪烁或者控制卡顿这个体验做过的都懂。更麻烦的是delay消抖本质上是一种事后补救它默认按键在20ms内一定会稳定但不同按键的抖动时间差异很大。我遇到过按压后要40ms才稳定的按键固定延时根本不够。所以工程上我更推荐状态机消抖它把按键有没有真正按下交给连续采样去判断不阻塞主循环也能自适应抖动时长。状态机的代码量比delay方案多一些但对带屏、带电机、带通信的项目来说这个投入非常值得。3.3 状态机消抖的落地写法状态机消抖的核心思想每10ms采样一次扫描结果连续两次读到稳定低电平才确认按下连续两次读到稳定高电平才确认释放。这样即便按键抖动持续十几毫秒状态机最终只会输出一次有效的按键事件。具体落地时定义一个全局变量保存上一次扫描结果再用一个计数器统计连续相同状态的次数。采样流程可以放进定时器中断每10ms触发一次也可以在主循环里配合SysTick做非阻塞延时。状态划分就四个空闲、按下待确认、按下确认、释放待确认。只有当状态从按下待确认跳转到按下确认时才真正执行一次键值处理这样从机制上就杜绝了重复上报。主循环里不用一直轮询结构清爽逻辑也稳。唯一的成本是代码看起来比delay方案绕一点但用顺手之后你会觉得这才是正常该有的写法。4. UART串口输出让键值走出MCU4.1 串口初始化的最小配置矩阵键盘扫描到键值之后下一步就是通过串口把数据发出去。最常用的调试串口是USART1对应PA9作为TX、PA10作为RX。初始化分三步使能GPIOA和USART1时钟配置PA9为复用推挽输出、PA10为浮空输入或上拉输入然后配置USART1为115200波特率、8位数据位、1位停止位、无校验。对调试用途来说这个参数组合已经够了。波特率不是随便选的。115200在绝大多数USB转串口模块下都很稳但如果你用ST-Link自带的虚拟串口某些环境下115200偶尔会丢字节可以降到9600试试。波特率一旦设错上位机收到的一定是乱码。标准库的串口初始化配置如下GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); USART_InitTypeDef USART_InitStructure; USART_InitStructure.USART_BaudRate 115200; USART_InitStructure.USART_WordLength USART_WordLength_8b; USART_InitStructure.USART_StopBits USART_StopBits_1; USART_InitStructure.USART_Parity USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode USART_Mode_RX | USART_Mode_TX; USART_Init(USART1, USART_InitStructure); USART_Cmd(USART1, ENABLE);4.2 printf重定向与半主机模式的坑串口发送键值最省事的方式是重定向printf。但这里有一个坑几乎每个新手都会踩在Keil环境下如果不勾选Use MicroLIB也不处理半主机模式程序会在第一次调用printf时卡死或跑飞。原因是标准C库的printf默认走半主机模式而裸机工程里根本没有实现半主机对应的底层环境。解决办法有两条。最省事的一条在Keil的Options - Target - Code Generation里勾选Use MicroLIB然后重写fputc函数。MicroLIB是ARM专门为嵌入式裁剪的C库不依赖半主机。另一条更完整通过#pragma import(__use_no_semihosting)禁用半主机模式并自己补上_sys_exit等函数。对绝大多数项目第一条就够了。重写后的fputc核心逻辑非常简单就是等待发送数据寄存器为空再写入一个字节int fputc(int ch, FILE *f) { while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); USART_SendData(USART1, (uint8_t)ch); return ch; }4.3 上位机联调调试助手选择和乱码排查串口调试助手的选择不少。Windows下我常用XCOM和SSCOM界面简单支持定时发送和十六进制显示macOS可以用minicom或Serial。无论用哪款第一次连接前都要先确认设备管理器里能正常识别到USB转串口模块。如果设备管理器出现黄色感叹号大概率是CH340或FTDI驱动没装好去官方渠道下载对应驱动重新装一遍即可。联调时最容易遇到的三个问题按出现频率排序第一TX和RX接反了。STM32板子的TX要接模块的RXRX要接模块的TX很多人想当然地同名相连结果一路都没数据。第二波特率不一致板子里程序设的115200上位机却选了9600。第三板子和模块没有共地。USB转串口模块的GND必须和开发板的GND连在一起否则两边电平参考不一致数据一定是乱的。5. 进阶扩展与踩坑清单照着做能少加三天班5.1 定时器扫描与串口协议帧从demo到产品的距离如果这个矩阵键盘串口输入方案要继续往产品方向走有几个升级点值得做。第一把扫描从主循环挪到定时器中断里每10ms扫描一次主循环只负责消费键值。这样就算主循环里正在做耗时操作按键事件也不会丢。第二把按键状态和串口发送模块化按键事件通过全局标志或队列传给上层逻辑而不是直接在扫描函数里发串口。扫描归扫描业务归业务改起来才不打架。第三给串口数据加一个简单的协议帧。裸发一个数字键值虽然调试方便但缺点明显没有帧边界不知道一串数据什么时候开始、什么时候结束没有校验数据被干扰了也发现不了。最简单的做法是帧头用0xAA、帧尾用0x55中间放按键编号比如发送AA 03 55表示按键3被按下。接收方只要找到以AA开头、55结尾的帧就认为是一个有效按键事件。如果以后想让上位机反向下发配置或命令这个帧结构同样能用只是把数据方向换一下。要做到更稳的收发还可以参考基于DMA加空闲中断的串口接收方案靠空闲中断判断一帧数据是否结束能省掉逐字节解析的麻烦。5.2 实战中真实遇到过的坑位清单我做过的矩阵键盘项目版本不少踩坑记录攒了一堆这里直接以清单形式分享出来省得大家再走一遍。现象最可能原因排查顺序串口乱码波特率不一致 / 晶振频率配置错误先对波特率再查RCC时钟配置完全收不到数据TX/RX接反 / 未共地先检查接线再查供电按一次出现多个键值缺少软件消抖加状态机消抖或20ms延时按键偶尔失灵上拉电阻偏弱 / 走线干扰加外部10k上拉理清布线编译找不到芯片型号MDK芯片包没安装安装对应型号的STM32器件支持包ST-Link连接失败接线松 / 固件旧 / 芯片调试保护从硬件到软件逐项排查这里单独说一下ST-Link连接失败的问题。在MDK或STM32CubeProgrammer里看到no stm32 target found之类的报错先检查SWDIO、SWCLK、GND三根线是否接对目标板是否独立供电。确认硬件没问题后再看ST-Link固件是不是太老老固件对部分新芯片可能连不上。另外部分新出厂的芯片默认使能了调试认证保护如果芯片被设置过保护等级老版本工具会被直接拒之门外这时候需要升级ST-Link固件并用新版软件解除保护。顺带提醒一句如果你的Keil MDK是从C51环境转过来的新建工程前记得先装对应STM32芯片包否则芯片型号列表里什么都找不到。还有一个和晶振相关的经典问题STM32F103的外部晶振通常是8MHz如果在RCC配置里误填成25MHz整个系统主频都会不对串口波特率自然也是一团糟。这种乱码不是上位机造成的而是单片机内部时钟源和期望值不一致。排查时可以观察串口助手收到的字符有规律但整体错误多半是波特率倍率问题完全无规律的乱码则要回头检查电压、接线和时钟配置。最后再分享一个我做这类输入设备时的小习惯把键值和功能之间的映射关系单独做成一张表放在程序开头而不是把判断逻辑写死在按键处理里。这样改键盘布局、换一块排列方式不同的矩阵键盘只需要更新映射表扫描和串口部分的代码一行都不用动。这个习惯帮我省了不少事尤其是在项目进入调试后期、频繁调整按键定义的时候价值会体现得非常明显。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门