恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
STM32蓝牙机械键盘DIY:矩阵扫描、BLE HID与低功耗实现
首页
资讯中心
/
STM32蓝牙机械键盘DIY:矩阵扫描、BLE HID与低功耗实现
STM32蓝牙机械键盘DIY:矩阵扫描、BLE HID与低功耗实现
发布时间:2026/9/11 7:07:24
简介一份基于STM32的蓝牙机械键盘DIY项目资源包面向电子爱好者、嵌入式开发者和客制化键盘玩家。项目围绕STM32微控制器展开涵盖轴体选型、键盘阵列排版、PCB设计、固件编写及外壳CNC制作等完整环节适合想从零打造无线机械键盘、深入理解软硬件协同实现的人。压缩包共286个文件大小约29.56MB主要内容包括STM32固件源码C/H文件、Keil工程配置uvprojx/uvoptx、PCB原理图与封装库SchDoc/PcbDoc/SchLib/PcbLib、3D外壳模型sldprt以及PDF、MD等说明文档可对照学习电路绘制、按键扫描、蓝牙通信与结构设计。已有166人学习下载这份资源将源码、硬件图纸与结构文件整合在同一包中从功能调试到外壳装配都有据可循适合以项目驱动方式掌握嵌入式开发流程。1. 为什么是STM32来做这把蓝牙机械键盘网上打着“DIY蓝牙机械键盘”旗号的项目绝大多数是nRF52840或ESP32装一套现成固件按键扫描、蓝牙协议栈全被SDK包住出了问题只能改配置基本谈不上“基于STM32”。真正把链路握在自己手里的做法反而是另一条路STM32当主控做矩阵扫描、去抖和HID报告组包蓝牙部分外挂一颗支持BLE HID的模块MCU只通过串口把8字节报告交给模块由模块完成GATT服务和连接管理。这条路适合两类人一类是想搞明白蓝牙键盘从“按下键”到“屏幕上出字”完整数据通路的开发老手另一类是做过单片机但没碰过蓝牙协议栈、想给现有键盘项目加无线的进阶玩家。它不需要碰BLE协议栈但对GPIO、定时器、功耗和串口时序的要求一点不少。下面这套方案以STM32F103C8T6为基板覆盖选型、电路、固件到调链路的关键步骤。2. STM32蓝牙键盘的链路两条路线与BLE HID选型2.1 经典蓝牙SPP到不了“即插即用键盘”做蓝牙键盘最先要排除的就是HC05、HC06这类经典蓝牙模块。它们跑的是SPP串口透传协议手机或电脑连上后只会把它识别成一个虚拟串口而不是输入设备。键盘按下后照样能往串口发数据但系统端没有任何HID协议栈去消费这些数据结果就是“连上了打不出字”。蓝牙键盘真正依赖的是BLE HID也就是HID over GATT。模块负责把BLE连接、GATT服务和Report Map描述符全部处理好STM32只需要通过串口把键盘报告也就是修饰键和按键码传给模块模块以Notification方式上报给主机。这样主控端不碰任何BLE协议帧难度系数直接降了一个量级。选型文档里反复出现的“蓝牙LE是什么意思”在这条链路里就非常明确主控走普通串口BLE全部由模块代理。2.2 三条路线的取舍SoC、ESP32与STM32BLE模块对比常见方案才能理解为什么“STM32BLE模块”适合硬核DIY而不是偷懒方案链路可控程度开发量典型问题nRF52840直接做主控高协议栈全开放大要熟Zephyr/nRF SDK按键扫描和蓝牙耦合在一个SDK里调试成本高ESP32蓝牙协议栈高中ESP32的蓝牙栈资源占用大按键响应容易受Wi-Fi任务干扰STM32F103BLE HID模块中高主控侧全可控中模块固件限制多需要确认HID模式是否开放STM32HC05经典蓝牙低低系统识别不了HID直接淘汰我一般会优先选第三条路线STM32只干GPIO扫描和报告组装这两件能完全掌控的事BLE模块用带HID固件的JDY-23这类串口模块。主控侧出错看得见摸得着串口日志能打印每一帧报告链路若还不通问题基本锁定在模块配置上排查范围就小了。2.3 选BLE模块的三个硬指标很多标着“BLE 4.0/5.0”的模块实际上只有透传模式GATT服务是自定义的根本没有HID Service。选模块时对着产品页面找三个点确认。第一固件必须内置HID Keyboard模式能通过AT指令切换。拿到模块先发一条ATVERSION看固件号再查手册里有没有HID或Keyboard相关指令。支持HID键盘的模块在串口层会有类似这样的指令族ATVERSION ATHIDEN1 # 启用HID模式 ATHID00,00,04,00,00,00,00,00 # 发送一个A键按下报告逻辑说明ATVERSION是确认固件版本的通用指令不同厂商写法略有差异。ATHIDEN1把模块的角色从普通透传切换成HID键盘之后模块会在BLE广播中携带HID服务电脑端配对时就能识别成输入设备。ATHID后面的8个字节就是标准键盘报告这里第二个字节是04对应USB HID Usage Table里字母A的键码。参数说明8字节报告的完整结构是[修饰键] [保留字节] [键值1..6]。修饰键0x02代表Shift0x01是Ctrl键值0x04代表A0x05是B数字1对应0x1E。注意模块进入HID模式后就不再响应串口调试指令需要看具体模块手册里如何退出一般是通过物理复位。第二报告帧入口形式要明确。有的模块收ATHID指令帧有的模块要求先切到透传模式再直接推8字节还有的自定义APP透传帧。这个选型阶段必须确认好固件里串口发送部分要照着写不然后面调试就是黑盒。第三连接间隔Connection Interval是否开放配置。键盘对延迟敏感模块默认的连接间隔可能是50ms甚至100ms打字时能感觉到明显的粘滞感。常见做法是通过AT指令把间隔调小到20到30ms能做到近似有线键盘的响应。功耗和延迟是反比关系具体调校放到第5章讲。3. 硬件搭架子矩阵扫描、二极管极性和3.3V供电3.1 引脚规划与矩阵扫描方向机械键盘普遍是6行×14列的矩阵结构就算塞进数字区和功能键也基本在这个范围。STM32F103C8T6有37个可用GPIO留出行列占用后会剩下串口和调试口足够用。一个稳妥的引脚分配如下功能引脚配置行引脚 R0-R5PB0-PB5推挽输出默认输出高列引脚 C0-C13PA0-PA7, PB6-PB13上拉输入检测低电平蓝牙模块 UARTPA2(TX), PA3(RX)复用推挽/浮空输入LED状态灯PC13推挽输出扫描方向定成“行输出低、列输入上拉”。每轮扫描先把所有行置高再单独把目标行拉低延时几个微秒等电平稳定然后读整排列引脚。若某列读到低电平说明该行该列的按键被按下。这种方向的优点是列引脚天然带内部上拉省掉外部上拉电阻且行线拉低后扫描的是“下降沿”受线路电容影响小。缺点是内部上拉的阻值约30kΩ到50kΩ对长走线键盘来说抗干扰差一些硬核方案里可以在每条列线上并一颗10kΩ外部上拉用ATE自动测试设备调过的人习惯这么做代价只是多14颗电阻。3.2 二极管极性与反串扰原理矩阵里必须给每个按键串一颗1N4148二极管极性是“阳极接列线、阴极接行线”和上面行低列高的扫描方向配套。反串扰的成因是这样的假设要扫描R1行R1被拉低其它行还维持高电平。如果此时用户同时按下了R1-C1和R2-C1两个键没有任何保护的话R2的高电平会通过按键、C1列线、再经R1-C1按键倒灌进R1行导致扫描器误判R1行的多个列都被按下。满足特定行列组合时还会出现“鬼键”。加入二极管后从行线到列线的方向是截止的R2的高电平走到C1节点时会先碰到R1-C1那颗二极管的阴极被反向阻断无法灌进R1。这里二极管的作用完全是逻辑保护对信号质量没有贡献所以选型不需要讲究1N4148和1N4007都能工作前者封装小更适合PCB后者批量采购更便宜。3.3 供电与去耦锂电池怎么接才稳键盘无线化后供电就要从USB线里解放出来。最省事的方案是一颗3.7V锂聚合物电池加一个LDO稳压芯片降到3.3V给STM32和BLE模块共电。AMS1117-3.3压差大、静态功耗高不适合电池设备我一般会用XC6206P332MR静态电流只有1到2μA压差在100mA负载下也只有0.2V左右对蓝牙键盘这种平均电流不到20mA的场景绰绰有余。BLE模块射频发射瞬间电流能达到40mA甚至更高如果LDO选太小电压会被瞬间拉低导致模块掉线。XC6206的最大输出300mA线性调整率约0.2%能满足瞬时电流需求。电容配置上LDO输入端放一颗10μF钽电容加一颗100nF陶瓷电容输出端放一颗10μF陶瓷电容紧贴模块VCC引脚。PCB上这组电容离蓝牙模块电源脚越近越好射频电流走的路越短连接稳定性越好。3.4 一个可以直接打样的物料清单全套物料并不复杂列一张清单方便备料打样时按这个BOM走基本不会缺件物料型号/规格数量说明主控STM32F103C8T61蓝板或自绘最小系统蓝牙模块JDY-23/HM-10等HID固件版1确认支持BLE KeyboardLDOXC6206P332MR1SOT-23封装二极管1N414884每个按键一颗电池3.7V锂聚合物 400mAh1续航与厚度折中按键轴Gateron红轴/套筒84按配列自选电容10μF/100nF各3去耦组合复位按键贴片6×61接NRST主控如果不想自己画最小系统板直接用市面十几块的STM32F103C8T6核心板也行但注意核心板上的LDO静态功耗高做无线键盘续航会很难看。硬核方向还是推荐自绘一版集成板嘉立创白嫖打样把矩阵引脚直接连到主控省掉杜邦线带来的接触不良问题。4. 固件实现定时器扫描、去抖状态机和HID报告4.1 定时器轮询优于外部中断矩阵键盘的按键数量通常在80个上下如果全部用GPIO外部中断每条中断线要负责多个按键得在中断里做多路复用判断代码复杂还容易漏触发。常见做法是用定时器产生固定间隔中断每2ms轮询一次完整矩阵所有按键状态都在这一个时间片里刷新。2ms的扫描间隔对应500Hz的按键采样率完全超过人手的物理极限。用HAL库的HAL_TIM_PeriodElapsedCallback做轮询入口扫描逻辑放在回调里主循环只处理HID报告的发送和低功耗切换。扫描84个按键的耗时在72MHz主频下不超过0.5ms不会阻塞其它任务。4.2 矩阵扫描函数按第3章的引脚方向扫描函数实现如下// matrix.h #define ROW_NUM 6 #define COL_NUM 14 // 按下为1未按下为0 uint8_t matrix[ROW_NUM]; // GPIO引脚掩码按实际CubeMX生成的引脚填 static const uint16_t row_mask[ROW_NUM] { GPIO_PIN_0, GPIO_PIN_1, GPIO_PIN_2, GPIO_PIN_3, GPIO_PIN_4, GPIO_PIN_5 }; static const uint16_t col_mask[COL_NUM] { 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_PIN_8, GPIO_PIN_9, GPIO_PIN_10, GPIO_PIN_11, GPIO_PIN_12, GPIO_PIN_13 }; void matrix_scan_all(void) { uint8_t r, c; // 先把所有行拉高避免上轮残留电平影响本轮 for (r 0; r ROW_NUM; r) { HAL_GPIO_WritePin(GPIOB, row_mask[r], GPIO_PIN_SET); } for (r 0; r ROW_NUM; r) { // 目标行拉低开始扫描 HAL_GPIO_WritePin(GPIOB, row_mask[r], GPIO_PIN_RESET); delay_us(5); matrix[r] 0; for (c 0; c COL_NUM; c) { GPIO_PinState st HAL_GPIO_ReadPin(GPIOA, col_mask[c]); if (st GPIO_PIN_RESET) { matrix[r] | (1 c); } } // 当前行扫描完恢复高再扫下一行 HAL_GPIO_WritePin(GPIOB, row_mask[r], GPIO_PIN_SET); } }逻辑说明外层循环逐行拉低内层循环读取该行14个列引脚。某列引脚读到低电平说明电流从列线经过二极管和按键流向低电平行线这里matrix[r]里对应位置1。扫描完一行立即把行引脚拉高防止多个行同时拉低造成矩阵短路。参数说明delay_us(5)是电平稳定时间机械轴按下时触点有弹跳电学上GPIO从高到低的翻转时间不到几十纳秒但考虑到按键走线和上拉的RC延迟5μs足够。如果板子走线特别长或用了弱上拉可以调到10μs代价是整轮扫描从0.5ms涨到1ms左右。4.3 去抖状态机机械按键按下和释放的瞬间会产生数毫秒的物理抖动处理不当就是一次按下出现重复字符。最常见做法是连续多次采样确认也就是状态机加计数器// debounce.h #define SCAN_INTERVAL_MS 2 #define DEBOUNCE_THRESHOLD 5 // 连续5次采样确认约10ms typedef struct { uint8_t stable; // 稳定状态向上层暴露 uint8_t counter; // 连续采样计数器 } key_state_t; key_state_t key_state[ROW_NUM][COL_NUM]; void debounce_update(uint8_t matrix[ROW_NUM]) { for (int r 0; r ROW_NUM; r) { for (int c 0; c COL_NUM; c) { uint8_t raw (matrix[r] (c 1)) 1; // 把第c位移到bit0 // 实际更稳妥的写法 uint8_t raw (matrix[r] c) 1; if (raw 1) { if (key_state[r][c].counter 255) { key_state[r][c].counter; } if (key_state[r][c].counter DEBOUNCE_THRESHOLD) { key_state[r][c].stable 1; } } else { key_state[r][c].counter 0; key_state[r][c].stable 0; } } } }逻辑说明稳定状态stable只在连续5次扫描都读到同一个电平时才翻转。抖动期间信号在0和1之间跳变计数器不断清零stable不会误动这比简单的延时去抖更省CPU——延时去抖是阻塞等待状态机去抖是边扫描边判断。参数说明阈值取5对应10ms确认窗口。红轴、青轴这类机械轴的抖动时间约2~8ms10ms是安全边界如果用了老轴的触点可把阈值调大到8至10次也就是16到20ms再大会明显拖慢响应手感。释放方向同样走这个状态机按键松开同样需要确认否则会在释放瞬间重复上报。代码里注释提到的移位写法要留意(matrix[r] c) 1才是取第c位的正解用(c1)是初版常见笔误编译不报错但位序错一位会导致整个键盘键位错乱。4.4 组装HID报告并发送给BLE模块去抖后的stable状态可以拿来扫描出当前所有按下的键然后组装成BLE键盘标准报告// usb_hid_keys.h // USB HID Keyboard Usage Table 中的部分映射 static const uint8_t keymap_ascii[128] { [a] 0x04, [b] 0x05, [c] 0x06, [d] 0x07, [e] 0x08, [f] 0x09, [g] 0x0A, [h] 0x0B, [i] 0x0C, [j] 0x0D, [k] 0x0E, [l] 0x0F, [m] 0x10, [n] 0x11, [o] 0x12, [p] 0x13, [q] 0x14, [r] 0x15, [s] 0x16, [t] 0x17, [u] 0x18, [v] 0x19, [w] 0x1A, [x] 0x1B, [y] 0x1C, [z] 0x1D, [1] 0x1E, [2] 0x1F, [3] 0x20 }; // main.c 中发送报告 static void build_and_send_report(void) { uint8_t report[8] {0}; // modifier, reserved, key1..key6 int slot 2; // 从byte2开始填充键值 for (int r 0; r ROW_NUM; r) { for (int c 0; c COL_NUM; c) { if (key_state[r][c].stable slot 8) { report[slot] keymap_ascii[keycode_at(r, c)]; } } } // 通过串口按AT帧协议发给BLE模块 char cmd[40]; int len snprintf(cmd, sizeof(cmd), ATHID%02X,%02X,%02X,%02X,%02X,%02X,%02X,%02X\r\n, report[0], report[1], report[2], report[3], report[4], report[5], report[6], report[7]); HAL_UART_Transmit(huart2, (uint8_t *)cmd, len, 10); }逻辑说明报告第一个字节是修饰键Ctrl对应0x01、Shift对应0x02、Alt对应0x04可以按位或组合第二个字节固定保留为0主机不会解析后六个字节是实际按键码最多同时上报6个键再多就属于HID协议的溢出场景通常直接丢弃后面的按键。keycode_at(r, c)里放的是扫描码到ASCII表的映射比如矩阵0行0列对应键盘左上角的Backspace0x2A。参数说明串口发送部分有两个重点。一是发送时机应该只在报告内容发生变化时发一直重复发同一份报告没有意义还费电二是模块进入HID模式后如果还收ATHID指令帧串口波特率和模块选型阶段确认的必须一致常见是115200。最后全部按键释放时要记得发一份全0报告告诉主机“没有任何键按下”否则系统会认为上一个键始终按着表现出来就是字母无限重复。5. 链路调试的三个技巧与低功耗收尾5.1 三分钟验证链路通不通固件写好后不要直接焊矩阵先验证蓝牙链路。用USB转TTL把BLE模块单独接到电脑发送一条测试报告ATHID00,00,1E,00,00,00,00,00这段命令发出去后如果电脑已经配对成功且模块处于HID模式光标处应当输出一个数字1键码0x1E在HID Usage Table里就是键盘上的1键。若没反应按顺序查三处模块是否真的运行在HID固件下ATVERSION能看到固件号电脑蓝牙列表里设备的服务UUID里有没有0x1812HID Service串口工具是否勾选了发送新行很多模块要求以\r\n结尾才执行。链路通了再验证主控到模块的接线。把上面这条AT指令直接写进STM32固件上电自动发一次电脑有输出说明MCU和模块之间UART物理链路是通的。到这一步排错范围就只剩矩阵扫描了。5.2 连接间隔是延迟与功耗的旋钮BLE模块上报数据的频率由连接间隔决定这是延迟和功耗互相拉扯的那个旋钮。模块默认连接间隔通常设置在50ms到100ms左右手能感觉出明显延迟打字时像带了buffering。对键盘这种交互设备把连接间隔调到20ms比较合理对应约20到30ms的端到端延迟和很多无线办公键盘体感一致。调法通过模块的AT指令集配置例如部分模块使用ATADVI5广播间隔和ATMINI5这类连接间隔指令不同固件命名差异很大但仍然以模块手册为准。间隔调到10ms以下延迟更小但电流会明显抬头键盘续航可能从两个月掉到三周不值得。5.3 低功耗三件套与电流实测键盘不是一直有人在敲低功耗做不好400mAh电池撑不过一周。三件套按顺序做扫描降频、睡眠唤醒、外设时钟关断。按键扫描间隔从2ms提到10ms绝大多数时间人不会这么高频地按键10ms对应100Hz采样率依然足够。再用RTC定时唤醒主控扫描扫描完立刻进STOP模式。按下动作本身可由外部中断唤醒——矩阵列线在无按键时是高电平按下变低正好触发EXTI。需要注意STOP模式下GPIO保持状态唤醒后矩阵扫描逻辑不需要改。实测电流用万用表串联电池端要选带平均模式或测积分电流的表。键盘空闲时应能稳住几十微安级别按键时电流峰值冲到40mA附近平均电流通常不到3mA这样400mAh电池能跑十天以上。连接断开的广播模式比连接态更费电广播间隔可以放宽到200ms以上省掉一部分无谓功耗。最后的落地技巧固件里留一个3字节的环形日志把每次发送的HID报告原样存下来蓝牙断连时通过USB转串口拉出来能直接看到最后一条报告是不是卡在了某个非零键值上——这是排查“键盘随机连打”最快的路径。本文还有配套的精品资源点击获取