示例:I2C 主机通信
本教程的核心逻辑适用于所有 ESP32 开发板,但所有操作步骤均以 微雪 ESP32-S3-Zero 迷你开发板 为例进行讲解。如果您使用其他型号的开发板,请根据实际情况修改相应设置。
本教程介绍如何使用乐鑫 ESP-IDF 框架的 I2C 主机驱动,扫描总线上的所有 I2C 设备并打印它们的地址。演示 I2C 总线初始化与
i2c_master_probe()的用法。
1. I2C 外设
I2C(Inter-Integrated Circuit)是一种两线制串行通信协议,常用于连接传感器、显示屏、EEPROM 等外设。其特点:
- 两线通信:仅需 SDA(数据线)和 SCL(时钟线),加共地共三根线。
- 主从架构:一根总线上可挂多个设备,每个设备有唯一的 7 位(或 10 位)地址。主设备发起所有通信,从设备根据地址响应。
- 开漏 + 上拉:所有设备只能把信号线拉低,回高电平依赖上拉电阻。这是 I2C 多设备共享总线的物理基础。
ESP32-S3 内置 2 个 I2C 控制器(I2C_NUM_0 和 I2C_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 通用步骤
-
包含头文件
#include "driver/i2c_master.h"并在
main/CMakeLists.txt中声明依赖:REQUIRES esp_driver_i2c。 -
创建总线:填一个
i2c_master_bus_config_t,调用i2c_new_master_bus()获得i2c_master_bus_handle_t。 -
添加设备:填一个
i2c_device_config_t(地址、SCL 速率),调用i2c_master_bus_add_device()获得i2c_master_dev_handle_t。 -
收发数据:
- 写:
i2c_master_transmit() - 读:
i2c_master_receive() - 写后读(常用于读寄存器):
i2c_master_transmit_receive() - 探测某地址是否有设备应答:
i2c_master_probe()(只用 bus handle,无需 device handle)
- 写:
-
(可选)释放资源:
i2c_master_bus_rm_device()、i2c_del_master_bus()。
2. 示例项目
本示例实现一个 I2C Scanner:遍历 7 位 I2C 地址空间(0x08 ~ 0x77),用 i2c_master_probe() 逐个探测,把发现的设备地址以网格形式打印出来。
拿到任何陌生 I2C 模块时,Scanner 是确认接线正确、设备应答正常、地址实际值的最快方法。
2.1 电路
需要使用的器件有:
- 任意 I2C 模块(本示例使用 微雪 1.5 寸 OLED 模块,地址
0x3D,板载上拉电阻) - 面包板 * 1
- 导线
- ESP32 开发板(微雪 ESP32-S3-Zero 迷你开发板)
接线表:
| 开发板引脚 | I2C 模块 | 说明 |
|---|---|---|
| GPIO 1 | SDA | I2C 数据线 |
| GPIO 2 | SCL | I2C 时钟线 |
| 3.3V | VCC | 电源正极 |
| GND | GND | 电源负极 |
ESP32-S3-Zero 引脚图


本示例使用的 OLED 模块已板载上拉电阻,无需额外连接。如果连接的是不含上拉电阻的裸芯片或自制模块,请在 SDA、SCL 各串一个 4.7 kΩ 电阻到 3.3V。本示例代码也启用了 ESP32 的内部弱上拉作为兜底,可保证不接外部上拉的情况下短距离通信仍可工作。
2.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 构建并烧录
-
配置烧录选项
首先,在构建和烧录之前,请务必检查并设置正确的目标设备、串口和烧录方式。参考 第 2 节 运行示例 - 1.3 配置项目。
-
点击
一键自动依次执行构建、烧录和监视。
-
烧录完成后,串口监视器中应输出类似
i2cdetect的网格。OLED 模块(地址0x3D)会出现在第 4 行第 14 列:Scanning I2C bus...0 1 2 3 4 5 6 7 8 9 a b c d e f00: -- -- -- -- -- -- -- --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_t、i2c_new_master_bus()、i2c_master_probe()等 API。属于esp_driver_i2c组件,需在CMakeLists.txt中REQUIRES 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-0x07和0x78-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. 参考链接
- ESP-IDF 编程指南 - ESP32-S3 I2C API 参考
- ESP-IDF 示例:peripherals/i2c/i2c_tools — 官方 i2c-tools 命令行工具,包含 i2cdetect / i2cget / i2cset 等
- ESP-IDF I2C 示例:peripherals/i2c
- I²C-bus specification and user manual(NXP 官方规范)