3. I2C总线协议:物理层、起始/停止条件、数据有效性、地址与读写位、应答机制

各位同学,今天我们来聊聊I2C总线。说实话,I2C是我个人非常喜欢的一种接口协议。为什么?因为它简单、优雅,两根线就能搞定一堆设备的通信。我在做嵌入式项目的那些年,几乎每个板子上都能看到它的身影。

但别被它的简单外表骗了。越简单的协议,坑往往越深。我曾经在一个量产项目上,因为I2C时序差了那么零点几微秒,导致整批产品在高温下随机死机。嗯,那滋味可不好受。所以今天咱们把I2C的每个细节都掰开揉碎了讲清楚。

3.1 物理层:两根线,一个故事

I2C总线物理上只需要两根线:

  • SCL(串行时钟线):由主设备驱动,控制通信节奏
  • SDA(串行数据线):双向数据线,主从设备都能拉低

这两根线都是开漏输出结构。什么叫开漏?说白了,就是芯片只能把线拉低到GND,不能主动拉高。那高电平谁来给?靠外部的上拉电阻。

关键参数:

  • 标准模式:100 kHz
  • 快速模式:400 kHz
  • 高速模式:3.4 MHz
  • 上拉电阻:通常4.7kΩ(100kHz时),400kHz时可能需要2.2kΩ

我个人的习惯是,如果总线上挂的设备超过4个,或者线长超过10cm,我会把上拉电阻换小一点。比如从4.7k换成2.2k。为什么?因为总线电容大了,上升沿会变缓,容易导致时序违规。你想想看,SCL的上升沿太缓,从设备可能误判时钟边沿。

避坑指南:我曾经在一个项目里,为了省电把上拉电阻换成了100kΩ。结果呢?总线根本跑不起来,波形像正弦波一样圆润。从那以后,我选上拉电阻再也不敢凭感觉了,都是先算一下RC时间常数。

3.2 起始条件和停止条件

I2C总线的通信,以起始条件开始,以停止条件结束。这两个条件非常特殊——它们发生在SCL为高电平时,SDA发生跳变。

条件 SDA行为 SCL状态
起始条件(S) 从高电平跳变到低电平 高电平
停止条件(P) 从低电平跳变到高电平 高电平

为什么要在SCL高电平时跳变?因为正常数据传输时,SDA只能在SCL低电平时变化。SCL高电平时SDA必须保持稳定。所以,在SCL高电平时改变SDA,就成了一个「特殊信号」。

我记得刚学I2C时,总觉得这个设计很巧妙。用两根线的电平组合,就能定义出起始和停止,省掉了一根额外的控制线。

3.3 数据有效性:时钟的节奏

数据传输的规则其实很简单:

  • SCL低电平:SDA可以变化(发送方准备下一位数据)
  • SCL高电平:SDA必须稳定(接收方采样数据)

每个时钟脉冲传输1位数据。一个字节(8位)需要8个时钟脉冲。但注意,I2C是MSB优先,也就是先传最高位。

小技巧:调试时,我习惯用逻辑分析仪抓SCL和SDA的波形。先看SCL高电平时SDA是否稳定。如果看到SDA在SCL高电平时有毛刺或抖动,那基本可以断定是时序问题或者上拉电阻不合适。

3.4 地址与读写位:找到对的人

起始条件之后,主设备要发送的第一个字节是从设备地址+读写位。格式如下:

bit 7 - bit 1: 7位从设备地址
bit 0: 读写位(0=写,1=读)

举个例子,假设一个温度传感器的地址是0x48(二进制1001000),你要读它:

发送字节: 1001000 1 = 0x91
         ↑地址    ↑读位

如果是写操作:

发送字节: 1001000 0 = 0x90
         ↑地址    ↑写位

这里有个容易混淆的地方。很多数据手册上写的设备地址是7位值,比如0x48。但你在代码里发送时,要左移一位再加上读写位。我刚开始做的时候,就因为这个吃了亏——直接发了0x48,结果设备死活没反应。

10位地址模式:有些设备地址不够用,I2C还支持10位地址。第一个字节的前5位是11110,后2位是地址的高2位。第二个字节是剩下的8位。不过说实话,我做了这么多年,用到10位地址的场景屈指可数。大部分时候7位地址(最多128个设备)完全够用。

3.5 应答机制:握手的艺术

I2C的每个字节后面,都要跟一个应答位。这是协议里我最欣赏的设计——它让通信变得可靠。

应答机制是这样的:

  1. 发送方发出8位数据后,释放SDA线(拉高)
  2. 发送方在第9个时钟脉冲时,产生一个SCL时钟
  3. 接收方如果收到数据,就把SDA拉低(应答ACK)
  4. 如果接收方不拉低SDA(保持高电平),就是非应答NACK

什么时候会出现NACK?

  • 从设备忙,无法处理数据
  • 从设备没收到完整数据
  • 主设备读数据时,读完最后一个字节后发送NACK,告诉从设备「别再发了」

我曾经踩过的坑:有一次,我写的I2C驱动在读取传感器数据时,总是卡在第一个字节。用示波器一看,从设备确实发出了ACK,但我的代码没检测到。后来发现,是中断服务程序占用了太多时间,导致检测ACK时SCL已经过去了。从那以后,我写I2C驱动都会用状态机,确保每个时序步骤都不被中断打断。

3.6 完整通信流程

把上面这些拼起来,一次完整的I2C写操作是这样的:

主设备: [起始条件] [地址+写位] [等待ACK] [数据字节1] [等待ACK] ... [停止条件]
从设备:              [拉低ACK]            [拉低ACK]

读操作稍微不同:

主设备: [起始条件] [地址+读位] [等待ACK] [接收数据] [发送NACK] [停止条件]
从设备:              [拉低ACK]            [发送数据]

注意读操作的最后,主设备要发送NACK。这是告诉从设备:「我读完了,别再发了」。如果主设备发了ACK,从设备会以为你还要继续读,就会继续发下一个字节。

3.7 知识体系总览

下面这张图,是我自己总结的I2C协议知识结构。每次做项目前,我都会对照着检查一遍,确保没有遗漏。

I2C总线协议 物理层 SCL / SDA 开漏输出 上拉电阻 4.7kΩ 起始/停止条件 SCL高电平时SDA跳变 起始: 高→低 / 停止: 低→高 数据有效性 SCL低电平变化 SCL高电平采样 地址与读写位 7位地址 + 1位读写 MSB优先传输 应答机制 第9个时钟脉冲 ACK=拉低 / NACK=高电平 完整通信流程 起始→地址→数据→停止 读/写操作差异 核心要点 两根线、开漏输出、上拉电阻、应答机制

好了,I2C协议的核心内容就这些。你想想看,其实I2C的设计哲学就是「简单可靠」。两根线,一个时钟,一个数据,再加上应答机制,就能让多个设备有序通信。下次你在板子上看到I2C总线时,不妨想想今天讲的这些细节——特别是那个上拉电阻,别像我一样踩坑。