I²C 通信协议

一、什么是 I²C?

I²C(Inter-Integrated Circuit)是一种常见的串行通信协议,主要用于芯片之间的短距离通信。

在嵌入式系统中,I²C 经常用于连接:

  • 温度传感器
  • EEPROM 存储芯片
  • RTC 实时时钟
  • GPIO 扩展芯片
  • 其他各种外围设备

例如,在树莓派上,可以通过 I²C 连接一个温度传感器,然后读取传感器内部的温度数据。

1. I²C 使用哪几根线?

I²C 主要使用两根信号线:

  • SCL(Serial Clock):时钟线,负责同步数据传输。
  • SDA(Serial Data):数据线,负责传输地址、数据和应答信号。

I²C 通常使用主从通信方式。主设备发起通信,从设备根据地址决定是否响应。

如,树莓派可以作为主设备,温度传感器作为从设备。

两根线通常都需要上拉电阻,设备通过开漏或开集电极方式控制信号线。

2. 为什么需要时钟线?

如果只有数据线,接收方可能无法判断一段电平持续了多久,以及它代表多少个数据位。

例如,数据线一直保持低电平,究竟表示一个 0,还是连续的多个 0?

因此,I²C 使用 SCL 提供时钟,让接收方知道何时读取数据。

类似时钟节拍:

  • SCL 决定读取数据的节奏。
  • SDA 决定每一位数据是 0 还是 1。

I²C 的普通数据位通常在 SCL 高电平期间保持稳定,并在 SCL 低电平期间改变。

二、START 和 STOP:一轮通信的开始与结束

I²C 需要明确告诉设备一轮通信什么时候开始、什么时候结束。

1. START:起始信号

当 SCL 保持高电平时,SDA 从高电平变为低电平,就表示 START。

它告诉总线上的设备:一轮新的通信开始了。

2. STOP:停止信号

当 SCL 保持高电平时,SDA 从低电平变为高电平,就表示 STOP。

它告诉设备:本次通信结束。

需要注意,普通数据位在 SCL 高电平期间保持稳定,而 START 和 STOP 是特殊信号,它们允许 SDA 在 SCL 高电平时发生变化。

三、设备地址:告诉总线上要和谁通信

同一条 I²C 总线上可以连接多个设备。

例如:

设备 示例地址
温度传感器 0x48
时钟芯片 0x68
存储芯片 0x50

以上地址仅用于举例。

当主设备发送 START 后,接下来会发送目标设备的地址。

总线上的设备都能观察到这些信号,但只有地址匹配的设备才应该响应。

1. 7 位地址与读写标志

I²C 常见的 7 位地址传输格式是:

7 位设备地址 + 1 位读写标志

读写标志的含义是:

  • 0:Write,主设备向从设备发送数据。
  • 1:Read,主设备从从设备接收数据。

例如,假设某设备的 7 位地址是 0x48:

  • 写方向:地址字节为 0x90
  • 读方向:地址字节为 0x91

这是因为在线上传输时,7 位地址会左移一位,再把读写标志放到最低位。

实际使用 Linux 常规 I²C API 时,通常传入 7 位地址 0x48,不需要自行左移。

四、ACK 和 NACK:确认是否应答

I²C 每传输 8 位数据后,会有第 9 个时钟用于应答。

接收方通过 SDA 表示 ACK 或 NACK。

1. ACK:应答

ACK 表示接收方在应答时钟期间把 SDA 拉低,也就是逻辑 0。

例如,主设备发送设备地址后,如果对应的传感器识别到自己的地址,就会发送 ACK。

2. NACK:未应答

NACK 表示接收方没有把 SDA 拉低,线路通常保持高电平,也就是逻辑 1。

如果主设备发送地址后没有收到 ACK,就可能是设备不存在、地址错误、设备未准备好,或者通信线路存在问题。

但要注意:读取最后一个字节后,主设备主动发送 NACK 通常是正常操作,表示不再需要更多数据。

ACK 只表示接收方在协议层面应答,并不保证数据内容一定正确。

五、I²C 写入时序

假设树莓派要向地址为 0x48 的传感器写入数据。

假设这个设备有一个地址为 0x01 的寄存器,我们想向它写入 0x80。

这里的寄存器布局和数据仅用于举例。

整个流程如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
START
  ↓
设备地址 + W
  ↓
ACK
  ↓
寄存器地址 0x01
  ↓
ACK
  ↓
数据 0x80
  ↓
ACK
  ↓
STOP

每一步的作用是:

  1. START:开始通信。
  2. 设备地址 + W:选择目标设备,并指定写方向。
  3. ACK:设备应答。
  4. 寄存器地址:告诉设备要访问哪个寄存器。
  5. ACK:设备应答寄存器地址。
  6. 数据:发送要写入的值。
  7. ACK:设备应答数据字节。
  8. STOP:结束通信。

这里有一个重要区别:

  • 设备地址用于选择总线上的设备。
  • 寄存器地址用于选择设备内部的寄存器。
  • 数据是实际要写入的内容。

六、I²C 读取时序

读取寄存器通常分为两个阶段:

  1. 先告诉设备要读取哪个寄存器。
  2. 再让设备把寄存器数据发送出来。

假设我们要读取设备 0x48 的寄存器 0x01。

常见的读取流程如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
START
  ↓
设备地址 + W
  ↓
ACK
  ↓
寄存器地址 0x01
  ↓
ACK
  ↓
Repeated START
  ↓
设备地址 + R
  ↓
ACK
  ↓
从设备发送数据
  ↓
主设备发送 NACK
  ↓
STOP

1. 为什么先使用写方向?

这里的 W 并不意味着马上向寄存器写入数据。

主设备先用写方向发送寄存器地址,是为了告诉传感器:接下来想读取哪个寄存器。

2. 为什么需要 Repeated START?

Repeated START 是重复起始信号。

在常见的寄存器读取操作中,主设备先发送寄存器地址,再通过 Repeated START 开始新的地址阶段,并将读写方向切换为读取。

它的作用是让两个阶段连续进行,而不先释放总线。

需要注意,真正指定读取方向的是后面地址字节中的 R 标志。Repeated START 本身并不代表读取。

某些设备允许使用 STOP 后再 START 的方式完成操作,但具体要遵循设备数据手册规定的时序。

3. 为什么读取结束时由主设备发送 NACK?

读取时,数据由从设备发送,主设备接收。

如果主设备还想继续读取后续字节,可以对当前字节发送 ACK;如果已经读取完毕,则通常对最后一个字节发送 NACK,随后发送 STOP。

七、I²C 通信失败时,如何排查 NACK?

实际开发中,如果出现 NACK,不要马上认定硬件损坏。

第一步是确认 NACK 出现在哪个阶段。

出错位置 优先检查的内容
设备地址后 供电、接线、设备地址、总线编号
寄存器地址后 寄存器地址、设备协议、通信时序
写入数据后 数据格式、设备状态、命令要求
读取最后一个字节后 主设备发送 NACK 通常是正常的

建议按以下顺序排查:

  1. 检查供电、GND、SDA、SCL 和上拉电阻。
  2. 查阅数据手册,确认设备的 7 位地址。
  3. 确认使用了正确的 I²C 控制器或总线。
  4. 检查读写顺序、寄存器地址以及是否需要 Repeated START。
  5. 如果问题仍然存在,再用逻辑分析仪观察真实波形。

在 Linux 中,i2cdetect 可以辅助检查设备地址响应,但部分设备不适合被随意探测,使用前应确认设备手册和驱动情况。

八、I²C 与 Linux 驱动的关系

在树莓派这样的嵌入式 Linux 系统中,应用程序通常不需要自己通过 GPIO 手动产生每一个 SCL 时钟。

一般情况下:

  • 应用程序提出读取或写入请求。
  • Linux I²C 子系统负责组织通信请求。
  • I²C 控制器驱动负责与硬件控制器交互。
  • 控制器根据协议产生实际的总线时序。
  • 传感器通过 SDA 返回数据。

因此,理解 I²C 时序,有助于进一步理解 Linux I²C 子系统、设备驱动和硬件控制器之间的关系。

九、总结

I²C 的核心概念可以归纳为:

概念 作用
SCL 提供时钟
SDA 传输数据
START 开始一轮通信
设备地址 选择目标设备
W / R 指定读写方向
ACK / NACK 应答或未应答
寄存器地址 选择设备内部的寄存器
Repeated START 在不释放总线的情况下开启新的地址阶段
STOP 结束通信

两种常见流程:

写寄存器:

START → 地址+W → ACK → 寄存器地址 → ACK → 数据 → ACK → STOP

读寄存器:

START → 地址+W → ACK → 寄存器地址 → ACK → Repeated START → 地址+R → ACK → 数据 → NACK → STOP

学习 I²C 不需要一次记住所有细节。先理解每一步为什么存在,再结合实际开发板观察信号波形,就能逐渐建立对通信协议的直觉。

具体设备的地址、寄存器布局、时序限制和读写要求,始终应以对应的数据手册为准。