第二章 嵌入式音频系统架构:音频编解码器、I2S/PCM接口、DMA传输、音频时钟管理

好,咱们进入正题。嵌入式音频系统,说白了就是让芯片能「听」和「说」的一套硬件流水线。很多刚入行的朋友以为只要把麦克风焊上去就能出声,结果一上电全是噪声——嗯,我当年也干过这事。

这一章我们拆开来看四个核心模块:编解码器怎么选、I2S/PCM怎么连、DMA怎么搬数据、时钟怎么管。搞懂这些,你的音频系统至少能跑起来不炸。

2.1 音频编解码器:芯片的「声带」和「耳朵」

编解码器(Codec)负责模数/数模转换。我习惯把它分成三类:

  • 模拟Codec:内置ADC/DAC,直接接麦克风或喇叭。比如WM8960、TLV320AIC系列。
  • 数字Codec:只有数字接口,需要外挂模拟前端。常见于高端音响。
  • SoC内置Codec:省成本,但性能一般。比如ESP32自带的I2S接口。

选型时我一般看三个参数:

参数 说明 我的经验值
信噪比(SNR) 决定底噪大小 低于85dB的芯片我基本不考虑
采样率支持 至少支持48kHz 做Hi-Fi要上192kHz
接口类型 I2S还是PCM? I2S更通用,PCM适合多通道
小提示: 我有个习惯——先看Codec的电源纹波抑制比(PSRR)。曾经有个项目,Codec指标漂亮,但一上电就有50Hz哼声,查了两天才发现是LDO纹波太大。后来换了颗PSRR 60dB以上的LDO,问题解决。

2.2 I2S/PCM接口:数字音频的「高速公路」

I2S和PCM,本质上都是串行总线。区别在哪?

  • I2S:三根线——BCLK(位时钟)、LRCLK(左右声道时钟)、SD(数据)。数据在LRCLK变化时对齐。适合立体声。
  • PCM:可以理解成I2S的变种。用帧同步信号代替LRCLK,支持多通道(比如TDM模式)。

我遇到过最坑的事:某次用STM32的I2S外设,发现LRCLK频率算错了。手册上说采样率48kHz时LRCLK应该是48kHz,但我忘了BCLK = 采样率 × 位深 × 通道数。结果BCLK设成了1.536MHz,LRCLK却配成了96kHz——声音直接变调。

关键公式:
BCLK = 采样率 × 位深 × 通道数
例如:48kHz × 16bit × 2通道 = 1.536MHz

接线时注意:I2S的SD线是双向的。发送数据用SD_OUT,接收用SD_IN。有些芯片把两根线合并成一根SDIO,这时候就要靠软件控制方向了。

2.3 DMA传输:让CPU喘口气

音频数据流是连续的。如果每个采样点都用CPU去搬,那CPU啥也别干了。DMA(直接存储器访问)就是干这个的——它自己把数据从内存搬到I2S发送寄存器,或者从I2S接收寄存器搬到内存。

配置DMA时,我一般关注三点:

  1. 传输方向:内存到外设(播放),还是外设到内存(录音)?
  2. 数据宽度:必须和I2S的位深一致。16位数据就用半字传输,32位就用字传输。
  3. 循环模式:音频流是连续的,必须开循环模式。否则DMA传完一轮就停了。
避坑指南: 我曾经在STM32F4上配DMA,忘了设置FIFO阈值。结果I2S的FIFO空了,DMA还没把数据送过来——产生下溢,喇叭里「噗噗」响。后来把DMA的FIFO阈值设成半满,问题解决。

代码示例(伪代码,以STM32 HAL库为例):

// 配置I2S发送DMA
hdma_spi_tx.Instance = DMA2_Stream3;
hdma_spi_tx.Init.Channel = DMA_CHANNEL_0;
hdma_spi_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
hdma_spi_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
hdma_spi_tx.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
hdma_spi_tx.Init.Mode = DMA_CIRCULAR;  // 循环模式,必须!
HAL_DMA_Init(&hdma_spi_tx);
__HAL_LINKDMA(&hi2s3, hdmatx, hdma_spi_tx);

2.4 音频时钟管理:一切的基础

音频对时钟抖动极其敏感。抖动大了,声音就「毛」了。我见过有人用MCU的内部RC振荡器做I2S时钟,结果高频段全是噪声——因为RC振荡器的抖动在纳秒级,而音频时钟要求皮秒级。

时钟管理我总结了三步走:

  • 第一步:选时钟源。优先用外部晶振,比如12.288MHz或24.576MHz。这两个频率能整除常见的音频采样率(48kHz、96kHz等)。
  • 第二步:配置PLL。如果MCU内部PLL够干净,也可以用。但记得看PLL的抖动指标。
  • 第三步:分频。把主时钟分频得到BCLK和LRCLK。分频系数要整数,否则会产生频率偏差。
时钟树示例:
外部晶振 12.288MHz → PLL倍频到 98.304MHz → 分频得到 BCLK 1.536MHz → 再分频得到 LRCLK 48kHz

为什么用12.288MHz?你算算:98.304MHz ÷ 64 = 1.536MHz(BCLK),1.536MHz ÷ 32 = 48kHz(LRCLK)。64是BCLK分频系数,32是LRCLK分频系数——都是整数,完美。

个人经验: 如果板子上有Wi-Fi模块,千万别让音频时钟和Wi-Fi时钟共用同一个PLL。Wi-Fi的突发传输会产生频率牵引,导致音频时钟抖动。我吃过这个亏,后来加了一颗独立的音频晶振才搞定。

最后说一句:时钟管理不是配完就完事的。上电后用示波器量一下BCLK的波形,看看上升沿干不干净。如果看到毛刺,多半是PCB走线太长或者阻抗不匹配。嗯,硬件工程师的活儿,咱们软件也得懂一点。

好了,这一章就到这。下一章我们讲音频数据流处理——怎么把PCM数据变成你想要的音效。