4. PMIC通信接口(I2C):I2C协议基础、PMIC从机地址、寄存器读写时序、多主设备仲裁
好,咱们进入PMIC驱动开发中最核心的一环——I2C通信。说实话,搞电源管理芯片,你绕不开I2C。为什么?因为PMIC不像普通的LDO,拧一下电位器就完事了。它内部有几十个寄存器,电压、电流、时序、保护阈值,全得靠I2C来配置。
我刚开始做PMIC驱动时,觉得I2C嘛,两根线,简单。结果第一次调板子,读回来的寄存器值全是0xFF,折腾了两天才发现是从机地址搞错了。嗯,这里面的坑,我一个个给你讲清楚。
4.1 I2C协议基础
I2C总线就两根线:SCL(时钟)和SDA(数据)。但你别小看这两根线,它背后有一套完整的协议。
说白了,I2C就是个主从通信模型。主设备发起通信,从设备响应。PMIC永远是从设备,MCU是主设备。当然,如果系统里有多个MCU,那就涉及多主仲裁,这个后面说。
几个关键信号你得记住:
- 起始条件:SCL高电平时,SDA从高变低。表示“我要开始通信了”。
- 停止条件:SCL高电平时,SDA从低变高。表示“通信结束”。
- 应答位(ACK):每发送完一个字节,接收方拉低SDA表示“收到了”。
- 非应答位(NACK):接收方不拉低SDA,表示“没收到”或“不想收”。
我个人的习惯:调试I2C时,先用逻辑分析仪抓一下起始条件和停止条件。很多问题都是起始条件没发对,导致PMIC根本不响应。
数据传输格式是这样的:
起始条件 | 从机地址(7位) + 读写位(1位) | ACK | 数据字节(8位) | ACK | ... | 停止条件
你想想看,每次传输都是以起始条件开始,以停止条件结束。中间可以连续发多个字节,但每个字节后面都要跟一个ACK。
4.2 PMIC从机地址
每个PMIC都有一个固定的从机地址。这个地址通常是7位的,比如0x6C、0x48之类的。但注意,有些PMIC的地址是可配置的,通过外部引脚的电平来决定。
我记得有一次,客户拿来的PMIC数据手册上写的是0x6C,但我怎么读写都没反应。后来仔细看手册,发现有个ADDR引脚,拉高是0x6C,拉低是0x6E。结果客户的板子上这个引脚悬空了,读回来是0x6E。你说坑不坑?
| PMIC型号 | 默认从机地址(7位) | 地址配置方式 |
|---|---|---|
| TPS65218 | 0x6C | 固定 |
| AXP209 | 0x34 | 固定 |
| MAX77620 | 0x6C / 0x6E | ADDR引脚 |
| SY8089 | 0x48 | 固定 |
避坑指南:我曾经因为没注意地址的移位问题,浪费了一整天。I2C的7位地址在发送时,要左移1位,最低位是读写位。比如地址0x6C,实际发送的是0xD8(写)或0xD9(读)。很多新手直接拿0x6C去发,PMIC当然不理你。
4.3 寄存器读写时序
PMIC的寄存器读写,本质上就是I2C的扩展应用。标准做法是:先写寄存器地址,再读写数据。
写寄存器时序:
起始条件 | 从机地址(写) | ACK | 寄存器地址 | ACK | 数据字节 | ACK | 停止条件
读寄存器时序:
起始条件 | 从机地址(写) | ACK | 寄存器地址 | ACK
起始条件 | 从机地址(读) | ACK | 数据字节 | NACK | 停止条件
注意看,读操作比写操作多了一个“重复起始条件”。为什么要这样?因为你要先告诉PMIC“我要读哪个寄存器”,然后再切换成读模式去拿数据。
我刚开始写驱动时,读操作总是返回0。后来发现,我忘了发那个重复起始条件,直接发了停止条件再发起始条件。虽然也能工作,但效率低,而且有些PMIC不认这种方式。
关键点:读操作的最后,主设备要发NACK,表示“我读完了,你别再发了”。然后发停止条件。如果你发了ACK,PMIC会继续发下一个字节,导致总线混乱。
实际代码实现(以Linux内核I2C子系统为例):
// 写PMIC寄存器
int pmic_reg_write(struct i2c_client *client, u8 reg, u8 val)
{
u8 buf[2] = {reg, val};
struct i2c_msg msg = {
.addr = client->addr,
.flags = 0, // 写操作
.len = 2,
.buf = buf,
};
return i2c_transfer(client->adapter, &msg, 1);
}
// 读PMIC寄存器
int pmic_reg_read(struct i2c_client *client, u8 reg)
{
u8 val;
struct i2c_msg msgs[2] = {
{
.addr = client->addr,
.flags = 0, // 写寄存器地址
.len = 1,
.buf = ®,
},
{
.addr = client->addr,
.flags = I2C_M_RD, // 读数据
.len = 1,
.buf = &val,
},
};
if (i2c_transfer(client->adapter, msgs, 2) != 2)
return -EIO;
return val;
}
4.4 多主设备仲裁
多主设备仲裁,说白了就是多个主设备抢总线。比如系统里有两个MCU,都想通过I2C去配置PMIC,怎么办?
I2C协议本身支持多主仲裁,原理很简单:谁先拉低SDA,谁就赢得总线。因为SDA是开漏输出,多个设备同时拉低时,总线电平就是低。如果一个设备想发高电平,但检测到总线是低电平,它就知道了——有人跟我抢,我退出。
但说实话,PMIC场景下,多主仲裁用得不多。我遇到过的真实案例是:一个MCU负责系统管理,另一个DSP负责音视频处理,两个都想调PMIC的电压。结果两个同时写寄存器,数据就乱了。
警告:多主仲裁虽然协议上支持,但很多PMIC的I2C接口设计得并不完善。我曾经遇到过,两个主设备同时写同一个寄存器,PMIC直接死锁,必须断电重启。所以,我的建议是:尽量用单一主设备控制PMIC,或者加一个I2C总线开关(如PCA9548)来隔离。
如果你非要搞多主,记住几点:
- 每个主设备在发送前,必须检测总线是否空闲(SCL和SDA都是高电平)。
- 发送过程中,要实时监控SDA电平。如果发现跟自己想发的不一致,立即释放总线。
- 仲裁失败的设备,要重新尝试发送,不能一直占着总线不放。
嗯,多主仲裁这块,我建议你直接用硬件I2C控制器,别自己用GPIO模拟。硬件控制器内部有仲裁逻辑,你只需要配置好寄存器就行。用GPIO模拟的话,仲裁逻辑全得自己写,很容易出bug。
好了,I2C通信这块就讲这么多。下一章咱们聊聊PMIC的寄存器映射和电压配置,那才是真正调电压的地方。