拓冰建站拓冰建站
首页 / 资讯中心 / 正文

ESP32-P4 USB Host 高速读鼠标:HID 枚举与报告解析实操

1. 为什么要在 ESP32-P4 上折腾 USB Host 读鼠标拿到 ESP32-P4 这块板子的时候我第一反应不是去点灯而是想试试它那颗高速 USB OTG 控制器到底能不能直接认鼠标。原因很简单ESP32-P4 是乐鑫第一颗把 USB 2.0 High-Speed480 MbpsHost/Device 控制器做进片内的芯片之前玩 ESP32-S3 的时候USB Host 只能跑全速12 Mbps接个鼠标键盘没问题但想接 U 盘、接摄像头就有点吃力。P4 这一代把 OTG 控制器升级到高速理论上接 HID 设备是降维打击但实际跑起来坑不少——时钟配置、PHY 上电时序、HID 报告描述符解析每一步都能让你卡半天。这篇东西就是我在 DNESP32P4 开发板上把 USB 鼠标跑通之后整理的一份实操记录。核心目标很明确让 ESP32-P4 作为 USB Host枚举一个标准 USB HID 鼠标解析出按键、X/Y 位移、滚轮数据并通过串口打印出来。涉及的关键词就是 ESP32-P4、USB、HID、Host、ESP-IDF 这一套。适合谁看如果你已经会用 ESP-IDF 建工程、烧固件但对 USB Host 协议栈和 HID 报告描述符没什么概念那这篇正好如果你只是想抄一份能跑的代码直接跳到第 3 节的实操部分也行但建议还是把第 2 节的原理过一遍不然出了问题你连日志都看不懂。我用的环境是 ESP-IDF v5.3P4 支持从 v5.2 开始逐步完善v5.3 相对稳定开发板是正点原子的 DNESP32P4鼠标用的是最普通的罗技 M90 有线鼠标另外还试了一个杂牌无线鼠标的接收器两个都能正常枚举。下面从设计思路开始讲把每一步为什么这么做说清楚。2. USB Host 实验的整体设计与方案选型2.1 为什么不用 TinyUSB 而是用 ESP-IDF 原生 USB Host 栈ESP-IDF 里其实有两套 USB Host 方案一套是乐鑫自己维护的usb_host组件在esp-idf/components/usb/下另一套是集成进来的 TinyUSB。很多人第一反应是用 TinyUSB因为它在 STM32 圈子里太出名了。但我实测下来在 ESP32-P4 上做 HID Host原生usb_host栈反而更省心原因有三点。第一原生栈和 ESP-IDF 的构建系统、FreeRTOS 任务模型结合得更紧。usb_host_install()之后你可以自己决定在哪个任务里跑usb_host_lib_handle_events()事件回调的上下文很清晰。TinyUSB 虽然也能跑但它的tusb_task()和 ESP-IDF 的事件循环容易打架尤其是你要同时用 WiFi 或者 BLE 的时候任务优先级一乱就出玄学问题。第二原生栈对 P4 的 High-Speed PHY 支持是官方直接适配的usb_host_install()里传的配置结构体直接就能选USB_SPEED_HIGH不用自己去改 PHY 初始化代码。TinyUSB 在 P4 上的高速模式支持是后来才补的早期版本还得手动 patch。第三HID 这块原生栈提供了usb_host_hid这个上层组件在esp-idf/components/usb/hid/下它帮你把 HID 描述符解析、报告传输这些脏活都干了。你只需要注册一个回调报告数据来了直接读就行。TinyUSB 的 HID Host 虽然也有但 API 风格偏底层解析报告描述符还得自己写。当然原生栈也不是没缺点它的文档相对少很多 API 得直接看头文件注释。但考虑到 P4 是新芯片跟着官方栈走踩坑概率最低。2.2 HID 鼠标的枚举流程到底发生了什么在写代码之前得先搞清楚一个 USB 鼠标插上去之后Host 这边到底经历了什么。很多人代码跑不通就是因为不知道枚举分几步日志一刷屏就懵了。整个流程大致是这样设备插入Host 检测到 D 或 D- 上的上拉电阻变化触发端口连接事件然后 Host 复位端口设备进入 Default 状态接着 Host 通过控制端点 0 读取设备描述符18 字节拿到 VID、PID、bMaxPacketSize0 这些信息再读配置描述符配置描述符里嵌套着接口描述符和端点描述符HID 设备还会多一个 HID 描述符和报告描述符读完这些Host 给设备分配一个地址Set Address设备进入 Address 状态最后 Host 读一次完整的配置描述符然后发 Set Configuration设备进入 Configured 状态这时候端点才真正可用。对于 HID 鼠标关键信息藏在报告描述符里。报告描述符是一段用 Item 编码的二进制数据它告诉 Host这个设备有几个报告每个报告多少字节每个字节的每一位是什么含义。比如一个标准三键鼠标报告通常是 4 字节Byte0 是按键位图bit0 左键、bit1 右键、bit2 中键Byte1 是 X 位移有符号 8 位Byte2 是 Y 位移Byte3 是滚轮。但不同鼠标这个格式可能不一样有的带 16 位位移有的把滚轮放在不同位置。所以绝对不能硬编码报告格式必须解析报告描述符。原生usb_host_hid组件的好处就在这它在枚举阶段会自动解析报告描述符把每个字段的用途、偏移、位宽都存下来你通过usb_host_hid_get_report_desc()之类的接口就能拿到结构化信息。不过实际用的时候我建议还是自己打印一遍原始报告描述符心里有底。2.3 硬件连接上的几个关键点DNESP32P4 开发板上 USB Host 接口用的是 Type-A 母座还是 Type-C这个得看你的板子版本。我手上这块是 Type-C 座但走的是 Host 模式所以需要一根 Type-C 转 Type-A 的 OTG 线或者直接用 Type-C 公头转 Type-A 母座的转接头。这里有个坑不是所有 Type-C 线都支持 Host 模式有些线只接了 USB 2.0 的 D/D-没接 CC 引脚插上去设备根本不供电。我一开始用了一根手机充电线死活认不到鼠标换了根带 OTG 功能的线立马就好了。另外 P4 的 USB PHY 供电是内部 LDO 提供的但 VBUS 供电需要外部提供。开发板上一般有一个 VBUS 使能引脚比如 GPIO 某个脚控制一个 MOS 管给 VBUS 供电。这个在usb_host_install()之前必须拉高否则设备插上去没电日志里只会看到端口连接事件反复触发但枚举失败。DNESP32P4 的原理图上 VBUS 使能是接在某个 GPIO 上的具体哪个脚看你的板子原理图我这边是 GPIO 46代码里初始化的时候拉高就行。还有一点如果你用的是高速模式D/D- 的走线阻抗要控制好开发板一般没问题但如果你自己飞线接鼠标线长了或者没做阻抗匹配枚举会不稳定表现为时好时坏。这种情况降到全速模式USB_SPEED_FULL往往就稳了代价是轮询率低一点但鼠标用完全够。3. 从零搭建 USB HID Host 工程的完整实操3.1 工程创建与关键配置项先用idf.py create-project usb_hid_mouse_host建一个空工程然后idf.py set-target esp32p4。接下来是menuconfig里几个必须改的地方我列个表你照着勾。配置项路径设置值为什么USB Host 支持Component config → USB Host勾选不勾这个usb_host组件根本不编译USB OTG 模式Component config → USB Host → USB OTG ModeHost默认可能是 Device必须改HID 支持Component config → USB Host → HID勾选启用usb_host_hid上层组件日志级别Component config → Log → Default log levelInfoDebug 级别日志太多Info 够用FreeRTOS TickComponent config → FreeRTOS → Tick rate1000USB 事件处理对时间敏感1000Hz 更稳这里重点说下 Tick rate。默认是 100 Hz也就是 10ms 一个 tick。USB 枚举过程中有些超时判断是按 tick 算的100 Hz 下精度太粗偶尔会出现枚举超时误判。改成 1000 Hz 之后1ms 一个 tick枚举稳定性明显提升。代价是任务切换开销略增但对这个实验来说无所谓。还有一个隐藏配置CONFIG_USB_HOST_CONTROL_TRANSFER_MAX_SIZE默认是 256 字节。有些鼠标的报告描述符比较长超过 256 字节的话读配置描述符会失败。我建议直接改成 512反正 RAM 够。3.2 初始化 USB Host 栈的代码骨架初始化分三步装 Host 栈、装 HID 驱动、起事件处理任务。我直接上代码关键地方加注释。#include usb/usb_host.h #include usb/hid_host.h static usb_host_handle_t host_handle; void usb_host_task(void *arg) { while (1) { uint32_t event_flags; // 这个函数会阻塞直到有事件或者超时 usb_host_lib_handle_events(portMAX_DELAY, event_flags); if (event_flags USB_HOST_LIB_EVENT_FLAGS_NO_CLIENTS) { // 没有客户端了可以清理 } if (event_flags USB_HOST_LIB_EVENT_FLAGS_ALL_FREE) { // 所有设备都释放了 } } } void app_main(void) { // 第一步配置 Host 栈 usb_host_config_t host_config { .skip_phy_setup false, // P4 内部 PHY必须初始化 .intr_flags ESP_INTR_FLAG_LEVEL1, }; ESP_ERROR_CHECK(usb_host_install(host_config)); // 第二步装 HID 驱动 hid_host_driver_config_t hid_config { .create_background_task true, .task_priority 5, .stack_size 4096, .core_id 0, .callback hid_host_callback, // 设备连接/断开回调 .callback_arg NULL, }; ESP_ERROR_CHECK(hid_host_install(hid_config)); // 第三步起事件处理任务 xTaskCreate(usb_host_task, usb_host, 4096, NULL, 4, NULL); }这里有个细节skip_phy_setup一定要设成false。P4 的 USB PHY 是片内集成的需要 Host 栈去初始化它。如果你设成true栈会以为 PHY 已经由外部初始化好了结果就是端口一直没反应。我一开始看别的例程里设了true照抄之后死活认不到设备查了半天才发现是这个问题。intr_flags用ESP_INTR_FLAG_LEVEL1就行USB 中断优先级不用太高免得抢占其他关键中断。3.3 设备连接回调与 HID 接口打开hid_host_callback是设备插拔的入口当鼠标插上时这个回调会被调用参数里带着设备句柄。你要在这个回调里打开 HID 接口注册报告接收回调。static void hid_host_callback(hid_host_device_handle_t hid_device_handle, const hid_host_driver_event_t event, void *arg) { if (event HID_HOST_DRIVER_EVENT_CONNECTED) { // 设备连上了配置 HID 接口 hid_host_device_config_t dev_config { .callback hid_host_interface_callback, .callback_arg NULL, }; ESP_ERROR_CHECK(hid_host_device_open(hid_device_handle, dev_config)); // 判断是不是鼠标 if (hid_host_device_get_device_type(hid_device_handle) HID_HOST_DEVICE_TYPE_MOUSE) { ESP_LOGI(TAG, Mouse detected); // 开始接收报告 hid_host_device_start(hid_device_handle); } else { ESP_LOGW(TAG, Not a mouse, ignoring); hid_host_device_close(hid_device_handle); } } }hid_host_device_get_device_type()这个接口是usb_host_hid组件提供的它会根据报告描述符里的 Usage Page 和 Usage ID 判断设备类型。鼠标的 Usage Page 是 0x01Generic DesktopUsage 是 0x02Mouse。键盘是 0x06Keyboard游戏手柄是 0x04 或 0x05。这个判断比自己解析报告描述符省事多了。注意hid_host_device_start()必须在hid_host_device_open()之后调用而且只有调用了 start报告数据才会通过回调送上来。我见过有人 open 完就等着收数据结果一直没反应就是漏了 start。3.4 报告数据的解析与打印报告回调里拿到的是一段原始字节长度和格式取决于报告描述符。对于标准三键鼠标通常是 4 字节。但为了通用我建议先打印原始 hex再按常见格式解析。static void hid_host_interface_callback(hid_host_device_handle_t hid_device_handle, const hid_host_interface_event_t event, void *arg) { uint8_t data[64]; size_t data_length 0; if (event HID_HOST_INTERFACE_EVENT_INPUT_REPORT) { esp_err_t ret hid_host_device_get_raw_input_report_data( hid_device_handle, data, sizeof(data), data_length); if (ret ! ESP_OK) { ESP_LOGE(TAG, Failed to get report: %s, esp_err_to_name(ret)); return; } // 打印原始数据方便调试 ESP_LOG_BUFFER_HEX_LEVEL(TAG, data, data_length, ESP_LOG_DEBUG); // 按标准鼠标格式解析 if (data_length 3) { uint8_t buttons data[0]; int8_t x (int8_t)data[1]; int8_t y (int8_t)data[2]; int8_t wheel (data_length 4) ? (int8_t)data[3] : 0; ESP_LOGI(TAG, Btn: L%d R%d M%d | X%d Y%d Wheel%d, buttons 0x01, (buttons 1) 0x01, (buttons 2) 0x01, x, y, wheel); } } else if (event HID_HOST_INTERFACE_EVENT_DISCONNECTED) { ESP_LOGI(TAG, Mouse disconnected); hid_host_device_close(hid_device_handle); } }这里hid_host_device_get_raw_input_report_data()拿到的就是端点上的原始数据。注意 X/Y 是有符号的鼠标往右移 X 是正往左是负所以必须转成int8_t再打印不然你会看到 255 这种值。滚轮的处理要小心不是所有鼠标都有滚轮所以data_length 4才去读第 4 字节。有些鼠标还有横向滚轮或者侧键报告长度可能是 5 字节甚至 7 字节那就得看报告描述符了。我建议先用ESP_LOG_BUFFER_HEX_LEVEL把原始数据打出来手动动一下鼠标观察哪个字节在变就能反推出格式。3.5 编译烧录与第一次上电验证代码写完idf.py build编译。如果报错说找不到usb/hid_host.h检查menuconfig里 HID 支持有没有勾上。编译通过后idf.py -p /dev/ttyUSB0 flash monitor烧录并打开串口。正常的话你会看到类似这样的日志I (320) USB_HID: Installing USB Host I (330) USB_HID: Installing HID driver I (450) USB_HID: Mouse detected I (460) USB_HID: Btn: L0 R0 M0 | X0 Y0 Wheel0 I (520) USB_HID: Btn: L1 R0 M0 | X3 Y-2 Wheel0如果插上鼠标后一直没反应先看有没有Mouse detected这行。没有的话问题出在枚举阶段往下看第 4 节的排查方法。有这行但没报告数据检查hid_host_device_start()有没有调用。4. 踩坑实录与常见问题速查4.1 枚举失败端口反复连接断开这是最常见的现象日志里USB_HOST_LIB_EVENT_FLAGS相关的连接事件反复刷但就是不出设备描述符。原因通常有三个。第一个是 VBUS 没供电。用万用表量一下 Type-A 座的 VBUS 脚正常应该是 5V。如果没有检查开发板的 VBUS 使能 GPIO 有没有拉高。DNESP32P4 上这个脚是 GPIO 46但不同版本可能不一样务必看原理图。第二个是 D/D- 接反了。USB 线序里 D 和 D- 是差分对接反了设备根本不会响应。如果你是自己焊的转接板重点查这个。第三个是 PHY 初始化失败。usb_host_install()返回ESP_OK不代表 PHY 一定好了有时候 PHY 时钟没锁上端口就是没反应。这种情况可以试着在usb_host_install()之前手动调用usb_phy_init()或者检查 P4 的 48MHz 时钟源配置对不对。4.2 报告数据全是 0 或者不更新设备枚举成功了Mouse detected也打出来了但报告数据一直是 0动鼠标也不变。这种情况八成是端点没配对。HID 鼠标通常有一个中断输入端点Interrupt IN Endpoint地址比如 0x81。usb_host_hid组件在 open 的时候会自动找这个端点并配置。但如果你的鼠标有多个接口比如带多媒体按键的鼠标会有两个 HID 接口组件可能打开了错误的接口。解决办法是在回调里打印接口号确认打开的是 Usage 为 Mouse 的那个接口。还有一种可能是轮询间隔bInterval设得太长。有些鼠标 bInterval 是 10ms意味着每 10ms 才上报一次你动一下鼠标可能刚好错过。这种情况多动几下就能看到数据。4.3 鼠标动一下就断连这个现象比较诡异通常是供电不足导致的。有些游戏鼠标功耗比较大尤其是带 RGB 灯的瞬时电流可能超过 100mA。P4 开发板的 VBUS 如果只靠 USB 口供电可能撑不住。解决办法是给开发板单独供电或者换一个低功耗的鼠标测试。另外如果用的是 USB Hub 扩展Hub 本身也会吃电更容易出这个问题。建议调试阶段直接插开发板不要经过 Hub。4.4 常见问题速查表现象可能原因排查方法解决无任何日志Host 栈没装成功看usb_host_install返回值检查 menuconfig 配置端口反复连接断开VBUS 没供电万用表量 VBUS拉高 VBUS 使能 GPIO枚举到一半失败控制传输缓冲区太小看日志里有没有CTRL transfer failed增大CONTROL_TRANSFER_MAX_SIZE认到设备但不是鼠标接口选错打印 Usage Page/Usage检查报告描述符报告数据不更新端点没 start确认hid_host_device_start被调用补上 start 调用动一下就断供电不足换低功耗鼠标测试独立供电或换鼠标4.5 几个我踩过的独家坑第一个坑hid_host_install()的create_background_task如果设成false你得自己起任务去跑hid_host_device_handle_events()否则回调永远不会触发。我一开始为了省任务设了false结果回调死活不进查了两小时才发现。第二个坑报告描述符里的 Logical Minimum/Maximum 决定了 X/Y 的取值范围。标准鼠标是 -127 到 127但有些高 DPI 鼠标是 -32767 到 32767用 16 位表示。如果你按 8 位解析数据就乱了。这种情况必须解析报告描述符拿到位宽不能硬编码。第三个坑热插拔的时候如果上一次的设备句柄没释放干净下一次枚举会失败。所以断开回调里一定要调hid_host_device_close()而且要在 close 之后再让 Host 栈去处理端口变化。顺序错了会死锁。5. 从能跑到好用几个进阶优化方向5.1 把鼠标数据映射成 UI 操作跑通枚举和报告解析只是第一步真正有意思的是把鼠标数据用起来。ESP32-P4 支持 MIPI-DSI 和 RGB 接口可以接屏幕。你可以把鼠标的 X/Y 位移累加成一个光标坐标按键状态映射成点击事件滚轮映射成滚动。这样就能在 P4 上做一个完整的 UI 交互 demo。具体做法是维护一个全局的cursor_x和cursor_y每次报告回调里累加位移然后 clamp 到屏幕范围内。按键用边沿检测按下瞬间触发点击事件。这部分逻辑不复杂但要注意报告回调是在 USB 任务上下文里跑的更新 UI 最好通过队列发给 UI 任务不要在回调里直接刷屏。5.2 支持多个 HID 设备同时接入如果你通过 Hub 接了鼠标和键盘usb_host_hid组件是支持多设备的每个设备有独立的句柄。你需要在回调里区分设备类型鼠标走鼠标的处理逻辑键盘走键盘的。键盘的报告格式又不一样通常是 8 字节Byte0 是修饰键Ctrl/Shift/AltByte1 保留Byte2-7 是按键码。这个可以另外开一篇讲。多设备场景下要注意任务栈大小。每个 HID 设备都会占用一定的栈空间如果设备多了hid_host_install里的stack_size要相应加大否则会栈溢出。5.3 低功耗场景下的 USB Host如果你的项目是电池供电的USB Host 一直开着很费电。P4 支持 USB Host 挂起Suspend和恢复Resume。当鼠标一段时间没动作可以让 Host 栈进入挂起状态电流能从几十毫安降到几毫安。检测到端口活动再恢复。这个功能在usb_host组件里有 API但文档不多需要自己摸索。我试过在鼠标空闲 30 秒后挂起动一下鼠标能正常恢复但恢复瞬间会丢一两个报告对鼠标来说无所谓。5.4 用 USB 抓包工具验证枚举过程如果你有 USB 协议分析仪比如 Total Phase 或者国产的抓包工具强烈建议抓一次枚举过程。你会看到 Host 发的每一个 Setup 包、设备回的每一个描述符比看日志直观一百倍。没有硬件分析仪的话P4 本身支持 USB Host 的调试日志把日志级别开到 Debug能看到控制传输的详细过程。虽然不如抓包直观但排查问题够用了。我个人在实际操作中的体会是USB Host 这块最难的不是写代码而是理解枚举流程和报告描述符。代码本身没几行但每一行背后都有协议层的含义。把协议搞懂了换任何 HID 设备都能快速适配。另外P4 的 USB 高速模式确实比 S3 的全速模式稳很多同样的鼠标在 S3 上偶尔会丢报告在 P4 上跑了一下午没出过一次错。如果你手上有 P4 的板子强烈建议把 USB Host 跑一遍这是这颗芯片相比前代最实用的升级之一。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门