首页 / 资讯中心 / 文章详情

STM32矩阵键盘计算器:扫描、消抖与状态机完整落地

STM32矩阵键盘计算器:扫描、消抖与状态机完整落地 ★ FEATURED ARTICLE
简介这是一份基于意法半导体Cortex-M3内核微控制器F103C8T6的矩阵键盘计算器完整工程包面向嵌入式初学者、课程设计及电子竞赛备赛者生动演示了如何借助通用输入输出口、外部中断和定时器完成矩阵键盘的行列扫描与按键识别并利用有机发光二极管屏幕实时显示输入数字和运算结果。压缩包内共收纳一百九十二个文件容量约七点三六兆字节主体为三十三份C语言源码与三十五份头文件还包含编译器生成的中间目标文件、链接映射文件、工程文件、可执行文件以及启动文件可以直接在集成开发环境中打开、编译和烧录运行文件目录按标准外设库组织便于分模块研读。工程覆盖按键消抖、键位映射、四则运算、除零保护等完整逻辑同时保留了编译产物有助于理解从源码到固化芯片的整个流程。已有六百四十人学习下载适合以此为基础扩展更多小型交互式嵌入式应用。1. 一块入门板做出计算器stm32 矩阵键盘的完整落地路径我在做一个课程设计时拿到一块 STM32F103C8T6 最小系统板只有一个四位数码管和一块 4x4 矩阵键盘模块任务是想办法让它变成一个能加减乘除的桌面计算器。stm32 矩阵键盘这个组合看起来简单真正把按键扫描、消抖、数码管刷新和运算状态机全部串起来还是有几个容易翻车的点。这篇文章从硬件选型、矩阵扫描原理、代码实现到排查技巧完整拆开适合刚接触 STM32 的开发者也适合拿来改装成密码锁、菜单选择器等交互设备。照着做一块十几块的最小系统板加两个模块就能完成不需要额外逻辑芯片。2. 矩阵扫描原理与硬件选型8 个 IO 控制 16 个按键的代价2.1 为什么 4x4 矩阵而不是 16 个独立按键先算一笔引脚账。独立按键每个占用 1 个 GPIO16 个按键就要 16 个引脚再加上数码管段选和位选STM32F103C8T6 虽然是 LQFP48 封装真正可用的 GPIO 也就 37 个看起来似乎够用但实际工程里往往还要接串口、蜂鸣器、状态 LED引脚很快就会吃紧。矩阵键盘把按键排列在 4 行 x 4 列的交叉点上8 个 GPIO 就能读取 16 个按键状态。节省了一半引脚代价是按键检测不再是「读某一个引脚」而变成了「分时扫描 行列交叉判定」。这个变化直接影响后续代码结构不能把每个按键当独立输入处理而是每次只驱动一行然后读取四列的状态反过来再对每一列做同样操作由行列组合反推出键值。矩阵键盘适合按键数量多、引脚资源紧张的场合。如果只是 2 到 3 个按键独立按键更简单可靠没必要引入扫描时序。如果超过 16 个按键同样引脚数可以用 4x5、5x6 矩阵继续扩展但扫描周期会变长对消抖和响应速度的要求也相应提高。STM32F103C8T6 的主频是 72MHz即使扫描周期做到 10msCPU 占用率也极低这是它适合做矩阵键盘计算器的基础。2.2 扫描法与时序行拉低、列读电平的完整分析最常用的矩阵扫描是行列扫描法也叫逐行扫描法。先把 4 个行引脚设置为推挽输出并全部输出高电平再把 4 个列引脚设置为上拉输入。没有按键按下时列引脚读到的是高电平。扫描开始后第一次只把第 0 行拉低其余行保持高电平。如果某个按键正好跨在第 0 行和第 N 列之间第 N 列就会被这个按键拉低。读取四个列引脚的状态得到一个 4 位列值。接着恢复第 0 行为高拉低第 1 行再读一次列值。四轮扫描结束就拿到了一个 4x4 的状态矩阵其中电平为 0 的位置就是被按下的按键。这里有一个常见的错误直接把行引脚配置成开漏输出列引脚不做上拉想着「反正行会拉低列自然读到低电平」。实际电路里如果行引脚悬空列引脚又没有内部上拉读取结果就是漂浮不定会随机出现误触发。我一般把列引脚配置为GPIO_PULLUP用 STM32 内部上拉电阻保证默认电平稳定省掉外接排阻。从时序角度看一次完整扫描四行的时间取决于每行之间的延时。扫描周期太短按键抖动会造成多次触发太长用户按下快一点就感觉卡顿。按键的机械抖动一般是 5ms 到 10ms我通常在每一行拉低后延时 1ms 等待电平稳定完整一次扫描约 4ms再加上消抖判断整体扫描周期控制在 10ms 到 20ms人眼和手感都无感知。2.3 消抖方案对比延时、定时器状态机和外部中断按键消抖是矩阵键盘工程里绕不开的话题。机械按键在按下和释放的瞬间触点会产生 5ms 到 20ms 的抖动电平波形不是干净的一次跳变而是反复通断。如果代码不做处理一次按下可能被识别成好几次计算器输入 5 可能变成 55 或者 555。消抖方式实现思路优点缺点适用场景延时消抖检测到电平变化后延时 20ms 再确认代码简单直接阻塞 CPU无法同时处理显示刷新按键少、无实时任务定时器状态机每 1ms 扫描一次连续 N 次电平一致才确认非阻塞响应稳定代码量稍大计算器、键盘面板外部中断 定时器电平跳变触发中断再启动定时器确认响应最快功耗低多个按键需共享中断线结构复杂低功耗手持设备计算器这种设备既需要及时响应按键又要保证数码管稳定刷新如果按键消抖用阻塞式 20ms 延时那么按键期间数码管动态扫描就会停顿肉眼会看到亮度闪烁。我选择定时器状态机方式先配一个 1ms 的定时器中断作为系统时基按键扫描函数在时基里每 10 次执行一轮连续两轮扫描到相同键值才算有效。这样消抖逻辑与显示刷新互不干扰后续如果要加蜂鸣器提示音也不需要改动按键状态机的结构。3. GPIO 初始化与按键扫描代码从寄存器配置到键值返回3.1 引脚分配与 GPIO 初始化参数我的引脚分配考虑了三个原则避开调试串口和 JTAG 复用冲突、电源引脚附近少放数字信号、数码管段选集中在同一组端口方便操作。实际工程里的分配如下功能引脚模式说明矩阵键盘行 0-3PA0-PA3推挽输出扫描时逐行拉低矩阵键盘列 0-3PA4-PA7输入 上拉读取按键状态数码管段选 a-dpPC0-PC7推挽输出共阴数码管段码数码管位选 0-3PB0-PB3推挽输出四位动态扫描位选调试串口PA9-PA10复用推挽可选用于打印键值注意 PB3 和 PB4 在 STM32F103C8T6 上默认是 JTAG 引脚直接当普通 GPIO 用会失灵。如果位选必须用到这两个引脚需要先调用__HAL_AFIO_REMAP_SWJ_NOJTAG()禁用 JTAG只保留 SWD。这个坑在后面的避坑章节会展开说这里先记住初始化顺序是先重映射再配置 GPIO。GPIO 初始化用 HAL 库的标准流程先使能端口时钟再对每一个引脚设置模式。行引脚是推挽输出因为扫描时要主动拉低电平列引脚是输入模式并且开启内部上拉保证没有按键按下时读到高电平。代码里这一段值得认真看参数void GPIO_Config(void) { __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_GPIOB_CLK_ENABLE(); __HAL_RCC_GPIOC_CLK_ENABLE(); GPIO_InitTypeDef GPIO_InitStruct {0}; // 行引脚 PA0-PA3 推挽输出最大翻转速度 2MHz GPIO_InitStruct.Pin GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); // 列引脚 PA4-PA7 上拉输入 GPIO_InitStruct.Pin GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7; GPIO_InitStruct.Mode GPIO_MODE_INPUT; GPIO_InitStruct.Pull GPIO_PULLUP; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); // 数码管段选 PC0-PC7 推挽输出 GPIO_InitStruct.Pin GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOC, GPIO_InitStruct); // 数码管位选 PB0-PB3 推挽输出 GPIO_InitStruct.Pin GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3; HAL_GPIO_Init(GPIOB, GPIO_InitStruct); }这里有个参数选择容易被忽略GPIO_SPEED_FREQ_LOW对于数码管和矩阵键盘这种低频信号完全够用还能减少电磁干扰。之前见过有人把所有引脚都配成GPIO_SPEED_FREQ_HIGH按键信号线和数码管段选线相互耦合扫描时偶尔出现串键换成低速模式后问题消失。不是高速不好而是这种场景没必要。还有一个细节是段选端口我选择 PC0-PC7 而不是分散在多个端口这样写段码时可以直接用一个 8 位无符号整数驱动整个 GPIO 端口代码更紧凑。如果用 HAL_GPIO_WritePin 逐位操作动态扫描的四位数码管刷新时间会变长显示亮度下降。3.2 扫描函数实现逐行拉低加列值解码扫描函数的核心思路在 2.2 节已经分析过代码上要解决两个问题一是每行拉低后要留出电平稳定的时间二是要把 4 行 x 4 列的状态矩阵映射成唯一的键值。uint16_t MatrixScan_ReadKey(void) { uint16_t row 0; uint16_t col 0; uint16_t key_value 0; uint8_t row_pins[4] {GPIO_PIN_0, GPIO_PIN_1, GPIO_PIN_2, GPIO_PIN_3}; uint8_t col_pins[4] {GPIO_PIN_4, GPIO_PIN_5, GPIO_PIN_6, GPIO_PIN_7}; for (row 0; row 4; row) { // 先将所有行拉高再把当前行拉低避免多行同时拉低造成误判 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3, GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOA, row_pins[row], GPIO_PIN_RESET); delay_us(50); // 等待电平稳定 for (col 0; col 4; col) { if (HAL_GPIO_ReadPin(GPIOA, col_pins[col]) GPIO_PIN_RESET) { // 该行该列有按键按下行列组合确定唯一位 key_value row * 4 col; return key_value; } } } return 0xFFFF; // 无按键 }扫描时先统一把四路行引脚全部拉高再拉低当前行这个操作顺序很重要。如果不先拉高其他行上一轮扫描遗留的低电平状态可能让本轮发生误判。delay_us(50)是给电平传输留出的稳定时间实际 10 到 100 微秒都没问题。返回的key_value是 0 到 15 的逻辑编号但这只是行列位置不代表真正的按键含义。比如第 2 行第 0 列可能是数字键 5也可能是运算符加号需要一张映射表来转换。这个映射表是板级相关的如果接线改变表的内容也要跟着变。3.3 键值映射与防抖确认去掉一次按键多次触发行列逻辑编号转换成键值后还要经过消抖确认才能交给上层逻辑。我在上一节说过采用定时器状态机方式实际代码是每 10ms 调用一次MatrixScan_ReadKey()连续两次得到的键值相同才认为是有效按键。键值映射我习惯用一个长度为 16 的数组而不是 switch-case因为数组的修改成本低换一块按键布局不同的板子只需要改数组内容。比如键值表设计为{ 1, 2, 3, A, 4, 5, 6, B, 7, 8, 9, C, *, 0, #, D }其中 A 代表加号B 代表减号C 代表乘号D 代表除号#当成等号*当成清除键。这种映射方式直观和计算器的排版也能对上。消抖确认的关键代码可以单独封装uint8_t Matrix_GetKey(void) { static uint16_t last_key 0xFFFF; static uint16_t confirmed_key 0xFFFF; uint16_t current_key MatrixScan_ReadKey(); if (current_key last_key) { confirmed_key current_key; } else { confirmed_key 0xFFFF; last_key current_key; } return confirmed_key; // 连续两次一致返回有效键值否则返回无按键 }上一节代码每 10ms 调用一次扫描连续两次都是同一个键值才返回相当于 20ms 消抖窗口。这行逻辑放在定时器里主循环不用等待所以数码管刷新不会卡顿。释放检测在计算器逻辑的状态机里处理思路是「按下确认一次后必须等释放才能接受下一个按键」具体实现放在第四章的状态机中展开。4. 计算器核心逻辑与按键状态机一次按键只触发一次运算4.1 状态定义从空闲到显示结果的完整流转拿到按键值之后剩下的工作本质是一个状态机问题。计算器需要记住「当前输入到哪一步」是正在输入第一个操作数还是刚刚选了运算符还是在输入第二个操作数最后按下等号输出结果。我定义了四个状态加一个计算状态typedef enum { CALC_STATE_IDLE, // 空闲等待第一个操作数 CALC_STATE_INPUT1, // 正在输入第一个操作数 CALC_STATE_OPERATOR, // 已选择运算符等待第二个操作数 CALC_STATE_INPUT2, // 正在输入第二个操作数 CALC_STATE_RESULT // 已计算结果等待下一次操作 } Calc_State;状态机的流转规则是空闲或结果显示状态下按数字键清空输入缓冲并进入 INPUT1INPUT1 状态下按数字键追加数字按运算符则记住当前操作数并进入 OPERATOROPERATOR 状态下按数字键开始输入第二个操作数INPUT2 状态下按数字键追加按等号时把两个操作数和运算符送到计算函数结果存入显示缓冲并进入 RESULT。清除键在任何状态下都回到 IDLE把操作数和运算符清零。这个规则的边界条件是进位到 RESULT 状态后用户想继续用结果做下一次运算。这种情况我做了特殊处理——在 RESULT 状态下按数字键会把当前结果当成新输入的第一个操作数开始而不是清空重来。这是普通计算器的人机交互习惯不加这个细节用起来会非常别扭。4.2 按键事件处理按下标志位与释放检测实现一次按键要真正触发一次运算状态机只是规则还需要底层的事件检测配合。我在第三章提到Matrix_GetKey()返回连续两次一致的键值但主循环每 10ms 扫描一次如果不做释放检测一次按键会在 200ms 内被读到 20 次运算逻辑执行 20 次。解决办法是引入一个边缘检测机制uint8_t Key_GetEvent(void) { static uint8_t last_key_valid 0; uint8_t key Matrix_GetKey(); uint8_t event KEY_NONE; if ((key ! 0xFF) (last_key_valid 0)) { event key; // 按下沿产生一次有效事件 last_key_valid 1; } else if (key 0xFF) { last_key_valid 0; // 释放后重置标志 } return event; }这个函数只返回一次事件之后即使主循环再次读到同一个键值因为last_key_valid已经是 1不会再产生新事件。直到用户释放按键扫描返回 0xFFFF标志位清零下一次按下才会重新产生事件。从触发到释放整个事件只上报一次计算器的运算逻辑也只执行一次。主循环的调用方式就变得很干净每 10ms 调用一次Key_GetEvent()如果有事件就交给状态机处理没有事件就直接刷新数码管。整个主循环没有任何阻塞等待CPU 大部分时间在空转按键响应和显示刷新互不干扰。4.3 运算函数与显示缓冲溢出、除零和负数处理运算逻辑本身不复杂但有几个显示层面的边界处理需要提前设计好。计算结果可能超过 4 位数码管的显示范围除法可能出现小数甚至除零错误减法可能出现负数。我的方案是运算都用整数除法转换成整数除法超出显示范围直接显示 ERR。下面是完整的运算处理代码包含操作数拼接和四则运算uint16_t Calc_Execute(uint16_t op1, uint16_t op2, uint8_t operator) { int32_t result 0; switch (operator) { case KEY_PLUS: result op1 op2; break; case KEY_MINUS: result op1 - op2; // 允许负数 break; case KEY_MULTIPLY: result op1 * op2; break; case KEY_DIVIDE: if (op2 0) { return DISPLAY_ERR; // 除零保护 } result op1 / op2; break; default: return 0; } if (result 9999 || result -999) { return DISPLAY_ERR; // 超过四位数码管显示范围 } return (uint16_t)result; // 负数处理见 4.3 节说明 }操作数本身的累积逻辑是把上一次输入的数字乘 10 再加新数字op op * 10 digit。如果用户在 INPUT1 状态输入了 5再按 6操作数从 5 变成 56。这个逻辑有个隐含限制连续输入超过 4 位时操作数可能溢出所以我在数字按键处理函数里增加判断op超过 999 就不再追加相当于最多输入四位数。负数显示的处理方式是如果结果为负数显示缓冲区存负号加绝对值。数码管没有独立负号位我在最左边一位显示-字符。具体实现是单独设一个neg_flag显示函数判断这个标志把最高位的段码改成减号的段码。只要结果大于等于 -999四位加上负号刚好够显示。除零和溢出都返回DISPLAY_ERR显示函数对这个特殊值直接点亮数码管的 E、r、r 三段。还有一个隐含问题整数除法得到 5 而不是 5.5这在计算器里可以被接受因为需求本身就是整数计算器。如果要支持小数需要引入浮点运算和更复杂的显示格式那就超出这个最小系统的设计边界了。4.4 主循环与定时器协作动态扫描刷新不闪屏主循环架构是「定时器中断负责按键扫描和显示刷新主循环只做事件分派」。这个划分避免了按键读取和显示更新互相抢占也让代码后续可维护性更好。定义如下volatile uint8_t timer_flag_10ms 0; void TIM2_IRQHandler(void) { if (__HAL_TIM_GET_FLAG(htim2, TIM_FLAG_UPDATE)) { __HAL_TIM_CLEAR_FLAG(htim2, TIM_FLAG_UPDATE); timer_flag_10ms 1; // 10ms 时基标志 Display_Refresh(); // 每 10ms 刷新一次动态扫描 } } int main(void) { HAL_Init(); SystemClock_Config(); GPIO_Config(); TIM2_Init(); uint8_t key KEY_NONE; while (1) { if (timer_flag_10ms) { timer_flag_10ms 0; key Key_GetEvent(); // 非阻塞读取按键事件 if (key ! KEY_NONE) { Calc_ProcessKey(key); // 交给状态机处理 } } } }动态扫描四位数码管时每一位点亮 1ms 左右就切换到下一位人眼的视觉暂留效应会让四位看起来同时点亮。如果每位数码管点亮时间超过 5ms刷新周期过长亮度会明显下降且出现闪烁感。定时器中断里 10ms 刷新一次动态扫描意味着每位点亮 2.5ms亮度在可接受范围。如果发现亮度偏暗把定时器改成 8ms 或者把每位点亮时间调长一点即可但要注意不能超过扫描周期除以位数这个上限。主循环不做任何HAL_Delay()调用所有时间相关的节奏统一由时基标志驱动。这个习惯是从吃过大亏之后养成的——早期版本在按键处理里加了一个 20ms 消抖延时按下按键的瞬间整个显示和扫描停止闪烁非常明显。之后把所有延时从主循环移除哪怕是最简单的延时也交给定时器计数完成问题彻底解决。5. 避坑指南五个导致按键失灵与显示异常的排错记录5.1 按键完全无响应JTAG 引脚被复用占用现象程序烧录后数码管能正常显示但矩阵键盘任何按键都没有反应。用万用表测量行引脚电平发现一行拉低时其他行也跟着被拉低。原因我一开始把行引脚分配到 PB3、PB4、PA15 这些引脚上这些引脚在 STM32F103C8T6 上默认复用为 JTAG 功能。初始化代码虽然执行了 GPIO 配置但 JTAG 功能没有关闭引脚的电平不受普通 GPIO 输出寄存器控制。最常见的是 PB3JTDO和 PA15JTDI这两个。解决在 GPIO 初始化之前先调用重映射函数把 JTAG 功能完全关闭只保留 SWD 调试接口__HAL_AFIO_REMAP_SWJ_NOJTAG();注意这行代码必须在使能 GPIOB 和 GPIOA 时钟之后、配置引脚之前调用。如果使用了 PA13、PA14 即 SWDIO 和 SWCLK 这两个引脚它们永远不能再当普通 GPIO 使用这也是选引脚时优先避开它们的原因。从那以后我每次画板子之前都先查一遍引脚复用表不再凭感觉分配。5.2 每按一次键数字增加好几次缺少边缘事件检测现象按下数字键 5数码管显示变成 55、555 甚至 5555按得越久数字增长越多。原因主循环每 10ms 读取一次按键值一个物理按键从按下到释放至少持续 100ms主循环会连续读到 10 次相同的键值。运算逻辑对每一次有效键值都执行了追加操作导致一次按下被识别成多次输入。这是矩阵键盘计算器最常见的逻辑错误不是硬件问题。解决引入 4.2 节中的last_key_valid标志位只有从无按键状态跳变到有按键状态的上升沿才产生一次事件。这个标志位在按键释放前始终保持置位后续扫描即使读到相同的键值也不会再次上报。代码就一行状态判断但效果立竿见影。5.3 数码管亮度不均匀且闪烁阻塞式延时破坏了动态扫描节奏现象按下按键后数码管某一两位明显变暗松开后恢复。按键操作频繁时整个显示区闪烁严重。原因按键识别逻辑中使用了阻塞式HAL_Delay(20)做消抖延时期间定时器中断虽然还在触发但主循环忙于消抖无法及时处理显示刷新。动态扫描的目标是每一位在固定周期内点亮相同时间一旦刷新间隔拉长且不均匀视觉上就是闪烁和亮度不均。解决把所有延时从主循环挪走消抖交给定时器时基的多次采样。按键扫描、消抖确认、显示刷新全部放到同一时基节拍下执行主循环只做事件分派。改完之后显示亮度在各位置之间几乎没有差异按键操作期间也没有视觉可感知的闪动。如果显示仍偏暗适当缩短定时器周期到 8ms效果会进一步改善。5.4 扫描时偶尔串键列引脚没有上拉悬空电平造成误触发现象按下键盘左上角的 1 键结果有时会同时触发 5 键。用手碰触列引脚时数码管出现随机数字。原因矩阵键盘的列引脚配置成了浮空输入。当某一列没有任何按键按下时该引脚电平完全由电路中的寄生电容和电磁干扰决定并不是稳定的高电平。扫描程序读到低电平后误判为按键按下出现串键。解决将列引脚从GPIO_MODE_INPUT改为GPIO_MODE_INPUT并且配置GPIO_PULLUP利用 STM32 内部上拉电阻把默认电平拉到 3.3V。这个修改不需要改动硬件但能让悬空问题彻底消失。如果内部上拉电阻的阻抗仍不够稳定可以在硬件上给每个列引脚加一个 10k 欧姆外部上拉电阻到 3.3V。5.5 烧录后没有任何反应BOOT0 引脚状态被忽略现象程序编译和烧录都提示成功但复位后板子没有任何反应数码管不亮串口也没有输出。原因ST MCU 上电时 BOOT0 引脚的电平决定启动方式。BOOT0 接高电平时芯片从系统存储器启动用户 Flash 里的程序根本不会运行。有些最小系统板的 BOOT0 默认由跳线设置跳线帽插错位置或者拔掉导致悬空都会造成程序不运行。解决检查 BOOT0 引脚是否通过跳线帽接到 GND确保启动方式为主 Flash。按住板子上的复位键再松开程序就会正常启动。如果这个现象反复出现可以在硬件设计上把 BOOT0 通过 10k 欧姆电阻下拉到 GND保证默认状态确定。排查这个问题时可以先量 BOOT0 引脚电平高电平就优先查启动配置这个检查顺序能省不少时间。6. 从能用到好用串口验证与三个可落地的二次开发方向6.1 先验证按键再接显示串口打印键值的调试习惯矩阵键盘计算器的调试顺序建议是先单独验证按键扫描确认每一个物理按键都能返回正确键值再接入状态机和数码管显示。直接在完整工程里排查问题按键和显示相互干扰很难分清是扫描问题还是显示问题。验证方法是在扫描函数后面加一段串口打印把Matrix_GetKey()返回的键值通过串口发送到电脑uint16_t key Matrix_GetKey(); if (key ! 0xFFFF) { char buf[16]; sprintf(buf, Key: %d\r\n, key); HAL_UART_Transmit(huart1, (uint8_t *)buf, strlen(buf), 100); }打开串口助手逐个按下按键正常情况应该看到 0 到 F 的键值序列每个按键按下一次打印一行。如果发现按键顺序和实际排版不一致只需要修改键值映射表。如果发现串键、漏键或者一次按下多次打印回到第五章的避坑清单排查。这段代码不加也能工作但调试效率会差很多尤其是在没有示波器的情况下串口是唯一能确认扫描时序是否正确的工具。6.2 二次开发方向从计算器延伸到更多场景基础计算器跑通之后三个方向的扩展性价比最高。第一个是掉电保存最后运算结果。STM32F103C8T6 内部 Flash 的最后几页可以用来存数据把结果写到固定地址下次上电时先读取判断有效性。实现上需要注意 Flash 写入前必须先擦除整个扇区而且写 Flash 时不能执行从同区域取指令的代码一般建议把 Flash 操作函数放到 RAM 里执行或者简单一点直接外挂一个小容量 I2C EEPROM代码复杂度更低。第二个方向是升级成表达式解析。现在的状态机一次只能处理一个运算符改成中缀转后缀表达式就可以支持12 5 * 3这种多运算符输入而不仅仅是逐次计算。核心是把操作数和运算符分别压栈遇到高优先级运算符时先弹出计算代码量增加大约 200 行难度主要在括号处理和栈溢出边界判断。第三个方向是加按键蜂鸣器反馈。一个无源蜂鸣器接到任意 GPIO在Key_GetEvent()返回有效事件时用定时器 PWM 驱动蜂鸣器响 10ms 就足够。这个功能实现的成本极低但对操作手感提升非常明显也是把计算器从「能显示结果」变成「像商品一样可用」的关键一步。回头看这个项目的教训我从这次拆解中学到的不是矩阵扫描的原理而是开发顺序的纠偏——先验证硬件边缘再写业务逻辑最后做用户体验优化。从那以后我做任何按键类的小项目都强制自己先走一遍串口验证流程确认 IO 映射、扫描时序和边缘触发都没有问题再开始设计上层逻辑。这套顺序帮我避开了很多回头排查的麻烦希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站