BLOG8
跳转至

I2C硬件接口实战

概述

I2C(Inter-Integrated Circuit,内部集成电路总线)是由Philips公司(现NXP)开发的一种同步、半双工、多主机的串行通信总线。I2C只需要两根信号线(SDA和SCL)就能实现多个设备之间的通信,广泛应用于MCU与传感器、EEPROM、RTC、LCD等外设的连接。

完成本教程学习后,你将能够:

  • 理解I2C的工作原理和硬件特性
  • 掌握I2C地址分配和寻址方式
  • 学会上拉电阻的计算和配置
  • 理解总线仲裁和多主机模式
  • 实现STM32和Arduino的I2C通信
  • 掌握常见I2C外设的应用

背景知识

I2C vs SPI vs UART

特性 I2C SPI UART
通信方式 同步 同步 异步
数据线 2根(SDA/SCL) 4根(MOSI/MISO/SCK/CS) 2根(TX/RX)
速度 中等(100kHz-3.4MHz) 很快(MHz级) 较慢(kbps级)
全双工 否(半双工) 是 是
多主机 支持 复杂 不支持
多从机 简单(地址) 简单(独立CS) 不支持
硬件复杂度 简单 中等 简单
传输距离 短(<1米) 短(<1米) 中(数米)
线数优势 ★★★ ★ ★★

I2C的优势

线数少: - 只需2根线(SDA、SCL) - 节省GPIO资源 - 简化PCB布线

多设备支持: - 支持多主机 - 支持多从机(最多127个) - 通过地址寻址

灵活性高: - 支持热插拔 - 支持动态地址分配 - 支持时钟拉伸

成本低: - 硬件简单 - 无需额外芯片 - 广泛支持

I2C工作原理

信号线定义

I2C使用2根信号线进行通信:

1. SDA(Serial Data)- 串行数据线 - 双向数据传输 - 开漏输出(Open-Drain) - 需要外部上拉电阻 - 数据在SCL高电平期间保持稳定

2. SCL(Serial Clock)- 串行时钟线 - 同步时钟信号 - 由主机产生(通常) - 开漏输出 - 需要外部上拉电阻 - 支持时钟拉伸(从机可拉低)

基本连接方式

单主机多从机(最常见):

主机(MCU)              从机1        从机2        从机3

VCC                     VCC          VCC          VCC
 │                       │            │            │
 ├─ Rp ─┬───────────────┼────────────┼────────────┤
 │      │               │            │            │
 │      └─ SDA ─────────┼─ SDA       │            │
 │                      │            │            │
 ├─ Rp ─┬───────────────┼────────────┼─ SDA       │
 │      │               │            │            │
 │      └─ SCL ─────────┼─ SCL ──────┼─ SCL ──────┼─ SDA
 │                      │            │            │
GND ────────────────────┴─ GND ──────┴─ GND ──────┴─ SCL
                                                    │
                                                   GND

Rp = 上拉电阻(通常4.7kΩ)

关键要点: - SDA和SCL都需要上拉电阻 - 所有设备共享同一对SDA/SCL - 通过地址区分不同从机 - 必须共地

开漏输出原理

为什么使用开漏输出:

开漏输出(Open-Drain):
VCC
 │
 └─ Rp ─┬─ SDA/SCL
        │
    ┌───┴───┐
    │  NMOS │  ← 只能拉低,不能拉高
    └───┬───┘
        │
       GND

工作原理:
- NMOS关闭:SDA/SCL被上拉电阻拉高(逻辑1)
- NMOS导通:SDA/SCL被拉低到GND(逻辑0)
- 任何设备都可以拉低总线
- 只有所有设备都释放,总线才能为高

优势: - 支持多主机(线与逻辑) - 支持时钟拉伸 - 支持不同电压设备(通过上拉电阻电压) - 防止总线冲突

数据传输格式

I2C数据帧结构:

起始条件 → 地址字节 → R/W位 → ACK → 数据字节 → ACK → ... → 停止条件

详细时序:
SDA: ──┐     ┌─┬─┬─┬─┬─┬─┬─┬─┬─┐ ┌─┬─┬─┬─┬─┬─┬─┬─┬─┐     ┌──
       └─────┘ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └─────┘
       起始    A6 A5 A4 A3 A2 A1 A0 R/W ACK D7 D6 D5 D4 D3 D2 D1 D0 ACK 停止

SCL: ────┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌────
         └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘

起始条件(Start Condition):

SCL保持高电平时,SDA从高到低跳变

SDA: ────┐
         └─────
SCL: ──────────
         ↑
       起始条件

停止条件(Stop Condition):

SCL保持高电平时,SDA从低到高跳变

SDA: ─────┌────
          │
SCL: ──────────
         ↑
       停止条件

数据传输规则: 1. SCL为高电平时,SDA必须保持稳定 2. SCL为低电平时,SDA可以改变 3. 数据在SCL上升沿后被采样 4. MSB(最高位)先传输

应答位(ACK/NACK):

ACK(应答):
- 接收方拉低SDA(逻辑0)
- 表示成功接收数据

NACK(非应答):
- 接收方释放SDA(逻辑1)
- 表示接收结束或错误

SDA: ───┐   ┌───  (ACK)
        └───┘

SDA: ───────────  (NACK)

SCL: ─┐   ┌─┐
      └───┘ └───

I2C地址分配

7位地址模式(最常用)

地址格式:

地址字节:A6 A5 A4 A3 A2 A1 A0 R/W

- A6-A0:7位设备地址(0x00-0x7F)
- R/W:读写位(0=写,1=读)
- 实际可用地址:0x08-0x77(保留地址除外)

保留地址:

地址 用途
0x00 通用调用地址
0x01 CBUS地址
0x02 保留
0x03 保留
0x04-0x07 Hs-mode主机代码
0x78-0x7F 10位地址

常见设备地址:

设备类型 典型地址 可配置位
EEPROM (AT24C) 0x50-0x57 A2, A1, A0
RTC (DS1307) 0x68 固定
RTC (PCF8563) 0x51 固定
温湿度 (SHT3x) 0x44/0x45 ADDR引脚
加速度计 (ADXL345) 0x53/0x1D SDO引脚
陀螺仪 (MPU6050) 0x68/0x69 AD0引脚
LCD (PCF8574) 0x20-0x27 A2, A1, A0
DAC (MCP4725) 0x60-0x67 A2, A1, A0

10位地址模式(不常用)

地址格式:

第一字节:1 1 1 1 0 A9 A8 R/W
第二字节:A7 A6 A5 A4 A3 A2 A1 A0

- 前5位固定为11110
- A9-A0:10位设备地址(0x000-0x3FF)
- 可寻址1024个设备

使用场景: - 需要大量设备 - 地址冲突无法解决 - 特殊应用

地址冲突解决

问题:两个设备地址相同

解决方案:

  1. 硬件地址配置:

    设备1:A2=0, A1=0, A0=0 → 地址0x50
    设备2:A2=0, A1=0, A0=1 → 地址0x51
    设备3:A2=0, A1=1, A0=0 → 地址0x52
    

  2. 使用I2C多路复用器:

    MCU ─── I2C ─── TCA9548A ───┬─ 通道0 ─── 设备1 (0x50)
                                ├─ 通道1 ─── 设备2 (0x50)
                                ├─ 通道2 ─── 设备3 (0x50)
                                └─ 通道7 ─── 设备8 (0x50)
    
    TCA9548A地址:0x70-0x77
    每个通道独立,可连接相同地址设备
    

  3. 软件I2C(BitBang):

    使用不同GPIO模拟多组I2C总线
    

地址扫描

扫描总线上的所有设备:

void I2C_ScanDevices(void) {
    printf("Scanning I2C bus...\n");
    printf("     0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F\n");

    for (uint8_t row = 0; row < 8; row++) {
        printf("%02X: ", row * 16);

        for (uint8_t col = 0; col < 16; col++) {
            uint8_t addr = (row * 16) + col;

            // 跳过保留地址
            if (addr < 0x08 || addr > 0x77) {
                printf("   ");
                continue;
            }

            // 尝试通信
            if (I2C_CheckDevice(addr)) {
                printf("%02X ", addr);
            } else {
                printf("-- ");
            }
        }
        printf("\n");
    }
}

// 检查设备是否存在
bool I2C_CheckDevice(uint8_t addr) {
    // 发送起始条件和地址
    I2C_Start();
    bool ack = I2C_SendAddress(addr, I2C_WRITE);
    I2C_Stop();

    return ack;  // 收到ACK表示设备存在
}

输出示例:

Scanning I2C bus...
     0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F
00:          -- -- -- -- -- -- -- -- -- -- -- -- -- 
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
40: -- -- -- -- 44 -- -- -- -- -- -- -- -- -- -- -- 
50: 50 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
60: -- -- -- -- -- -- -- -- 68 -- -- -- -- -- -- -- 
70: -- -- -- -- -- -- -- --

找到3个设备:
- 0x44: SHT3x温湿度传感器
- 0x50: AT24C EEPROM
- 0x68: MPU6050陀螺仪

上拉电阻配置

为什么需要上拉电阻

开漏输出特性: - I2C使用开漏输出 - 设备只能拉低总线,不能拉高 - 需要外部上拉电阻将总线拉高

没有上拉电阻的后果: - 总线始终为低电平 - 无法通信 - 数据全为0

上拉电阻计算

计算公式:

Rp(min) = (VCC - VOL(max)) / IOL

Rp(max) = tr / (0.8473 × Cb)

其中:
- VCC:电源电压(3.3V或5V)
- VOL(max):最大低电平输出电压(通常0.4V)
- IOL:低电平输出电流(3mA)
- tr:上升时间(标准模式300ns,快速模式300ns,快速+模式120ns)
- Cb:总线电容(包括线路电容和设备电容)

简化计算:

速度模式 频率 推荐电阻值 适用场景
标准模式 100kHz 4.7kΩ - 10kΩ 长距离、多设备
快速模式 400kHz 2.2kΩ - 4.7kΩ 常规应用
快速+模式 1MHz 1kΩ - 2.2kΩ 短距离、少设备
高速模式 3.4MHz 470Ω - 1kΩ 特殊应用

实际选择建议:

总线长度 < 10cm,设备 < 3个:
- 100kHz: 10kΩ
- 400kHz: 4.7kΩ
- 1MHz: 2.2kΩ

总线长度 10-50cm,设备 3-7个:
- 100kHz: 4.7kΩ
- 400kHz: 2.2kΩ
- 1MHz: 1kΩ

总线长度 > 50cm,设备 > 7个:
- 100kHz: 2.2kΩ
- 400kHz: 1kΩ
- 不建议使用1MHz

上拉电阻连接

标准连接:

VCC (3.3V或5V)
 │
 ├─ 4.7kΩ ─┬─ SDA ─── 设备1 ─── 设备2 ─── 设备3
 │         │
 └─ 4.7kΩ ─┴─ SCL ─── 设备1 ─── 设备2 ─── 设备3
           │
          GND

注意:
- 只需要一组上拉电阻(通常在主机端)
- 不要在每个设备上都加上拉电阻
- 多个上拉电阻并联会降低总阻值

多电压系统:

3.3V MCU                    5V设备

3.3V                        5V
 │                           │
 ├─ 4.7kΩ ─┬─ SDA ──────────┼─ SDA
 │         │                 │
 └─ 4.7kΩ ─┴─ SCL ──────────┼─ SCL
           │                 │
          GND ───────────────┴─ GND

说明:
- 上拉到3.3V
- 5V设备的输入通常容忍3.3V
- 5V设备的输出为开漏,不会输出5V

上拉电阻测试

测试方法:

// 1. 测量空闲电压
// 应该接近VCC(3.3V或5V)

// 2. 测量上升时间
void Test_RiseTime(void) {
    // 使用示波器测量
    // 从10%到90%的时间应该 < 300ns(快速模式)
}

// 3. 测量下降时间
void Test_FallTime(void) {
    // 使用示波器测量
    // 从90%到10%的时间应该 < 300ns(快速模式)
}

// 4. 测试通信
void Test_Communication(void) {
    // 尝试读取设备
    uint8_t data;
    if (I2C_Read(0x50, 0x00, &data)) {
        printf("Communication OK\n");
    } else {
        printf("Communication Failed\n");
        printf("Check pull-up resistors!\n");
    }
}

常见问题:

现象 可能原因 解决方法
总线始终为低 缺少上拉电阻 添加上拉电阻
通信不稳定 上拉电阻过大 减小电阻值
波形振铃 上拉电阻过小 增大电阻值
速度慢 上拉电阻过大 减小电阻值
功耗高 上拉电阻过小 增大电阻值

总线仲裁与多主机模式

总线仲裁机制

什么是总线仲裁: - 多个主机同时发起通信时的冲突解决机制 - 基于"线与"逻辑 - 自动、无损的仲裁过程

仲裁原理:

主机1和主机2同时发送数据:

时钟周期:  1    2    3    4    5    6    7    8
主机1发送: 1    0    1    1    0    1    0    1
主机2发送: 1    0    1    0    1    1    0    1
总线实际:  1    0    1    0    0    1    0    1
                      ↑
                   主机1检测到冲突,停止发送
                   主机2继续发送

规则:
- 0优先于1(线与逻辑)
- 发送1但检测到0的主机失败
- 失败的主机立即停止,等待总线空闲

仲裁过程:

1. 两个主机同时发送起始条件
2. 同时发送地址字节
3. 逐位比较:
   - 发送1,检测到1:继续
   - 发送0,检测到0:继续
   - 发送1,检测到0:失败,停止
   - 发送0,检测到1:不可能(线与逻辑)
4. 获胜的主机继续通信
5. 失败的主机等待总线空闲后重试

时钟拉伸(Clock Stretching)

什么是时钟拉伸: - 从机拉低SCL,暂停通信 - 给从机更多时间处理数据 - 主机必须等待从机释放SCL

工作原理:

主机产生时钟:
SCL: ─┐   ┌─┐   ┌─┐   ┌─
      └───┘ └───┘ └───┘

从机拉伸时钟:
SCL: ─┐   ┌─┐   ┌─────────┐   ┌─
      └───┘ └───┘         └───┘
                ↑
            从机拉低SCL
            主机等待

应用场景: - 从机处理速度慢 - 从机需要时间准备数据 - EEPROM写入操作 - ADC转换等待

代码示例:

// 主机等待时钟拉伸
void I2C_WaitClockStretch(void) {
    uint32_t timeout = 1000;

    // 释放SCL
    I2C_SCL_High();

    // 等待从机释放SCL
    while (I2C_SCL_Read() == 0) {
        timeout--;
        if (timeout == 0) {
            // 超时错误
            return;
        }
        DelayUs(1);
    }
}

多主机配置

硬件连接:

主机1              主机2              从机1        从机2

VCC                VCC                VCC          VCC
 │                  │                  │            │
 ├─ 4.7kΩ ─┬────────┼──────────────────┼────────────┤
 │         │        │                  │            │
 │         └─ SDA ──┼─ SDA ────────────┼─ SDA       │
 │                  │                  │            │
 ├─ 4.7kΩ ─┬────────┼──────────────────┼────────────┼─ SDA
 │         │        │                  │            │
 │         └─ SCL ──┼─ SCL ────────────┼─ SCL ──────┼─ SCL
 │                  │                  │            │
GND ────────────────┴─ GND ────────────┴─ GND ──────┴─ GND

注意:
- 只需要一组上拉电阻
- 所有设备共享SDA/SCL
- 主机也可以作为从机

软件实现:

// 多主机发送
bool I2C_MultiMaster_Send(uint8_t addr, uint8_t *data, uint16_t len) {
    int retry = 3;

    while (retry > 0) {
        // 等待总线空闲
        if (!I2C_WaitBusIdle(1000)) {
            return false;
        }

        // 发送起始条件
        I2C_Start();

        // 发送地址
        if (!I2C_SendAddress(addr, I2C_WRITE)) {
            // 仲裁失败
            I2C_Stop();
            retry--;
            HAL_Delay(10);  // 等待后重试
            continue;
        }

        // 发送数据
        for (uint16_t i = 0; i < len; i++) {
            if (!I2C_SendByte(data[i])) {
                I2C_Stop();
                return false;
            }
        }

        I2C_Stop();
        return true;
    }

    return false;  // 重试失败
}

// 检查总线是否空闲
bool I2C_WaitBusIdle(uint32_t timeout) {
    uint32_t start = HAL_GetTick();

    while ((HAL_GetTick() - start) < timeout) {
        // 检查SDA和SCL是否都为高
        if (I2C_SDA_Read() && I2C_SCL_Read()) {
            return true;
        }
        HAL_Delay(1);
    }

    return false;
}

多主机注意事项

优点: - 灵活性高 - 可以实现对等通信 - 适合分布式系统

缺点: - 软件复杂度高 - 需要处理仲裁失败 - 可能降低总线效率

最佳实践: 1. 尽量避免同时发起通信 2. 使用优先级机制(低地址优先) 3. 实现重试机制 4. 添加超时保护 5. 考虑使用单主机+中断方式

I2C速度模式

标准速度模式

模式 频率 上升时间 下降时间 应用场景
标准模式 100kHz ≤1000ns ≤300ns 长距离、多设备
快速模式 400kHz ≤300ns ≤300ns 常规应用
快速+模式 1MHz ≤120ns ≤120ns 短距离、少设备
高速模式 3.4MHz ≤80ns ≤80ns 特殊应用
超快速模式 5MHz ≤40ns ≤40ns 极少使用

速度配置

STM32配置示例:

// 标准模式(100kHz)
void I2C_Init_Standard(void) {
    // 假设APB1时钟为36MHz
    // CCR = 36MHz / (2 × 100kHz) = 180
    I2C1->CCR = 180;
    I2C1->TRISE = 37;  // (1000ns / 28ns) + 1
}

// 快速模式(400kHz)
void I2C_Init_Fast(void) {
    // CCR = 36MHz / (3 × 400kHz) = 30
    I2C1->CCR = 30 | I2C_CCR_FS;  // 快速模式
    I2C1->TRISE = 12;  // (300ns / 28ns) + 1
}

// 使用HAL库
void I2C_Init_HAL(void) {
    hi2c1.Instance = I2C1;
    hi2c1.Init.ClockSpeed = 400000;  // 400kHz
    hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;
    hi2c1.Init.OwnAddress1 = 0;
    hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
    hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
    hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
    hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;
    HAL_I2C_Init(&hi2c1);
}

Arduino配置示例:

#include <Wire.h>

void setup() {
    Wire.begin();  // 默认100kHz

    // 设置为400kHz
    Wire.setClock(400000);

    // 设置为1MHz(快速+模式)
    // Wire.setClock(1000000);
}

速度选择建议

100kHz(标准模式): - 总线长度 > 50cm - 设备数量 > 7个 - 对速度要求不高 - 兼容性最好

400kHz(快速模式): - 总线长度 < 50cm - 设备数量 < 7个 - 常规应用 - 推荐使用

1MHz(快速+模式): - 总线长度 < 10cm - 设备数量 < 3个 - 需要高速传输 - 需要验证设备支持

I2C硬件设计要点

PCB布线建议

布线规则:

  1. 短而直:
  2. SDA/SCL走线尽量短(<30cm)
  3. 避免长距离走线
  4. 减少寄生电容

  5. 平行走线:

  6. SDA和SCL平行走线
  7. 间距保持一致(5-10mil)
  8. 减少串扰

  9. 地平面:

  10. 信号线下方保留完整地平面
  11. 提供低阻抗回流路径
  12. 减少EMI

  13. 远离干扰源:

  14. 远离电源线、高速信号
  15. 避免与其他信号平行走线
  16. 必要时添加地线隔离

  17. 上拉电阻位置:

    推荐:靠近主机端
    VCC
     │
     ├─ Rp ─┬─ SDA ─── 主机 ─── 从机1 ─── 从机2
     │      │
     └─ Rp ─┴─ SCL
    
    不推荐:分散在各个设备
    

  18. 去耦电容:

    每个IC的VCC引脚附近:
    VCC ─┬─ 0.1μF ─┬─ GND
         └─ 10μF ──┘
    

ESD保护

保护电路:

SDA ──┬─────────→ 外部接口
      │
      ├─ TVS二极管 ─┬─ GND
      │             │
      └─ 100Ω ──────┘

SCL ──┬─────────→ 外部接口
      │
      ├─ TVS二极管 ─┬─ GND
      │             │
      └─ 100Ω ──────┘

推荐TVS型号:
- PESD3V3L2BT(双通道)
- TPD2E001(双通道)

电平转换

3.3V ↔ 5V电平转换:

方案1:使用专用芯片(推荐)

3.3V侧          PCA9306          5V侧

3.3V ────────→ VREF1    VREF2 ←──── 5V
SDA ─────────→ SDA1     SDA2 ←───→ SDA
SCL ─────────→ SCL1     SCL2 ←───→ SCL
GND ─────────→ GND      GND ←───── GND

特点:
- 双向转换
- 自动方向检测
- 支持400kHz

方案2:使用MOSFET(经济)

3.3V侧                    5V侧

3.3V                      5V
 │                         │
 ├─ 10kΩ ─┬───────────────┼─ 10kΩ
 │        │   BSS138      │
 │        ├─ G    D ───────┤
 │        │   │    │       │
SDA ──────┼─ S    S ───────┼─ SDA
          │                │
         GND              GND

SCL同样连接

特点:
- 成本低
- 双向转换
- 需要两个MOSFET

总线电容

电容来源: - PCB走线电容(~1pF/cm) - 设备输入电容(~10pF/设备) - 连接器电容(~5pF)

总电容计算:

Cb = 走线电容 + 设备电容 × 设备数量 + 连接器电容

示例:
- 走线长度:20cm → 20pF
- 设备数量:5个 × 10pF → 50pF
- 连接器:2个 × 5pF → 10pF
- 总电容:20 + 50 + 10 = 80pF

电容限制: - 标准模式:≤400pF - 快速模式:≤400pF - 快速+模式:≤550pF

超出限制的解决方案: 1. 减少设备数量 2. 缩短走线长度 3. 使用I2C缓冲器/中继器 4. 降低速度

实践示例

示例1:STM32 I2C硬件配置

硬件连接:

STM32F103C8T6          EEPROM (AT24C256)
PB6 (I2C1_SCL) ──────→ SCL
PB7 (I2C1_SDA) ──────→ SDA
3.3V ─┬─ 4.7kΩ ─┬────→ VCC
      └─ 4.7kΩ ─┘
GND ──────────────────→ GND

寄存器配置:

// 1. 使能时钟
RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;   // I2C1时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;   // GPIOB时钟

// 2. 配置GPIO(复用开漏输出)
// PB6 (SCL) - 复用开漏输出,50MHz
GPIOB->CRL &= ~(0xF << 24);
GPIOB->CRL |= (0xF << 24);  // 复用开漏,50MHz

// PB7 (SDA) - 复用开漏输出,50MHz
GPIOB->CRL &= ~(0xF << 28);
GPIOB->CRL |= (0xF << 28);

// 3. 复位I2C
I2C1->CR1 |= I2C_CR1_SWRST;
I2C1->CR1 &= ~I2C_CR1_SWRST;

// 4. 配置I2C
// 设置APB1时钟频率(36MHz)
I2C1->CR2 = 36;

// 配置时钟控制寄存器(400kHz快速模式)
// CCR = 36MHz / (3 × 400kHz) = 30
I2C1->CCR = 30 | I2C_CCR_FS;

// 配置上升时间(300ns)
I2C1->TRISE = 12;  // (300ns / 28ns) + 1

// 使能I2C
I2C1->CR1 |= I2C_CR1_PE;

基本操作函数:

// 发送起始条件
void I2C_Start(void) {
    I2C1->CR1 |= I2C_CR1_START;
    while (!(I2C1->SR1 & I2C_SR1_SB));  // 等待起始条件发送完成
}

// 发送停止条件
void I2C_Stop(void) {
    I2C1->CR1 |= I2C_CR1_STOP;
}

// 发送地址
bool I2C_SendAddress(uint8_t addr, uint8_t direction) {
    I2C1->DR = (addr << 1) | direction;

    // 等待地址发送完成
    uint32_t timeout = 10000;
    while (!(I2C1->SR1 & I2C_SR1_ADDR) && timeout--);

    if (timeout == 0) {
        return false;  // 超时
    }

    // 清除ADDR标志
    (void)I2C1->SR1;
    (void)I2C1->SR2;

    return true;
}

// 发送数据
bool I2C_SendByte(uint8_t data) {
    // 等待发送缓冲区空
    uint32_t timeout = 10000;
    while (!(I2C1->SR1 & I2C_SR1_TXE) && timeout--);

    if (timeout == 0) {
        return false;
    }

    I2C1->DR = data;

    // 等待字节传输完成
    timeout = 10000;
    while (!(I2C1->SR1 & I2C_SR1_BTF) && timeout--);

    return (timeout > 0);
}

// 接收数据
uint8_t I2C_ReceiveByte(bool ack) {
    // 配置ACK
    if (ack) {
        I2C1->CR1 |= I2C_CR1_ACK;
    } else {
        I2C1->CR1 &= ~I2C_CR1_ACK;
    }

    // 等待接收缓冲区非空
    while (!(I2C1->SR1 & I2C_SR1_RXNE));

    return I2C1->DR;
}

EEPROM读写示例:

// 写入单个字节
void EEPROM_WriteByte(uint16_t addr, uint8_t data) {
    I2C_Start();
    I2C_SendAddress(0x50, I2C_WRITE);  // EEPROM地址
    I2C_SendByte(addr >> 8);           // 地址高字节
    I2C_SendByte(addr & 0xFF);         // 地址低字节
    I2C_SendByte(data);                // 数据
    I2C_Stop();

    HAL_Delay(5);  // 等待写入完成(tWR)
}

// 读取单个字节
uint8_t EEPROM_ReadByte(uint16_t addr) {
    uint8_t data;

    // 写入地址
    I2C_Start();
    I2C_SendAddress(0x50, I2C_WRITE);
    I2C_SendByte(addr >> 8);
    I2C_SendByte(addr & 0xFF);

    // 重新起始,读取数据
    I2C_Start();
    I2C_SendAddress(0x50, I2C_READ);
    data = I2C_ReceiveByte(false);  // NACK
    I2C_Stop();

    return data;
}

// 页写入(最多64字节)
void EEPROM_WritePage(uint16_t addr, uint8_t *data, uint16_t len) {
    I2C_Start();
    I2C_SendAddress(0x50, I2C_WRITE);
    I2C_SendByte(addr >> 8);
    I2C_SendByte(addr & 0xFF);

    for (uint16_t i = 0; i < len; i++) {
        I2C_SendByte(data[i]);
    }

    I2C_Stop();
    HAL_Delay(5);
}

// 顺序读取
void EEPROM_ReadSequential(uint16_t addr, uint8_t *buffer, uint16_t len) {
    // 写入地址
    I2C_Start();
    I2C_SendAddress(0x50, I2C_WRITE);
    I2C_SendByte(addr >> 8);
    I2C_SendByte(addr & 0xFF);

    // 重新起始,读取数据
    I2C_Start();
    I2C_SendAddress(0x50, I2C_READ);

    for (uint16_t i = 0; i < len; i++) {
        if (i == len - 1) {
            buffer[i] = I2C_ReceiveByte(false);  // 最后一个字节NACK
        } else {
            buffer[i] = I2C_ReceiveByte(true);   // ACK
        }
    }

    I2C_Stop();
}

示例2:Arduino I2C配置

硬件连接:

Arduino Uno           传感器
A4 (SDA) ────────────→ SDA
A5 (SCL) ────────────→ SCL
5V ─┬─ 4.7kΩ ─┬──────→ VCC
    └─ 4.7kΩ ─┘
GND ─────────────────→ GND

基本配置:

#include <Wire.h>

void setup() {
    Serial.begin(115200);

    // 初始化I2C(主机模式)
    Wire.begin();

    // 设置时钟频率
    Wire.setClock(400000);  // 400kHz

    Serial.println("I2C Ready!");
}

void loop() {
    // 扫描I2C设备
    scanI2CDevices();
    delay(5000);
}

// 扫描I2C总线
void scanI2CDevices() {
    Serial.println("Scanning I2C bus...");
    int count = 0;

    for (uint8_t addr = 1; addr < 127; addr++) {
        Wire.beginTransmission(addr);
        uint8_t error = Wire.endTransmission();

        if (error == 0) {
            Serial.print("Device found at 0x");
            if (addr < 16) Serial.print("0");
            Serial.println(addr, HEX);
            count++;
        }
    }

    Serial.print("Found ");
    Serial.print(count);
    Serial.println(" device(s)");
}

读写示例:

// 写入数据到设备
void writeI2C(uint8_t addr, uint8_t reg, uint8_t data) {
    Wire.beginTransmission(addr);
    Wire.write(reg);   // 寄存器地址
    Wire.write(data);  // 数据
    Wire.endTransmission();
}

// 从设备读取数据
uint8_t readI2C(uint8_t addr, uint8_t reg) {
    // 写入寄存器地址
    Wire.beginTransmission(addr);
    Wire.write(reg);
    Wire.endTransmission(false);  // 重复起始

    // 读取数据
    Wire.requestFrom(addr, (uint8_t)1);
    if (Wire.available()) {
        return Wire.read();
    }
    return 0;
}

// 读取多个字节
void readI2CMultiple(uint8_t addr, uint8_t reg, uint8_t *buffer, uint8_t len) {
    // 写入寄存器地址
    Wire.beginTransmission(addr);
    Wire.write(reg);
    Wire.endTransmission(false);

    // 读取数据
    Wire.requestFrom(addr, len);
    for (uint8_t i = 0; i < len; i++) {
        if (Wire.available()) {
            buffer[i] = Wire.read();
        }
    }
}

MPU6050示例:

#define MPU6050_ADDR 0x68
#define PWR_MGMT_1   0x6B
#define ACCEL_XOUT_H 0x3B

void setup() {
    Serial.begin(115200);
    Wire.begin();
    Wire.setClock(400000);

    // 唤醒MPU6050
    writeI2C(MPU6050_ADDR, PWR_MGMT_1, 0x00);
    delay(100);

    Serial.println("MPU6050 initialized");
}

void loop() {
    // 读取加速度数据
    int16_t ax, ay, az;

    Wire.beginTransmission(MPU6050_ADDR);
    Wire.write(ACCEL_XOUT_H);
    Wire.endTransmission(false);

    Wire.requestFrom(MPU6050_ADDR, (uint8_t)6);
    ax = (Wire.read() << 8) | Wire.read();
    ay = (Wire.read() << 8) | Wire.read();
    az = (Wire.read() << 8) | Wire.read();

    Serial.print("AX: "); Serial.print(ax);
    Serial.print(" AY: "); Serial.print(ay);
    Serial.print(" AZ: "); Serial.println(az);

    delay(100);
}

示例3:软件模拟I2C(BitBang)

// 定义引脚
#define I2C_SDA_PIN   GPIO_PIN_0
#define I2C_SCL_PIN   GPIO_PIN_1
#define I2C_PORT      GPIOA

// 延时函数
void I2C_Delay(void) {
    for (volatile int i = 0; i < 20; i++);
}

// SDA控制
void I2C_SDA_Out(uint8_t val) {
    if (val) {
        GPIOA->ODR |= I2C_SDA_PIN;
    } else {
        GPIOA->ODR &= ~I2C_SDA_PIN;
    }
}

uint8_t I2C_SDA_In(void) {
    return (GPIOA->IDR & I2C_SDA_PIN) ? 1 : 0;
}

// SCL控制
void I2C_SCL_Out(uint8_t val) {
    if (val) {
        GPIOA->ODR |= I2C_SCL_PIN;
    } else {
        GPIOA->ODR &= ~I2C_SCL_PIN;
    }
}

// 初始化GPIO
void I2C_GPIO_Init(void) {
    // 使能GPIOA时钟
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;

    // SDA, SCL - 开漏输出
    GPIOA->CRL &= ~(0xFF << 0);
    GPIOA->CRL |= (0x77 << 0);  // 开漏输出,50MHz

    // 初始状态
    I2C_SDA_Out(1);
    I2C_SCL_Out(1);
}

// 起始条件
void I2C_Start_SW(void) {
    I2C_SDA_Out(1);
    I2C_SCL_Out(1);
    I2C_Delay();
    I2C_SDA_Out(0);  // SDA下降沿
    I2C_Delay();
    I2C_SCL_Out(0);
}

// 停止条件
void I2C_Stop_SW(void) {
    I2C_SDA_Out(0);
    I2C_SCL_Out(1);
    I2C_Delay();
    I2C_SDA_Out(1);  // SDA上升沿
    I2C_Delay();
}

// 发送一个字节
bool I2C_SendByte_SW(uint8_t data) {
    for (int i = 7; i >= 0; i--) {
        I2C_SCL_Out(0);
        I2C_Delay();

        // 输出数据位
        I2C_SDA_Out((data >> i) & 0x01);
        I2C_Delay();

        I2C_SCL_Out(1);  // 时钟高电平
        I2C_Delay();
    }

    // 接收ACK
    I2C_SCL_Out(0);
    I2C_SDA_Out(1);  // 释放SDA
    I2C_Delay();

    I2C_SCL_Out(1);
    I2C_Delay();

    bool ack = !I2C_SDA_In();  // ACK为低电平

    I2C_SCL_Out(0);

    return ack;
}

// 接收一个字节
uint8_t I2C_ReceiveByte_SW(bool ack) {
    uint8_t data = 0;

    I2C_SDA_Out(1);  // 释放SDA

    for (int i = 7; i >= 0; i--) {
        I2C_SCL_Out(0);
        I2C_Delay();

        I2C_SCL_Out(1);
        I2C_Delay();

        // 读取数据位
        if (I2C_SDA_In()) {
            data |= (1 << i);
        }
    }

    // 发送ACK/NACK
    I2C_SCL_Out(0);
    I2C_Delay();

    I2C_SDA_Out(ack ? 0 : 1);
    I2C_Delay();

    I2C_SCL_Out(1);
    I2C_Delay();
    I2C_SCL_Out(0);

    return data;
}

常见问题与调试

问题1:无法通信

可能原因:

  1. 缺少上拉电阻
  2. 检查:用万用表测量SDA/SCL电压
  3. 解决:添加4.7kΩ上拉电阻到VCC

  4. 地址错误

  5. 检查:确认设备地址(7位还是8位)
  6. 解决:查看数据手册,使用正确地址

  7. 速度过快

  8. 检查:降低I2C时钟频率
  9. 解决:从100kHz开始测试

  10. 接线错误

  11. 检查:SDA/SCL是否接反
  12. 解决:SDA连SDA,SCL连SCL

调试步骤:

// 1. 检查总线电压
void Debug_CheckBusVoltage(void) {
    // 用万用表测量
    // SDA和SCL空闲时应该接近VCC
    printf("Measure SDA and SCL voltage\n");
    printf("Should be close to VCC (3.3V or 5V)\n");
}

// 2. 扫描设备
void Debug_ScanDevices(void) {
    printf("Scanning I2C bus...\n");
    for (uint8_t addr = 0x08; addr <= 0x77; addr++) {
        if (I2C_CheckDevice(addr)) {
            printf("Found device at 0x%02X\n", addr);
        }
    }
}

// 3. 测试读写
void Debug_TestReadWrite(uint8_t addr) {
    // 尝试写入
    I2C_Start();
    if (I2C_SendAddress(addr, I2C_WRITE)) {
        printf("Write address OK\n");
    } else {
        printf("Write address FAILED\n");
    }
    I2C_Stop();

    // 尝试读取
    I2C_Start();
    if (I2C_SendAddress(addr, I2C_READ)) {
        printf("Read address OK\n");
    } else {
        printf("Read address FAILED\n");
    }
    I2C_Stop();
}

问题2:数据错误

可能原因:

  1. 时序问题
  2. 检查:用示波器观察波形
  3. 解决:调整延时或降低速度

  4. 干扰

  5. 检查:走线是否靠近干扰源
  6. 解决:重新布线,添加滤波电容

  7. 电容过大

  8. 检查:计算总线电容
  9. 解决:减少设备或缩短走线

数据验证:

// 写入并读回验证
bool Verify_WriteRead(uint8_t addr, uint8_t reg, uint8_t data) {
    // 写入
    writeI2C(addr, reg, data);
    HAL_Delay(10);

    // 读回
    uint8_t readback = readI2C(addr, reg);

    if (readback == data) {
        printf("Verify OK: 0x%02X\n", data);
        return true;
    } else {
        printf("Verify FAILED: wrote 0x%02X, read 0x%02X\n", data, readback);
        return false;
    }
}

问题3:总线挂死

现象: - SDA或SCL被拉低 - 无法通信 - 复位后仍然挂死

可能原因: - 从机在传输过程中被复位 - 从机等待时钟但主机已停止 - 总线仲裁失败

解决方法:

// 总线恢复
void I2C_BusRecovery(void) {
    printf("Attempting bus recovery...\n");

    // 1. 禁用I2C外设
    I2C1->CR1 &= ~I2C_CR1_PE;

    // 2. 配置GPIO为开漏输出
    GPIOB->CRL &= ~(0xFF << 24);
    GPIOB->CRL |= (0x77 << 24);

    // 3. 检查SDA状态
    if (!(GPIOB->IDR & GPIO_PIN_7)) {
        printf("SDA is stuck low, sending clock pulses...\n");

        // 4. 发送9个时钟脉冲
        for (int i = 0; i < 9; i++) {
            GPIOB->ODR &= ~GPIO_PIN_6;  // SCL低
            HAL_Delay(1);
            GPIOB->ODR |= GPIO_PIN_6;   // SCL高
            HAL_Delay(1);

            // 检查SDA是否释放
            if (GPIOB->IDR & GPIO_PIN_7) {
                printf("SDA released after %d pulses\n", i + 1);
                break;
            }
        }
    }

    // 5. 发送停止条件
    GPIOB->ODR &= ~GPIO_PIN_7;  // SDA低
    HAL_Delay(1);
    GPIOB->ODR |= GPIO_PIN_6;   // SCL高
    HAL_Delay(1);
    GPIOB->ODR |= GPIO_PIN_7;   // SDA高(停止条件)
    HAL_Delay(1);

    // 6. 重新配置I2C
    GPIOB->CRL &= ~(0xFF << 24);
    GPIOB->CRL |= (0xFF << 24);  // 复用开漏

    I2C1->CR1 |= I2C_CR1_SWRST;
    I2C1->CR1 &= ~I2C_CR1_SWRST;
    I2C1->CR1 |= I2C_CR1_PE;

    printf("Bus recovery complete\n");
}

问题4:多设备冲突

可能原因: - 地址冲突 - 上拉电阻不足 - 总线电容过大

解决方案:

// 检查地址冲突
void Check_AddressConflict(void) {
    uint8_t devices[128] = {0};
    int count = 0;

    // 扫描所有地址
    for (uint8_t addr = 0x08; addr <= 0x77; addr++) {
        if (I2C_CheckDevice(addr)) {
            devices[count++] = addr;
        }
    }

    // 检查是否有重复
    for (int i = 0; i < count; i++) {
        for (int j = i + 1; j < count; j++) {
            if (devices[i] == devices[j]) {
                printf("Address conflict detected: 0x%02X\n", devices[i]);
            }
        }
    }
}

// 测试上拉电阻
void Test_PullUpResistor(void) {
    // 1. 测量空闲电压
    printf("Measure idle voltage on SDA and SCL\n");
    printf("Should be close to VCC\n");

    // 2. 测量上升时间
    printf("Use oscilloscope to measure rise time\n");
    printf("Should be < 300ns for 400kHz\n");

    // 3. 计算建议电阻值
    // Rp = tr / (0.8473 × Cb)
    float tr = 300e-9;  // 300ns
    float Cb = 100e-12; // 100pF(估计值)
    float Rp = tr / (0.8473 * Cb);
    printf("Recommended pull-up: %.1f kΩ\n", Rp / 1000);
}

常见I2C外设应用

1. EEPROM(AT24C系列)

特点: - 容量:1KB - 256KB - 地址:0x50-0x57(可配置) - 页大小:8-64字节 - 应用:配置存储、数据记录

基本操作:

#define EEPROM_ADDR 0x50

// 写入字节
void EEPROM_WriteByte(uint16_t addr, uint8_t data) {
    I2C_Start();
    I2C_SendAddress(EEPROM_ADDR, I2C_WRITE);
    I2C_SendByte(addr >> 8);
    I2C_SendByte(addr & 0xFF);
    I2C_SendByte(data);
    I2C_Stop();
    HAL_Delay(5);  // 写入延时
}

// 读取字节
uint8_t EEPROM_ReadByte(uint16_t addr) {
    uint8_t data;

    I2C_Start();
    I2C_SendAddress(EEPROM_ADDR, I2C_WRITE);
    I2C_SendByte(addr >> 8);
    I2C_SendByte(addr & 0xFF);

    I2C_Start();  // 重复起始
    I2C_SendAddress(EEPROM_ADDR, I2C_READ);
    data = I2C_ReceiveByte(false);
    I2C_Stop();

    return data;
}

2. RTC(DS1307)

特点: - 实时时钟 - 地址:0x68(固定) - 备用电池供电 - 应用:时间记录、定时任务

基本操作:

#define DS1307_ADDR 0x68

// BCD转十进制
uint8_t BCD2DEC(uint8_t bcd) {
    return ((bcd >> 4) * 10) + (bcd & 0x0F);
}

// 十进制转BCD
uint8_t DEC2BCD(uint8_t dec) {
    return ((dec / 10) << 4) | (dec % 10);
}

// 设置时间
void DS1307_SetTime(uint8_t hour, uint8_t minute, uint8_t second) {
    I2C_Start();
    I2C_SendAddress(DS1307_ADDR, I2C_WRITE);
    I2C_SendByte(0x00);  // 秒寄存器地址
    I2C_SendByte(DEC2BCD(second));
    I2C_SendByte(DEC2BCD(minute));
    I2C_SendByte(DEC2BCD(hour));
    I2C_Stop();
}

// 读取时间
void DS1307_GetTime(uint8_t *hour, uint8_t *minute, uint8_t *second) {
    I2C_Start();
    I2C_SendAddress(DS1307_ADDR, I2C_WRITE);
    I2C_SendByte(0x00);

    I2C_Start();
    I2C_SendAddress(DS1307_ADDR, I2C_READ);
    *second = BCD2DEC(I2C_ReceiveByte(true) & 0x7F);
    *minute = BCD2DEC(I2C_ReceiveByte(true));
    *hour = BCD2DEC(I2C_ReceiveByte(false) & 0x3F);
    I2C_Stop();
}

3. 温湿度传感器(SHT3x)

特点: - 高精度温湿度 - 地址:0x44/0x45 - 数字输出 - 应用:环境监测

基本操作:

#define SHT3X_ADDR 0x44

// 读取温湿度
bool SHT3X_ReadTempHumidity(float *temp, float *humidity) {
    uint8_t data[6];

    // 发送测量命令(高重复性)
    I2C_Start();
    I2C_SendAddress(SHT3X_ADDR, I2C_WRITE);
    I2C_SendByte(0x2C);  // 命令高字节
    I2C_SendByte(0x06);  // 命令低字节
    I2C_Stop();

    HAL_Delay(15);  // 等待测量完成

    // 读取数据
    I2C_Start();
    I2C_SendAddress(SHT3X_ADDR, I2C_READ);
    for (int i = 0; i < 6; i++) {
        data[i] = I2C_ReceiveByte(i < 5);
    }
    I2C_Stop();

    // 计算温度
    uint16_t temp_raw = (data[0] << 8) | data[1];
    *temp = -45 + 175 * ((float)temp_raw / 65535.0);

    // 计算湿度
    uint16_t hum_raw = (data[3] << 8) | data[4];
    *humidity = 100 * ((float)hum_raw / 65535.0);

    return true;
}

4. 陀螺仪加速度计(MPU6050)

特点: - 6轴IMU - 地址:0x68/0x69 - 内置DMP - 应用:姿态检测、运动追踪

基本操作:

#define MPU6050_ADDR 0x68

// 初始化
void MPU6050_Init(void) {
    // 唤醒设备
    writeI2C(MPU6050_ADDR, 0x6B, 0x00);
    HAL_Delay(100);

    // 配置陀螺仪量程(±250°/s)
    writeI2C(MPU6050_ADDR, 0x1B, 0x00);

    // 配置加速度计量程(±2g)
    writeI2C(MPU6050_ADDR, 0x1C, 0x00);
}

// 读取加速度
void MPU6050_ReadAccel(int16_t *ax, int16_t *ay, int16_t *az) {
    uint8_t data[6];
    readI2CMultiple(MPU6050_ADDR, 0x3B, data, 6);

    *ax = (data[0] << 8) | data[1];
    *ay = (data[2] << 8) | data[3];
    *az = (data[4] << 8) | data[5];
}

// 读取陀螺仪
void MPU6050_ReadGyro(int16_t *gx, int16_t *gy, int16_t *gz) {
    uint8_t data[6];
    readI2CMultiple(MPU6050_ADDR, 0x43, data, 6);

    *gx = (data[0] << 8) | data[1];
    *gy = (data[2] << 8) | data[3];
    *gz = (data[4] << 8) | data[5];
}

性能优化

提高传输速度

1. 使用更高时钟频率

// 从100kHz提升到400kHz
void I2C_SetFastMode(void) {
    I2C1->CR1 &= ~I2C_CR1_PE;  // 禁用I2C

    // 配置400kHz
    I2C1->CCR = 30 | I2C_CCR_FS;
    I2C1->TRISE = 12;

    I2C1->CR1 |= I2C_CR1_PE;  // 使能I2C
}

// 注意事项:
// - 确保所有设备支持400kHz
// - 减小上拉电阻(4.7kΩ → 2.2kΩ)
// - 缩短走线长度

2. 使用DMA传输

// 配置DMA
void I2C_DMA_Config(void) {
    // 使能DMA时钟
    RCC->AHBENR |= RCC_AHBENR_DMA1EN;

    // 配置DMA通道6(I2C1_TX)
    DMA1_Channel6->CPAR = (uint32_t)&I2C1->DR;
    DMA1_Channel6->CCR = DMA_CCR_MINC |  // 内存地址递增
                         DMA_CCR_DIR |   // 内存到外设
                         DMA_CCR_TCIE;   // 传输完成中断

    // 使能I2C的DMA请求
    I2C1->CR2 |= I2C_CR2_DMAEN;
}

// DMA发送
void I2C_DMA_Send(uint8_t addr, uint8_t *data, uint16_t len) {
    // 设置内存地址和数据长度
    DMA1_Channel6->CMAR = (uint32_t)data;
    DMA1_Channel6->CNDTR = len;

    // 发送起始条件和地址
    I2C_Start();
    I2C_SendAddress(addr, I2C_WRITE);

    // 使能DMA
    DMA1_Channel6->CCR |= DMA_CCR_EN;
}

3. 批量传输

// 不好:逐字节传输
for (int i = 0; i < 100; i++) {
    EEPROM_WriteByte(i, data[i]);
    HAL_Delay(5);  // 每次写入延时
}
// 总时间:100 × 5ms = 500ms

// 好:页写入
for (int i = 0; i < 100; i += 64) {
    EEPROM_WritePage(i, &data[i], 64);
    HAL_Delay(5);  // 每页写入延时
}
// 总时间:2 × 5ms = 10ms

降低功耗

1. 动态时钟控制

// 不使用时关闭I2C
void I2C_Sleep(void) {
    I2C1->CR1 &= ~I2C_CR1_PE;  // 禁用I2C
    RCC->APB1ENR &= ~RCC_APB1ENR_I2C1EN;  // 关闭时钟
}

// 使用时重新使能
void I2C_Wakeup(void) {
    RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;  // 使能时钟
    I2C1->CR1 |= I2C_CR1_PE;  // 使能I2C
}

2. 降低时钟频率

// 低功耗模式:降低到10kHz
void I2C_LowPowerMode(void) {
    I2C1->CR1 &= ~I2C_CR1_PE;
    I2C1->CCR = 1800;  // 36MHz / (2 × 10kHz)
    I2C1->TRISE = 37;
    I2C1->CR1 |= I2C_CR1_PE;
}

3. 增大上拉电阻

标准模式:4.7kΩ → 功耗:VCC²/4.7kΩ = 2.3mA @ 3.3V
低功耗模式:10kΩ → 功耗:VCC²/10kΩ = 1.1mA @ 3.3V

注意:
- 上拉电阻越大,功耗越低
- 但速度会降低
- 需要权衡

提高可靠性

1. 添加超时保护

// 带超时的发送
bool I2C_SendByte_Timeout(uint8_t data, uint32_t timeout) {
    uint32_t start = HAL_GetTick();

    // 等待发送缓冲区空
    while (!(I2C1->SR1 & I2C_SR1_TXE)) {
        if ((HAL_GetTick() - start) > timeout) {
            return false;  // 超时
        }
    }

    I2C1->DR = data;

    // 等待字节传输完成
    start = HAL_GetTick();
    while (!(I2C1->SR1 & I2C_SR1_BTF)) {
        if ((HAL_GetTick() - start) > timeout) {
            return false;  // 超时
        }
    }

    return true;
}

2. 实现重试机制

#define MAX_RETRY 3

bool I2C_WriteWithRetry(uint8_t addr, uint8_t reg, uint8_t data) {
    for (int retry = 0; retry < MAX_RETRY; retry++) {
        I2C_Start();

        if (!I2C_SendAddress(addr, I2C_WRITE)) {
            I2C_Stop();
            HAL_Delay(10);
            continue;
        }

        if (!I2C_SendByte(reg)) {
            I2C_Stop();
            HAL_Delay(10);
            continue;
        }

        if (!I2C_SendByte(data)) {
            I2C_Stop();
            HAL_Delay(10);
            continue;
        }

        I2C_Stop();
        return true;  // 成功
    }

    return false;  // 失败
}

3. 添加CRC校验

// 计算CRC8
uint8_t CRC8(uint8_t *data, uint16_t len) {
    uint8_t crc = 0xFF;

    for (uint16_t i = 0; i < len; i++) {
        crc ^= data[i];
        for (uint8_t j = 0; j < 8; j++) {
            if (crc & 0x80) {
                crc = (crc << 1) ^ 0x31;
            } else {
                crc <<= 1;
            }
        }
    }

    return crc;
}

// 带CRC的写入
bool I2C_WriteWithCRC(uint8_t addr, uint8_t *data, uint16_t len) {
    uint8_t crc = CRC8(data, len);

    I2C_Start();
    I2C_SendAddress(addr, I2C_WRITE);

    for (uint16_t i = 0; i < len; i++) {
        I2C_SendByte(data[i]);
    }

    I2C_SendByte(crc);  // 发送CRC
    I2C_Stop();

    return true;
}

总结

通过本教程学习,你已经掌握了:

  • ✅ I2C基础:工作原理、信号线定义、开漏输出特性
  • ✅ 地址分配:7位/10位地址、地址冲突解决、设备扫描
  • ✅ 上拉电阻:计算方法、选择建议、测试方法
  • ✅ 总线仲裁:多主机模式、时钟拉伸、仲裁机制
  • ✅ 硬件设计:PCB布线、电平转换、ESD保护
  • ✅ 实践应用:STM32/Arduino配置、常见外设使用
  • ✅ 故障排查:常见问题诊断和解决方法
  • ✅ 性能优化:速度提升、功耗降低、可靠性增强

关键要点

  1. I2C两线制:
  2. SDA:双向数据线
  3. SCL:时钟线
  4. 都需要上拉电阻
  5. 开漏输出

  6. 地址寻址:

  7. 7位地址(最常用)
  8. 通过地址区分设备
  9. 最多127个设备
  10. 注意保留地址

  11. 上拉电阻:

  12. 必须添加
  13. 标准模式:4.7kΩ
  14. 快速模式:2.2kΩ
  15. 根据速度和负载调整

  16. 多主机支持:

  17. 自动总线仲裁
  18. 线与逻辑
  19. 支持时钟拉伸
  20. 需要软件处理

  21. 速度模式:

  22. 标准:100kHz
  23. 快速:400kHz(推荐)
  24. 快速+:1MHz
  25. 高速:3.4MHz

实践建议

初学者练习

  1. 基础通信:
  2. 实现MCU与EEPROM的I2C通信
  3. 读写EEPROM数据
  4. 实现地址扫描功能

  5. 传感器应用:

  6. 读取温湿度传感器
  7. 读取加速度计数据
  8. 实现RTC时钟

  9. 多设备系统:

  10. 连接2-3个I2C设备
  11. 实现设备管理
  12. 处理地址冲突

进阶项目

  1. 环境监测站:
  2. 多传感器数据采集
  3. EEPROM数据记录
  4. RTC时间戳

  5. 姿态检测系统:

  6. MPU6050数据读取
  7. 姿态解算
  8. 数据融合

  9. I2C总线分析仪:

  10. 实时监控I2C通信
  11. 协议解析
  12. 错误检测

延伸阅读

相关文章

外设应用

  • EEPROM存储管理
  • RTC实时时钟应用
  • 传感器数据采集

高级主题

  • I2C多路复用器应用
  • SMBus协议详解
  • PMBus电源管理

参考资料

技术标准

  1. I2C规范:NXP UM10204 I2C-bus specification and user manual
  2. SMBus规范:System Management Bus Specification
  3. PMBus规范:Power Management Bus Specification

数据手册

  1. AT24C EEPROM
  2. DS1307 RTC
  3. SHT3x Sensor
  4. MPU6050 IMU
  5. STM32F1 Reference Manual - I2C章节

在线工具

  1. I2C地址计算器
  2. 上拉电阻计算器
  3. Logic分析仪

开源项目

  1. Arduino Wire Library
  2. I2C Tools - Linux I2C工具
  3. TinyI2C - 软件I2C实现

常见应用场景

1. 存储应用

EEPROM: - 配置参数存储 - 校准数据保存 - 用户设置记录

FRAM: - 高速非易失存储 - 无限次写入 - 低功耗应用

2. 时钟应用

RTC: - 实时时钟 - 定时任务 - 时间戳记录

3. 传感器应用

温湿度: - 环境监测 - 气象站 - 智能家居

IMU: - 姿态检测 - 运动追踪 - 无人机控制

4. 显示应用

LCD: - 字符显示 - I2C背包 - 简化接线

OLED: - 图形显示 - 低功耗 - 高对比度

5. 扩展应用

GPIO扩展: - PCF8574(8位) - MCP23017(16位) - 引脚扩展

ADC/DAC: - ADS1115(16位ADC) - MCP4725(12位DAC) - 模拟信号处理

附录

A. I2C速度对照表

模式 频率 上升时间 下降时间 上拉电阻 总线电容
标准 100kHz ≤1000ns ≤300ns 4.7kΩ-10kΩ ≤400pF
快速 400kHz ≤300ns ≤300ns 2.2kΩ-4.7kΩ ≤400pF
快速+ 1MHz ≤120ns ≤120ns 1kΩ-2.2kΩ ≤550pF
高速 3.4MHz ≤80ns ≤80ns 470Ω-1kΩ ≤100pF

B. 常见I2C设备地址表

设备 地址范围 默认地址 可配置
AT24C EEPROM 0x50-0x57 0x50 A2,A1,A0
DS1307 RTC 0x68 0x68 固定
PCF8563 RTC 0x51 0x51 固定
SHT3x 0x44-0x45 0x44 ADDR
MPU6050 0x68-0x69 0x68 AD0
ADXL345 0x53,0x1D 0x53 SDO
BMP280 0x76-0x77 0x77 SDO
PCF8574 LCD 0x20-0x27 0x27 A2,A1,A0
MCP4725 DAC 0x60-0x67 0x60 A2,A1,A0
ADS1115 ADC 0x48-0x4B 0x48 ADDR

C. 故障排查检查清单

硬件检查: - [ ] 电源电压正常(3.3V或5V) - [ ] SDA和SCL都有上拉电阻 - [ ] 上拉电阻值合适(2.2kΩ-10kΩ) - [ ] GND已连接 - [ ] 走线长度合理(<30cm) - [ ] 设备地址无冲突

软件检查: - [ ] I2C时钟已使能 - [ ] GPIO配置为复用开漏 - [ ] 时钟频率配置正确 - [ ] 地址格式正确(7位) - [ ] 读写位正确(0=写,1=读) - [ ] ACK/NACK处理正确

测试方法: - [ ] 万用表测量SDA/SCL电压(应接近VCC) - [ ] 示波器观察波形 - [ ] 逻辑分析仪抓包 - [ ] 地址扫描测试 - [ ] 回环测试

D. 性能对比

参数 I2C SPI UART
线数 2 4+ 2
速度 中(100kHz-3.4MHz) 快(MHz级) 慢(kbps级)
全双工 否 是 是
多主机 支持 困难 不支持
多从机 简单(地址) 简单(CS) 不支持
距离 短(<1m) 短(<1m) 中(数米)
功耗 低-中 中 低
复杂度 中 中 低
成本 低 中 低

反馈与支持: - 如果你在学习过程中遇到问题,欢迎在评论区留言 - 分享你的I2C项目经验,与其他学习者交流 - 发现文档错误?请提交Issue帮助我们改进

版权声明:本文采用 CC BY-SA 4.0 协议,欢迎分享和改编,但请注明出处。