简介:这是一份面向Linux/Android嵌入式驱动开发者的USB复合设备驱动资源,聚焦UAC1(USB Audio)与CDC/ACM(虚拟串口)的复合实现。驱动基于Rockchip平台开发,采用legacy模式,系统启动后无需在linux下配置usb_gadget或修改Android init.rc,即可直接枚举为USB音频设备和UART串口;同时提供改为脚本方式编译的备选路径,方便需要动态配置功能的场景,并声明理论全平台适用。资源包共12个文件,以Kconfig和Makefile构建脚本为主,配合两个C驱动源文件与两个RK3308的defconfig内核配置,体量仅41KB,结构清晰,适合快速移植与裁剪。目前已有1010人学习下载,对有复合设备驱动移植、USB gadget功能组合或UAC+ACM调试需求的工程师,能提供直接的参考实现与配置思路。
1. usb_audio+cdc复合设备驱动:为什么这个压缩包值得你花一下午
做音频类硬件的老工程师,几乎都被同一个问题卡过:设备要出声音,还要和上位机通信调参数,结果电脑上插了两根USB线,既占端口又容易在客户现场被当成“这产品怎么还带尾巴”。usb_audio+cdc复合设备驱动,就是把USB声卡(UAC)和虚拟串口(CDC)塞进同一个USB口,用一套描述符让系统同时枚举出两个逻辑设备。这个能力在做声卡 DSP 调试、对讲机音频单元、音频采集盒子时特别实用。它解决的不只是“少插一根线”,而是“一个枚举链路里稳定挂两套驱动”的根本问题。我见过不少团队在 USB 复合设备上翻了车,设备管理器里两个设备只出来一个,或者出声音但串口一开就爆音。这篇就把描述符结构、实现代码和排查经验一次讲透。
2. 复合设备描述符结构:UAC与CDC共存的底层逻辑
2.1 为什么不能直接写两个独立的接口描述符
很多人第一次做复合设备时,会直接把一个UAC设备描述符和一个CDC设备描述符拼起来。USB协议层面,是允许一个设备(Device Descriptor)下有多个接口(Interface)的,但操作系统在枚举时会遇到一个问题:怎么知道接口0到接口2属于第一个功能,接口3到接口5属于第二个功能?如果没有明确的归属信息,Windows 会尝试按接口独立安装驱动,导致音频功能或通信功能加载失败。
这个归属信息在USB规范里被称为IAD(Interface Association Descriptor),它专门用于把连续的一段接口描述符合并成一个逻辑功能。UAC通常占用接口0(Audio Control + Audio Streaming),CDC在复合配置下会占用接口1和接口2(Communications + Data)。IAD 的作用就是告诉系统:接口0单独属于第一个功能,接口1和接口2合起来属于第二个功能。
我一般在代码里这样组织描述符数组:先是设备描述符,然后配置描述符,紧接着IAD,再往后是两个功能的接口描述符。IAD 必须放在它所描述的接口段之前,这一点很多人会漏。
2.2 IAD描述符的字段取值要点
IAD 用起来并不复杂,但五个字段一个都不能错。bFirstInterface 填的是当前功能占用的第一个接口号,bInterfaceCount 填的是该功能占用的接口总数,bFunctionClass 和 bFunctionSubClass 要和对应类协议匹配,bFunctionProtocol 则根据实际需求填。
一个常见的坑是:系统枚举复合设备时,是先从设备描述符拿到ID_Vendor 和 ID_Product,然后拿配置描述符,再去解析IAD。如果 bFirstInterface 填了0,但音频功能实际占用的起始接口不是0,Windows 会把 CDC 的数据接口错误地合并到音频功能里,结果串口能打开但收发完全没反应。所以写代码前先画一张接口分配图:音频占0,串口占1和2,然后在 IAD 里填 bFirstInterface=0, bInterfaceCount=1 给音频;再写第二个 IAD,bFirstInterface=1, bInterfaceCount=2 给串口。
以下是一段典型的描述符数组写法,基于 STM32 的 USB Device Library 工程:
__ALIGN_BEGIN static uint8_t usbd_desc[] = { /* 设备描述符 */ 0x12, /* bLength */ USB_DESC_TYPE_DEVICE, /* bDescriptorType */ 0x00, 0x02, /* bcdUSB = 2.00 */ 0xEF, 0x02, 0x01, /* bDeviceClass: IAD 复合设备 */ 0x40, /* bMaxPacketSize0 = 64 */ 0x5A, 0x12, /* idVendor = 0x125A */ 0x01, 0x00, /* idProduct = 0x0001 */ 0x00, 0x01, /* bcdDevice */ 0x01, /* iManufacturer */ 0x02, /* iProduct */ 0x03, /* iSerialNumber */ 0x01, /* bNumConfigurations */ /* 配置描述符 */ 0x09, /* bLength */ USB_DESC_TYPE_CONFIGURATION,/* bDescriptorType */ 0x??, 0x00, /* wTotalLength:根据实际编译调整 */ 0x03, /* bNumInterfaces = 3 */ 0x01, /* bConfigurationValue */ 0x00, /* iConfiguration */ 0x80, /* bmAttributes: 总线供电 */ 0x32, /* bMaxPower = 100mA */ /* 第一个IAD:负责把接口0归为音频功能 */ 0x08, /* bLength */ USB_DESC_TYPE_IAD, /* bDescriptorType = 0x0B */ 0x00, /* bFirstInterface = 0 */ 0x01, /* bInterfaceCount = 1 */ 0x01, /* bFunctionClass: Audio */ 0x02, /* bFunctionSubClass: Streaming? 按实际填 */ 0x00, /* bFunctionProtocol */ 0x00, /* iFunction = 0 */ /* 第二个IAD:负责把接口1、2归为CDC功能 */ 0x08, USB_DESC_TYPE_IAD, 0x01, /* bFirstInterface = 1 */ 0x02, /* bInterfaceCount = 2 */ 0x02, /* bFunctionClass: CDC */ 0x02, /* bFunctionSubClass: ACM */ 0x01, /* bFunctionProtocol: AT命令 */ 0x00, };代码里最关键的是 bDeviceClass 必须填 0xEF(Miscellaneous),并且提供 IAD 描述符,否则 Windows 的 usbccgp 驱动不会启动复合设备的枚举流程。很多自制的 UAC 设备单独插入能识别,一加上 CDC 接口就被识别成 Unknown Device,多半就是这个字段没改。
2.3 端点规划与描述符长度计算
复合设备每个功能都要独立的端点号。音频功能一般是 1 个同步传输端点用于播放,1 个中断端点用于控制;CDC 功能需要 1 个中断端点和 1 个批量端点对。四个功能的端点不能共用,不能用同号。如果固件里只预留了 4 个端点对,音频和 CDC 加起来很容易超。
计算 wTotalLength 是另一个易翻车的地方。配置描述符的总长度 = 9(配置)+ 8(音频IAD, 若有)+ 9 + 9(音频接口描述符,按声卡实际)+ 端点描述符长度 + 8(CDC IAD)+ 4 + 5 + 5(CDC接口描述符与功能描述符)+ 端点描述符。我一般先把每个功能的描述符长度算出来,再回头填 wTotalLength。先写代码后算长度的顺序,往往会导致 Windows 报“设备配置描述符错误”直接拒绝枚举。
3. 用STM32CubeMX配置复合设备:从工程生成到描述符移植
3.1 CubeMX里如何同时勾选UAC和CDC
在 STM32CubeMX 中,USB_DEVICE 的 Middleware 选项默认只能选一个类,界面里没有直接勾选“UAC + CDC”的复合模式。常见的做法是先生成一个 CDC 工程,再手动把 AUDIO 的类文件拷进去。具体操作是:在 USB_DEVICE 配置里选择 Communication Device Class(Virtual Port COM),生成工程后,从 ST 官网或标准库的 Audio 示例中复制 usbd_audio.c、usbd_audio.h、usbd_audio_if.c 到工程目录,并在 usbd_conf.c 里注册音频类。
每次重新生成 CubeMX 代码时,中间层会被覆盖,所以建议把已改好的描述符和类注册部分,单独放到一个不被重新生成的文件夹里,比如 App/custom_usbd/。如果你直接改在 Middlewares 下,生成一次就丢一次,这是最大的原坑。
3.2 注册两个类时的调用顺序陷阱
在 usbd_conf.c 里,USBD_Init 需要传入一个包含所有类回调的结构体数组。一般来说,先把 AUDIO 类放前面,再放 CDC 类,部分 USB 库在枚举时会按数组顺序发送 Set Interface 请求。实际经验是:顺序影响不大,但初始化函数的返回值和 HAL_Delay 执行顺序很重要。USB 刚上电时,内部时钟和 PLL 可能还没完全稳定,立即注册设备会导致第一次枚举失败。常见的做法是在 USBD_Init 前加 10ms 延时:
HAL_Delay(10); USBD_Init(&hUsbDeviceFS, &USB_Desc, DEVICE_FS); USBD_RegisterClass(&hUsbDeviceFS, &USBD_AUDIO); USBD_RegisterClass(&hUsbDeviceFS, &USBD_CDC); USBD_Start(&hUsbDeviceFS);加这个延时的原因是,复合设备枚举时间比单功能设备更长,系统在设备插入时会先读取描述符,再加载 usbccgp,再为每个子功能找驱动。如果初始化太快,设备回答“设备描述符请求失败”后,Windows 会停止后续请求,必须重新插拔。曾经有一块板子每十次只有三四次被识别,排查到最后就是主控上电复位时间不够,HAL_Delay 一加就稳定了。
3.3 自定义字符串描述符与产品信息修改
复合设备在产品管理器里会显示“USB 音频设备”和“USB 串行设备”,如果想区分不同子设备,需要修改 usbd_desc.c 里的字符串描述符。描述符索引在 USBD_StrDesc 回调里分配。注意 iProduct 字符串不能同时挂在两个功能描述符上,每个功能要独立指定字符串索引。
如果设备没有上传字符串描述符,某些版本的 Windows 会反复重试枚举,导致设备管理器出现“未知 USB 设备(设备描述符请求失败)”。这不是硬件问题,而是字符串索引和描述符内容不匹配。检查方法是把 iManufacturer、iProduct、iSerialNumber 三个索引对应的字符串描述符都保留,至少留一个产品字符串。
4. 在Linux下用ConfigFS搭建UAC和CDC复合设备
4.1 不用写代码的复合设备方案
如果你的系统是 Linux,实现 UAC + CDC 复合设备可以直接用内核自带的 ConfigFS,不需要自己写 USB 描述符。它适合用在树莓派、嵌入式 Linux 板卡或者需要快速验证的场合。同样是音频功能加串口功能,ConfigFS 大概只需要十几条命令。
加载模块的命令:
sudo modprobe libcomposite sudo modprobe usb_f_uac2 sudo modprobe usb_f_acm先确认目标板的内核开启了哪些配置项。一般要查 /boot/config-uname -r,如果没有 CONFIG_USB_CONFIGFS_F_UAC2 和 CONFIG_USB_CONFIGFS_ACM,后面操作会直接报找不到符号。
4.2 最小可用的ConfigFS配置脚本
创建复合设备的过程其实就是操作 sysfs 里的文件。脚本照下面这个框架写,注意不能有空格错位,否则 echo 时会出现无效参数:
#!/bin/bash cd /sys/kernel/config/usb_gadget/ mkdir g1 && cd g1 echo 0x1d6b > idVendor echo 0x0104 > idProduct echo 0x0100 > bcdDevice echo 0x0200 > bcdUSB mkdir -p strings/0x409 echo "MyCompany" > strings/0x409/manufacturer echo "USB Audio Combo" > strings/0x409/product echo "0123456789" > strings/0x409/serialnumber # 配置1:两个功能 mkdir -p configs/c.1/strings/0x409 echo "UAC2+CDC" > configs/c.1/strings/0x409/configuration echo 250 > configs/c.1/MaxPower # 音频功能 mkdir -p functions/uac2.0 echo 3 > functions/uac2.0/p_chmask echo 48000 > functions/uac2.0/p_srate echo 2 > functions/uac2.0/p_ssize # CDC功能 mkdir -p functions/acm.0 ln -s functions/uac2.0 configs/c.1/ ln -s functions/acm.0 configs/c.1/ # USB控制器接入 echo "musb-hdrc.0.auto" > UDCp_chmask 在 UAC2 的 ConfigFS 接口里是播放通道掩码,值为 3 表示左右双声道;p_srate 是采样率,写上 48000;p_ssize 是采样位深,2 代表 16bit。如果不设置这些,UAC2 功能默认参数可能不是你预期的,播放时会出怪声。脚本跑完后,在主机端插入USB口,lsusb 就能看到多出一个 “Linux Foundation Multi-Device” 的设备。如果 /sys/class/tty/ 下出现 ttyACM0,说明 CDC 部分也成功了。
4.3 配置失败时的内核日志排查
ConfigFS 配置出错时,用户空间几乎没有馈,报错全在内核日志里。配置完 UDC 后设备没有出现在总线上,跑 dmesg 看最后几十行。常见错误是:
- “failed to bind gadget” —— 说明某个功能 bind 失败,常见原因是驱动模型的 UDC 名称写错,比如实际是
musb-hdrc.0.auto,写成了musb-hdrc.0,bind 函数找不到设备。 - “can't create configfs item” —— 说明当前内核没编译对应功能模块,回去检查 CONFIG_USB_CONFIGFS_F_UAC2。
- 如果在 UDC 目录下 ls 看到了控制器名,但
cat /sys/kernel/config/usb_gadget/g1/UDC有内容也没用,以 dmesg 为准。
Linux 下还有一个小坑:树莓派或部分开发板默认的 USB 控制器只有一个,且 OTG 口驱动加载后只能作为 device 或 host 其中一种角色。做这个测试时,确保 OTG 模式下 dr_mode 已被设为 device,否则 echo UDC 时会提示设备忙。
5. 避坑合集:UAC+CDC复合设备的五个必踩之地
5.1 现象:Windows 只能识别其中一个功能,另一个显示感叹号
原因:最常见的是设备描述符里 bDeviceClass 没设为 0xEF,或 IAD 描述符的长度少了一个字节(应该 0x08)。复合设备必须明确告诉操作系统它由多个功能组成,而0xEF正是“Miscellaneous”标志,配合 bNumInterfaces 大于 1 才能触发复合枚举流程。解决:对照逻辑分析仪把描述符数据抓下来,检查前几个字节。bDeviceClass 必须等于 0xEF,bDeviceSubClass 通常填 0x02,bDeviceProtocol 填 0x01,这套值是复合设备的规范推荐值,不要自创。
5.2 现象:USB 音频播放正常,但虚拟串口一收发数据,声音就断续
原因:UAC 的同步传输端点占用了大量带宽,CDC 的批量传输会抢占总线,两者在帧时间内发生冲突。USB 全速模式下每帧只有 1ms,同步端点一旦约定带宽就固定分配,批量传输挤不掉它,但如果 CDC 端点在描述符里被配置成了中断传输且 poll 间隔太短,带宽会被串口吃光。解决:CDC 数据端点用批量传输,不要改成中断端点,端点描述符里 bmAttributes 填 0x02。另外,音频端点的 wMaxPacketSize 在全速模式下不要超过 192 字节(对应16bit双声道48kHz每帧正好192字节),超了就必然抓帧失败。
5.3 现象:设备在 Linux 下多播放一会儿就 UAC 停止,串口却能继续工作
原因:UAC2 驱动在缓冲不足时召回工厂的重置机制,长时间没有主机读取音频数据会导致 underrun,缓冲设置太小。解决:Linux 的 ALSA 配置里把 period_size 调大,或者在设备侧把 EP 的 FIFO 开大。部分 STM32 平台的 UAC 实现是在 USB 中断里往 I2S 发数据,如果 DMA 优先级低于 USB 中断,音频数据就会因为等 I2S 而堆积。在 NVIC 里把 DMA 中断优先级提到和 USB 相同或更高一级,问题能缓解大半。
5.4 现象:设备插到有些电脑上能识别,有些电脑上显示“无法启动”(代码10)
原因:这种机器通常对配置描述符的长度和端点数量有限制,或者 USB 控制器驱动对 IAD 的解析不完整。老款 USB 2.0 控制器对每个功能的接口数有上限,如果 CDC 功能占了 3 个接口(部分实现会用 2 个接口,另一部分用 3 个),会导致描述符长度超出设备控制器的解析能力。解决:把 CDC 功能压缩成 2 个接口的 ACM 实现(Communication Interface + Data Interface),也就是 bInterfaceCount=2。USB 音频功能也只用 1 个接口(包含两个 alternate setting),不要为音频控制单独再多开一个接口。接口越少,兼容性越好。
5.5 现象:设备休眠唤醒后串口还能识别,音频却变成无声
原因:同步传输端点在系统睡眠时被挂起,部分主控在恢复后不会自动重新发送 Set Interface(1) 请求,UAC 流没重新启动。解决:在上位机驱动或应用层监听音频流错误,检测到暂停超过 2 秒后,主动发送 SET_INTERFACE 请求重新选到 alternate setting 1。如果设备是裸 MCU,也可以通过监听 RESUME 信号主动重新初始化 FIFO,但这需要 MCU 支持远程唤醒。如果产品现场遇到这个坑,最直接的缓解方式是让设备禁用远程唤醒,或者引导用户重新插拔;这不是懒惰,而是很多 Windows 版本在电源管理上根本不往回走标准流程。
6. 验证与进阶:如何用带宽计算和协议分析把驱动做到“干净”
判断一个复合设备驱动合不合格,不能光看设备管理器里有没有感叹号,还要看它在长时间、高负载工况下稳不稳定。我习惯用一个包含“播放 48kHz/16bit/双声道 + 每 100ms 写一次串口”的压测程序跑两小时,同时在 Windows 下开 USBlyzer 抓描述符,确认三件事:IAD 段有没有被正确解析,两个功能是否独立出现在枚举树里;端点的带宽预留比例;以及传输错误的计数是否一直为零。
全速 USB 模式下,每帧 1ms 的总带宽是 1000 帧 * 1500 字节左右(实际可用约 90%)。一个 48kHz 双声道 16bit 的音频流,每帧 192 字节,约占用 13% 的带宽;CDC 批量传输没有固定带宽,但大批量突发时会捣乱。如果整个描述符里音频流切成 32 字节的小包,你会发现实际上总线利用率反而更高,因为冲突重传少了。端点理想配置是:音频流端点 wMaxPacketSize=192 字节,CDC 批量端点 64 字节。这套配置下,满载播放的同时串口以 115200 波特率传输,USB 总线的错误计数应该始终为零。
进阶且非常实用的技巧:把自己编译出来的描述符和 Linux 下用 ConfigFS 搭出来的设备分别抓包,对比两者在枚举阶段的请求顺序。看标准请求 Get_Descriptor(Configuration) 返回的先和 9 字节配置描述符,然后是接口、端点,IAD 出现在接口之前。如果逻辑分析仪获得的描述符顺序与预期不符,说明代码生成顺序有问题,而正常配置在 Windows 下是能识别、但是不稳定时,多半是 IAD 顺序正确但功能描述符之间有裁剪错误。这类问题我不建议一遍遍试,建议直接用 USBlyzer 的 tree view 看每一步枚举流程,绝大多数“有时行有时不行”全是描述符细节问题。
我还习惯在每个功能的字符串描述符里打上固件版本号。这不是规范要求,而是一个后悔药:现场设备多了以后,光看外壳分辨不了固件差异,有了版本号,客户报问题时首先问“设备管理器里产品字符串是什么”,能筛掉一半环境问题。我自己的经验是:给字符串描述符和端点规划做好文档,比调多久的描述符代码都管用,因为复合设备最大的成本不在代码调试,而在跨团队沟通时每个人都得知道接口分配。希望这个方案能帮你少走点弯路。
本文还有配套的精品资源,点击获取