3、I2C协议详解:物理层、时序、地址、读写操作,以及ESP32的I2C外设配置

各位同学,今天我们来啃一块硬骨头——I2C协议。说实话,I2C这玩意儿看起来简单,就两根线,但坑是真不少。我最早用I2C驱动传感器时,被时序问题折磨了好几天,后来才发现是上拉电阻没选对。嗯,咱们今天就把这些坑一个个填平。

3.1 I2C物理层:就两根线,但别小看它

I2C总线只需要两根线:SCL(时钟线)和SDA(数据线)。所有设备都挂在这两根线上,通过地址区分谁跟谁说话。

你想想看,这么多设备共用两根线,怎么保证不乱套?关键在于两点:

  • 开漏输出:每个设备的SCL和SDA引脚都是开漏结构,只能拉低,不能拉高。拉高靠外部的上拉电阻。
  • 线与逻辑:只要有一个设备拉低总线,总线就是低电平。这特性用来做仲裁和时钟拉伸。

上拉电阻怎么选? 我一般用4.7kΩ做通用值。总线电容大(线长超过20cm)就用2.2kΩ,想省电就用10kΩ。但别超过10kΩ,否则上升沿太慢,通讯会出错。

标准I2C速率分三档:

模式 速率 适用场景
标准模式 100 kbit/s 大多数传感器、EEPROM
快速模式 400 kbit/s 高速传感器、显示驱动
高速模式 3.4 Mbit/s 视频、大容量存储(ESP32不支持)

ESP32的I2C外设最高支持到1 MHz,但我建议你老老实实用400 kHz以下。为什么?因为很多传感器芯片的时序余量不大,跑快了容易丢数据。我在项目里吃过这个亏。

3.2 I2C时序:每个脉冲都有讲究

I2C的时序说白了就是一套握手规则。咱们来看几个关键动作:

3.2.1 起始条件与停止条件

这两个条件由主机产生,是每次通讯的开始和结束:

  • 起始条件(S):SCL高电平时,SDA从高变低
  • 停止条件(P):SCL高电平时,SDA从低变高

我刚开始学的时候老记反,后来有个老工程师教我一招:“起始是下降,停止是上升”。记住了吧?

3.2.2 数据有效性

数据采样有个铁律:SCL高电平时,SDA必须稳定。数据变化只能在SCL低电平时发生。这跟SPI正好相反,别搞混了。

3.2.3 应答机制

每发送完一个字节(8位),接收方必须回一个ACK(低电平)或NACK(高电平)。主机在第9个时钟周期释放SDA,从机拉低表示“收到了”。

注意: 如果从机没回ACK,说明设备不存在、忙或者出错了。我建议你在驱动里加上ACK检测,别盲目相信每次通讯都能成功。

3.3 I2C地址:7位还是10位?

标准I2C地址是7位的,范围0x00~0x7F。但有些地址是保留的,实际可用的是0x08~0x77。

每个I2C设备出厂时都有一个固定地址,通常还留出几个引脚让你改地址。比如常见的MPU6050,AD0引脚接地时地址是0x68,接VCC时是0x69。这样一条总线上就能挂两个同样的传感器。

10位地址用得少,ESP32也支持,但我个人建议:能用7位就别用10位,兼容性好,代码也简单。

3.4 I2C读写操作:主机和从机的对话

I2C的读写操作其实就两种格式:

3.4.1 写操作

主机往从机写数据,流程如下:

  1. 主机发送起始条件
  2. 主机发送从机地址 + 写位(0)
  3. 等待从机ACK
  4. 主机发送寄存器地址(要写到哪里)
  5. 等待从机ACK
  6. 主机发送数据字节
  7. 等待从机ACK
  8. 重复步骤6-7,直到数据发完
  9. 主机发送停止条件

3.4.2 读操作

读操作稍微复杂点,需要先写寄存器地址,再重新开始读:

  1. 主机发送起始条件
  2. 主机发送从机地址 + 写位(0)
  3. 等待从机ACK
  4. 主机发送寄存器地址
  5. 等待从机ACK
  6. 主机发送重复起始条件
  7. 主机发送从机地址 + 读位(1)
  8. 等待从机ACK
  9. 从机发送数据,主机回ACK
  10. 重复步骤9,直到数据收完
  11. 主机发送NACK + 停止条件

小技巧: 读操作最后一定要发NACK,告诉从机“别发了,我要停了”。否则从机会一直发,总线就卡死了。我曾经忘了这一步,调试了一下午才发现。

3.5 ESP32的I2C外设配置

ESP32有两个I2C控制器,可以配置为主机或从机。咱们做传感器驱动,主要用主机模式。

配置步骤其实就四步:

  1. 配置引脚:指定SCL和SDA用哪个GPIO
  2. 配置时钟:设置通讯速率
  3. 安装驱动:分配资源,启动I2C
  4. 读写操作:用API收发数据

来看代码,我习惯用ESP-IDF的I2C驱动库:

#include "driver/i2c.h"

#define I2C_MASTER_NUM      I2C_NUM_0
#define I2C_MASTER_SCL_IO   22
#define I2C_MASTER_SDA_IO   21
#define I2C_MASTER_FREQ_HZ  100000  // 100kHz

void i2c_master_init(void)
{
    i2c_config_t conf = {
        .mode = I2C_MODE_MASTER,
        .sda_io_num = I2C_MASTER_SDA_IO,
        .scl_io_num = I2C_MASTER_SCL_IO,
        .sda_pullup_en = GPIO_PULLUP_ENABLE,
        .scl_pullup_en = GPIO_PULLUP_ENABLE,
        .master.clk_speed = I2C_MASTER_FREQ_HZ,
    };

    i2c_param_config(I2C_MASTER_NUM, &conf);
    i2c_driver_install(I2C_MASTER_NUM, conf.mode, 0, 0, 0);
}

这里有个细节:sda_pullup_en和scl_pullup_en是使能内部上拉。但ESP32的内部上拉电阻约45kΩ,只适合短距离、低速场景。我建议外部再加4.7kΩ上拉电阻,更可靠。

3.5.1 读写API实战

ESP-IDF提供了两个核心函数:

// 写操作
esp_err_t i2c_master_write_to_device(i2c_port_t i2c_num, 
                                      uint8_t dev_addr,
                                      const uint8_t *data,
                                      size_t data_len,
                                      TickType_t timeout);

// 读操作
esp_err_t i2c_master_read_from_device(i2c_port_t i2c_num,
                                       uint8_t dev_addr,
                                       uint8_t *data,
                                       size_t data_len,
                                       TickType_t timeout);

这两个函数封装了起始条件、地址、ACK检测等细节,用起来很方便。但要注意,timeout参数的单位是FreeRTOS的tick,一般用pdMS_TO_TICKS(1000)表示1秒超时。

来看一个完整的读写示例,驱动一个虚拟的温度传感器:

#define SENSOR_ADDR  0x48
#define TEMP_REG     0x00

float read_temperature(void)
{
    uint8_t reg = TEMP_REG;
    uint8_t raw[2] = {0};

    // 先写寄存器地址
    i2c_master_write_to_device(I2C_MASTER_NUM, SENSOR_ADDR, 
                               ®, 1, pdMS_TO_TICKS(100));

    // 再读两个字节
    i2c_master_read_from_device(I2C_MASTER_NUM, SENSOR_ADDR,
                                raw, 2, pdMS_TO_TICKS(100));

    // 解析数据(假设是12位精度)
    int16_t temp = (raw[0] << 8) | raw[1];
    return temp * 0.0625f;
}

注意: 有些传感器要求连续读写,中间不能有停止条件。这时候要用i2c_master_write_read_device函数,它内部用重复起始条件,不会产生停止条件。

3.6 I2C协议知识体系

下面这张图把I2C的核心知识点串起来了,方便你复习:

I2C协议知识体系 I2C协议 物理层 SCL + SDA 两根线 时序 起始/停止/数据/ACK 地址 7位 / 10位 写操作 地址+寄存器+数据 读操作 写地址+重复起始+读 ESP32配置 引脚/时钟/驱动安装 核心要点 1. 开漏输出 + 上拉电阻是I2C的基础 2. 时序必须严格:SCL高时SDA不能变 3. 读操作要用重复起始条件,不能有停止

3.7 避坑指南

最后,分享几个我踩过的坑:

  • 总线锁死:从机没回ACK或者时钟拉伸太久,总线就卡住了。解决办法是加超时检测,或者用GPIO模拟I2C时手动拉时钟恢复。
  • 地址左移:很多数据手册给的地址是7位值,但ESP-IDF的API需要8位地址(左移1位)。比如手册写0x68,代码里要用0xD0。我建议统一用7位地址,让API自己处理移位。
  • 上拉电阻不匹配:总线电容大时用大电阻,上升沿太慢,通讯会出错。我一般用示波器看波形,上升沿超过1μs就换小电阻。