4. Codec架构概览
好,咱们进入第四章节。前面几章我们把音频Codec的硬件架构、电源管理聊了一遍。但有个关键问题:芯片怎么知道你要它做什么?采样率设多少?音量多大?要不要开启麦克风偏置?
答案就是——I2C。这个只有两根线的串行总线,承担了Codec所有的控制任务。我个人习惯把I2C比作Codec的「神经末梢」,音频数据流走的是I2S或TDM,但配置指令全靠I2C传递。
4.1 I2C协议速览:两根线搞定一切
I2C协议其实不复杂。SCL(时钟线)和SDA(数据线),就这么两根。主设备(通常是MCU或DSP)控制时钟,从设备(Codec)响应地址。
我刚开始做嵌入式音频时,总觉得I2C时序很玄乎。后来调了一次逻辑分析仪,发现核心就几个状态:
- 起始条件:SCL高电平时,SDA从高拉低
- 停止条件:SCL高电平时,SDA从低拉高
- 数据传输:SCL低电平时改变SDA,高电平时采样
- 应答位(ACK):每8位数据后,接收方拉低SDA表示收到
核心要点:I2C地址通常是7位,加上读写位组成8位。比如Codec的7位地址是0x1A,那么写操作就是0x1A << 1 | 0 = 0x34,读操作是0x1A << 1 | 1 = 0x35。
嗯,这里要注意:不同Codec的I2C地址可能通过引脚配置。我遇到过一块板子,两个Codec共用同一条I2C总线,结果地址冲突,一个都读不到。后来发现是ADDR引脚电平没拉对。
4.2 Codec寄存器读写:标准操作流程
Codec的寄存器读写,说白了就是I2C协议的一个具体应用。大多数音频Codec采用「寄存器地址 + 数据」的格式。
写操作流程:
- 主设备发送起始条件
- 发送7位设备地址 + 写位(0)
- 等待从设备ACK
- 发送寄存器地址(8位或16位,取决于Codec)
- 等待从设备ACK
- 发送要写入的数据(8位)
- 等待从设备ACK
- 发送停止条件
读操作流程:
- 主设备发送起始条件
- 发送7位设备地址 + 写位(0)——先写寄存器地址
- 发送寄存器地址
- 等待从设备ACK
- 主设备发送重复起始条件
- 发送7位设备地址 + 读位(1)
- 等待从设备ACK
- 读取数据(主设备发送NACK表示结束)
- 发送停止条件
个人经验:我建议在调试阶段,每次读写操作后都加一个延时(比如1ms)。有些Codec内部状态机切换需要时间,连续读写太快会导致数据错位。量产时再优化时序。
4.3 初始化流程:让Codec从沉睡中醒来
Codec上电后不会自动工作。你得按顺序给它发指令。我总结了一套通用的初始化流程:
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 硬件复位 | 拉低RST引脚至少10ms,或发送软件复位命令 |
| 2 | 等待稳定 | 复位后等待50ms,让内部PLL和参考电压稳定 |
| 3 | 读取芯片ID | 验证I2C通信正常,确认Codec型号 |
| 4 | 配置电源管理 | 开启需要的电源域(模拟、数字、PLL等) |
| 5 | 配置时钟 | 设置采样率、MCLK分频、PLL参数 |
| 6 | 配置音频接口 | 设置I2S/TDM格式、位宽、主从模式 |
| 7 | 配置音频路径 | 选择ADC/DAC、混音器、增益 |
| 8 | 解除静音 | 最后一步,否则会有POP声 |
避坑指南:我曾经在初始化时先解除了静音再配置音频路径,结果喇叭里传出一声巨响。原因是路径上的增益还没设置,默认值可能很大。记住:先配路径,再开声音。
4.4 实战:驱动一颗Codec(以WM8960为例)
光说不练假把式。我们拿一颗常见的Codec——WM8960来实战。这颗芯片在便携式音频设备里很常见,双声道DAC/ADC,内置PLL,支持I2C控制。
先看它的I2C地址:WM8960的7位地址是0x1A(ADDR引脚拉低时)。如果ADDR拉高,地址变成0x1B。
下面是一段典型的初始化代码(基于STM32 HAL库):
// WM8960 寄存器配置表
// 格式:{寄存器地址, 数据}
const uint8_t wm8960_config[][2] = {
// 软件复位
{0x0F, 0x00}, // 软件复位寄存器
// 电源管理
{0x19, 0x00}, // 关闭所有电源域,先配置再开启
{0x1A, 0x00}, // 关闭所有输出
// 时钟配置:假设MCLK=12.288MHz,采样率48kHz
{0x04, 0x80}, // 采样率控制:48kHz
{0x05, 0x80}, // 时钟分频:MCLK直接使用
// 音频接口配置:I2S,24位,从模式
{0x07, 0x02}, // I2S格式,左对齐,24位
// 音频路径配置
{0x0A, 0x00}, // 左声道DAC路径
{0x0B, 0x00}, // 右声道DAC路径
{0x0C, 0x00}, // 左声道ADC路径
{0x0D, 0x00}, // 右声道ADC路径
// 音量设置:先设到最小
{0x02, 0x79}, // 左声道DAC音量:0dB
{0x03, 0x79}, // 右声道DAC音量:0dB
// 开启电源域
{0x19, 0x7F}, // 开启所有电源域
{0x1A, 0x7F}, // 开启所有输出
// 解除静音
{0x02, 0x79}, // 左声道解除静音
{0x03, 0x79}, // 右声道解除静音
};
// I2C写寄存器函数
HAL_StatusTypeDef wm8960_write_reg(uint8_t reg, uint8_t data) {
uint8_t buf[2] = {reg, data};
return HAL_I2C_Master_Transmit(&hi2c1, WM8960_ADDR << 1, buf, 2, 100);
}
// 初始化函数
void wm8960_init(void) {
// 硬件复位:拉低RST引脚
HAL_GPIO_WritePin(CODEC_RST_GPIO_Port, CODEC_RST_Pin, GPIO_PIN_RESET);
HAL_Delay(10);
HAL_GPIO_WritePin(CODEC_RST_GPIO_Port, CODEC_RST_Pin, GPIO_PIN_SET);
HAL_Delay(50);
// 读取芯片ID,验证通信
uint8_t id = 0;
wm8960_read_reg(0x00, &id); // 读取ID寄存器
if (id != 0x60) { // WM8960的ID值
// 错误处理:芯片未响应
Error_Handler();
}
// 逐条写入配置
for (int i = 0; i < sizeof(wm8960_config) / sizeof(wm8960_config[0]); i++) {
wm8960_write_reg(wm8960_config[i][0], wm8960_config[i][1]);
HAL_Delay(1); // 每步延时1ms,确保稳定
}
}
调试技巧:我习惯在初始化代码里加一个「回读验证」函数。每写完一个寄存器,马上读回来对比。如果数据不一致,说明I2C时序有问题或者Codec没准备好。这个习惯帮我抓出过好几次硬件虚焊的问题。
4.5 I2C时序的常见坑
做I2C驱动,有几个坑我踩过不止一次:
- 速率不匹配:标准模式100kHz,快速模式400kHz。有些Codec不支持400kHz,强行用会导致数据错位。我建议初期先用100kHz,稳定后再提频。
- 上拉电阻:I2C总线需要上拉电阻,通常4.7kΩ。电阻太大,上升沿变慢;电阻太小,功耗增加。我一般先按4.7kΩ来,如果总线电容大(比如走线长),换成2.2kΩ。
- 时钟延展:有些Codec在内部处理时会拉低SCL,要求主设备等待。如果MCU的I2C外设不支持时钟延展,通信会卡死。解决办法是改用GPIO模拟I2C。
- 重复起始条件:读操作时,从写地址切换到读地址,必须用重复起始条件,不能先发停止再发起始。否则有些Codec会认为操作结束,释放总线。
我曾经在一个项目里,I2C通信偶尔失败,查了两天才发现是SDA和SCL接反了。PCB布局时丝印标错,软件怎么调都没用。所以拿到新板子,第一件事用万用表量一下I2C引脚通不通。
4.6 本章知识体系
下面这张图把I2C控制Codec的核心流程串起来了:
嗯,这张图把I2C控制Codec的层次和初始化流程都串起来了。你写驱动时,就按这个顺序来,基本不会出大问题。