I2C 笔记
摘要
对 I2C 串行通信协议的详细技术解释,涵盖其拓扑结构、开漏架构以及使用线与逻辑的总线仲裁。
<p><a href="https://lobste.rs/s/akyfrt/notes_on_i2c">评论</a></p>
查看缓存全文
缓存时间: 2026/05/20 20:31
# Emaan Rana
来源:https://rana-emaan.com/read/notes/i2c/
> 我在7天内重写了三次,并且在这些图表上花了太多时间,希望这次能达到标准。
## I2C
- 代表 Inter-Integrated Circuit(集成电路间总线),缩写为 I2C 或 I²C。
- 它是一种同步、双线制的串行通信总线,用于短距离连接低速外设。
## SDA
- 代表 `Serial DAta`(串行数据)。
- 在设备之间双向传输数据。
## SCL
- 代表 `Serial CLock`(串行时钟)。
- 承载时钟信号,用于同步所有数据传输。
## 拓扑结构
- 总线上至少有一个控制器和一个目标。
- 常见情况是单个控制器和一个或多个目标。
- 不常见的情况下,可以有多个控制器(多控制器)。
- 目标可以连接到线路上的任意点。
- 每条线都连接到各自的上拉电阻,这些电阻连接到相同的正电压。
## 开漏
- 在 I2C 中,`SDA` 和 `SCL` 空闲时为 `HIGH`(高电平),即默认状态为高电平。因此,I2C 被认为是一个“开漏”系统。
- 当漏极闭合时,`SDA` 和 `SCL` 被拉至 `LOW`(低电平,接地)。
- 当漏极断开时,`SDA` 和 `SCL` 返回 `HIGH`(高电平,连接到正电压)。
### 总线仲裁
- I2C 可以在同一总线上支持多个控制器。解决冲突的方式自然源于开漏设计。
#### 线与逻辑
- 在开漏总线上,驱动 `LOW` 是一个主动动作(将线路拉至地),而驱动 `HIGH` 是一个被动释放(让上拉电阻将线路恢复到正电压)。
- 如果一个设备拉 `LOW`,而另一个设备释放为 `HIGH`,则线路变为 `LOW`。也就是说:
- **拉低总是覆盖释放**。
- 因此,总线在逻辑上等同于将所有设备的输出进行“与”运算。这个特性称为“线与逻辑”,它是实现仲裁的电气基础。
#### 解决冲突
- 每个发送中的控制器都必须在发送时监视 `SDA`。
- 在每个位上,控制器比较它试图发送的内容与 `SDA` 线上实际出现的内容。
- 如果匹配,则无冲突,控制器继续发送。
- 如果控制器试图发送 `HIGH` 但读回的是 `LOW`,则说明另一个控制器正在拉低,该控制器失去仲裁并停止驱动总线,等待下一个“停止条件”,然后再次尝试申请总线。
- 获胜的控制器永远不会知道发生了冲突。它在线路上看到自己的位,并继续其未受损坏的交易。
- 总之,开漏使仲裁成为电气设计的被动结果,而非主动的协议特性。
> 两个控制器都可以通过在“起始条件”期间将 `SDA` 拉低来成功申请总线。冲突在紧随其后的地址传输期间解决:当两个控制器逐位输出它们的目标地址时,它们第一个不同的位就是仲裁的决胜点:
> - 试图发送 `HIGH` 而另一个发送 `LOW` 的控制器检测到不匹配并退出。
> - 获胜的控制器继续传输,甚至不知道发生了仲裁。
> 如果两个控制器在整个交易过程中发送了**相同**的位呢?
> - 两者都不会检测到冲突!
> - 两个控制器都看到总线与它们发送的每一位相匹配,都认为它们拥有总线,并且都成功完成了交易!
> - 规范实际上指出了这一点:
>> 如果发送内容相同,两个控制器实际上可以完成整个交易而不会出错。
> - 这不是一个缺陷,而是设计的故意特性。目标仍然收到一个连贯的交易,两个控制器驱动总线的事实是不可见的且无害的,因为它们发送给目标的信号是相同的。
> - 出于其他原因(我不在此赘述),规范建议多控制器系统应设计其软件协议,以尽可能避免相同传输的场景,或者接受相同交易是无害的重复。
随着我们稍后探索这些概念,这一点会变得更加清晰。如果现在还不明白,请稍后再回头重读这一部分!
## 上拉
- 拉**低**一条线通常比拉**高**一条线快得多。
- 上拉时间是总线电容和上拉电阻值的函数。
- 这些电阻的典型值为 1-10 kΩ。上拉电阻是一种折衷:
- 高电阻会增加上拉线路所需的时间,从而限制最大总线速度。
- 较低的电阻可实现更快的通信,但需要更高的功耗。
## 速度
| 模式 | 速度 |
|------|------|
| 标准模式 | 100 kbps |
| 快速模式 | 400 kbps |
| 快速+模式 | 1 Mbps |
| 高速模式 | 3.4 Mbps |
| 超快模式 | 5 Mbps |
- I2C 可以以不同的总线速度运行,通常称为模式。
- 硬件被指定为符合其理论上能达到的最高模式。
- 超快模式是只写的,并对协议进行了一些修改。
- 高速模式设备向后兼容较低速度。
## 角色
- 在 I2C 中,有些角色不会改变,我称之为“固定角色”。
- 固定角色描述了谁控制总线。
- 还有一些角色会根据交易类型(读/写)**发生变化**,我称之为“交易角色”。
- 交易角色描述了在交易过程中谁在说话,谁在监听。
- 控制器始终是控制器,目标始终是目标,但它们可能是发送者或接收者,具体取决于当前交易(读/写)的方向。
## 固定角色
- 这些角色由硬件/软件设计决定,在交易期间不会改变。
### 控制器
- 控制器是在 I2C 交易期间“拥有”总线的设备。
- 控制交易何时开始(`START` 条件)和何时停止(`STOP` 条件)。
- 决定交易是“读”还是“写”。
- 驱动时钟线 `SCL`。
- 选择哪个目标将参与交易。
> 注意:这原本被称为“Master(主设备)”,在许多解释和库中仍如此称呼。目前正在努力将其改为“Controller(控制器)”,这也是我选择使用的术语。
### 目标
- 目标是一种被动参与者,从不会主动发起任何操作。
- 持续监听其地址,以便参与由控制器发起的交易。
- 仅在控制器寻址时响应。
- 从不触碰时钟线 `SCL`,**除了**有时执行 [时钟拉伸](https://rana-emaan.com/read/notes/i2c/#clock-stretching)。
> 注意:这原本被称为“Slave(从设备)”,在许多解释和库中仍如此称呼。目前正在努力将其改为“Target(目标)”,这也是我选择使用的术语。
## 交易角色
- 这些角色是控制器或目标在交易中可能承担的角色,**取决于**交易是“读”还是“写”。
### 发送者
- 实际驱动数据线 `SDA` 并发送数据的设备。
- 发送者是:
- `读`:目标
- `写`:控制器
### 接收者
- 读取数据线 `SDA` 的设备。
- 接收者是:
- `读`:控制器
- `写`:目标
## R/W 位
- 这是控制器在交易期间设置的一个位,决定执行“读”还是“写”,进而决定交易角色(谁是发送者,谁是接收者)。
- `读`:1
- 控制器希望从目标接收数据。
- `写`:0
- 控制器希望向目标发送数据。
### 读
- 控制器 `读`:
- 发送者:目标
- 接收者:控制器
> 注意:在控制器 `读` 期间,控制器仍保持其固定角色(控制 `SCL`、发起交易),但承担了接收者的交易角色,而目标则作为发送者拥有 `SDA`。
### 写
- 控制器 `写`:
- 发送者:控制器
- 接收者:目标
## ACK/NACK 位
- 这是交易中每个字节数据之后的“握手”位。
- 在 **数据传输** 期间,`ACK/NACK` 位**始终**由交易的接收者设置,接收者可以是控制器**或**目标。
- 在 **目标选择** 期间(在任何数据传输之前),`ACK/NACK` 位**始终**由目标设置,无论交易是“读”还是“写”,也不管目标是发送者还是接收者。
### ACK
- 代表 `ACKnowledge`(确认)。
- 拉低至 0,这在开漏系统中是主动操作。
### NACK
- 代表 `Negative ACKnowledge`(否定确认)。
- 释放为高电平 1,这在开漏系统中是被动操作或“无操作”。
#### 读
- 在“读”交易期间,控制器是接收者,并在数据传输期间设置 `ACK/NACK` 位。
- 当控制器设置 `NACK` 位时,它**不**表示错误(这种情况仅发生在“读”交易中)。
- 在这种情况下,`NACK` 告诉发送者(这里是目标)停止向控制器发送数据。
#### 写
- 在“写”交易期间,目标是接收者,并在数据传输期间设置 `ACK/NACK` 位。
- 当目标在目标选择**或**数据传输期间设置 `NACK` 位时,表示发生了错误。
## 信号
- I2C 总线上的所有设备持续监视 `SDA` 和 `SCL`。
- 每个设备都需要知道交易何时开始和结束,以便适时参与。
## 数据有效性
- “数据有效性”是控制数据位传输的规则。
- I2C 规范定义“数据有效性”为:
> - `SDA` 线上的数据必须在时钟 `HIGH` 期间保持稳定。数据线 `SDA` 的 `HIGH` 或 `LOW` 状态只能在 `SCL` 线上时钟信号为 `LOW` 时改变。
## 条件
- “数据有效性”规则能够为两个特殊的 `START` 和 `STOP` 条件创造信号空间。
- 这些条件故意违反“数据有效性”规则,以便明确地指示 I2C 交易的 `START` 和 `STOP`。
- `START` 和 `STOP` 条件是 `SDA` 在 `SDA` 为 `HIGH` 时发生的跳变。
- 当设备在 `SCL` 为 `HIGH` 时检测到 `SDA` 跳变,它知道这不可能是数据位,并将其解释为:
- `START`(如果 `SDA` 下降)
- `STOP`(如果 `SDA` 上升)
### 起始条件
- “`START` 条件”发生在 `SDA` 被拉低而 `SCL` 仍为 `HIGH` 时。
- “`START` 条件”发生后,`SCL` 被拉低,交易开始。
### 停止条件
- “`STOP` 条件”发生在 `SDA` 被释放为 `HIGH` 且 `SCL` 也为 `HIGH` 时。
- “`STOP` 条件”发生后,交易完成,I2C 总线空闲,可供申请。
## 时钟拉伸
- “时钟拉伸”是一种机制,允许目标设备通过将 `SCL` 保持为 `LOW` 来暂停 I2C 交易。
- 通常控制器控制时钟 `SCL`,它驱动时钟,目标响应。
- 由于两条线都是开漏的,目标可以将 `SCL` 保持为 `LOW` 来暂停控制器。
- 如果控制器试图释放 `SCL` 为高以开始下一个时钟脉冲,但目标将其保持为 `LOW`,那么 `SCL` 将保持低电平。
- 控制器被设计为在继续之前检查 `SCL` 是否确实变为 `HIGH`;如果发现 `SCL` 没有变高,它会等待。
- 目标将 `SCL` 保持为 `LOW` 所需的时间,然后将其释放为 `HIGH`,允许交易正常进行。
- 目标在需要额外时间处理或准备数据时可能使用“时钟拉伸”。
> 注意:并非所有 I2C 控制器都支持时钟拉伸。如果目标拉伸时钟而控制器不支持,可能会导致问题。
## 帧
- “帧”可以定义为 I2C 协议中传输的基本单元,始终由 9 位组成,其中最后一位是 `ACK/NACK` 位。
- “帧”这个术语并非 I2C 规范的“官方”用语,但在理解和讨论 I2C 时经常被使用。
- 有两种类型的帧,虽然都由 9 位组成,但第一个字节的构成略有不同:
1. 目标地址帧
2. 数据帧
## 目标地址帧
- “目标地址帧”始终是任何 I2C 交易中的**第一个**帧。
- 它确定哪个目标将参与交易,以及将发生何种类型的交易(读/写)。
### 目标地址
- “目标地址帧”的前 7 位是“目标地址”。
- I2C 上的每个目标必须有一个固定地址。
- 地址通常为 7 位(较少使用 10 位),先发送最高有效位(MSB)。
- 地址对于设备来说是硬编码的,并且可能(部分)通过外部地址线或跳线进行配置。
- 例如,MPU-6050 的默认 I2C 地址是 `0x68`,但可以通过焊接设备上的跳线配置为 `0x69`。
- 如果我们想在 I2C 总线上放置多个相同的设备,以便将其区分为两个不同的目标,这非常有用。
- 由于地址是 7 位,地址范围是 128 个唯一地址,从 `0x00` 到 `0x7F`。
- 实际上,其中一些地址被 I2C 规范保留。
### R/W 位
- “目标地址帧”中的第 8 位是“R/W 位”。
- I2C 交易可以是“读”或“写”。这通过设置“R/W 位”来决定。
- `读`:1(释放为 `HIGH`)
- `写`:0(拉低)
### ACK/NACK 位
- “目标地址帧”中的第 9 位(最后一位)是“`ACK/NACK` 位”。
- “目标地址帧”中的 `ACK/NACK` **始终**由目标设置。
## 数据帧
- “数据帧”包含 I2C 交易的实际数据有效载荷。
- I2C 交易中的第一个“数据帧”始终紧跟在“目标地址帧”之后。
### 数据字节
- “数据字节”是 8 位,先发送最高有效位(MSB),**就是**实际的数据有效载荷。
- 如前所述,数据由发送者(取决于 R/W 位,控制器或目标)发送。
> - 注意 `ACK/NACK` 由接收者设置,但“数据字节”由发送者设置。
- 这些数据字节可能都是数据,但通常其中一个字节指示目标设备中的内部地址或寄存器位置。
- 例如,如果控制器想要向目标中的特定寄存器写入某个值,数据字节可能是该寄存器的地址或位置,第二个数据字节则是要写入该位置的实际数据。
- 在许多情况下,一个 I2C 交易中会发送多个数据字节。
- 额外的字节简单地连接到前一个字节之后,中间用 `ACK` 位分隔。稍后会更清楚地说明这一点。
### ACK/NACK 位
- “数据帧”中的第 9 位(最后一位)是“`ACK/NACK` 位”。
- 由交易中的接收者设置。
## 交易
- 此图一目了然地展示了 I2C 交易。
- 交易始终遵循相同布局:
1. 起始条件
2. 目标地址帧
3. 数据帧
4. 重复起始 (https://rana-emaan.com/read/notes/i2c/#repeated-start)(可选)
5. 停止条件
## 写
- 此图具体展示了 I2C “控制器 `写`”,并说明了是控制器还是目标设置了数据线 `SDA` 上的位。
## 多次写
`START` 目标地址 `W` `ACK` 数据字节 `ACK` ... 数据字节 `ACK` `STOP`
> 注意:与“多次读”不同,在一连串的 `写` 中,目标是 `ACK/NACK` 位的设置者,并且它**不**以 `NACK` 终止写序列,而是进行 `ACK` 和 `STOP`。
## 代码
```c
#define MPU6050_ADDRESS 0x68
#define TICKS (1000 / portTICK_PERIOD_MS) // 1 秒
void write(uint8_t reg, uint8_t data) {
i2c_cmd_handle_t cmd = i2c_cmd_link_create(); // 命令链
```
相似文章
关于Intel 8086处理器算术逻辑单元的笔记
详细的技术分析,探讨Intel 8086处理器算术逻辑单元(ALU)的控制电路,解释微码和控制信号如何协调执行28种不同操作。
Intel 386标准单元逻辑中的异常电路
探索Intel 386标准单元逻辑中的异常电路,包括大型多路复用器和非标准反相器,突出芯片的设计历史和自动布局布线技术。
这里有龙:386芯片中防止静电损伤、闩锁效应和亚稳态的设计
对Intel 386处理器I/O电路的详细逆向工程分析,解释了它如何处理静电、闩锁效应和亚稳态以保护芯片。
关于奔腾处理器微码电路的笔记
对奔腾处理器微码ROM的详细剖析,描述其结构、容量以及微码如何在硬件层面实现机器指令。
Intel 8087浮点芯片的指令解码
对Intel 8087浮点协处理器指令解码的详细逆向工程分析,解释主CPU与协处理器之间的交互、微码ROM的使用以及总线接口单元。