尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

STM32 USB复合类实战:CDC ACM与HID组合配置指南

STM32 USB复合类实战:CDC ACM与HID组合配置指南 去年我在一个产品里需要同时实现虚拟串口和HID控制指令一开始我以为就是两个USB例程拼起来结果在STM32上折腾了很久。后来用STM32CubeMX里的Azure RTOS USBX包重新搭工程才把CDC ACM和HID复合类稳定跑起来。这篇文章就基于这次经历整理一下怎么在STM32上用Azure包实现USB设备复合类重点聊配置思路、代码结构、端点分配和现场踩坑。USB复合类听起来很高端其实本质就是一个USB物理设备里挂多个Interface比如串口和HID控制同时存在。Windows手机上看到一个USB设备展开却是“COM口 HID设备”。这类应用在工业上位机、USB转调试工具、需要配置参数又要实时上报的场景里非常常见。如果你正好有类似需求又不想自己从底层USB协议拼类那读懂这篇就够了。1. 为什么要做USB复合类设备从真实需求说起1.1 一个设备只做一件事越来越不够用很多STM32项目早期都只把USB当串口用hal库自带CDC虚拟串口例程改一改就能通气。可一旦你需要在调试口之上再塞一个自定义控制通道比如调整PID参数、读写内部Flash配置、下发固件升级指令老方案就不太舒服了。都用串口当然也能跑但上位机要同时维护两个COM口还要自己定协议区分数据和命令很多团队最后都会做成“一个虚拟串口收发带协议头的帧”维护成本高、排错也麻烦。HID类的好处是系统免驱、延迟可预测、轮询机制稳定非常适合做设备状态上报和少量控制命令。于是很自然地就会想到能不能一个USB口同时出现一个CDC虚拟串口和一个HID设备这部分需求在Linux下用usb gadget很容易搞定STM32上则要看你用的USB协议栈怎么组织类实例。裸机HAL库的USB Device Library虽然也能改配置描述符实现复合类但要做好几层手工拼接端点管理、初始化顺序、类回调全都要自己兜着一旦枚举异常排查周期会非常长。所以我最后选择的是STM32CubeMX集成的Azure RTOS USBX中间件。USBX天然把“设备类”当成可以注册的组件CDC、HID、MSC、DFU这些都是现成的Class实例组合起来比从裸机改描述符清爽太多。它最核心的价值不是代码量更少而是把USB协议栈底层、设备枚举、类事件调度都封装好了你只需要关心每个类的业务逻辑。1.2 复合类与组合设备的区别别再搞混这里必须说清楚一个概念复合类Composite Device和组合设备Composite Device在资料里经常混用但严格讲一个物理设备里的多个Interface如果由单一设备实现并且靠Interface Association DescriptorIAD把同一个功能的多个Interface归组那才是真正意义上的“复合设备”。而组合设备通常指多功能设备通过内置Hub实现比如USB键盘自带的Hub再接一个HIDWindows里面会看到同一个物理外壳下有多个USB设备节点。STM32上做的多是前者一个Device Descriptor多个Interface每个Interface代表一种USB类功能。USBX的做法是把多个Class实例挂到同一个设备栈里比如CDC_ACM需要两个Interface通信类AC 数据类Data和HID一个Interface配置描述符里累计就有三个Interface。这里Windows能否正确识别最关键的是Device Descriptor里的bDeviceClass、bDeviceSubClass和bDeviceProtocol必须设置成0xEF、0x02、0x01也就是定义成“多类设备Miscellaneous class”。有的初学者在自定义配置描述符时把bDeviceClass写成了0xFFVendor Specific结果Windows直接弹“未知设备”。USBX的好处是你只要在CubeMX里开启多个Class实例它生成的代码会把这些描述符处理好不用自己掰手指头算接口数量。不过这不意味着你可以完全不管描述符HID的Report Descriptor和CDC的串口描述符依然要自己按需定义。1.3 为什么我最终选择了Azure RTOS USBXSTM32的USB栈大致有三类官方HAL库自带的USB Device Library、中间件层的Azure RTOS USBX、以及第三方比如TinyUSB。每个方案都能做复合类但落地体验差别很大。HAL库自带的方案在单一类场景很成熟可是要做多类需要手动改配置描述符、端点映射和各类的类特定回调工程一旦上规模以后代码很散。TinyUSB做得也不错逻辑清晰支持复合类也方便但问题是要自己移植到STM32CubeMX工程里还要把CubeMX的USB驱动和TinyUSB的底层对接。对于很多人来说学习成本不在USB类本身而在FTDI、CDC、HID的一堆类描述符怎么填。USBX是微软Azure RTOS的一部分已经被STM32CubeMX原生集成不需要移植选中间件、生成代码、直接调API就行。它的线程调度和底层中断处理也是自带的不会占用你主循环太多时间。当然USBX也有学习门槛因为它的初始化不是简单地调用一个库函数你需要理解ux_system_initialize、ux_device_stack_initialize以及Class Register这几步的关系。可一旦理清楚这个流程后面再加一个MSC类或DFU类就是几分钟的事。对比下来在STM32上做复合类我用Azure RTOS USBX的体验最舒服。2. 用STM32CubeMX快速搭建USBX工程骨架2.1 需要准备的软硬件环境做这一步之前你需要确认手里的开发板和工具都齐了。我这边用的是STM32F407G-DISC1板子芯片自带USB OTG FSPA11为DM、PA12为DPST-Link的虚拟串口和板载USB口焊在一起调试时注意区分。如果你用的是F103走的是PD0/PD1加USB DP上拉电阻的老架构代码上差别不大但CubeMX配置的引脚和时钟要单独选择。软件方面我用的STM32CubeMX 6.9以上固件包选择STM32F4xx的HAL库然后额外在Middleware里勾选Azure RTOS USBX。CubeMX会提示你安装Azure RTOS软件包这一步需要联网下载。建议直接安装最新稳定版因为USBX的类注册接口在不同版本里有一点变化太旧版本的例程跟新CubeMX生成的代码可能对不上。另外准备一个Bus Hound或者USBLyzer调USB类这种活没有抓包工具会非常痛苦。时钟配置是USB能不能枚举成功的命门。STM32内部USB PHY需要48MHz时钟F4系列通常用PLLQCLK或者PLL48CLK输出。你可以在Clock Configuration界面里看左边树状图确保USB OTG FS和RNG等外设旁边的时钟值稳定显示为48MHz。我调试时遇到过枚举不稳定最后发现是时钟树配置里M、N、P、Q那几个系数给错了48MHz变成了47.x MHz设备在Windows上能枚举但一传数据就丢包。CubeMX的时钟树图形化界面相对友好但如果手工改过系统时钟初始化一定要重新确认PLL的配置。2.2 CubeMX中开启USBX的详细步骤具体操作我是这样做的先初始化USB_OTG_FS外设速度选Full Speed12Mbps不要和HAL库的USB Device库混淆USBX中间件会自己接管底层。然后在Middleware菜单里勾选Azure RTOS USBX界面上会有一堆ThreadX、FileX、NetX的选项卡我们只用USBX相关部分。在USBX配置页里勾选Device Stack然后选择Device Class实例。USBX生成的工程默认只支持一个Class。要做复合类我需要把同一个工程里同时启用CDC_ACM和HID。CubeMX的USBX配置界面会把Common和Class相关的选项组织在一起你可以通过添加Class实例的方式把需要的类都加进去。有的CubeMX版本叫“Add Class”有的直接在Device Class下拉里复选我用的版本是可以分别添加多个类并在各自子页签里填参数。CDC_ACM的默认参数有串口波特率、数据位、停止位HID类里要选择Report Descriptor来源选了模板以后还能修改模板里的原厂ID和用法页。配置完成后在Project Manager里勾选“Generate peripheral initialization as a pair of .c/.h files per peripheral”再确认编译器是IAR/Keil/STM32CubeIDE中的哪一个。我一般用STM32CubeIDE生成代码后打开工程会发现已经自动加入了ThreadX和USBX的源码main.c里也生成了MX_AZURE_RTOS_Init()的调用。这步没什么坑唯一要留意的是不要勾选独立生成“USB_DEVICE”中间件有些人同时勾了HAL库USB设备和USBX结果Pin冲突、Descriptor冲突编译不过。2.3 生成代码后需要手动调整的工程细节CubeMX生成的代码不是拿来就能跑的尤其是复合类有几个地方需要手动处理。第一ThreadX的全局堆大小。USBX的CDC和HID类都会默认申请内存池和线程控制块如果默认的TX_BYTE_POOL_SIZE太小初始化到一半会卡死表现是USB枚举不出来调试器看变量又没死循环。建议把ThreadX的Heap Size调整到至少2KB以上如果你的CDC数据缓冲很大还要更大。我测试时用了默认的1024字节加HID后直接UX_INITIALIZE_FAILED把这个值改成4096就正常了。第二USBX的设备参数结构体。生成的 usbx_device.c 里有ux_device_stack_initialize回调里面会调用ux_device_stack_initialize和ux_device_stack_class_register。这段里需要检查每个类注册时传入的参数个数和端点地址。比如CDC ACM注册时有一个参数是“用户事件回调”这个回调里会收到UX_DEVICE_CDC_ACM_EVENT_SEND_DATA等事件如果没实现上位机打开串口时可能没有响应。HID类注册时参数里一般要传入一个HID Report Descriptor的指针和长度CubeMX已经生成好模板但要改成你自己定义的报告格式不然上位机只能读到默认鼠标/键盘描述符。第三中断优先级的配置。USB OTG中断优先级不能设成ThreadX不认识的组别。具体来说在NVIC设置里要把OTG_FS_IRQn和PVD、RTC等放一起统一使用优先级分组。否则USB的ISR会嵌套进ThreadX的内核临界区运行一段时间后整个系统卡死。设置完毕后手动在usbx_device.c的HAL_GPIO_EXTI_Callback等地方检查一次。3. 复合类核心Class Instance的注册与配置深度解析3.1 USBX的物体模型从Host/Device到ClassUSBX的逻辑模型说出来其实不复杂。它把USB协议栈分成了Host和Device两端两边都有总线管理层。Device端基于一个或几个物理接口构建设备栈每个设备栈上再挂一个或多个Class实例。你定义的每个USB Class实例最终对应的就是USB配置描述符里的一组Interface。USBX初始化时首先调用ux_system_initialize配置内存池和中断相关的资源然后调用ux_device_stack_initialize绑定设备的厂商信息、产品信息、序列号、端点配置和类注册函数最后在类注册函数里逐个调用ux_device_stack_class_register。这里面最容易被忽略的一点是USBX的设备描述符并不是保存在大的const数组里而是由ux_device_stack_initialize里的UX_DEVICE_DESCRIPTOR、UX_CONFIGURATION_DESCRIPTOR等结构体来描述的。你在CubeMX里看到的VID、PID、字符串描述符最后都被这些结构体引用。做复合类时USBX会自动把每个Class实例的接口描述符合并进配置描述符。它的合并顺序取决于你调用ux_device_stack_class_register的顺序。如果注册顺序反了接口在枚举时不会报错但有些苛刻的上位机可能按索引寻找HDI设备结果发现第一个接口是CDC就又有了兼容性问题。我习惯把相对“稳定”的HID类放在前面CDC ACM放在后面让上位机优先识别控制设备。当然这只是经验具体顺序按你的业务需求来也不会有大问题。3.2 CDC ACM HID复合类配置实战下面以一个典型的CDC ACM HID复合类为例把注册流程落到具体代码块上。CubeMX生成的代码会包含大量配置项我简化一下核心脉络方便你理解应该怎么填参数。// usbx_device.c 里的回调概要 UINT mx_usbx_device_initialize(VOID) { UINT status UX_SUCCESS; // 1. 初始化USBX系统资源 status ux_system_initialize(usbx_byte_pool, USER_BYTE_POOL_SIZE, UX_NULL, 0); if (status ! UX_SUCCESS) return status; // 2. 初始化设备栈挂上VID/PID和端点参数 status ux_device_stack_initialize( device_descriptor, // 设备描述符 config_descriptor, // 配置描述符会由USBX合并生成 device_string_descriptor, // 字符串描述符 0, // 设备内存池大小由协议栈管理 0x1234, 0x5678, // VID、PID 3); // 设备权限自供电等 if (status ! UX_SUCCESS) return status; // 3. 按顺序注册每个类实例 status ux_device_stack_class_register( HID, ux_device_hid_entry, 1, 0, hid_parameter); if (status ! UX_SUCCESS) return status; status ux_device_stack_class_register( CDC_ACM, ux_device_cdc_acm_entry, UX_DEVICE_CLASS_CDC_ACM_DEVICE_INTERFACE, 0, cdc_parameter); if (status ! UX_SUCCESS) return status; return UX_SUCCESS; }这段代码就是整个USBX设备端的骨架。HID类注册时传的是hid_parameter里面包含了HID报告描述符的地址、长度以及用于外部中断通知的回调。CDC类注册时传的是cdc_parameter里面包含串口参数和数据收发回调。你可以在CubeMX生成的usbx_device.h里找到这些参数结构体的定义。一个容易被坑的地方是HID类的用法页、报告ID、端点包长度不能用默认模板。比如默认模板是鼠标Report Descriptor只有4字节你说我想用它来传输64字节传感器数据不动Report Descriptor就永远只能收到4字节。修改Report Descriptor的方法是在hid_parameter里重新指定一个const数组。还要注意报告的MaxInputReportSize默认是64如果改成256一个包会分成多帧和你预期的“一次收全”完全不一样。CDC类这边重点是CDC的线路编码结构。上位机打开串口时会发送SET_LINE_CODING请求STM32端要能处理这个请求否则串口打不开。USBX的CDC类里有一个内置处理但你要在cdc_parameter里至少提供串口基地址和波特率参数。我做的例子里还挂了DMA然后回调里收到的数据都是DMA缓冲区的指针这块跟裸机使用串口中断的概念有很大差异需要认真看USBX的缓冲区所有权规则搞错了会内存越界。3.3 为什么PNP和设备描述符能决定电脑认不认设备每次插入USB设备Windows的PNP管理器都会向设备发送GET_DESCRIPTOR请求读取设备描述符、配置描述符和字符串描述符。如果你做的是复合类Windows要求配置描述符里必须有IADInterface Association Descriptor否则它不知道两个Interface是同一个功能还是相互独立的。USBX在多个类实例场景下会自动添加IAD吗这里是个关键点严格说USBX本身并不会对所有复合类自动添加IAD是否添加取决于你注册的类实例和底层描述符生成逻辑。我在实际项目中验证过的方案是手工往配置描述符里加一段IAD。USBX提供了挂自定义描述符的钩子在ux_device_stack_initialize的回调里可以追加一个UX_DEVICE_DESCRIPTOR节点也可以直接在Class注册前插入原始的USB_DESCRIPTOR_INTERFACE_ASSOCIATION结构体。我当时把HID类和CDC ACM类的两个Interface用IAD关联起来Windows的设备管理器显示一个“USB 复合设备”父节点下面挂两个子节点非常干净。如果你偷懒不处理IAD很多情况下Windows依然能识别CDC和HID但遇到强制检查IAD的主机或Linux的usbfs时可能出现枚举失败。所以建议在工程初期就先把IAD加好。过程不复杂在CubeMX的USBX配置里你可以编写一个回调函数在配置描述符组装完成后动态追加IAD。加上之后设备枚举信息会稳定很多也能避免一些USB分析仪报warnings的问题。4. 实操过程中绕不开的坑与排查方法4.1 设备管理器未知设备或驱动加载失败拿到新板子或者第一次烧录USBX程序最容易看到的就是Windows弹“未知设备”。这不是USBX独有的问题任何自己写的USB程序都有这个阶段。常见原因有几个第一USB时钟不对这个我前面说过第二上拉电阻没有使能很多STM32开发板的USB DP上拉是靠软件控制的F1系列可能要在初始化时把DP拉高F4的OTG内部自动处理但如果你是自制的板子外部要加1.5k上拉到3V3第三设备描述符里的VID/PID和Class配置冲突。排查方法建议按顺序来先用USBlyzer或者Bus Hound抓包看PC有没有发GET_DESCRIPTOR请求。如果抓不到任何数据问题基本在硬件层或者USB中断没有触发如果能抓到设备回数据但PC不认那多半是描述符内容有误。用Bus Hound打开USB设备节点能看到枚举失败的设备返回的descriptor对照USB规范逐字节检查即可。我这边有一份标准的MS OS Descriptor请求如果你的设备没有响应也会导致智能安装识别异常。如果Windows提示“该设备无法启动”可以检查设备管理器里是否出现了Microsoft WINUSB接口。对CDC类来说它通常不加载USBX自身的inf而是系统自带的usbser.sys。HID类一般不需要驱动。如果两个类都加载了但设备节点上有个黄叹号右键更新驱动手动选“从计算机选择”Windows会自动匹配一次。很多时候这不是你的代码问题而是第一次插拔时缓存了错误描述符重新插一下就好。4.2 枚举正常但数据传输丢失或卡死枚举成功只是第一步。数据传着传着就掉或者上位机打不开串口这类问题通常都出在端点、缓冲区和线程上。USBX的CDC类要求你提供发送和接收缓冲区如果你用的缓冲区是局部数组发送函数返回后数据可能还在DMA搬运局部数组就被重用了必然丢包。正确做法是定义全局缓冲区并在发送回调中等待端点发送完成事件。HID类的数据发送有个特点它的类协议规定每次报告大小必须和Report Descriptor里定义的一致。如果你定义的是8字节报告却想一次发64字节上位机只能收到前面8个字节其他全部丢弃。很多人在调试时反复查USBX的HID发送函数实际是Report Descriptor没写对。另外有些主机的固件会在USB总线空闲时挂起如果你在低功耗模式里关了USB时钟唤醒后数据也会卡死。这时需要在USB中断或事件回调里重新初始化端点。线程调度问题也常见。USBX的回调函数运行在ThreadX上下文中如果你在HID事件回调里直接调用HAL_Delay或者操作大量耗时任务会影响USB类线程的调度。解决办法是把接收到的数据拷贝到自己的消息队列然后在业务线程里处理。我经历过一个案例HID接收回调里做了Flash写操作结果USB枚举后稳定跑不了几分钟就断开就是因为回调里做Flash擦写耗时太长USB域超时了。4.3 复合设备USB地址和端点分配注意事项复合类里最头疼的就是端点地址冲突。看USBX例程CDC ACM默认占用通知端点IN 0x81数据端点IN 0x82和OUT 0x01HID类默认占用IN端点0x84或0x83。不同版本协议栈对HID端点的默认值不一样建议每次生成代码后在usbx_device.c里查一下UX_DEVICE_CLASS_HID_ENDPOINT_INCDC_ACM相关的USBD_CDC_ACM_NX_DEVICE_EVENT_ENDPOINT等宏定义。STM32F4的USB OTG有4个IN和4个OUT物理端点端点0给控制传输所以可用的端点数是有限度的。如果一个CDC ACM占用了2个IN和1个OUTHID再占1个IN还剩1个IN和3个OUT但你在配置里可能还需要一个MSC类那资源就紧张了。解决办法有两个一是复用一个端点比如HID走中断传输CDC走批量传输两者物理上是同一个端点地址但在不同interface里USBX允许某些情况下共享端点二是在你的设备功能规划上砍掉不必要的数据通道减少端点数量。我给你做一个表格直观展示我常用的一组端点分配方便你对照检查功能传输类型端点地址方向说明控制传输Control0x00 / 0x80双向枚举和类请求USBX自动管理CDC ACM 通知Interrupt0x81IN串口状态变化比如DTR变化CDC ACM 数据Bulk0x82IN设备发给上位机CDC ACM 数据Bulk0x01OUT上位机发给设备HID 定制报告Interrupt0x84INHID输入报告HID 定制报告Interrupt0x84OUT部分HID设备可带OUT端点非必须注意端点地址在不同协议栈版本里是可以调整的但调整后必须同步修改配置描述符里的端点信息。如果你用CubeMX生成它一般会自动匹配但如果你手工改过HID端点一定要把配置描述符里的“bEndpointAddress”也改一致。否则USB枚举时主机会发现设备说“我有端点0x84”实际物理端点根本没开启传输请求直接卡住。5. 我的一些心得与建议5.1 先单独跑通单个类再做复合类如果你第一次接触USBX强烈建议别直接上复合类。在一个工程里先只开CDC ACM用串口助手收发数据确认板子枚举、USBX线程、CDC回调都正常了再添加HID。HID单独跑时可以用一个简单的GetReport请求测试复合类那边再把两个类合到一起。这样做的好处是出问题的时候缩小范围至少能判断是协议栈配置问题还是你自己的业务逻辑问题。刚开始动手时我看到很多例程已经把USBX的初始化都写好了以为直接编译就能跑。但实际上每个开发板的时钟配置、外部晶振、USB DP上拉方式都有差异。CubeMX生成的工程默认不一定带正确的描述符因为HAL库和USBX之间有自己的配置风格。我建议先从官方例程或CubeMX生成的最简工程跑通一个鼠标设备再从鼠标改成CDC一点点叠加不要一上来就写满全部代码。5.2 用好抓包工具比看日志高效十倍调试USB这个领域示波器不是必需的但抓包工具必须有。Windows下最简单的用Bus Hound选“Device”模式可以看到主机发的每个URB和设备的响应。如果你把USB设备枚举期间的所有事务都记录下来就能看到计算机实际上是如何读取描述符的有没有超时有没有STALL这样你会比拿着代码到处打印日志有把握得多。我第一次调试IAD时就是靠Bus Hound看到配置描述符返回的接口顺序不对。原来我以为USBX会自动把HID接口放在前面但实际注册顺序是CDC在前HID在后调整注册顺序后接口顺序才正常。这类问题靠看代码很难发现抓包一眼就明白。学会抓包做USB复合类就有了一半的底气。5.3 不要忽视电源和地线USB复合类设备通常比单类设备耗电高一些尤其是同时跑CDC和HID线程时。如果板子通过USB总线直接供电要考虑总线上是否有其他负载。STM32开发板的USB稳压器一般够用但自制板子最好在VBUS脚加100uF大电容并在靠近MCU的VBUS脚放一个0.1uF去耦电容。不然设备在低功耗模式下枚举时容易被USB主机的bus power限制卡住表现为插上后提示“集线器端口上的电涌”或设备一直复位。我遇到过一次诡异的问题是Windows显示“USB设备不识别”但示波器看USB DP/DM波形完全正常最后发现是开发板USB连接器的外壳地没接好共模噪声干扰了DP/DM信号。把USB连接器外壳地焊接到PCB地平面后问题消失。这个坑写出来是提醒你有时候不是软件问题而是硬件接触不可靠。5.4 把参数都留成宏定义方便后续扩展最后一个小建议做USBX复合类时把所有跟硬件相关的参数都提到头文件里定义比如CDC端点、HID端点、缓冲区大小、VID/PID、HID报告ID。这样以后从F407换成H743或者从CDCHID换成CDCMSC只需要改宏定义不用动类注册逻辑。我在项目的usbx_config.h里维护了几套配置一套给调试用CDCHID一套给量产用HIDMSC切换起来非常方便。USB复合类这个方向做完一次以后会非常顺手。USBX的类注册思想其实就是把每个USB功能当成一个插件组合、拆分都很直观。希望这份经验能让你少走一些弯路。
返回列表