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Ω之间,具体看总线电容和通信速率。

我的经验:上拉电阻选多大,我有个简单公式——总线电容越大、速率越高,电阻就得越小。100kHz标准模式用4.7kΩ基本不会翻车,400kHz快速模式建议用2.2kΩ。有一次我在一个长距离I2C项目里用了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)。

注意:很多I2C器件的数据手册里给出的地址是7位值,比如0x50。但实际发送时,你需要左移一位,再把读写位拼到最低位。所以写操作发送的是0xA0,读操作发送的是0xA1。这个坑我见过不少人踩过。

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写操作流程是这样的:

  1. 主设备发送起始条件
  2. 主设备发送7位从机地址 + 写标志位(0)
  3. 从设备回ACK
  4. 主设备发送1个字节数据(比如寄存器地址)
  5. 从设备回ACK
  6. 主设备发送1个或多个数据字节
  7. 每个字节后从设备回ACK
  8. 主设备发送停止条件

读操作稍微复杂一点,通常需要先写寄存器地址,然后重新发送起始条件,再发地址+读标志位,最后从设备开始发数据。这叫复合通信(Combined Format)。

2.4 I2C时序分析

搞嵌入式的人,光懂协议还不够,还得会看时序图。我给大家画一张I2C通信的完整时序图,把每个细节都标清楚。

I2C完整通信时序图 SCL SDA 起始 S 从机地址 (7位) R/W ACK 数据字节 (8位) ACK 停止 P 时序关键参数说明 • fSCL:时钟频率,标准模式100kHz,快速模式400kHz,高速模式3.4MHz • tHD:STA:起始条件保持时间,最小4μs(标准模式) • tSU:STO:停止条件建立时间,最小4μs(标准模式) • tBUF:停止条件到下一个起始条件的总线空闲时间,最小4.7μs(标准模式)

这张图里我标注了几个关键时序参数。在实际项目中,这些时序参数必须严格遵守,否则通信就会出问题。我给大家整理了一个速查表:

参数 符号 标准模式 (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
避坑指南:我曾经在一个项目里用软件模拟I2C时序,结果SCL低电平时间没算对,比标准模式要求的4.7μs短了一点点。板子跑起来大部分时候正常,但偶尔会丢数据。用示波器一抓才发现时序违规。从那以后,我写I2C驱动都会在代码里加时序检查,确保每个时间参数都留有余量。

2.5 多字节传输与复合通信

实际项目中,我们很少只传一个字节。比如读写EEPROM,通常要先发2个字节的地址,再发多个字节的数据。这种场景下,I2C支持连续传输:

  • 主设备可以连续发送多个数据字节,每个字节后从设备回ACK
  • 如果从设备来不及处理,可以拉低SCL(时钟拉伸)来让主设备等待
  • 读操作时,如果主设备不想再读了,就在最后一个字节后回NACK,然后发停止条件

复合通信(Combined Format)是I2C的一个强大特性。比如你要读一个传感器,流程是:

  1. 发起始条件
  2. 发从机地址 + 写标志
  3. 发寄存器地址(告诉传感器你要读哪个寄存器)
  4. 发重复起始条件(Repeated Start)
  5. 发从机地址 + 读标志
  6. 从传感器读取数据
  7. 发停止条件

这里的关键是重复起始条件。它跟普通起始条件一样,但前面没有停止条件。这样做的好处是:总线不会被释放,其他主设备抢不走控制权。

我的习惯:在Bootloader里用I2C读写外部Flash时,我一般把一次读操作封装成一个函数。函数内部处理复合通信的所有细节,上层调用者只需要告诉我要读哪个地址、读多少字节就行。这样代码复用率高,也不容易出错。

2.6 本章小结

I2C协议看起来简单,但细节很多。我给大家总结几个必须记住的点:

  • 两根线:SCL和SDA,开漏输出,必须加上拉电阻
  • 起始条件:SCL高电平时SDA下降沿
  • 停止条件:SCL高电平时SDA上升沿
  • 地址+读写位:7位地址左移1位,最低位是R/W
  • 应答机制:每字节后第9个时钟,接收方拉低SDA表示ACK
  • 时序参数:必须严格遵守,尤其是SCL高低电平时间