4. I2C驱动实现:STM32硬件I2C驱动编写、WM8960读写寄存器与通信验证
好,到了真正动手写代码的时候了。前面我们搞定了硬件连接,现在要让STM32和WM8960能“说上话”。WM8960的配置全靠I2C,所以这一章的核心就是——把I2C驱动写扎实,把读写寄存器函数封装好,最后验证通信是否正常。
我个人习惯把I2C驱动分成三层:底层硬件初始化、中间层读写函数、上层寄存器操作。这样分层清晰,出了问题也好定位。
4.1 STM32硬件I2C初始化
STM32的I2C外设,说白了就是一个硬件状态机。你配好时钟、引脚、速率,它就能自动处理起始信号、地址、数据、应答这些繁琐的时序。我们只需要往数据寄存器里填数就行。
以STM32F4系列为例,我用的是I2C1,PB6-SCL、PB7-SDA。初始化代码大致如下:
void MX_I2C1_Init(void)
{
hi2c1.Instance = I2C1;
hi2c1.Init.ClockSpeed = 100000; // 100kHz标准模式
hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;
hi2c1.Init.OwnAddress1 = 0;
hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
hi2c1.Init.OwnAddress2 = 0;
hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;
HAL_I2C_Init(&hi2c1);
}
引脚配置别忘了开漏输出和上拉。STM32的I2C引脚必须配成开漏模式,外部上拉电阻已经焊在板子上了。如果你用的是内部上拉,记得使能GPIO的上拉电阻。
4.2 WM8960写寄存器函数
WM8960的I2C设备地址是0x1A(7位地址)。写寄存器时,先发设备地址+写位,再发寄存器地址(8位),最后发寄存器数据(16位)。注意数据是16位的,但I2C每次只能传8位,所以要分两次发——先高8位,再低8位。
嗯,这里要注意:WM8960的寄存器地址只有7位有效,但协议里用了一个字节来传。高位的bit7必须为0,否则芯片不认。
HAL_StatusTypeDef WM8960_WriteReg(uint8_t reg_addr, uint16_t reg_val)
{
uint8_t buf[3];
buf[0] = reg_addr & 0x7F; // 寄存器地址,只取低7位
buf[1] = (reg_val >> 8) & 0xFF; // 数据高8位
buf[2] = reg_val & 0xFF; // 数据低8位
return HAL_I2C_Master_Transmit(&hi2c1, WM8960_ADDR << 1, buf, 3, 100);
}
为什么地址要左移一位?因为HAL库的地址参数需要包含读写位。WM8960_ADDR是7位地址0x1A,左移一位变成0x34,最低位留给HAL库内部填充读写标志。
4.3 WM8960读寄存器函数
读寄存器稍微麻烦一点。需要先发一个写操作,把寄存器地址告诉WM8960,然后发一个重复起始信号,再发读操作,从芯片里读回两个字节的数据。
HAL_StatusTypeDef WM8960_ReadReg(uint8_t reg_addr, uint16_t *reg_val)
{
uint8_t buf[2];
HAL_StatusTypeDef ret;
// 第一步:发送寄存器地址
ret = HAL_I2C_Master_Transmit(&hi2c1, WM8960_ADDR << 1, ®_addr, 1, 100);
if (ret != HAL_OK) return ret;
// 第二步:读取两个字节的数据
ret = HAL_I2C_Master_Receive(&hi2c1, WM8960_ADDR << 1, buf, 2, 100);
if (ret != HAL_OK) return ret;
*reg_val = ((uint16_t)buf[0] << 8) | buf[1];
return HAL_OK;
}
读回来的数据是高字节在前,低字节在后,和写的时候顺序一致。这个顺序在WM8960的数据手册里有明确说明,但很多初学者会搞反。我建议你拿到数据后先打印出来看看,确认高低字节对不对。
4.4 通信验证方法
代码写完了,怎么知道通信是不是通的?我一般用三个步骤来验证:
- 读芯片ID寄存器——WM8960的寄存器0x00存放芯片ID和版本号,读出来应该是0x0043(具体值看芯片版本)。如果读到0xFFFF或0x0000,说明I2C没通。
- 写一个已知值再读回来——选一个可读可写的寄存器(比如0x02的电源管理寄存器),先写一个值,再读回来对比。这叫“回读验证”。
- 用示波器看波形——这是最直接的方法。看SCL和SDA上有没有完整的起始信号、地址、数据、应答。如果只有起始信号没有应答,说明地址不对或者WM8960没上电。
下面这张图展示了I2C通信验证的完整流程:
4.5 验证代码示例
下面是一段完整的验证代码,你可以直接放到main函数里跑:
void I2C_Test(void)
{
uint16_t chip_id = 0;
uint16_t test_val = 0;
uint16_t read_back = 0;
// 第一步:读芯片ID
if (WM8960_ReadReg(0x00, &chip_id) == HAL_OK)
{
printf("Chip ID: 0x%04X\r\n", chip_id);
if (chip_id == 0x0043)
{
printf("芯片ID验证通过!\r\n");
}
else
{
printf("芯片ID错误,请检查硬件!\r\n");
return;
}
}
else
{
printf("I2C通信失败!\r\n");
return;
}
// 第二步:回读验证
test_val = 0x1234;
WM8960_WriteReg(0x02, test_val);
HAL_Delay(10); // 等待写入完成
WM8960_ReadReg(0x02, &read_back);
if (read_back == test_val)
{
printf("回读验证通过!写入: 0x%04X, 读出: 0x%04X\r\n", test_val, read_back);
}
else
{
printf("回读验证失败!写入: 0x%04X, 读出: 0x%04X\r\n", test_val, read_back);
}
}
- I2C初始化:时钟100kHz,引脚开漏输出,外部上拉
- 写寄存器:先发地址,再发数据(高字节在前)
- 读寄存器:先写地址,再重复起始,读回两字节
- 验证方法:读芯片ID + 回读验证 + 示波器看波形
说实话,I2C驱动这部分看起来简单,但坑不少。我见过太多人卡在地址左移、高低字节顺序、应答超时这些小问题上。我的建议是——先把读芯片ID这一步跑通,只要ID对了,后面的配置就顺理成章了。