RK平台UAC1+CDC复合设备:legacy驱动开机即枚举方案
简介这份资源面向嵌入式Linux与Android底层驱动开发者提供基于Rockchip平台实现的UAC与CDC串口复合设备驱动方案解决设备开机后需依赖配置脚本才能枚举为USB音频与串口的问题。驱动采用legacy方式Linux下无需设置usb_gadgetAndroid下无需修改init.rc上电即可直接枚举为USB Audio与UART若需脚本方式只需不编译legacy驱动并在USB配置脚本中启用acm与uac1功能即可理论全平台适用。压缩包共12个文件约41KB包含4个Kconfig、4个Makefile、2个C源文件及2份rk3308_linux_defconfig覆盖内核配置、编译规则、驱动实现与板级默认配置结构紧凑便于移植参考。目前已有1010人学习下载适合需要快速验证UACCDC复合设备枚举、理解legacy与脚本两种加载差异的开发者参考。1. 开机即枚举的 uac1cdc 复合设备这份 legacy 驱动到底省了哪一步做过 RK 平台 USB 外设的兄弟大概率都碰过这个场景板子要同时当 USB 声卡和 USB 串口用插上主机后系统里得同时冒出card和ttyACM两个设备。常规做法是在init.rc或usb_gadget脚本里配一堆 configfs 节点功能是acm加uac1每次开机跑一遍脚本才枚举。这份usb_audiocdc复合设备驱动.rar走的是另一条路——legacy 内核驱动方式把 uac1 和 cdc acm 直接编进 gadget 驱动里开机不做任何脚本配置插上就枚举成 USB Audio 和 UART 两个接口。它基于 RK 平台验证过理论上全平台通用适合那些不想在用户态维护 gadget 脚本、又需要音频加串口同时工作的嵌入式项目。下面按「这是什么、怎么编、怎么改、坑在哪」的顺序拆一遍。2. legacy 与 configfs 两条路uac1cdc 复合设备的驱动选型与内核配置2.1 为什么这份驱动走 legacy 而不是 configfsLinux 的 USB gadget 框架有两条实现路径。一条是老的 legacy 方式gadget 功能在内核里静态注册模块加载或内核启动时就绑定好用户态不用管另一条是 configfs 方式内核只提供libcomposite具体功能由用户态往/sys/kernel/config/usb_gadget/里写目录和属性来拼装。Android 从某个版本开始默认推 configfs因为灵活可以运行时切换功能组合。但灵活是有代价的。configfs 方案要求开机阶段有脚本或init.rc去创建 gadget、写 VID/PID、配字符串、绑 UDC任何一步顺序错了或者节点没建全枚举就失败。而且音频功能在 configfs 下的 uac1 配置项比较绕chmask、c_chmask、p_chmask、c_ssize、p_ssize这些参数得对着 UAC1 描述符规范填填错主机端要么认不出声卡要么采样率不对。legacy 方式把这些都固化在驱动源码里。这份资源的核心就是一份已经写好 uac1 加 cdc acm 复合功能的 gadget 驱动编译进内核后gadget 在 probe 阶段就把两个 function 注册好UDC 一就绪就枚举。省掉的是用户态脚本这一层换来的是「开机即用」的确定性。对于产品形态固定、不需要运行时切换功能的设备legacy 反而更省心。2.2 内核配置项怎么开不管走哪条路内核侧的 USB gadget 基础配置都得先到位。常见做法是在arch/arm64/configs/下对应的 defconfig 里确认这几项# USB gadget 基础框架 CONFIG_USB_GADGETy CONFIG_USB_GADGET_DUALSPEEDy # legacy gadget 驱动入口这份资源依赖它 CONFIG_USB_GADGET_LEGACYy # 音频功能uac1 走这个 CONFIG_USB_CONFIGFS_F_UAC1y CONFIG_SND_USB_AUDIOy # cdc acm 串口功能 CONFIG_USB_CONFIGFS_F_ACMy CONFIG_USB_CONFIGFS_F_CDCy # RK 平台 UDC 控制器按实际 SoC 选 CONFIG_USB_DWC3y CONFIG_USB_DWC3_DUAL_ROLEy这里有个容易翻车的点CONFIG_USB_GADGET_LEGACY和CONFIG_USB_CONFIGFS不是互斥的但如果你同时开了 configfs 又想让 legacy 驱动生效得确认 legacy 驱动的注册优先级。有些内核版本里 legacy gadget 和 configfs 会抢同一个 UDC表现为udc绑定失败或者枚举出零个配置。稳妥做法是产品定型后只留一条路要么纯 legacy要么纯 configfs别混着来。2.3 复合设备的描述符是怎么拼的uac1 加 cdc acm 复合本质是把两组接口描述符塞进同一个 configuration 里。主机枚举时读 configuration descriptor看到两个 interface association一个归 audio class一个归 cdc class。legacy 驱动里这部分通常是一个usb_descriptor数组按device → config → interface → endpoint的顺序排。音频侧要提供 UAC1 的 AC 接口控制和 AS 接口流AS 接口里带一个 isochronous endpoint 用于音频数据。CDC ACM 侧要提供 communication interface 和 data interfacedata interface 上挂 bulk in/out 两个 endpoint。两组的 endpoint 地址不能冲突比如音频用0x81/0x01CDC 就用0x82/0x02。这份资源既然能开机直接枚举出 audio 和 uart说明描述符拼接和 endpoint 分配是验证过的改的时候别乱动地址。注意UAC1 的采样率描述符是固定列表主机只会从你声明的列表里选。如果你只写了 48000主机就不会出 44100 的选项别指望它自适应。3. 把驱动编进内核并验证枚举从源码到 card 和 ttyACM 出现3.1 源码放置与编译接入拿到压缩包后先看目录结构。legacy gadget 驱动一般放在drivers/usb/gadget/legacy/下文件名可能是audio_cdc.c或类似。把它拷进内核树对应位置然后改Makefile和Kconfig# drivers/usb/gadget/legacy/Makefile obj-$(CONFIG_USB_GADGET_LEGACY_AUDIO_CDC) audio_cdc.o# drivers/usb/gadget/legacy/Kconfig config USB_GADGET_LEGACY_AUDIO_CDC tristate Audio CDC ACM composite gadget depends on USB_GADGET_LEGACY select USB_F_UAC1 select USB_F_ACM help UAC1 CDC ACM composite gadget, no userspace config needed.然后在 defconfig 里把CONFIG_USB_GADGET_LEGACY_AUDIO_CDCy打开。编译时如果报undefined reference to usb_get_function之类的错说明USB_F_UAC1或USB_F_ACM没被 select 上回去检查 Kconfig 的 select 链。3.2 开机枚举的验证步骤烧录后开机先看内核日志里 gadget 有没有绑上 UDCdmesg | grep -i -E gadget|udc|dwc3正常应该能看到类似audio_cdc gadget: bound to udc或者dwc3 ... registered的输出。如果只有 UDC 注册没有 gadget 绑定多半是 legacy 驱动没编进去或者 probe 失败。接着在设备端确认音频和串口节点# 音频应该出现一个 usb audio card cat /proc/asound/cards # 串口应该出现 ttyACM0 ls /dev/ttyACM*主机端比如一台 Linux PC插上后# 主机侧看声卡 arecord -l aplay -l # 主机侧看串口 ls /dev/ttyACM*主机侧能看到 card 和 ttyACM说明复合枚举成功。如果只有其中一个回去查描述符里对应 interface 的 endpoint 是不是被另一个功能占用了。3.3 音频通路的参数确认UAC1 枚举出来后别急着测声音先确认采样率和通道数。设备端可以用tinymix或直接看/proc/asound/cardX/stream0cat /proc/asound/card1/stream0输出里会列出Format、Channels、Rates。这份驱动如果声明的是 48kHz 双声道主机侧arecord就得按这个参数录arecord -D hw:1,0 -f S16_LE -r 48000 -c 2 test.wav参数对不上会报cannot set hw params或者录出来全是噪声。CDC 侧相对简单ttyACM出来就能用minicom或picocom收发波特率在 ACM 下是虚拟的设多少都行但两端要一致。4. 改功能组合与参数从 uac1acm 切到纯 acm 或调采样率4.1 不编译 legacy 驱动改用脚本方式这份资源的一个灵活点是如果你哪天想切回 configfs 脚本方式不用改驱动源码只要不编译 legacy 驱动然后在init.rc或启动脚本里配acm和uac1就行。脚本方式的核心是往 configfs 写节点# 创建 gadget mkdir /sys/kernel/config/usb_gadget/g1 cd /sys/kernel/config/usb_gadget/g1 # 写 VID/PID echo 0x2207 idVendor echo 0x0001 idProduct # 创建配置 mkdir configs/c.1 # 加 acm 功能 mkdir functions/acm.gs0 ln -s functions/acm.gs0 configs/c.1/ # 加 uac1 功能 mkdir functions/uac1.gs0 echo 0x3 functions/uac1.gs0/c_chmask echo 48000 functions/uac1.gs0/c_srate echo 0x3 functions/uac1.gs0/p_chmask echo 48000 functions/uac1.gs0/p_srate ln -s functions/uac1.gs0 configs/c.1/ # 绑 UDC echo $(ls /sys/class/udc | head -1) UDCc_chmask和p_chmask是通道掩码0x3表示双声道c_srate/p_srate是采样率。这套脚本方式的好处是可以在运行时改坏处是开机得保证脚本跑完Android 下还要处理init.rc的触发时机。4.2 调整音频采样率和通道数legacy 驱动里改采样率找描述符数组里的srate字段。UAC1 的采样率在 AS interface 的 type I format descriptor 里通常是三个字节的小端表示48000 就是0x80 0xBB 0x00。改完重新编译烧录。通道数改bNrChannels同时 endpoint 的wMaxPacketSize要跟着调48kHz 双声道 16bit 的包大小是48000 * 2 * 2 / 1000 192字节每毫秒单声道减半。注意改完采样率后主机侧缓存可能还留着旧参数拔插一次或者rmmod/insmod主机驱动再测别被缓存骗了。4.3 CDC ACM 的端口号和权限ttyACM的编号是内核按枚举顺序分的如果系统里还有别的 CDC 设备编号可能不是 0。产品里最好用 udev 规则固定名字# /etc/udev/rules.d/99-audio-cdc.rules SUBSYSTEMtty, ATTRS{idVendor}2207, ATTRS{idProduct}0001, SYMLINKttyAudioCdc这样应用层固定用/dev/ttyAudioCdc不用管ttyACM几。权限方面非 root 进程要访问得加dialout组或者改 udev 的MODE。5. 避坑与排查枚举失败、音频不出声、串口丢数据的血泪经验5.1 插上主机只认到一个功能现象主机侧只出现声卡或者只出现ttyACM另一个功能消失。原因通常是两个 interface 的 endpoint 地址撞了或者 configuration 的bNumInterfaces数不对。legacy 驱动里描述符是手写的加功能时容易漏改总数。解决核对config描述符的bNumInterfaces是否等于两组 interface 之和再逐个检查 endpoint 地址bEndpointAddress有没有重复。5.2 声卡枚举出来但录音全是噪声现象arecord能跑但录出来是白噪声或者断断续续。原因多半是 isochronous endpoint 的wMaxPacketSize和实际音频带宽不匹配或者 UDC 的 FIFO 分配不够。RK 平台的 dwc3 对 iso endpoint 的 FIFO 有要求音频这种周期性传输要预留足够深度。解决查dmesg里有没有dwc3 ... no fifo之类的报错有的话调dr_mode或 FIFO 配置没有的话核对包大小和采样率是否自洽。5.3 开机偶发枚举失败重启就好现象冷启动时主机认不到设备热重启正常。原因是 legacy gadget 在 UDC 就绪前就尝试绑定或者电源域还没稳定。RK 平台常见于 USB PHY 初始化时序。解决在 gadget 驱动的 probe 里加 UDC 就绪等待或者用late_initcall替代module_init延后注册。也可以查dmesg里 gadget 绑定和 UDC 注册的先后顺序绑定早于注册就会失败。5.4 串口大数据量下丢包现象ttyACM小数据能通一跑高速传输就丢。原因是 CDC 的 bulk endpoint 包大小设小了或者应用层没做流控。ACM 本身没有硬件流控全靠 USB 的 bulk 重传但主机侧缓冲有限。解决把 bulk endpoint 的wMaxPacketSize设到 512高速或 64全速应用层加发送间隔或者用tcdrain等发送完成。5.5 换内核版本后编译报错现象在新内核上编译 legacy 驱动报 API 不匹配比如usb_ep_autoconfig参数变了。原因是 legacy gadget API 在内核演进中改过。解决对照新内核drivers/usb/gadget/legacy/下现成的驱动比如zero.c或audio.c改函数签名别硬套老代码。这份资源基于 old kernel 验证new kernel 上要留出适配时间。6. 进阶用 configfs 动态切功能组合并做自动化验证legacy 驱动胜在开机即用但产品如果需要在「纯串口」和「音频加串口」之间切换legacy 就力不从心了得回到 configfs。一个实用技巧是把 configfs 配置封装成脚本开机时根据 GPIO 电平或者配置文件决定加载哪套功能。比如检测到某个引脚拉高就只配acm拉低就配acm加uac1#!/bin/sh # /usr/bin/usb_mode.sh GADGET/sys/kernel/config/usb_gadget/g1 UDC$(ls /sys/class/udc | head -1) setup_base() { mkdir -p $GADGET echo 0x2207 $GADGET/idVendor echo 0x0001 $GADGET/idProduct mkdir -p $GADGET/configs/c.1 } add_acm() { mkdir -p $GADGET/functions/acm.gs0 ln -sf $GADGET/functions/acm.gs0 $GADGET/configs/c.1/ } add_uac1() { mkdir -p $GADGET/functions/uac1.gs0 echo 0x3 $GADGET/functions/uac1.gs0/c_chmask echo 48000 $GADGET/functions/uac1.gs0/c_srate echo 0x3 $GADGET/functions/uac1.gs0/p_chmask echo 48000 $GADGET/functions/uac1.gs0/p_srate ln -sf $GADGET/functions/uac1.gs0 $GADGET/configs/c.1/ } setup_base add_acm # 读 GPIO 或配置文件决定是否加音频 if [ $(cat /etc/usb_audio_enable 2/dev/null) 1 ]; then add_uac1 fi echo $UDC $GADGET/UDC这个脚本的关键是ln -sf建符号链接的顺序和UDC写入的时机UDC 必须最后写写早了功能没配全就枚举成空设备。验证方法切模式后拔插一次主机侧lsusb -t看接口树确认 audio 和 cdc 的 interface 都在或者只有 cdc。自动化验证可以写个 udev 规则设备枚举时触发脚本记录lsusb -v输出对比预期描述符。我一般会在产测脚本里加一步插上后等 2 秒arecord录 1 秒静音echo test /dev/ttyACM0回环读回两个都过才算这板子 USB 复合功能正常。从那以后我每次改描述符或者换内核都强制走一遍这个回环省得批量出货了才发现音频通道没通。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

Windows应用锁全攻略:如何有效保护微信QQ隐私

Windows应用锁全攻略:如何有效保护微信QQ隐私

临时离开座位,最怕的不是电脑卡顿,而是别人顺手点开你的微信聊天记录、QQ对话框,或者翻翻你桌面上的文件。很多人第一反应是设个开机密码,但密码只能防君子不防小人——你人还在工位上,系统处于登录状态,屏…

2026/10/9 3:12:59 阅读更多 →
伪装成即时通讯更新的间谍软件:攻击链路与防御指南

伪装成即时通讯更新的间谍软件:攻击链路与防御指南

1. 危害场景复盘:更新弹窗如何变成间谍软件入口1.1 一条异常更新请求引发的损失先说一个我实际参与处置过的案例。某家不足两百人的贸易公司,财务总监在办公电脑上看到即时通讯工具弹出更新提示,窗口样式、图标、按钮布局都和官方更新界面几乎…

2026/10/9 3:12:59 阅读更多 →
Linux网络编程实战:自定义协议实现网络计算器

Linux网络编程实战:自定义协议实现网络计算器

最近把Linux下的网络编程重新捋了一遍,顺手写了一个基于自定义协议的网络版本计算器。这个东西看起来简单,但牵扯到的点一点不少:自定义协议怎么设计、TCP粘包怎么处理、表达式怎么解析、多客户端怎么并发,全都要落实。正好借这个…

2026/10/9 3:12:59 阅读更多 →

最新新闻

openGym游客模式详解:无账号使用与数据存储位置完整指南

openGym游客模式详解:无账号使用与数据存储位置完整指南

openGym游客模式详解:无账号使用与数据存储位置完整指南 【免费下载链接】openGym Self-hosted gym & body-weight tracker — plan routines, log workouts (supersets, warm-ups, cardio), see which muscles are trained, fatigued or detrained, import fro…

2026/10/9 3:46:21 阅读更多 →
NU40 DK 20分钟快速上手:nRF52840蓝牙开发环境搭建与例程实战

NU40 DK 20分钟快速上手:nRF52840蓝牙开发环境搭建与例程实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 3:46:21 阅读更多 →
可见磁粉与荧光磁粉探伤怎么选?从原理到实操讲清楚

可见磁粉与荧光磁粉探伤怎么选?从原理到实操讲清楚

干无损检测这些年,磁粉探伤是绕不开的基本功。经常有人问我:可见磁粉探伤和荧光磁粉探伤,到底该用哪个?这问题看起来简单,真要说清楚,得从原理到实操捋一遍。我尽量用大白话讲,把两种方法的底细…

2026/10/9 3:46:21 阅读更多 →
西电计网复习资料:TCP/UDP与Wireshark实战指南

西电计网复习资料:TCP/UDP与Wireshark实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 3:46:21 阅读更多 →
Android用GSON替代手写序列化:数据持久化改造完整指南

Android用GSON替代手写序列化:数据持久化改造完整指南

1. 项目概述与改造思路:把原生序列化代码替换为GSON的核心逻辑做Android开发超过一年的人,大概率都经历过这种场景:手里有一个用户信息对象,或者一张订单列表,想存到本地,于是自己封装了一套SharedPreferen…

2026/10/9 3:46:21 阅读更多 →
Claude Opus 5.5 焚诀实战:Sub-agent、CLAUDE.md 与 effort 配置指南

Claude Opus 5.5 焚诀实战:Sub-agent、CLAUDE.md 与 effort 配置指南

1. 这次“焚诀”到底更新了什么:从标题拆解到核心能力全景“Claude Opus 5.5 最新焚诀发布了”这个标题,第一次看到的时候我愣了一下——“焚诀”这个词在圈子里其实是个半开玩笑的说法,指的是那种把模型能力压榨到极限、把工作流烧到最精简的…

2026/10/9 3:45:20 阅读更多 →

日新闻

Java时间API实战:LocalDate、Date与ZonedDateTime的转换与避坑指南

Java时间API实战:LocalDate、Date与ZonedDateTime的转换与避坑指南

Java时间API这个话题,隔三差五就会在群里被翻出来讨论一次。上周还有个同事线上处理一个订单超时问题,排查到最后发现是ZonedDateTime序列化后时区丢了,用户在下单当天晚上看到的时间整整差了8个小时。这类问题几乎每个做Java开发的人都遇到过…

2026/10/9 0:00:49 阅读更多 →
EasyTier实践:从NAT穿透到子网代理的异地组网部署与排错

EasyTier实践:从NAT穿透到子网代理的异地组网部署与排错

前几个月我手头有好几台机器需要互相访问:办公室台式机、家里 NAS、还有一台云主机。如果只是偶尔传个文件倒还好,问题是工作场景经常要在几处环境之间来回切换,每次都先登录跳板机再层层代理,实在折腾。我先后试过端口映射、自建…

2026/10/9 0:00:49 阅读更多 →
AI Agent工程实战:从七要素到七个决策点的系统设计指南

AI Agent工程实战:从七要素到七个决策点的系统设计指南

AI Agent 这个词在过去一年里被反复提及,但真正动手搭过一套能跑起来的 Agent 系统的人都知道,从"知道它是什么"到"让它稳定干活"之间隔着一整套工程决策。我前后参与过几个 Agent 项目的落地,从最初用现成框架拼装&…

2026/10/9 0:01:50 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:32 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:40 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 10:10:36 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 21:13:17 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/7 13:34:55 阅读更多 →