多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

USB复合设备驱动实战:usb_audio与CDC共存原理与避坑指南

USB复合设备驱动实战:usb_audio与CDC共存原理与避坑指南 简介该资源面向嵌入式Linux与Android底层驱动开发者提供一套基于legacy方式实现的UAC1音频与CDC串口复合设备驱动方案解决设备开机后无需额外配置脚本即可直接枚举为USB声卡与UART串口的问题适用于RK平台并理论兼容全平台。压缩包共12个文件约41KB包含4个Kconfig与4个Makefile用于内核驱动编译配置2个C源文件承载核心驱动逻辑以及2份rk3308_linux_defconfig内核配置参考结构精简、便于移植。若需改用脚本方式只需不编译legacy驱动并在USB配置脚本中将功能设为acm与uac1即可。目前已有1010人学习下载读者可借此掌握复合设备枚举流程、legacy与脚本两种实现路径的差异以及内核配置与驱动编译的完整思路适合需要快速验证UACCDC方案的工程师参考。1. 从一份 usb_audiocdc 复合设备驱动压缩包说起它到底解决什么问题如果你手头有一份名为usb_audiocdc复合设备驱动.rar的工程大概率你正卡在同一个场景里一块 MCU 板子插到电脑上系统里同时冒出两个设备——一个 USB 声卡usb_audio一个虚拟串口CDC ACM。声卡负责音频采集和播放串口负责传控制命令、日志或者配置参数。听起来很美好但真正动手时你会发现单做一个 usb_audio 或者单做一个 CDC 都不难难的是让它们在同一个 USB 设备里共存还要让 Windows、Linux、macOS 都能正确枚举、正确加载驱动、不打架。这个标题里的三个词——usb_audio、cdc、复合设备驱动——其实指向的是同一件事USB Composite Device。复合设备的核心是「一个 USB 设备、多个接口、多套描述符、多个功能单元」主机侧靠接口关联描述符IAD和配置描述符把它们组织起来。usb_audio 走的是 UACUSB Audio ClassCDC 走的是 Communication Device Class两者在端点分配、带宽占用、描述符顺序上都有各自的脾气。这份驱动包要解决的就是让它们在同一套 USB 协议栈里和平共处。适合读这篇的人有三类一是做 USB 音频外设麦克风阵列、声卡、会议终端的嵌入式工程师想加一路串口做控制通道二是做 CDC 数据采集设备的想顺带把音频流也塞进同一个 USB 口三是被「复合设备枚举失败」「声卡能识别但串口不出现」这类问题折磨过、想搞清楚底层描述符怎么排的人。下面我按「先立住原理、再动手复现、最后讲坑」的顺序把这份驱动背后的东西拆开讲。2. usb_audio 与 CDC 复合的底层逻辑描述符、端点与带宽怎么分2.1 复合设备的描述符层级与 IAD 的作用USB 复合设备不是简单把两套描述符拼在一起就行。主机枚举时先读设备描述符再读配置描述符集合。配置描述符里包含一个或多个接口Interface每个接口有若干端点Endpoint。对于 usb_audio通常至少有两个接口一个 AudioControl 接口负责音量、静音、采样率等控制一个或多个 AudioStreaming 接口负责实际音频数据流。CDC 则通常有两个接口一个 Communication Class 接口负责枚举、线路状态、通知一个 Data Class 接口负责实际串口数据。问题来了USB 规范里一个接口只能属于一个功能。如果主机看到两个接口它默认认为这是两个独立功能会分别加载驱动。但 usb_audio 和 CDC 各自都要求「我的多个接口属于同一个功能」这时候就需要Interface Association DescriptorIAD。IAD 告诉主机「接下来这几个接口是一伙的请把它们当成一个复合功能处理。」没有 IADWindows 可能把 AudioControl 和 AudioStreaming 拆成两个设备或者干脆不加载驱动。在描述符顺序上常见做法是配置描述符 → IAD音频→ AudioControl 接口 → AudioStreaming 接口 → IADCDC→ Communication 接口 → Data 接口。注意 IAD 必须紧跟在它关联的第一个接口之前且bFirstInterface和bInterfaceCount要准确。很多枚举失败就是这两个字段写错了。2.2 端点分配与带宽预算为什么音频一开串口就卡USB 全速Full Speed设备每帧 1ms高速High Speed设备每微帧 125μs。音频流对带宽和延迟敏感CDC 虽然数据量小但它的通知端点Interrupt IN会周期性占用带宽。如果端点分配不当会出现「音频播放正常但串口一发大数据就爆音」或者「串口正常音频丢包」的现象。我一般会这样分配端点功能端点类型方向典型最大包长FS说明音频播放IsochronousOUT192 字节/帧48kHz 16bit 立体声约 192B/ms音频采集IsochronousIN192 字节/帧同上CDC 通知InterruptIN16 字节线路状态、串口事件CDC 数据BulkIN/OUT64 字节实际串口数据关键点同步端点Isochronous必须预留带宽高速下每微帧最多 80% 带宽给周期传输。CDC 的 Interrupt 端点虽然小但也会占周期带宽。如果音频已经占了 70%CDC 通知端点再占一点就可能超限。解决办法是把 CDC 通知端点的bInterval调大比如从 16 调到 32降低占用频率。另外CDC 数据端点用 Bulk 而不是 InterruptBulk 不占周期带宽只在空闲带宽里传对音频影响最小。2.3 复合设备驱动在主机侧的加载逻辑主机侧看到复合设备后会根据接口的 Class/SubClass/Protocol 字段加载驱动。usb_audio 的 AudioControl 接口 Class0x01SubClass0x01Audio ControlProtocol0x00。AudioStreaming 接口 Class0x01SubClass0x02Audio StreamingProtocol0x00。CDC 的 Communication 接口 Class0x02SubClass0x02ACMProtocol0x01AT 命令。Data 接口 Class0x0A。Windows 下usb_audio 走usbaudio.sysCDC 走usbser.sys。如果 IAD 正确Windows 会为整个复合设备创建一个物理设备对象PDO然后为每个功能创建子设备。Linux 下snd-usb-audio和cdc_acm两个模块会分别绑定对应接口。macOS 下AppleUSBAudio和AppleUSBCDCACM各管各的。只要描述符正确三平台都能正常识别。提示如果 Windows 只识别出声卡、不识别串口先检查 CDC 的 Communication 接口是否带了 IAD以及bInterfaceCount是否包含了 Data 接口。3. 从零复现用 STM32 和 TinyUSB 搭一个 usb_audiocdc 复合设备3.1 工程结构与关键文件说明这份usb_audiocdc复合设备驱动.rar大概率是基于某个 MCU 厂商的 USB 协议栈比如 ST 的 STM32 USB Device Library或者 TinyUSB改的。我以 TinyUSB 为例因为它对复合设备的支持比较干净。工程结构通常是这样project/ ├── src/ │ ├── main.c │ ├── usb_descriptors.c # 描述符定义 │ ├── usb_audio.c # 音频回调 │ └── usb_cdc.c # CDC 回调 ├── inc/ │ ├── tusb_config.h # TinyUSB 配置 │ └── usb_descriptors.h └── Makefile / CMakeLists.txt核心是tusb_config.h里要同时使能CFG_TUD_AUDIO和CFG_TUD_CDC并且给它们分配足够的端点。下面是一个最小配置示例// tusb_config.h #define CFG_TUD_AUDIO 1 #define CFG_TUD_CDC 1 // 音频端点播放 OUT 1个采集 IN 1个 #define CFG_TUD_AUDIO_FUNC_1_N_AS_INT 1 #define CFG_TUD_AUDIO_FUNC_1_N_BYTES_PER_SAMPLE_RX 2 #define CFG_TUD_AUDIO_FUNC_1_N_BYTES_PER_SAMPLE_TX 2 #define CFG_TUD_AUDIO_FUNC_1_N_CHANNELS_RX 2 #define CFG_TUD_AUDIO_FUNC_1_N_CHANNELS_TX 2 // CDC 端点通知 IN 1个数据 IN/OUT 各1个 #define CFG_TUD_CDC_EP_BUFSIZE 64 #define CFG_TUD_CDC_RX_BUFSIZE 256 #define CFG_TUD_CDC_TX_BUFSIZE 256参数说明CFG_TUD_AUDIO_FUNC_1_N_AS_INT是音频流接口数量一般 1 个就够。N_BYTES_PER_SAMPLE和N_CHANNELS决定音频格式48kHz 16bit 立体声就是 2 字节 × 2 通道。CDC 的EP_BUFSIZE是端点最大包长全速下 64 字节高速下可以到 512。RX_BUFSIZE和TX_BUFSIZE是软件 FIFO 大小根据你的数据量调。3.2 描述符配置IAD 与接口顺序的代码实现描述符是复合设备最容易翻车的地方。下面这段代码展示了如何用 TinyUSB 的宏定义组织 usb_audio 和 CDC 的复合描述符// usb_descriptors.c uint8_t const desc_configuration[] { // 配置描述符 TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, CONFIG_TOTAL_LEN, 0x00, 100), // IAD for Audio TUD_AUDIO_DESC_IAD(ITF_NUM_AUDIO_CONTROL, 2, 0x00, 0x01, 0x00), // Audio Control 接口 TUD_AUDIO_DESC_STD_AC(ITF_NUM_AUDIO_CONTROL, 0, 0x00), TUD_AUDIO_DESC_AC_FEATURE_UNIT(...), TUD_AUDIO_DESC_STD_AS_INT(...), // Audio Streaming 接口 TUD_AUDIO_DESC_STD_AS(ITF_NUM_AUDIO_STREAMING, 0, 0x00), TUD_AUDIO_DESC_AS_FORMAT_TYPE(...), TUD_AUDIO_DESC_STD_AS_EP(...), // IAD for CDC TUD_CDC_DESC_IAD(ITF_NUM_CDC, 2, 0x00, 0x02, 0x02, 0x01), // CDC Communication 接口 TUD_CDC_DESC_ACM(...), TUD_CDC_DESC_HEADER(...), TUD_CDC_DESC_CALL_MGMT(...), TUD_CDC_DESC_ACM_CM(...), TUD_CDC_DESC_UNION(...), // CDC Data 接口 TUD_CDC_DESC_DATA(...), TUD_CDC_DESC_DATA_EP(...), };逻辑说明TUD_AUDIO_DESC_IAD的第一个参数是起始接口号第二个是接口数量AudioControl AudioStreaming 2后面是 Class/SubClass/Protocol。CDC 的 IAD 类似起始接口号要接在音频接口之后接口数量为 2Communication Data。ITF_NUM_TOTAL必须是所有接口的总和音频 2 个 CDC 2 个 4。如果这个数字写错主机枚举时会直接失败。参数说明TUD_CONFIG_DESCRIPTOR里的CONFIG_TOTAL_LEN是整个配置描述符集合的长度必须精确计算少一个字节都会导致枚举异常。bMaxPower设为 100即 200mA如果音频和 CDC 同时工作电流较大要适当调高。3.3 音频与 CDC 数据回调的协同处理描述符配好后实际数据流靠回调处理。TinyUSB 的音频回调在usb_audio.c里CDC 回调在usb_cdc.c里。关键是要保证音频的同步传输不被 CDC 的 Bulk 传输阻塞。下面是一个典型的音频播放回调// usb_audio.c bool tud_audio_rx_done_post_read_cb(uint8_t rhport, uint16_t n_bytes_received, uint8_t func_id, uint8_t ep_out, uint8_t cur_alt_setting) { // 从端点读取音频数据写入 I2S/DAC static int16_t audio_buf[CFG_TUD_AUDIO_FUNC_1_N_BYTES_PER_SAMPLE_RX * CFG_TUD_AUDIO_FUNC_1_N_CHANNELS_RX]; tud_audio_read(audio_buf, n_bytes_received); // 这里做实际播放比如 I2S 发送 i2s_write(audio_buf, n_bytes_received); return true; }CDC 的数据回调类似// usb_cdc.c void tud_cdc_rx_cb(uint8_t itf) { uint8_t buf[64]; uint32_t count tud_cdc_read(buf, sizeof(buf)); // 处理串口命令 process_command(buf, count); }逻辑说明音频回调是同步的必须在下一个 USB 帧到来之前完成读取否则会丢包。CDC 回调是异步的可以在主循环里轮询或者用中断触发。注意不要在音频回调里做耗时操作比如打印日志否则音频会卡顿。CDC 的数据处理可以放在主循环但要注意 FIFO 溢出。参数说明n_bytes_received是本次收到的字节数通常等于端点最大包长。tud_audio_read的第二个参数是缓冲区大小必须大于等于n_bytes_received。CDC 的tud_cdc_read返回实际读到的字节数如果为 0 表示没有数据。4. 避坑指南usb_audiocdc 复合设备最常见的 5 个翻车现场4.1 现象Windows 只识别声卡串口不出现原因CDC 的 IAD 缺失或者bInterfaceCount只写了 1。Windows 看到 Communication 接口和 Data 接口没有 IAD 关联会把它们当成两个独立设备但 Data 接口没有驱动匹配于是串口不出现。解决检查 CDC 的 IAD 描述符确保bFirstInterface指向 Communication 接口bInterfaceCount为 2。同时确认 Communication 接口的 Class/SubClass/Protocol 为 0x02/0x02/0x01Data 接口为 0x0A/0x00/0x00。4.2 现象音频播放正常但串口一发大数据就爆音原因CDC 数据端点用了 Interrupt 而不是 Bulk或者 CDC 通知端点的bInterval太小占用了太多周期带宽导致音频同步端点带宽不足。解决把 CDC 数据端点改为 Bulk 类型Bulk 不占周期带宽。CDC 通知端点的bInterval从 16 调到 32 或更大。如果还是爆音检查音频端点的wMaxPacketSize是否超过了当前速度下的带宽限制。4.3 现象Linux 下能识别两个设备但 macOS 只识别一个原因macOS 对 IAD 的解析比较严格如果 IAD 的bFirstInterface和实际接口号不匹配或者 IAD 放在了错误的位置比如放在了接口之后macOS 会忽略 IAD。解决确保 IAD 紧跟在配置描述符之后、第一个关联接口之前。bFirstInterface必须等于第一个关联接口的bInterfaceNumber。用 USB 分析仪抓包确认描述符顺序。4.4 现象枚举时好时坏插拔几次才能识别原因描述符总长度CONFIG_TOTAL_LEN计算错误或者端点地址冲突。比如音频用了 EP1 INCDC 也用了 EP1 IN主机在枚举时读到冲突的端点描述符就会随机失败。解决用lsusb -vLinux或者 USB Tree ViewerWindows查看实际枚举出的描述符。确保每个端点的地址唯一且方向不冲突。CONFIG_TOTAL_LEN用sizeof(desc_configuration)自动计算不要手写。4.5 现象CDC 串口能发不能收或者收的数据不完整原因CDC 的 Data 接口端点方向搞反了。CDC Data 接口通常需要两个 Bulk 端点一个 IN设备到主机一个 OUT主机到设备。如果只配了一个或者方向写反就会出现单向通信。解决检查TUD_CDC_DESC_DATA_EP宏的参数确保 IN 和 OUT 端点都正确声明。另外CDC 的tud_cdc_read要在主循环里持续调用不能只调一次。注意如果用了 DMA 搬运音频数据要确保 DMA 缓冲区和 USB 端点缓冲区不冲突否则会出现数据错位。5. 进阶技巧用 USB 分析仪验证复合设备枚举与带宽分配5.1 抓包看描述符确认 IAD 和接口顺序光看代码不够真正靠谱的验证手段是 USB 分析仪。我用的是 Total Phase Beagle USB 480便宜点的可以用 Wireshark usbmonLinux或者 USBlyzerWindows。抓包时重点看三个地方第一设备描述符的bDeviceClass应该是 0x00复合设备用 0x00由接口描述符定义功能bNumConfigurations为 1。第二配置描述符集合里IAD 的bFirstInterface和bInterfaceCount是否和实际接口匹配。第三每个接口的端点描述符bEndpointAddress是否唯一wMaxPacketSize是否符合预期。下面是一个抓包后解析描述符的 Python 脚本片段用来快速检查 IADimport usb.core dev usb.core.find(idVendor0x1234, idProduct0x5678) cfg dev.get_active_configuration() for intf in cfg: print(fInterface {intf.bInterfaceNumber}, Class {intf.bInterfaceClass}, SubClass {intf.bInterfaceSubClass}) for ep in intf: print(f Endpoint 0x{ep.bEndpointAddress:02x}, Type {ep.bmAttributes}, MaxPacket {ep.wMaxPacketSize})逻辑说明这段脚本遍历当前配置的所有接口和端点打印出 Class、SubClass 和端点信息。如果 IAD 正确你会看到音频的 AudioControl 和 AudioStreaming 接口号连续CDC 的 Communication 和 Data 接口号连续。如果接口号跳变或者 Class 不对说明描述符有问题。参数说明idVendor和idProduct换成你设备的实际值。bInterfaceClass为 0x01 是音频0x02 是 CDC Communication0x0A 是 CDC Data。5.2 带宽验证用音频流压力测试暴露 CDC 干扰描述符对了不代表带宽分配合理。我一般会做一个压力测试让音频以最大采样率持续播放同时 CDC 以最大速率发送数据观察音频是否丢包。在 Linux 下可以用arecord和socat同时跑# 终端1录音并统计丢包 arecord -D hw:1,0 -f S16_LE -r 48000 -c 2 -d 60 test.wav # 终端2向 CDC 串口持续写数据 socat -d -d pty,raw,echo0,link/dev/ttyACM0 exec:yes 0123456789abcdef如果录音文件里有明显的爆音或者arecord报 overrun说明 CDC 占用了太多带宽。这时候要回头检查 CDC 通知端点的bInterval或者把 CDC 数据端点从 Interrupt 改成 Bulk。5.3 一个我踩过的坑IAD 的 bInterfaceCount 写成了 1最后说一个血泪教训。有一次我调一个 usb_audiocdc 复合设备Windows 下声卡正常串口死活不出来。抓包看描述符发现 CDC 的 IAD 里bInterfaceCount写成了 1只包含了 Communication 接口Data 接口被漏掉了。主机认为 Data 接口是独立设备但又没有对应的驱动于是直接忽略。改成 2 之后串口立刻出现。这个坑的隐蔽性在于Linux 下cdc_acm模块比较宽容即使 IAD 不完整也能绑定 Data 接口所以 Linux 测试通过不代表 Windows 没问题。我的习惯是每次改完描述符先在 Windows 上用 USB Tree Viewer 看一遍再用 Linux 的lsusb -v交叉验证。两个平台都过了基本就稳了。希望帮到你。本文还有配套的精品资源点击获取
返回列表