4. I2C通信协议详解:起始/停止条件、从机地址、读写操作、时钟拉伸与仲裁
I2C总线,说白了就是两根线——SCL(时钟)和SDA(数据)。在BMS项目里,AFE芯片和MCU之间基本都用它通信。我最早接触I2C时觉得挺简单,结果调试AFE寄存器时被坑了好几次。今天把这几个核心概念掰开揉碎讲清楚。
4.1 起始条件与停止条件
总线空闲时,SCL和SDA都是高电平。这是规矩,谁都不能占线。
起始条件(START):SCL为高时,SDA从高变低。
停止条件(STOP):SCL为高时,SDA从低变高。
嗯,这里要注意:这两个条件都是主设备产生的。从设备只能被动响应。
关键时序参数(我常用的AFE芯片典型值)
| 参数 | 符号 | 最小值 | 典型值 |
|---|---|---|---|
| 起始条件保持时间 | tHD;STA | 4.0 µs | 4.7 µs |
| 起始条件建立时间 | tSU;STA | 4.0 µs | 4.7 µs |
| 停止条件建立时间 | tSU;STO | 4.0 µs | 4.7 µs |
| 总线空闲时间 | tBUF | 4.7 µs | 5.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寄存器配置基本都用多字节,因为要连续读写电压、温度数据。
写操作流程:
- 主设备发START
- 主设备发从机地址 + 写位(0)
- 从设备应答ACK
- 主设备发寄存器地址(高字节)
- 从设备应答ACK
- 主设备发寄存器地址(低字节)
- 从设备应答ACK
- 主设备发数据字节
- 从设备应答ACK
- 重复第8-9步直到数据发完
- 主设备发STOP
读操作流程:
- 主设备发START
- 主设备发从机地址 + 写位(0)
- 从设备应答ACK
- 主设备发寄存器地址(高字节)
- 从设备应答ACK
- 主设备发寄存器地址(低字节)
- 从设备应答ACK
- 主设备发重复起始条件(RESTART)
- 主设备发从机地址 + 读位(1)
- 从设备应答ACK
- 从设备发数据字节
- 主设备应答ACK(继续读)或NACK(最后一字节)
- 重复第11-12步直到数据收完
- 主设备发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,每一步都标清楚了。
4.7 常见问题与调试技巧
调试I2C时,我最常用的工具就是逻辑分析仪。抓一下波形,什么都清楚了。
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 无ACK响应 | 地址错误、从设备未上电、总线被拉死 | 检查地址、供电、SCL/SDA电平 |
| 数据错位 | 时钟频率不匹配、时序参数不满足 | 降低时钟频率、增加延时 |
| 总线卡死 | 从设备时钟拉伸超时、主设备未释放总线 | 加超时处理、复位总线 |
| 读回数据全0xFF | 从设备未应答、上拉电阻失效 | 检查上拉电阻、测量SDA电平 |
避坑指南:我曾经在调试一块AFE板子时,I2C总线一直卡在低电平。查了半天,发现是SDA的上拉电阻焊错了,用了10kΩ而不是4.7kΩ。换掉后一切正常。上拉电阻的选择很关键,一般4.7kΩ到10kΩ之间,具体看总线电容和通信速率。
好了,I2C通信协议的核心内容就这些。记住:起始/停止条件要严格按时序来,从机地址别写错,读写操作分清流程,时钟拉伸要加超时,仲裁在BMS里很少用但得知道。把这些搞定了,AFE芯片的驱动开发就成功了一半。