4. I2C通信协议详解:起始/停止条件、从机地址、读写操作、时钟拉伸与仲裁

I2C总线,说白了就是两根线——SCL(时钟)和SDA(数据)。在BMS项目里,AFE芯片和MCU之间基本都用它通信。我最早接触I2C时觉得挺简单,结果调试AFE寄存器时被坑了好几次。今天把这几个核心概念掰开揉碎讲清楚。

4.1 起始条件与停止条件

总线空闲时,SCL和SDA都是高电平。这是规矩,谁都不能占线。

起始条件(START):SCL为高时,SDA从高变低。
停止条件(STOP):SCL为高时,SDA从低变高。

嗯,这里要注意:这两个条件都是主设备产生的。从设备只能被动响应。

关键时序参数(我常用的AFE芯片典型值)

参数符号最小值典型值
起始条件保持时间tHD;STA4.0 µs4.7 µs
起始条件建立时间tSU;STA4.0 µs4.7 µs
停止条件建立时间tSU;STO4.0 µs4.7 µs
总线空闲时间tBUF4.7 µs5.0 µs

我曾经在项目里遇到一个问题:连续读写AFE寄存器时,停止条件后没等够tBUF就发下一个起始条件。结果AFE直接不响应,卡死在那。后来查手册才发现,有些AFE芯片对总线空闲时间要求很严,尤其是BQ系列。

4.2 从机地址与读写位

I2C地址是7位的,加上读写位(R/W)凑成第一个字节。读写位:0表示写,1表示读。

举个例子,AFE芯片地址0x08,写操作时发送0x10(0x08左移1位 + 0),读操作时发送0x11(0x08左移1位 + 1)。

我的习惯:在代码里直接用宏定义,别手算。比如:

#define AFE_ADDR        0x08
#define AFE_WRITE       ((AFE_ADDR << 1) | 0x00)
#define AFE_READ        ((AFE_ADDR << 1) | 0x01)

这样改地址时只改一处,不容易出错。

你想想看,如果AFE地址是0x10,左移后变成0x20,写操作就是0x20。但有些AFE芯片支持10位地址,BMS里很少用,我就不展开了。

4.3 读写操作流程

I2C读写分两种:单字节和多字节。AFE寄存器配置基本都用多字节,因为要连续读写电压、温度数据。

写操作流程:

  1. 主设备发START
  2. 主设备发从机地址 + 写位(0)
  3. 从设备应答ACK
  4. 主设备发寄存器地址(高字节)
  5. 从设备应答ACK
  6. 主设备发寄存器地址(低字节)
  7. 从设备应答ACK
  8. 主设备发数据字节
  9. 从设备应答ACK
  10. 重复第8-9步直到数据发完
  11. 主设备发STOP

读操作流程:

  1. 主设备发START
  2. 主设备发从机地址 + 写位(0)
  3. 从设备应答ACK
  4. 主设备发寄存器地址(高字节)
  5. 从设备应答ACK
  6. 主设备发寄存器地址(低字节)
  7. 从设备应答ACK
  8. 主设备发重复起始条件(RESTART)
  9. 主设备发从机地址 + 读位(1)
  10. 从设备应答ACK
  11. 从设备发数据字节
  12. 主设备应答ACK(继续读)或NACK(最后一字节)
  13. 重复第11-12步直到数据收完
  14. 主设备发STOP

避坑指南:我曾经在读AFE电压数据时,忘了发RESTART,直接发了STOP再发START。结果AFE认为是一次新的写操作,寄存器指针被重置了。读回来的数据全是错的。记住:读操作必须用RESTART切换方向。

4.4 时钟拉伸

时钟拉伸(Clock Stretching)是从设备的一种“慢下来”机制。从设备把SCL拉低,让主设备等它准备好。

为什么会这样?AFE芯片在做ADC转换时,内部寄存器还没更新。如果主设备这时来读,数据就是旧的。AFE会拉低SCL,告诉主设备:“等等,我还没准备好。”

我建议你在驱动代码里加上超时处理。比如:

uint8_t I2C_WaitForClockRelease(I2C_TypeDef *I2Cx, uint32_t timeout_us)
{
    uint32_t tick = 0;
    while (I2C_GetFlagStatus(I2Cx, I2C_FLAG_SCL_LOW) == SET) {
        if (tick++ > timeout_us) {
            return 1;  // 超时,返回错误
        }
        delay_us(1);
    }
    return 0;  // 时钟释放成功
}

有些AFE芯片的时钟拉伸时间长达几百微秒。我遇到过最夸张的一次,某款AFE在读取电压数据时拉伸了1.2ms。如果MCU的I2C外设不支持时钟拉伸,就得用GPIO模拟I2C。

4.5 仲裁

仲裁(Arbitration)发生在多个主设备同时尝试控制总线时。I2C是开漏结构,谁拉低谁赢。

仲裁规则很简单:谁先输出低电平,谁就赢得总线控制权。输的那个主设备会检测到SDA电平与自己输出的不一致,然后退出。

在BMS系统里,一般只有一个MCU做主设备,很少出现多主仲裁。但如果你用I2C做多机通信(比如多个AFE芯片级联),就要注意了。

我的经验:多AFE级联时,每个AFE的地址必须不同。有些AFE芯片支持通过引脚配置地址,比如ADBMS6830有3个地址引脚,可以配置8个不同地址。我习惯在原理图阶段就把地址分配好,避免软件里打架。

4.6 I2C通信流程图

下面这张图把I2C通信的完整流程画出来了。从START到STOP,每一步都标清楚了。

I2C通信完整流程图 主设备发START 发从机地址 + 写位(0) 从设备应答ACK 发寄存器地址(高+低) 判断是读还是写? 写 发数据字节 + 等待ACK 发STOP,结束通信 读 发RESTART + 地址+读位 接收数据 + 发ACK/NACK 写 操 作 流 程 读 操 作 流 程

4.7 常见问题与调试技巧

调试I2C时,我最常用的工具就是逻辑分析仪。抓一下波形,什么都清楚了。

现象可能原因解决方法
无ACK响应地址错误、从设备未上电、总线被拉死检查地址、供电、SCL/SDA电平
数据错位时钟频率不匹配、时序参数不满足降低时钟频率、增加延时
总线卡死从设备时钟拉伸超时、主设备未释放总线加超时处理、复位总线
读回数据全0xFF从设备未应答、上拉电阻失效检查上拉电阻、测量SDA电平

避坑指南:我曾经在调试一块AFE板子时,I2C总线一直卡在低电平。查了半天,发现是SDA的上拉电阻焊错了,用了10kΩ而不是4.7kΩ。换掉后一切正常。上拉电阻的选择很关键,一般4.7kΩ到10kΩ之间,具体看总线电容和通信速率。

好了,I2C通信协议的核心内容就这些。记住:起始/停止条件要严格按时序来,从机地址别写错,读写操作分清流程,时钟拉伸要加超时,仲裁在BMS里很少用但得知道。把这些搞定了,AFE芯片的驱动开发就成功了一半。