I2C协议基础:物理层、起始条件、停止条件、数据有效性与应答机制
各位同学,今天我们正式进入I2C协议的核心地带。说实话,很多做嵌入式开发的朋友,调了几年I2C设备,遇到总线锁死还是只会复位。我个人觉得,这往往是因为对协议底层缺乏直觉。今天我们就从物理层开始,把I2C的每一个细节掰开揉碎。
2.1 I2C总线物理层:两条线怎么干活?
I2C总线只需要两根线:SDA(串行数据线)和SCL(串行时钟线)。但这两根线不是普通的推挽输出,而是开漏输出。
什么叫开漏?说白了,就是芯片只能把线拉低到GND,不能主动拉高。那高电平谁来给?上拉电阻。总线空闲时,两根线都被上拉电阻拉到VDD(通常是3.3V或5V)。
关键点:开漏结构允许多个设备共用总线。任何设备都可以拉低总线,但谁都不能独占。这就是I2C“多主多从”的物理基础。
我在项目中遇到过一个问题:OLED屏幕在长线传输时偶尔花屏。查了半天,发现是上拉电阻选大了,信号上升沿太慢。嗯,这里要注意——上拉电阻的取值直接影响通信速率。
| 总线速率 | 推荐上拉电阻 | 总线电容 |
|---|---|---|
| 标准模式(100kHz) | 4.7kΩ ~ 10kΩ | < 400pF |
| 快速模式(400kHz) | 2.2kΩ ~ 4.7kΩ | < 200pF |
| 高速模式(3.4MHz) | 1kΩ ~ 2.2kΩ | < 100pF |
我的经验:调试OLED时,先用4.7kΩ电阻。如果波形上升沿太缓,再换2.2kΩ。别一上来就用1kΩ,电流大了反而可能烧IO口。
2.2 起始条件与停止条件:总线的“开始”和“结束”
I2C通信的起点和终点,由SDA和SCL的特定电平变化来定义。你想想看,如果总线上挂着好几个设备,怎么告诉它们“我要开始说话了”?
- 起始条件(S):SCL为高电平时,SDA从高电平跳变到低电平。
- 停止条件(P):SCL为高电平时,SDA从低电平跳变到高电平。
为什么这么设计?因为SCL高电平时,SDA本应保持稳定。一旦SDA变化,就表示“这不是正常数据,是控制信号”。
曾经踩过的坑:有一次我写代码,起始条件后忘了发停止条件,结果总线一直处于“忙”状态,其他设备全都不响应。后来我养成习惯:每次通信结束,一定检查是否发送了停止条件。
我们来看一段典型的起始和停止代码(基于STM32 HAL库):
// 起始条件:SCL高时,SDA从高变低
void I2C_Start(void) {
SDA_HIGH();
SCL_HIGH();
delay_us(5); // 保持时间 > 4.7μs
SDA_LOW();
delay_us(5); // 保持时间 > 4μs
SCL_LOW(); // 准备传输数据
}
// 停止条件:SCL高时,SDA从低变高
void I2C_Stop(void) {
SDA_LOW();
SCL_HIGH();
delay_us(5); // 保持时间 > 4μs
SDA_HIGH();
delay_us(5); // 总线释放时间
}
2.3 数据有效性:什么时候读数据?
I2C的数据传输有个铁律:数据在SCL低电平时变化,在SCL高电平时采样。
为什么会这样?你想想看,如果数据在SCL高电平时变化,接收方就不知道哪个时刻是“稳定”的。所以协议规定:
- SCL为低电平时,SDA可以变化(发送方准备下一位数据)。
- SCL为高电平时,SDA必须保持稳定(接收方读取数据)。
我建议你在看逻辑分析仪波形时,重点关注SCL的上升沿。每个上升沿,就是一次数据采样点。
数据有效性规则:每个SCL时钟传输1位数据,高位在前(MSB first)。8个时钟后,完成一个字节的传输。
来看一个字节发送的时序图:
SCL: ▔▁▔▁▔▁▔▁▔▁▔▁▔▁▔▁▔▁▔▁▔▁▔▁
SDA: ▔▁▁▁▔▔▔▁▁▔▔▁▁▔▔▔▁▁▔▔▁▔
↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑
7 6 5 4 3 2 1 0 (数据位,MSB first)
2.4 应答机制:确认收到
每发送完一个字节(8位数据),发送方会释放SDA,接收方需要在第9个时钟周期拉低SDA,表示“我收到了”。这就是应答位(ACK)。
如果接收方不拉低SDA(保持高电平),那就是非应答(NACK)。NACK通常表示:
- 从设备忙,无法接收数据。
- 从设备不支持该命令。
- 数据接收完毕,不再需要更多数据。
调试技巧:我曾经调一个OLED驱动,发现发送地址后总是收到NACK。用示波器一看,地址字节的第8位(读写位)写反了。记住:地址左移1位,最低位是读写位(0=写,1=读)。
应答检测的代码实现:
// 检测从设备应答
uint8_t I2C_CheckACK(void) {
SDA_INPUT(); // 释放SDA,改为输入模式
SCL_HIGH();
delay_us(5);
uint8_t ack = SDA_READ(); // 读取SDA电平
SCL_LOW();
SDA_OUTPUT(); // 恢复输出模式
return (ack == 0) ? 1 : 0; // 0=ACK, 1=NACK
}
2.5 知识体系总览
下面这张图,把I2C协议的核心要素串在一起。我建议你把它存下来,调试时对照着看。
2.6 完整通信流程
一个完整的I2C写操作流程是这样的:
- 主机发送起始条件(S)
- 主机发送从机地址(7位地址 + 1位读写位,写=0)
- 从机应答(ACK)
- 主机发送寄存器地址(告诉从机要写哪个寄存器)
- 从机应答(ACK)
- 主机发送数据字节(可以连续发多个)
- 从机应答(每字节后都有ACK)
- 主机发送停止条件(P)
记住这个流程:起始 → 地址+读写位 → ACK → 数据 → ACK → ... → 停止。这是所有I2C设备通信的通用模板。
以OLED驱动芯片SSD1306为例,写一个字节到命令寄存器的代码:
void OLED_WriteCmd(uint8_t cmd) {
I2C_Start(); // 1. 起始条件
I2C_SendByte(0x78 << 1 | 0); // 2. 地址0x78 + 写位
I2C_CheckACK(); // 3. 等待应答
I2C_SendByte(0x00); // 4. 命令寄存器标识
I2C_CheckACK(); // 5. 等待应答
I2C_SendByte(cmd); // 6. 发送命令字节
I2C_CheckACK(); // 7. 等待应答
I2C_Stop(); // 8. 停止条件
}
曾经踩过的坑:有一次我连续写多个字节,忘了在每字节后检测ACK。结果从机在第3个字节时NACK了,我还在继续发数据。从那以后,我所有I2C代码都加了ACK检测,一旦收到NACK立即停止并重试。
好了,I2C协议的基础就讲到这里。这些内容看起来简单,但每一个细节都可能在调试时给你“惊喜”。我个人建议你花点时间,用逻辑分析仪抓一下实际波形,对照着协议规范看一遍。相信我,这比看十遍文档都管用。