3. HAL库I2C基础API:掌握核心收发函数
好,咱们进入实战环节。配置WM8960,说白了就是通过I2C往它的寄存器里写数据。你想想看,一个音频编解码器,少说几十个寄存器,每个寄存器控制着不同的功能模块——音量、采样率、输入输出路由等等。不会用I2C API,后面所有配置都无从谈起。
我个人习惯,在开始写代码之前,先把HAL库提供的I2C收发函数摸透。别急着复制粘贴,搞清楚每个参数的含义,后面调试能省一半时间。
3.1 核心API全景图
先给你一张总览图,看看HAL库为I2C主机模式提供了哪些“武器”:
嗯,这张图你应该能看懂。对于配置WM8960这种场景——说白了就是上电后一次性写几十个寄存器——我建议你用阻塞式(轮询)。为什么?因为配置阶段数据量小,频率低,用中断或DMA反而增加了代码复杂度,没必要。
3.2 最常用的两个函数
咱们先看两个最基础的:发送和接收。虽然配置WM8960主要用发送,但接收也得会,万一你要回读寄存器确认配置呢?
3.2.1 HAL_I2C_Master_Transmit
这个函数负责往从机设备发送数据。原型长这样:
HAL_StatusTypeDef HAL_I2C_Master_Transmit(
I2C_HandleTypeDef *hi2c, // I2C句柄
uint16_t DevAddress, // 从机地址(7位左对齐)
uint8_t *pData, // 要发送的数据缓冲区
uint16_t Size, // 数据长度(字节数)
uint32_t Timeout // 超时时间(毫秒)
);
参数看着多,其实就五个。我一个个说:
- hi2c:你在CubeMX里配置好的I2C句柄,比如 &hi2c1。
- DevAddress:从机地址。注意!HAL库要求7位地址左对齐。WM8960的7位地址是0x1A,左对齐后就是0x1A << 1 = 0x34。我刚开始用的时候忘了左移,折腾了半天才发现地址不对。
- pData:指向要发送数据的指针。对于WM8960,通常是一个包含寄存器地址和值的数组。
- Size:发送的字节数。
- Timeout:超时时间。配置阶段我一般设100ms,足够了。
返回值:HAL_OK 表示成功,HAL_ERROR 表示参数错误,HAL_BUSY 表示I2C总线忙,HAL_TIMEOUT 表示超时。每次调用后务必检查返回值!
3.2.2 HAL_I2C_Master_Receive
接收函数,从从机读取数据:
HAL_StatusTypeDef HAL_I2C_Master_Receive(
I2C_HandleTypeDef *hi2c,
uint16_t DevAddress,
uint8_t *pData,
uint16_t Size,
uint32_t Timeout
);
参数和发送函数完全一样,只是数据方向反了。pData指向的缓冲区用来存放接收到的数据。
我的经验:WM8960的寄存器回读需要先发送寄存器地址,再重新启动接收。这时候用HAL_I2C_Master_Transmit发送地址,再用HAL_I2C_Master_Receive读数据,中间要加一个短暂的延时(至少10μs),否则从机可能来不及准备数据。
3.3 专为寄存器操作设计的Mem系列函数
等等,上面那两个函数虽然能用,但每次都要手动处理寄存器地址,有点麻烦。HAL库贴心地提供了Mem系列函数,专门用来操作有内部地址空间的设备——WM8960就是这种设备。
| 函数名 | 功能 | 适用场景 |
|---|---|---|
| HAL_I2C_Mem_Write | 向指定内存地址写入数据 | 配置WM8960寄存器(最常用) |
| HAL_I2C_Mem_Read | 从指定内存地址读取数据 | 回读WM8960寄存器状态 |
| HAL_I2C_Mem_Write_IT | 中断版写操作 | 非阻塞场景(本课程不涉及) |
| HAL_I2C_Mem_Read_DMA | DMA版读操作 | 大数据量场景(本课程不涉及) |
看看Mem_Write的原型:
HAL_StatusTypeDef HAL_I2C_Mem_Write(
I2C_HandleTypeDef *hi2c,
uint16_t DevAddress,
uint16_t MemAddress, // 寄存器地址(WM8960为16位)
uint16_t MemAddSize, // 寄存器地址长度:I2C_MEMADD_SIZE_8BIT 或 16BIT
uint8_t *pData,
uint16_t Size,
uint32_t Timeout
);
看到没?多了一个MemAddress和MemAddSize参数。WM8960的寄存器地址是7位,但HAL库要求按16位传,所以MemAddSize用I2C_MEMADD_SIZE_8BIT。我见过有人用16BIT,结果地址发出去多了一个字节,WM8960根本不认。
注意:WM8960的寄存器地址虽然只有7位有效,但HAL库的Mem_Write函数在MemAddSize=8BIT时,会发送一个字节的地址(高1位填充0)。所以你的MemAddress直接传0~0x7F范围内的值就行,不需要左移。
3.4 实战:写一个WM8960寄存器
光说不练假把式。咱们直接写一个函数,用来配置WM8960的任意寄存器:
/**
* @brief 写WM8960寄存器
* @param reg_addr: 寄存器地址 (0x00 ~ 0x7F)
* @param reg_val: 寄存器值 (9位,但HAL库按16位传,高7位忽略)
* @retval HAL_OK: 成功,其他: 失败
*/
HAL_StatusTypeDef WM8960_WriteReg(uint8_t reg_addr, uint16_t reg_val)
{
uint8_t data[2];
// WM8960的寄存器值虽然是9位,但I2C传输时按16位组织
// 高字节在前,低字节在后
data[0] = (reg_val >> 8) & 0xFF; // 高8位
data[1] = reg_val & 0xFF; // 低8位
// 使用Mem_Write,自动处理寄存器地址
return HAL_I2C_Mem_Write(
&hi2c1, // 假设使用I2C1
WM8960_ADDR << 1, // 7位地址左移,WM8960_ADDR = 0x1A
reg_addr, // 寄存器地址
I2C_MEMADD_SIZE_8BIT, // 8位地址长度
data, // 要写入的数据
2, // 写入2个字节
100 // 超时100ms
);
}
这段代码我实际项目里一直在用,稳定得很。有几个细节你注意一下:
- WM8960_ADDR << 1:地址左移,因为HAL库要求左对齐。WM8960的7位地址是0x1A,左移后变成0x34。
- data数组:WM8960的寄存器值是9位,但I2C传输时按16位传,高字节在前。所以data[0]放高8位,data[1]放低8位。
- Size=2:每次写2个字节。WM8960的寄存器就是这么规定的,不能多也不能少。
避坑指南:我曾经犯过一个错——把data数组定义成uint16_t类型,然后直接传指针。结果HAL库按字节发送,把16位数据的高8位和低8位顺序搞反了。记住,pData参数要求的是字节数组,不是字数组。
3.5 读寄存器:验证配置是否成功
有时候你需要确认一下刚才写的值对不对。WM8960支持寄存器回读,用Mem_Read实现:
HAL_StatusTypeDef WM8960_ReadReg(uint8_t reg_addr, uint16_t *reg_val)
{
uint8_t buf[2];
HAL_StatusTypeDef status;
// 先发送寄存器地址(Mem_Read内部会处理)
status = HAL_I2C_Mem_Read(
&hi2c1,
WM8960_ADDR << 1,
reg_addr,
I2C_MEMADD_SIZE_8BIT,
buf,
2,
100
);
if (status == HAL_OK) {
// 组合成16位值,注意字节序
*reg_val = ((uint16_t)buf[0] << 8) | buf[1];
}
return status;
}
嗯,这里有个小坑。WM8960回读时,返回的也是2个字节,高字节在前。所以组合的时候要buf[0]左移8位再或上buf[1]。顺序搞反了,读出来的值就是错的。
3.6 超时时间的玄学
Timeout参数设多少合适?我一般遵循这个原则:
| 场景 | 推荐超时 | 说明 |
|---|---|---|
| 上电初始化阶段 | 100ms | WM8960刚上电可能还没准备好,给足时间 |
| 正常运行时的配置 | 50ms | 设备已稳定,超时太长会阻塞主循环 |
| 批量写多个寄存器 | 每个寄存器10ms | 连续写时,每个操作之间留点余量 |
我个人习惯在调试阶段把超时设大一点(比如500ms),等程序稳定了再改小。不然I2C总线一堵,程序卡死在HAL_I2C_Mem_Write里,你连调试器都连不上。
3.7 错误处理:别让程序死得不明不白
每次调用I2C函数后,一定要检查返回值。我见过太多人写完寄存器就不管了,结果WM8960根本没配置成功,音频出不来还找不到原因。
推荐的做法:
if (WM8960_WriteReg(0x00, 0x0000) != HAL_OK) {
// 错误处理:可以重试,或者记录错误标志
Error_Handler();
}
如果连续失败,别盲目重试。先检查硬件:
- I2C总线上拉电阻有没有焊?我遇到过没焊上拉电阻,总线一直低电平。
- WM8960的供电正常吗?特别是AVDD和DVDD。
- 地址对不对?0x1A左移后是0x34,别写成0x1A。
核心要点总结:
- 配置WM8960用HAL_I2C_Mem_Write,阻塞模式最省心
- 从机地址必须左移1位(7位转8位)
- 寄存器值按高字节在前组织,用uint8_t数组传
- 每次调用后检查返回值,别偷懒
- 超时时间根据场景灵活调整,调试阶段设大点