2. I2C协议时序:起始条件、停止条件、数据有效性、字节格式与应答机制
说实话,搞I2C这么多年,我见过太多工程师上来就调代码,结果波形一塌糊涂。时序这东西,看着简单,但坑特别多。咱们今天就把I2C最核心的五个时序要素掰开揉碎了讲清楚。
2.1 起始条件(S)与停止条件(P)
I2C总线的起始和停止,说白了就是SCL和SDA两根线的电平跳变游戏。我习惯用一句话记:SCL高电平时,SDA跳变才算数。
| 条件 | SCL状态 | SDA跳变方向 | 谁发起 |
|---|---|---|---|
| 起始条件(S) | 高电平 | 高 → 低 | 主机 |
| 停止条件(P) | 高电平 | 低 → 高 | 主机 |
| 重复起始(Sr) | 高电平 | 高 → 低 | 主机(不发送P) |
这里有个细节——重复起始条件(Sr)。我在项目中遇到过有人用停止+起始来实现多字节读写,结果总线效率直接砍半。其实你想想看,Sr就是在不释放总线的情况下再发一次S,省掉中间的P时间。这在EEPROM随机读、传感器多寄存器访问时特别有用。
2.2 数据有效性
I2C的数据采样规则其实很反直觉:SCL低电平时SDA可以变,SCL高电平时SDA必须稳。
为什么会这样?因为从机是在SCL的上升沿采样SDA。如果你在SCL高电平期间动了SDA,从机可能采到错误电平,甚至误判成起始或停止条件。
数据有效性的核心约束就两条:
- SCL低电平期间:SDA允许变化,主机可以准备下一位数据
- SCL高电平期间:SDA必须保持稳定,从机在此窗口采样
2.3 字节格式与传输顺序
I2C的字节传输格式非常固定:8位数据 + 1位应答。每次传输9个SCL时钟周期。
字节内的位顺序是MSB first(最高位在前)。我记得刚入行时写了个驱动,把LSB first的SPI习惯带过来了,结果地址对不上,折腾了一下午才发现是位序搞反了。
// 一个典型的I2C字节发送流程(伪代码)
void i2c_send_byte(uint8_t data) {
for (int i = 7; i >= 0; i--) { // MSB first
SDA = (data >> i) & 0x01;
SCL = 1; // 从机采样
delay_half_period();
SCL = 0; // 允许SDA变化
delay_half_period();
}
// 第9个时钟:释放SDA,等待应答
SDA = 1; // 释放总线
SCL = 1;
ack = SDA; // 读取应答位
SCL = 0;
}
2.4 应答机制(ACK/NACK)
应答是I2C协议里最优雅的设计之一。每发完一个字节,接收方要在第9个时钟周期拉低SDA表示ACK,保持高电平表示NACK。
我总结了几种常见场景:
- 主机发送地址后:从机ACK表示存在,NACK表示无应答(设备不在线或地址错误)
- 主机发送数据后:从机ACK表示接收成功,NACK表示无法处理(比如缓冲区满)
- 主机接收数据后:主机ACK表示还要继续读,NACK表示够了,发停止
2.5 完整时序图
下面这张图把上面讲的五个要素串起来了。我画图时习惯把SCL和SDA分开画,这样每个时钟周期对应什么数据一目了然。
2.6 时序参数速查
不同速率模式下,时序参数差异很大。我整理了一张常用对照表,调试时直接查:
| 参数 | 标准模式(100kHz) | 快速模式(400kHz) | 快速+模式(1MHz) |
|---|---|---|---|
| SCL低电平时间 | ≥4.7μs | ≥1.3μs | ≥0.5μs |
| SCL高电平时间 | ≥4.0μs | ≥0.6μs | ≥0.26μs |
| 起始保持时间 | ≥4.0μs | ≥0.6μs | ≥0.26μs |
| 停止建立时间 | ≥4.0μs | ≥0.6μs | ≥0.26μs |
| 数据保持时间 | ≥0ns | ≥0ns | ≥0ns |
| 上升/下降时间 | ≤1000ns | ≤300ns | ≤120ns |
2.7 常见时序问题排查
最后分享几个我实际项目中遇到的时序问题,你调试时可以参考:
- 起始条件不满足:SDA先于SCL拉低,但时间太短。检查代码中SDA和SCL的GPIO操作顺序
- 应答位丢失:从机没拉低SDA。先确认从机供电和地址,再用示波器看第9个时钟的SDA电平
- 数据错位:字节内位序搞反。打印原始数据,对比MSB/LSB顺序
- 总线卡死:从机一直拉低SDA。发9个SCL时钟强制复位从机状态机
嗯,时序这部分就这些。记住一句话:SCL高电平看跳变(S/P),SCL低电平看数据。把这个原则刻在脑子里,写驱动时基本不会出大问题。