4. WM8960 寄存器配置:I2C 通信与关键寄存器初始化

好,这一章我们正式进入 WM8960 的配置环节。说实话,很多初学者拿到芯片手册就头大——几百页的文档,上百个寄存器,到底该配哪些?

我个人习惯是:先搭好通信链路,再按「时钟→接口→电源」的顺序逐个击破。你想想看,芯片没通电、时钟没跑起来,你配啥都没用,对吧?

4.1 I2C 通信初始化

WM8960 支持 I2C 和 SPI 两种控制接口。我一般用 I2C,因为它只需要两根线,省引脚。STM32 这边我们用 HAL 库的 I2C 外设来驱动。

4.1.1 硬件连接

STM32 引脚WM8960 引脚说明
PB6 (I2C1_SCL)SCLK时钟线
PB7 (I2C1_SDA)SDIN数据线
3.3VVDD数字电源
GNDDGND数字地
注意:WM8960 的 I2C 地址是 7 位地址 0x1A(左对齐),左移一位后为 0x34(写)和 0x35(读)。我曾经见过有人直接用 0x1A 去调 HAL_I2C_Mem_Write,结果死活通信不上——因为 HAL 库需要的是 8 位地址。

4.1.2 I2C 初始化代码

我用 CubeMX 生成 I2C1 的初始化代码,然后自己封装一个写寄存器函数。嗯,这里要注意:WM8960 的寄存器是 7 位地址 + 9 位数据,总共 16 位,所以发送时要先发高 8 位,再发低 8 位。

// I2C 句柄定义
I2C_HandleTypeDef hi2c1;

// I2C 初始化(CubeMX 生成,略)
void MX_I2C1_Init(void)
{
    hi2c1.Instance = I2C1;
    hi2c1.Init.ClockSpeed = 100000;          // 100kHz 标准模式
    hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;
    hi2c1.Init.OwnAddress1 = 0;
    hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
    hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
    hi2c1.Init.OwnAddress2 = 0;
    hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
    hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;
    HAL_I2C_Init(&hi2c1);
}

// 写 WM8960 寄存器封装函数
// reg_addr: 7 位寄存器地址(0x00-0x7F)
// reg_val:  9 位寄存器数据
HAL_StatusTypeDef WM8960_WriteReg(uint8_t reg_addr, uint16_t reg_val)
{
    uint8_t buf[2];
    buf[0] = (reg_addr << 1) | ((reg_val >> 8) & 0x01);  // 高 8 位:7 位地址 + 数据 bit8
    buf[1] = reg_val & 0xFF;                                // 低 8 位:数据 bit7-bit0
    return HAL_I2C_Master_Transmit(&hi2c1, WM8960_I2C_ADDR, buf, 2, 100);
}
小技巧:我习惯把 WM8960_I2C_ADDR 定义成 0x34(写地址),这样调用 HAL_I2C_Master_Transmit 时就不用再左移了。代码更直观,不容易出错。

4.2 时钟配置——让芯片先跑起来

WM8960 内部有 PLL,可以对外部时钟进行倍频。我个人建议:如果板子上有 12.288MHz 或 24.576MHz 晶振,直接用它做 MCLK,省去 PLL 配置的麻烦。但很多开发板只给了 12MHz 晶振,那就得用 PLL 了。

4.2.1 时钟树分析

WM8960 的时钟路径是这样的:

  • MCLK:主时钟输入,可以是外部晶振或来自 STM32 的 MCO 输出
  • PLL:内部锁相环,将 MCLK 倍频到需要的频率
  • BCLK:位时钟,由 MCLK 分频得到,决定采样率
  • LRCLK:帧时钟,等于采样率(如 48kHz、44.1kHz)

举个例子:我们要实现 48kHz 采样率,16 位数据,双声道。那么 BCLK = 48k × 16 × 2 = 1.536MHz。如果 MCLK 是 12MHz,就需要配置分频器让 MCLK 分频得到 BCLK。

4.2.2 时钟寄存器配置

关键寄存器有两个:R2(0x02) 和 R3(0x03)。

// 配置时钟寄存器
// R2 (0x02): 时钟管理 1
// bit[5:4] = 00: MCLK 作为时钟源
// bit[3:2] = 00: 不使用 PLL
// bit[1:0] = 00: 无分频
WM8960_WriteReg(0x02, 0x0000);

// R3 (0x03): 时钟管理 2
// bit[6] = 0: 禁用 PLL
// bit[5:1] = 00000: 无额外分频
// bit[0] = 0: 正常模式
WM8960_WriteReg(0x03, 0x0000);
核心要点:时钟配置的核心是保证 BCLK 和 LRCLK 的比例正确。对于 16 位立体声,BCLK = 32 × LRCLK。如果比例不对,音频数据就会错位,出来的声音全是噪声。

4.3 音频接口配置——告诉芯片怎么传数据

WM8960 支持多种音频接口格式:I2S、左对齐、右对齐、DSP 模式。我们最常用的是 I2S 标准格式,也就是飞利浦格式。

4.3.1 接口格式寄存器

R4(0x04) 是音频接口控制寄存器,这里面的配置决定了数据怎么在 STM32 和 WM8960 之间传输。

// R4 (0x04): 音频接口控制
// bit[7:6] = 10: I2S 标准格式(飞利浦格式)
// bit[5:4] = 00: 16 位数据长度
// bit[3]   = 0: 左声道在 LRCLK 低电平时有效
// bit[2]   = 0: 主模式(WM8960 产生 BCLK 和 LRCLK)
// bit[1:0] = 00: 无字长调整
WM8960_WriteReg(0x04, 0x0080);

这里有个坑:我刚开始做项目时,把主从模式搞反了。STM32 作为 I2S 主机,WM8960 作为从机,结果 WM8960 的 BCLK 和 LRCLK 引脚一直没信号。后来查手册才发现,WM8960 默认是从机模式,需要把 bit[2] 置 1 才能让它输出时钟。

4.3.2 数据路径配置

接口配好了,还得告诉芯片数据往哪走。R5(0x05) 和 R6(0x06) 控制左右声道的输入输出路径。

// R5 (0x05): 左声道输入路径
// bit[8] = 1: 使能左声道 PGA
// bit[7:4] = 0000: 选择 LINPUT1 作为输入
// bit[3:0] = 0000: PGA 增益 0dB
WM8960_WriteReg(0x05, 0x0100);

// R6 (0x06): 右声道输入路径
// bit[8] = 1: 使能右声道 PGA
// bit[7:4] = 0000: 选择 RINPUT1 作为输入
// bit[3:0] = 0000: PGA 增益 0dB
WM8960_WriteReg(0x06, 0x0100);

4.4 电源管理——别让芯片饿着

WM8960 的电源管理很灵活,可以单独开关各个模块。录音模式下,我们需要开启:

  • 模拟电源(VMID)
  • 输入 PGA(可编程增益放大器)
  • ADC 模块
  • 参考电压

4.4.1 电源管理寄存器

R1(0x01) 是电源管理寄存器 1,控制模拟部分的供电。

// R1 (0x01): 电源管理 1
// bit[7] = 1: 使能 VMID(模拟电源中值)
// bit[6] = 1: 使能参考电压
// bit[5] = 1: 使能左声道输入 PGA
// bit[4] = 1: 使能右声道输入 PGA
// bit[3] = 1: 使能左声道 ADC
// bit[2] = 1: 使能右声道 ADC
// bit[1] = 0: 禁用左声道输出
// bit[0] = 0: 禁用右声道输出
WM8960_WriteReg(0x01, 0x00FC);
重要提醒:VMID 必须最先使能,而且要等它稳定后再配其他模块。我一般会在使能 VMID 后加一个 10ms 的延时。如果不这样做,ADC 上电瞬间可能会出现很大的 pop 噪声,严重时甚至会损坏麦克风。

4.4.2 上电时序

我总结了一个安全的电源上电顺序:

  1. 使能 VMID 和参考电压(R1 bit[7:6])
  2. 延时 10ms
  3. 使能输入 PGA(R1 bit[5:4])
  4. 使能 ADC 模块(R1 bit[3:2])
  5. 配置音频接口(R4)
  6. 配置时钟(R2, R3)

4.5 完整初始化流程

好了,把上面这些串起来,就是一个完整的初始化函数。我个人习惯把初始化分成两步:先配电源和时钟,再配接口和路径。

void WM8960_Init(void)
{
    // 第一步:电源管理
    // 先开 VMID 和参考电压
    WM8960_WriteReg(0x01, 0x00C0);  // 只开 VMID 和参考
    HAL_Delay(10);                   // 等待稳定
    
    // 再开 PGA 和 ADC
    WM8960_WriteReg(0x01, 0x00FC);  // 开全部录音所需模块
    
    // 第二步:时钟配置
    // 使用外部 MCLK,不分频
    WM8960_WriteReg(0x02, 0x0000);
    WM8960_WriteReg(0x03, 0x0000);
    
    // 第三步:音频接口配置
    // I2S 格式,16 位,主模式
    WM8960_WriteReg(0x04, 0x0080);
    
    // 第四步:输入路径配置
    // 选择 LINPUT1/RINPUT1,PGA 增益 0dB
    WM8960_WriteReg(0x05, 0x0100);
    WM8960_WriteReg(0x06, 0x0100);
    
    // 第五步:取消静音
    // R47 (0x2F) bit[7:6] = 0: 左/右声道 ADC 取消静音
    WM8960_WriteReg(0x2F, 0x0000);
}
调试技巧:如果初始化后没有声音,先检查 I2C 通信是否正常。我习惯在初始化后读回 R1 寄存器的值,看看是不是 0xFC。如果读回来是 0x00,说明 I2C 没通,先查硬件连接和地址。

4.6 本章知识体系

下面这张图总结了 WM8960 初始化的核心流程和寄存器依赖关系,你可以对照着看:

WM8960 初始化流程与寄存器依赖关系 WM8960 初始化流程与寄存器依赖关系 I2C 通信初始化 电源管理 (R1) 时钟配置 (R2,R3) 音频接口 (R4) 输入路径 (R5,R6) 取消静音 (R47) 关键依赖关系: • 电源管理必须在时钟配置之前完成(VMID 需要先稳定) • 音频接口配置必须在输入路径配置之前(先定协议,再定数据流向) • 取消静音必须在所有配置之后(否则可能听到上电噪声) 本章涉及的寄存器一览: R1 (0x01) - 电源管理 1:控制 VMID、参考电压、PGA、ADC 供电 R2 (0x02) - 时钟管理 1:选择时钟源、配置分频 R3 (0x03) - 时钟管理 2:PLL 控制、额外分频 R4 (0x04) - 音频接口:格式选择、数据长度、主从模式 R5/R6 (0x05/0x06) - 输入路径:输入源选择、PGA 增益 R47 (0x2F) - ADC 控制:静音控制、数字音量