ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

USB Audio与CDC复合设备驱动:UAC与虚拟串口共存实现与避坑

2026/10/4 16:27:45 拓冰建站 浏览量
USB Audio与CDC复合设备驱动:UAC与虚拟串口共存实现与避坑 简介这是一份面向Linux/Android嵌入式驱动开发者的USB复合设备驱动资源聚焦UAC1USB Audio与CDC/ACM虚拟串口的复合实现。驱动基于Rockchip平台开发采用legacy模式系统启动后无需在linux下配置usb_gadget或修改Android init.rc即可直接枚举为USB音频设备和UART串口同时提供改为脚本方式编译的备选路径方便需要动态配置功能的场景并声明理论全平台适用。资源包共12个文件以Kconfig和Makefile构建脚本为主配合两个C驱动源文件与两个RK3308的defconfig内核配置体量仅41KB结构清晰适合快速移植与裁剪。目前已有1010人学习下载对有复合设备驱动移植、USB gadget功能组合或UACACM调试需求的工程师能提供直接的参考实现与配置思路。1. usb_audiocdc复合设备驱动为什么这个压缩包值得你花一下午做音频类硬件的老工程师几乎都被同一个问题卡过设备要出声音还要和上位机通信调参数结果电脑上插了两根USB线既占端口又容易在客户现场被当成“这产品怎么还带尾巴”。usb_audiocdc复合设备驱动就是把USB声卡UAC和虚拟串口CDC塞进同一个USB口用一套描述符让系统同时枚举出两个逻辑设备。这个能力在做声卡 DSP 调试、对讲机音频单元、音频采集盒子时特别实用。它解决的不只是“少插一根线”而是“一个枚举链路里稳定挂两套驱动”的根本问题。我见过不少团队在 USB 复合设备上翻了车设备管理器里两个设备只出来一个或者出声音但串口一开就爆音。这篇就把描述符结构、实现代码和排查经验一次讲透。2. 复合设备描述符结构UAC与CDC共存的底层逻辑2.1 为什么不能直接写两个独立的接口描述符很多人第一次做复合设备时会直接把一个UAC设备描述符和一个CDC设备描述符拼起来。USB协议层面是允许一个设备Device Descriptor下有多个接口Interface的但操作系统在枚举时会遇到一个问题怎么知道接口0到接口2属于第一个功能接口3到接口5属于第二个功能如果没有明确的归属信息Windows 会尝试按接口独立安装驱动导致音频功能或通信功能加载失败。这个归属信息在USB规范里被称为IADInterface Association Descriptor它专门用于把连续的一段接口描述符合并成一个逻辑功能。UAC通常占用接口0Audio Control Audio StreamingCDC在复合配置下会占用接口1和接口2Communications Data。IAD 的作用就是告诉系统接口0单独属于第一个功能接口1和接口2合起来属于第二个功能。我一般在代码里这样组织描述符数组先是设备描述符然后配置描述符紧接着IAD再往后是两个功能的接口描述符。IAD 必须放在它所描述的接口段之前这一点很多人会漏。2.2 IAD描述符的字段取值要点IAD 用起来并不复杂但五个字段一个都不能错。bFirstInterface 填的是当前功能占用的第一个接口号bInterfaceCount 填的是该功能占用的接口总数bFunctionClass 和 bFunctionSubClass 要和对应类协议匹配bFunctionProtocol 则根据实际需求填。一个常见的坑是系统枚举复合设备时是先从设备描述符拿到ID_Vendor 和 ID_Product然后拿配置描述符再去解析IAD。如果 bFirstInterface 填了0但音频功能实际占用的起始接口不是0Windows 会把 CDC 的数据接口错误地合并到音频功能里结果串口能打开但收发完全没反应。所以写代码前先画一张接口分配图音频占0串口占1和2然后在 IAD 里填 bFirstInterface0, bInterfaceCount1 给音频再写第二个 IADbFirstInterface1, bInterfaceCount2 给串口。以下是一段典型的描述符数组写法基于 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 必须填 0xEFMiscellaneous并且提供 IAD 描述符否则 Windows 的 usbccgp 驱动不会启动复合设备的枚举流程。很多自制的 UAC 设备单独插入能识别一加上 CDC 接口就被识别成 Unknown Device多半就是这个字段没改。2.3 端点规划与描述符长度计算复合设备每个功能都要独立的端点号。音频功能一般是 1 个同步传输端点用于播放1 个中断端点用于控制CDC 功能需要 1 个中断端点和 1 个批量端点对。四个功能的端点不能共用不能用同号。如果固件里只预留了 4 个端点对音频和 CDC 加起来很容易超。计算 wTotalLength 是另一个易翻车的地方。配置描述符的总长度 9配置 8音频IAD, 若有 9 9音频接口描述符按声卡实际) 端点描述符长度 8CDC IAD 4 5 5CDC接口描述符与功能描述符 端点描述符。我一般先把每个功能的描述符长度算出来再回头填 wTotalLength。先写代码后算长度的顺序往往会导致 Windows 报“设备配置描述符错误”直接拒绝枚举。3. 用STM32CubeMX配置复合设备从工程生成到描述符移植3.1 CubeMX里如何同时勾选UAC和CDC在 STM32CubeMX 中USB_DEVICE 的 Middleware 选项默认只能选一个类界面里没有直接勾选“UAC CDC”的复合模式。常见的做法是先生成一个 CDC 工程再手动把 AUDIO 的类文件拷进去。具体操作是在 USB_DEVICE 配置里选择 Communication Device ClassVirtual 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 UAC2CDC 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 是采样率写上 48000p_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.0bind 函数找不到设备。“cant 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. 避坑合集UACCDC复合设备的五个必踩之地5.1 现象Windows 只能识别其中一个功能另一个显示感叹号原因最常见的是设备描述符里 bDeviceClass 没设为 0xEF或 IAD 描述符的长度少了一个字节应该 0x08。复合设备必须明确告诉操作系统它由多个功能组成而0xEF正是“Miscellaneous”标志配合 bNumInterfaces 大于 1 才能触发复合枚举流程。解决对照逻辑分析仪把描述符数据抓下来检查前几个字节。bDeviceClass 必须等于 0xEFbDeviceSubClass 通常填 0x02bDeviceProtocol 填 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也就是 bInterfaceCount2。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 字节的小包你会发现实际上总线利用率反而更高因为冲突重传少了。端点理想配置是音频流端点 wMaxPacketSize192 字节CDC 批量端点 64 字节。这套配置下满载播放的同时串口以 115200 波特率传输USB 总线的错误计数应该始终为零。进阶且非常实用的技巧把自己编译出来的描述符和 Linux 下用 ConfigFS 搭出来的设备分别抓包对比两者在枚举阶段的请求顺序。看标准请求 Get_Descriptor(Configuration) 返回的先和 9 字节配置描述符然后是接口、端点IAD 出现在接口之前。如果逻辑分析仪获得的描述符顺序与预期不符说明代码生成顺序有问题而正常配置在 Windows 下是能识别、但是不稳定时多半是 IAD 顺序正确但功能描述符之间有裁剪错误。这类问题我不建议一遍遍试建议直接用 USBlyzer 的 tree view 看每一步枚举流程绝大多数“有时行有时不行”全是描述符细节问题。我还习惯在每个功能的字符串描述符里打上固件版本号。这不是规范要求而是一个后悔药现场设备多了以后光看外壳分辨不了固件差异有了版本号客户报问题时首先问“设备管理器里产品字符串是什么”能筛掉一半环境问题。我自己的经验是给字符串描述符和端点规划做好文档比调多久的描述符代码都管用因为复合设备最大的成本不在代码调试而在跨团队沟通时每个人都得知道接口分配。希望这个方案能帮你少走点弯路。本文还有配套的精品资源点击获取