2. I2C协议时序:起始条件、停止条件、数据有效性、字节格式与ACK/NACK机制、时钟同步与仲裁
各位同学,咱们今天聊点实在的。I2C协议看着简单,就两根线——SCL和SDA。但真要在项目里把它调稳了,你得把它的时序吃透。我刚开始用I2C的时候,总觉得不就是发个起始信号、传几个字节、再发个停止信号嘛。结果呢?从设备就是不响应,折腾了两天才发现是起始条件没满足时序要求。嗯,咱们今天就把它掰开揉碎了讲清楚。
2.1 起始条件与停止条件
I2C总线上所有的通信,都从起始条件(S)开始,以停止条件(P)结束。这两个信号由主设备产生,从设备只能被动响应。
| 信号 | SCL状态 | SDA变化 | 说明 |
|---|---|---|---|
| 起始条件(S) | 高电平 | 从高→低跳变 | SCL为高时,SDA由高变低 |
| 停止条件(P) | 高电平 | 从低→高跳变 | SCL为高时,SDA由低变高 |
说白了,起始和停止就是SCL在高电平期间,SDA来一个反向跳变。我习惯在代码里这样写:
// 起始条件:SCL高时,SDA从高拉低
void i2c_start(void) {
SDA_HIGH();
SCL_HIGH();
delay_us(5); // 保持建立时间
SDA_LOW();
delay_us(5); // 保持保持时间
SCL_LOW(); // 准备传输数据
}
// 停止条件:SCL高时,SDA从低拉高
void i2c_stop(void) {
SDA_LOW();
SCL_HIGH();
delay_us(5);
SDA_HIGH();
delay_us(5);
}
2.2 数据有效性
I2C的数据采样有个铁律:SCL高电平时,SDA必须稳定;SCL低电平时,SDA才能变化。你想想看,如果SCL高的时候SDA还在跳变,那到底是数据还是起始/停止条件?协议就乱套了。
具体来说:
- SCL为低电平时,发送器可以改变SDA的电平
- SCL为高电平时,接收器采样SDA的电平
- 每个SCL时钟脉冲传输1位数据
我在调试一个温湿度传感器时遇到过一个问题:传感器偶尔返回错误数据。用逻辑分析仪一看,发现是MCU的SCL高电平时间太短,传感器还没来得及采样SDA就变了。后来把SCL高电平的占空比调到50%以上,问题就解决了。
2.3 字节格式与传输顺序
I2C每次传输一个字节(8位),高位在前(MSB first)。每个字节后面紧跟着一个应答位(ACK/NACK)。
一个完整的字节传输流程是这样的:
- 发送器在SCL低电平时逐位设置SDA(从MSB到LSB)
- SCL每来一个高电平脉冲,接收器采样一位
- 第8位(LSB)传输完成后,发送器释放SDA
- 第9个SCL脉冲,接收器拉低SDA表示ACK,或保持高表示NACK
// 发送一个字节,并读取应答位
uint8_t i2c_send_byte(uint8_t data) {
uint8_t ack;
for (int i = 7; i >= 0; i--) {
if (data & (1 << i))
SDA_HIGH();
else
SDA_LOW();
SCL_HIGH();
delay_us(5);
SCL_LOW();
delay_us(5);
}
// 第9个时钟:释放SDA,读取ACK
SDA_HIGH(); // 释放总线
SCL_HIGH();
delay_us(3);
ack = SDA_READ(); // 0=ACK, 1=NACK
SCL_LOW();
return ack;
}
2.4 ACK/NACK机制
ACK/NACK是I2C协议的握手信号。每个字节传输完成后,接收器必须给出回应:
| 应答 | SDA电平(第9个SCL) | 含义 |
|---|---|---|
| ACK(应答) | 低电平 | 接收成功,可以继续发送下一字节 |
| NACK(非应答) | 高电平 | 接收失败,或接收器不想继续接收 |
这里有个坑:谁拉低SDA谁就是ACK的发送方。发送器发送完字节后必须释放SDA,由接收器拉低。如果接收器没拉低(比如地址不匹配、设备忙、或者根本没这个设备),SDA保持高电平,就是NACK。
2.5 时钟同步与仲裁
这两个机制是I2C多设备通信的基石。很多同学觉得I2C只能一主多从,其实它支持多主设备,靠的就是时钟同步和仲裁。
时钟同步
多个主设备同时驱动SCL时,谁拉低SCL,SCL就是低电平;谁先释放,SCL也不会立即变高,要等所有主设备都释放了,SCL才变高。这叫线与(Wired-AND)。
说白了,SCL的低电平时间由拉低时间最长的主设备决定,高电平时间由拉高时间最短的主设备决定。这样所有主设备的时钟就同步了。
仲裁
仲裁发生在SDA线上。多个主设备同时发送数据时,如果某个主设备想拉高SDA,但另一个主设备在拉低SDA,那么SDA实际是低电平。拉高失败的那个主设备检测到SDA与自己发送的电平不一致,就知道自己仲裁失败了,立即释放总线。
仲裁的规则是:低电平优先。谁先发送0,谁就赢得总线控制权。
我曾在项目中用两个MCU通过I2C互传数据,两个MCU都配置为主设备。一开始担心会冲突,后来发现只要处理好仲裁,两个主设备可以轮流占用总线,完全没问题。当然,前提是每个主设备都要有仲裁失败后的重试机制。
2.6 本章知识结构图
下面这张图把I2C协议时序的核心知识点串起来了,方便你对照理解: