做HID设备双向通信的时候,我在LAT1658这块STM32H5板卡上踩了一个很典型的坑:USBx中间件默认生成的HID工程,只有一条IN端点,设备能往主机发数据,主机却连一条指令都发不下来。鼠标键盘这类纯上报场景够用,但一旦涉及双向交互,比如上位机下发PID控制参数、校准指令、配置信息,缺少OUT端点就等于通道只修了一半。这篇文章就记录我如何在USBx裸机环境下,给HID类设备手工添加OUT端点,补上主机到设备的这条下行链路,实现完整的双向通信。整个过程涉及HID报告描述符、配置描述符、USBx类驱动回调、端点接收缓冲管理几个关键改动,适合手头正好在用STM32H5或同系列芯片做USB HID设备的开发者参考。
1. 先把需求看清楚:为什么HID默认只有IN端点
1.1 HID类设备的基本模型与端点方向
USB协议里,端点方向是以主机为参照的。IN端点是设备往主机方向传输数据,OUT端点是主机往设备方向传输数据。HID类全称Human Interface Device,最初为键盘、鼠标、游戏手柄这类人机交互设备设计,典型工作模式就是设备采集用户的按键、移动轨迹,然后通过IN端点主动上报给主机。ST的USBx中间件在生成HID工程时,自然只注册了一个IN端点,地址通常是0x81,中断传输类型,最大包长64字节。
这样设计没有错,但问题在于,一旦你把HID当作通用双向通信手段来用,默认模板就不够用了。比如设备需要接收主机下发的PID参数、阈值配置,或者主机要向设备发起某个控制指令,你会发现上位机根本找不到能写的接口。因为配置描述符里只声明了IN端点,主机侧驱动就认为这个设备只支持从设备读取数据,写操作无路可走。
1.2 为什么在USBx下继续选HID而不是换其他类
有人会问,既然HID默认只有IN端点,干脆换USB CDC(虚拟串口)或者Vendor自定义类不就行了?我在实际选型的时候权衡过这几点,最终还是用HID:
第一,免驱动。HID是系统原生支持的类,Windows、Linux、macOS插上就能用,不需要安装额外的驱动文件。CDC类在Windows上虽然也有系统驱动,但不少精简系统或者特定环境下会出现驱动签名、兼容性问题。Vendor自定义类就更麻烦,必须自己写驱动或者依赖WinUSB/WDM方案。
第二,权限友好。HID设备在Linux下访问通常只需要基本的USB权限配置,不像某些自定义设备动不动要root权限。在Windows下直接用HID API读写,开发门槛低。
第三,改动范围可控。USBx的HID模板已经帮你把类描述符、初始化流程、IN端点收发回调搭好了,只要在原有基础上补一个OUT端点,就能实现双向,整体代码结构清晰,出问题也好排查。
所以结论很直接:在HID类上补OUT端点,是改动最小、成本最低、兼容性最好的双向通信方案。
1.3 添加OUT端点的整体思路
一句话概括整个改造思路:要让主机识别到设备具备下行接收能力,必须同时修改三类信息,缺一不可。
报告描述符(Report Descriptor)里要声明Output报表。HID设备的读写能力不只由端点决定,还依赖报告描述符定义。只加端点不加Output报告,系统可能仍然认为设备只支持输入。
配置描述符(Configuration Descriptor)里要增加OUT端点描述符。这个很好理解,主机枚举时就是靠它知道设备有哪些端点可用。
代码层要注册OUT端点并实现接收回调。描述符只是让主机"知道"有这个端点,真正要让数据进得来,还得在USBx的类回调里把OUT端点打开,准备好接收缓冲区,并处理PCD层的数据到达事件。
这三个改动必须同步完成,漏掉任何一个,双向通信都会以某种诡异的方式失败。后面几章我按实际操作顺序拆开讲。
2. 准备工作:从CubeMX生成HID工程开始
2.1 LAT1658板卡的工程环境
LAT1658是我手头这块基于STM32H5系列MCU的评估板。STM32H5是Cortex-M33内核,主频能跑到250MHz,片上带一个USB_DRD_FS外设,支持全速USB2.0,可以作为Device、Host或者DRD(双角色设备)使用。做HID设备时,我把它配置为Device Only模式。
工程用STM32CubeMX搭建,步骤很简单:
在Pinout&Configuration里使能USB_DRD_FS,模式选Device Only。中间件栏会多出USBx Device Class for USB FS选项,打开后Class选HID。时钟配置里记得把USB时钟源设为48MHz,STM32H5的USB外设对时钟要求比较严格,时钟不对会直接导致枚举失败。代码生成时选择单独的main.c和中断管理方式,方便后续手动添加代码。
这里要提醒一下,STM32H5使用USBx中间件,不是老的STM32 USB Device Library。两者的类驱动接口、文件名、回调函数定义都有差异。网上很多教程还是老的USB Device库写法,直接套到USBx上编译会报错,看代码时要先确认工程用的到底是哪个中间件。
2.2 生成的HID代码骨架分析
CubeMX生成后,和HID相关的核心代码主要在三个位置:
usbd_hid.h和usbd_hid.c,这是USBx的HID类驱动。里面定义了HID_EPIN_ADDR、HID数据最大包长、类初始化回调、配置描述符数组等。我们后面的大部分改动都在这个文件里。
usbd_desc.c,存放设备描述符、字符串描述符,以及PID/VID定义。HID描述符的String Descriptor等也在这里。
main.c,包含MX_USB_DEVICE_Init等初始化流程。USBx中间件的初始化顺序是USBD_Init、USBD_RegisterClass、USBD_Start,这个流程不用改,我们只是在HID类驱动内部动刀。
打开usbd_hid.h,你会看到类似这样的定义:
#define HID_EPIN_ADDR 0x81U #define HID_DATA_MAX_PACKET_SIZE 64U #define USB_HID_CONFIG_DESC_SIZ 34U注意这里只有HID_EPIN_ADDR,没有HID_EPOUT_ADDR,这就是默认模板"只支持上行"的证据。usbd_hid.c里的HID_IO_CfgDesc是配置描述符数组,类初始化函数HID_IO_ClassInit里用USBD_LL_OpenEP注册了IN端点,但同样没有涉及OUT端点的事。
2.3 裸机环境下的中断与回调机制
有人可能担心,裸机下做USB通信会不会很难处理收发时序。其实USBx中间件已经把底层数据搬运、协议解析都封装好了,裸机环境我们要做的事情远没有想象中复杂。
STM32H5的USB_DRD_FS产生中断后,中断服务函数进入HAL_PCD_IRQHandler,PCD层解析事件后回调HAL_PCD_DataOutStageCallback、HAL_PCD_DataInStageCallback等函数,USBx内核在回调中完成Class分发,最终调用HID类驱动里的OutEvent、DataIn等回调函数。这条链路在USBx的USB中断处理函数中自动完成。
裸机环境下,应用层只需要做两件事:在OutEvent回调中把数据从USBx的缓冲区搬走并置一个标志位,主循环轮询这个标志位做后续处理。回调函数里不要做耗时操作,这是裸机USB编程的铁律。数据搬走,标志置位,剩下的交给主循环,这样既保证了USB中断的实时响应,又不阻塞协议栈运行。
3. 添加OUT端点的完整实操流程
3.1 第一步:修改HID报告描述符
先改报告描述符,因为很多HID调试工具在上位机打开设备时,会先读取报告描述符决定是否支持写操作。如果报告描述符里没有Output报表,哪怕底层端点已经加好了,设备管理器里看也是正常的,但HID API的写接口依然不可用。
HID报告描述符是一串有固定格式的字节数组。我在usbd_hid.c中找到了HID_ReportDesc数组,默认是一份标准的鼠标报告描述符,长度34字节左右。我把它替换为自定义的双向通信报告描述符:
__ALIGN_BEGIN static uint8_t HID_ReportDesc[] __ALIGN_END = { 0x06, 0x00, 0xFF, // USAGE_PAGE (Vendor Defined) 0x09, 0x01, // USAGE (Vendor Usage 1) 0xA1, 0x01, // COLLECTION (Application) // Input report: 设备上报到主机,64字节 0x09, 0x01, // USAGE (Vendor Usage 1) 0x15, 0x00, // LOGICAL_MINIMUM (0) 0x26, 0xFF, 0x00, // LOGICAL_MAXIMUM (255) 0x75, 0x08, // REPORT_SIZE (8) 0x95, 0x40, // REPORT_COUNT (64) 0x81, 0x02, // INPUT (Data,Var,Abs) // Output report: 主机下发到设备,64字节 0x09, 0x02, // USAGE (Vendor Usage 2) 0x15, 0x00, // LOGICAL_MINIMUM (0) 0x26, 0xFF, 0x00, // LOGICAL_MAXIMUM (255) 0x75, 0x08, // REPORT_SIZE (8) 0x95, 0x40, // REPORT_COUNT (64) 0x91, 0x02, // OUTPUT (Data,Var,Abs) 0xC0 // END_COLLECTION };这段描述符定义了两个方向各64字节的报告,方向由Input和Output项区分,主机侧HID API的read对应Input,write对应Output。注意Report Count我设为0x40也就是64字节,和端点最大包长保持一致,这样一包数据正好对应一份报告,逻辑简单清晰。
如果你需要更复杂的交互,可以引入Report ID来区分多份报告,比如一个ID用于数据通道,另一个ID用于配置通道。但基础双向通信,上面这份已经够用,Report ID的引入反而会让缓冲区和报告解析复杂不少。
3.2 第二步:修改配置描述符数组
配置描述符是主机枚举时判断设备端口能力的核心依据。USBx的HID模板默认生成一份只有IN端点的配置描述符,原始数组大致是:
__ALIGN_BEGIN static uint8_t USBD_HID_CfgDesc[] __ALIGN_END = { 0x09, // bLength: Configuration Descriptor 0x02, // bDescriptorType: Configuration USB_HID_CONFIG_DESC_SIZ, 0x00, // wTotalLength 0x01, // bNumInterfaces 0x01, // bConfigurationValue 0x00, // iConfiguration 0x80, // bmAttributes: Bus Powered 0x32, // bMaxPower (100mA) 0x09, // bLength: Interface Descriptor 0x04, // bDescriptorType: Interface 0x00, // bInterfaceNumber 0x00, // bAlternateSetting 0x01, // bNumEndpoints: 1 0x03, // bInterfaceClass: HID 0x00, // bInterfaceSubClass 0x00, // bInterfaceProtocol 0x00, // iInterface 0x09, // bLength: HID Descriptor 0x21, // bDescriptorType: HID 0x11, 0x01, // bcdHID 1.11 0x00, // bCountryCode 0x01, // bNumDescriptors 0x22, // bDescriptorType: Report sizeof(HID_ReportDesc), 0x00, // wDescriptorLength 0x07, // bLength: Endpoint Descriptor 0x05, // bDescriptorType: Endpoint HID_EPIN_ADDR, // bEndpointAddress: IN 0x03, // bmAttributes: Interrupt HID_DATA_MAX_PACKET_SIZE, 0x00, // wMaxPacketSize 0x01, // bInterval: 1ms };这个数组实际长度是43字节,和CubeMX里USB_HID_CONFIG_DESC_SIZ宏默认值不一致是正常的,因为宏是中间件模板预设值,并不是数组的真实长度。我第一反应是直接改这个数组追加端点描述符,但有一个更重要的地方必须先改:USB_HID_CONFIG_DESC_SIZ宏。
修改后的配置描述符数组在接口描述符部分,把bNumEndpoints从1改成2:
0x02, // bNumEndpoints: 2然后在IN端点描述符后面补齐OUT端点描述符:
0x07, // bLength: Endpoint Descriptor 0x05, // bDescriptorType: Endpoint HID_EPOUT_ADDR, // bEndpointAddress: OUT 0x03, // bmAttributes: Interrupt HID_DATA_MAX_PACKET_SIZE, 0x00, // wMaxPacketSize 0x01, // bInterval: 1ms同时把数组开头的wTotalLength从0x2B(43字节)改成0x32(50字节),因为多了一个7字节的端点描述符。这里必须手动重新计算总长度,否则主机读取配置描述符时长度不匹配,会直接导致枚举失败。
usbd_hid.h里的USB_HID_CONFIG_DESC_SIZ宏也要同步改成50,这个宏被多处引用,比如类初始化时分配描述符缓冲区大小,不同步的话USBD_GetCfgDesc等接口返回的长度错误,同样会引起枚举异常。
3.3 第三步:在类驱动中注册OUT端点
描述符改完后,主机已经知道设备有OUT端点,但USBx中间件在初始化时并不会主动打开这个端点。如果不注册,OUT端点就像一扇没有安装的门,门框上是标了位置,但实际没法通过。必须在HID_IO_ClassInit中把OUT端点打开。
打开usbd_hid.c,在HID_IO_ClassInit里找到IN端点注册的那行代码,紧跟着加上OUT端点的注册:
static uint8_t HID_IO_ClassInit(USBD_HandleTypeDef *pdev, uint8_t cfgidx) { // 原有IN端点注册 USBD_LL_OpenEP(pdev, HID_EPIN_ADDR, USBD_EP_TYPE_INTR, HID_DATA_MAX_PACKET_SIZE); pdev->pClassData = USBD_malloc(sizeof(USBD_HID_HandleTypeDef)); // 新增加代码:打开OUT端点并预备接收 USBD_LL_OpenEP(pdev, HID_EPOUT_ADDR, USBD_EP_TYPE_INTR, HID_DATA_MAX_PACKET_SIZE); USBD_LL_WriteRx(pdev, HID_EPOUT_ADDR, HID_RxBuffer, HID_DATA_MAX_PACKET_SIZE); return USBD_OK; }同时在usbd_hid.h中补充OUT端点地址定义:
#define HID_EPOUT_ADDR 0x01UUSBD_LL_OpenEP的第一个参数是端点地址,注意虽然OUT端点地址常写为0x01,但在USBx的寄存器操作中,它会通过地址的低4位加上方向位来区分IN和OUT。USBD_LL_WriteRx是准备接收缓冲区的关键调用,它告诉USB硬件控制器,收到OUT数据时往哪个缓冲区写,写多少字节。这一步不做,即使端点打开了,主机发数据设备也收不到。
3.4 第四步:实现OUT数据的接收回调
USBx的HID类驱动中定义了一个接口结构体USBD_HID_ItfTypeDef,包含初始化、反初始化、Setup、OutEvent、DataIn等回调。默认模板里OutEvent通常是空函数或者是注释掉的状态,我们需要把它改成实际可用的接收处理函数。
在工程中,HID_OutEvent_FS这个函数是开放给应用层的。CubeMX生成的模板里它可能长这样:
static int8_t HID_OutEvent_FS(uint8_t *buf, uint32_t len) { return USBD_OK; }这个函数被USBx在接收完一包OUT数据后调用,buf指向USBx内部的接收缓冲区,len是实际接收到的数据长度。我在这个函数里加上数据搬运和标志位操作:
static int8_t HID_OutEvent_FS(uint8_t *buf, uint32_t len) { HID_RxLen = len; memcpy(HID_RxBuffer, buf, len); HID_RxFlag = 1; // 准备下一次接收,必须在数据被搬离USBx缓冲区后重新调用 USBD_LL_WriteRx(&hUsbDeviceFS, HID_EPOUT_ADDR, HID_RxBuffer, HID_DATA_MAX_PACKET_SIZE); return USBD_OK; }关键点在于,处理完后必须再次调用USBD_LL_WriteRx。这个函数的作用是重新武装端点,让USB外设可以继续接收下一包数据。如果漏掉这一步,第一次接收能成功,后续所有OUT数据都会石沉大海。
HID_RxBuffer和HID_RxFlag、HID_RxLen这些变量,我放在usbd_hid.c文件头部用volatile修饰,裸机环境下主循环轮询HID_RxFlag判断是否有新数据到达。volatile关键字必须加,否则编译器可能会把轮询优化成死循环。
3.5 实际修改时最容易踩的三个细节
这段实操里,我梳理了三个很容易出问题但又不太起眼的地方,值得单独提醒。
第一,接口结构体里的OutEvent不能直接改名字或者随便新增。USBx的HID_IO_Init函数里,OutEvent回调是通过pdev->pData接口调用的,如果你改写了USBD_HID_ItfTypeDef结构体的字段或者函数签名没对上,编译可能过,但运行时会导致回调不触发。
第二,USBD_LL_WriteRx的缓冲区必须保证在USB传输期间不被释放或改写。我最初图省事,直接把局部变量传进去作为接收缓冲,结果数据全被覆盖了。一定要用全局数组或者长期有效的静态缓冲区,长度不小于最大包长。
第三,端点地址方向位不能搞错。IN端点地址0x81,OUT端点地址0x01,高方向位0x80是标识方向用的。你不能把OUT端点地址也写成0x80或者0x81,否则USBD_LL_OpenEP内部会按IN端点处理,总线枚举时就会出问题。
4. 双向通信的收发流程与验证手段
4.1 主机到设备的完整下行链路
以Windows上位机通过HID API向设备发送64字节数据为例,完整流程是这样的:
上位机调用HidD_SetOutputReport或者WriteFile,数据先经过Windows HID类驱动,再由USB主机控制器以中断传输的形式发送到OUT端点。STM32H5的USB_DRD_FS外设收到数据后,PCD层产生DataOut事件,USBx内核查找到对应的是HID类,调用HID_IO_OutEvent回调。我们在回调里把数据从USBx缓冲区拷贝到应用缓冲区,置HID_RxFlag标志。主循环检测到标志后,进入数据处理逻辑,比如解析PID参数并应用到控制环路。
这条链路中,最容易出问题的环节是USBD_LL_WriteRx的重新武装。如果回调执行完没有再次调用,端点缓冲区就一直处于"已满"状态,后续所有OUT传输都会被主机认为失败或者根本不会发起。这算USBx HID双向通信里最典型的"只通一次"问题,排查时优先检查这里。
4.2 设备到主机的上行链路
设备到主机的发送,用USBx提供的接口直接发送即可:
uint8_t data[64]; // 填充 data USBD_HID_SendReport(&hUsbDeviceFS, data, sizeof(data));这个函数把数据放入IN端点发送缓冲区,USBx会在合适时机发送。裸机环境下要注意发送频率,上一次发送还没完成就再次调用,可能返回USBD_BUSY。我的做法是维护一个发送完成标志,在HID_IO_DataIn回调里置位,主循环发送前检查这个标志。
上行链路在默认HID模板里本来就是完整的,所以这部分改动不多,主要注意发送节奏和缓冲管理。
4.3 用HID调试工具和Python脚本验证
验证双向通信,我推荐三层手段:设备管理器看枚举信息、HID调试助手交互测试、自写脚本做自动化验证。
设备管理器或者UsbTreeView可以直观看到设备枚举后的接口和端点列表。如果修改成功,UsbTreeView里应该能看到HID接口下有两个中断端点,一个是0x81 IN,一个是0x01 OUT。如果只有IN端点,说明配置描述符有问题,主机没解析到OUT端点。
交互测试我用过不少HID调试助手,这类工具通常会列出系统里的HID设备,显示设备的输入报告和输出报告,可以直接在界面上填写数据并发送。但在Windows下,部分工具对自定义HID设备的写支持不是很好,如果一个工具不支持写,换个工具再试,有时候不是设备问题,是工具本身的兼容性问题。
自动化验证我推荐用Python配合hidapi库,代码量很小:
import hid # 替换为自己的VID/PID device = hid.device() device.open(0x1234, 0x5678) # 读取设备输入报告(IN方向) data = device.read(64, timeout=1000) print("recv from device:", data) # 发送数据到设备(OUT方向) device.write([0x00] + list(range(1, 65)))注意hidapi的write需要在数据前加一个Report ID字节,即使你没有显式使用Report ID,这个字节也要留出来写0x00。这个细节很多人第一次会忽略,导致上位机报错或者数据错位。
Linux下我用同样的hidapi库跑测试,效果一致。如果系统识别到HID设备但没有读写权限,需要给设备添加udev规则,这个和使用USBx的裸机配置无关,属于Linux USB权限管理的基本操作。
4.4 从协议层面确认数据正确性
不少朋友在双向通信成功后,直接拿业务逻辑测试,一旦数据不对就怀疑是端点代码的问题。我的习惯是先做环回测试:设备收到主机发来的64字节数据后,原样或者加上固定特征码再通过IN端点发回主机。上位机检查环回数据和发送数据是否一致。这样能快速把USB链路的问题和业务逻辑的问题分开。
环回测试通过后,再把业务数据格式套进来。这个过程看起来绕了一圈,实际上排查效率最高,避免把USB层问题和应用层问题混在一起。我这次调试PID参数下发功能,就是用环回测试确认链路没问题后,才去查业务解析的bug,结果真的发现上位机的字节序和设备的解析逻辑不一致,跟USB本身一点关系都没有。
5. 常见问题与排查实录
5.1 典型问题速查表
我把实际操作中遇到的典型问题整理成了一张表,方便对照排查。
| 现象 | 可能原因 | 排查和解决方法 |
|---|---|---|
| 枚举失败,设备管理器显示未知设备 | 配置描述符长度错误、wTotalLength未同步、端点地址冲突 | 用UsbTreeView抓取描述符原文,逐字节核对代码数组 |
| 设备枚举成功但HID工具无法打开 | 报告描述符没有Output项 | 检查报告描述符是否包含OUTPUT (0x91)字段 |
| OUT发送一次后后续全部失效 | 接收回调没有重新调用USBD_LL_WriteRx | 在OutEvent处理完数据后立即重新武装端点 |
| 设备能上报,主机写操作报错 | OUT端点没有注册或地址写错 | 检查HID_IO_ClassInit是否调用USBD_LL_OpenEP |
| 接收数据错位或长度不对 | hidapi发送时少了Report ID引导字节 | 上位机write时在第0字节填0x00 |
| 枚举正常但系统提示设备无法启动 | bInterval为0或过大 | 全速中断端点bInterval建议为0x01到0x10 |
| IN发送偶发失败 | 上一次发送未完成就再次调用SendReport | 用DataIn回调标志位控制发送节奏,避免连续调用 |
5.2 我被"Code 43"折腾了一天
这次调试过程中,最让我记忆深刻的是设备管理器反复报Code 43,设备一插入就显示"Windows已停止此设备,因为其有问题"。最开始我一度以为是时钟配置问题,把HSI48、PLL各种时钟源试了个遍,问题依旧。后来用UsbTreeView抓取总线上的描述符,才发现配置描述符长度字段是34,但实际数组长度是43,主机解析时读取长度失败,整个描述符被判定为非法。
这里经验就是,改描述符类代码时,必须用总线抓包工具验证实际枚举内容,不要只看代码逻辑。UsbTreeView、Wireshark USBPCAP、逻辑分析仪,能抓到原始描述符数据的工具都可以。手动计算描述符长度容易漏,尤其当你在数组中间插入了字段,后续长度都要重新算一遍。
5.3 接收数据偶尔丢包的定位过程
还有一个问题让我花了不少时间:主机连续下发数据时,设备偶发性丢包。起初以为是USBx缓冲不够,把HID_RxBuffer加大后问题依旧。后来在OutEvent回调里加了串口调试输出,发现丢包发生在上位机快速连续写操作时,往往是上一次接收还没处理完,下一次就来了。
USBx协议栈在OutEvent回调返回后才会处理下一个OUT事务,如果OutEvent里耗时太长,端点来不及重新武装,新数据就会丢失。我在OutEvent里只做数据搬运和标志置位,把数据解析全部移到主循环,丢包问题明显缓解。如果数据量更大,可以考虑双缓冲机制,OutEvent里交替使用两块缓冲区,能进一步降低丢包概率。
5.4 裸机环境下两个容易被忽略的优化点
第一,主循环轮询间隔不要太长。裸机环境下主循环可能被其他任务占用,如果设备处理USB接收不及时,缓冲区数据可能被覆盖。我的做法是保证主循环周期在1ms以内,同时给HID_RxFlag设置一个超时清零机制,防止标志位长期置位导致逻辑误判。
第二,所有在中断和主循环共享的变量,都用volatile修饰。裸机开发没有RTOS的互斥锁机制,编译器优化有时候会踩坑。比如HID_RxFlag如果不用volatile,主循环里可能出现永久轮询不到的情况,这个坑排查起来非常隐蔽。
5.5 其他类似设备的经验迁移
这次调试经验同样适用于其他使用USBx中间件的芯片,比如STM32F4、STM32L4等。这些芯片的USBx HID类驱动结构高度相似,只是底层的PCD实现略有差异。如果你在用I2C HID设备,设备管理器报感叹号,原理其实是相通的,都是描述符解析失败。I2C HID设备的描述符在I2C控制器里通过HID Descriptor寄存器暴露给主机,如果寄存器值配错,主机同样无法正确枚举。排查思路和USB HID是类似的,都是沿着描述符的字节流去核对。
6. 最后的实操体会
这块LAT1658板子的USB功能调试完以后,我最大的感受是:USB协议栈本身并不难,难的是对协议字段的理解和对调试工具的熟练使用。很多USB问题看起来莫名其妙,实际上就是描述符里某个字段值不对,用抓包工具对照协议规范看一遍,问题基本都能浮出水面。
修改HID双向通信时,我建议按这个顺序操作:先改报告描述符,再改配置描述符,再注册端点和实现回调,最后用环回测试验证链路。每一步改完都可以编译烧录,在设备管理器里看枚举结果对不对,不要一上来把改动全部做完再调试,否则出了问题你根本不知道是哪个环节引入的。
另外,如果后续想在这个基础上做复合HID设备,比如一个接口同时支持键盘、鼠标和自定义双向通道,核心思路是一样的,只是描述符的并集关系会更复杂,Report ID要细分到每个功能上。万变不离其宗,把OUT端点的添加逻辑吃透,后面扩展就顺理成章了。