4、音频编解码器驱动:I2S/PCM接口协议、Codec初始化流程、时钟配置、DMA传输

好,咱们今天聊点硬核的——音频编解码器驱动。说白了,就是怎么让你的主控芯片和Codec芯片“对上话”。我刚开始做音频驱动时,总觉得这玩意儿不就是配几个寄存器吗?结果第一次调板子,喇叭里全是刺耳的噪声,折腾了两天才发现是I2S的位时钟极性配反了。嗯,这种坑,踩过一次就忘不了。

4.1 I2S与PCM接口协议

先搞清楚物理层在传什么。I2S和PCM,其实是两种最常用的数字音频串行总线。它们都至少需要三根线:位时钟(BCLK)、帧同步(LRCK/FS)、数据线(SDATA)。

核心区别一句话:I2S的帧同步信号在数据变化前一个BCLK跳变,PCM的帧同步和数据变化是同步的。说白了,I2S的LRCK比数据早半拍,PCM的FS和数据同时变。

我个人习惯用这张表来对比,清晰明了:

特性 I2S(飞利浦标准) PCM(左对齐/右对齐)
帧同步时序 LRCK在MSB前一个BCLK跳变 FS与MSB同时开始
数据对齐 MSB在LRCK跳变后第二个BCLK上升沿 MSB在FS有效后第一个BCLK上升沿
常见应用 消费级Codec(WM8960、TLV320AIC) 通信/专业音频(PCM3168、ADAU)
槽位灵活性 固定2通道 支持多槽位(TDM模式)

你想想看,为什么会有这两种标准?其实历史原因占大头。I2S是飞利浦在80年代为CD播放器定义的,PCM则更多来自电信和DSP领域。现在很多Codec都支持两种模式,通过寄存器切换。

避坑指南:我曾经在一个项目里,主控用I2S模式,Codec配成了PCM左对齐。结果数据能传,但左右声道反了,而且音量只有一半。后来用示波器一抓,发现数据位偏移了1个BCLK周期。所以,协议模式必须主从端严格一致。

4.2 Codec初始化流程

Codec上电后不是直接就能用的。我总结了一套“三板斧”流程,基本覆盖90%的Codec芯片:

  1. 硬件复位与电源稳定——拉低复位引脚至少10ms,等待各供电轨稳定。
  2. I2C/SPI控制总线初始化——配置Codec内部寄存器,包括接口格式、采样率、电源管理。
  3. 音频接口激活——使能I2S/PCM收发,设置DMA通道。

拿一颗常见的WM8960举例,初始化代码大概长这样:

// 伪代码:WM8960初始化流程
void wm8960_init(void) {
    // 1. 硬件复位
    gpio_set_low(CODEC_RST_PIN);
    delay_ms(10);
    gpio_set_high(CODEC_RST_PIN);
    delay_ms(50);

    // 2. 通过I2C写寄存器
    i2c_write(WM8960_ADDR, 0x1F, 0x0000); // 软件复位
    delay_ms(5);

    // 3. 配置音频接口
    i2c_write(WM8960_ADDR, 0x04, 0x01C0); // I2S格式,24位数据
    i2c_write(WM8960_ADDR, 0x05, 0x0000); // 采样率控制

    // 4. 电源管理
    i2c_write(WM8960_ADDR, 0x19, 0x00F0); // 使能DAC、ADC、VMID

    // 5. 设置音量
    i2c_write(WM8960_ADDR, 0x0A, 0x00F9); // 左声道音量+6dB
    i2c_write(WM8960_ADDR, 0x0B, 0x00F9); // 右声道音量+6dB
}

注意:很多Codec的寄存器是双字节的,而且地址和数据分两次发送。我见过有人把地址和数据写反了,结果Codec直接“死机”,只能断电重启。建议每次写寄存器后读回验证。

4.3 时钟配置

时钟是音频驱动的命脉。Codec内部通常有一个PLL,用来把外部晶振或主控提供的MCLK转换成内部需要的时钟。

时钟配置的核心公式:

BCLK = 采样率 × 位宽 × 通道数
LRCK = 采样率

例如:48kHz采样率,24位立体声
BCLK = 48000 × 24 × 2 = 2.304 MHz
LRCK = 48 kHz

但实际中,Codec的PLL配置要复杂得多。我一般按这个步骤来:

  • 确定MCLK频率——常见的有12.288MHz、24.576MHz、11.2896MHz等。
  • 计算PLL分频比——根据目标采样率查Codec数据手册的推荐表。
  • 配置时钟树——选择MCLK还是PLL输出作为内部时钟源。

为什么会这么麻烦?因为不同的采样率族(44.1kHz vs 48kHz)需要不同的MCLK。你想想看,如果主控只提供了一个12.288MHz晶振,要生成44.1kHz采样率,就必须靠PLL倍频/分频。我踩过最大的坑,就是没注意PLL的锁定时间——配置完PLL后立刻开始传数据,结果前几帧全是乱的。

我的习惯:配置完PLL后,至少等待5ms再启动音频流。有些Codec还提供锁定状态寄存器,轮询它直到返回“锁定”状态,更保险。

4.4 DMA传输

音频数据量大,用CPU逐字节搬数据?不现实。DMA是标配。DMA传输的核心是把内存中的音频缓冲区,自动搬运到I2S外设的发送FIFO,或者从接收FIFO搬到内存。

配置DMA时,这几个参数必须搞对:

参数 说明 常见值
数据宽度 必须与I2S数据位宽一致 16位/24位/32位
传输方向 内存到外设(播放)或外设到内存(录音) MEM2PER / PER2MEM
循环模式 音频流通常需要循环传输 使能
中断触发 半传输完成/传输完成中断 半传输中断用于双缓冲

我常用的双缓冲(ping-pong buffer)结构:

// 双缓冲DMA配置思路
#define AUDIO_BUF_SIZE  1024

int16_t ping_buf[AUDIO_BUF_SIZE];
int16_t pong_buf[AUDIO_BUF_SIZE];

// DMA配置为循环模式,交替使用ping和pong
// 半传输中断 -> 填充ping_buf
// 传输完成中断 -> 填充pong_buf

这样做的好处是,CPU可以在DMA搬运一个缓冲区时,去填充另一个缓冲区,不会出现“断流”。

我曾经遇到过一个诡异的问题:DMA传输偶尔会丢一个字节,导致整个音频流往后错位,声音像“机器人说话”。查了两天,发现是DMA的FIFO阈值和I2S的FIFO深度不匹配。I2S发送FIFO空了,DMA还没来得及填充,就产生了欠载。解决办法是调整DMA的突发传输大小,或者增大I2S的FIFO阈值。

4.5 整体驱动架构图

下面这张图,是我自己画的一个Codec驱动整体架构。它把上面讲的几个模块串起来了:

音频Codec驱动架构图 音频Codec驱动整体架构 应用层(播放/录音API) DMA管理层(双缓冲/循环传输) I2S/PCM驱动层(协议配置/时钟) Codec控制层(I2C/SPI寄存器配置) 硬件层(Codec芯片/晶振/电源) 关键配置项 • 采样率 • 位宽 • 通道数 • MCLK频率 • PLL分频比 • DMA缓冲区大小 • FIFO阈值

从这张图可以清楚看到,数据流是自上而下的:应用层发起请求,DMA负责搬运,I2S/PCM驱动层处理协议时序,Codec控制层通过I2C/SPI配置内部寄存器,最终硬件层完成数模转换。每一层都有自己需要关注的参数,任何一个环节出错,声音都会出问题。

4.6 调试与验证

驱动写完了,怎么验证?我一般按这个顺序来:

  • 第一步:抓I2S波形——用示波器看BCLK、LRCK、SDATA是否正常。重点检查LRCK频率是否等于目标采样率,BCLK频率是否等于采样率×位宽×通道数。
  • 第二步:读Codec寄存器——通过I2C/SPI回读关键寄存器,确认配置是否写入成功。
  • 第三步:发正弦波测试——在DMA缓冲区里填充1kHz正弦波数据,用耳机或喇叭听声音是否纯净。如果有杂音,大概率是时钟或DMA配置问题。
  • 第四步:录音回环测试——把ADC采集的数据直接送到DAC播放,听有没有延迟或失真。

一个小技巧:调试时可以在DMA中断里翻转一个GPIO,用示波器看中断频率是否等于预期的采样率/缓冲区大小。如果中断频率不对,说明DMA配置或时钟有问题,不用等到听声音就能发现。

好了,这一章的内容就这些。I2S/PCM协议、Codec初始化、时钟配置、DMA传输,这四个模块是音频驱动的基石。你把这些搞透了,后面调任何Codec芯片都会顺手很多。