尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32 USB复合设备实战:HID+CDC设计要点与调试经验

STM32 USB复合设备实战:HID+CDC设计要点与调试经验 简介这是一份基于STM32F103ZE与Keil 5实现的USB复合设备工程将HID与CDC功能融合于同一USB接口适用于需要同时进行人机交互与虚拟串口通信的嵌入式开发场景。资源使用STD标准库并基于官方例程修改实测可在电脑上同时识别HID和CDC且两者可并行工作为学习USB协议栈和设备枚举提供直接参考。压缩包共253个文件约6.75MB其中源代码以c/h文件为主同时包含uvprojx工程文件、hex/axf烧录文件、map/lst编译输出及配置文件便于直接打开、编译与烧录验证。项目涵盖大量STM32标准外设驱动如定时器、ADC、USART、I2C等可作为外设调用和USB复合设备移植的模板。已有2114人学习下载适合具备一定单片机基础、希望深入理解USB复合设备开发的工程师与学生。 做嵌入式这些年USB 这块儿算是既绕不开又容易踩坑的环节。最近把一个 HIDCDC 复合设备工程整理出来了也就是一个 USB 设备同时枚举出键盘/鼠标HID和虚拟串口CDCWindows 下能看到一个 HID 设备加一个 COM 口。这个组合在游戏外设、工控 HMI、测试仪器、刷卡终端里非常常见——一个设备既要上报按键状态又要和上位机跑私有协议甚至还要支持固件升级。这篇文章就围绕这个工程讲讲设计思路、描述符怎么写、STM32 上怎么落地以及我在调设备管理器里那堆报错时攒下来的排查经验。不管是刚接触 USB 的初学者还是被复合设备折磨过的老手这篇都应该能帮你省点时间。1. 为什么要做 HIDCDC 复合设备1.1 一个设备同时干两件事的真实场景先说最常见的场景我做这类设备的起因是一个桌面控制器项目。硬件上有几个实体按键、一个旋钮背后还要跟 PC 端软件通信。如果用两个独立 USB 设备一个 HID 一个串口那就得占两个 USB 口产品形态和用户体验都很糟糕。用复合设备就能在一个 USB 口上同时搞定按键、旋钮、LED 状态这类实时性要求高的走 HID配置参数、日志输出、上下位机协议走 CDC 虚拟串口。这种需求其实覆盖面很广。游戏外设里的宏键盘按键映射用 HID 实现同时还要一个串口给驱动做配置和固件升级工控领域的人机界面触摸屏的坐标上报走 HID和 PLC 或上位机的 Modbus 协议走 CDC医疗设备里操作按键走 HID生命体征数据流走 CDC。可以说只要设备同时具备“人机交互”和“数据通信”这两类功能HIDCDC 就是最合理的 USB 实现方案。还有一个非常典型的应用是自动化测试治具。我之前做过一个测试工装通过 HID 模拟键盘输入完成被测产品的操作同时通过 CDC 回读设备输出的调试串口日志。这样一台测试电脑只需要插一根 USB 线既不用额外转接板也不用手动插拔切换非常稳。1.2 复合设备和联合设备怎么选刚接触这个概念的人容易把复合设备(Composite Device)和联合设备(Compound Device)搞混。我简单解释一下一个物理 USB 设备里包含多个功能接口每个接口独立工作这叫复合设备。如果设备里内置了一个 USB HubHub 下面挂了多个独立的物理功能设备这叫联合设备。复合设备的优势在于成本低、体积小、不需要 Hub 芯片而且枚举后每个功能接口在操作系统里还是独立设备节点。缺点是实现复杂度高描述符要做精细的接口和端点规划。联合设备实现上反而更像“插了多个独立设备”驱动兼容性天然好但硬件上多一颗 Hub 芯片成本和功耗都上去了。对于 HIDCDC 这种组合绝大多数情况选复合设备就够了。而且标准做法是用接口关联描述符(IADInterface Association Descriptor)把属于同一个功能的多个接口绑定在一起这样操作系统才能正确地把 CDC 的通信接口和数据接口识别成一个完整的虚拟串口而不是报错或者只正常一半。2. 描述符的设计整个工程的灵魂2.1 设备描述符为什么是 0xEF、0x02、0x01USB 设备枚举过程中主机首先要读设备描述符这里面 bDeviceClass、bDeviceSubClass、bDeviceProtocol 这三个字段决定了设备的“身份”。对于单纯的 HIDbDeviceClass 是 0x03对于单纯的 CDCbDeviceClass 是 0x02。但复合设备里有多个功能设备级就无法用单一类别概括了所以 USB-IF 规定用 0xEF、0x02、0x01 表示“杂项设备使用 IAD 描述符组织多个接口”。这个细节非常关键。如果设备描述符直接写了 0x00 或者只写了 HID 的 0x03某些操作系统可能不会正确解析后面的 IAD导致 CDC 功能无法被识别。我之前就见过有人把设备描述符写成 bDeviceClass0x00结果 Windows 下只识别出 HIDCDC 一直报“未知 USB 设备”。设备描述符还需要注意的一点是 idVendor、idProduct 和 bcdDevice 的组合。Windows 会用这三个字段生成设备实例 ID 并匹配驱动调试阶段建议固定一个厂商 ID比如 0xFFFF 这类开发者常用的范围避免每次改固件后 Windows 把设备和旧驱动缓存搞混。2.2 CDC 的接口组合通信接口 数据接口CDCCommunication Device Class实现虚拟串口时通常需要两个接口一个是通信接口Communication Interface负责管理类操作比如设置波特率、DTR/RTS 信号另一个是数据接口Data Interface负责真正的数据收发。为了让操作系统知道这两个接口属于同一个功能必须用 IAD 把它们关联起来。这里有一个很多初学者容易踩的坑IAD 的 bFirstInterface 和 bInterfaceCount 必须和后面的接口编号严格一致。比如 IAD 声明从接口 0 开始、共 2 个接口那么通信接口必须是接口 0数据接口必须是接口 1。如果中间多写了一个接口或者编号顺序乱了Windows 会直接认为描述符非法设备根本无法完成枚举。CDC 功能描述符这块Header、Call Management、ACMAbstract Control Management、Union 四个功能描述符一个都不能少。尤其是 Union 描述符它的 bControlInterface 和 bSubordinateInterface 指定了通信接口和数据接口的归属关系。很多 CDC 串口被识别成“无法启动的设备”或者干脆不出现 COM 口都是因为 Union 描述符里的接口编号写错了。2.3 HID 报告描述符键盘、鼠标还是自定义设备HID 部分的核心是报告描述符它决定了设备是键盘、鼠标还是一个自定义 HID。最简单的键盘报告描述符大概是这样的0x05, 0x01, // Usage Page (Generic Desktop) 0x09, 0x06, // Usage (Keyboard) 0xA1, 0x01, // Collection (Application) 0x05, 0x07, // Usage Page (Keyboard) 0x19, 0xE0, // Usage Minimum (Left Control) 0x29, 0xE7, // Usage Maximum (Right GUI) 0x15, 0x00, // Logical Minimum (0) 0x25, 0x01, // Logical Maximum (1) 0x75, 0x01, // Report Size (1) 0x95, 0x08, // Report Count (8) 0x81, 0x02, // Input (Data, Variable, Absolute) 0x95, 0x01, // Report Count (1) 0x75, 0x08, // Report Size (8) 0x81, 0x01, // Input (Constant) 0x95, 0x05, // Report Count (5) 0x75, 0x01, // Report Size (1) 0x05, 0x08, // Usage Page (LEDs) 0x19, 0x01, // Usage Minimum (Num Lock) 0x29, 0x05, // Usage Maximum (Kana) 0x91, 0x02, // Output (Data, Variable, Absolute) 0x95, 0x01, // Report Count (1) 0x75, 0x03, // Report Size (3) 0x91, 0x01, // Output (Constant) 0x95, 0x06, // Report Count (6) 0x75, 0x08, // Report Size (8) 0x15, 0x00, // Logical Minimum (0) 0x25, 0x65, // Logical Maximum (101) 0x05, 0x07, // Usage Page (Keyboard) 0x19, 0x00, // Usage Minimum (Reserved) 0x29, 0x65, // Usage Maximum (Keyboard Application) 0x81, 0x00, // Input (Data, Array) 0xC0 // End Collection这段描述符定义了一个标准 8 字节键盘报告第 1 字节是修饰键Ctrl、Shift、Alt 等第 2 字节保留第 3 到第 8 字节是当前按下的按键键码。这种格式的好处是能被 BIOS/Windows/Linux 原生识别不需要额外驱动。但如果你的产品不是标准键盘鼠标而是自定义数据上报设备那就得用 Vendor-defined Usage Page 写自定义报告描述符。比如我做过的一个旋钮设备报告里包含一个 16 位编码器值和两个开关状态这时候报告长度、字段偏移都由自己定义Windows 通过 HID API 读写。这种方案更灵活但上位机要自己处理 HID 报告开发量会增加一些。3. STM32F103 上的实操实现3.1 工程搭建与 USB 库选型以最常见的 STM32F103C8T6 为例这颗芯片内置全速 USB 2.0 设备控制器有 8 个端点EP0-EP7每个端点都有独立的缓冲区描述表。实现 HIDCDC 复合设备的方案有两条路一是用 STM32CubeMXHAL 库生成工程二是用老的 StdPeriph 库加官方 USB 例程移植。我的建议是直接用 CubeMX虽然生成的代码量看起来大但 USB 描述符、端点初始化这些底层的东西都不用自己写你只需要在 usbd_desc.c、usbd_cdc_if.c、usbd_hid.c 这几个文件里改配置。CubeMX 的 USB_DEVICE 配置里可以勾选 CDC 和 HID但要注意CubeMX 默认生成的工程往往是“CDC HID 两个独立类”的枚举方式并不一定带 IAD。严格来说HID 是单接口设备CDC 是双接口设备两个类放在同一个配置描述符里设备描述符的类别字段必须按 IAD 方式声明否则 Windows 对 CDC 的识别会出问题。实操上我通常先在 CubeMX 里勾选 USB_DEVICE 并选 Communication Device Class再手动把 HID 相关的描述符、端点和回调函数加进去。这样能少走很多弯路因为 CubeMX 原生生成的 CDC 描述符结构已经很完整HID 部分手动扩展相对容易。3.2 端点分配与 PMA 缓冲区规划STM32F103 的 USB 控制器使用 Packet Memory AreaPMA来存放端点数据。每个端点收发各需要一块 PMA 缓冲区大小必须是 2 的整数倍且至少 2 字节。复合设备里端点资源需要精打细算我的分配方案如下端点方向传输类型功能PMA 大小EP0IN/OUT控制枚举与标准请求6464 字节EP1IN批量CDC 数据上传设备到主机64 字节EP2OUT批量CDC 数据下发主机到设备64 字节EP3IN中断HID 数据上报16 字节或报告长度因为 F103 只有 8 个端点CDC 还需要一个中断端点用于通信接口的管理类通知有些官方例程里直接把 EP1 复用为中断 IN但为了代码清晰、避免干扰批量传输建议单独分配端点。如果端点不够可以把 CDC 的通信接口中断 IN 端点直接复用 HID 的端点前提是两者不会同时大量通信。我的经验是宁可少做一个功能也不要让端点冲突排查端点冲突的成本远高于省一个端点省下的那点资源。PMA 缓冲区地址也要注意对齐。F103 的 USB SRAM 从 0x40006000 开始每个端点描述符占 8 字节缓冲区地址必须是 2 的倍数。分配缓冲区时建议在 USB 库的 usb_pma.c 里统一管理不要自己在好几个文件里硬编码地址否则一旦改描述符缓冲区就乱了。3.3 收发逻辑与状态同步代码CDC 部分的数据收发逻辑比较直接。官方生成的模板里通常有 CDC_Receive_FS 和 CDC_Transmit_FS 两个函数前者是接收回调后者是发送函数。HID 部分需要自己补一个上报函数典型实现如下uint8_t HID_Report[8] {0}; void HID_SendKeyboardReport(uint8_t mod, uint8_t key1, uint8_t key2, uint8_t key3) { HID_Report[0] mod; HID_Report[1] 0; HID_Report[2] key1; HID_Report[3] key2; HID_Report[4] key3; HID_Report[5] 0; HID_Report[6] 0; HID_Report[7] 0; USBD_HID_SendReport(hUsbDeviceFS, HID_Report, 8); }这里有个关键点HID 中断端点的最大包长必须和报告长度匹配。如果报告长度是 8 字节端点描述符里 wMaxPacketSize 就必须是 8不能随便填个 16 或 64否则主机收包时会出现数据长度不一致导致键盘按键识别不稳定。另一个容易忽略的地方是状态同步。USB 挂起Suspend时如果 MCU 还在往端点里写数据数据会丢而且可能导致总线状态异常。所以我的做法是在 USB_Resume 回调里置一个标志位只有总线激活时才允许上报 HID 数据。这个坑我踩过设备睡眠唤醒后第一下按键永远无效排查了半天才发现是唤醒后立即上报主机还没准备好接收。4. 高频问题排查代码 12、断连、识别异常4.1 Windows 代码 12找不到足够资源这个报错在复合设备调试里特别常见尤其是当你把多个功能堆到一个设备时。设备管理器里显示“该设备找不到足够资源可以使用。(代码 12)”本质是 Windows 无法为设备的某些端点或者接口分配足够的资源。这在开发环境下最常见的原因有三个第一个是端点地址冲突。比如 CDC 用了 EP1 INHID 也写了 EP1 IN这时候固件和描述符不一致枚举时就会失败或资源不足。解决方法是把工程里实际的端点使用情况打印出来和描述符逐项对比。第二个是 PMA 缓冲区越界。F103 的 USB SRAM 总共 512 字节如果缓冲区分配总和超过这个值端点数据就直接写飞了。第三个是驱动层面的问题尤其在你频繁插拔、反复刷固件后Windows 的 USB 驱动栈可能会把旧的资源分配信息缓存下来。这时可以到设备管理器里把该设备卸载再扫描硬件改动重装一次驱动通常能解决。如果是量产设备遇到代码 12还要考虑是不是 USB 控制器本身资源不足。全速 USB 总带宽是固定的如果同时挂了多个大流量端点就会出现资源不足。解决办法是降低端点轮询频率或减小数据包长度。4.2 HID 设备识别异常或按键无反应HID 被识别成未知设备最常见原因是报告描述符有错误。Windows 对报告描述符的解析非常严格哪怕一个字节的 Report Count 和 Report Size 不匹配都会导致整个设备不被识别。我调试时的方法是用 USB 分析仪抓枚举数据重点看 GET_DESCRIPTOR 请求返回的报告描述符长度是否和描述符里的 wReportLength 一致。很多人的描述符里写的是 0x3A58 字节但实际返回了 60 字节主机解析就会错乱。按键无反应的问题则多半出在报告发送长度上。比如报告描述符定义了 8 字节报告但发送时只发了 6 字节主机的 HID 驱动会认为数据不完整而丢弃。另外发送间隔也要注意HID 中断端点的 bInterval 一般不小于 1ms如果发送频率超过端点轮询频率数据会被丢弃。这个在低速设备上尤其明显。4.3 CDC 串口不稳定与数据丢失CDC 虚拟串口的数据稳定性问题首先查端点描述符。CDC 数据接口的两个批量端点 wMaxPacketSize 通常是 64 字节。如果你的下位机一次发送超过 64 字节USB 库会自动分包但接收端需要正确拼包。STM32 的 USB 库在接收时如果上层处理速度跟不上DMA 缓冲会被覆盖出现丢包。解决办法是加大接收缓冲或者在应用层做流控。还有一个隐蔽的坑是 CDC 的 ACM 描述符里的 bmCapabilities 字段。这个字段如果设置不当Windows 的 usbser.sys 驱动可能不会理睬 DTR/RTS 信号导致一些依赖流控的上位机收发异常。我通常设置为 0x02表示支持请求 Set_Line_Coding、Set_Control_Line_State 等管理请求但不支持硬件流控。CDC 和 HID 同时工作时还要注意两者不能同时抢占 USB 总线。F103 的 USB 控制器是单 DMA 的两个端点同时提交传输会竞争。我的处理方式是在 USB 的 SOF 中断里做简单的调度比如优先处理 HID 上报再处理 CDC 的数据发送避免两个端点同时触发总线传输导致的数据交错。4.4 枚举顺序和复合设备的驱动匹配复合设备枚举时主机是按接口顺序逐个初始化的。如果你的 HID 接口在 CDC 前面Windows 会先弹出“HID 设备已就绪”然后过一会儿才出现 COM 口。如果等了很久 COM 口也没出现大概率是 CDC 的接口描述符有问题而不是 HID 的问题。另外如果产品需要在多个操作系统上运行Windows、Linux、macOS建议在描述符里再检查一下字符串描述符。Windows 用字符串描述符的 iProduct、iSerialNumber 来识别设备实例如果序列号不写或者每次上电都变Windows 会反复枚举设备导致复合设备的两个功能节点出现“先 HID 后 CDC”但其中一个来不及加载的情况。我在量产固件里会固定一个序列号避免这类问题。5. 应用扩展与选型思考HIDCDC 这套组合的可扩展性其实比很多人想象的大。同一个工程里可以再增加 HID 端点的数量做成多按键矩阵也可以把 CDC 改造成多路虚拟串口比如 CDC CDC HID 的三功能复合设备。但 F103 的端点资源确实有限如果功能种类超过三个建议换到 STM32F407 或者带 USB HS 的平台上端点数量和数据吞吐都会充裕很多。如果做的是高速率数据采集设备要注意 CDC 的极限吞吐。全速 USB 的 CDC 理论吞吐在 1MB/s 左右实际有效数据率受驱动差异影响Windows 下大概 800KB/s 就是比较稳的上限。而 HID 的中断传输实际速率更低只有几十 KB/s 级别不适合传大块数据。在设计系统架构时就要想清楚数据走哪条通道不要等到调完才发现带宽不够。做产品化的时候还有一点值得注意工厂产测和固件升级。HIDCDC 复合设备在产测时可以只打开 CDC 做自动化测试通过定制协议触发 HID 功能测试固件升级也走 CDC 通道HID 保持暂时静默。这种设计能有效减少产线工具的开发复杂度。我后来的几个项目都沿用了这套架构新需求基本只在描述符和协议层做小改动就可以快速衍生出各种变体产品。最后分享一个小技巧调试复合设备时准备一台装了 USB 协议分析仪哪怕是开源软件加一个硬件抓包器的电脑枚举阶段如果描述符有问题协议分析仪能直接看到主机返回的 STALL 和错误名称。这个工具在排查“代码 12”和其它枚举疑难杂症的时候比盲猜效率高太多了。踩过几次坑之后我现在的调试顺序都是先抓枚举包再看描述符最后才改固件代码——整个流程走下来复合设备其实并不神秘就是个“描述符对了剩下的全是细节”的工程。本文还有配套的精品资源点击获取
返回列表