3. WM8960 寄存器配置:I2C 控制接口、时钟树分析、PLL 配置、音频路径设置

好,这一章我们正式进入 WM8960 的配置环节。说实话,很多朋友拿到 codec 芯片的第一反应就是「寄存器好多,怎么配?」。别急,我带你捋一遍,其实核心就四个模块:怎么跟它说话(I2C)、怎么给它喂时钟(时钟树 & PLL)、怎么走信号(音频路径)。搞明白这四块,WM8960 基本就玩转了。

3.1 I2C 控制接口:怎么跟 WM8960 打招呼

WM8960 的寄存器读写走的是 I2C 总线,地址是固定的 0x1A(7 位地址,左移一位后是 0x34 用于写操作)。我个人习惯在初始化时先发一个软复位(寄存器 0x0F 写 0x00),确保芯片处于已知状态。

关键点:WM8960 的 I2C 时序是标准的 100kHz/400kHz,STM32 的 I2C 外设完全兼容。但要注意,WM8960 的寄存器是 7 位地址 + 9 位数据,总共 16 位。发送时先发寄存器地址(高 7 位有效),再发数据(高 9 位有效,低 7 位忽略)。

下面是我常用的 I2C 写寄存器函数,直接贴出来:

// WM8960 I2C 写寄存器
// reg: 7 位寄存器地址 (0x00 - 0x7F)
// val: 9 位数据值 (0x000 - 0x1FF)
HAL_StatusTypeDef WM8960_WriteReg(uint8_t reg, uint16_t val)
{
    uint8_t buf[2];
    buf[0] = (reg << 1) | ((val >> 8) & 0x01);  // 高字节:寄存器地址 + 数据 bit8
    buf[1] = val & 0xFF;                           // 低字节:数据 bit7-bit0
    return HAL_I2C_Master_Transmit(&hi2c1, WM8960_ADDR, buf, 2, 100);
}

嗯,这里要注意:WM8960 的寄存器地址是 7 位,但数据是 9 位。所以发送的两个字节里,第一个字节的高 7 位是寄存器地址,最低位是数据的第 8 位(bit8);第二个字节是数据的低 8 位(bit7-bit0)。我第一次用的时候没仔细看 datasheet,直接把 16 位数据当两个字节发,结果读回来的数据全是乱的……后来对着逻辑分析仪才找到问题。

3.2 时钟树分析:WM8960 的「心跳」怎么搭

WM8960 的时钟系统比想象中灵活,但也容易踩坑。它的核心时钟源有三个:MCLK(主时钟)、BCLK(位时钟)、内部 PLL。我一般用 MCLK 作为主时钟源,然后通过 PLL 倍频到需要的内部时钟。

先看一张时钟树的结构图,帮你建立整体认知:

WM8960 时钟树结构图 WM8960 时钟树结构 MCLK 输入 CLK_DIV PLL 倍频 内部时钟 ADC 时钟 DAC 时钟 BCLK 生成 LRCLK 生成 说明: MCLK 输入后先经过 CLK_DIV 分频,再进入 PLL 倍频到 256×Fs 或 384×Fs。 内部时钟再分配给 ADC/DAC 采样、BCLK 和 LRCLK 生成模块。

从图里可以看到,MCLK 进来后先经过一个可编程分频器(CLK_DIV),然后可以选择直通或者进 PLL 倍频。我个人习惯用 12.288MHz 的 MCLK,配合 PLL 倍频到 256×Fs(比如 48kHz 采样率时,内部时钟 = 12.288MHz)。

3.3 PLL 配置:算好倍频比,别让时钟跑偏

WM8960 的 PLL 配置是很多人的噩梦,其实公式很简单:

PLL 输出频率 = MCLK × (R × N) / (D × 4)

其中:
- R:参考分频系数(寄存器 0x36 的 PLL_R[3:0])
- N:倍频系数(寄存器 0x36 的 PLL_N[3:0])
- D:后分频系数(寄存器 0x37 的 PLL_D[3:0])

举个例子,MCLK = 12.288MHz,目标内部时钟 = 256 × 48kHz = 12.288MHz。那 PLL 其实不需要倍频,直接旁路就行(寄存器 0x34 的 PLL_EN = 0)。但如果 MCLK 是 11.2896MHz(常见于 44.1kHz 系统),要得到 256 × 44.1kHz = 11.2896MHz,同样可以旁路。

我的经验:如果 MCLK 刚好是采样率的 256 倍或 384 倍,直接旁路 PLL,省电又稳定。只有 MCLK 跟目标频率不匹配时才启用 PLL。我曾经在一个项目里用了 13MHz 的 MCLK,硬算 PLL 参数算了半天,后来发现换个 12.288MHz 晶振就全解决了……

PLL 配置的寄存器如下:

寄存器地址 寄存器名称 位域 说明
0x34 PLL Control 1 PLL_EN (bit0) 0 = 旁路 PLL,1 = 启用 PLL
0x36 PLL Control 2 PLL_R[3:0] (bit7:4), PLL_N[3:0] (bit3:0) R 和 N 系数
0x37 PLL Control 3 PLL_D[3:0] (bit3:0) D 系数

3.4 音频路径设置:ADC/DAC 的信号怎么走

WM8960 的音频路径非常灵活,但配置不当容易产生噪声或信号串扰。我一般把路径分为两条:录音路径(ADC)和播放路径(DAC)。

3.4.1 播放路径(DAC → 耳机输出)

播放路径的典型配置是:

  1. DAC 使能:寄存器 0x05 的 DACL_EN (bit7) 和 DACR_EN (bit6) 置 1。
  2. 数字音量控制:寄存器 0x0A(左声道)和 0x0B(右声道),范围 0x00(静音)到 0xFF(最大)。
  3. 模拟混音:寄存器 0x02 的 DACL_TO_HPOUT1L (bit7) 和 DACR_TO_HPOUT1R (bit6) 置 1。
  4. 耳机输出使能:寄存器 0x04 的 HPOUT1L_EN (bit7) 和 HPOUT1R_EN (bit6) 置 1。
注意:DAC 和耳机输出之间有模拟混音器,如果混音器配置不对,可能出现「有数据但没声音」的情况。我曾经调试一个项目,I2S 数据正常,DAC 寄存器也配了,就是没声音。最后发现是混音器里 DAC 到 HPOUT 的开关没打开……嗯,这种低级错误犯过一次就不会再犯了。

3.4.2 录音路径(麦克风输入 → ADC)

录音路径的典型配置是:

  1. 麦克风偏置使能:寄存器 0x01 的 MICB_EN (bit0) 置 1。
  2. 输入 PGA 配置:寄存器 0x00 的 LINPUT1 (bit7:4) 和 RINPUT1 (bit3:0) 设置输入增益。
  3. ADC 使能:寄存器 0x05 的 ADCL_EN (bit3) 和 ADCR_EN (bit2) 置 1。
  4. 数字音量控制:寄存器 0x0C(左声道)和 0x0D(右声道)。

下面是一个完整的初始化序列示例,包含了时钟、PLL 和路径配置:

void WM8960_Init(void)
{
    // 1. 软复位
    WM8960_WriteReg(0x0F, 0x00);
    HAL_Delay(10);
    
    // 2. 时钟配置:MCLK = 12.288MHz,旁路 PLL
    WM8960_WriteReg(0x34, 0x00);  // PLL 旁路
    WM8960_WriteReg(0x35, 0x00);  // 时钟分频器默认
    
    // 3. 接口格式:I2S 16 位,主模式
    WM8960_WriteReg(0x07, 0x02);  // I2S 格式,16 位数据
    
    // 4. 播放路径使能
    WM8960_WriteReg(0x05, 0xC0);  // DACL_EN + DACR_EN
    WM8960_WriteReg(0x02, 0xC0);  // DACL_TO_HPOUT1L + DACR_TO_HPOUT1R
    WM8960_WriteReg(0x04, 0xC0);  // HPOUT1L_EN + HPOUT1R_EN
    
    // 5. 设置音量(0dB 左右)
    WM8960_WriteReg(0x0A, 0xE0);  // 左声道 DAC 音量
    WM8960_WriteReg(0x0B, 0xE0);  // 右声道 DAC 音量
    
    // 6. 解除静音
    WM8960_WriteReg(0x02, 0xC8);  // 取消 DAC 静音
}
小技巧:初始化完成后,可以用一个 1kHz 的正弦波测试信号灌入 I2S 总线,用示波器量耳机输出端的波形。如果看到正弦波,说明路径通了;如果只有直流电平,检查混音器开关和静音位。

3.5 避坑指南:我踩过的几个坑

  • I2C 地址搞错:WM8960 的 7 位地址是 0x1A,但有些库要求左移一位后的 8 位地址(0x34)。我刚开始用 HAL 库时直接传 0x1A,结果一直 NACK……后来才发现 HAL 库内部会左移一位。
  • PLL 锁定时间不够:启用 PLL 后,需要等待至少 5ms 让 PLL 锁定。我习惯在 PLL_EN 置 1 后加一个 HAL_Delay(10)。
  • 静音位没清除:WM8960 上电后默认所有输出都是静音的。寄存器 0x02 的 bit3(DAC_MUTE)和 bit2(OUTPUT_MUTE)必须清零才能听到声音。
  • 时钟极性搞反:I2S 的时钟极性(CKP)如果配反,数据会错位。我一般用标准 I2S 格式(CKP = 0),跟 STM32 的 I2S 外设默认配置一致。