基于ESP32-C3的USB HID转蓝牙适配器:实现有线键鼠蓝牙化与宏功能(附指纹解锁方案)
本文详细介绍了如何利用ESP32-C3微控制器制作一个USB HID转蓝牙适配器,实现将有线鼠标、键盘等设备无线化,并支持通过网页配置宏功能。项目核心在于通过软件模拟USB主机,解析并翻译HID报告,最终通过蓝牙连接。文章深入剖析了其工作原理、硬件设计、固件编译指南,并展望了集成指纹解锁等进阶功能,为DIY爱好者和嵌入式开发者提供了完整的实现方案。
基于ESP32-C3的USB HID转蓝牙适配器:实现有线键鼠蓝牙化与宏功能(附指纹解锁方案)
最近在整理桌面,看着一堆缠在一起的有线鼠标键盘,还有那个专门用来打游戏的带宏功能的有线鼠标,总觉得不够清爽。有没有办法让这些老设备也“无线化”,还能保留甚至增强它们的自定义功能呢?于是,我动手做了一个小玩意儿——一个基于ESP32-C3的USB HID转蓝牙适配器。
这个小板子能干什么呢?简单说,它能把你的有线鼠标、键盘、甚至游戏手柄,通过蓝牙连接到电脑、平板或手机上,瞬间变成蓝牙设备。更棒的是,它还支持通过网页配置鼠标键盘宏,并且未来计划加入指纹解锁电脑的功能。听起来是不是很酷?今天,我就带你从零开始,深入理解这个项目的原理,并手把手教你如何复现它。
1. 项目概览:它到底能做什么?
在开始研究代码和电路之前,咱们先搞清楚这个适配器的核心功能,这样你才知道它是否适合你的需求。
核心功能清单:
- 有线转无线:通过两个USB-A母口,接入传统的有线鼠标、键盘等USB HID设备,让它们通过蓝牙与主机(电脑、手机等)连接。
- 双工作模式:
- 翻译模式 (TRANSLATE):对于常见的鼠标和键盘,适配器会智能解析其USB报告描述符,并将其数据“翻译”成标准的蓝牙HID报告格式。这是最理想、即插即用的模式。
- 直通模式 (PASSTHROUGH):对于游戏手柄等不常见或解析失败的设备,适配器会将原始的报告描述符和数据原封不动地转发给蓝牙主机。兼容性更强,但设置稍显复杂。
- 网页配置宏:你可以通过浏览器访问一个管理网页,为工作在翻译模式下的鼠标或键盘定义复杂的宏命令(比如一键连招、组合键等)。
- 电池管理:板子支持电池供电,也可以用USB口供电,或者通过USB口给电池充电,非常灵活。
- 指纹解锁(开发中):计划集成指纹模块,通过模拟键盘输入密码来实现Windows系统的指纹解锁。
- 无线管理:所有状态查看和宏配置,都通过蓝牙连接的管理网页完成,无需额外数据线。
硬件核心:ESP32-C3 整个项目的“大脑”是一颗乐鑫的ESP32-C3芯片。这是一款集成Wi-Fi和蓝牙5.0的RISC-V架构微控制器,性能足够,功耗也低,非常适合这种需要无线连接和复杂协议处理的应用。
2. 核心原理剖析:如何用GPIO“模拟”出USB主机?
这是本项目最硬核也最有趣的部分。ESP32-C3本身并没有USB主机(USB Host)控制器,那它怎么读取USB设备的数据呢?答案是:用GPIO引脚,通过软件模拟出一个低速USB主机。
2.1 软件模拟USB总线
这个功能基于开源项目 esp32_usb_soft_host。简单理解,就是通过程序精确控制两个GPIO引脚(一个模拟D+数据线,一个模拟D-数据线)的电平变化时序,来模拟USB低速模式(1.5 Mbps)的通信。
注意:这个软实现仅支持低速USB HID设备。如何识别呢?给设备上电后,如果D-线被拉高,就是低速设备;如果D+线被拉高,就是全速或高速设备。大部分鼠标和键盘都是低速设备,所以够用。
为了让这个软USB主机稳定工作,需要对ESP-IDF(乐鑫的开发框架)进行两个关键配置:
- 将编译优化等级设置为
O2。 - 关闭内存保护功能(
Component config -> ESP System Setting -> Memory protection)。这是因为模拟代码需要动态向可执行内存段写入指令。
配置好后,USB主机的核心逻辑在一个 1ms的定时器中断 中运行。我们来看看这个中断服务程序(ISR)的简化代码:
void IRAM_ATTR usb_process()
{
// 启用周期计数器(用于精确计时)
#if CONFIG_IDF_TARGET_ESP32C3
cpu_ll_enable_cycle_count();
#endif
// 遍历两个USB端口
for(int k=0; k<NUM_USB; k++)
{
current = ¤t_usb[k];
if(current->isValid) // 如果该端口有设备
{
setPins(current->DP, current->DM); // 切换到对应GPIO
timerCallBack(); // 处理NRZI编码/解码
fsm_Mashine(); // 更新USB状态机
}
}
}
每个1ms,程序会检查每个USB端口,并执行两步操作:timerCallBack 根据上一周期的状态进行实际的数据位读写,fsm_Mashine 则更新整个USB枚举和数据传输的状态机。
2.2 精准的微秒级延时
USB低速模式的位周期大约是0.667微秒。在中断里,我们没法用 usleep 这样的函数。那怎么实现如此精确的延时呢?作者用了一个非常巧妙的方法:动态生成并执行NOP(空操作)指令。
NOP 指令在RISC-V(ESP32-C3的架构)中就是 addi x0, x0, 0(向零寄存器加0,结果丢弃)。执行一条NOP需要固定的CPU周期。通过向内存中写入特定数量的NOP指令,然后跳转过去执行,就能实现精确的指令周期级延时。
下面这段 setDelay 函数就是干这个的,它会根据需要的延时周期(ticks),在内存中“编织”出一段由NOP指令组成的函数。
void setDelay(uint8_t ticks)
{
// ... (内存分配等准备工作)
uint8_t* pnt = (uint8_t*)pntS;
// 写入ticks条NOP指令的机器码
for(int k=0; k<ticks; k++)
{
*pnt++ = 0x01; // NOP指令的部分机器码
*pnt++ = 0x00; // NOP指令的部分机器码
}
// 写入函数返回的指令尾
*pnt++ = 0x82;
*pnt++ = 0x80;
// 将这块内存区域重新分配为可执行属性
delay_pntA = heap_caps_realloc(pntS, MAX_DELAY_CODE_SIZE, MALLOC_CAP_32BIT | MALLOC_CAP_EXEC);
// 之后调用 (*delay_pntA)(); 就会执行刚刚写入的NOP指令流,实现延时
}
这就是为什么必须关闭内存保护——因为程序需要动态修改可执行代码段的内容。初始的ticks值(比如110)会在启动时通过测量NOP+GPIO操作的实际时间来校准,以保证延时精度。
3. 从USB到蓝牙:HID报告的获取与转发
成功模拟USB主机并与设备通信后,下一步就是获取设备的“身份证”和“语言”——即HID报告描述符(Report Descriptor),然后决定如何转发数据。
3.1 获取HID报告描述符
HID报告描述符是一种非常紧凑且灵活的数据结构,它定义了设备发送的数据报告(Report)的格式和含义。比如,一个鼠标的报告里,哪几个比特代表左键,哪几个字节代表X轴移动量,都是描述符定义的。
在USB枚举过程中,主机需要向设备请求这个描述符。在状态机(fsm_Mashine)中,对应着发送特定请求的步骤:
// 状态机中的某个状态(例如状态660)
else if(current->fsm_state == 660){
// 发送标准请求:获取报告描述符
Request(T_SETUP, ASSIGNED_USB_ADDRESS, 0b0000, T_DATA0, 0x81, 0x6, 0x2200, 0x0000, DEF_BUFF_SIZE, current->hid[current->hid_report_desc_count].wDescriptorLength);
current->fsm_state = 661;
return;
} else if(current->fsm_state == 661){
// 接收并存储描述符数据
int len = current->hid[current->hid_report_desc_count].wDescriptorLength;
if(current->acc_decoded_resp_counter == len) // 确认接收完整
{
memcpy(¤t->hid_report_desc_buffer[current->hid_report_desc_count], current->acc_decoded_resp, len);
current->hid_report_desc_count++;
// ... 处理下一个接口的描述符或进入下一步
}
}
拿到描述符后,程序会解析其开头的 Usage Page 和 Usage 字段,来判断插入的是鼠标、键盘还是其他设备。
3.2 翻译模式:让设备说“蓝牙语”
对于识别为鼠标或键盘的设备,适配器会尝试解析其报告描述符,并建立一个“翻译规则”。
1. 解析与映射 解析的目的是找出原始报告中每个功能(如X坐标、左键)对应的数据在报告字节中的位置(偏移量、比特位)、长度(占多少比特)以及数值范围(逻辑最小值/最大值)。
2. 建立线性变换 然后,程序会计算一个线性变换公式,将原始设备的数据映射到一个预定义的标准报告模型上。这个模型是蓝牙HID设备通用的格式。
例如,标准鼠标报告模型定义如下:
#pragma pack(1)
typedef struct {
uint8_t report_id; // 报告ID
uint8_t buttons; // 8个按钮,每位代表一个按键
int16_t x; // X轴位移,范围-32767到32767
int16_t y; // Y轴位移
int16_t wheel; // 滚轮位移
} standard_mouse_report_t;
#pragma pack()
对应的报告描述符定义了8个按钮位和3个16位有符号位移量。
翻译规则结构体 translate_item_t 就保存了每个数据项的映射关系:
typedef struct {
unsigned defined: 1; // 该项是否有效
unsigned data_signed: 1; // 数据是否有符号
uint8_t byte_offset; // 在原始报告中的字节偏移
uint8_t bit_offset; // 在字节中的比特偏移
uint8_t bit_count; // 占用的比特数
// 线性变换参数:输出 = (输入 + pre_scale_bias) * scale_factor + post_scale_bias
int32_t pre_scale_bias;
int32_t post_scale_bias;
double scale_factor;
} translate_item_t;
这样,无论你的鼠标报告里X坐标是8位还是12位,范围是0-255还是-127-127,都能通过这个公式转换成标准的-32767到32767的范围。
3.3 直通模式:当个“传声筒”
对于游戏手柄等不常见设备,或者解析失败的鼠标键盘,适配器会启用直通模式。这个模式简单粗暴:把从USB设备获取到的原始报告描述符和数据,原封不动地打包成蓝牙HID报告发送出去。
注意:直通模式的使用有“坑” 由于蓝牙HID连接的特性,直通模式用起来比翻译模式麻烦一点:
- 启动顺序:需要先成功连接一次蓝牙,USB总线才会开启。否则可能崩溃。
- 描述符缓存:蓝牙连接前需要确定报告描述符。因此,新插入的直通设备,其描述符会被保存到Flash中,然后设备自动重启。之后每次连接都使用这个缓存的描述符。
- 系统缓存:Windows等系统会缓存已配对设备的描述符。所以,换了新的直通设备后,需要在电脑上删除旧设备并重新配对。
正确使用步骤:连接蓝牙 -> 插入新设备 -> (设备自动重启) -> 在电脑上删除旧蓝牙设备并重新搜索配对 -> 连接使用。
4. 特色功能:网页配置与鼠标宏
让设备无线化只是基础,可编程的宏功能才是提升效率的利器。
4.1 宏的定义与触发
宏,本质上是一套“如果…就…”的规则。在这个项目中,宏以翻译模式设备的标准报告作为输入,当输入条件满足时,就输出另一个标准报告。
一个宏定义主要包含三部分:
- 触发条件:比如,鼠标的某个按键(或组合键)被按下。
- 动作内容:当触发后,要执行什么操作。比如,输出一连串的键盘按键序列(模拟快捷键操作)。
- 动作参数:比如,动作开始前的延迟(
action_delay),以及输出报告持续的时长(report_duration)。
宏的数据结构如下(以鼠标宏为例):
typedef struct{
saved_list_head_t head; // 链表节点
uint8_t version; // 版本
// 触发条件部分
bool cancel_input_report; // 触发后是否阻止原输入报告继续上报
macro_model_t input_model; // 输入设备类型(如鼠标)
union{
struct{ // 鼠标触发条件
uint8_t trigger_buttons_mask; // 触发按钮的掩码,比如0x01代表左键
};
};
// 动作部分
macro_model_t output_model; // 输出设备类型(如键盘)
macro_action_type_t action_type; // 动作类型(单次、连续等)
uint16_t action_delay; // 动作延迟(毫秒)
uint16_t report_duration; // 报告持续时间(毫秒)
union{
standard_mouse_report_t mouse_output_report; // 输出的鼠标报告
// 未来会有 keyboard_output_report 等
};
} macro_t;
例如,你可以定义一个宏:当按下鼠标侧键(映射为buttons的某一位)时,触发并输出一个 Ctrl+C 的键盘报告,从而实现一键复制。
4.2 网页管理界面
所有宏的配置、设备状态的查看,都是通过一个Web页面完成的。这个页面是一个纯前端的Vue3应用,开源在另一个仓库。
它的工作原理很巧妙:利用浏览器最新的WebHID API。当你的适配器通过蓝牙连接到电脑后,这个网页可以通过WebHID API直接与适配器进行HID协议通信,读取状态、下发配置。
提示:WebHID目前仍是实验性功能,请使用PC版Chrome 89+、Edge 89+或Opera 75+浏览器访问。
通信协议 为了在网页和适配器之间通信,它们定义了一套基于HID报告的私有协议。协议采用了类似HTTP的请求-响应模型,并且为了兼顾短消息的效率和长消息的传输,定义了两个不同大小的报告通道(Report ID 3和4)。
短消息结构如下:
// 请求
typedef struct{
uint16_t opcode; // 操作码,类似HTTP Method
uint16_t session; // 会话ID,用于匹配请求和响应
uint16_t length; // 数据长度
uint8_t data[]; // 数据(可能是JSON或普通文本)
} request_t;
// 响应
typedef struct{
uint16_t session; // 对应请求的会话ID
uint16_t length; // 数据长度
uint8_t success; // 成功标志(0失败,1成功)
uint8_t data[]; // 响应数据
} response_t;
对于更长的数据(比如传输复杂的配置),则采用分片传输机制,每个分片512字节,包含了分片序号等信息,在接收端重新组装。
5. 动手实践:编译与烧录指南
如果你对这个项目感兴趣,想自己动手做一块或者修改代码,可以参考以下步骤。
5.1 准备开发环境
- 获取源码:克隆主仓库和相关的子模块。
git clone --recursive https://github.com/dnstzzx/usb-hid-bler.git - 设置ESP-IDF:本项目锁定了特定的ESP-IDF版本(
97fb98a91b308d4f4db54d6dd1644117607e9692),以确保API兼容性。你需要安装并切换到该版本。 - 应用补丁:由于蓝牙HID任务栈大小问题,需要替换一个官方文件。将项目
.github/目录下的ble_hidd.c.replace文件,复制并覆盖到你的ESP-IDF目录中的components/esp_hid/src/ble_hidd.c。 - 关键配置:在
idf.py menuconfig中,务必完成以下两项配置:Compiler options -> Optimization Level设置为-O2。Component config -> ESP System Setting -> Memory protection关闭。
5.2 编译与烧录
配置完成后,进入项目目录,使用标准的IDF命令进行编译和烧录即可。
idf.py set-target esp32c3
idf.py build
idf.py -p /dev/ttyUSB0 flash monitor # 请将 /dev/ttyUSB0 替换为你的实际串口
注意:每次执行
idf.py set-target后,优化等级可能会被重置,需要重新在menuconfig中检查并设置为-O2。
如果你在本地环境编译遇到困难,也可以直接Fork项目到你的GitHub,利用仓库中已经配置好的 GitHub Actions 工作流进行在线编译,它会自动生成固件文件供你下载。
5.3 硬件组装参考
原作者提供了PCB设计,你可以在立创EDA等平台查看。核心部件包括:
- ESP32-C3模块(建议选择带外部天线接口的版本,信号更稳)
- 两个USB-A母座
- 锂电池充电管理芯片(如TP4056)
- 电平转换电路(确保USB信号电平匹配)
- 必要的电容、电阻和指示灯
组装时,注意USB数据线(D+, D-)要连接到ESP32-C3指定的GPIO上,并在代码中正确配置引脚号。电源部分要确保能稳定提供5V和3.3V。
6. 进阶与展望:指纹解锁与更多可能
目前,指纹解锁功能还在开发中。其思路是集成一个指纹识别模块(如FPMxx系列),当指纹验证通过后,适配器模拟键盘输入预设的Windows登录密码,从而实现“指纹解锁”。这相当于一个物理层面的密码管理器,安全性取决于硬件本体的保管。
这个项目展示了ESP32-C3 GPIO模拟能力的强大,以及HID协议的灵活性。你可以基于此进行很多扩展:
- 支持更多设备:在翻译器中加入对游戏手柄、绘图板等设备的解析规则。
- 宏功能增强:实现更复杂的逻辑判断、循环、随机延迟等,让宏更像编程。
- 多设备切换:让一个适配器记忆多套键鼠配置,通过按键切换连接不同的主机。
- 低功耗优化:当没有检测到USB设备插入时,让ESP32-C3进入深度睡眠,仅由蓝牙广播唤醒,极大延长电池续航。
希望这篇详细的解析能帮你彻底理解这个有趣的DIY项目。从软件模拟USB到协议翻译,再到无线配置,每一个环节都充满了嵌入式开发的乐趣和挑战。如果你在复现过程中遇到了问题,不妨去原项目的GitHub仓库看看Issue,或者自己动手调试一番,这本身就是最好的学习过程。
更多推荐
所有评论(0)