1. 从一个戒指说起:为什么我要用 ESP32 做 BLE 鼠标
第一次冒出“把鼠标戴在手指上”这个念头,是在一次给客户做演示的时候。当时我一手拿着开发板,一手在笔记本触控板上划来划去,翻页、点选、拖拽,动作别扭到连自己都嫌弃。演示结束我就在想,能不能做一个戴在手指上的小环,通过手势就能控制光标?回家翻了一圈市面上的指环鼠标,要么是价格劝退,要么是封闭生态没法自己改。既然手头常年堆着 ESP32,那干脆自己造一个。
这个项目我给它起名叫ESP32 BLE Ring Mouse,核心思路非常直接:用 ESP32 作为主控,通过BLE(低功耗蓝牙)模拟一个标准HID 鼠标设备,主机(电脑、平板、手机)把它当成普通蓝牙鼠标来识别,不需要装任何驱动。戒指本体上放一个按键或者触摸传感器,配合姿态数据,实现移动光标、单击、拖拽这些基础操作。Part 1 我先把“能被电脑认出来的 BLE 鼠标”这条链路打通,也就是先让 ESP32 变成一个合法的蓝牙鼠标,后面 Part 2 再叠加姿态识别和戒指形态的硬件。
为什么选 ESP32 而不是别的芯片?几个很现实的原因。第一,ESP32 自带BLE 4.2 及以上的射频和协议栈,乐鑫的 ESP-IDF 里 HID 相关的例程和 API 都比较成熟,不用自己从零撸蓝牙协议。第二,它的算力对于处理传感器数据、跑简单的姿态解算绰绰有余,双核 240MHz 足够。第三,开发板便宜、资料多,烧录方式也简单,esptool一把梭。第四,社区里关于ble鼠标uuid、蓝牙app控制esp32的讨论很多,遇到坑基本能搜到线索。
这篇文章适合谁看?如果你已经会用 Arduino 或者 ESP-IDF 点个灯、串口打印个“hello”,想进一步玩蓝牙 HID,那这篇就是给你写的。如果你完全没碰过 ESP32,建议先去看esp32教程和arduino esp32开发环境的搭建,把esp32烧录这一步跑通再回来。整篇我会把“为什么这么设计”“参数怎么算”“坑在哪”讲透,代码和配置尽量给到能直接抄的程度。
需要提前说明的是,Part 1 的目标是功能验证,不是最终形态。我会用一块普通的 ESP32 开发板加一个轻触按键来演示,戒指的机械结构和姿态传感器放到 Part 2。你跟着做,最后能得到一个能被 Windows、macOS、Linux 甚至部分安卓设备识别为鼠标的蓝牙设备,按一下按键就能触发一次左键单击。
2. 整体方案设计与关键技术选型
2.1 为什么是 BLE HID,而不是经典蓝牙或 2.4G
做无线鼠标,摆在面前的路其实有三条:经典蓝牙(BR/EDR)的 HID、BLE 的 HID over GATT、以及自研的 2.4G 私有协议配 USB 接收器。我选 BLE,理由得说清楚。
经典蓝牙 HID 确实是最“正统”的鼠标协议,兼容性极好,但它的功耗偏高,而且 ESP32 的经典蓝牙协议栈在 Arduino 环境下支持不如 BLE 完善,配置起来更折腾。2.4G 私有协议方案延迟最低、最省电,但必须配一个 USB dongle,用户得占用一个 USB 口,而且跨平台(尤其是平板和手机)基本没戏,这跟“戒指鼠标随手连”的初衷相悖。
BLE HID over GATT 是折中里最优的:免驱、跨平台、低功耗。现代操作系统原生支持 BLE HID,Windows 10 以后、macOS、iOS、Android、Linux 都能直接识别。你搜ble鼠标uuid会看到一堆标准 UUID,这就是 HID over GATT 规范定义好的,主机认这些 UUID 就知道你是个鼠标。代价是 BLE 的连接间隔(Connection Interval)最小一般是 7.5ms,理论延迟比 2.4G 高一点,但对于非电竞场景完全够用。
提示:BLE HID 的延迟很大程度取决于主机协商的连接间隔。iOS 和 macOS 通常会给比较激进的参数,Windows 有时会保守一些。如果你后面做实时性要求高的场景,可以在连接参数更新请求里主动争取更小的间隔。
2.2 HID over GATT 的核心:那几个必须记住的 UUID
BLE 的一切都围绕 GATT(通用属性配置文件)展开。简单类比:GATT 就像一棵服务树,每个“服务(Service)”下面挂着若干“特征(Characteristic)”,特征里存着实际的数据。鼠标要工作,就得按 HID 规范把这棵树搭对。
HID 服务本身的标准 UUID 是0x1812。在这个服务下,有几个关键特征:
| 特征名称 | UUID | 作用 | 属性 |
|---|---|---|---|
| HID Information | 0x2A4A | 声明 HID 版本、国家码等 | Read |
| Report Map | 0x2A4B | 描述报告的数据格式(核心) | Read |
| HID Control Point | 0x2A4C | 控制挂起/恢复 | Write Without Response |
| Report | 0x2A4D | 实际传输鼠标数据 | Read/Write/Notify |
| Protocol Mode | 0x2A4E | 报告协议或启动协议 | Read/Write |
其中Report Map是最关键也最容易踩坑的地方。它是一段字节流,用 HID 描述符语言写成,告诉主机“我发过来的数据包长这样:第一个字节是按键,第二三字节是 X 位移,第四五字节是 Y 位移……”。主机解析了这段描述符,才知道怎么解读你后面通过 Report 特征发来的数据。写错了,主机要么不认,要么把光标搞得乱飞。
2.3 报告描述符的设计取舍
一个最简鼠标报告描述符,通常包含按键(Buttons)、X/Y 相对位移、滚轮(Wheel)。我在 Part 1 里只做按键加位移,滚轮先留着,因为戒指形态下滚轮交互还没想好怎么映射。
报告格式我定为4 字节:[buttons, x_low, x_high, y_low, y_high]其实是 5 字节,但为了对齐和后续扩展,我实际用了标准的 4 字节布局——按键 1 字节,X 和 Y 各用有符号 8 位。为什么不用 16 位位移?因为戒指鼠标单次移动幅度小,8 位(-127 到 127)足够,而且包更短、发送更快。如果你后面要做快速甩动,可以改成 16 位,代价是每个包多 2 字节。
这里有个经验:位移字段一定要用相对坐标(Relative),不要用绝对坐标(Absolute)。鼠标是相对设备,主机根据你每次上报的增量来移动光标。如果你错写成绝对坐标,光标会瞬间跳到某个固定位置,完全没法用。描述符里对应的是Logical Minimum (-127)、Logical Maximum (127),并且 Input 项的标记不带Absolute。
2.4 硬件选型与引脚规划
Part 1 的硬件极简:
- 一块 ESP32 开发板(我用的是 ESP32-WROOM-32 核心板,
esp32引脚布局通用) - 一个轻触按键,接在 GPIO0 上(很多板子自带 BOOT 按键,可以直接借用)
- 一根 USB 线供电和烧录
选 GPIO0 是因为大多数开发板的 BOOT 键就接在这里,省得额外接线。但要注意,GPIO0 在启动时是 strapping 引脚,上电瞬间的电平会影响启动模式。所以按键要接成“按下拉低、松开由外部上拉拉高”的形式,且不能在上电时被按住,否则芯片会进下载模式。这个坑我在esp32外部中断实战里踩过,后面会细说。
供电方面,BLE 发射瞬间电流会有尖峰,USB 口一般能扛住,但如果用电池供电,记得在电源脚旁边并一个 100uF 以上的电容,不然容易复位。
3. 开发环境搭建与工程配置
3.1 Arduino 还是 ESP-IDF,我为什么两个都提
这个问题被问过无数次。我的建议是:想快速验证用 Arduino,想深入控制和省资源用 ESP-IDF。
Arduino 环境下有现成的BLEMouse库(属于 ESP32 BLE Arduino 库的一部分),几行代码就能把鼠标跑起来,非常适合 Part 1 这种功能验证。缺点是库封装得比较厚,你想改报告描述符或者精细控制连接参数时,得翻库源码。
ESP-IDF 环境下用esp_hid组件或者直接操作esp_gatts,控制粒度最细,但代码量大,新手容易被 GATT 回调、事件循环绕晕。
Part 1 我主推 Arduino 路线,因为它能让你在半小时内看到光标动起来,成就感来得快。ESP-IDF 的版本我会在关键处给出对应思路。
3.2 Arduino ESP32 环境的国内源配置
arduino安装esp32这一步,国内网络直接走官方源经常卡到怀疑人生。开发板管理器里那个 JSON 地址,建议换成国内镜像。具体操作:打开 Arduino IDE 的“首选项”,在“附加开发板管理器网址”里填入国内镜像的 package index 地址(搜esp32 arduino阿里巴巴国内镜像源或esp32国内源能找到当前可用的地址,镜像地址会变,我不写死)。
然后在开发板管理器里搜索esp32,安装esp32 by Espressif Systems。版本选择上,esp32 arduino 3.3.11这类较新的版本对 BLE 库的支持比较稳定。如果你网络实在差,可以找arduino esp32 3.3.11 完整离线包 windows这种离线包,解压到 Arduino 的 hardware 目录下,省去下载。
注意:离线包解压后目录结构要对,通常是
Arduino/hardware/espressif/esp32,里面要有boards.txt、platform.txt这些文件。放错层级 IDE 是认不出来的。
3.3 开发板参数设置
烧录前,工具菜单里几个参数必须设对:
- 开发板:选
ESP32 Dev Module(对应 ESP32-WROOM-32) - 上传速度:
921600比较快,如果烧录失败就降到115200 - Flash 大小:按你的板子选,常见
4MB - 分区方案:默认即可,BLE 库占的空间不小,别选太抠的分区
- 端口:选对 COM 口(Windows)或
/dev/ttyUSB0(Linux)
如果你用的是 ESP32-C3 或 S3,开发板要选对应型号。esp32 c3 mini版原理图和esp32 s3的引脚定义跟经典 ESP32 不同,BLE 库的兼容性也略有差异,Part 1 我以经典 ESP32 为准。
3.4 烧录方式与常见失败处理
esp32烧录失败是新手第一道坎。常见原因和排查:
| 现象 | 可能原因 | 解决 |
|---|---|---|
| 找不到端口 | 驱动没装 | 装 CP210x 或 CH340 驱动 |
| 一直 Connecting... | 没进下载模式 | 按住 BOOT 再按 EN 松开,或手动拉低 GPIO0 |
| 烧录到一半失败 | 波特率太高 | 降到 115200 |
| 烧录成功但不运行 | 分区/Flash 设置错 | 检查 Flash 大小和分区方案 |
esp32烧录器如果是外接的,注意 TX/RX 要交叉接,且烧录时 GPIO0 拉低。用开发板自带的 USB 转串口最省事。
4. 核心代码实现:让 ESP32 变成一个鼠标
4.1 最小可运行代码框架
Arduino 环境下,核心就是BLEMouse这个类。下面是我验证用的最小代码,能直接编译烧录:
#include <BleMouse.h> BleMouse bleMouse("ESP32 Ring Mouse", "Espressif", 100); const int BUTTON_PIN = 0; int lastButtonState = HIGH; void setup() { Serial.begin(115200); pinMode(BUTTON_PIN, INPUT_PULLUP); bleMouse.begin(); Serial.println("BLE Mouse started, waiting for connection..."); } void loop() { if (bleMouse.isConnected()) { int currentState = digitalRead(BUTTON_PIN); if (lastButtonState == HIGH && currentState == LOW) { bleMouse.click(MOUSE_LEFT); Serial.println("Left click sent"); } lastButtonState = currentState; } delay(10); }这段代码做了几件事:创建一个名为 “ESP32 Ring Mouse” 的 BLE 鼠标设备,初始化按键为上拉输入,主循环里检测按键下降沿并发送一次左键单击。bleMouse.click(MOUSE_LEFT)内部会先按下再松开,模拟一次完整点击。
4.2 报告描述符在库里的位置与修改方法
BLEMouse库默认的报告描述符已经包含了按键、X/Y 位移和滚轮,符合大多数场景。如果你想改,得去库目录里找BleMouse.cpp,里面有一个hidReportDescriptor数组。改这个数组就是改报告格式,改完记得同步改发送数据的代码,否则主机按新描述符解析、你按旧格式发,数据就错位了。
我建议 Part 1 先别动描述符,用默认的跑通再说。等你确认连接、点击都正常了,再回来定制。这是典型的“先跑通再优化”思路,能帮你把问题范围缩小。
4.3 连接参数与设备名称的讲究
BleMouse构造函数三个参数分别是设备名、制造商名、电池电量。设备名会显示在系统的蓝牙列表里,起个好认的名字很重要,不然一堆 “ESP32” 你分不清哪个是哪个。
连接参数方面,库默认会请求一个连接间隔。如果你发现光标响应有延迟,可以在库源码里找connParams相关的设置,把minInterval和maxInterval调小。但要注意,主机不一定接受你的请求,最终参数是双方协商的结果。
提示:电池电量参数填 100 只是占位,Part 1 没有电量检测。如果你后面加了电池,可以接个分压电阻到 ADC 引脚,定期更新这个值,系统蓝牙设置里就能看到电量。
4.4 按键消抖与中断的取舍
上面代码用的是轮询加delay(10),简单但不够优雅。delay会阻塞,如果后面要同时处理传感器数据,就得改成非阻塞。更专业的做法是用外部中断,按键接 GPIO0,配置下降沿触发,在 ISR 里置个标志位,主循环处理。
但 GPIO0 做中断有个坑:它是 strapping 引脚,且很多板子上电时如果按键被按住会进下载模式。所以实际产品里我会换一个普通 GPIO,比如 GPIO4 或 GPIO5。esp32外部中断实战里提到,ISR 里不要做耗时操作,发 BLE 数据这种活儿必须放到主循环,否则会触发看门狗复位。
消抖方面,机械按键抖动一般 5-20ms,软件消抖可以在检测到下降沿后延时 20ms 再确认电平。硬件消抖就是并一个 0.1uF 电容,效果更稳。
5. 配对、测试与问题排查实录
5.1 各平台配对流程与差异
烧录完成后,ESP32 会广播自己。打开电脑蓝牙,搜索 “ESP32 Ring Mouse”,点击配对。不同平台表现不一样:
- Windows 10/11:配对后自动安装 HID 驱动,任务栏会出现鼠标图标。有时需要等几秒。
- macOS:配对后直接在“蓝牙”偏好设置里显示为鼠标,光标立即可用。
- Linux:用
bluetoothctl配对,pair、trust、connect三步走。 - Android:部分机型支持 BLE HID,配对后光标出现,但安卓对鼠标的支持因厂商而异。
mac mouse fix这类工具是给普通鼠标调参数的,对 BLE HID 设备同样适用,如果你觉得指针速度不合适,可以在系统设置里调,或者用这类工具做更细的加速曲线。
5.2 常见问题速查表
| 问题 | 排查方向 | 解决 |
|---|---|---|
| 搜不到设备 | 广播没起来 | 检查bleMouse.begin()是否执行,串口有无报错 |
| 配对失败 | 描述符不合法 | 用默认描述符先验证,确认 UUID 正确 |
| 配对成功但光标不动 | Report 特征没 Notify | 检查库是否在连接后使能了 Notify |
| 点击无反应 | 按键电平逻辑反了 | 确认上拉/下拉,打印电平调试 |
| 光标乱飘 | 位移数据异常 | 检查是否误发了绝对坐标或数据错位 |
| 连接后频繁断开 | 供电不足或连接参数太激进 | 加电容,放宽连接间隔 |
| 烧录后无反应 | 进了下载模式 | 松开 GPIO0 重新上电 |
5.3 用蓝牙调试工具验证数据
ble蓝牙助手 小牛这类手机 App 可以扫描并连接 BLE 设备,查看服务和特征。用它连上你的 ESP32,找到0x1812服务,看0x2A4B(Report Map)能不能读出来,0x2A4D(Report)有没有 Notify。这能帮你确认 GATT 树搭对了没有,比盲猜高效得多。
如果 App 里能看到服务但读 Report Map 报错,多半是描述符数组有问题。如果连服务都看不到,那就是广播或 GATT 注册阶段就挂了,回去看串口日志。
5.4 我踩过的几个坑
第一个坑是描述符里的 Report ID。如果你的描述符用了 Report ID,那发送数据时第一个字节必须是 Report ID,否则主机解析全乱。默认库一般不用 Report ID,但你自己改的时候容易忘。
第二个坑是连接后没使能 Notify。BLE 的 Notify 需要主机主动写 CCCD(客户端特征配置描述符)来开启。库一般会自动处理,但如果你自己撸 GATT,忘了这一步,数据发了主机也收不到。
第三个坑是GPIO0 上电被按住。我有一次调试时按键卡住了,结果每次上电都进下载模式,串口一直打印等待下载的日志,排查了半天才发现是物理按键的问题。后来我换到 GPIO4,世界清净了。
第四个坑是多个 BLE 设备名字冲突。如果你同时开了好几个 ESP32 都在广播,系统蓝牙列表里一堆同名设备,配对时容易连错。给每个设备起唯一的名字,或者用 MAC 地址后缀区分。
6. 从 Part 1 到戒指形态的演进思路
Part 1 跑通后,你手里已经有一个能被识别的 BLE 鼠标了。接下来往戒指形态走,要解决的核心问题是姿态到光标位移的映射。
戒指鼠标不可能像普通鼠标那样在桌面上滑动,它靠的是手指的微小动作。常见方案是用 IMU(惯性测量单元,比如 MPU6050 或 BMI160)采集加速度和角速度,做姿态解算,把手指的倾斜或挥动映射成光标位移。这里涉及传感器融合、零漂处理、死区设置等一堆细节,esp32加差分rtk模块那种高精度定位思路在这里用不上,但姿态解算的数学基础是相通的。
另一个方向是用触摸传感器,比如 ESP32-S3 自带的触摸功能,esp32 s3触摸屏教程里的触摸采集思路可以借鉴。手指在戒指表面的滑动方向映射成光标方向,点击用轻触。这种方案更省电,但交互精度依赖触摸算法的调校。
低功耗也是戒指形态的刚需。esp32 s3 ardunio 睡眠低功耗里提到的 light sleep 和 deep sleep 策略,配合 BLE 的连接间隔拉长,能把续航从几小时拉到几天。但要注意,esp32 休眠 i2c复位这类问题——休眠唤醒后外设可能没复位,IMU 读不出数据,得在唤醒流程里重新初始化。
Part 1 的意义就在于,它把最不确定的“蓝牙链路”先锁死了。后面无论你加什么传感器、怎么改交互,底层都是一个已经被系统认可的鼠标,你只需要往 Report 里填数据就行。这种分层验证的思路,是我做所有嵌入式项目的一贯做法:先把通信打通,再堆功能,最后做形态。
我个人在实际操作中的体会是,BLE HID 这类项目,80% 的坑都在描述符和连接参数上,跟你的应用逻辑关系不大。所以 Part 1 花时间把 GATT 树和报告格式搞扎实,后面会省下大量返工。如果你在配对阶段就卡住了,别急着怀疑代码,先用蓝牙调试 App 把服务和特征看一遍,往往一眼就能看出问题在哪。