最近把吃灰许久的ESP32-C3翻出来,做了一个BLE HID键盘,把办公室那套“只能看不能用”的机械键盘方案彻底换掉了。以前大家总说蓝牙鼠标能DIY,键盘就没见过几个像样的教程,其实用ESP32-C3做HID键盘比想象中简单得多,而且成本极低——一块开发板加上几个按键,就能实现自定义键位、组合键、快捷宏,甚至还能调功耗做到一颗电池用很久。
这篇就把整个项目从选型到固件到调试一条龙讲清楚。核心围绕三个词:ESP32-C3、BLE、HID。你不需要有很深的嵌入式基础,会用Arduino IDE、能看懂GPIO就能跟着做。代码是完整可用的,烧进去就能连手机或电脑打字,剩下的就是往矩阵里加你自己的键位映射。
1. 为什么是ESP32-C3:方案对比与硬件特性
1.1 常见BLE HID方案怎么选
做蓝牙键盘,市面上常见的芯片方案就那么几类:nRF52840、STM32WB系列、ESP32-C3,再加上各类专用键盘主控比如Atmel/Microchip的USB HID芯片。如果你只想要一个标准键盘,买现成的键盘主控板最省事,但灵活性很差,想加宏定义、改键位布局、做自己专属的功能键就很痛苦。
nRF52840是北欧半导体的老牌BLE芯片,协议栈成熟,很多客制化键盘都在用,但缺点是开发门槛偏高,资料大多是英文的,板子价格也不算便宜。STM32WB系列性能强、资源丰富,但同样存在开发环境复杂、BLE协议栈上手成本高的问题。
ESP32-C3的优势在于:价格便宜(十几块到二十几块一个模组),集成BLE 5.0,GPIO数量够用,最关键是开发资料多、踩坑的人多,你遇到问题一搜就能找到答案。虽然它定位是Wi-Fi+BLE双模芯片,但做HID键盘时我们只跑BLE,把Wi-Fi彻底关掉,功耗也能压到可接受范围。对于“想折腾、想完全自定义”的DIY玩家,ESP32-C3是目前性价比最高的选择。
1.2 ESP32-C3的核心硬件资源
ESP32-C3用的是RISC-V单核处理器,主频最高160MHz,对于键盘扫描、HID报文组装这种轻量任务来说性能绰绰有余。它内置了BLE 5.0,支持长广播、扩展广播等功能,射频性能在室内环境下实测能穿一堵墙,办公场景完全够用。
常用开发板是SuperMini或者合宙的C3开发板,引脚引出比较全,板载USB口可以用来烧录和供电。GPIO方面,做一个小键盘(比如6键宏键盘、数字小键盘)完全够用。就算做全键位矩阵键盘,C3的GPIO数量也能覆盖——常见的4x4矩阵用8个GPIO就能实现16个按键,5x6矩阵用11个GPIO就能覆盖30键,日常DIY妥妥的。
1.3 这个方案能实现什么效果
我用这套方案做了一个20键的小键盘,包含数字区、回车、退格、音量控制和几个自定义宏键。连上电脑后系统直接识别为“蓝牙键盘”,不需要装任何驱动,手机、平板、电脑通吃。因为是标准的HID协议,所以不光是打字,媒体键、音量键、甚至开机进入BIOS时都能用(前提是电脑BIOS支持蓝牙键盘,这个后面会讲)。
和市面上成品蓝牙键盘相比,自制的最大优势就是“键位我说了算”。想把某个按键映射成组合键Ctrl+C?改一行代码的事。想做一个“一键打开终端并输入特定命令”的宏键?代码里写好字符串发送逻辑就行。这种自由度是成品键盘给不了的,也是DIY的核心乐趣。
2. 硬件准备与电路设计
2.1 物料清单
做这个项目需要的东西不多,大部分都是几块钱的零件:
- ESP32-C3开发板(SuperMini或任意C3核心板,注意选引出GPIO多的版本)一块
- 机械轴或轻触按键,数量看你需要的键位
- 二极管1N4148或1N5819,数量与按键数量相同
- 10k欧姆电阻若干(如果打开内部上拉可以省掉)
- 面包板、洞洞板或PCB(面包板适合原型验证)
- 导线若干、USB线一根(供电和烧录)
- 可选:3D打印外壳、锂电池、充电模块(做便携版)
按键用机械轴的话手感更好,Gateron、Cherry轴都行,但需要配轴座或直接焊到洞洞板上。如果只是想快速验证功能,用几个轻触开关在面包板上搭一个最小系统就够跑通逻辑了。
2.2 矩阵键盘的原理
按键一多,一个按键占一个GPIO的方式就浪费引脚了。矩阵扫描的原理是:把按键排列成M行N列的网格,行线接GPIO输出,列线接GPIO输入。扫描时逐行拉低(或拉高),然后读列的电平变化,就能判断是哪个按键被按下。
我自己用的20键布局是4行×5列,一共占9个GPIO。如果是全键盘104键,用12×8矩阵也才20个GPIO,仍然在C3的能力范围内。矩阵方案是所有键盘主控的通用做法,省引脚、原理简单,唯一的坑是“鬼键”问题——当三个按键围成一个矩形时,第四角会被误判为按下。解决方案就是给每个按键串联一个二极管,利用二极管的单向导通特性切断反向通路。所以物料清单里的二极管不是可选项,矩阵键盘必须有,不然会出现按键错乱。
2.3 接线图与上拉配置
接线思路:定义行引脚和列引脚,每个按键的一端接行线、另一端经过二极管接列线。二极管方向要统一,比如都从行到列导通,这样扫描信号不会被串到其他行列。列引脚设置为输入模式并打开内部上拉,行引脚设置为输出模式,默认输出高电平,扫描时依次将某一行拉低,然后读所有列的电平。被按下的按键会把对应列的电平拉低,就能准确定位键位。
我实测过,直接使用ESP32-C3的内部上拉电阻就能稳定工作,不需要外接上拉电阻。但要注意:C3的内部上拉阻值较高,在面包板环境干扰大时可能会出现误触发,这时给列引脚外接10kΩ下拉或上拉电阻能提高稳定性。另外,全部GPIO走线尽量短,不要和电源线并行走,减少耦合干扰。
3D打印外壳这一步可以后置,先把电路在面包板上跑通,确认键位和扫描逻辑没问题再考虑外壳,不然一次做太多事,出了问题不好排查。
3. 固件编写与BLE HID核心逻辑
3.1 BLE HID协议栈是怎么工作的
BLE HID的本质是把蓝牙设备模拟成一个标准的HID设备,比如键盘、鼠标、游戏手柄。BLE协议栈里有个GATT(通用属性协议)层,HID设备就是通过GATT服务来暴露输入数据的。核心的Profile包括HID Service、Battery Service和Device Information Service,其中HID Service里面最重要的两个特征值是HID Report Map和HID Report。
HID Report Map描述的是“这个设备是键盘还是鼠标、有哪些按键、报告的长度是多少”,相当于告诉主机“我是一把键盘”。真正的按键数据通过HID Report特征值上报,比如标准键盘每按一次就发送一个8字节的报告——第1字节是修饰键(Ctrl、Shift、Alt、Win),第2字节是保留字节,第3到第8字节是当前HID键码。这里说的键码是HID Usage ID,比如字母A对应0x04,字母B对应0x05,和ASCII码不是一回事。
在建连时,BLE HID设备还有一个“Boot Mode”的概念。这个模式是用最简化的HID报告格式,不需要主机的HID驱动做复杂解析。很多电脑的BIOS只认Boot Mode的蓝牙键盘,所以如果你想在开机时用这键盘进BIOS,必须在代码里先初始化并进入Boot Mode,否则进系统后才能识别。
3.2 ESP32-C3上跑BLE HID的代码结构
我用的是Arduino框架,配合Espressif官方的BLE库和HID库。Arduino框架把ESP-IDF的很多细节封装好了,对DIY玩家非常友好。代码核心就四块:初始化BLE、创建HID设备、扫描矩阵按键、发送HID报告。
第一步,初始化BLE并设置设备名称,比如“ESP32 Keyboard”。广播参数里要带上HID服务的UUID,这样手机或电脑的蓝牙列表里会显示它是一个键盘图标而不是未知设备。初始化HID设备时,要注册Report Map、设置输入报告回调,并调用hidDev.setBootMode(0)让设备进入Boot Mode。
第二步是写报告描述符。标准键盘的报告描述符大概几十个字节,定义了输入报告的各个字段。这部分我直接从USB HID规范里抄的标准描述符,你完全可以照用,只要不改动字段定义就不用动它。
第三步才是真正和硬件打交道的地方——按键扫描。扫描函数要处理好防抖、组合键、连续按键的情况。后面细说。
3.3 矩阵按键扫描与防抖
按键扫描的逻辑不复杂,但要做好细节。我用的扫描函数框架如下:
// 定义行列引脚 const int ROW_PINS[] = {4, 5, 6, 7}; // 行引脚 const int COL_PINS[] = {8, 9, 10, 11, 12}; // 列引脚 // 键位映射表:0xFF表示无对应按键,其他值为HID键码 const uint8_t keymap[4][5] = { {0x52, 0x54, 0x55, 0x56, 0x57}, // 上、下、左、右、回车 {0x59, 0x5A, 0x5B, 0x5C, 0x5D}, // 数字1~4、退格 {0x04, 0x05, 0x06, 0x07, 0x08}, // A~D {0x09, 0x0A, 0x0B, 0x0C, 0x2A}, // E~H、删除 }; uint8_t readMatrix() { for (int r = 0; r < 4; r++) { pinMode(ROW_PINS[r], OUTPUT); digitalWrite(ROW_PINS[r], LOW); for (int c = 0; c < 5; c++) { if (digitalRead(COL_PINS[c]) == LOW) { return keymap[r][c]; } } digitalWrite(ROW_PINS[r], HIGH); pinMode(ROW_PINS[r], INPUT); } return 0; }这段代码的思路:依次把每行拉低,检查所有列引脚,只要某列变成低电平,就说明该行该列的按键被按下。扫描到按键后,从keymap里取出对应的HID键码。
防抖是关键,机械按键在按下和释放的瞬间会产生几十毫秒的电平抖动,不处理就会出现一次按键发送多次、或者按一下没反应的情况。我用的软件防抖方案是:第一次检测到电平变化时不立即上报,而是延时10~20ms后再读一次,确认电平稳定了才发送。这个方案简单实用,不需要外部硬件滤波。
扫描周期也要控制好。我在主循环里每10ms扫描一次矩阵,加上20ms的防抖确认,整体的响应速度在30ms左右,人眼完全感觉不到延迟,和成品键盘的手感差距不大。
3.4 组合键、修饰键和NKRO
键盘报告的前两个字节专门用来放修饰键,比如Ctrl、Shift、Alt、Win(对应位是bit0~bit3)。要发送Ctrl+C,就需要在报告里同时把Control修饰键位置1,再把C的键码(0x06)放到键码区。
BLE HID的普通报告一次最多带6个按键的键码。这意味着理论上你最多同时按6个不同按键,足以覆盖正常打字场景。游戏场景下可能需要同时按更多键,这就是所谓的NKRO(全键无冲)。但标准BLE HID在Boot Mode下面不支持NKRO,只有部分专业游戏键盘通过私有协议实现。DIY场景下,除非你要打音游或者某些需要多键同按的游戏,否则6键无冲完全够用。如果要追求更高的同按数量,可以尝试修改报告描述符把按键字段扩展到14字节以上,但很多操作系统和HID驱动不一定认,兼容性会变差,建议别折腾。
修饰键的处理是另一个容易踩坑的地方。我一开始只发送HID键码,结果Ctrl+C发出去变成了单独按C。后来才发现,修饰键必须和普通按键在同一个报告里发送,主机才能解析成组合键。我在代码里维护了一个modifierKey变量,扫描到Ctrl键时把它对应的位赋值,同时把其他按键放入键码区,组装成完整的8字节报告一起发送。
4. 烧录、调试与常见问题
4.1 烧录失败的排查思路
ESP32-C3的烧录相比老ESP32还多了一些坑,很多人在第一步就卡住。最常见的现象是烧录时报A fatal error occurred: Failed to connect to ESP32-C3: No serial data received。
原因和解决方式按概率排序:
- 没有进入下载模式。C3需要按住BOOT按键再上电或复位,才能进入串口下载模式。用Arduino IDE烧录时,先按住BOOT键,点烧录,看到“Connecting...”时松开。国产开发板很多没有自动下载电路,这个步骤绕不开。
- 串口驱动没装。C3的USB转串口芯片很多是CH340或CP2102,需要安装对应驱动。如果电脑识别不到串口,多半是驱动问题。
- 端口选错。拔掉其他USB串口设备,只保留C3,重新打开设备管理器看端口号。
- 烧录速度太高。在Arduino IDE或esptool里把烧录速度降到115200,有些劣质USB线在高速下载时不稳定。
你如果反复尝试都不行,换一根短一点的USB线试试。USB线的供电和数据线质量直接影响下载成功率,这个问题我排查了整整一晚上才找到,教训深刻。
4.2 设备搜不到、连不上、爱断连
烧录成功后,第一次搜索蓝牙设备搜不到,或者连上了几分钟就断开,这几乎是每个人都遇到过的问题。
搜不到设备时先检查广播参数。确保广播包里包含了HID服务的UUID,并且广播类型设置为可连接广播。另外,ESP32-C3默认可能会同时广播Wi-Fi信息,确认Wi-Fi已经被关掉了,否则广播包包含不必要的信息,干扰HID设备发现。
连接后频繁断开多半和两方面有关:一是电源不稳定。C3在BLE广播和连接事件期间电流会有明显波动,如果你用的是电池或者劣质USB供电,电压跌落会导致射频模块重启。给板子加一个大电容(100μF电解电容)或者换一个稳定的5V电源就能解决。二是连接参数不匹配。ESP32-C3默认的连接间隔可能偏长,造成主机认为设备无响应。我在代码里把最小连接间隔设为15ms,最大30ms,从机延迟设为0,这样连接稳定性好很多,同时也不至于太耗电。
还有一个很常见的场景是手机连得上、电脑连不上,反过来也一样。这通常是电脑蓝牙适配器对Boot Mode报告解析的兼容性问题。我个人的经验是Windows 10/11的蓝牙驱动有时候在设备首次配对后需要重启蓝牙适配器才能正确识别HID服务,笔记本的Fn键开关蓝牙也会影响搜索。
4.3 功耗优化实测
做无线键盘,功耗是绕不开的痛点。ESP32-C3跑BLE在不做任何优化时,工作电流大概在80~120mA,这显然不能接受。我优化后把整机平均电流压到了5mA以下,两节AA电池能用一个月以上。
优化的核心就几点:
- 彻底关闭Wi-Fi。只跑BLE时调用
WiFi.mode(WIFI_OFF),可以省掉很大一部分射频功耗。 - 降低CPU频率。键盘扫描不需要160MHz,把主频降到40MHz甚至20MHz,对功耗改善非常明显。
- 进入modem sleep模式。C3在待机时会自动进入modem sleep,射频部分关闭,只有BLE协议栈定时醒来监听广播或连接事件。实测modem sleep期间电流只有1mA左右。
- 按键唤醒。给按键对应的GPIO配置唤醒功能,键盘空闲时进入深度睡眠,按下任意键才唤醒,这样键盘放着不用的功耗可以降到几十微安级别。
我实测不同状态下的电流数据如下表:
| 状态 | 平均电流 |
|---|---|
| 无优化、Wi-Fi开启 | 约110mA |
| 关闭Wi-Fi、默认主频 | 约35mA |
| 关闭Wi-Fi、40MHz、modem sleep | 约5mA |
| 深度睡眠+GPIO唤醒 | 约50μA |
如果你做的是便携键盘,强烈建议加上深度睡眠。唤醒后BLE重连需要1~2秒,打字时完全能接受,但换来的续航提升非常可观。
4.4 用BLE调试工具验证HID报告
写完代码烧录后,需要验证HID报告有没有正确发出。我常用的是手机上的nRF Connect应用,它能看到设备所有的GATT服务和特征值,也能手动读出HID Report的值。
调试方法:连上设备后,找到HID Service(UUID是0x1812),点开Report特征值,打开通知开关,然后按你的键盘按键,就能实时看到报告数据。比如按下字母A,应该能看到类似00 00 04 00 00 00 00 00的数据流。第一位是修饰键,第三位0x04就是A的HID键码。如果看到数据但没有按下对应功能,多半是HID键码映射错了。
电脑端还有一个更强大的工具是Wireshark搭配蓝牙适配器抓包,可以看到完整的BLE协议栈通信过程,包括广播、连接、ATT读写、HID报告上报,层次非常清晰。不过配置相对复杂,建议遇到疑难杂症才用,平时用nRF Connect就够。
4.5 常见问题速查表
| 问题表现 | 可能原因 | 解决办法 |
|---|---|---|
| 烧录时连接失败 | 没进下载模式 | 按住BOOT再上电 |
| 设备搜索不到 | 广播参数不对 | 检查HID UUID是否在广播包里 |
| 连接经常断开 | 供电不稳定 | 加电容或换稳定电源 |
| 按键按一下发两次 | 软件防抖不充分 | 延长防抖确认时间到20ms |
| 组合键不生效 | 修饰键和按键码不同包 | 在同一份报告里发送修饰键+键码 |
| 电脑识别为“未知设备” | HID描述符有误 | 用nRF Connect核对Report Map |
| 键盘不能进BIOS | 未启用Boot Mode | 初始化时调用setBootMode(0) |
5. 进阶玩法与经验总结
5.1 从“能用”到“好用”的几个扩展方向
键盘跑通基础功能只是第一步,DIY的乐趣在于按自己的需求不断迭代。我目前正在往这几个方向扩展:
一是加旋钮编码器。EC11旋钮的三根引脚接到空闲GPIO上,通过旋钮的正反转控制音量和页面滚动,按下去作为静音键。编码器是增量式的硬件输入,直接读GPIO电平变化就能判断方向,实现起来比按键扫描更简单,但交互手感提升非常明显。
二是加OLED屏幕。ESP32-C3支持I2C接口的0.91寸或0.96寸OLED,可以实时显示当前层数、大小写状态、电量等信息。显示驱动库选择U8g2,内存占用大约2KB左右,C3完全吃得消。在做宏键盘时,屏幕上显示每个按键当前的宏功能,就不用费劲记键位了。
三是做多按键层。现在的键盘固件大多支持多层键位,类似于键盘的Fn层、电竞模式层。按下某个切层键,整个键位映射表就切换成另一套,相当于用一个20键的小键盘实现了40键甚至60键的功能。层切换在代码里就是一个索引数组的切换,逻辑非常简单,但实用性极强。
四是加锂电池和充电模块。用TP4056充电模块加一块3.7V锂电池,把开发板接到电池正负极上,C3内部有LDO稳压,电压范围在2.8~3.6V之间都能稳定工作。再配合深度睡眠逻辑,一个真正的无线键盘就成型了。注意电池负极要接开发的GND,不要和USB共地混乱。
做得再深一点,可以给键盘加上USB HID模式。C3的USB外设能模拟成USB键盘,这样同一个硬件,蓝牙连不了的时候插线还能当有线键盘用。固件里做一个切换逻辑,检测到USB接入就自动切到USB HID模式,拔线后再切回BLE。
5.2 我踩过的几个坑,希望你绕开
写出来这几个坑,都是我实际遇到过、排查了很久才解决的,供大家参考:
第一个是关于GPIO的复用功能。C3很多GPIO同时是SPI、I2C、UART、USB、ADC的复用引脚,如果某个按键总是触发异常,先查一下它是不是被复用了。我遇到过把按键接到GPIO18上,结果每次扫描都会触发一次莫名其妙的中断,查了半天才发现这个引脚默认绑定了USB D+功能,需要先调用pinMatrixOutAttach或配置IO MUX才能当普通GPIO用。
第二个是HID键码和键盘布局的关系。HID键码定义的是物理位置,不是字符。同样是0x04,在美式键盘上是字母A,在德式键盘上是A也就是同一个位,但如果你切换到俄语布局,物理位置对应的字符就会变。这个不是键盘固件的问题,是主机系统的键盘布局在起作用。如果你给中文用户做键盘,最好参考系统默认的美式布局来映射键位。
第三个是BLE重连的问题。C3在连接断开后默认不会自动重新广播,这就导致键盘闲置几分钟断开后,你想再用时必须手动按一下配对键重新进入广播状态。我在代码里加了一个检测逻辑:连接断开后立即重新开始广播,这样再次配对就不用按任何按键,体验和成品键盘基本一致。还有一个办法是让某个按键专门负责“断开并重新广播”,作为应急键。
5.3 最后分享一个实用技巧
调试期间最怕的就是反复烧录、反复拔线,很伤开发板的USB口。我的做法是:所有代码里都保留一个“自检模式”——按住某个指定按键再上电,板子会跳过BLE初始化,直接把当前扫描到的键值通过串口打印出来。这样排查矩阵接线、防抖参数都不用重新烧录,直接在串口监视器看数据就行。这个自检模式我每次做键盘都会加上,能省不少事。
如果你也准备入坑自制键盘,我的建议是:先从最小系统开始,6个按键、4行X3列先跑通,确认能在手机上打字成功,再往上面加功能。一次贪多容易陷入“哪里都坏了”的排查泥潭。把BLE HID的原理吃透,剩下的就是堆功能而已。