ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于CherryUSB的STM32 UAC复合音频设备开发实战

基于CherryUSB的STM32 UAC复合音频设备开发实战 做了这么多年嵌入式我发现很多人一到 USB 音频设备就头疼UAC 协议栈又长又绕描述符字段一错整个设备在电脑上就是“未知 USB 设备设备描述符请求失败”查半天查不出所以然。正好最近用 CherryUSB 配合 STM32 从零做了一个 UAC 麦克风加扬声器的复合音频设备Windows、Linux、手机插上就能认不用装任何驱动。这篇文章就把整套从硬件选型、描述符配置到数据通路的思路和踩坑记录完整写出来给准备做 USB 声卡、USB 麦克风或者音频回环设备的同学做个参考尤其是那些 “ 枚举成功了但没声音 ” 的疑难杂症基本都能在这里找到答案。1. 整体设计思路与方案选型1.1 为什么是 UAC而不是 HID 音频或自定义协议USB 音频设备在行业里默认走的就是 UACUSB Audio Class规范也就是系统自带的音频类驱动。选 UAC 最直接的好处是跨平台免驱Windows 10 之后的系统对 UAC1 和 UAC2 都有原生支持macOS、Linux 也自带驱动Android 从 5.0 开始也支持 UAC 设备。也就是说你插上就变成一块标准声卡在系统声音设置里能直接看到麦克风和扬声器不需要用户去装任何 exe 或者 .inf。有人会问能不能用 HID 做音频传输HID 确实能传数据但系统不会把它当作音频设备主机端要专门写一个应用拿 HID 数据再播放而且 HID 的轮询方式做实时音频会带来额外的延迟和调度抖动。还有的团队想干脆自定义 Bulk 端点协议自己写上位机这种方案做产品原型没问题但凡是需要对接第三方软件、通话 App、会议系统都不如 UAC 来得通用。我的经验是只要目标是做 “ 插上就能用的声卡/麦克风 ”UAC 就是唯一正解。1.2 UAC1 还是 UAC2怎么选UAC 规范有两个大版本对嵌入式来说最关键的差别是带宽和兼容性的权衡。UAC1 是早期规范设计跑在 Full Speed 12Mbps 下描述符结构比较简单兼容性最好Windows XP 到 Win11 通吃很多单片机老项目里也都是 UAC1。但它的端点是采样率自适应模式音频数据以每毫秒一个事务的方式同步传输实际有效带宽不高通道数和采样率受限。比如你要做 32bit/96kHz 的立体声麦克风UAC1 在 Full Speed 下就比较勉强容易出现带宽不足的问题。UAC2 是 USB-IF 在 USB 2.0 时代推出的规范本身瞄准 High Speed 480Mbps支持高采样率、多通道、低延迟还引入了数据反馈Feedback机制让同步更精确。Win10 1703 之后对 UAC2 驱动是原生支持的所以现在做新品我强烈建议直接上 UAC2。但注意如果你的 MCU 只有 Full Speed 12Mbps 的 USB 控制器硬上 UAC2 会很痛苦因为 High Speed 的带宽模型和 Full Speed 微帧机制完全不同FS 下 UAC2 能跑但只能跑低采样率的单声道。所以我的方案选择很简单主控带 High Speed 控制器比如 STM32H750 配合外部 ULPI PHY直接 UAC2只有 Full Speed 控制器比如 F4 内置 OTG FS优先 UAC1把采样率控制在 48kHz/16bit/双声道以内稳定且兼容性最好。实际做下来Jitter 和兼容性都足够好。1.3 为什么用 CherryUSB而不是 ST 官方 USB 库ST 官方 USB 库其实也能做但代码封装层太厚描述符是散落在一个巨型结构体里改一个字段要翻好几个文件。而且 ST 的库不是所有系列代码结构一致F4、H7 之间切换还得重新适应。CherryUSB 的最大优势是架构干净把 Device Controller DriverDCD、USB Device Core、Class Driver 分层剥离你有问题可以直接看到协议层的处理逻辑不用猜。另外 CherryUSB 对所有支持的 MCU 抽象度比较高class 层的代码是通用的下面换不同的 DCD 就能换芯片。我用它把 F407 和 H750 两个平台的工程跑起来上层应用基本没动。对一个产品来说这种可移植性太重要了板上改方案时不至于把软件推倒重来。2. 硬件平台搭建与开发环境准备2.1 主控选型建议做 UAC 音频设备主控核心看三方面USB 外设类型、I2S/SAI 音频接口、RAM 带宽。如果只是 Full Speed 的 UAC1 麦克风/扬声器STM32F4 系列性价比很高。F4 内部有 USB OTG FS支持 Device 模式配合 SAI 或 I2S 接口做音频采集和播放。F103 虽然也能做但 F1 的 USB 是老的 Device-only 控制器没有 OTGDMA 路径也差一些做复杂音频链路会吃力。如果想上 UAC2 高速模式推荐 STM32H750 或者 H743 搭配 USB3300 / USB3320 ULPI PHY。H7 内置 USB OTG HS可以工作在 High Speed注意它要外接 PHY 才能跑 480Mbps内置 PHY 只能跑 Full Speed这个别搞混。H7 的 SAI 接口和 DMA 带宽都强很多多路音频流同时跑也不会有压力。2.2 USB 时钟树配置48MHz 是红线STM32 的 USB 模块时钟必须精确等于 48MHz差一点都不行。F4 的 USB OTG FS 时钟从 PLL Q 输出取 48MHz系统时钟 168MHz 时 PLL_Q 设 48 很好配如果系统时钟跑 180MHz也要保证 PLL_Q48不然 USB 枚举会失败或者 CRC 错误不断。H7 系列要小心因为 H7 的主 PLL1、PLL2、PLL3 分工不同USB 时钟是从 PLL3_Q 的 48MHz 出来给 OTG FS 用的HS 模式下还要给 ULPI PHY 提供 60MHz 参考时钟。很多人第一次跑 H7 的 USB 例子发现设备连不上一查就是 PLL 配置里没给 USB 分出一路 48MHz或者 PLL3 频率不对导致 PHY 时钟不稳。建议直接进 CubeMX 的 Clock Configuration 页面打开 USB 外设确认 USB 48MHz 那一栏是绿色 48再进工程。2.3 音频硬件方案从模拟到数字的取舍麦克风输入有三种常见做法。第一种是模拟驻极体麦克风加运放放大再进 STM32 的 ADC 采样这是最容易上手的方案但注意 STM32 ADC 采样率做不到像音频 Codec 那样精确也难处理 DC 偏置底噪大。第二种是模拟麦克风接外部 I2S ADC 芯片比如 CS5340、WM8960再由 I2S 接口输入 STM32音质有明显提升适合对麦克风灵敏度要求高的应用。第三种是数字 MEMS 麦克风比如 INMP441直接通过 PDM/SAI 接口把 1bit PDM 数据送给 MCU在 MCU 内部做抽取滤波转 PCM这种方案抗干扰强、电路最简单近年很多集成 USB 麦克风产品都在用。扬声器输出更直接I2S 到外部 DAC 功放是最常规的PCM5102A 这类 DAC 芯片便宜好用如果只要基本提示音级别用 STM32 内置 DAC 或者 PWM 滤波也行但要预期好音质只是一般般别指望它播高质量音乐。2.4 开发环境与调试工具软件部分用 Keil MDK 或者 GCC 都行CherryUSB 提供 CMake 工程也可以用 GCC 编译。建议准备三样调试工具USB 分析仪、逻辑分析仪、I2S 协议分析。USB 分析仪不是必须但强烈建议枚举阶段的问题靠它一下就能定位到描述符字段逻辑分析仪用来抓 I2S/SAI 的波形确认音频数据流有没有正常搬动如果你的 USB 分析仪支持解码 UAC那基本能看清 host 发了哪些类请求。3. USB 音频核心原理与描述符设计3.1 USB 枚举流程和 UAC 的特殊之处USB 设备插上后Host 会对设备发一系列标准请求Get Descriptor、Set Address、再 Get Descriptor、Set Configuration、Set Interface。这些是标准 USB 枚举流程任何设备都必须响应。UAC 设备的特殊性在于配置描述符里嵌入了 Audio ControlAC和 Audio StreamingAS两类接口Host 枚举完标准描述符后还会发送音频类请求比如 Get/Set Current 来读采样率、音量等 Control 信息。也就是说你的设备不仅要响应标准请求还得响应类请求。主机枚举时如果发现配置描述符里音频接口的结构不对就会把这个设备判定为“不支持”或者干脆不刷新出音频设备。3.2 UAC 描述符结构核心细节以 UAC1 麦克风加扬声器复合设备为例描述符大致布局如下设备描述符bDeviceClass 设 0x00表示从接口描述符再确定设备类。bcdUSB 设为 0x0200告诉系统这是一个 USB 2.0 设备即使只有 Full Speed 也算规范。配置描述符内部包含两个独立的接口集合一个是麦克风部分Audio Control Audio Streaming一个是扬声器部分Audio Control Audio Streaming它们可以用同一个 Configuration也可以用不同的 Interface Number 分区分好。麦克风接口集合里AC 接口主要放 Input Terminal给主机一个输入端子 1、Output Terminal把音频流交给 Streaming 接口、IT 和 OT 之间有 Feature Unit音量控制或者没有都行。AS 接口里有端点描述符McMicrophone方向bEndpointAddress 的 bit7 要置 1 表示 IN 端点也就是设备往主机发数据。描述符会声明 wMaxPacketSize每帧传输字节数、bInterval、采样频率、位宽、通道数。扬声器方向刚好相反AS 接口里的端点是 OUT 端点设备从主机收数据。复合设备在配置描述符里要包含麦克风 Control/Streaming 和扬声器 Control/Streaming 四组接口定义好接口号别冲突。说实话手写这些描述符很费神容易漏字段CherryUSB 的 audio class 样例里已经给了完整的 UAC 描述符直接改采样率、通道数、端点号这些字段就能用。3.3 等时传输与带宽预算UAC 的音频数据基本走等时传输Isochronous特点是每毫秒都有固定的传输机会但不保证重传丢失一个 packet 就不会再补。对实时音频来说这是合理的因为人耳无法接受几十毫秒的补包延迟但代价是要求你的缓冲设计要平滑下溢和溢出都很难听。Full Speed 下每个毫秒帧可以传输多个事务但所有等时传输的总字节数要低于带宽上限。48kHz/16bit/单声道麦克风每秒 48000 个采样帧每帧 2 字节每秒要传 96000 字节。在 12Mbps 带宽里大约占用不到 10%看起来余量很大但实际上等时传输每一笔事务有协议开销token、CRC、包间隔而且描述符里 wMaxPacketSize 是按帧给的48kHz/16bit 单声道每帧 96 字节一个 USB 帧能塞下如果是 8 通道 24bit/96kHzFull Speed 绝对爆掉所以那种应用必须上 High Speed。3.4 采样率与同步方式UAC1 里有两种同步模式异步Asynchronous和同步Synchronous。同步模式下主机按固定的 SOF 节奏发送数据设备端跟着总线节奏走但设备本地时钟和主机的 1kHz SOF 之间可能有微小偏差音频应用里会导致周期性 buffer 溢出或欠载。异步模式下设备端通过 Feedback 端点把本地采样时钟反馈给主机主机按反馈值微调传输速率这是专业声卡常用的方式但反馈端点和 PID 控制写起来复杂。CherryUSB 的自带例程对 UAC1 用的是同步模式实际测试中只要晶振精度够20ppm 以内长时间播放不会有明显问题如果要求更严格可以把反馈端点开启用标准的 SampleRate Feedback 机制。做产品时我会先用同步模式把功能跑通再去调异步反馈避免一上来复杂度太高。4. CherryUSB 工程搭建与核心配置4.1 源码获取与工程骨架从 GitHub 拉 CherryUSB 源码建议用 release 版本而不是 master 分支master 有时候在重构期间函数名会变。目录里 bsp/ 下是一堆板卡样例直接拷贝一个同系列的板级工程最简单。不是必须从零开始建工程从我经验看先让官方 audio_mic_sample 或 audio_speaker_sample 在你的板子上跑起来再往里面加自己应用逻辑效率最高。代码里核心文件关系大概是dcd_xxx.c 是底层 USB 控制器驱动和 HAL 强相关usbh/ 是主机栈不需要管usbd/ 是设备栈核心class/ 下有 uac 相关的 class 驱动。你的应用只要把 usb_dc_init() 和 usbd_initialize() 调好然后注册好 class 的 callback 就行。4.2 描述符与宏配置修改打开 usbd_conf.h 或者 SDK 的 config 头文件能看到 USB 端点数、FIFO 大小、buffer 对齐方式等配置。做音频时注意调整好端点 buffer 大小一般至少是 wMaxPacketSize 的 2 倍因为内部要做 ping-pong buffer。缓冲区不够会出现枚举成功但传数据丢包的现象。描述符默认给的是单声道还是双声道要看模板我习惯在 uac_desc.h 里搜索 AUDIO_SAMPLING_FREQ 和 channels 相关宏全局改成 48000 和 2立体声注意要同时改麦克风方向和扬声器方向的描述符如果你做的复合设备两个方向都要改到位。4.3 双向音频的初始化流程初始化流程大致是时钟和 GPIO 先配好I2S/SAI 外设初始化并启动 DMA然后 usb_dc_init() 启动 USB 控制器usbd_initialize() 把 device 层的 class driver 装上接着设置描述符、注册控制请求回调最后使能 USB 中断进入主循环。关键区别普通控制类设备在 while 循环里不停检查标志位但 UAC 设备在枚举完成后音频数据是硬实时流不能在 while 里丢数据。你的主循环可以完全空转因为数据通路是靠中断驱动的。USB 等时传输的中断来了就触发 class 的 send/receive 回调你在回调里搬数据I2S DMA 中断到了把下一块 buffer 填给 I2S。这种中断驱动的模型是 USB 音频稳定运行的根本。5. 麦克风和扬声器数据通路的落地实现5.1 麦克风链路DMA 采集到 USB 发送麦克风方向的核心是什么把 I2S/SAI 收到的连续 PCM 数据切成固定大小的块通过 USB IN 端点发送给主机。这里最忌讳的是在中断里做太多数据搬移否则会引入延迟和抖动。我用的是三块 buffer 轮转I2S DMA 采集满一块 buffer 后产生中断DMA 自动切到下一块在中断里把刚采满的那块数据指针交给 USB class 层发送。USB 发送完成后再释放。第一块在采集、第二块在传输、第三块空闲形成流水线这样 CPU 很少参与数据搬移延迟也能控制在几个毫秒内。有一个很关键的细节I2S 采样率和 USB 帧率不匹配会导致长时间运行后 buffer 漂移。比如 I2S 实际采样率是 48048Hz而 USB 按 48000Hz 的节奏发数据积少成多最终要么 buffer 空掉要么溢出。解决思路是把环形缓冲做大比如 512 个采样帧每次漂移一点时通过插值或者重复/丢弃一个采样来校正。这个课题很经典很多声卡驱动里叫 “ Adaptive resampling 或 rate matching ”。刚开始可以先不做但要知道这一步是绕不开的坑。5.2 扬声器链路USB 接收数据到 DAC扬声器方向反过来USB OUT 端点收到主机的 PCM 数据放进 buffer然后 I2S DMA 按采样率从 buffer 读取发送给 DAC。如果 USB 数据来的速率比 I2S 消费快buffer 会溢出如果主机发送稍慢I2S 会读到空 buffer 导致爆音。解决方法同样是环形缓冲 水位管理。采到一个很实用的技巧扬声器 buffer 初始化时先填一段静音数据再启动 I2S不要一上来就空转。否则在主机启动流的瞬间I2S 可能先读到一个空样值产生“啪”的一声。软件上初始化环形缓冲全部填零启动 I2S 后先从静音开始填充等 USB 数据到达后再换成真实数据就没有爆音了。5.3 环形缓冲实现要点多生产者消费者的场景USB 中断和 DMA 中断都可能写/读同一个环形缓冲。单生产者单消费者的情况我推荐用无锁环形缓冲一个读索引一个写索引每个索引只有一方修改写入时先写数据再更新索引读取时先读索引再读数据配合内存屏障就行。如果加了锁中断里取锁很容易死锁别小看这个问题我见过好几个项目在这里翻车。实际工程里我会把读写索引都定义成 volatile 无符号整型并保证索引自增的代码不做跨指令优化简单可靠。如果产品要上 RTOS多线程访问就必须用关中断或者 mutex但中断里也拿 mutex 就麻烦了所以我的经验是UAC 数据通路尽量保持中断上下文进程上下文只做控制和状态展示。5.4 音质相关硬件细节音质问题很多时候不是软件而是电路和布局。I2S 信号线尽量短DAC 的模拟电源和数字电源分开模拟地单点连接。PCM5102 这类 DAC 的 VCC 引脚滤波电容放得离引脚越近越好我用 10uF 0.1uF 组合效果好很多。PDM 麦克风的时钟和数据线也要和 USB 走线拉开距离避免 USB 高速信号耦合到底噪里。如果发现录下来的声音有节奏性的“滋滋”声大概率是 USB 的帧同步信号干扰了模拟链路或者电源纹波没滤干净。排查时用电池给模拟部分单独供电如果噪音消失就说明数字电源串扰严重优先改进 LDO 滤波和 PCB 布局。5.5 复合设备容易踩的“描述符坑”做麦克风扬声器复合设备时最典型的坑是 AC 接口重复。一个 Audio Control 接口只能控制一个功能拓扑如果麦克风和扬声器共用一个 AC 接口却在里面声明了两个 Input Terminal 和两个 Output Terminal一些系统可以识别但很容易出现只有麦克风或者只有扬声器工作的问题。规范做法是两组接口各自有自己的 AC 和 AS互相独立。另外别忘记配置描述符的总长度会自动变化如果你修改了接口数量总长度要重新计算。CherryUSB 的 usbd_get_config_desc 会返回描述符长度打印出来检查一下如果长度不对或 Host 枚举时读到截断的数据设备会直接枚举失败。6. 常见问题与排查技巧实录6.1 枚举失败设备完全识别不到遇到插入 USB 后系统提示“未知 USB 设备”优先检查USB 时钟是否 48MHz设备是否有 1.5k 上拉电阻到 DFull Speed 设备F4 的内置上拉由 USB 控制器控制如果使用 OTG 模式要配置 IDD 引脚否则 VBUS 检测不对控制器不使能上拉Host 根本发现不了设备再就是 D / D- 的走线或者焊接问题。有逻辑分析仪的话能看到 Host 发的 Set Address 前的第一次 Get Descriptor 有没有回复。如果设备连 Device Descriptor 都没回基本是底层 DCD 驱动、时钟、上拉的问题如果回了但收到的是错误长度则要检查描述符数组有没有写错。6.2 枚举成功但系统不显示音频设备插上之后设备管理器能识别到 USB 输入设备但声音设置里就是没有“麦克风”或“扬声器”这种情况十有八九是配置描述符里的音频接口结构有问题导致操作系统不认为这是 UAC 设备。注意确认设备描述符里的 bDeviceClass 是 0x00、配置描述符里接口描述符的 bInterfaceClass 是 0x01Audio而不是被误写成了 0xFF 厂商自定义。另一种情况是使用 UAC2 时主机系统驱动没加载比如老版本 Win7 不支持 UAC2而 Linux 某些发行版默认没开 snd-usb-audio 模块。如果系统已经刷新出“USB 麦克风”图标但录音电平完全没有信号先看麦克风方向有没有数据包在发。用 USB 分析仪抓包如果 Host 发了 Set Interface 后设备没有收到音频数据发送的请求可能 AS 接口的端点地址错了比如设备应该用 IN 端点但你描述符里写成了 OUT。6.3 播放有声音但断断续续、爆音最常见原因是采样率不匹配。主机用 48kHz你的 I2S 实际工作在 48016Hz长时间下来缓冲会周期性溢出。初期排查把主机播放软件的采样率固定为 48kHz确认问题是否消失如果稳定则是时钟偏差建议做 rate matching 或者改用异步反馈。另一个常见问题是 USB 端点和 I2S DMA 的 buffer 大小不是整数倍关系导致每次切换边界时会产生半个采样帧的错位数据错位会表现为持续高频杂音同步两边 block size 为最小公倍数即可缓解。还有一个容易被忽略的等时传输在 USB 总线繁忙时会丢包USB 高速设备共享带宽时等时传输的优先级并不绝对总线拥堵时一样丢。所以产品上如果 USB 麦克风和鼠标、U 盘共存保不齐有瞬间丢包加入一定的丢失隐藏把上一包数据再放一次或者插值可以显著改善听感。6.4 回声、底噪和“嗡嗡”声多设备同时使用时麦克风采集到扬声器播出来的声音形成回声这是声学问题UAC 设备本身解决不了但可以为上层提供全双工能力由上位机做 AEC。底噪问题则要从硬件链路排查模拟麦克风的偏置电压是否干净、电源纹波、I2S 走线是否被 USB 高频信号干扰、差分输出的 DA 是否有共地噪声。如果是“嗡嗡”的 50Hz 工频声很大概率是模拟部分接地环路问题尝试把模拟地和数字地单点连接不要大面积覆铜混接。PDM 数字麦克风出现异常高频噪音则检查 PDM 时钟频率和抽取滤波参数是否匹配MEMS 麦克风校准增益是否设置过高。6.5 快速定位问题的排查顺序表我每次调 UAC 问题都按这个顺序来能省至少一半时间先确认 USB 枚举是否成功设备管理器/系统报告再用 lsusb -v 或者 USB 分析仪看描述符是否完整、类请求是否有响应再确认 Endpoint 配置是否和实际收发的方向一致接着用 printf 串口日志在 USB class 回调里打点看 USB 数据有没有进入应用层最后再上 I2S 侧逻辑分析仪确定音频源到底有没有采到数据。很多“没声音”问题最终不是 USB 侧而是 I2S 完全没有配置好 MCLK或者 DAC 芯片静音引脚没拉高这种低级错误在软件和硬件的边界上最容易发生。7. 后续扩展方向与个人经验总结做完基础的 USB 麦克风加扬声器复合设备其实整个 UAC 框架已经搭起来了后面做功能扩展比如 USB 声卡加按键静音、音量旋钮、RGB 灯效联动都是在现有拓扑往里加 HID 接口的问题HID 这部分也是 CherryUSB 里现成的。想往专业方向走可以研究异步反馈、类复合设备的多采样率切换甚至把麦克风采集到的音频经过 DSP 做噪声抑制再传给主机这些在 Cortex-M7 上都有算力余量。另外我强烈建议新手把官方的 audio demo 先烧进板子不去改任何代码确认跑通一遍枚举和录音播放。很多人上来就改描述符结果基础能力都没验证出了问题根本不知道是协议栈的问题还是自己改崩的。先跑通再增量修改每改一点都插电脑验证这是做 USB 设备最稳妥的节奏。就我自己的实操体验来说UAC 音频设备 80% 的价值都在描述符配置和数据流稳定的细节上而 CherryUSB 很好地解决了前者的复杂度让我可以把精力集中在后者。如果你也在做相关方案建议手头始终留一个 USB 分析仪很多玄学问题最后都能在总线层面找到真实原因。做完这个项目再回头看从零搭一个免驱的 USB 音频设备真的没有想象中那么神秘关键在于按规范把每一层打通——总线协议交给协议栈应用逻辑自己掌控做一个好用的 USB 声卡就是水到渠成的事。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进