2. I2C通信协议详解
各位同学,今天我们来啃一块硬骨头——I2C通信协议。说实话,我当年刚接触嵌入式时,觉得I2C不就是两根线嘛,能有多复杂?结果第一次调EEPROM读写,波形乱成一团,折腾了两天才发现是上拉电阻选错了。嗯,从那以后我再也不敢小看这个看似简单的协议了。
I2C(Inter-Integrated Circuit)总线,说白了就是飞利浦公司搞出来的一种串行通信总线。它最大的特点就是——只需要两根线,就能挂一堆设备。你想想看,一个MCU要同时跟传感器、存储器、IO扩展芯片通信,如果每个设备都单独拉一组数据线和时钟线,那PCB上就成蜘蛛网了。I2C就是来解决这个痛点的。
2.1 I2C总线物理层
先看看物理层长什么样。I2C总线只有两根线:
- SCL(Serial Clock):串行时钟线,由主设备驱动
- SDA(Serial Data):串行数据线,双向传输
这两根线都是开漏输出结构。什么叫开漏?就是芯片只能把线拉低,不能主动拉高。那高电平谁来给?靠外部的上拉电阻。所以I2C总线上必须接上拉电阻,阻值一般在1kΩ到10kΩ之间,具体看总线电容和通信速率。
I2C总线上每个设备都有一个唯一的7位从机地址(也有10位地址模式,但用得少)。主设备通过地址来选择跟谁说话。总线上的设备分为两种角色:
| 角色 | 说明 | 典型设备 |
|---|---|---|
| 主设备(Master) | 发起通信、产生时钟、决定通信方向 | MCU、MPU |
| 从设备(Slave) | 响应主设备、不能主动发起通信 | 传感器、存储器、ADC/DAC |
这里有个关键点:同一时刻只能有一个主设备控制总线。如果多个主设备同时想说话,I2C有仲裁机制来解决冲突。不过在我们Bootloader开发场景里,MCU永远是唯一的主设备,所以仲裁这块我就不展开了。
2.2 I2C协议帧格式
好,物理层搞清楚了,我们来看看数据是怎么在线上跑的。I2C的通信帧结构非常规整,我把它拆成几个关键步骤来讲。
2.2.1 起始条件(Start Condition)
总线空闲时,SCL和SDA都是高电平。主设备要开始通信,必须发送一个起始信号:
起始条件:SCL保持高电平,SDA从高电平切换到低电平。
这个下降沿告诉所有从设备:“注意了,我要开始说话了!”
2.2.2 从机地址与读写位
起始条件之后,主设备紧接着发送7位从机地址,然后是1位读写标志位:
- R/W = 0:主设备要写数据到从设备
- R/W = 1:主设备要从从设备读数据
地址和读写位加起来正好8位,也就是一个字节。发送顺序是高位在前(MSB first)。
2.2.3 应答机制(ACK/NACK)
每发送完一个字节(8位数据),接收方必须在第9个时钟周期拉低SDA线,表示“我收到了”。这就是应答位(ACK)。如果接收方不拉低SDA(保持高电平),那就是非应答(NACK),表示出问题了。
我个人习惯把应答机制看作一种握手协议。发送方每扔出一个字节,都要等对方回一句“收到”。如果没等到,就得处理异常。
2.2.4 数据字节
地址发完、应答收到之后,就开始传数据了。每个数据字节也是8位,高位在前。每发完一个字节,接收方都要回一个ACK。数据可以连续发多个字节,没有数量限制。
2.2.5 停止条件(Stop Condition)
通信结束时,主设备发送停止信号:
停止条件:SCL保持高电平,SDA从低电平切换到高电平。
这个上升沿告诉所有从设备:“我说完了,总线释放了。”
2.3 完整的I2C通信流程
把上面这些拼起来,一次完整的I2C写操作流程是这样的:
- 主设备发送起始条件
- 主设备发送7位从机地址 + 写标志位(0)
- 从设备回ACK
- 主设备发送1个字节数据(比如寄存器地址)
- 从设备回ACK
- 主设备发送1个或多个数据字节
- 每个字节后从设备回ACK
- 主设备发送停止条件
读操作稍微复杂一点,通常需要先写寄存器地址,然后重新发送起始条件,再发地址+读标志位,最后从设备开始发数据。这叫复合通信(Combined Format)。
2.4 I2C时序分析
搞嵌入式的人,光懂协议还不够,还得会看时序图。我给大家画一张I2C通信的完整时序图,把每个细节都标清楚。
这张图里我标注了几个关键时序参数。在实际项目中,这些时序参数必须严格遵守,否则通信就会出问题。我给大家整理了一个速查表:
| 参数 | 符号 | 标准模式 (100kHz) | 快速模式 (400kHz) |
|---|---|---|---|
| SCL时钟频率 | fSCL | ≤100 kHz | ≤400 kHz |
| 起始条件保持时间 | tHD:STA | ≥4.0 μs | ≥0.6 μs |
| SCL低电平时间 | tLOW | ≥4.7 μs | ≥1.3 μs |
| SCL高电平时间 | tHIGH | ≥4.0 μs | ≥0.6 μs |
| 数据建立时间 | tSU:DAT | ≥250 ns | ≥100 ns |
| 数据保持时间 | tHD:DAT | ≥0 ns | ≥0 ns |
| 停止条件建立时间 | tSU:STO | ≥4.0 μs | ≥0.6 μs |
| 总线空闲时间 | tBUF | ≥4.7 μs | ≥1.3 μs |
2.5 多字节传输与复合通信
实际项目中,我们很少只传一个字节。比如读写EEPROM,通常要先发2个字节的地址,再发多个字节的数据。这种场景下,I2C支持连续传输:
- 主设备可以连续发送多个数据字节,每个字节后从设备回ACK
- 如果从设备来不及处理,可以拉低SCL(时钟拉伸)来让主设备等待
- 读操作时,如果主设备不想再读了,就在最后一个字节后回NACK,然后发停止条件
复合通信(Combined Format)是I2C的一个强大特性。比如你要读一个传感器,流程是:
- 发起始条件
- 发从机地址 + 写标志
- 发寄存器地址(告诉传感器你要读哪个寄存器)
- 发重复起始条件(Repeated Start)
- 发从机地址 + 读标志
- 从传感器读取数据
- 发停止条件
这里的关键是重复起始条件。它跟普通起始条件一样,但前面没有停止条件。这样做的好处是:总线不会被释放,其他主设备抢不走控制权。
2.6 本章小结
I2C协议看起来简单,但细节很多。我给大家总结几个必须记住的点:
- 两根线:SCL和SDA,开漏输出,必须加上拉电阻
- 起始条件:SCL高电平时SDA下降沿
- 停止条件:SCL高电平时SDA上升沿
- 地址+读写位:7位地址左移1位,最低位是R/W
- 应答机制:每字节后第9个时钟,接收方拉低SDA表示ACK
- 时序参数:必须严格遵守,尤其是SCL高低电平时间