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 外设分了三层:配置层决定怎么连、功能层决定怎么传、硬件层决定怎么跑。你想想看,只要把这三层理解透了,写驱动就跟搭积木一样简单。
我的建议:刚开始学的时候,先用中断模式把单个传感器调通。等流程跑顺了,再考虑要不要上 DMA。别一上来就追求极致性能,稳定可靠才是第一位的。