实操:ESP32 用 ESP-IDF 做蓝牙 HID 设备
【免费下载链接】esp-idfEspressif IoT Development Framework. Official development framework for Espressif SoCs.项目地址: https://gitcode.com/GitHub_Trending/es/esp-idf
按下实体按键,笔记本上的光标立刻跟着走——把 ESP32 当作一只蓝牙鼠标,背后靠的就是蓝牙 HID 协议和 ESP-IDF 这套乐鑫官方开发框架:ESP-IDF 把协议栈、HID 设备 API 和现成示例都准备好了,你负责的是把硬件接上、把描述符改对。
这份框架里已经有什么
- 完整协议栈:Bluedroid 与 NimBLE 两套可选,经典蓝牙 HID 走 Bluedroid。
- 现成 ESP-IDF HID 示例:
examples/bluetooth/bluedroid/classic_bt/bt_hid_mouse_device/下是一个能直接编译烧录的鼠标设备。 - HID 设备 API:
esp_hidd_api.h覆盖注册、配对、报告收发、协议模式切换全流程。 - 电源管理:自动浅睡眠、深度睡眠、ESP32 低功耗蓝牙射频调参一应俱全,为电池供电设备留足空间。
从克隆到烧录:一条命令链跑通
下面这条命令链依次完成取代码、装工具链、建环境、编译烧录、看日志五件事,中间不用切换任何工具:
git clone https://gitcode.com/GitHub_Trending/es/esp-idf cd esp-idf && ./install.sh # 自动安装交叉编译工具链 . ./export.sh # 加载 idf.py 等环境变量 # 示例位于 examples/bluetooth/bluedroid/classic_bt/bt_hid_mouse_device/ idf.py -p /dev/ttyUSB0 flash monitor烧录成功后,设备以HID Mouse Example的名字开始广播,你在主机的蓝牙设置里搜到它、点连接、完成配对,鼠标指针就会自动移动。整个示例的默认配置写在sdkconfig.defaults里,编译时自动生效,经典蓝牙和 HID 设备支持已经替你打开:
CONFIG_BT_CLASSIC_ENABLED=y CONFIG_BT_HID_ENABLED=y CONFIG_BT_HID_DEVICE_ENABLED=y从架构上看,应用代码只和 Bluedroid 打交道,协议栈再往下通过 VHCI 接口把命令交给片内蓝牙控制器,射频细节完全不暴露给你的代码:
数据到底怎么交换
报告描述符:先声明,再发数
HID 设备和主机的第一次「对话」不传数据,传的是蓝牙 HID 报告描述符——一份二进制声明,告诉主机这份设备能发什么。示例里的鼠标描述符声明了三个按键、X/Y 移动量和滚轮,主机就是靠它来拆解之后收到的每一字节:
uint8_t hid_mouse_descriptor[] = { 0x05, 0x01, // USAGE_PAGE (Generic Desktop) 0x09, 0x02, // USAGE (Mouse) 0xa1, 0x01, // COLLECTION (Application) 0x09, 0x01, // USAGE (Pointer) 0xa1, 0x00, // COLLECTION (Physical) 0x05, 0x09, // USAGE_PAGE (Button) 0x19, 0x01, // USAGE_MINIMUM (Button 1) 0x29, 0x03, // USAGE_MAXIMUM (Button 3) 0x15, 0x00, // LOGICAL_MINIMUM (0) 0x25, 0x01, // LOGICAL_MAXIMUM (1) };想把鼠标改成 ESP32 蓝牙键盘或摇杆,动的就是这份描述符数组,发送报告的框架代码可以原样保留。
启动与自定义:两种协议的取舍
链路建立后,主机会把设备切到两种工作模式之一,设备端在ESP_HIDD_SET_PROTOCOL_EVT回调里感知切换、调整后续报告格式:
case ESP_HIDD_SET_PROTOCOL_EVT: if (param->set_protocol.protocol_mode == ESP_HIDD_BOOT_MODE) { s_local_param.x_dir = -1; // 按启动模式固定布局回发 } else if (param->set_protocol.protocol_mode == ESP_HIDD_REPORT_MODE) { // 按自己的描述符格式回发 } s_local_param.protocol_mode = param->set_protocol.protocol_mode; break;| 模式 | 适用场景 | 一句话差异 |
|---|---|---|
| 启动模式 | 兼容性优先的键盘、鼠标 | 字节布局由规范固定,无需描述符,主机随时可切换 |
| 自定义报告模式 | 滚轮、多媒体键等扩展功能 | 布局由你的描述符决定,表达力更强,默认启用 |
示例代码在两种模式下分别处理报告编号和报告长度,这段切换逻辑可以直接照抄。
把平均电流压下去
广播间隔:单位 0.625 毫秒,从 100 ms 拉到 2048(约 1.28 秒),发射占空比直接掉一个数量级以上;代价是主机扫描列表里出现的速度变慢,对遥控器这类设备无所谓。
深度睡眠:空闲期整芯片掉到微安级电流,按键 GPIO 唤醒后恢复广播;代价是唤醒到可配对有几百毫秒空窗。
从机连接间隔:射频忙闲比由它主导,调大更省电,按键手感却更钝,需要在续航和响应之间取舍。
ESP-IDF 的电源管理用esp_pm_configure()统一入口,把最小主频和自动浅睡眠声明出来即可:
esp_pm_config_t pm_cfg = { .max_freq_mhz = 240, .min_freq_mhz = 10, .light_sleep_enable = true, }; esp_pm_configure(&pm_cfg);配合任务调度,让报告任务之外没有常驻高优先级工作,空闲时间就会自然落进睡眠窗口。
从 Demo 到货架
描述符改完,产品形态就出来了:ESP32 蓝牙键盘、多轴游戏手柄、媒体遥控器、带加速度计的体感控制器,走的都是同一条「描述符 + 输入报告」路线,差别只在字节布局。比如一个多媒体遥控器,用一个字节位域就能装下音量加减、播放暂停、切歌和电源六个键位,再配一颗 GPIO 接的 LED 做状态反馈,硬件成本几乎可以忽略。
安全配对有四种手段:Just Works:无交互确认,体验最顺、强度最弱;Passkey Entry:用户输入六位数字,强度中等;Numeric Comparison:两端比对同一个数字,适合有屏幕的设备;Out of Band:通过 NFC 等旁路交换密钥,强度最高。ESP32 的经典蓝牙链路可以同时维持多条连接,多主机场景下为每条连接独立保存一份报告状态即可,信道切换由协议栈代劳。
踩坑速查
| 现象 | 排查方向 | 处理动作 |
|---|---|---|
| 设备在扫描列表里始终不出现 | 可发现模式与设备名 | 日志里确认esp_bt_gap_set_scan_mode()已把设备置为可发现、名字非空 |
| 配对比键成功,光标却不动 | 报告编号与长度 | 对照描述符,检查两种协议模式下报告 ID 和长度是否一致 |
| 连接频繁中断 | 射频拥塞与供电 | 换数据线、远离 2.4 GHz 干扰源,复测是否稳定 |
| 按键后指针延迟明显 | 任务优先级与报告发送周期 | 提高报告任务优先级,避免跨任务长时间持锁 |
| 续航远低于预期 | 广播占空比与睡眠策略 | 逐项核对广播间隔、浅睡眠开关和从机连接间隔 |
接着往哪走
仓库内先看这三处:经典蓝牙全部示例在 examples/bluetooth/bluedroid/classic_bt/,协议栈实现在 components/bt/,通用 HID 设备 API 在 components/esp_hid/。想继续深入,可以换成更轻量的 NimBLE 栈、研究 BLE Mesh 组网,或者做 Wi-Fi 与经典蓝牙的双模共存。
下一步:打开examples/bluetooth/bluedroid/classic_bt/bt_hid_mouse_device/main/main.c,把描述符数组和设备名改成你自己的设备类型,编译烧录验证主机能否正确识别。
【免费下载链接】esp-idfEspressif IoT Development Framework. Official development framework for Espressif SoCs.项目地址: https://gitcode.com/GitHub_Trending/es/esp-idf
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考