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。
这里的寄存器布局和数据仅用于举例。
整个流程如下:
|
|
每一步的作用是:
- START:开始通信。
- 设备地址 + W:选择目标设备,并指定写方向。
- ACK:设备应答。
- 寄存器地址:告诉设备要访问哪个寄存器。
- ACK:设备应答寄存器地址。
- 数据:发送要写入的值。
- ACK:设备应答数据字节。
- STOP:结束通信。
这里有一个重要区别:
- 设备地址用于选择总线上的设备。
- 寄存器地址用于选择设备内部的寄存器。
- 数据是实际要写入的内容。
六、I²C 读取时序
读取寄存器通常分为两个阶段:
- 先告诉设备要读取哪个寄存器。
- 再让设备把寄存器数据发送出来。
假设我们要读取设备 0x48 的寄存器 0x01。
常见的读取流程如下:
|
|
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 通常是正常的 |
建议按以下顺序排查:
- 检查供电、GND、SDA、SCL 和上拉电阻。
- 查阅数据手册,确认设备的 7 位地址。
- 确认使用了正确的 I²C 控制器或总线。
- 检查读写顺序、寄存器地址以及是否需要 Repeated START。
- 如果问题仍然存在,再用逻辑分析仪观察真实波形。
在 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 不需要一次记住所有细节。先理解每一步为什么存在,再结合实际开发板观察信号波形,就能逐渐建立对通信协议的直觉。
具体设备的地址、寄存器布局、时序限制和读写要求,始终应以对应的数据手册为准。