简介:这份资源面向嵌入式开发初学者与有一定基础的工程师,围绕STM32微控制器实现USB声卡的完整设计展开,帮助读者理解音频信号从采集、编解码到与PC通信的全链路原理。压缩包共153个文件,约1.31MB,以C源码与头文件为主,辅以汇编启动文件、HTML说明页及Keil工程配置,涵盖STM32外设驱动、USB设备控制、SPI通信与音频处理等模块,便于对照源码梳理工程结构。资源涉及USB音频设备类规范、ADC/DAC转换、固件开发与硬件电路设计等关键知识点,可帮助读者掌握USB通信协议、音频数据处理流程及嵌入式系统调试思路。目前已有1662人学习下载,适合希望深入理解STM32 USB应用与音频类设备开发的读者参考实践。
1. 从一块“哑巴”开发板到能被电脑认出的USB声卡
很多人第一次把 STM32 插上电脑,看到设备管理器里蹦出一个“未知 USB 设备”,心里就凉了半截。基于 STM32 的 USB 声卡设计,本质上就是让一颗通用 MCU 冒充成一个标准 USB Audio Class 设备,让 Windows、macOS、Linux 不装任何驱动就能把它当成扬声器和麦克风用。它解决的是“低成本、可定制、能自己改固件”的音频接口需求,适合做嵌入式音频采集、自定义音效处理、教学实验,或者给老设备加一个能编程的声卡。难点不在写代码,而在时钟、端点、描述符这三件事上——任何一个没对齐,电脑要么不认,要么认了但全是爆音。下面按我实际调通的顺序,把选型、描述符、I2S 对接和排错一次讲清。
2. USB Audio Class 1.0 还是 2.0:先定协议再选芯片
2.1 为什么大多数 STM32 方案停在 UAC1.0
USB Audio Class 有两个主流版本。UAC1.0 最高支持 24bit/96kHz,用全速 USB(12Mbps)就能跑,协议简单,Windows 和 macOS 原生支持,不需要额外驱动。UAC2.0 支持更高采样率和更多通道,但需要高速 USB(480Mbps),而且 Windows 10 以前对 UAC2.0 的支持并不完整,经常要装厂商驱动。
STM32 里带全速 USB 外设的型号很多,比如 F103、F411、G474、L4 系列,但带高速 USB 的只有 H7、F2/F4 的部分型号。对于声卡这种 2 进 2 出、48kHz/24bit 的场景,全速 USB 的带宽完全够用:2 通道 × 48kHz × 24bit ≈ 2.3Mbps,加上反馈端点也远低于 12Mbps。所以常见做法是选 UAC1.0 + 全速 USB,芯片选 STM32F411 或 STM32G474,主频够、RAM 够、I2S 外设齐全。
提示:如果你只是想让电脑认出并出声,不要一上来就冲 UAC2.0。UAC1.0 的描述符和端点逻辑简单得多,调通之后再考虑升级。
2.2 时钟树:USB 48MHz 和音频 MCLK 必须同源
这是第一个大坑。USB 外设需要精确的 48MHz 时钟,音频 I2S 又需要 256×fs 或 384×fs 的 MCLK。如果两个时钟来自不同的 PLL,就会有频差,表现出来就是每隔几秒“咔”一声,或者播放速度慢慢漂移。
以 STM32F411 为例,常见做法是:
- 外部晶振选 8MHz 或 25MHz;
- PLL 配置出 48MHz 给 USB;
- 同一个 PLL 的另一个输出分频给 I2S 的 PLLI2S,产生 12.288MHz(256×48kHz)。
具体到 CubeMX 里,PLLI2SN、PLLI2SR 要算准。12.288MHz = 8MHz / 8 × 192 / 2 这类组合需要自己试。如果 MCLK 有偏差,I2S 时钟就会偏,录音和播放的采样率对不上,时间一长就漂。
/* 以 STM32F411 为例,SystemClock_Config 中关键部分 */ /* 目标:SYSCLK=96MHz, USB=48MHz, PLLI2S 输出 12.288MHz */ RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE; RCC_OscInitStruct.HSEState = RCC_HSE_ON; RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; RCC_OscInitStruct.PLL.PLLM = 8; /* 8MHz / 8 = 1MHz */ RCC_OscInitStruct.PLL.PLLN = 192; /* 1MHz × 192 = 192MHz VCO */ RCC_OscInitStruct.PLL.PLLP = 2; /* 192 / 2 = 96MHz SYSCLK */ RCC_OscInitStruct.PLL.PLLQ = 4; /* 192 / 4 = 48MHz USB */ HAL_RCC_OscConfig(&RCC_OscInitStruct); /* PLLI2S 配置,输出 12.288MHz 给 I2S */ RCC_PeriphCLKInitTypeDef PeriphClkInit; PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_I2S; PeriphClkInit.PLLI2S.PLLI2SN = 192; /* VCO 192MHz */ PeriphClkInit.PLLI2S.PLLI2SR = 4; /* 192 / 4 = 48MHz? 不对,这里要算 */ /* 实际要得到 12.288MHz,需要 PLLI2SN/PLLI2SR = 12.288/1 = 12.288 */ /* 常用组合:PLLI2SN=258, PLLI2SR=3, 但 258 超出范围,需换晶振或分频 */上面这段代码里,PLLI2S 的配置需要根据实际晶振频率重新算。如果晶振是 8MHz,要得到 12.288MHz,可以先把 PLLI2S 的输入分频到 1MHz,再乘 192 除 16 之类。关键是让 MCLK 和 USB 的 48MHz 来自同一个 VCO,这样两者严格同步,不会漂。
参数说明:PLLM 决定 VCO 输入频率,PLLN 决定 VCO 频率,PLLQ 决定 USB 时钟,PLLI2SN 和 PLLI2SR 决定 I2S 时钟。任何一个算错,要么 USB 枚举失败,要么音频有杂音。
2.3 端点规划:等时端点不能重传
USB 音频用等时传输(Isochronous),特点是保证带宽但不保证可靠,没有重传。所以端点缓冲区必须足够大,且不能因为 MCU 忙不过来就丢包。
一个典型的 2 进 2 出 UAC1.0 声卡需要:
| 端点 | 方向 | 类型 | 用途 |
|---|---|---|---|
| EP1 IN | 设备到主机 | 等时 | 录音数据 |
| EP1 OUT | 主机到设备 | 等时 | 播放数据 |
| EP2 IN | 设备到主机 | 等时 | 反馈端点(可选) |
反馈端点用来告诉主机实际采样率,避免主机按固定速率发数据导致缓冲区溢出或欠载。如果 MCU 的 I2S 时钟和 USB 时钟同源,反馈端点可以省略,但很多主机仍然会要求。常见做法是加上反馈端点,用 10.14 格式发送实际采样率比值。
3. 描述符写不对,电脑永远认不出你
3.1 设备描述符、配置描述符、接口关联描述符的顺序
USB 描述符是一棵嵌套的树。主机枚举时先读设备描述符,再读配置描述符,然后根据配置里的接口数量去读接口描述符和端点描述符。音频设备还多一层:接口关联描述符(IAD)和音频控制接口。
一个 UAC1.0 声卡的描述符顺序通常是:
- 设备描述符
- 配置描述符
- 接口关联描述符(IAD)
- 音频控制接口描述符
- 音频控制接口的类特定描述符(头、输入终端、输出终端、特性单元)
- 音频流接口描述符(播放)
- 音频流接口的类特定描述符(AS 通用、格式类型)
- 标准等时音频数据端点描述符
- 音频流接口描述符(录音)
- 音频流接口的类特定描述符
- 标准等时音频数据端点描述符
任何一层描述符的长度、类型或索引写错,主机就会在枚举阶段卡住,设备管理器里显示“未知 USB 设备”或者“该设备无法启动”。
/* 音频控制接口描述符示例,注意 bInterfaceClass=0x01, bInterfaceSubClass=0x01 */ const uint8_t AudioControlInterface[] = { 0x09, /* bLength */ 0x04, /* bDescriptorType: INTERFACE */ 0x00, /* bInterfaceNumber */ 0x00, /* bAlternateSetting */ 0x00, /* bNumEndpoints: 控制接口无端点 */ 0x01, /* bInterfaceClass: AUDIO */ 0x01, /* bInterfaceSubClass: AUDIO_CONTROL */ 0x00, /* bInterfaceProtocol */ 0x00 /* iInterface */ }; /* 音频控制头描述符,bInCollection=1 表示有 1 个流接口 */ const uint8_t AudioControlHeader[] = { 0x09, /* bLength */ 0x24, /* bDescriptorType: CS_INTERFACE */ 0x01, /* bDescriptorSubtype: HEADER */ 0x00, 0x01, /* bcdADC: 1.00 */ 0x09, 0x00, /* wTotalLength: 后续描述符总长 */ 0x01, /* bInCollection */ 0x01 /* baInterfaceNr: 流接口编号为 1 */ };逻辑说明:音频控制接口不传数据,只负责告诉主机“我有几个输入终端、几个输出终端、支持什么采样率”。流接口才是真正传音频数据的地方。bInterfaceNumber 必须和后面流接口的编号对应,否则主机找不到数据通道。
参数说明:bcdADC 是音频类版本,UAC1.0 写 0x0100。wTotalLength 是从头描述符之后到控制接口结束的总字节数,算错会导致主机读越界。
3.2 用 USB 抓包工具验证枚举过程
描述符写完之后,不要靠猜。用 Wireshark 加 USB 抓包插件,或者用硬件 USB 分析仪,看主机发了哪些标准请求,设备回了什么。重点看:
- GET_DESCRIPTOR 设备描述符是否返回 18 字节;
- GET_DESCRIPTOR 配置描述符是否返回完整长度;
- SET_CONFIGURATION 是否成功;
- SET_INTERFACE 是否切到了正确的备用设置。
如果主机在 GET_DESCRIPTOR 配置描述符之后就不再发请求,通常是配置描述符里的 wTotalLength 和实际返回长度不一致。如果 SET_INTERFACE 失败,检查流接口的 bAlternateSetting 和端点描述符是否匹配。
注意:有些主机会先读一次短配置描述符(只读前 9 字节),再读完整长度。设备必须能正确处理这种分段读取,不能只返回固定长度。
3.3 采样率描述符:三个字节的坑
UAC1.0 的采样率描述符里,采样率用 3 个字节小端表示。比如 48000Hz = 0x00BB80,写成字节就是 0x80, 0xBB, 0x00。很多人习惯写 4 字节,结果主机解析成 0x00BB8000,直接不认。
/* 采样率描述符,支持 48000Hz */ const uint8_t SampleRateDesc[] = { 0x0B, /* bLength: 11 字节 */ 0x24, /* CS_INTERFACE */ 0x02, /* AS_FORMAT_TYPE */ 0x01, /* bFormatType: FORMAT_TYPE_I */ 0x02, /* bNrChannels: 2 通道 */ 0x02, /* bSubFrameSize: 每样本 2 字节 */ 0x10, /* bBitResolution: 16bit */ 0x01, /* bSamFreqType: 1 个离散采样率 */ 0x80, 0xBB, 0x00 /* 48000Hz,小端 3 字节 */ };逻辑说明:bSamFreqType=1 表示只支持一个固定采样率。如果要支持多个,改成对应数量,后面跟多组 3 字节。bSubFrameSize 是每个通道每个样本占的字节数,16bit 就是 2,24bit 就是 3。bBitResolution 是有效位数,可以和 bSubFrameSize 不同,比如 24bit 样本装在 4 字节里。
参数说明:这三个字节的顺序不能错,低字节在前。写完之后用抓包工具确认主机读到的采样率和你预期一致。
4. I2S 对接 Codec:从数据流到模拟信号
4.1 I2S 主从模式选择:STM32 做主机还是从机
I2S 有主模式和从模式。主模式下 STM32 产生 BCLK 和 LRCK,Codec 跟着走;从模式下 Codec 产生时钟,STM32 接收。对于 USB 声卡,常见做法是 STM32 做 I2S 主机,因为 USB 的采样率由主机控制,STM32 需要根据 USB 的反馈来调整 I2S 时钟。
但这里有个矛盾:USB 的 48MHz 和 I2S 的 MCLK 虽然同源,但 I2S 的分频比是固定的,不能动态微调。如果主机实际发送的采样率和 STM32 的 I2S 时钟有微小偏差,缓冲区就会慢慢溢出或欠载。解决办法有两个:一是用反馈端点让主机调整发送速率;二是用 STM32 的 I2S 从模式,让 Codec 提供时钟,STM32 只负责数据搬运。
我一般会先用主模式调通,确认数据流没问题,再根据缓冲区水位决定是否切从模式或加反馈。
/* I2S 初始化,主模式,16bit,48kHz */ hi2s2.Instance = SPI2; hi2s2.Init.Mode = I2S_MODE_MASTER_TX; hi2s2.Init.Standard = I2S_STANDARD_PHILIPS; hi2s2.Init.DataFormat = I2S_DATAFORMAT_16B; hi2s2.Init.MCLKOutput = I2S_MCLKOUTPUT_ENABLE; hi2s2.Init.AudioFreq = I2S_AUDIOFREQ_48K; hi2s2.Init.CPOL = I2S_CPOL_LOW; hi2s2.Init.ClockSource = I2S_CLOCK_PLL; HAL_I2S_Init(&hi2s2);逻辑说明:I2S_MODE_MASTER_TX 表示 STM32 产生时钟并发送数据。DataFormat 要和 Codec 匹配,常见的是 16bit 或 24bit。MCLKOutput 使能后,STM32 会输出 MCLK 给 Codec。AudioFreq 设置目标采样率,HAL 库会自动算分频。
参数说明:如果 Codec 需要 256×fs 的 MCLK,而 STM32 的 PLLI2S 输出的是 12.288MHz,那 48kHz 时正好是 256×48k。如果采样率改成 44.1kHz,MCLK 就要变成 11.2896MHz,PLLI2S 需要重新配置。这就是为什么很多声卡只支持 48kHz 一族,不支持 44.1kHz 一族——时钟树不好兼顾。
4.2 DMA 双缓冲:别让音频断流
USB 等时传输是每 1ms 一包,48kHz 16bit 2 通道时,每包 48 个样本 × 2 通道 × 2 字节 = 192 字节。STM32 要在 1ms 内把这 192 字节通过 I2S 发出去,同时还要接收录音数据。如果用轮询,CPU 根本忙不过来,必须用 DMA。
常见做法是给 I2S 的 TX 和 RX 各开一个 DMA 通道,用双缓冲模式。USB 中断里把数据从端点缓冲区搬到 DMA 缓冲区,DMA 自动发给 I2S。录音反过来,DMA 收完一缓冲,中断里再搬到 USB 端点。
/* I2S DMA 双缓冲配置 */ hdma_spi2_tx.Instance = DMA1_Stream4; hdma_spi2_tx.Init.Channel = DMA_CHANNEL_0; hdma_spi2_tx.Init.Direction = DMA_MEMORY_TO_PERIPH; hdma_spi2_tx.Init.PeriphInc = DMA_PINC_DISABLE; hdma_spi2_tx.Init.MemInc = DMA_MINC_ENABLE; hdma_spi2_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; hdma_spi2_tx.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; hdma_spi2_tx.Init.Mode = DMA_CIRCULAR; hdma_spi2_tx.Init.Priority = DMA_PRIORITY_HIGH; HAL_DMA_Init(&hdma_spi2_tx); __HAL_LINKDMA(&hi2s2, hdmatx, hdma_spi2_tx); /* 启动双缓冲传输,buffer0 和 buffer1 各 192 字节 */ HAL_I2S_Transmit_DMA(&hi2s2, (uint16_t*)audio_buffer, 96);逻辑说明:DMA_CIRCULAR 模式下,DMA 会自动循环搬运,不需要每次重新启动。双缓冲通过 HAL_I2SEx_TransmitReceive_DMA 或者手动切换缓冲区实现。关键是缓冲区大小要至少能装下 2ms 的数据,防止 USB 中断延迟导致断流。
参数说明:PeriphDataAlignment 和 MemDataAlignment 要一致,16bit 音频用 HALFWORD。如果 Codec 是 24bit,要用 WORD 对齐,但实际只发 3 字节,需要自己处理。
4.3 用 GPIO 翻转测中断延迟
调音频最怕“玄学爆音”。有时候代码看起来没问题,但就是每隔几秒响一下。这时候可以在 USB 中断入口翻转一个 GPIO,用示波器看中断间隔。正常应该是 1ms 一次,如果偶尔变成 2ms 或更长,说明有更高优先级的中断在抢,或者 DMA 传输阻塞了。
我一般会留一个测试引脚,调通之后再关掉。这个习惯帮我省了很多“后悔药”——有一次发现是 SD 卡检测中断优先级配错了,导致 USB 中断被延迟,音频就爆音。
5. 避坑与排查:那些让声卡变“哑巴”的细节
5.1 现象:设备管理器显示“未知 USB 设备”,代码 43
原因:USB 枚举失败,通常是描述符长度或类型错误,或者时钟不是精确 48MHz。
解决:先用示波器测 USB 的 DP/DM 上有没有 1.5k 上拉,再测晶振频率。如果晶振是 8MHz,检查 PLL 配置是否输出 48MHz。然后用抓包工具看主机读到哪一步失败。常见的是配置描述符的 wTotalLength 写大了,主机读不到那么多字节,超时。
5.2 现象:电脑认出声卡,但播放全是杂音或爆音
原因:I2S 数据格式和 Codec 不匹配,或者 MCLK 和 LRCK 相位不对。
解决:先确认 Codec 的寄存器配置,比如采样率、数据位宽、主从模式。然后用示波器看 BCLK、LRCK、DATA 三根线。如果 DATA 在 LRCK 跳变时也在跳,说明相位错了,改 I2S_CPOL 或 Codec 的格式设置。如果杂音是周期性的,检查 DMA 缓冲区是否溢出。
5.3 现象:录音正常,播放正常,但同时开就卡顿
原因:USB 带宽不够,或者 MCU 处理不过来。
解决:UAC1.0 全速 USB 的等时端点每帧最多 1023 字节。2 进 2 出 48kHz 16bit 时,每方向 192 字节,加起来 384 字节,带宽够。但如果加了反馈端点,或者采样率更高,就可能超。检查端点描述符里的 wMaxPacketSize 是否合理。另外,如果 MCU 主频太低,DMA 和 USB 中断处理不过来,也会卡。换更高主频的型号,或者优化中断优先级。
5.4 现象:Windows 上正常,macOS 上不认
原因:macOS 对 USB 描述符的检查更严格,尤其是音频控制接口的类特定描述符。
解决:检查音频控制头描述符里的 wTotalLength 是否包含了所有子描述符。macOS 还会检查输入终端和输出终端的通道数是否和流接口一致。如果录音是单声道,输入终端的 bNrChannels 要写 1,流接口的 bNrChannels 也要写 1,不能一个写 1 一个写 2。
5.5 现象:播放一段时间后声音变调或越来越慢
原因:USB 主机和设备之间的采样率有微小偏差,缓冲区逐渐溢出或欠载。
解决:加反馈端点,让设备告诉主机实际采样率。反馈值用 10.14 格式,即实际采样率 / 目标采样率 × 16384。如果不想加反馈,可以定期微调 I2S 的分频,但效果不如反馈端点稳定。
6. 进阶:用反馈端点把采样率偏差压到 1ppm 以内
反馈端点是 UAC1.0 里最容易被忽略、但最能提升稳定性的部分。它的作用是让设备主动告诉主机:“我实际能处理的采样率是这个值,你按这个发。”主机收到后,会微调发送速率,避免缓冲区溢出。
实现上,反馈端点是一个等时 IN 端点,每帧(或每 2 帧)发送一个 4 字节的反馈值。格式是 10.14 定点数:高 10 位是整数部分,低 14 位是小数部分。比如实际采样率是 48000.5Hz,目标 48000Hz,比值是 1.0000104,乘以 16384 约等于 16384.17,取整后发送 0x00004000 + 0x00000001 之类。
/* 计算反馈值,10.14 格式 */ uint32_t calc_feedback(uint32_t actual_freq, uint32_t target_freq) { /* 先算比值,再乘以 16384 */ uint64_t ratio = ((uint64_t)actual_freq << 14) / target_freq; /* 限制在合理范围,避免主机拒绝 */ if (ratio > 0x0000FFFF) ratio = 0x0000FFFF; if (ratio < 0x00008000) ratio = 0x00008000; return (uint32_t)ratio; } /* 在 USB 中断里更新反馈端点数据 */ void USB_ISR_Handler(void) { static uint32_t feedback; feedback = calc_feedback(actual_sample_rate, 48000); /* 把 feedback 写入 EP2 IN 缓冲区 */ USB_WriteEndpoint(EP2_IN, &feedback, 4); }逻辑说明:actual_sample_rate 可以通过测量 I2S 的 LRCK 频率得到,或者用 STM32 的定时器捕获。如果 MCLK 和 USB 同源,实际采样率就是 48000,反馈值就是 0x00004000。但晶振有误差,实际可能是 47999.8 或 48000.2,反馈值就会略有不同。
参数说明:10.14 格式的范围是 0 到 1023.9999,对应比值 0 到 1024。实际采样率不会偏离目标太多,所以反馈值通常在 0x00004000 附近。主机收到反馈后,会在下一帧调整发送的样本数,比如从 48 个调到 49 个,再调回来,平均下来就是实际采样率。
验证方法:用示波器同时测 USB 的 SOF 信号和 I2S 的 LRCK,看两者的长期稳定性。如果反馈生效,LRCK 的频率会非常稳定,不会随时间漂移。也可以用音频分析软件播放一个 1kHz 正弦波,看频谱有没有展宽。如果反馈没生效,频谱会慢慢偏移。
我自己的习惯是:每次改完描述符或时钟配置,先不接 Codec,直接用 USB 抓包看枚举和反馈端点数据。确认主机认了、反馈值在合理范围,再接模拟部分。这样能把数字问题和模拟问题分开,少走很多弯路。希望帮到你。
本文还有配套的精品资源,点击获取