第17章:I2C 通信协议详解
本章深入讲解 I2C 通信协议的工作原理、时序分析与设备通信方法。
17.1 I2C 协议概述
17.1.1 基本特点
I2C(Inter-Integrated Circuit)是一种同步串行通信协议:
- 两线制:SDA(数据线)+ SCL(时钟线)
- 多主多从:支持多个主机和从机
- 地址寻址:每个从机有唯一 7 位地址
- 双向通信:同一数据线收发
17.1.2 硬件连接
VCC
│
[Rp] 上拉电阻 (4.7K)
│
主机 ─┴───┬───┬─── 从机1
│ │
SDA SCL
│ │
主机 ────┴───┴─── 从机2
17.1.3 Pico I2C 资源
| I2C | 默认 SDA | 默认 SCL | 备选引脚 |
|---|---|---|---|
| I2C0 | GP4 | GP5 | GP0/GP1, GP8/GP9, GP12/GP13, GP16/GP17, GP20/GP21 |
| I2C1 | GP14 | GP15 | GP2/GP3, GP6/GP7, GP10/GP11, GP18/GP19, GP26/GP27 |
17.2 I2C 时序分析
17.2.1 起始和停止条件
起始条件 (S):
SCL: ────────────┐
SDA: ────┐ │
└───────┘
停止条件 (P):
SCL: ────────────────
SDA: ────────┐
└───────
17.2.2 数据传输
数据位传输:
┌─────┐ ┌─────┐
SCL: ──┘ └─────┘ └─────
← 数据有效 → ← 数据有效 →
SDA: ─────────────────────────
(高或低,SCL 高时稳定)
17.2.3 应答信号
ACK (应答):
接收方在第 9 个时钟周期拉低 SDA
NACK (非应答):
SDA 保持高电平
17.3 MicroPython I2C 编程
17.3.1 初始化 I2C
from machine import I2C, Pin
# 硬件 I2C
i2c = I2C(0, sda=Pin(20), scl=Pin(21), freq=400000)
# 软件 I2C (SoftI2C)
from machine import SoftI2C
i2c_soft = SoftI2C(sda=Pin(20), scl=Pin(21), freq=100000)
17.3.2 扫描设备
'''
实验:扫描 I2C 总线
'''
devices = i2c.scan()
print(f"发现 {len(devices)} 个设备:")
for addr in devices:
print(f" 地址: 0x{addr:02X} ({addr})")
17.3.3 读写操作
# 写入数据
i2c.writeto(addr, bytes([data]))
# 写入寄存器
i2c.writeto_mem(addr, reg, bytes([data]))
# 读取数据
data = i2c.readfrom(addr, num_bytes)
# 读取寄存器
data = i2c.readfrom_mem(addr, reg, num_bytes)
17.4 ADXL345 加速度传感器
17.4.1 传感器参数
| 参数 | 数值 |
|---|---|
| I2C 地址 | 0x53 (ALT=GND) 或 0x1D (ALT=VCC) |
| 测量范围 | ±2g / ±4g / ±8g / ±16g |
| 分辨率 | 13 位 (最高) |
| 数据格式 | 小端序,二进制补码 |
17.4.2 寄存器定义
# ADXL345 寄存器
ADXL345_ADDR = 0x53
REG_DEVID = 0x00
REG_POWER_CTL = 0x2D
REG_DATA_FORMAT = 0x31
REG_DATAX0 = 0x32 # X 轴低字节
REG_DATAX1 = 0x33 # X 轴高字节
# Y, Z 依次递增
17.4.3 驱动代码
'''
实验:ADXL345 I2C 通信
'''
from machine import I2C, Pin
import time
i2c = I2C(0, sda=Pin(20), scl=Pin(21), freq=400000)
ADXL345_ADDR = 0x53
def write_reg(reg, value):
i2c.writeto_mem(ADXL345_ADDR, reg, bytes([value]))
def read_reg(reg, length=1):
return i2c.readfrom_mem(ADXL345_ADDR, reg, length)
def init_adxl345():
"""初始化 ADXL345"""
# 检查设备 ID
dev_id = read_reg(0x00, 1)[0]
if dev_id != 0xE5:
print(f"设备 ID 错误: 0x{dev_id:02X}")
return False
# 设置数据格式: ±2g, 全分辨率
write_reg(0x31, 0x08)
# 启用测量模式
write_reg(0x2D, 0x08)
return True
def read_xyz():
"""读取三轴加速度"""
data = read_reg(0x32, 6)
# 解析数据(小端序,有符号)
x = int.from_bytes(data[0:2], 'little', True)
y = int.from_bytes(data[2:4], 'little', True)
z = int.from_bytes(data[4:6], 'little', True)
# 转换为 g (±2g 范围,灵敏度 3.9mg/LSB)
return x * 0.0039, y * 0.0039, z * 0.0039
# 初始化
if init_adxl345():
print("ADXL345 初始化成功")
while True:
x, y, z = read_xyz()
print(f"X: {x:+.2f}g Y: {y:+.2f}g Z: {z:+.2f}g")
time.sleep(0.2)
17.4.4 数据解码详解
def decode_twos_complement(data, bits=16):
"""
二进制补码解码
data: 原始字节数组 [低字节, 高字节]
"""
value = data[0] | (data[1] << 8)
# 如果最高位为 1,则为负数
if value & (1 << (bits - 1)):
value -= (1 << bits)
return value
# 示例
raw = [0x5E, 0xFF] # -162 的补码
value = decode_twos_complement(raw)
print(f"解码值: {value}") # -162
17.5 DS1307 实时时钟
17.5.1 模块介绍
DS1307 是一款 I2C 接口的实时时钟芯片:
- I2C 地址:0x68
- 年/月/日/星期/时/分/秒
- 支持 12/24 小时格式
- 备用电池保持
17.5.2 寄存器结构
| 地址 | 内容 | 格式 |
|---|---|---|
| 0x00 | 秒 | BCD |
| 0x01 | 分 | BCD |
| 0x02 | 时 | BCD |
| 0x03 | 星期 | 1-7 |
| 0x04 | 日 | BCD |
| 0x05 | 月 | BCD |
| 0x06 | 年 | BCD |
17.5.3 BCD 编码解码
def bcd_to_dec(bcd):
"""BCD 转十进制"""
return (bcd >> 4) * 10 + (bcd & 0x0F)
def dec_to_bcd(dec):
"""十进制转 BCD"""
return ((dec // 10) << 4) | (dec % 10)
17.5.4 DS1307 驱动
'''
实验:DS1307 实时时钟
'''
from machine import I2C, Pin
import time
i2c = I2C(0, sda=Pin(20), scl=Pin(21))
DS1307_ADDR = 0x68
def bcd_to_dec(bcd):
return (bcd >> 4) * 10 + (bcd & 0x0F)
def dec_to_bcd(dec):
return ((dec // 10) << 4) | (dec % 10)
def read_time():
"""读取当前时间"""
data = i2c.readfrom_mem(DS1307_ADDR, 0x00, 7)
sec = bcd_to_dec(data[0] & 0x7F)
min = bcd_to_dec(data[1])
hour = bcd_to_dec(data[2] & 0x3F)
day = bcd_to_dec(data[4])
month = bcd_to_dec(data[5])
year = bcd_to_dec(data[6]) + 2000
return (year, month, day, hour, min, sec)
def set_time(year, month, day, hour, min, sec):
"""设置时间"""
data = bytes([
dec_to_bcd(sec),
dec_to_bcd(min),
dec_to_bcd(hour),
1, # 星期
dec_to_bcd(day),
dec_to_bcd(month),
dec_to_bcd(year - 2000)
])
i2c.writeto_mem(DS1307_ADDR, 0x00, data)
# 设置时间(首次使用)
# set_time(2026, 4, 10, 23, 30, 0)
while True:
t = read_time()
print(f"{t[0]}-{t[1]:02d}-{t[2]:02d} {t[3]:02d}:{t[4]:02d}:{t[5]:02d}")
time.sleep(1)
17.6 I2C 多设备管理
17.6.1 地址冲突处理
'''
检测 I2C 地址冲突
'''
def check_address_conflict():
devices = i2c.scan()
# 已知设备地址
known_devices = {
0x53: 'ADXL345',
0x68: 'DS1307',
0x3C: 'SSD1306 OLED',
0x3F: 'LCD1602 (PCF8574)',
0x27: 'LCD1602 (PCF8574A)',
}
for addr in devices:
name = known_devices.get(addr, '未知设备')
print(f"0x{addr:02X}: {name}")
17.6.2 多设备同时通信
'''
同时使用多个 I2C 设备
'''
class I2CDeviceManager:
def __init__(self, i2c):
self.i2c = i2c
self.devices = {}
def register_device(self, name, addr):
if addr in self.i2c.scan():
self.devices[name] = addr
print(f"注册设备: {name} @ 0x{addr:02X}")
else:
print(f"设备未找到: {name} @ 0x{addr:02X}")
def write(self, name, reg, data):
if name in self.devices:
self.i2c.writeto_mem(self.devices[name], reg, bytes([data]))
def read(self, name, reg, length):
if name in self.devices:
return self.i2c.readfrom_mem(self.devices[name], reg, length)
return None
# 使用
manager = I2CDeviceManager(i2c)
manager.register_device('accel', 0x53)
manager.register_device('rtc', 0x68)
17.7 常见问题排查
17.7.1 设备扫描不到
# 检查清单:
# 1. 接线是否正确 (SDA/SCL 是否接反)
# 2. 上拉电阻是否存在 (通常模块自带)
# 3. 供电电压是否正确
# 4. I2C 地址是否正确
# 尝试更低频率
i2c = I2C(0, sda=Pin(20), scl=Pin(21), freq=100000)
17.7.2 通信超时
# 增加超时时间
try:
data = i2c.readfrom_mem(addr, reg, length)
except OSError as e:
print(f"I2C 错误: {e}")
# 尝试重新初始化
i2c.init()
17.8 本章小结
本章介绍了 I2C 通信协议:
- 协议基础:
- 两线制:SDA + SCL
- 7 位地址寻址
- 起始、停止、应答信号
- 数据编码:
- 二进制补码(有符号数)
- BCD 编码(时钟芯片)
- 小端序/大端序
- 实际应用:
- ADXL345 加速度传感器
- DS1307 实时时钟
下一章将学习单总线协议与 DS18B20。