☰
RK平台UAC1+CDC ACM复合设备驱动:legacy gadget实现开机即用
2026/10/1 3:36:04 网站建设 项目流程

简介:这份资源面向 Linux 与 Android 底层驱动开发者,尤其是需要在 RK 平台上实现 USB 复合设备功能的工程师。它提供了一套基于 legacy 方式的 UAC1 与 CDC 串口复合设备驱动方案,开机即可直接枚举为 USB Audio 与 UART,无需额外配置脚本;若改用脚本方式,只需不编译 legacy 驱动,并在 USB 脚本中配置 acm、uac1 功能即可,灵活性较高。压缩包共 12 个文件,约 41KB,包含 4 个 Kconfig、4 个 Makefile、2 个 C 源码文件以及 2 个 rk3308_linux_defconfig 配置文件,覆盖驱动编译配置、内核选项与平台适配入口,便于快速移植到其他平台。目前已有 1010 人学习下载,适合希望理解 USB Gadget 复合设备枚举流程、掌握 UAC 与 CDC 组合驱动实现思路的开发者参考,也可作为 RK 平台音频与串口一体化方案的实践起点。

1. 一个 .rar 里塞着 UAC1 与 CDC 的复合设备驱动,到底解决了什么

做过 RK 平台 USB 外设的兄弟大概率遇到过这种需求:一块板子既要当 USB 声卡用,又要同时暴露一个串口给上位机通信。单独配 UAC 或者单独配 CDC-ACM 都不难,难的是让它们在同一个 USB 配置里共存,还要在 Linux 和 Android 上都能开机直接枚举,不依赖任何 init.rc 或 gadget 脚本。这个usb_audio+cdc复合设备驱动.rar就是干这件事的——它基于 legacy gadget 框架,把 UAC1 音频和 CDC ACM 串口打包成一个复合设备,插上 USB 线就能同时看到声卡和 tty 设备节点,不需要额外跑配置脚本。适合正在做 RK 平台 USB 音频+串口二合一产品的嵌入式工程师,也适合想搞清楚 legacy gadget 与 configfs 两种复合设备实现差异的人。关键词就三个:uac1cdc复合设备、usbaudio复合设备、uac+acm,下面拆开讲。

2. legacy gadget 与 configfs 两条路:为什么这份驱动选了前者

2.1 legacy 与 configfs 的本质区别

Linux USB gadget 框架发展到现在,实际上存在两套并行的实现方式。一套是早期的 legacy 方式,驱动代码里直接定义struct usb_composite_driver,把功能模块(function)和配置描述符全部硬编码在 C 文件里,模块加载即完成枚举。另一套是 configfs 方式,内核只提供骨架,用户空间通过写/sys/kernel/config/usb_gadget/下的文件来动态组合功能,Android 上通常由 init.rc 触发脚本完成配置。

legacy 的优势在于确定性。代码编译进去,开机自动加载,枚举流程完全可预期,不会因为脚本执行顺序、文件系统挂载时机、SELinux 策略等问题导致 gadget 没起来。劣势是灵活性差,想换一个 PID/VID 或者调整功能组合,得改代码重新编译。configfs 反过来,灵活但依赖用户空间配合,Android 上尤其容易因为 init 阶段时序问题翻车。

这份驱动选 legacy,核心原因就一个:开机即用,零配置。对于量产设备来说,少一个环节就少一类现场故障。

2.2 复合设备描述符的组织方式

UAC1 + CDC ACM 复合设备的关键在于描述符的正确组织。USB 复合设备需要一套完整的描述符树:设备描述符、配置描述符、接口关联描述符(IAD)、以及各功能自己的接口描述符和端点描述符。

UAC1 音频功能通常占用两个接口:一个控制接口(用于音量、静音等控制)和一个流接口(用于音频数据传输),流接口需要一个等时端点。CDC ACM 则占用两个接口:一个通信类接口(含中断端点用于通知)和一个数据类接口(含批量输入输出端点)。两者加起来,一个配置里至少有四个接口、三个端点(一个等时、一个中断、两个批量)。

描述符里最容易出错的是 IAD 的使用。当复合设备中某个功能占用多个接口时,必须用 IAD 把它们关联起来,否则主机端可能只识别到部分接口。Windows 对 IAD 的解析尤其严格,Linux 相对宽松,但为了兼容性,该加的 IAD 一个都不能少。

2.3 代码结构与关键文件

解压后的源码目录通常包含以下几个核心文件:

文件作用
f_uac1.cUAC1 音频功能实现,含音频控制与流接口逻辑
f_acm.cCDC ACM 串口功能实现,含 tty 注册与数据收发
f_audio_cdc.c复合设备主驱动,组织描述符、注册 composite driver
Makefile/Kconfig编译配置,决定是否编入 legacy 路径
audio_cdc_defs.h描述符宏定义与端点地址分配

主驱动里会定义一个usb_composite_driver结构体,.name字段通常为"audio_cdc",.dev指向设备描述符,.strings指向字符串描述符。在audio_cdc_bind()回调里,依次调用acm_bind_config()和uac1_bind_config()把两个功能挂到同一个配置上。

static struct usb_composite_driver audio_cdc_driver = { .name = "audio_cdc", .dev = &audio_cdc_device_desc, .strings = audio_cdc_strings, .max_speed = USB_SPEED_HIGH, .bind = audio_cdc_bind, .unbind = audio_cdc_unbind, }; static int __init audio_cdc_init(void) { return usb_composite_probe(&audio_cdc_driver); } module_init(audio_cdc_init);

这段代码的逻辑很直白:模块加载时调用usb_composite_probe()注册复合驱动,内核 gadget 层随后调用.bind回调完成功能绑定。max_speed设为高速,如果板子只支持全速,改成USB_SPEED_FULL即可。注意module_init的加载时机,如果驱动编成模块,需要确保在 USB 控制器驱动之后加载。

2.4 端点分配与带宽考量

高速模式下,UAC1 音频流端点通常用等时传输,每个微帧传 192 字节(48kHz、16bit、双声道)。CDC ACM 的中断端点一般 16 字节,批量端点 512 字节。端点地址不能冲突,UAC1 的等时端点常用0x01(OUT)和0x82(IN),CDC 的中断端点用0x83,批量端点用0x04(OUT)和0x85(IN)。这些地址在audio_cdc_defs.h里以宏定义形式给出,改的时候要同步改描述符和功能代码里的端点引用,漏改一处就枚举失败。

3. 从编译到枚举:RK 平台上的完整落地步骤

3.1 内核配置与编译

拿到源码后,第一步是确认内核版本和 gadget 框架的配置。RK 平台常见的 4.19 和 5.10 内核都支持 legacy gadget,但配置项名称略有差异。在arch/arm64/configs/下找到板级 defconfig,确保以下配置打开:

CONFIG_USB_GADGET=y CONFIG_USB_CONFIGFS=n CONFIG_USB_LEGACY=y CONFIG_USB_AUDIO=y CONFIG_USB_CDC_COMPOSITE=y CONFIG_SND_USB_AUDIO=y

如果内核里没有CONFIG_USB_LEGACY这个选项,说明 gadget 框架已经全面转向 configfs,legacy 路径可能被裁剪了。这种情况下需要把源码里的 legacy 驱动文件放到drivers/usb/gadget/legacy/下,并在该目录的 Makefile 和 Kconfig 里添加对应条目。

obj-$(CONFIG_USB_AUDIO_CDC) += audio_cdc.o audio_cdc-y := f_audio_cdc.o f_uac1.o f_acm.o

这段 Makefile 把三个源文件编成一个模块audio_cdc.ko。f_uac1.o和f_acm.o如果内核里已有同名文件,需要重命名避免冲突,比如改成f_uac1_cdc.o和f_acm_cdc.o,同时改掉源码里的函数名和导出符号。

3.2 设备树与 UDC 确认

RK 平台的 USB 控制器设备树节点通常叫usbdrd3_0或usb@fcc00000,里面要确保dr_mode是otg或peripheral,status是okay。如果dr_mode设成host,gadget 侧根本不会注册 UDC,驱动加载了也枚举不了。

&usbdrd3_0 { status = "okay"; dr_mode = "otg"; };

确认 UDC 是否就绪,可以在板子起来后执行:

ls /sys/class/udc/

有输出(比如fcc00000.dwc3)说明 UDC 注册成功。如果目录为空,先查 USB 控制器驱动有没有加载、设备树状态对不对、供电是否正常。

3.3 加载驱动与验证枚举

驱动编译成模块后,推送到板子加载:

insmod audio_cdc.ko dmesg | tail -30

dmesg 里应该能看到audio_cdc相关的 probe 日志,以及gadget: audio_cdc ready之类的提示。然后检查功能节点:

ls /dev/ttyGS* ls /dev/snd/

/dev/ttyGS0是 CDC ACM 的串口节点,/dev/snd/下应该出现一个 USB 音频设备。如果 ttyGS 没出来,查f_acm.c里gs_alloc_req和gs_bind的返回值;如果声卡没出来,查 UAC1 的snd_card注册流程。

主机端(PC 或另一块板子)插上 USB 线后,lsusb应该能看到一个设备,包含 Audio 和 CDC 两个功能。Linux 主机上dmesg会打印usb 1-1: new high-speed USB device以及cdc_acm和snd-usb-audio的绑定信息。Windows 上可能需要装 CDC ACM 驱动才能识别串口,音频部分通常免驱。

3.4 音频通路与串口回环测试

音频测试用arecord和aplay:

arecord -D hw:1,0 -f S16_LE -r 48000 -c 2 -d 5 test.wav aplay -D hw:1,0 test.wav

hw:1,0里的1是声卡编号,具体值用aplay -l查。如果录音全是静音,检查 UAC1 的流接口端点是否使能、时钟源配置是否正确。CDC 串口回环测试:

stty -F /dev/ttyGS0 115200 raw -echo cat /dev/ttyGS0 & echo "hello" > /dev/ttyGS0

主机端用串口工具打开对应的 COM 口或/dev/ttyACM0,发数据应该能在板子端cat出来。如果数据丢包,查批量端点的req->length和actual是否匹配,以及f_acm.c里acm_cdc_notify的中断端点有没有正常上报。

4. 避坑与排查:枚举失败、音频无声、串口丢数据的血泪经验

4.1 枚举失败,lsusb 看不到设备

现象:驱动加载了,dmesg 有 probe 日志,但主机端lsusb完全看不到设备,或者只看到一个未知设备。

原因:最常见的是描述符总长度计算错误。复合设备的配置描述符wTotalLength必须等于所有接口、端点、类特定描述符的字节数之和。少算一个 IAD 或者端点描述符,主机就会在解析配置时失败。另一个原因是端点地址冲突,两个功能用了同一个端点号。

解决:用usbmon抓包看主机端到底收到了什么。wTotalLength的值和实际字节数对不上时,主机通常会直接复位设备。逐个核对audio_cdc_defs.h里的描述符宏,确保每个USB_DT_*的bLength和实际定义一致。端点地址用grep -n "0x8" f_*.c全局搜一遍,确认没有重复。

4.2 声卡识别了但录音/播放无声

现象:/dev/snd/下有设备节点,aplay -l也能看到,但播放没声音,录音全是零。

原因:UAC1 的流接口端点没有正确使能,或者音频时钟源配置不对。UAC1 规范里,流接口的bAlternateSetting为 0 时是零带宽,为 1 时才真正传输数据。主机端snd-usb-audio驱动会先选 alt 0,开始播放时切到 alt 1。如果驱动里 alt 1 的端点描述符有问题,切换就会失败。

解决:检查f_uac1.c里uac1_bind_config()中流接口的bAlternateSetting和端点描述符。确保 alt 1 的bNumEndpoints为 1,端点属性是USB_ENDPOINT_XFER_ISOC,wMaxPacketSize和采样率匹配。另外确认snd_usb_audio模块在主机端已加载,dmesg里搜cannot set freq或cannot submit urb之类的错误。

4.3 CDC 串口节点出不来

现象:/dev/ttyGS0不存在,或者存在但打不开。

原因:f_acm.c里gs_bind()返回失败,通常是gs_alloc_req()分配端点请求失败,或者tty_register_driver没执行。RK 平台上如果CONFIG_USB_GADGET的 UDC 没就绪,usb_composite_probe会延迟,功能绑定也不会执行。

解决:先确认/sys/class/udc/有设备。没有的话查 USB 控制器驱动和设备树。有 UDC 但 ttyGS 没出来,在f_acm.c的gs_bind里加pr_info打印每一步返回值,定位是gs_alloc_req失败还是tty_register_driver失败。gs_alloc_req失败通常是端点被占用或 UDC 不支持所需传输类型。

4.4 串口大数据量传输丢包

现象:小数据量收发正常,一跑高速率(比如 1Mbps 以上)就丢数据。

原因:批量端点的请求队列深度不够,或者f_acm.c里acm_read_complete回调没有及时重新提交请求。CDC ACM 的批量传输依赖 URB 链,如果只提交一个请求,处理完再提交下一个,中间的空窗期就会丢数据。

解决:在gs_bind里多分配几个gs_alloc_req,比如 4 到 8 个,每个请求的 buffer 大小设为端点最大包长的整数倍。在完成回调里立即重新提交请求,保持队列始终有数据。另外检查f_acm.c里的GS_LOG或类似调试开关,打开后能看到请求提交和完成的时序。

4.5 Android 上 SELinux 拦截导致节点不可用

现象:Linux 上一切正常,Android 上/dev/ttyGS0存在但应用打不开,或者声卡节点权限不对。

原因:Android 的 SELinux 策略默认不允许普通应用访问ttyGS设备节点,ueventd.rc里也没有对应的权限规则。

解决:在ueventd.rc里添加/dev/ttyGS0 0660 system system,并在 SELinux 策略里给对应 domain 添加rw_file_perms。音频节点同理,需要确认audio_device的 label 和权限。如果不想改策略,把应用放到system分区或者用su调试,但量产固件必须走正规策略配置。

5. 进阶玩法:从 legacy 切到 configfs 脚本方式,以及一个验证习惯

legacy 驱动虽然省心,但产品迭代时难免要调 PID/VID、换功能组合、或者在同一固件里支持多种配置。这时候把功能代码保留、只切到 configfs 脚本方式,是个平滑过渡方案。具体做法是:不编译f_audio_cdc.c这个 legacy 主驱动,只保留f_uac1.c和f_acm.c作为功能模块,然后在用户空间用脚本组合。

#!/bin/sh GADGET=/sys/kernel/config/usb_gadget/audio_cdc mkdir -p $GADGET echo 0x2207 > $GADGET/idVendor echo 0x0010 > $GADGET/idProduct mkdir -p $GADGET/strings/0x409 echo "RK Audio CDC" > $GADGET/strings/0x409/product mkdir -p $GADGET/functions/acm.usb0 mkdir -p $GADGET/functions/uac1.usb0 mkdir -p $GADGET/configs/c.1 ln -s $GADGET/functions/acm.usb0 $GADGET/configs/c.1/ ln -s $GADGET/functions/uac1.usb0 $GADGET/configs/c.1/ ls /sys/class/udc/ | head -1 > $GADGET/UDC

这段脚本的逻辑是:创建 gadget 目录,设置 VID/PID 和字符串,实例化 acm 和 uac1 两个功能,挂到配置 c.1 下,最后把 UDC 写进去触发枚举。idVendor和idProduct按实际项目改,uac1.usb0的音频参数(采样率、通道数)可以在功能目录下的p_srate、c_srate、p_chmask等文件里调。脚本方式的好处是改配置不用重编内核,坏处是 Android 上要处理 init 时序和 SELinux,量产时得把脚本固化到init.rc或vendor分区。

我自己的习惯是:每次改完描述符或者端点分配,先在 Linux 主机上用usbmon抓一次完整枚举流程,确认wTotalLength、接口数、端点地址全部正确,再推到板子上跑。这个习惯帮我省了至少三次“板子端一切正常但主机不认”的排查时间。另外,legacy 和 configfs 不要同时开,内核里两个路径都编进去时,加载顺序不同可能导致 UDC 被先注册的那个占用,后加载的报-EBUSY。从那以后我每次切方案都先把旧路径的CONFIG_关干净,再编新路径。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询