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与STM32+BLE模块

对比常见方案才能理解为什么“STM32+BLE模块”适合硬核DIY而不是偷懒:

方案链路可控程度开发量典型问题
nRF52840直接做主控高,协议栈全开放大,要熟Zephyr/nRF SDK按键扫描和蓝牙耦合在一个SDK里,调试成本高
ESP32+蓝牙协议栈ESP32的蓝牙栈资源占用大,按键响应容易受Wi-Fi任务干扰
STM32F103+BLE HID模块中高,主控侧全可控模块固件限制多,需要确认HID模式是否开放
STM32+HC05经典蓝牙系统识别不了HID,直接淘汰

我一般会优先选第三条路线:STM32只干GPIO扫描和报告组装这两件能完全掌控的事,BLE模块用带HID固件的JDY-23这类串口模块。主控侧出错看得见摸得着,串口日志能打印每一帧报告;链路若还不通,问题基本锁定在模块配置上,排查范围就小了。

2.3 选BLE模块的三个硬指标

很多标着“BLE 4.0/5.0”的模块实际上只有透传模式,GATT服务是自定义的,根本没有HID Service。选模块时对着产品页面找三个点确认。

第一,固件必须内置HID Keyboard模式,能通过AT指令切换。拿到模块先发一条AT+VERSION看固件号,再查手册里有没有HID或Keyboard相关指令。支持HID键盘的模块在串口层会有类似这样的指令族:

AT+VERSION AT+HIDEN=1 # 启用HID模式 AT+HID=00,00,04,00,00,00,00,00 # 发送一个A键按下报告

逻辑说明:AT+VERSION是确认固件版本的通用指令,不同厂商写法略有差异。AT+HIDEN=1把模块的角色从普通透传切换成HID键盘,之后模块会在BLE广播中携带HID服务,电脑端配对时就能识别成输入设备。AT+HID后面的8个字节就是标准键盘报告,这里第二个字节是04,对应USB HID Usage Table里字母A的键码。

参数说明:8字节报告的完整结构是[修饰键] [保留字节] [键值1..6]。修饰键0x02代表Shift,0x01是Ctrl;键值0x04代表A,0x05是B,数字1对应0x1E。注意模块进入HID模式后就不再响应串口调试指令,需要看具体模块手册里如何退出,一般是通过物理复位。

第二,报告帧入口形式要明确。有的模块收AT+HID指令帧,有的模块要求先切到透传模式再直接推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 Keyboard
LDOXC6206P332MR1SOT-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~8ms,10ms是安全边界;如果用了老轴的触点,可把阈值调大到8至10次,也就是16到20ms,再大会明显拖慢响应手感。释放方向同样走这个状态机,按键松开同样需要确认,否则会在释放瞬间重复上报。

代码里注释提到的移位写法要留意:(matrix[r] >> c) & 1才是取第c位的正解,用(c+1)是初版常见笔误,编译不报错但位序错一位,会导致整个键盘键位错乱。

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), "AT+HID=%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列对应键盘左上角的Backspace(0x2A)。

参数说明:串口发送部分有两个重点。一是发送时机,应该只在报告内容发生变化时发,一直重复发同一份报告没有意义还费电;二是模块进入HID模式后如果还收AT+HID指令帧,串口波特率和模块选型阶段确认的必须一致,常见是115200。最后,全部按键释放时要记得发一份全0报告,告诉主机“没有任何键按下”,否则系统会认为上一个键始终按着,表现出来就是字母无限重复。

5. 链路调试的三个技巧与低功耗收尾

5.1 三分钟验证链路通不通

固件写好后不要直接焊矩阵,先验证蓝牙链路。用USB转TTL把BLE模块单独接到电脑,发送一条测试报告:

AT+HID=00,00,1E,00,00,00,00,00

这段命令发出去后,如果电脑已经配对成功且模块处于HID模式,光标处应当输出一个数字1(键码0x1E在HID Usage Table里就是键盘上的1键)。若没反应,按顺序查三处:模块是否真的运行在HID固件下,AT+VERSION能看到固件号;电脑蓝牙列表里设备的服务UUID里有没有0x1812(HID Service);串口工具是否勾选了发送新行,很多模块要求以\r\n结尾才执行。

链路通了再验证主控到模块的接线。把上面这条AT指令直接写进STM32固件,上电自动发一次,电脑有输出说明MCU和模块之间UART物理链路是通的。到这一步,排错范围就只剩矩阵扫描了。

5.2 连接间隔是延迟与功耗的旋钮

BLE模块上报数据的频率由连接间隔决定,这是延迟和功耗互相拉扯的那个旋钮。模块默认连接间隔通常设置在50ms到100ms,左右手能感觉出明显延迟,打字时像带了buffering。

对键盘这种交互设备,把连接间隔调到20ms比较合理,对应约20到30ms的端到端延迟,和很多无线办公键盘体感一致。调法通过模块的AT指令集配置,例如部分模块使用AT+ADVI5广播间隔和AT+MINI5这类连接间隔指令,不同固件命名差异很大,但仍然以模块手册为准。间隔调到10ms以下延迟更小但电流会明显抬头,键盘续航可能从两个月掉到三周,不值得。

5.3 低功耗三件套与电流实测

键盘不是一直有人在敲,低功耗做不好,400mAh电池撑不过一周。三件套按顺序做:扫描降频、睡眠唤醒、外设时钟关断。

按键扫描间隔从2ms提到10ms,绝大多数时间人不会这么高频地按键,10ms对应100Hz采样率依然足够。再用RTC定时唤醒主控扫描,扫描完立刻进STOP模式。按下动作本身可由外部中断唤醒——矩阵列线在无按键时是高电平,按下变低,正好触发EXTI。需要注意STOP模式下GPIO保持状态,唤醒后矩阵扫描逻辑不需要改。

实测电流用万用表串联电池端,要选带平均模式或测积分电流的表。键盘空闲时应能稳住几十微安级别,按键时电流峰值冲到40mA附近,平均电流通常不到3mA,这样400mAh电池能跑十天以上。连接断开的广播模式比连接态更费电,广播间隔可以放宽到200ms以上,省掉一部分无谓功耗。

最后的落地技巧:固件里留一个3字节的环形日志,把每次发送的HID报告原样存下来,蓝牙断连时通过USB转串口拉出来,能直接看到最后一条报告是不是卡在了某个非零键值上——这是排查“键盘随机连打”最快的路径。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询