跳到主要内容

示例:I2C 主机通信

重要提示:关于开发板的兼容性

本教程的核心逻辑适用于所有 ESP32 开发板,但所有操作步骤均以 微雪 ESP32-S3-Zero 迷你开发板 为例进行讲解。如果您使用其他型号的开发板,请根据实际情况修改相应设置。

本教程介绍如何使用乐鑫 ESP-IDF 框架的 I2C 主机驱动,扫描总线上的所有 I2C 设备并打印它们的地址。演示 I2C 总线初始化与 i2c_master_probe() 的用法。

1. I2C 外设

I2C 连接

I2C(Inter-Integrated Circuit)是一种两线制串行通信协议,常用于连接传感器、显示屏、EEPROM 等外设。其特点:

  • 两线通信:仅需 SDA(数据线)和 SCL(时钟线),加共地共三根线。
  • 主从架构:一根总线上可挂多个设备,每个设备有唯一的 7 位(或 10 位)地址。主设备发起所有通信,从设备根据地址响应。
  • 开漏 + 上拉:所有设备只能把信号线拉低,回高电平依赖上拉电阻。这是 I2C 多设备共享总线的物理基础。

ESP32-S3 内置 2 个 I2C 控制器I2C_NUM_0I2C_NUM_1),均支持主机/从机模式,SDA/SCL 可通过 GPIO 矩阵 映射到几乎任意空闲 GPIO。

1.1 上拉电阻

I2C 总线的 SDA、SCL 两根线必须连到上拉电阻,否则空闲时电平不确定,通信无法进行。三种常见做法:

来源阻值适用场景
模块内置通常 10 kΩ多数现成模块(如微雪 OLED)已板载,直接连接即可
外部独立4.7 kΩ 推荐总线较长、设备多、速率高(400 kHz 以上)、或自制电路
ESP32 内部约 45 kΩ(弱)应急、原型验证;长导线或高速通信不可靠

ESP32 内部上拉阻值较大(数十 kΩ),仅适合短距离、低速、设备数量少的场景;正式电路应使用外部 4.7 kΩ 上拉电阻或选用已内置上拉的模块。

1.2 bus + device 模型

ESP-IDF 的 I2C 主机驱动 driver/i2c_master.h 用两层句柄描述硬件:

  • 总线 (bus) — 对应物理上的一组 SDA/SCL 引脚。i2c_master_bus_handle_t 描述这条总线本身(引脚、时钟源、毛刺过滤等)。
  • 设备 (device) — 对应挂在总线上的某个具体 I2C 芯片。i2c_master_dev_handle_t 描述这个设备的属性(地址、SCL 速率等)。

一条总线可以 add_device 多次,每个设备维护自己的速率配置。后续 i2c_master_transmit() / i2c_master_receive() 只需要传入对应的 device handle,驱动会自动切换到该设备的速率执行传输。

备注

ESP-IDF 还保留有一套老的 I2C 驱动 driver/i2c.h,在 v6.0 中已标记 End-of-Life,将于 v7.0 移除。若在网上看到的教程里 #include "driver/i2c.h",那是老 API,按本文写法替换为 driver/i2c_master.h 即可。

1.3 通用步骤

  1. 包含头文件

    #include "driver/i2c_master.h"

    并在 main/CMakeLists.txt 中声明依赖:REQUIRES esp_driver_i2c

  2. 创建总线:填一个 i2c_master_bus_config_t,调用 i2c_new_master_bus() 获得 i2c_master_bus_handle_t

  3. 添加设备:填一个 i2c_device_config_t(地址、SCL 速率),调用 i2c_master_bus_add_device() 获得 i2c_master_dev_handle_t

  4. 收发数据

    • 写:i2c_master_transmit()
    • 读:i2c_master_receive()
    • 写后读(常用于读寄存器):i2c_master_transmit_receive()
    • 探测某地址是否有设备应答:i2c_master_probe()只用 bus handle,无需 device handle
  5. (可选)释放资源i2c_master_bus_rm_device()i2c_del_master_bus()

2. 示例项目

本示例实现一个 I2C Scanner:遍历 7 位 I2C 地址空间(0x08 ~ 0x77),用 i2c_master_probe() 逐个探测,把发现的设备地址以网格形式打印出来。

拿到任何陌生 I2C 模块时,Scanner 是确认接线正确、设备应答正常、地址实际值的最快方法。

2.1 电路

需要使用的器件有:

接线表:

开发板引脚I2C 模块说明
GPIO 1SDAI2C 数据线
GPIO 2SCLI2C 时钟线
3.3VVCC电源正极
GNDGND电源负极
ESP32-S3-Zero 引脚图

ESP32-S3-Zero-Pinout

接线图
上拉电阻说明

本示例使用的 OLED 模块已板载上拉电阻,无需额外连接。如果连接的是不含上拉电阻的裸芯片或自制模块,请在 SDA、SCL 各串一个 4.7 kΩ 电阻到 3.3V。本示例代码也启用了 ESP32 的内部弱上拉作为兜底,可保证不接外部上拉的情况下短距离通信仍可工作。

2.2 创建项目

  1. 创建一个项目。如果不清楚如何操作,请参考 从模板创建项目

  2. 查看 I2C API 参考。根据文档中的指引完成以下步骤。

    首先在 main.c 中包含头文件:

    #include "driver/i2c_master.h"

    然后在 main/CMakeLists.txt 中声明 esp_driver_i2c 组件:

    idf_component_register(SRCS "main.c"
    INCLUDE_DIRS "."
    REQUIRES esp_driver_i2c)

2.3 示例代码

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_err.h"
#include "driver/gpio.h"
#include "driver/i2c_master.h"

static const char *TAG = "example";

#define I2C_PORT I2C_NUM_0
#define I2C_SDA_PIN GPIO_NUM_1
#define I2C_SCL_PIN GPIO_NUM_2
#define I2C_PROBE_TIMEOUT_MS 50

static i2c_master_bus_handle_t bus_handle;

static void i2c_bus_init(void)
{
i2c_master_bus_config_t bus_cfg = {
.clk_source = I2C_CLK_SRC_DEFAULT,
.i2c_port = I2C_PORT,
.sda_io_num = I2C_SDA_PIN,
.scl_io_num = I2C_SCL_PIN,
.glitch_ignore_cnt = 7,
.flags.enable_internal_pullup = true,
};
ESP_ERROR_CHECK(i2c_new_master_bus(&bus_cfg, &bus_handle));
}

static void i2c_scan(void)
{
int found = 0;
printf("\nScanning 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;

// 跳过 I2C 协议保留地址区:0x00-0x07 和 0x78-0x7F
if (addr < 0x08 || addr > 0x77) {
printf(" ");
continue;
}

esp_err_t err = i2c_master_probe(bus_handle, addr, I2C_PROBE_TIMEOUT_MS);
if (err == ESP_OK) {
printf("%02x ", addr);
found++;
} else {
printf("-- ");
}
}
printf("\n");
}

if (found == 0) {
ESP_LOGW(TAG, "No I2C devices found. Check wiring and pull-ups.");
} else {
ESP_LOGI(TAG, "Scan complete: %d device(s) found.", found);
}
}

void app_main(void)
{
i2c_bus_init();

while (1) {
i2c_scan();
vTaskDelay(pdMS_TO_TICKS(5000));
}
}

2.4 构建并烧录

  1. 配置烧录选项

    首先,在构建和烧录之前,请务必检查并设置正确的目标设备、串口和烧录方式。参考 第 2 节 运行示例 - 1.3 配置项目

    VS Code 工具栏

  2. 点击 VS Code 一键构建烧录监视图标 一键自动依次执行构建、烧录和监视。

  3. 烧录完成后,串口监视器中应输出类似 i2cdetect 的网格。OLED 模块(地址 0x3D)会出现在第 4 行第 14 列:

    Scanning I2C bus...
    0 1 2 3 4 5 6 7 8 9 a b c d e f
    00: -- -- -- -- -- -- -- --
    10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
    20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
    30: -- -- -- -- -- -- -- -- -- -- -- -- -- 3d -- --
    40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
    50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
    60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
    70: -- -- -- -- -- -- -- --
    I (xxxx) example: Scan complete: 1 device(s) found.

    若整张表都是 -- 且日志显示 No I2C devices found,请按以下顺序排查:

    • SDA / SCL 是否接反;
    • 模块是否上电(VCC、GND);
    • 模块是否带上拉电阻;不带时可外接 4.7 kΩ 上拉至 3.3V,或保留代码中 .flags.enable_internal_pullup = true

2.5 代码解析

1. 包含头文件

#include "driver/gpio.h"
#include "driver/i2c_master.h"
  • driver/i2c_master.h:ESP-IDF 新版 I2C 主机驱动入口。包含 i2c_master_bus_config_ti2c_new_master_bus()i2c_master_probe() 等 API。属于 esp_driver_i2c 组件,需在 CMakeLists.txtREQUIRES esp_driver_i2c
  • driver/gpio.h:提供 GPIO_NUM_x 枚举,用于书写 SDA / SCL 引脚号。

2. 定义常量

#define I2C_PORT I2C_NUM_0
#define I2C_SDA_PIN GPIO_NUM_1
#define I2C_SCL_PIN GPIO_NUM_2
#define I2C_PROBE_TIMEOUT_MS 50
  • I2C_NUM_0:使用 0 号 I2C 控制器。ESP32-S3 有 0、1 两个控制器,本示例任选其一即可。也可填 -1 让驱动自动选空闲端口,在多组件项目中可避免端口冲突。乐鑫官方 i2c_tools 示例用的就是这种写法。
  • I2C_SDA_PIN / I2C_SCL_PIN:I2C 通过 GPIO 矩阵映射到任意空闲 GPIO,本示例使用 GPIO1 / GPIO2,与 Arduino 教程接线一致。
  • I2C_PROBE_TIMEOUT_MS:每次 probe 的超时时间。50 ms 足以覆盖 100 kHz 速率下一次地址传输(约几十 μs);过短会误报 ESP_ERR_TIMEOUT,过长会让整轮扫描变慢。

3. 总线初始化(i2c_bus_init

i2c_master_bus_config_t bus_cfg = {
.clk_source = I2C_CLK_SRC_DEFAULT,
.i2c_port = I2C_PORT,
.sda_io_num = I2C_SDA_PIN,
.scl_io_num = I2C_SCL_PIN,
.glitch_ignore_cnt = 7,
.flags.enable_internal_pullup = true,
};
ESP_ERROR_CHECK(i2c_new_master_bus(&bus_cfg, &bus_handle));
  • clk_source = I2C_CLK_SRC_DEFAULT:由驱动选择默认时钟源(通常是 APB 时钟)。
  • i2c_port / sda_io_num / scl_io_num:指定控制器编号和物理引脚。
  • glitch_ignore_cnt = 7:毛刺过滤计数。窄于 7 个时钟周期的脉冲会被硬件忽略,可滤除接线上的抖动。7 是官方示例的常用值。
  • flags.enable_internal_pullup = true:启用 GPIO 内部弱上拉。属于兜底措施,详见 1.1 节。

i2c_new_master_bus() 返回 bus_handle,整条总线由它代表。

备注

本示例只创建了 bus,没有调用 i2c_master_bus_add_device()。这是因为 i2c_master_probe() 的设计就是用来扫描"还不知道存在哪些设备"的总线,它只需要 bus handle 和待探测的地址,不要求事先注册 device。做正常数据收发(i2c_master_transmit / _receive)则必须先 add_device 拿到 device handle。

4. 扫描逻辑(i2c_scan

按 16 列 × 8 行打印地址网格,每个位置打印探测结果:

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;
}
esp_err_t err = i2c_master_probe(bus_handle, addr, I2C_PROBE_TIMEOUT_MS);
...
}
}
  • 跳过保留地址:7 位 I2C 地址范围是 0x00 ~ 0x7F,其中 0x00-0x070x78-0x7F 是 I2C 规范保留给特殊用途(广播、10 位地址等),不分配给普通设备,扫描时应跳过。

  • i2c_master_probe(bus_handle, addr, timeout):向 addr 发送一次地址 + 写命令,根据从机的应答决定返回值:

    返回值含义
    ESP_OK收到 ACK,该地址有设备
    ESP_ERR_NOT_FOUND收到 NACK,该地址无设备
    ESP_ERR_TIMEOUT总线无 ACK/NACK 响应。官方文档明确指出这通常是 SDA/SCL 上拉缺失或不当,可先检查上拉再排查接线。

    本示例只区分"有 (ESP_OK)"和"无 (其他)"两种情况。如果想细分错误类型,可以单独处理 ESP_ERR_TIMEOUT 来提示上拉问题。

5. 主循环

while (1) {
i2c_scan();
vTaskDelay(pdMS_TO_TICKS(5000));
}

每 5 秒扫描一次。这样在调试时可以热拔插模块,立即看到设备出现或消失的变化,方便确认接线。

3. 参考链接