目录
[2.1 I2C.c](#2.1 I2C.c)
[2.2 I2C.h](#2.2 I2C.h)
[2.3 代码解释](#2.3 代码解释)
[void I2C_master_init(void)](#void I2C_master_init(void))
[void I2C_master_T_R(void)](#void I2C_master_T_R(void))
[void I2C_slave_init(void)](#void I2C_slave_init(void))
[void I2C_slave_task(void *arg)](#void I2C_slave_task(void *arg))
[2.4 mian](#2.4 mian)
前言
本文基于第一章创建好的工程,来写一个基于I2C的通信工程,本章不对I2C这个通讯技术本身进行讲解,在STM32篇已经涉及到了。重点在于梳理I2C在ESP32上的应用
开发板芯片是ESP32-P4、ESP-IDF版本是6.0。
一、ESP32中I2C
ESP32-P4 在主系统有两个 I2C 控制器,在低功耗系统有一个 I2C 控制器。其中,主系统中的 I2C 控制器既可作 为主机又可作为从机,低功耗系统中的 I2C 控制器则只可作为主机使用,特点是在主系统休眠时仍能工作。
所有I2C 控制器均支持标准模式 (100 Kbit/s)和快速模式 (400 Kbit/s)
I2C 控制器通过 GPIO 交换矩阵可配置使用任意 GPIO 管脚。 LP_I2C 控制器通过 LP GPIO 交换矩阵可配置使用任意 LP GPIO 管脚。这是ESP32的一个特点
I2C 总线上可以挂载一个或多个主机以及一个或多个从机。
I3C
I3C 是 MIPI 联盟推出的 I2C 升级版 ,核心是:速度大幅提升、功耗更低、支持带内中断、动态地址、热插拔、多主仲裁,且向下兼容 I2C。
| 维度 | I2C | I3C |
|---|---|---|
| 全称 | Inter-Integrated Circuit | Improved Inter-Integrated Circuit |
| 制定组织 | NXP(原 Philips) | MIPI Alliance |
| 物理层 | 纯开漏(Open-Drain),必须外部上拉 | 混合驱动:SCL 开漏,SDA 高电平推挽(Push-Pull) |
| 最高速率 | 标准 100 kbps / 快速 400 kbps / 高速 3.4 Mbps | SDR:12.5 Mbps;HDR‑TSP:~33 Mbps;多通道可达 100 Mbps+ |
| 地址机制 | 7/10 位静态地址,易冲突,不支持热加入 | 7 位动态地址(主机分配),支持热插拔(Hot‑Join) |
| 中断 | 必须额外 GPIO 做中断线 | 带内中断(IBI),无需额外引脚 |
| 功耗 | 上拉持续耗电,静态功耗高 | 推挽无持续上拉,高速传输后快速休眠,功耗仅 I2C 1/5~1/18 |
| 时钟拉伸 | 支持,易导致总线卡死 | 严格限制,兼容 I2C 时需关闭时钟拉伸 |
| 多主 | 支持但仲裁简单 | 支持多主仲裁、主角色切换(CRR) |
| 兼容性 | 仅 I2C 设备 | 向下兼容 I2C 从机,可混合组网 |
| 从机模式 | 支持 | 支持,且带标准化管理命令(CCC) |
关键差异详解
- 速度与驱动
- I2C:全程开漏,靠上拉电阻拉高,速率受限、功耗高。
- I3C:SCL 开漏兼容 I2C,SDA 高电平用推挽,大幅提速并降低功耗。
- 地址与设备管理
- I2C:静态地址,多设备易冲突,无法热插拔。
- I3C:主机动态分配地址,支持热加入,总线扩展性强。
- 中断机制
- I2C:从机发无法通知主机中断,除非另外加1根线,称为INT线 。
- I3C :IBI(In‑Band Interrupt),从机直接在 SDA 发中断包。
适用场景
- I2C 适合:低速、简单、低成本场景,如 EEPROM、RTC、温湿度传感器、OLED 等。
- I3C 适合:高密度传感器、手机 / 可穿戴、汽车电子、工业 IoT、机器人、AI 服务器等,需要高速、低功耗、多设备、实时性的场景。
ESP32-P4 带有一个 I3C 主机接口,但是官方目前还没有更新出这个模块的资料。
二、工程编写
创建一个新的组件I2C,在CMakeLists.txt完善依赖
cpp
idf_component_register(SRCS "I2C.c"
INCLUDE_DIRS "include"
REQUIRES esp_driver_gpio
PRIV_REQUIRES esp_driver_i2c)
代码功能:
利用I2C 控制器0创建了一个主机,再利用I2C 控制器1创建了一个从机,然后进行通信来验证功能
2.1 I2C.c
cpp
#include <stdio.h>
#include "I2C.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
static const char *TAG = "I2C";
i2c_master_dev_handle_t i2c_master_device_handle = NULL;
i2c_slave_dev_handle_t i2c_slave_device_handle = NULL;
QueueHandle_t I2C_slaveT_queue;
QueueHandle_t I2C_slaveR_queue;
QueueSetHandle_t I2C_slave_queueSet = NULL;
bool master_read_callback(i2c_slave_dev_handle_t i2c_slave, const i2c_slave_request_event_data_t *evt_data, void *arg);
bool master_write_callback(i2c_slave_dev_handle_t i2c_slave, const i2c_slave_rx_done_event_data_t *evt_data, void *arg);
void I2C_slave_task(void *arg);
/*---------------------------------------------------------------------------------- */
/**
* @brief I2C主机初始化
* @param[in] void
* @param[out] void
* @return void
*/
/*---------------------------------------------------------------------------------- */
void I2C_master_init(void)
{
// I2C主机初始化代码
i2c_master_bus_handle_t i2c_master_handle = NULL;
i2c_master_bus_config_t master_config = {
.i2c_port = I2C_NUM_0,
.sda_io_num = master_sda_io,
.scl_io_num = master_scl_io,
.clk_source = I2C_CLK_SRC_DEFAULT,
.glitch_ignore_cnt = 7,
.intr_priority = 0,
.flags.enable_internal_pullup = true,
};
ESP_ERROR_CHECK(i2c_new_master_bus(&master_config, &i2c_master_handle));
i2c_device_config_t device_config = {
.dev_addr_length = I2C_ADDR_BIT_7,
.device_address = 0x28, // 设备地址,根据实际情况修改
.scl_speed_hz = 300000, // 100kHz
.scl_wait_us = 0,
};
ESP_ERROR_CHECK(i2c_master_bus_add_device(i2c_master_handle, &device_config, &i2c_master_device_handle));
uint8_t transmit_state = 0x20;
uint8_t receive_state = 0;
i2c_master_transmit_receive(i2c_master_device_handle, &transmit_state, 1, &receive_state, 1, 1000);
if (receive_state == 0x55)
{
ESP_LOGI(TAG, "master transmit receive success");
}
else
{
ESP_LOGI(TAG, "master transmit receive failed");
}
}
/*---------------------------------------------------------------------------------- */
/**
* @brief I2C主机读写数据代码
* @param[in] void
* @param[out] void
* @return void
*/
/*---------------------------------------------------------------------------------- */
void I2C_master_T_R(void)
{
// I2C主机读写数据代码
uint8_t transmit_buffer1[2] = {0x11, 0x22};
uint8_t transmit_buffer2[2] = {0xaa, 0xbb};
uint8_t receive_buffer[10] = {0};
i2c_master_transmit(i2c_master_device_handle, transmit_buffer1, sizeof(transmit_buffer1), -1);
i2c_master_transmit_multi_buffer_info_t buf[2] = {
{.write_buffer = transmit_buffer1, .buffer_size = sizeof(transmit_buffer1)},
{.write_buffer = transmit_buffer2, .buffer_size = sizeof(transmit_buffer2)},
};
i2c_master_multi_buffer_transmit(i2c_master_device_handle, buf, 2, -1);
i2c_master_receive(i2c_master_device_handle, receive_buffer, 5, -1);
ESP_LOGI(TAG, "master receive : %x %x %x %x %x", receive_buffer[0], receive_buffer[1], receive_buffer[2], receive_buffer[3], receive_buffer[4]);
}
/*---------------------------------------------------------------------------------- */
/**
* @brief I2C从机初始化代码
* @param[in] void
* @param[out] void
* @return void
*/
/*---------------------------------------------------------------------------------- */
void I2C_slave_init(void)
{
// I2C从机初始化代码
i2c_slave_config_t slave_config = {
.i2c_port = I2C_NUM_1,
.sda_io_num = slave_sda_io,
.scl_io_num = slave_scl_io,
.clk_source = I2C_CLK_SRC_DEFAULT,
.send_buf_depth = 128,
.receive_buf_depth = 128,
.slave_addr = 0x28, // 从机地址,根据实际情况修改
.intr_priority = 0,
};
ESP_ERROR_CHECK(i2c_new_slave_device(&slave_config, &i2c_slave_device_handle));
i2c_slave_event_callbacks_t slave_cbs = {
.on_request = master_read_callback, // 可以根据需要设置回调函数
.on_receive = master_write_callback, // 可以根据需要设置回调函数
};
i2c_slave_register_event_callbacks(i2c_slave_device_handle, &slave_cbs, NULL); // 注册事件回调函数
I2C_slaveT_queue = xQueueCreate(2, sizeof(uint8_t)); // 创建一个队列来处理从机事件
I2C_slaveR_queue = xQueueCreate(10, sizeof(i2c_slave_rx_done_event_data_t)); // 创建一个队列来处理从机事件
I2C_slave_queueSet =xQueueCreateSet(2);
xQueueAddToSet(I2C_slaveT_queue, I2C_slave_queueSet); // 将队列添加到队列集合
xQueueAddToSet(I2C_slaveR_queue, I2C_slave_queueSet); // 将队列添加到队列集合
xTaskCreate(I2C_slave_task, "I2C_slave_task", 4096, NULL, 5, NULL); // 创建从机任务
}
bool master_read_callback(i2c_slave_dev_handle_t i2c_slave, const i2c_slave_request_event_data_t *evt_data, void *arg)
{
uint8_t event = 0x01;
BaseType_t xWoken = pdFALSE;
xQueueSendFromISR(I2C_slaveT_queue, &event, &xWoken); // 将事件发送到队列
return xWoken;
}
bool master_write_callback(i2c_slave_dev_handle_t i2c_slave, const i2c_slave_rx_done_event_data_t *evt_data, void *arg)
{
BaseType_t xWoken = pdFALSE;
xQueueSendFromISR(I2C_slaveR_queue, evt_data, &xWoken); // 将事件发送到队列
return xWoken;
}
void I2C_slave_task(void *arg)
{
uint32_t write_len = 0;
uint8_t flag = 0;
QueueSetMemberHandle_t xQueue;
uint8_t eventR_data;
i2c_slave_rx_done_event_data_t evt_data;
uint8_t data1 = 0x55;
uint8_t data2[5] = {0x01,0x02,0x03,0x04,0x05};
while (1)
{
xQueue = xQueueSelectFromSet(I2C_slave_queueSet, portMAX_DELAY); // 等待事件
if (xQueue == I2C_slaveT_queue){
if (flag == 1)
{
i2c_slave_write(i2c_slave_device_handle, &data1, sizeof(data1), &write_len, 100);
}
else{
xQueueReceive(I2C_slaveT_queue, &eventR_data, portMAX_DELAY);
i2c_slave_write(i2c_slave_device_handle, data2, sizeof(data2), &write_len, 100);
}
}
else if (xQueue == I2C_slaveR_queue)
{
xQueueReceive(I2C_slaveR_queue, &evt_data, portMAX_DELAY);
if (evt_data.buffer[0] == 0x20 && evt_data.length == 1)
{
flag = 1;
}
else{
flag = 0;
ESP_LOGI("I2C", "slave receive : %02x %02x %02x %02x", evt_data.buffer[0], evt_data.buffer[1], evt_data.buffer[2], evt_data.buffer[3]);
}
}
}
}
2.2 I2C.h
cpp
#ifndef I2C_H
#define I2C_H
#include "driver/i2c_master.h"
#include "driver/i2c_slave.h"
#include "driver/i2c_types.h"
#include "driver/gpio.h"
#define master_sda_io GPIO_NUM_7
#define master_scl_io GPIO_NUM_8
#define slave_sda_io GPIO_NUM_22
#define slave_scl_io GPIO_NUM_21
void I2C_master_init(void);
void I2C_master_T_R(void);
void I2C_slave_init(void);
#endif // I2C_H
2.3 代码解释
代码有点长,我们一点点来看。关于函数的应用,可参考官方的编程手册和我整理的《ESP32 实用API指南2》
void I2C_master_init(void)
首先通过函数i2c_new_master_bus,创建一个I2C总线。结构体配置很好理解,注意不要配置trans_queue_depth,异步传输是一个实验性功能,很不稳定。
接着使用函数i2c_master_bus_add_device给这个总线添加一个主机设备,注意一条总线只能有一个主机设备。结构体中的device_address确定的是从机设备,主机会按照这个地址去寻找从机通信,注意不要带读写位。
接着我使用了一个先发后收的函数i2c_master_transmit_receive来判断通信是否成功。
void I2C_master_T_R(void)
这个一个主机测试代码,注意一次性发送多个不连续缓冲函数i2c_master_multi_buffer_transmit的结构体配置形式。
void I2C_slave_init(void)
先使用函数i2c_new_slave_device创建一个从机设备,注意从机地址要和主机地址一致。
必须使用函数i2c_slave_register_event_callbacks给从机设备注册**事件回调函数,**结构体中的on_request是从机硬件收到主机的读请求便会触发的回调函数声明,on_receive是从机硬件收到主机的写数据便会触发的回调函数声明。
接着创建了两个队列、一个队列集合和一个任务。注意将两个队列都要加入到队列集合中
master_read_callback
这是主机需要读数据时触发的回调函数,工作是向队列中写入一个0x01,表明是读请求。
master_write_callback
这是主机向从机写数据时触发的回调函数,写的数据在结构体i2c_slave_rx_done_event_data_t中,包括数据内容和数据长度,将这个结构体直接写入到对了队列中即可。
void I2C_slave_task(void *arg)
这个是任务回调函数,再while循环里通过队列集合xQueueSelectFromSet来判断是哪个队列收到的数据。
如果是写数据队列,那么判断是不是一开始的握手信号,是的话标志为1,不是握手信号直接将数据打印出来即可。
如果是读请求队列,判断标志是不是为1,是的话,发送0x55,代表从机收到握手请求并发送握手信号。如果标志不是1,直接将预定的数据发送给主机。
这里注意一下,函数i2c_slave_write的参数write_len是一个输出参数,用来返回实际成功发送的字节数,虽然有时用不到,但也不能设置为NULL,否则会报错,必须按格式接收才行。
其实从整体来看,主机读数据请求用信号量最为合适,这里是为了体会一下队列集合的用法才使用队列的。
2.4 mian
cpp
#include <stdio.h>
#include "user.h"
#include "I2C.h"
void app_main(void)
{
CONSOLE_REPL_INIT(); // 初始化控制台REPL环境
I2C_slave_init();
I2C_master_init();
while (1)
{
I2C_master_T_R();
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
注意初始化顺序,要先初始从机,再初始主机。因为一旦先初始主机,主机便会发送握手信号,此时从机刚要初始,无法处理握手信号,便会报错。
三、结果展示

可以看出来,从机的接收缓存区是不会自动清空的。如果新的数据覆盖不够,剩下的区域会保留之前的数据。