第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 验证步骤
- 调用
WM8960_WriteReg(0x00, 0x0000)复位芯片。 - 延时10毫秒,等待复位完成。
- 读取寄存器0x02(左声道输入音量),默认值应该是0x00DF。
- 读取寄存器0x03(右声道输入音量),默认值应该是0x00DF。
- 读取寄存器0x04(左声道输出音量),默认值应该是0x0079。
- 如果读回来的值跟默认值一致,说明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专用的读写函数,再通过回读验证确保通信正常。
3.6 小结
这一章我们完成了WM8960的I2C读写驱动封装。核心就三件事:搞清楚芯片的I2C协议细节,用HAL库的API封装成专用函数,再通过回读验证确保通信正常。
代码量不大,但这一步是后面所有音频功能的基础。如果I2C通信有问题,后面配置音量、切换模式、设置滤波器全都白搭。所以,我建议你在实际项目中,每次上电后都跑一遍这个验证流程,确保硬件链路是通的。