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芯片:
- 硬件复位与电源稳定——拉低复位引脚至少10ms,等待各供电轨稳定。
- I2C/SPI控制总线初始化——配置Codec内部寄存器,包括接口格式、采样率、电源管理。
- 音频接口激活——使能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驱动整体架构。它把上面讲的几个模块串起来了:
从这张图可以清楚看到,数据流是自上而下的:应用层发起请求,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芯片都会顺手很多。