ESP32-P4 USB Host实战:读取USB鼠标HID报告全解析
2026/9/14 3:17:30 网站建设 项目流程

我们可以先拆一下这个实验的本质:ESP32-P4 这颗芯片上跑 USB Host,把普通的 USB 鼠标插上去,由芯片主动枚举外设、接收 HID 报告,最后把鼠标的位移和按键在显示屏或串口上呈现出来。这个实验最直接的收获不是“能控制鼠标”本身,而是把 USB 协议栈里最核心的枚举、端点通信、中断传输、HID 报告解析这一整条链路完整地跑通一遍。后面你不管是做 USB 键盘、USB 游戏手柄,还是 USB 转串口、U 盘读写,走的都是同一套框架,只是接口描述符和 Class 协议不同。

这篇就按我自己调这个实验的实际顺序来讲,把原理、代码、配置和踩坑放在一起,尽量让你照着做就能跑起来。

1. 项目背景与方案选型

1.1 ESP32-P4 的 USB 控制器资源

ESP32-P4 和之前的 ESP32-S3 不一样,它内部提供了两个独立的 USB OTG 控制器,一个支持 Full Speed(FS, 12Mbps),一个同时支持 High Speed(HS, 480Mbps)。这颗芯片定位就是做多媒体、边缘计算这类对 IO 性能有要求的场景,USB 资源给得比较足。

DNESP32P4 开发板在设计上把这两个 USB 控制器都引出来了,一个做成 Device 口(通常接 PC 用于烧录和虚拟串口调试),另一个就是专门给 Host 实验用的。我们的 USB 鼠标实验用的就是 Host 控制器对应的那个 USB-A 口。这个口可以直接插标准 USB 鼠标、键盘,不需要额外转接板。

这里有个好处:之前的 ESP32 系列做 Host 实验,经常要外接 USB PHY 芯片,比如 USB3300,或者必须用芯片内置的 Full-Speed PHY 再加上外部电平转换。P4 的 Host 口直接把 PHY 集成好了,硬件上省很多事,软件上也不用关心 PHY 配置,聚焦到协议本身即可。

1.2 为什么这个实验选 Host 模式而不是 Device

有些刚接触 USB 的朋友会有个疑问:鼠标本身是 Device,为什么不能让两个设备直接对接?

USB 协议是主从架构。一条 USB 总线上必须有且只有一个 Host,由 Host 负责发起所有传输事务、管理总线供电、分配地址、配置设备。鼠标这种外设属于 Device,它永远不能主动往总线上发数据,只能等 Host 来“问”它。

所以让 ESP32-P4 读鼠标,P4 就必须扮演 Host。这个角色切换在硬件上由 OTG 控制器决定,软件上就是usb_host_install()usb_otg_switch_role()的事。实验里我们把 OTG 角色强制设为 Host,然后插上鼠标,P4 就会像电脑一样去识别这个鼠标。

1.3 鼠标数据传输用什么传输类型

USB 定义了四种传输类型:控制传输、中断传输、批量传输、等时传输。鼠标、键盘这类 HID 设备用的是中断传输,这里“中断”不是硬件中断,而是指“周期性轮询”。

Host 必须按照端点描述符里写的bInterval值,每隔一段时间(比如 1ms 或 10ms)主动读取一次鼠标发来的报告。鼠标的报告量很小,通常一次就 4 到 8 个字节,没必要用批量传输,中断传输刚好兼顾了低延迟和总线占用小。

此外设备初始化阶段靠的是控制传输,也就是枚举流程。控制传输走端点 0,所有 USB 设备必须实现端点 0,Host 通过一系列标准请求(Get_Descriptor、Set_Address、Set_Configuration 等)把设备初始化好。

把握这两个传输类型,代码逻辑就清晰了:枚举阶段全是控制传输,枚举完成后打开中断 IN 端点持续轮询。

2. 硬件连接与工程搭建

2.1 开发板 USB 接口确认

DNESP32P4 开发板的 USB 接口有好几个,做这个实验前先确认你插的是哪个。通常板子上丝印会标USB_HOSTHOST,那个是 USB-A 母座,对应的是 HS 控制器,支持 480Mbps。如果板卡丝印看不清,可以看原理图上芯片引脚的连接关系,找连接到芯片 USB_OTG2 引脚的那组 D+/D-。

我手上这块板子,Host 口是标准 USB-A,鼠标线直接插进去就能供电,不需要外接电源——USB Host 口会输出 5V 给设备供电,P4 的 USB 控制器支持这个供电控制。

注意不要插到标着UARTDEVICE的那个 Type-C 口,那是 Device 模式用的,往那儿插鼠标没用。

2.2 硬件接线与电平注意事项

如果是拿模块自己搭电路,需要注意 D+/D- 的连接。USB 2.0 的 D+/D- 是差分对,要求 90 欧姆差分阻抗,所以 PCB 走线时要保持差分等长、远离干扰源,尽量短。

P4 内置 PHY 后,D+/D- 直接连到芯片引脚上,不需要外部上拉电阻——这个和用 ESP32-S2 时的做法类似,内置 PHY 会处理好终端电阻和上拉。

如果用的是杜邦线飞线连接(比如从扩展排针引出),线长不要超过 10cm,否则高速信号完整性会出问题,表现为枚举不稳定、鼠标频繁掉线。我之前试过用 20cm 的杜邦线接 USB 鼠标,结果 5 次里有 2 次枚举失败,后来换成短粗线就稳定了。

电源方面,USB Host 口负责给鼠标供电,如果用的是非标电源适配器或者劣质 USB 线,鼠标的供电纹波会大,导致芯片内部复位。有条件的用 5V/2A 以上的适配器给开发板供电,尽量避免电脑 USB 口同时给开发板供电和烧录,容易引入地回路干扰。

2.3 工程配置与 menuconfig 关键项

正点原子的例程基于 ESP-IDF,我用的版本是 v5.2 以上(实测 5.3 也没问题)。打开工程后,先跑一次idf.py set-target esp32p4,然后进入 menuconfig 做以下配置:

  • Component config → USB → Host Stack:打开USB Host,同时确认Number of USB Host Clients至少为 1 到 2。例程默认是 1,够用。
  • Component config → USB → Host Stack → Disable BulkBulk transfers:默认不用动,鼠标用不到。
  • Component config → USB → Host Stack → Enable Host tests:不要开,这是测试用的,开了会干扰正常枚举。
  • Component config → ESP32P4-Specific → USB:如果是 HS 口,确认Enable USB High Speed打开;如果用的是 FS 口,则保持关闭。

这些配置本质上是决定 USB Host 库在编译时启用哪些功能。ESP-IDF 的 USB Host 库是可裁剪的,比如不需要支持多客户端时,就可以减少内存占用。

配置保存后,先idf.py build编译一次,确认没有宏定义冲突。常见的一个坑是:如果你同时使能了 TinyUSB 的 Device 模式,会和 Host 栈争用 OTG 控制器,编译能过但运行时无法切换角色,或者开机就被对方抢占。例程里默认只使能 Host,不要额外打开 TinyUSB 的USB_DEVICE

2.4 烧录与串口监视

烧录命令:

idf.py build flash monitor

如果开发板 Device 口没有连 PC,Host 口插鼠标且没有其他串口输出,可能会误以为程序没跑。实际上程序已经通过 Host 口枚举鼠标了,但你看不到日志。建议先用 USB 线连接 PC 和 Device 口,用串口终端看日志。

波特率我习惯设 115200,日志级别idf.py -p /dev/ttyUSB0 monitor默认就能看到。

3. 协议基础与代码实现

3.1 USB 鼠标枚举过程拆解

这一节是整个实验最值得深挖的地方。USB 枚举过程看起来复杂,但理解成“Host 和设备之间的一次自我介绍”就清晰了。

鼠标插上后,Host 首先通过端点 0 发送GET_DESCRIPTOR(Device)请求,拿到设备描述符,这个描述符里包含了idVendoridProductbcdUSBbMaxPacketSize0等关键信息。接着 Host 会给设备分配一个唯一的地址,发送SET_ADDRESS请求。

分配完地址后,Host 重新用新地址和设备通信,依次获取配置描述符、接口描述符、端点描述符。这里要特别说明:配置描述符是一个复合结构,GET_DESCRIPTOR(Configuration)返回的是一整块数据,里面包含配置描述符、一个或多个接口描述符、每个接口下的端点描述符。

鼠标的配置描述符结构通常是:

Configuration Descriptor: bNumInterfaces = 1 Interface 0: bInterfaceClass = 0x03 (HID) bInterfaceSubClass = 0x01 (Boot Interface) bInterfaceProtocol = 0x02 (Mouse) Endpoint IN: bEndpointAddress = 0x81 bmAttributes = 0x03 (Interrupt) wMaxPacketSize = 4 或 8 bInterval = 10 (即 10ms)

Host 拿到这些信息后,发送SET_CONFIGURATION(1)让鼠标正式开始工作。之后 Host 就知道要周期性读取端点 0x81 的数据了。

典型鼠标的枚举日志(抓包/日志输出):

USB Host: New device connected USB Host: Address assigned: 1 USB Host: Device descriptor: VID=046d, PID=c077 USB Host: Config descriptor: interfaces=1, endpoints=2 USB Host: HID device found, subclass=1, protocol=2 USB Host: Using boot protocol mouse

3.2 代码框架总览

正点原子例程的代码结构大致分三层:

  • 底层:ESP-IDF 的 USB Host Stack,负责总线事务、URB 管理、设备连接断开检测。
  • 中间层:HID Class 驱动逻辑,负责解析 HID 描述符、选择 Boot Protocol、处理键盘/鼠标报告。
  • 应用层:把解析出来的鼠标位移和按键映射到屏幕光标或串口打印。

我们不用重复造轮子,直接基于 ESP-IDF 的usb_host接口来写应用层代码,但理解中间层的 HID 报告解析是核心。

3.3 初始化与事件循环

先看主流程代码:

#include "usb/usb_host.h" #include "usb/hid_host.h" static void usb_lib_init(void) { const usb_host_config_t host_config = { .skip_phy_setup = false, .intr_flags = ESP_INTR_FLAG_LEVEL1, }; usb_host_install(&host_config); } static void usb_client_event_cb(const usb_host_client_event_msg_t *event, void *arg) { switch (event->event) { case USB_HOST_CLIENT_EVENT_NEW_DEV: // 新设备插入,触发枚举和打开 break; case USB_HOST_CLIENT_EVENT_DEV_GONE: // 设备拔出,释放资源 break; default: break; } } static void usb_client_init(void) { const usb_host_client_config_t client_config = { .is_synchronous = false, .max_num_event_msg = 5, .async = { .event_cb = usb_client_event_cb, .arg = NULL, }, }; usb_host_client_register(&client_config, &client_hdl); }

几点说明:

  • usb_host_install()只需要调用一次,它初始化底层控制器,建立默认的根 Hub。
  • 客户端(client)是 USB Host 栈的概念,它代表一个“对设备进行操作的主体”。注册客户端时传入事件回调,当总线上有新设备插入或设备拔出时,这个回调会被触发。
  • 事件回调是在中断上下文还是任务上下文,取决于配置。例程默认是任务上下文,可以在回调里调用阻塞 API,但最好还是把重的操作丢给专门的任务处理。

初始化完成后,要有一个任务不断调用usb_host_client_handle_events(client_hdl, timeout),这个函数负责处理所有与客户端相关的 USB 事件。如果程序卡在某个环节不推进,最常见的原因就是没有调用这个函数。

void usb_host_task(void *arg) { while (1) { usb_host_client_handle_events(client_hdl, portMAX_DELAY); } }

这里有个细节:handle_events返回之前,内部会调用客户端注册的事件回调。如果事件回调里做了阻塞操作,会直接影响后续事件的处理,所以回调里只做标记,不要长时间停留。

3.4 设备打开与描述符解析

当检测到新设备插入后,需要从设备列表里拿到设备地址,然后打开设备。

static void process_new_device(void) { // 获取设备信息 usb_host_device_info_t dev_info; usb_host_device_open(client_hdl, dev_addr, &dev_hdl); // 获取配置描述符 const usb_config_desc_t *config_desc; usb_host_get_active_config_descriptor(dev_hdl, &config_desc); // 解析接口与端点 parse_hid_interface(config_desc); }

解析接口这一步是重点。配置描述符的整体结构是:先是usb_config_desc_t,后面跟一系列接口描述符(usb_intf_desc_t)、端点描述符(usb_ep_desc_t)以及 HID 描述符(usb_hid_desc_t)。我们不能简单地强转指针访问,因为描述符之间有对齐填充,必须用USB_ITERATE_INTF或者手动按长度字段跳过。

我这里给一个通用的遍历方法:

static void parse_hid_interface(const usb_config_desc_t *config_desc) { int offset = 0; const uint8_t *p = (const uint8_t *)config_desc; while (offset < config_desc->wTotalLength) { const usb_standard_desc_t *desc = (const usb_standard_desc_t *)(p + offset); if (desc->bDescriptorType == USB_W_VALUE_DT_INTF) { const usb_intf_desc_t *intf = (const usb_intf_desc_t *)desc; if (intf->bInterfaceClass == USB_CLASS_HID) { // 记录接口编号,用于后续 SET_INTERFACE / SET_PROTOCOL hid_intf_num = intf->bInterfaceNumber; // 遍历该接口下的端点 int ep_offset = offset + intf->bLength; while (ep_offset < config_desc->wTotalLength) { const usb_standard_desc_t *ep_desc = (const usb_standard_desc_t *)(p + ep_offset); if (ep_desc->bDescriptorType == USB_W_VALUE_DT_ENDPOINT) { const usb_ep_desc_t *ep = (const usb_ep_desc_t *)ep_desc; if ((ep->bmAttributes & USB_BM_ATTRIBUTE_XFERTYPE_MASK) == USB_BM_ATTRIBUTE_XFER_INT && (ep->bEndpointAddress & USB_B_ENDPOINT_ADDRESS_EP_NUM_MASK) == 0) { // 0x81 IN 端点 in_ep_addr = ep->bEndpointAddress; } } else if (ep_desc->bDescriptorType == USB_HID_DESC_TYPE_HID) { // 记录 HID 描述符 } ep_offset += ep_desc->bLength; } } } offset += desc->bLength; } }

遍历时按bLength跳转,千万不要硬编码偏移。不同鼠标的配置描述符长度不一样,有些带 HID 报告描述符,有些带多个接口,跳错了就什么也解析不出来。

3.5 设置配置与选择 HID 协议

设备暴露了多个配置时(通常鼠标只有一个),我们需要选择第一个可用的配置,发送SET_CONFIGURATION

usb_host_set_config(client_hdl, dev_hdl, config_desc->bConfigurationValue);

之后对 HID 设备还有一个“协议选择”步骤。HID 规范里定义了 Boot Protocol 和 Report Protocol。鼠标的 Boot Protocol 是固定格式的 3 字节报告:

  • 字节 0:按键状态(bits:0=左键,1=右键,2=中键)
  • 字节 1:X 方向位移(有符号)
  • 字节 2:Y 方向位移(有符号)

相对位移报告里,X/Y 是带符号的 int8_t,向上为正还是向下为正取决于具体的坐标约定,通常在屏幕上 Y 轴向下为正,鼠标向下移动时 Y 会增大。

我们需要发送SET_PROTOCOL(Boot Protocol)请求,这样即使不同厂商的鼠标内部报告格式有差异,Host 也能用统一格式解析:

const usb_ctrl_request_t req = { .bRequestType = USB_B_REQUEST_TYPE_TYPE_CLASS | USB_B_REQUEST_TYPE_RECIP_INTERFACE | USB_B_REQUEST_TYPE_DIR_OUT, .bRequest = USB_HID_REQUEST_SET_PROTOCOL, .wValue = 0, // 0 = Boot Protocol .wIndex = hid_intf_num, .wLength = 0, }; usb_host_transfer_submit_ctrl(client_hdl, &req, NULL, 0, NULL, timeout);

这个请求走控制传输管道,不需要额外分配传输缓冲区。

3.6 中断传输与报告解析

配置完成后,申请一个 URB,提交中断 IN 传输请求:

usb_host_transfer_t *xfer = NULL; usb_host_transfer_alloc(8, 0, &xfer); xfer->device_handle = dev_hdl; xfer->bEndpointAddress = in_ep_addr; xfer->callback = hid_report_cb; xfer->context = NULL; xfer->num_bytes = 8; xfer->data_buffer_size = 8; usb_host_transfer_submit(xfer);

回调函数里,数据已经填充到了xfer->data_buffer,我们只需要按 Boot Protocol 解析:

static void hid_report_cb(usb_host_transfer_t *xfer) { if (xfer->status == USB_TRANSFER_STATUS_COMPLETED) { uint8_t *data = xfer->data_buffer; int8_t dx = (int8_t)data[1]; int8_t dy = (int8_t)data[2]; uint8_t buttons = data[0]; printf("btns=%02x dx=%d dy=%d\n", buttons, dx, dy); // 重新提交下一次中断传输 usb_host_transfer_submit(xfer); } else { // 传输失败,处理异常 } }

注意:usb_host_transfer_submit只能在前一次传输结束后再次调用,不能在回调还没触发时就重复提交。否则会出现多个 URB 挂在同一个端点上,轻则日志报错,重则导致中断传输彻底卡死。

按我的经验,把usb_host_transfer_submit放在回调最后一行是标准做法,既保证连续性,又避免丢事件。

3.7 鼠标数据如何变成屏幕光标

例程里通常还带一个 LCD 显示,把鼠标位移叠加到光标坐标上:

static int16_t cursor_x = 240; static int16_t cursor_y = 135; void mouse_apply_delta(int8_t dx, int8_t dy) { cursor_x += dx; cursor_y += dy; if (cursor_x < 0) cursor_x = 0; if (cursor_x > 479) cursor_x = 479; if (cursor_y < 0) cursor_y = 0; if (cursor_y > 270) cursor_y = 270; lcd_draw_cursor(cursor_x, cursor_y); }

这里会遇到一个实际体验问题:鼠标的位移值默认是“鼠标 tick”,屏幕上光标移动的像素数是 1:1 的,但如果鼠标设置过高的 DPI,tick 值会比较大,光标移动飞快。建议加一个可调缩放系数:

cursor_x += (int16_t)dx * cursor_scale;

我习惯把缩放系数做成一个变量,初始设为 1,需要时在调试终端里调,方便适配不同 DPI 的鼠标。

分辨率这块也要注意,开发板的屏幕分辨率是多少就按多少来,别写死,我的例程试过 480x272 和 800x480 都有适配问题。

4. 实操过程与关键现象记录

4.1 接线与首次上电

按前面说的,把 USB 鼠标插到开发板的 Host 口,上电。首次上电时如果鼠标的 LED 没有亮,先检查是不是 Host 口选错了,或者供电不足。

我强烈建议插那种有线、基础款鼠标。有些电竞鼠标带有多个配置文件,上电时会先进入“固件升级模式”或“自定义 RGB 模式”,枚举过程会异常,表现为枚举成功但拿不到 HID 报告。基础办公鼠标最稳。

如果日志里没有出现New device connected,多半是硬件问题,先用示波器或逻辑分析仪看 D+ 的电平。USB 设备插入后 D+ 会被设备内部上拉电阻拉高(Full Speed 设备拉高 D+),如果 D+ 一直是低电平,说明设备没进入工作状态或者数据线接反了。

4.2 运行日志样例

正常的运行日志大致是:

I (278) usb_host: USB Host installed I (285) usb_host: Client registered I (290) main: Waiting for device... I (310) usb_host: New device connected I (315) usb_host: Device address 1 assigned I (320) usb_host: Config descriptor length 34 I (326) hid_host: HID interface found I (330) hid_host: EP 0x81, interval 10ms I (335) hid_host: Set boot protocol I (340) main: Open device OK I (352) main: btns=00 dx=0 dy=0 I (362) main: btns=01 dx=1 dy=-2 I (372) main: btns=02 dx=0 dy=0

如果日志卡在枚举阶段,比如Set address failed,大概率是 USB 线有问题或者电源不够,换根线试试。

4.3 用屏幕光标验证鼠标通信

如果接入了 LCD,跑起来后移动鼠标,光标会跟着动;按下左键、右键、中键,界面上对应的按键状态会变。这个验证很直观。

没有 LCD 也没关系,串口打印足够验证数据。我习惯把按键、X、Y 三个值用 CSV 格式输出,方便后续用脚本统计丢包率:

event,button,x,y move,0,3,-1 click,1,0,0 move,0,0,1

5. 常见问题与排查技巧实录

5.1 枚举成功后鼠标无数据

现象:日志里New device connectedInterface found都打印了,但btns/dx/dy没有任何输出。

排查思路:

  • 检查是否调用了usb_host_transfer_submit提交中断传输。很多人初始化完配置就忘记提交第一次传输,导致端点一直空闲。
  • 检查端点地址是否解析正确。如果你硬编码0x81但鼠标的 IN 端点实际是0x82,传输自然会失败。必须从配置描述符里解析。
  • 检查传输回调里是否重新提交了下一次传输。如果回调里只是打印一次,没有再次 submit,那就是一次性读到了数据但不再继续了。

这类问题占到整个实验调试量的六成以上。

5.2 鼠标指针乱跳或反向

现象:鼠标不动,光标自己跑;或者左移右移方向相反。

原因分析:

  • 反向:Y 轴的符号约定问题。屏幕坐标系的 Y 轴向下为正,而鼠标报告里的 Y 正负是“相对鼠标移动方向”的,两者正好相反,取反即可。
  • 乱跳:没有按字节长度判断就盲目处理增量,某些鼠标在滚轮滚动时会多发数据,导致后续字节被当成 X/Y。建议严格按 Boot Protocol 长度,如果有滚轮,滚轮在第四个字节(索引 3),不要把滚轮数据当成位移。

5.3 热插拔不稳定

现象:鼠标用着用着突然没反应,拔出重插后有时能恢复,有时不能。

原因:USB 拔出时,Host 端会有USB_HOST_CLIENT_EVENT_DEV_GONE事件。如果应用层没有释放旧设备的资源和 URB,重插时枚举可能失败,因为usb_host_device_open会返回资源不足。

代码里建议在DEV_GONE事件回调中:

usb_host_transfer_free(xfer); usb_host_device_close(client_hdl, dev_hdl);

重新插入后,NEW_DEV事件会再次触发,重新走枚举流程。别偷懒不释放,资源泄漏几次后系统会变得非常不稳定。

5.4 wTotalLength 遍历越界

解析配置描述符时,如果直接用config_desc->wTotalLength作为循环条件,但拿到的指针实际是缓存复用的,可能出现越界读取。我遇到过一次,是因为usb_host_get_active_config_descriptor返回的描述符在客户端事件处理完成后被释放了,回调里再访问就变成悬垂指针。

解决方案:在设备打开后立即把配置描述符拷贝到自己管理的缓冲区中,再解析,不要长期持有库内部的指针。ESP-IDF 文档里也明确说过,配置描述符缓冲区的所有权属于调用者,用完之后要释放。

5.5 鼠标多为 3 字节报告但某些鼠标是 8 字节

有些鼠标,比如带侧键的办公鼠标,Boot Protocol 下报告可能是 3 字节(满足最基本要求),但一些多按键鼠标在 Report Protocol 下会输出 6 到 8 字节的自定义格式。

如果你一定要支持侧键,可以读取 HID 报告描述符,根据 Report ID 和 Usage 解析每个字段。这个工作量大一些,得写一个小的报告描述符解析器。新手阶段先用 Boot Protocol 就够了。

提示:USB 鼠标的 HID 报告描述符可以用/sys/kernel/debug/hid/路径(在 Linux 上)导出,或者用 Wireshark 的 USB 抓包功能,对比分析各个字段的含义,非常直观。

5.6 中断传输的 bInterval 与延迟

鼠标的bInterval常见值是 10(单位 ms),意思是 Host 每 10ms 轮询一次。部分高性能鼠标报告间隔是 1ms 或 2ms。这个值由设备决定,Host 不能随意修改。

如果觉得光标响应有延迟,先检查bInterval是不是太大,再看打印日志的时间戳。实际体验中 10ms 的轮询对普通桌面应用完全够用,肉眼感受不到延迟。

6. 这个实验还可以怎么扩展

做完鼠标 Host 实验,扩展方向非常明确,这里分享几个我觉得性价比很高的方向。

第一个方向是把同一套 Host 框架改造成 USB 键盘。键盘的枚举过程和鼠标几乎一样,只是bInterfaceProtocol = 0x01,报告格式变成 8 字节,Modifier(Shift/Ctrl/Alt)、保留字节、6 个按键码。改一个解析函数就能用。

第二个方向是接收无线鼠标接收器。很多无线鼠标的接收器在主机看来就是一个标准 HID 键盘或鼠标,不需要特殊处理。实测罗技的优联接收器在 ESP32-P4 上可以直接枚举成功,省去单独调 RF 协议。

第三个方向是结合 LVGL 做 HID 输入控制。在开发板的触摸屏 UI 上,把鼠标当作一个额外的输入设备,实现桌面式的操作体验。LVGL 的lv_indev_drv_t注册一个 pointer 设备,喂 dx/dy 和按键状态就行。

第四个方向是 USB Host 加蓝牙 Host 共存,做一个统一的外设接入网关,鼠标键盘既可以有线接入也可以蓝牙接入,然后统一注入到上位机系统。这在工控、教育硬件里应用挺多。

我个人更看好的是把 USB Host 和 TinyUSB Device 做成“双角色动态切换”的小工具:平时接电脑当 HID 设备,切换到 Host 模式读取 U 盘或鼠标。P4 的两个 USB 控制器正好可以一个做 Host 一个做 Device,这种玩法在之前的 ESP32 上很难实现。当然这是后话,先把鼠标 Host 实验吃透,后面一切好说。

我自己在调完这个实验之后,最大的感受是:USB 协议没有想象中那么神秘,它就是把“设备是什么、怎么通信、传什么格式的数据”这三件事用描述符写得清清楚楚。能读完一篇设备描述符,再照着它的约定去发请求、读端点,USB 开发你就已经入门了。剩下的多是一些设备的“不标准行为”,那就靠多抓包、多对比各种鼠标来积累经验了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询