第3章 I2C驱动编写:WM8960的I2C地址、寄存器读写函数实现

好,咱们进入第三章节。前两章我们把WM8960的硬件架构和引脚连接理清楚了,现在该让STM32和这颗Codec真正“说上话”了。

通信的桥梁就是I2C。WM8960的控制接口只支持I2C,音频数据走的是I2S,这个分工要记清楚。我刚开始做这个项目时,就犯过把I2C和I2S搞混的低级错误——调了两天发现寄存器写不进去,最后发现是I2C的SCL和SDA接反了。嗯,这种坑踩过一次就忘不了。

3.1 WM8960的I2C地址

WM8960的I2C从机地址是7位模式,固定为0x1A(二进制:001 1010)。但注意,HAL库的I2C地址传递方式有点绕——它需要的是左移1位后的8位地址。

为什么?因为I2C协议里,7位地址加上读写位(R/W)正好凑成一个字节。HAL库的地址参数要求你传入这个完整的字节,所以实际代码里要写成:

#define WM8960_I2C_ADDR    (0x1A << 1)   // 0x34

我个人习惯在头文件里把这个宏定义好,所有读写函数统一引用。这样万一换板子改地址,只改一处就行。

关键点:WM8960的7位地址是0x1A,HAL库传入时需要左移1位变成0x34。别搞反了。

3.2 寄存器读写函数实现

WM8960的寄存器是16位宽,地址范围0x00~0x3F。读写流程其实很标准:

  • 写寄存器:先发设备地址,再发寄存器地址(8位),最后发16位数据(高字节在前)。
  • 读寄存器:先发设备地址+写位,发寄存器地址,然后重启总线,发设备地址+读位,接收2字节数据。

说白了,就是标准的I2C内存访问模式。HAL库正好提供了HAL_I2C_Mem_Write和HAL_I2C_Mem_Read这两个封装好的函数,咱们直接拿来用就行。

3.2.1 写寄存器函数

/**
  * @brief  写WM8960寄存器
  * @param  reg: 寄存器地址 (0x00~0x3F)
  * @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,
                             reg, I2C_MEMADD_SIZE_8BIT,
                             data, 2, HAL_MAX_DELAY);
}

这里要注意I2C_MEMADD_SIZE_8BIT这个参数。WM8960的寄存器地址是8位,所以选8位地址模式。有些Codec用16位地址,那就得改成I2C_MEMADD_SIZE_16BIT。我在项目中见过有人照搬别的芯片代码,结果地址发错了,寄存器全写偏了。

3.2.2 读寄存器函数

/**
  * @brief  读WM8960寄存器
  * @param  reg: 寄存器地址
  * @retval 16位寄存器值,失败返回0xFFFF
  */
uint16_t WM8960_ReadReg(uint8_t reg)
{
    uint8_t data[2] = {0, 0};

    if (HAL_I2C_Mem_Read(&hi2c1, WM8960_I2C_ADDR,
                         reg, I2C_MEMADD_SIZE_8BIT,
                         data, 2, HAL_MAX_DELAY) != HAL_OK)
    {
        return 0xFFFF;  // 读失败返回全F
    }

    return ((uint16_t)data[0] << 8) | data[1];
}

读操作比写多一个步骤:内部其实做了“写地址+重启+读数据”的时序,但HAL库帮我们封装好了。你只管调用,不用操心底层的START、RESTART信号。

小技巧:调试阶段可以在读函数里加个打印,每次读完把值打出来。我习惯用串口输出,配合逻辑分析仪看波形,定位问题特别快。

3.3 HAL_I2C_Mem_Write/Read封装

直接调用HAL函数虽然能用,但每次都要传一堆参数,代码里到处都是hi2c1、HAL_MAX_DELAY,看着就烦。我建议做一层薄封装,把WM8960相关的I2C操作统一管理起来。

下面是我常用的封装方式:

/* wm8960_i2c.h */
#ifndef __WM8960_I2C_H
#define __WM8960_I2C_H

#include "stm32f4xx_hal.h"

/* I2C句柄声明,由外部提供 */
extern I2C_HandleTypeDef hi2c1;

/* WM8960 I2C地址(7位地址左移1位) */
#define WM8960_I2C_ADDR     (0x1A << 1)

/* 寄存器读写接口 */
HAL_StatusTypeDef WM8960_WriteReg(uint8_t reg, uint16_t val);
uint16_t          WM8960_ReadReg(uint8_t reg);

/* 批量写(用于初始化时一次写多个寄存器) */
HAL_StatusTypeDef WM8960_WriteMultiReg(const uint16_t *regval_table, uint16_t count);

#endif

批量写函数在初始化时特别有用。WM8960上电后要配置十几个寄存器,一个一个调函数太啰嗦。我习惯把寄存器配置做成一个表格,然后循环写入:

HAL_StatusTypeDef WM8960_WriteMultiReg(const uint16_t *table, uint16_t count)
{
    for (uint16_t i = 0; i < count; i++)
    {
        uint8_t  reg = (table[i] >> 8) & 0xFF;  // 高字节是寄存器地址
        uint16_t val = table[i] & 0x00FF;        // 低字节是数据(实际是9位,但简化处理)

        // 注意:这里假设表格格式为 (地址<<8) | 数据
        // 实际WM8960的寄存器地址只有7位,数据9位,需要按芯片手册调整

        if (WM8960_WriteReg(reg, val) != HAL_OK)
        {
            return HAL_ERROR;
        }
    }
    return HAL_OK;
}
注意:WM8960的寄存器数据是9位,不是8位。上面示例为了演示做了简化,实际项目中要按芯片手册的位宽处理。我曾在批量写时忽略了第9位,结果音频输出全是噪声。

3.4 通信验证

函数写好了,怎么确认I2C通信正常?别急着往下写音频配置,先做验证。我一般分三步走:

  1. 检测设备应答:用HAL_I2C_IsDeviceReady检查WM8960是否在线。
  2. 写读回环:写一个已知值到寄存器,再读回来对比。
  3. 读芯片ID:WM8960的寄存器0x00是芯片ID,复位后默认值0x0000,但可以写测试。

验证代码示例:

uint8_t WM8960_Check(void)
{
    uint16_t test_val, read_val;

    // 第一步:检测设备是否在线
    if (HAL_I2C_IsDeviceReady(&hi2c1, WM8960_I2C_ADDR, 3, 100) != HAL_OK)
    {
        printf("WM8960 I2C 设备未响应!\n");
        return 0;
    }
    printf("WM8960 I2C 设备已检测到\n");

    // 第二步:写读回环测试
    // 选择寄存器0x02(左声道输入音量),写一个测试值
    test_val = 0x00FF;  // 随便写个值
    WM8960_WriteReg(0x02, test_val);
    read_val = WM8960_ReadReg(0x02);

    if (read_val != test_val)
    {
        printf("寄存器回环测试失败:写入0x%04X,读出0x%04X\n", test_val, read_val);
        return 0;
    }
    printf("寄存器回环测试通过\n");

    return 1;
}

我曾经遇到过一种情况:I2C设备能检测到,但读写回环就是失败。最后用逻辑分析仪抓波形,发现是I2C上拉电阻没焊,信号幅值不够。所以验证时别只看软件返回值,硬件波形也要看。

3.5 本章知识体系

下面这张图把本章的核心逻辑串起来了,从I2C地址到验证流程,一目了然:

第3章 知识体系:I2C驱动编写 WM8960 I2C地址:0x1A HAL库传入:0x1A << 1 = 0x34 WM8960_WriteReg(reg, val) WM8960_ReadReg(reg) HAL_I2C_Mem_Write / Read 封装 通信验证:检测→回环→读ID 核心:地址正确 → 函数可靠 → 验证通过 → 后续音频配置才有基础

3.6 避坑总结

最后把我这些年踩过的坑列一下,你写代码时多留个心眼:

常见问题 现象 解决方法
I2C地址传错 设备无应答 确认7位地址左移1位,用逻辑分析仪抓地址帧
寄存器地址位宽选错 写进去的值不对 WM8960用8位地址,选I2C_MEMADD_SIZE_8BIT
数据位宽忽略第9位 音量控制异常 WM8960数据是9位,读写时按9位处理
I2C上拉电阻缺失 通信时好时坏 SCL/SDA各加4.7kΩ上拉到3.3V
HAL超时时间太短 偶尔通信失败 超时设长一点,HAL_MAX_DELAY或100ms