ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32矩阵按键入门:从行列扫描原理到状态机消抖实战

STM32矩阵按键入门:从行列扫描原理到状态机消抖实战 简介基于STM32F103的44矩阵按键工程示例面向嵌入式初学者、STM32应用开发人员及电子竞赛备赛者解决单芯片上仅用8个GPIO引脚实现16键紧凑阵列检测的问题。整个方案覆盖按键扫描、机械去抖、多按键冲突处理与中断响应等核心知识点适合作为基础驱动模块直接嵌入仪器仪表、密码面板等场景。压缩包共80个文件以C源文件、H头文件、启动汇编文件、Keil工程配置为主同时包含可直接烧录的HEX固件和J-Link调试相关文件整体仅325KB体积小巧目录按CMSIS、固件库、用户应用与输出模块划分结构清晰。已有4102人学习下载具有较高参考价值。工程采用标准外设库开发不仅实现行列分时扫描还加入语音播报、SysTick延时和USART串口辅助调试等联动内容便于理解GPIO工作模式、RCC时钟配置和嵌入式前后台任务组织。通过阅读和编译该工程能够快速掌握44矩阵按键从硬件电路配置到软件状态机的完整流程为课程设计、毕业设计或实际项目提供可直接复用的参考实现。 做嵌入式项目绕不开按键。不管是大屏交互设备还是工业控制面板按键始终是最直接、最可靠的输入方式之一。而一旦按键数量超过8个再用独立IO逐个接板子就成了蜘蛛网。这时候4x4矩阵按键就派上用场了。这篇文章就以STM32F103搭配4x4矩阵按键为例把从原理到代码再到调试验证的全过程捋一遍。项目不大但足够经典适合刚入门、想在按键交互上彻底搞懂行列扫描的人也适合被按键误触发、连击问题折磨过的老手。1. 为什么是矩阵按键省IO才是硬道理先算一笔账。4x4矩阵按键一共有16个按键如果用独立按键方案每个按键占用一个IO16个按键需要16个IO。用矩阵方案4条行线加4条列线一共8个IO直接省了一半。STM32F103C8T6这种小封装芯片总共就37个IO省出8个IO意味着你可以多接一个OLED、多挂两个传感器甚至在板子尺寸不变的情况下把功能翻一倍。对成本敏感的产品来说这是实打实的收益。矩阵按键的原理其实很简单16个按键被安排在4行4列的交叉点上行线和列线分别接到单片机的两组IO。扫描的时候逐行把行线拉低其余行保持高电平然后读取列线的电平状态。如果某一行被拉低时某一列也变成了低电平那就说明这个交叉点上的按键被按下。整个过程就像在一个4x4的座位表里按行点名每点一行就问一次“这一列有没有人举手”哪一行哪一列同时有人举手位置就锁定了。注意矩阵按键省的是IO不是省按键数量。按键越多矩阵优势越明显如果只有三五个按键老老实实用独立按键反而更省事代码也更简单。选型的时候还要区分场景。矩阵按键适合“每次只按一个键、按下后输出确定电平”的场合比如菜单选择、数字输入、模式切换。如果你要做多点触控或者需要检测按压力度矩阵按键就不是这个路子了得考虑ADC按键或者专用触摸方案。另外4x3、4x5矩阵在代码上和4x4完全通用只需要改宏定义的引脚数量和按键编号映射表后面我会给出具体改法。我见过不少人一上来就想把16个按键做成“组合键”玩法在矩阵上硬刚多键同按最后被串键问题折腾到怀疑人生这个坑在第4部分专门讲。2. 硬件设计引脚分配和那些容易踩的坑2.1 引脚分配要躲开JTAG做STM32F103的硬件设计第一个要记住的是PB3、PB4、PA15这三个引脚在芯片复位后默认是JTAG调试功能不是普通IO。如果你在这三个引脚上接了矩阵按键程序跑起来之后GPIO配置根本不生效按键读到的电平永远是乱的。我早期做一块板子把列线接到了PB4整整查了两天才发现是这么回事。解决办法是在初始化GPIO之前加一行代码把JTAG关闭、保留SWD调试GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);这样PB3和PB4就能当普通IO用了。SWD调试接口不受影响下载程序、在线调试都正常。如果你用的是PA13、PA14作为SWD调试口那这两个引脚就别动了留给调试器用。我的习惯是行线用PB0~PB3列线用PB4~PB7这样全部避开PA的调试引脚也方便用排线引出。2.2 上拉电阻内部上拉不是不能用但外部10k更省心列线作为输入必须确保在没有按键按下时读到稳定的高电平。STM32F103内部有上拉电阻配置成上拉输入模式就能用省掉外部电阻。但内部上拉的阻值在30k~50k之间比较弱如果按键的连接线超过10厘米、或者板子附近有电机、继电器这类干扰源弱上拉很容易被拉出误电平。我的做法是开发调试时用内部上拉减少飞线做正式板子时在每一列线上加一个10k的外部上拉电阻。这样抗干扰能力明显好一截。如果你有示波器可以对比一下内部上拉和外部上拉在按下瞬间的波形外部上拉的下冲更干脆回弹也更干净这直接关系到软件消抖的难度。还有一个容易被忽略的问题STM32F103的普通IO不是5V容忍的只有数据手册里标注FT的引脚才能直接接5V。如果你按键的另一端接的是5V电源比如和5V的单片机系统共用按键接到非FT引脚上等着冒烟吧。具体哪些引脚是FT不同封装之间有差异不要凭感觉猜做板子之前对着数据手册逐个查或者干脆用3.3V作为按键的参考电平最稳妥。2.3 最小系统和时钟按键扫描对主频不敏感关于时钟热词里有人在问“如何让STM32F103工作在内部晶振模式下”。如果你只是想跑矩阵按键扫描内部8MHz RC振荡器完全够用按键扫描对时钟精度几乎没要求。但要注意一个坑标准外设库的SystemInit()函数默认会等待外部高速晶振HSE就绪如果你的最小系统板上没焊外部晶振程序会卡死在SystemInit()里现象就是LED不亮、按键没反应连仿真器都连不上。解决办法是在system_stm32f10x.c里把时钟源改成HSI或者直接用带8MHz晶振的最小系统板。大部分成品最小系统板都带晶振省心。如果你确定要用内部晶振记得把SystemInit里等待HSE的逻辑注释掉改成直接使用HSI。按键扫描对主频不敏感用内部晶振完全不会影响按键的响应速度这一点可以放心。3. 软件实现扫描算法、消抖防连击3.1 GPIO初始化和行列扫描核心逻辑软件是整个项目的重头戏。先初始化GPIO行线配置为推挽输出列线配置为上拉输入#define ROW_PORT GPIOB #define COL_PORT GPIOB #define ROW0_PIN GPIO_Pin_0 #define ROW1_PIN GPIO_Pin_1 #define ROW2_PIN GPIO_Pin_2 #define ROW3_PIN GPIO_Pin_3 #define COL0_PIN GPIO_Pin_4 #define COL1_PIN GPIO_Pin_5 #define COL2_PIN GPIO_Pin_6 #define COL3_PIN GPIO_Pin_7 void Key_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); // 行线推挽输出 GPIO_InitStructure.GPIO_Pin ROW0_PIN | ROW1_PIN | ROW2_PIN | ROW3_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(ROW_PORT, GPIO_InitStructure); // 列线上拉输入 GPIO_InitStructure.GPIO_Pin COL0_PIN | COL1_PIN | COL2_PIN | COL3_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; GPIO_Init(COL_PORT, GPIO_InitStructure); // 如果用了PB3/PB4先关闭JTAG // GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE); }基础扫描函数的核心逻辑是先把4条行线全部输出高电平再把当前扫描的行拉低然后读取4条列线。读到低电平的列和当前行组合起来就是一个按键编号static uint8_t Key_Scan(void) { uint8_t row, col; uint16_t row_pins[4] {ROW0_PIN, ROW1_PIN, ROW2_PIN, ROW3_PIN}; uint16_t col_pins[4] {COL0_PIN, COL1_PIN, COL2_PIN, COL3_PIN}; for (row 0; row 4; row) { // 行线全部拉高再把当前行拉低 GPIO_SetBits(ROW_PORT, ROW0_PIN | ROW1_PIN | ROW2_PIN | ROW3_PIN); GPIO_ResetBits(ROW_PORT, row_pins[row]); DelayUs(5); // 等电平稳定 for (col 0; col 4; col) { if (GPIO_ReadInputDataBit(COL_PORT, col_pins[col]) RESET) { return row * 4 col 1; // 返回1~16 } } } return 0; // 无按键 }这个函数每次调用会完整扫描一遍16个按键返回0表示没有按键返回1~16表示对应编号的按键被按下。5微秒的延时是留给GPIO电平翻转稳定的实测去掉也能工作但留着更保险特别是在高速主频下。拿到1~16的编号之后你可以自己写一个映射表把它转成实际功能。比如1~10对应数字键0~911对应确认12对应取消13~16对应上下左右方向键。这样逻辑清晰后面改功能也方便。很多项目就是靠这一张映射表把矩阵按键复用出菜单导航、密码输入多种玩法。3.2 用状态机消抖别再用延时硬扛新手最容易犯的错误是扫描到按键后用delay延时20ms再扫一次这叫阻塞式消抖。在裸机小项目里勉强能用但主循环里一旦还有其他任务比如显示刷新、串口通信20ms的阻塞会把整个系统卡得一跳一跳的按键还容易连击。在有RTOS的项目里在任务里延时20ms更是浪费CPU一个低优先级的按键任务延时阻塞高优先级的任务调度也跟着受影响。推荐的做法是定时器节拍扫描加状态机消抖。让SysTick或者一个定时器每10ms调用一次按键任务用状态机判断按键的按下、确认、释放过程。10ms扫描一次两次确认才能触发一次事件天然消抖状态机里加入释放判断就不会连击。typedef struct { uint8_t state; uint8_t key_id; } KeyFSM; KeyFSM key_fsm {0, 0}; void Key_Task_10ms(void) { uint8_t key Key_Scan(); switch (key_fsm.state) { case 0: // 空闲状态 if (key ! 0) { key_fsm.key_id key; // 记录第一次扫描到的按键 key_fsm.state 1; // 进入确认状态 } break; case 1: // 确认状态 if (key key_fsm.key_id) { Key_Event_Report(key_fsm.key_id); // 两次一致确认为有效按键 key_fsm.state 2; // 进入等待释放状态 } else { key_fsm.state 0; // 两次不一致认为是抖动复位 } break; case 2: // 等待释放状态 if (key 0) { key_fsm.state 0; // 按键释放回到空闲 } break; default: key_fsm.state 0; break; } }状态机的核心思想第一次扫描到按键不立即响应等10ms后再扫描一次如果两次结果一致才认为是有效按键这比固定延时20ms更科学响应速度和抗抖动能力都更好。“等待释放”状态确保一次按下只会触发一次事件松开之前不会反复上报连击问题彻底解决。10ms这个周期是我反复试过的折中值太短消抖不干净太长按键手感发肉。如果按键本身质量一般、弹片回弹有毛刺把周期调到15ms也问题不大。3.3 扩展中断唤醒与低功耗设计如果你做的是电池供电设备按键就是天然唤醒源。但这里有个矛盾矩阵按键的扫描需要CPU参与停机模式下CPU停跑了怎么用按键唤醒我的做法是硬件上做一点改动每根列线串一个二极管二极管的阴极接列线阳极并到一起再连接到PA0WKUP引脚。PA0配置为外部中断输入并启用内部上拉。任意按键按下时所在列线被拉低通过二极管把PA0也拉低PA0检测到下降沿就能把芯片从停机模式STOP唤醒。没按键时二极管不导通静态功耗为零。唤醒之后先恢复时钟再重新初始化GPIO然后进入正常的按键扫描流程。提示如果你不想加二极管也可以把4条列线用或门逻辑接到同一个外部中断引脚但要注意列线上有10k上拉按下时会被拉低释放后恢复高电平这个下降沿正好适合作为唤醒信号。实际测试中按键按下到唤醒的延迟在微秒级手感上完全感觉不到。还要注意停机模式下外部中断唤醒之后外设时钟状态和唤醒前不完全一样。我习惯在唤醒代码里重新调用一遍Key_GPIO_Init()把GPIO重新初始化一遍避免个别引脚模式异常。如果你用了外部上拉和内部上拉混搭这一步更重要否则按键扫描结果可能时好时坏。4. 实际调试中的坑与排查思路4.1 按键没反应先查配置再查硬件按下按键完全没反应是最常见的问题。排查顺序我建议第一步检查GPIO模式。行线必须是推挽输出列线必须是上拉输入。很多新手把列线配置成了开漏输出读回来的电平永远是0按键自然没反应。第二步用万用表测列线电压没按键时应该是高电平3.3V按下时应该被拉低。如果电压一直是0说明上拉没生效如果一直是高说明行线没有拉低。第三步检查引脚是不是被JTAG占用了前面说的PB3、PB4就是经典坑。第四步确认代码有没有卡在SystemInit里无外部晶振的板子非常容易在这翻车。4.2 按键乱跳、误触发如果按键按下时能触发但经常出现错误的按键编号或者没按也触发一般是两个原因一是消抖不够状态机扫描周期太短建议10ms扫描一次不要低于5ms二是供电问题STM32F103的电源引脚附近必须加100nF去耦电容电源纹波大会直接影响GPIO的输入阈值判断。还有一个容易被忽视的坑行列线如果走线过长或平行走线按键之间的信号会互相串扰解决办法是把行列线分开走或者加一个0.1uF的滤波电容在列线上。我遇到过一台样机按键矩阵和电源线绑在同一个线束里一按按键屏幕亮度就闪把线束分开就全好了。4.3 需要多键同时按吗4x4矩阵按键在硬件上不支持任意的多键组合。举个例子同时按下行1列1和行2列2两个键扫描时可能会误判出行1列2或行2列1也被按下这就是“串键”现象。如果你确实需要支持组合键两个方案一是把矩阵按键改成每个按键独立接IO二是给按键串联二极管。串二极管后线路复杂不少一般不建议在4x4场景里做多键组合产品设计上直接限制“同时只允许按一个键”最省事。4.4 5V电平与按键接法如果你手里的矩阵按键模块是从5V系统上拆下来的或者按键矩阵的工作电压是5V直接接到STM32F103上要小心。前面说了只有FT引脚能承受5V而不是所有IO都能。做板子之前对着数据手册逐个查别靠猜。最稳妥的方案是接入前用电阻分压或者三极管做电平转换虽然多两个元件但能保证芯片不烧。我在调试时习惯用万用表先量一下按键矩阵的供电电压确认是3.3V还是5V再接线这个习惯帮我避免了好几次烧引脚的事故。4.5 关于内部晶振的“假死”问题最后再说一遍内部晶振的坑因为它真的太常见了。很多淘宝买的最小系统板没有焊外部晶振只有一颗8MHz晶振的位置空着。如果你拿标准库模板直接编译下载程序会在SystemInit里等HSE就绪永远等不到看起来就像单片机变砖了。验证方法按住复位键连接仿真器如果能连上说明芯片没坏是程序初始化卡住了。解决方法是改用HSI内部时钟或者焊上8MHz晶振。一旦时钟源搞定按键扫描本身基本不出问题。我把这些坑整理成一张速查表方便排查现象可能原因排查方向按键完全无反应GPIO模式配置错误行线推挽输出列线上拉输入按键无反应引脚被JTAG占用检查PB3、PB4、PA15关闭JTAG按键编号错乱行列线接反核对原理图行对行、列对列未按也触发消抖不够或电源纹波大状态机扫描10ms增强滤波多键同按时乱报串键问题加二极管或限制单键按下低功耗无法唤醒停机模式无中断源列线经二极管或门接入EXTI程序跑不起来无外部晶振SystemInit卡死改用HSI内部时钟或补焊晶振矩阵按键这个项目我在通信设备和仪表类产品上复用至少四五次每次就是改改引脚宏定义和按键编号表核心的扫描函数和状态机基本没动过。写这篇文章时回想了一下几个关键决策——外部上拉、状态机消抖、时钟源确认——都是在实际产品上踩过坑之后才固化成习惯的。如果你正在做类似的项目我的建议是先把基础扫描跑通再逐步加上状态机和低功耗别一上来就追求完整方案。硬件上多花两毛钱加个外部上拉比软件上多调两天省心得多。本文还有配套的精品资源点击获取
返回列表