znlgis 博客

GIS开发与技术分享

第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 通信协议:

  1. 协议基础
    • 两线制:SDA + SCL
    • 7 位地址寻址
    • 起始、停止、应答信号
  2. 数据编码
    • 二进制补码(有符号数)
    • BCD 编码(时钟芯片)
    • 小端序/大端序
  3. 实际应用
    • ADXL345 加速度传感器
    • DS1307 实时时钟

下一章将学习单总线协议与 DS18B20。