I2C 同步总线
I2C 同步总线
一、概述
I2C(Inter-Integrated Circuit,集成电路间总线)是 Philips 提出的同步、半双工、多主串行总线,仅用 SDA(数据)和 SCL(时钟)两根线即可挂接多个设备,广泛用于传感器、EEPROM、RTC、显示屏等低速外设。
一句话核心:所有设备共挂 SDA/SCL 两根线,主机发起起始条件,按7 位从机地址寻址,通过ACK 应答确认通信,最后发停止条件结束——多主机用仲裁决定谁用总线。
二、两线结构与开漏
| 信号 | 作用 |
|---|---|
| SDA | 串行数据线(双向,开漏) |
| SCL | 串行时钟线(主机驱动,开漏) |
为什么要用"开漏 + 上拉电阻"?
普通 GPIO 用推挽输出:既能主动拉高、也能主动拉低。但如果多个设备共用一条线、都用推挽,就会"打架"——一个想发 0(拉低)、另一个想发 1(拉高),两个互相顶,轻则数据错乱、重则损坏引脚。
I2C 改用开漏输出(Open-Drain)来避免冲突:
| 设备内部动作 | 引脚电平 | 由谁决定 |
|---|---|---|
| MOS 导通(接地) | 低(0) | 设备自己"拉低" |
| MOS 断开(悬空) | 高(1) | 外部上拉电阻把引脚拉到 VCC |
也就是说,开漏设备只能拉低、不能主动拉高;引脚被拉高,全靠外部上拉电阻。所以每个设备只有两种动作:拉低 或 释放。
"线与"(Wire-AND)机制
因为所有设备都只是"拉低 / 释放",多根线并在一起就形成"线与":
- 任何一个设备拉低 → 整条总线就是低;
- 所有设备都释放 → 总线才被上拉成高。
| 设备 A | 设备 B | 总线电平 |
|---|---|---|
| 拉低 | 拉低 | 低 |
| 拉低 | 释放 | 低 |
| 释放 | 拉低 | 低 |
| 释放 | 释放 | 高(上拉) |
连接示意:
VCC
│ 上拉电阻(SDA 和 SCL 各接一个到 VCC)
├─┬─┬─┬─┬─┐
│ │ │ │ │ │
SDA═══╪═╪═╪═╪═══ 任一设备可拉低
│ │ │ │ │ │
SCL═══╪═╪═╪═╪═══
主机1 从机1 从机2... (全部共线,都开漏)为什么这对 I2C 很关键:
- 多设备共享:开漏 + 线与,天然允许任意多个设备挂同一条总线,只要它们不同时强推;
- 多主仲裁:两个主机同时发 1 和 0 时,总线实际是 0(被拉低),发 1 的那方读到不匹配就主动退出(见第七节仲裁);
- 电平自由:上拉电阻接到不同电压(如 5V 或 3.3V),总线电平随之不同,便于不同供电的外设共线。
三、寻址(7 位地址)
I2C 用 7 位从机地址(地址不足时可扩展为 10 位)。地址在起始条件后由主机发出,紧跟一个读写位(R/W):
起始条件 │ 7位地址(MSB先发) │ 读写位(0=写/1=读) │ ACK7 位地址结构:7 位地址占 bit6~bit0,范围 0x00~0x7F。主机实际发出的地址是一个 8 位字节 = 7 位地址 + 1 位 R/W,MSB 先发:
| 位号 | b7 | b6 | b5 | b4 | b3 | b2 | b1 | b0 |
|---|---|---|---|---|---|---|---|---|
| 含义 | 地址 bit6 | bit5 | bit4 | bit3 | bit2 | bit1 | bit0 | R/W |
| 写字节 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
| 读字节 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 1 |
7 位地址 → 8 位字节的换算(这是最容易搞混的地方):
- 写:
地址字节 = (7位地址 << 1) | 0 - 读:
地址字节 = (7位地址 << 1) | 1
数值演算(EEPROM 0x50):
- 7 位地址
0x50 = 101_0000; - 写字节:
0x50 << 1 | 0 = 1010_0000 = 0xA0; - 读字节:
0x50 << 1 | 1 = 1010_0001 = 0xA1。
所以 0xA0(写)和 0xA1(读)其实是同一个 7 位地址 0x50,只是最低位的 R/W 不同——手册给"两个地址",别当成两个设备。
为什么手册常直接给 8 位地址:很多芯片手册(如 EEPROM)直接写"写地址 0xA0、读地址 0xA1",把 7 位地址和 R/W 位拼成的字节当作"地址"给出来。实际在代码里通常写 0x50 << 1 | 0(写)或 0x50 << 1 | 1(读),或直接写 0xA0/0xA1。
常见设备地址示例:
- AT24C02 EEPROM:7 位地址
0x50(写0xA0、读0xA1) - SHT 温湿度传感器系列:
0x44/0x45 - BMP280 气压计:
0x76(或0x77)
10 位地址(扩展):当 7 位地址不够用时,用 10 位。发送时用两个字节:第 1 个字节 = 11110 + 地址高 2 位 + R/W(前 5 位固定 11110),第 2 个字节 = 剩余 8 位地址,每个字节后各跟一个 ACK。
例子:10 位地址 0x2AB(高 2 位 = 10,低 8 位 = 1010_1011)
| 字节 | 内容 | 位序列 | 十六进制 |
|---|---|---|---|
| 第 1 字节 | 11110 + 高 2 位 10 + R/W | 写:1111_0100、读:1111_0101 | 写 0xF4、读 0xF5 |
| 第 2 字节 | 低 8 位地址 | 1010_1011 | 0xAB |
数值演算:
- 第 1 字节(写):
11110+10+0=1111_0100 = 0xF4; - 第 1 字节(读):
11110+10+1=1111_0101 = 0xF5; - 第 2 字节:
1010_1011 = 0xAB。
所以寻址 0x2AB 时,主机发送:0xF4(写)/0xF5(读)→ 等 ACK → 0xAB → 等 ACK。多数简单外设(EEPROM、传感器)只用 7 位地址,用不到 10 位。
同型号多设备怎么区分(A0/A1/A2 引脚):很多芯片(如多片 EEPROM)的地址低 3 位由外部引脚 A0/A1/A2 的电平决定。一片总线上,同型号设备可编 2^3 = 8 个不同地址(如 0x50~0x57)。
如何获取设备地址:
- 查数据手册:每种芯片的地址是固定值或由引脚决定,手册里都会给出(如 EEPROM
0x50、BMP2800x76); - 看硬件接线:可编程地址的芯片,地址由 A0/A1/A2 引脚电平决定,看原理图上这些引脚接高(VCC)还是接低(GND);
- 用 I2C 总线扫描:不确定时,写个扫描程序——依次向
0x00~0x7F每个地址发"起始 + 地址 + 等 ACK",能收到 ACK 的地址就是在线的设备。这是最常用、最可靠的找地址方法。
for (uint8_t addr = 0; addr < 0x80; addr++) {
I2C_Start();
if (I2C_SendByte(addr << 1 | 0) == 0) // 返回 0 = 收到 ACK
printf("找到设备:0x%02X\n", addr);
I2C_Stop();
}设备地址是否会冲突:
- 会冲突:如果总线上有两个设备用了相同的 7 位地址,主机无法区分——发地址时两者都会 ACK,读写会串到错误的设备;
- 常见冲突场景:① 两块同型号 EEPROM,且 A0/A1/A2 接成相同电平;② 两块地址固定且无法改的传感器(如都默认
0x48的 ADC); - 如何避免:① 用 A0/A1/A2 引脚给同型号设备编不同地址;② 选地址互不相同的不同型号设备;③ 冲突实在无法避免时,用 I2C 复用器(MUX) 或多路独立总线隔离;④ 设计阶段先跑一遍地址扫描,确认各设备地址唯一。
关键理解:地址字节 = (7 位地址 << 1) | R/W。手册给的"8 位地址"已含 R/W 位,7 位地址 = 8 位地址右移 1 位(如
0xA0 >> 1 = 0x50)。代码里要分清:0x50 << 1 | 0是写、0x50 << 1 | 1是读。
四、起始条件、停止条件、ACK
4.1 起始条件(START)
SCL 为高时,SDA 由高变低 → 总线开始传输。
SCL: ──── 高 ────┐ ┌──────
└──────────┘
SDA: ── 高 ──┐ ┌────────
└── 低 ───────┘
(SDA 下降时 SCL 仍为高 → 被识别为 START)举例说明:结合第二节的开漏结构——总线空闲时,SDA 和 SCL 都被上拉电阻拉到高电平。主机要发 START,就在 SCL 保持为高的这一拍把 SDA 拉低:
| 时刻 | SCL | SDA | 说明 |
|---|---|---|---|
| 空闲 | 高 | 高 | 总线空闲,SDA/SCL 均被上拉为高 |
| START 瞬间 | 高 | 高 → 低 | SCL 为高时 SDA 被拉低,产生起始条件 |
关键点:必须"SCL 为高 + SDA 由高变低"同时成立才被识别为 START。若 SDA 是在 SCL 为低时变化的,只会被当成普通数据位翻转,不算起始条件。
4.2 停止条件(STOP)
SCL 为高时,SDA 由低变高 → 总线结束传输。
SCL: ──── 高 ────┐ ┌──────
└──────────┘
SDA: ┌── 高 ─────────
── 低 ─┘
(SDA 上升时 SCL 仍为高 → 被识别为 STOP)举例说明:停止条件是传输结束时,主机在 SCL 保持为高的这一拍把 SDA 释放/拉高(开漏下即不再拉低,由总线恢复为高):
| 时刻 | SCL | SDA | 说明 |
|---|---|---|---|
| 传输中 | 高 | 低 | 数据 / ACK 传输中 |
| STOP 瞬间 | 高 | 低 → 高 | SCL 为高时 SDA 拉高,产生停止条件 |
| 结束后 | 高 | 高 | 总线回到空闲,可被其他主机使用 |
关键点:STOP 后总线释放回空闲(SDA/SCL 都被上拉为高),其他主机才能仲裁占用总线。与 START 类似,"SCL 为高 + SDA 由低变高"才被识别为 STOP;传输中 SDA 只在 SCL 为低时变化,所以 SCL 高时的 SDA 上升沿不会被误认为数据。
关键:起始/停止都发生在 SCL 为高时 SDA 跳变。而传输数据位时,SDA 只能在 SCL 为低时变化(SCL 为高时 SDA 必须保持稳定)——否则会被误判成起始/停止条件。这是 I2C 的时序铁律。
4.3 ACK / NACK
每传输完 1 字节(8 位),接收方在第 9 个时钟拉低 SDA(ACK)表示收到:
SCL: ┌┐┌┐┌┐┌┐┌┐┌┐┌┐┌┐ ┌┐ ← 第 1~8 个时钟传数据,第 9 个时钟回 ACK
SDA: ──── 8 个数据位 ────┐
└── 低 = ACK(收到,继续)/ 高 = NACK(结束)- ACK(低):接收方正常接收,继续下一字节;
- NACK(高):接收方不想收/收不了,或读操作的最后 1 字节(表示"读够了,发 NACK 结束")。
关键理解:ACK/NACK 本质就是第 9 个时钟里 SDA 上的一位电平——ACK(低)= 传一个 0,NACK(高)= 传一个 1。它们和普通数据位一样遵循 I2C 时序(SCL 为高时 SDA 稳定、SCL 为低时才能变化),只是在"第 9 个时钟"这个特定位置被赋予了"确认"的含义,主机不需要关心这"一位 0/1"会被当数据,只按"低 = 收到、高 = 没收到/结束"来解读即可。
ACK / NACK 由谁发:
| 场景 | 谁在第 9 个时钟控制 SDA | ACK(低) | NACK(高) |
|---|---|---|---|
| 主机写从机 | 从机回 | 收到,继续下一字节 | 从机忙 / 收不了 |
| 主机读从机 | 主机回 | 继续发下一字节 | 读到最后一字节,结束 |
注意方向:写时是从机确认(主机发、从机说"收到");读时是主机确认(从机发、主机说"继续/够了")。读最后一字节回 NACK,从机看到高电平就知道"该停了"。
完整事务时序(写 1 字节:START → 地址字节 → ACK → 数据字节 → ACK → STOP):START 是帧头、STOP 是帧尾,中间的"每 1 字节 + 1 个 ACK"重复出现:
SCL: START │ ── 地址字节(8 位)── │ ACK │ ── 数据字节(8 位)── │ ACK │ STOP
SDA: │ [7位地址+R/W] │ 低 │ [数据] │ 低 │小结:START/STOP 界定一次事务的起止(都发生在 SCL 为高时 SDA 跳变);ACK/NACK 是每个字节后的"确认位"(第 9 个时钟)。数据只在 START 与 STOP 之间、且只在 SCL 为低时变化。
五、数据读写时序
5.1 写操作(主机写从机)
START │ [7位地址][W=0] │ ACK │ [寄存器地址] │ ACK │ [数据] │ ACK │ STOP
主发 │ 主发 │ 从回│ 主发 │ 从回│ 主发 │ 从回 │ 主发方向标注:写操作全程都是主机发、从机收——
[7位地址]、[寄存器地址]、[数据]三段都由主机驱动 SDA,而三个ACK都由从机在第 9 个时钟拉低 SDA 回应(见 4.3 节"主机写从机 → 从机回")。主机每发完一段就释放 SDA,交给从机回 ACK;若从机拉低 = ACK(收到),拉高 = NACK(忙/收不了)。
扩展:连续写多字节:写操作可一次写入连续寄存器,中间不停顿——
START │ [地址][W=0] │ ACK │ [起始寄存器地址] │ ACK │ [数据1] │ ACK │ [数据2] │ ACK │ … │ [数据N] │ ACK │ STOP,每字节后从机各回一个 ACK,寄存器地址由从机内部自增。若需换到别的寄存器,则要先发 STOP 再重新 START。
寄存器地址从哪来(与 7 位从机地址别混淆):
| 概念 | 是什么 | 从哪来 |
|---|---|---|
| 从机地址(7 位) | 总线上"选哪个设备",如 0x50 | 设备固定 / A0A1A2 引脚决定 |
| 寄存器地址 | 设备内部"访问哪个寄存器/存储单元",如 0x05 | 芯片数据手册 |
寄存器地址是芯片内部的编址空间,必须查数据手册(Datasheet)的"寄存器映射表(Register Map)"获取:
- EEPROM(存储芯片):寄存器地址 = 存储单元地址。如 AT24C02 容量 2Kbit = 256 字节,地址范围
0x00~0xFF,例中的0x05就是第 6 个字节。更大容量(如 AT24C16 为 2KB)时,高地址位可能并入从机地址字节,需查手册确认。 - 传感器/外设(如 BMP280、SHT):寄存器地址由手册定义,每个地址对应固定功能和含义。如 BMP280 的
0xF4是控制寄存器、0xFA~0xFC是温度数据。想改什么配置、读什么数据,就到手册对应寄存器表里找地址。 - 官方驱动/例程:驱动里写死的
#define XXX_REG_TEMP 0xFA这类地址,本质也来自手册。 - 地址宽度:多数 I2C 芯片寄存器地址是 1 字节(0x00~0xFF);个别大容量 EEPROM(如 24C256)用 2 字节地址,发送时先高位后低位。
例:向 EEPROM 地址 0x05 写 1 字节 0x2A
START → 发送 0xA0(地址+写) → 等 ACK
→ 发送 0x05(寄存器地址) → 等 ACK
→ 发送 0x2A(数据) → 等 ACK
→ STOP逐位演算 1:发送地址字节 0xA0(7 位地址 0x50 + 写位 0,即 0x50 << 1 | 0 = 0xA0 = 1010_0000)
I2C 地址字节 = 7 位地址(MSB 先发)+ 1 位读写位。8 个时钟发完,第 9 个时钟等从机 ACK:
| 时钟 | SDA 发送位 | 说明 |
|---|---|---|
| 1 | 1 | 地址 bit6 |
| 2 | 0 | 地址 bit5 |
| 3 | 1 | 地址 bit4 |
| 4 | 0 | 地址 bit3 |
| 5 | 0 | 地址 bit2 |
| 6 | 0 | 地址 bit1 |
| 7 | 0 | 地址 bit0 |
| 8 | 0 | 读写位(0 = 写) |
| 9 | (释放 SDA) | 从机回 ACK(拉低) |
发送位序列 1 0 1 0 0 0 0 0 = 0xA0,后面跟第 9 位的 ACK。
逐位演算 2:发送数据字节 0x2A = 0010_1010
同样 MSB 先发,8 个时钟发送 + 第 9 个时钟等 ACK:
| 时钟 | SDA 发送位 |
|---|---|
| 1~8 | 0 0 1 0 1 0 1 0 |
发送位序列 0 0 1 0 1 0 1 0 = 0x2A,随后从机回 ACK。
5.2 读操作(主机读从机)
读操作需要先写寄存器地址(写模式),再切换为读模式。前半段与写操作一样:START → 地址[W=0] → ACK → 寄存器地址 → ACK;然后用重复起始条件(Repeated START)把方向切到读,后半段改为从机发数据、主机收:
START │ [地址][W=0] │ ACK │ [寄存器地址] │ ACK │[重复START]│ [地址][R=1] │ ACK │ [数据] │ NACK │ STOP
主发 │ 主发 │ 从回 │ 主发 │ 从回 │ 主发 │ 主发 │ 从回 │ 从发 │ 主回 │ 主发方向标注(读操作方向会切换,务必看清):
- 前半段(写寄存器地址):
[地址][W=0]、[寄存器地址]都由主机发,两个ACK由从机回——与 5.1 写操作完全一样;- [重复START]:主机不释放总线地再次发 START,把方向从"写"切到"读",不需要先发 STOP(一旦 STOP,总线被释放,再读就得重新寻址);
- 后半段(读数据):
[地址][R=1]由主机发(从机回 ACK),随后方向反转——[数据]由从机发、主机收,最后的ACK/NACK由主机回:主机还要下一字节就回 ACK(拉低),读完最后一个字节就回 NACK(拉高),从机看到 NACK 就知道"该停了"。
例:从 EEPROM 地址 0x05 读 1 字节(寄存器地址 0x05 从芯片手册的存储单元编址获取,见 5.1 节说明)
START → 发送 0xA0(写) → ACK → 发送 0x05(地址) → ACK
→ 重复START → 发送 0xA1(读) → ACK → 读 1 字节 → 发 NACK → STOP扩展:连续读多字节:重复 START 后,只要主机每收一字节回 ACK,从机就会继续发下一字节(寄存器地址自动自增);主机回 NACK 即表示"读够了",随后发 STOP。读 N 字节的标准收尾是"前 N-1 字节回 ACK、最后 1 字节回 NACK"。
关键理解:读操作的核心是用重复起始条件切换方向,且数据段的 ACK/NACK 由主机发(方向反转),这与写操作"ACK 都由从机回"正好相反——见 4.3 节"主机读从机 → 主机回"。
六、标准/快速/高速模式
| 模式 | 速率 | 实现要点 |
|---|---|---|
| 标准模式(Standard) | 100 kbit/s | 常规上拉(典型 10kΩ)即可 |
| 快速模式(Fast) | 400 kbit/s | 需较小上拉(典型 4.7kΩ),SMBus 兼容 |
| 快速+(Fast Plus) | 1 Mbit/s | 需更强上拉驱动(1~2kΩ 甚至增强电流源) |
| 高速模式(High-speed) | 3.4 Mbit/s | 需激活主编码 + 增强电流源 + 改变电平阈值,不能简单设快时钟 |
高速模式(3.4 Mbit/s)为何特殊:它不能靠把 SCL 时钟设快就达到,必须满足三件事:① 主机在 START 后先发送一个特殊 8 位主编码
00001xxx把总线"激活"到高速状态;② 上拉灌电流能力必须增强(普通上拉电阻驱动不了这么快的上升沿);③ 高速阶段的总线电平阈值会改变。因此实际项目中,3.4 Mbit/s 很少用,绝大多数场景用 100k/400k/1M。
共线设备取最低速率:总线上所有设备必须都支持同一模式才能按该模式运行。若线上同时挂了只支持 100 kHz 的旧从机,整体就必须按 100 kHz 跑,不能单独把别的设备提速——否则慢速从机跟不上,通信失败。
速率怎么配置(决定 SCL 频率的两层):
| 层面 | 手段 | 说明 |
|---|---|---|
| 硬件(上拉电阻 R) | 选合适上拉阻值 | SCL 上升时间受 T≈R×C(C 为总线电容,由线长/设备数决定)限制。阻值过大 → 上升沿太缓 → 高频跟不上。100k 用 10kΩ、400k 用 4.7kΩ、1M 用 1~2kΩ |
| 软件(延时/分频) | 控制 SCL 高/低电平时长 | 软件模拟 I2C 靠延时:半周期 T/2 = 1/(2×f),如 400kHz → 半周期 1/(2×400000) = 1.25 μs。硬件 I2C 靠时钟分频:f_SCL ≈ f_PCLK / 分频系数,具体见 MCU 手册 |
数值演算(写 1 字节耗时,标准模式 100kHz):
| 计算项 | 代入计算 | 结果 |
|---|---|---|
| 1 位时长 | 1 / 100000 | = 10 μs |
| 1 字节 = 8 数据位 + 1 ACK = 9 位 | 9 × 10 μs | = 90 μs |
地址字节 0xA0(7 位地址 + 写位) | 1 × 90 μs | = 90 μs |
寄存器地址字节 0x05 | 1 × 90 μs | = 90 μs |
数据字节 0x2A | 1 × 90 μs | = 90 μs |
| 完整写序列(地址 + 寄存器地址 + 数据) | 3 × 90 μs | ≈ 270 μs |
起始/停止条件占的时间通常远小于 1 字节,可忽略。不过向 EEPROM 写完后,还要等它内部写周期(典型 5 ms)真正落盘,这段时间远比 270 μs 长,才是写操作的真正瓶颈。
七、多主仲裁(Arbitration)
多个主机同时想用总线时,通过仲裁决定谁先用。仲裁是逐位进行的硬件过程,不需要主机软件额外协调。
仲裁的触发时机:当两个(或以上)主机在同一时刻都发出 START 并开始发地址字节时,就会发生仲裁。现实中常因多个主机的时钟/事务恰好同时启动而撞车。
仲裁过程:
- 每个主机发送每一位时,都同时监听 SDA(硬件在发的同时采样总线);
- 若自己发的位是 1,但读到 SDA 实际是 0(说明被别的设备拉低了),则判定仲裁失败,立即停止发送、退出总线;
- 若自己发的位是 0,即使别人也发 0,总线仍是 0,双方都不退出,继续下一位;
- 直到某个位分出胜负(一位发 1、另一位发 0),发 1 的那方退出,发 0 的那方继续完成剩余发送;
- 赢得仲裁的主机不被打断,正常走完整个事务(发地址 → ACK → 数据 → STOP);失败的主机稍后重新发起。
逐位演算:两个主机同时发地址,谁赢?
| 位序 | 主机 A 发 | 主机 B 发 | SDA 实际电平(线与) | 谁退出 |
|---|---|---|---|---|
| 第 1 位 | 0 | 0 | 0(都拉低) | 都不退出,继续 |
| 第 2 位 | 0 | 1 | 0(A 拉低) | B 退出(发 1 读到 0,失败) |
| 第 3 位 | — | — | — | A 独占总线,继续发剩余位 |
- A 发
0b00...,B 发0b01...:第 2 位时 B 发 1、读到 0,B 仲裁失败退出,A 胜出继续。 - 这个例子对应"地址更小(更多 0)者获胜":主机 A 的地址前缀
00< 主机 B 的01,所以 A 赢。
如果地址完全相同呢?:仲裁会一路进行到数据/应答阶段,直到某一位分出胜负(例如某主机拉低 SDA 发 0、另一个发 1)。理论上地址相同仍可能撞车,但概率极低,且 I2C 规范保证最终只有一方留下。
关键理解:因为开漏 + 线与,两个主机同时发 1 和 0 时,SDA 实际是 0——发 1 的主机读到总线是 0(不匹配自己发的 1),据此判定失败退出。仲裁的关键是"发 0 者优先、发 1 者让步",本质是利用"线与"让"发 0 的那方天然赢得总线",全程由硬件完成,无需主机软件判断,也不损坏任何设备(开漏不会短路)。
八、I2C vs SPI 对比
| 维度 | I2C | SPI |
|---|---|---|
| 线数 | 2(SDA/SCL) | 4(MOSI/MISO/SCK/CS) |
| 半/全双工 | 半双工 | 全双工 |
| 寻址 | 7/10 位地址 | CS 片选 |
| 多主机 | 支持(仲裁) | 少见 |
| 速率 | 100k/400k/1M | 几十 MHz |
| 应用 | 低速外设/传感器 | 高速外设/Flash/屏 |
九、常见坑与排查
| 问题 | 可能原因 | 排查 |
|---|---|---|
| 找不到设备(NACK 一直) | 地址错、未上拉、从机供电 | 核对 7 位地址,检查上拉电阻 |
| 设备偶发失联 | SCL/SDA 被拉死 | 开漏+上拉确认,避免总线死锁 |
| 读到 0xFF | 地址错或从机未 ACK | 用 I2C 扫描工具找地址 |
| 写 EEPROM 立即读失败 | EEPROM 写周期未完成 | 写后延时(典型 5ms) |
| 软件 I2C 时序不稳 | 延时不对 | 按从机手册保证建立/保持时间 |