第3章 I2C通信驱动:WM8960寄存器读写函数实现

WM8960这颗芯片,控制接口走的是I2C协议。说白了,你想让它干活——设置音量、切换模式、配置滤波器——都得通过I2C往它的寄存器里写数据。反过来,你想知道它当前的状态,也得通过I2C把寄存器读出来。

这一章,我们就来搞定WM8960的I2C读写驱动。我会带着你从HAL库的API封装开始,一步步写出稳定可靠的读写函数,最后再做个通信验证,确保硬件链路是通的。

本章核心目标:实现WM8960寄存器的读写函数,并通过回读验证确保I2C通信正常。

3.1 WM8960的I2C协议细节

WM8960的I2C地址是固定的——7位地址0x1A,左对齐后变成0x34(写)和0x35(读)。这个地址在芯片手册里写得很清楚,但我在项目中遇到过有人把地址搞反,折腾了半天才发现是地址错了。嗯,这里要注意:WM8960的地址引脚ADDR如果接地,就是0x1A;如果接高电平,会变成0x1B。大多数开发板都是接地,所以我们用0x1A。

WM8960的寄存器是16位宽的,但I2C一次只能传8位数据。所以读写一个寄存器需要两步:先传高8位,再传低8位。芯片内部会自动拼接成一个16位值。

项目 值
I2C 7位地址 0x1A
写地址(左对齐) 0x34
读地址(左对齐) 0x35
寄存器宽度 16位
数据传输顺序 高字节在前,低字节在后

3.2 封装HAL_I2C_Mem_Write和HAL_I2C_Mem_Read

STM32的HAL库提供了HAL_I2C_Mem_Write和HAL_I2C_Mem_Read这两个函数,专门用来操作I2C从设备的内存(或者说寄存器)。我们只需要把它们封装成WM8960专用的读写函数就行。

我个人习惯把封装放在一个单独的文件里,比如wm8960_i2c.c,这样代码结构清晰,后期维护也方便。

3.2.1 写寄存器函数

写寄存器的逻辑很简单:把寄存器地址作为内存地址,把16位数据拆成两个字节传过去。HAL库的HAL_I2C_Mem_Write会自动处理地址和数据的发送顺序。

/* wm8960_i2c.c */
#include "wm8960_i2c.h"

#define WM8960_I2C_ADDR    0x1A
#define WM8960_I2C_TIMEOUT 100

/**
  * @brief  写WM8960寄存器
  * @param  reg: 寄存器地址(0x00 ~ 0xFF)
  * @param  val: 要写入的16位值
  * @retval HAL_OK 表示成功,其他值表示失败
  */
HAL_StatusTypeDef WM8960_WriteReg(uint8_t reg, uint16_t val)
{
    uint8_t data[2];
    data[0] = (val >> 8) & 0xFF;  // 高字节
    data[1] = val & 0xFF;         // 低字节

    return HAL_I2C_Mem_Write(&hi2c1,
                             WM8960_I2C_ADDR << 1,
                             reg,
                             I2C_MEMADD_SIZE_8BIT,
                             data,
                             2,
                             WM8960_I2C_TIMEOUT);
}

小技巧:地址左移一位是因为HAL库内部要求传入8位地址(左对齐)。如果你直接传0x1A,HAL库会把它当成10位地址来处理,结果就错了。我曾经在这个坑里爬了半小时,后来翻HAL库源码才发现的。

3.2.2 读寄存器函数

读寄存器稍微复杂一点。你需要先告诉WM8960你想读哪个寄存器(通过写寄存器地址),然后再从它那里读取两个字节的数据。HAL库的HAL_I2C_Mem_Read把这两步合并成了一个函数调用,用起来很方便。

/**
  * @brief  读WM8960寄存器
  * @param  reg: 寄存器地址(0x00 ~ 0xFF)
  * @param  val: 指向存储读取值的16位变量
  * @retval HAL_OK 表示成功,其他值表示失败
  */
HAL_StatusTypeDef WM8960_ReadReg(uint8_t reg, uint16_t *val)
{
    uint8_t data[2];
    HAL_StatusTypeDef ret;

    ret = HAL_I2C_Mem_Read(&hi2c1,
                           WM8960_I2C_ADDR << 1,
                           reg,
                           I2C_MEMADD_SIZE_8BIT,
                           data,
                           2,
                           WM8960_I2C_TIMEOUT);
    if (ret == HAL_OK) {
        *val = ((uint16_t)data[0] << 8) | data[1];
    }

    return ret;
}

注意:读回来的数据顺序是高字节在前,低字节在后。拼接的时候千万别搞反了。我见过有人写成data[1] << 8 | data[0],结果读出来的值完全不对。

3.3 通信验证:确保I2C链路正常

函数写好了,怎么知道它能不能用?最简单的方法就是:写一个已知值到某个寄存器,再读回来,看看是不是一样的。

WM8960有一个寄存器特别适合做这个测试——寄存器0x00(软件复位寄存器)。往里面写0x0000,芯片会复位到默认状态。复位后,大部分寄存器的值都会变成默认值。我们可以读几个关键寄存器,跟手册里的默认值对比一下。

3.3.1 验证步骤

  1. 调用WM8960_WriteReg(0x00, 0x0000)复位芯片。
  2. 延时10毫秒,等待复位完成。
  3. 读取寄存器0x02(左声道输入音量),默认值应该是0x00DF。
  4. 读取寄存器0x03(右声道输入音量),默认值应该是0x00DF。
  5. 读取寄存器0x04(左声道输出音量),默认值应该是0x0079。
  6. 如果读回来的值跟默认值一致,说明I2C通信正常。
/* main.c 中的验证代码 */
void WM8960_VerifyI2C(void)
{
    uint16_t val;

    /* 复位WM8960 */
    WM8960_WriteReg(0x00, 0x0000);
    HAL_Delay(10);

    /* 读取寄存器0x02,预期值0x00DF */
    WM8960_ReadReg(0x02, &val);
    if (val == 0x00DF) {
        printf("寄存器0x02验证通过: 0x%04X\n", val);
    } else {
        printf("寄存器0x02验证失败: 期望0x00DF, 实际0x%04X\n", val);
    }

    /* 读取寄存器0x03,预期值0x00DF */
    WM8960_ReadReg(0x03, &val);
    if (val == 0x00DF) {
        printf("寄存器0x03验证通过: 0x%04X\n", val);
    } else {
        printf("寄存器0x03验证失败: 期望0x00DF, 实际0x%04X\n", val);
    }

    /* 读取寄存器0x04,预期值0x0079 */
    WM8960_ReadReg(0x04, &val);
    if (val == 0x0079) {
        printf("寄存器0x04验证通过: 0x%04X\n", val);
    } else {
        printf("寄存器0x04验证失败: 期望0x0079, 实际0x%04X\n", val);
    }
}

经验之谈:如果验证失败,别急着怀疑代码。先检查I2C引脚有没有接对,上拉电阻有没有焊。我遇到过好几次,代码逻辑完全正确,结果发现是SDA和SCL接反了。

3.4 常见问题与避坑指南

  • I2C地址错误:WM8960的7位地址是0x1A,但HAL库需要左对齐后的8位地址(0x34)。如果你直接传0x1A,HAL库会把它当成10位地址,通信会失败。
  • 超时时间太短:WM8960在复位后需要一段时间才能响应I2C命令。我建议超时时间至少设100毫秒,别设太短。
  • 寄存器地址范围:WM8960的寄存器地址是8位的,范围0x00~0xFF。但有些地址是保留的,读写保留地址可能会得到不确定的结果。
  • 数据字节顺序:WM8960要求高字节在前,低字节在后。这个顺序不能搞反,否则写入的值会错位。

3.5 本章知识体系

下面这张图展示了本章的核心逻辑:从HAL库的I2C API出发,封装成WM8960专用的读写函数,再通过回读验证确保通信正常。

I2C通信驱动知识体系 I2C通信驱动:WM8960寄存器读写 HAL_I2C_Mem_Write HAL_I2C_Mem_Read WM8960_WriteReg WM8960_ReadReg 回读验证 写→读→对比 关键细节 • I2C地址左对齐(0x1A→0x34) • 数据高字节在前,低字节在后 • 超时时间≥100ms 验证寄存器 • 0x02:左声道输入音量 • 0x03:右声道输入音量 • 0x04:左声道输出音量 验证通过后,I2C通信链路就确认正常了,可以继续后续的音频配置

3.6 小结

这一章我们完成了WM8960的I2C读写驱动封装。核心就三件事:搞清楚芯片的I2C协议细节,用HAL库的API封装成专用函数,再通过回读验证确保通信正常。

代码量不大,但这一步是后面所有音频功能的基础。如果I2C通信有问题,后面配置音量、切换模式、设置滤波器全都白搭。所以,我建议你在实际项目中,每次上电后都跑一遍这个验证流程,确保硬件链路是通的。