3. ESP32 I2C外设架构

做嵌入式开发这么多年,我接触过不少 MCU 的 I2C 外设。坦白说,ESP32 的 I2C 控制器设计得相当灵活,但坑也不少。这一章,咱们就把它彻底拆开看看。

ESP32 I2C 控制器特性

ESP32 内部有两个独立的 I2C 控制器,分别叫 I2C0 和 I2C1。它们功能完全一样,可以同时工作。我个人习惯把 I2C0 留给板载传感器,I2C1 留给外接扩展。

特性 参数
控制器数量 2 个(I2C0, I2C1)
支持模式 主机模式、从机模式
最大时钟频率 标准模式 100kHz,快速模式 400kHz,快速+模式 1MHz
数据缓冲 硬件 32 字节 FIFO
地址位宽 7 位或 10 位
中断支持 14 种中断事件
DMA 支持 支持,通过 GDMA 通道

这里有个关键点:ESP32 的 I2C 控制器是 双缓冲架构。发送数据时,你可以往 FIFO 里写下一包数据,当前数据还在总线上传输。这能有效提高吞吐量。

核心优势:硬件 FIFO + 可编程中断,让 CPU 不用死等总线操作。这在采集多传感器数据时特别有用。

I2C 引脚映射与配置

ESP32 的 GPIO 是矩阵互联的。I2C 的 SDA 和 SCL 信号可以映射到几乎任意 GPIO 引脚。嗯,这里要注意——不是所有引脚都能随便用。

我踩过这个坑:有一次把 SCL 映射到了 GPIO 12,结果上电后一直通信失败。查了半天才发现,GPIO 12 是 MTDI 引脚,上电时序会影响 flash 电压。

引脚选择避坑指南

  • 避开启动引脚:GPIO 0、2、5、12、15 在启动时有特殊功能,尽量别用
  • 注意内部上拉:I2C 总线需要外部上拉电阻,ESP32 内部有弱上拉,但只适合低速调试
  • 电平匹配:ESP32 是 3.3V 电平,如果接 5V 传感器,需要电平转换

配置代码很简单,用 ESP-IDF 的 i2c_set_pin() 函数就行:

i2c_config_t conf = {
    .mode = I2C_MODE_MASTER,
    .sda_io_num = GPIO_NUM_21,
    .scl_io_num = GPIO_NUM_22,
    .sda_pullup_en = GPIO_PULLUP_ENABLE,
    .scl_pullup_en = GPIO_PULLUP_ENABLE,
    .master.clk_speed = 400000
};
i2c_param_config(I2C_NUM_0, &conf);

警告:千万不要在总线上带电插拔传感器。我曾经烧坏过一个 BMP280,就是因为调试时手贱热插拔了。I2C 是开漏结构,带电插拔会产生瞬间大电流。

I2C 时钟频率设置

时钟频率直接影响通信速度和稳定性。ESP32 的 I2C 时钟源是 APB 时钟(通常 80MHz),通过分频得到目标频率。

计算公式其实不复杂:

SCL 高电平时间 = (SCL_HIGH_PERIOD + 1) * SCLK_DIV * 12.5ns
SCL 低电平时间 = (SCL_LOW_PERIOD + 1) * SCLK_DIV * 12.5ns

但说实话,我很少手动算这些。直接用 ESP-IDF 的 i2c_param_config() 设置 master.clk_speed 字段就行,驱动会自动算好分频系数。

目标频率 实际设置值 适用场景
100kHz 100000 长距离走线、兼容旧设备
400kHz 400000 大多数传感器,推荐使用
1MHz 1000000 高速传感器,需注意总线电容

经验之谈:我一般先用 400kHz 调试。如果通信不稳定,再降到 100kHz。别一上来就追求 1MHz,总线电容和上拉电阻没匹配好,高速下全是乱码。

I2C 中断与 DMA 模式

这是 ESP32 I2C 外设最强大的地方。你可以让硬件自己处理总线事务,CPU 去干别的活。

中断模式

ESP32 的 I2C 控制器有 14 种中断事件,常用的就这几个:

  • 发送完成:数据已发出,可以准备下一包
  • 接收完成:数据已收到,可以读取
  • 仲裁丢失:总线冲突,需要重试
  • 超时错误:从机没响应,SCL 被拉低太久

我习惯在中断服务函数里只做标志位设置,具体数据处理放到任务循环里。中断里做太多事,容易影响系统实时性。

static void i2c_isr_handler(void *arg) {
    uint32_t int_status = i2c_ll_get_intr_status(I2C_LL_GET_HW(I2C_NUM_0));
    if (int_status & I2C_LL_INTR_TRANS_COMPLETE) {
        xSemaphoreGiveFromISR(sem_i2c_done, NULL);
    }
    i2c_ll_clear_intr_status(I2C_LL_GET_HW(I2C_NUM_0), int_status);
}

DMA 模式

DMA 模式适合大数据量传输,比如从摄像头或音频芯片读取数据块。ESP32 使用 GDMA(通用 DMA)来搬运 I2C 数据。

启用 DMA 后,CPU 只需要配置好 DMA 描述符,剩下的数据搬运全由硬件完成。我做过一个项目,用 DMA 模式从 9 轴 IMU 读取数据,CPU 占用率从 30% 降到了 2%。

什么时候用 DMA:单次传输超过 32 字节,或者需要周期性批量采集。如果只是读个温度值(2 字节),中断模式就够了,DMA 反而增加配置复杂度。

配置 DMA 的代码片段:

i2c_master_bus_config_t bus_config = {
    .i2c_port = I2C_NUM_0,
    .sda_io_num = GPIO_NUM_21,
    .scl_io_num = GPIO_NUM_22,
    .clk_source = I2C_CLK_SRC_DEFAULT,
    .glitch_ignore_cnt = 7,
    .intr_priority = 0,
    .trans_queue_depth = 4,
    .flags.enable_internal_pullup = true,
};
i2c_new_master_bus(&bus_config, &bus_handle);

注意:DMA 模式下,数据缓冲区必须放在 DRAM 中,不能放在 flash 或 PSRAM。否则 DMA 访问不到,会触发异常。我因为这个 bug 调了整整一个下午。

知识体系总览

下面这张图把 ESP32 I2C 外设的核心模块和关系画清楚了:

ESP32 I2C 外设架构图 ESP32 I2C 外设架构 I2C 控制器 (I2C0 / I2C1) 时钟发生器 (SCLK) 数据 FIFO (32 字节) 中断控制器 频率分频 SCL 高/低电平时间 发送/接收缓冲 硬件自动管理 14 种中断事件 可编程使能 GPIO 矩阵 (引脚映射) GDMA 通道 (可选) APB 总线 (80MHz) 核心思路:硬件 FIFO 减少 CPU 干预,中断/DMA 实现非阻塞通信 时钟频率、引脚映射、中断配置三者必须协同,缺一不可 配置层 功能层 硬件层

从图上能看出来,ESP32 的 I2C 外设分了三层:配置层决定怎么连、功能层决定怎么传、硬件层决定怎么跑。你想想看,只要把这三层理解透了,写驱动就跟搭积木一样简单。

我的建议:刚开始学的时候,先用中断模式把单个传感器调通。等流程跑顺了,再考虑要不要上 DMA。别一上来就追求极致性能,稳定可靠才是第一位的。