第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
我个人习惯在头文件里把这个宏定义好,所有读写函数统一引用。这样万一换板子改地址,只改一处就行。
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;
}
3.4 通信验证
函数写好了,怎么确认I2C通信正常?别急着往下写音频配置,先做验证。我一般分三步走:
- 检测设备应答:用
HAL_I2C_IsDeviceReady检查WM8960是否在线。 - 写读回环:写一个已知值到寄存器,再读回来对比。
- 读芯片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.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 |