I2C 通信协议详解
- 嵌入式开发
- 3小时前
- 10热度
- 0评论
I2C 通信协议(Inter-Integrated Circuit)是飞利浦设计的两线同步串行总线,仅用 SCL(时钟)和 SDA(数据)两根线即可实现一主多从通信。本文结合 ESP32-C3 + SHT40 实战,从 I2C 通信协议时序原理到 MicroPython 代码逐层拆解。
一、I2C 通信时序详解
| 信号 | 解决的问题 | 触发条件 |
|---|---|---|
| START | 通信开始边界 | SCL 高电平期间,SDA 下降沿 |
| 地址帧 | 多设备寻址(7位地址 + R/W位) | START 后紧跟的首个字节 |
| ACK/NACK | 实时可靠性确认 | 每 8 位后的第 9 个时钟周期 |
| STOP | 通信结束边界 | SCL 高电平期间,SDA 上升沿 |
1.1 带外信令:START/STOP 为何能与数据共存于 2 根线
数据传输铁律:SCL 高时 SDA 绝对不允许变化(接收方正在采样)。
飞利浦工程师将「SCL 高时 SDA 变化」这个正常传输中永远不会出现的非法状态重新定义为控制信号:
SCL 高 + SDA 下降沿→ STARTSCL 高 + SDA 上升沿→ STOP
天然无歧义,无需第三根线,不占用任何数据编码空间。这种「借用不可能状态传控制信息」的技巧称为带外信令(Out-of-band Signaling),USB 的 SE0 复位状态、RS-232 的 BREAK 信号均为同类设计。
1.2 整体流程概览
写操作(主机向从机发数据):
主机发出:[START] → [7位地址 + W位(0)] → [寄存器/命令字节] → [数据字节] → [STOP]
从机响应: ↑ACK↑ ↑ACK↑ ↑ACK↑
读操作(主机从从机读数据):
主机发出:[START] → [7位地址 + R位(1)] → (释放SDA) → ... → [NACK] → [STOP]
从机响应: ↑ACK↑ [数据字节] [数据字节]
1.3 START 条件(起始信号)
规则:SCL 高电平期间,SDA 由高变低(下降沿)

1.4 地址帧 + R/W 位(共 8 位)
格式:[A6][A5][A4][A3][A2][A1][A0][R/W]
- 高 7 位:目标从机地址(如 SHT40 =
0x44=1000100) - 第 8 位:
0= 写(Write),1= 读(Read)
每一位的传输规则:
SCL低电平期间,主机改变SDA(允许数据变化)SCL高电平期间,SDA必须稳定(从机在此采样)
SCL: ___/‾‾‾\___/‾‾‾\___/‾‾‾\___
↑采样 ↑采样 ↑采样
SDA: ══[A6]══════[A5]══════[A4]══ (低电平期间可变,高电平期间锁定)
1.5 ACK / NACK(应答位)
每传完 1 字节(8 位) 后,第 9 个时钟周期为应答位,SDA 由接收方驱动:
| 场景 | 谁驱动 SDA | 0(低)= ACK |
1(高)= NACK |
|---|---|---|---|
| 主机写,从机应答 | 从机(主机先释放 SDA) | 「收到,继续」 | 「未识别 / 忙」 |
| 主机读,主机应答 | 主机 | 「继续发下一字节」 | 「读完了,停」 |

主机写 → 从机应答;主机读最后一字节 → 主机发 NACK 通知从机停止。
1.6 STOP 条件(停止信号)
规则:SCL 高电平期间,SDA 由低变高(上升沿)

所有从机检测到这个信号后,知道本次通信结束,总线释放回空闲状态。
1.7 完整时序图:以读 SHT40 为例
SHT40 读取流程分两步:
- 写:发送测量命令
0xFD - 读:读回 6 字节数据

1.8 时序参数速查
| 参数 | 标准模式(100kHz) | 快速模式(400kHz) | 说明 |
|---|---|---|---|
| SCL 频率 | 100 kHz | 400 kHz | 时钟速率 |
| SCL 高电平时间 | ≥ 4.0 μs | ≥ 0.6 μs | 采样窗口 |
| SCL 低电平时间 | ≥ 4.7 μs | ≥ 1.3 μs | 数据变化窗口 |
| START 保持时间 | ≥ 4.0 μs | ≥ 0.6 μs | START 后到第一个 SCL 下降沿 |
| 数据建立时间 | ≥ 250 ns | ≥ 100 ns | SDA 变化到 SCL 上升沿的间隔 |
freq 参数。遇到通信不稳定时,先把频率从 400kHz 降到 100kHz 试试。
二、ESP32-C3 接线
2.1 SHT40 接线
| SHT40 引脚 | ESP32-C3 | 逻辑分析仪通道 |
|---|---|---|
VDD |
3.3V |
— |
GND |
GND |
— |
SDA |
GPIO4 |
ch0 |
SCL |
GPIO5 |
ch1 |
2.2 逻辑分析仪 PulseView 设置
- 采样率:设置为目标频率的 10 倍以上
- 400kHz 快速模式 → 采样率 ≥ 4MHz
- 100kHz 标准模式 → 采样率 ≥ 1MHz
- 经验规则:
采样率 = 通信频率 × 10(奈奎斯特要求 2 倍,10 倍是工程安全裕量)
- 解码器:添加
I2C协议解码器,指定SCL、SDA对应通道 - 运行程序 → 点击「Run」采集 → 观察解码结果

解码结果可以看到:
START条件(SDA 在 SCL 高时下降)- 地址帧:如
0x44 W(写 SHT40)或0x44 R(读 SHT40) - 每字节后的
ACK/NACK位 - 数据字节的十六进制值
STOP条件
三、MicroPython 代码
3.1 扫描总线上的设备
第一步永远是扫描,确认设备已正确连接并响应。
from machine import I2C, Pin
# 初始化 I2C,SDA=GPIO4,SCL=GPIO5,频率 400kHz
i2c = I2C(0, scl=Pin(5), sda=Pin(4), freq=400000)
# 扫描总线,返回所有响应设备的地址列表
devices = i2c.scan()
if devices:
print("发现设备:", [hex(d) for d in devices])
else:
print("未发现任何设备,请检查接线")
正常输出示例:
发现设备: ['0x3c', '0x44']
其中 0x3c 是 OLED,0x44 是 SHT40。
3.2 读取 SHT40 温湿度
from machine import I2C, Pin
import time
i2c = I2C(0, scl=Pin(5), sda=Pin(4), freq=400000)
SHT40_ADDR = 0x44 # SHT40 默认地址
# 发送测量命令:高精度单次测量
i2c.writeto(SHT40_ADDR, bytes([0xFD]))
# 等待测量完成(高精度模式约需 10ms)
time.sleep_ms(10)
# 读取 6 字节原始数据
# 格式:[温度高位, 温度低位, 温度CRC, 湿度高位, 湿度低位, 湿度CRC]
data = i2c.readfrom(SHT40_ADDR, 6)
for byte in data:
print(hex(byte))
# 拼合温度原始值并换算
raw_temp = (data[0] << 8) | data[1]
temperature = -45 + 175 * raw_temp / 65535
# 拼合湿度原始值并换算
raw_hum = (data[3] << 8) | data[4]
humidity = -6 + 125 * raw_hum / 65535
humidity = max(0, min(100, humidity))
print(f"温度:{round(temperature, 1)} 湿度:{round(humidity, 1)}")

3.3 关键波形规则
写操作(主机 → 从机):
- 主机控制
SCL,在 SCL 低电平期间改变SDA - 从机在 SCL 高电平期间采样
SDA - 每字节结束,主机释放
SDA,从机拉低表示 ACK
读操作(从机 → 主机):
- 主机先发地址帧(R位=1),释放
SDA - 从机接管
SDA,逐位输出数据 - 主机在最后一字节后发 NACK,再发 STOP
四、进阶知识
4.1 开漏输出 + 上拉电阻
I2C 总线允许多主机、多从机挂在同一对线上。若使用推挽输出,一个设备输出高、另一个输出低,会直接导致电源短路。
开漏 + 上拉电阻的优势:
| 特性 | 说明 |
|---|---|
| 线与逻辑(Wired-AND) | 任何设备拉低即为低;所有设备释放才被上拉到高,无短路风险 |
| 时钟延展(Clock Stretching) | 从机可拉低 SCL 暂停通信,请求主机等待 |
| 热插拔安全 | 设备接入时不产生浪涌电流冲突 |
典型上拉电阻:4.7 kΩ(标准模式)/ 2.2 kΩ(快速模式)。阻值越小上升沿越快,但静态功耗越大。
4.2 时钟延展(Clock Stretching)
从机在 SCL 变高之前主动将 SCL 拉低,强迫主机等待,直到从机处理完毕才释放 SCL。
- 适用场景:从机需较长时间完成 ADC 转换、EEPROM 写入等操作
- ESP32-C3:硬件 I2C 控制器支持作为主机时检测从机的时钟延展,会自动暂停并等待 SCL 释放
4.3 地址冲突解决方案
同一总线上两个设备地址相同时,数据会发生冲突,可能导致总线永久阻塞。
解决方案:
- 硬件地址引脚:许多芯片有
ADDR引脚,接 VCC 或 GND 可切换地址(如 SHT40:0x44/0x45) - 使用不同总线:ESP32-C3 有两组硬件 I2C,可将冲突设备分开
- I2C Mux:如 TCA9548A,将多个同地址设备接到不同通道,通过通道切换访问
五、I2C vs SPI vs UART
| 特性 | I2C | SPI | UART |
|---|---|---|---|
| 引脚数 | 2(SCL + SDA) | 4+(SCLK/MOSI/MISO/CS×N) | 2(TX + RX) |
| 拓扑 | 一主多从(地址寻址) | 一主多从(片选线寻址) | 点对点 |
| 速率 | 100k / 400kHz | 可达数十 MHz | 典型 115200 bps |
| 全双工 | 半双工 | 全双工 | 全双工 |
| 时钟 | 同步(主机提供 SCL) | 同步 | 异步(需配置波特率) |
| 适用场景 | 低速传感器、EEPROM | 高速外设(屏幕、Flash) | 调试串口、GPS 模块 |
| 设备 | 默认地址 | 备注 |
|---|---|---|
| SHT40 | 0x44 / 0x45 |
温湿度传感器,ADDR 引脚决定 |
| OLED SSD1306 | 0x3C / 0x3D |
OLED 显示屏 |
| BMP280 | 0x76 / 0x77 |
气压/温度传感器 |
| MPU6050 | 0x68 / 0x69 |
六轴 IMU,AD0 引脚决定 |
| EEPROM AT24Cxx | 0x50–0x57 |
A0/A1/A2 引脚决定 |
六、常见问题
Q1:START 和 STOP 信号如何定义?为什么这样设计?
- **START**:SCL 高电平期间,SDA 产生**下降沿**(高→低)
- **STOP**:SCL 高电平期间,SDA 产生**上升沿**(低→高)
正常数据传输铁律是「SCL 高时 SDA 不得变化」,因此「SCL 高 + SDA 变化」在正常通信中是**永远不会出现的非法状态**。飞利浦工程师将这两个非法状态重新定义为控制信号,天然无歧义,无需额外引脚——即**带外信令(Out-of-band Signaling)**。
Q2:I2C 的 ACK / NACK 机制是什么?谁来发送?
每传完 1 字节(8 位)后,第 9 个时钟周期为应答位,由**接收方**驱动 SDA:
| 场景 | 发送方 | ACK(SDA=0) | NACK(SDA=1) |
| :--- | :--- | :--- | :--- |
| 主机写数据 | 从机 | 收到,请继续 | 未识别 / 忙 |
| 主机读数据(最后一字节) | 主机 | 继续发下一字节 | 读完了,停止 |
口诀:**谁接收,谁应答;低电平 = ACK,高电平 = NACK。**
Q3:I2C 为什么使用开漏输出 + 上拉电阻?
多设备共线时推挽输出会短路。开漏结构实现线与逻辑,任何设备均可安全拉低总线,不会与其他设备冲突。同时支持时钟延展,从机可主动暂停通信。
Q4:7 位地址最多支持多少设备?
理论 128 个地址(0x00–0x7F),保留 0x00(广播)、0x01–0x07(特殊用途)、0x78–0x7F(10位地址扩展前缀),实际可用约 **112 个**。需要更多设备可使用 10 位扩展地址模式(最多 1024 个)。
Q5:读取 SHT40 的完整 I2C 流程是什么?
**阶段一:写命令**
```
[START] → [0x44 + W(0)] → ACK → [0xFD(高精度命令)] → ACK → [STOP]
```
**阶段二:等待 ≥10ms 后读数据**
```
[START] → [0x44 + R(1)] → ACK → [data0] ACK → [data1] ACK → [data2] ACK
→ [data3] ACK → [data4] ACK → [data5] NACK → [STOP]
```
返回 6 字节:温度高字节、温度低字节、温度CRC、湿度高字节、湿度低字节、湿度CRC。
换算公式:
- temperature = -45 + 175 × raw_temp / 65535
- humidity = -6 + 125 × raw_hum / 65535(clamp 到 [0, 100])