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的寄存器映射和电压配置,那才是真正调电压的地方。