1. 简介
I2C(Inter-Integrated Circuit),是由Philips公司在1980年代初开发的一种半双工的同步串行总线,它利用一根时钟线和一根数据线在连接总线的两个器件之间进行信息的传递,为设备之间数据交换提供了一种简单高效的方法。每个连接到总线上的器件都有唯一的地址,任何器件既可以作为主机也可以作为从机,但同一时刻只允许有一个主机。
I2C在硬件上是采用了开漏输出,在这种模式下GPIO没有输出负载的能力,因此总线上的每根管脚都需要接上拉电阻才能正常通信。
I2C每次通信都由主机发起,主机发出起始信号(在 SCL 为高电平时拉低 SDA 线),则通讯开始;接下来主机会发送从机地址(7bit或10bit)和读写位(1bit),如果从机地址匹配,从机会发生应答信号(拉低 SDA 线);接下来,根据读写位,主机和从机可以发送/接收更多的数据;最后主机发出停止信号(在 SCL 为高电平时,拉高 SDA 线),终止通信。
速度方面,ESP32支持100kbps(标准模式)和400kbps(快速模式)两种速度。
2. 控制器
每个I2C控制器都有一块32字节的RAM用于存放数据;SCL_FSM和SDA_FSM用于控制SCL和SDA信号线;DATA_Shifter用于将字节一位一位的发送出去。
2.1 主机控制器
主机控制器会有16个命令寄存器和一个命令控制器,命令寄存器的内容决定了数据如何传输。
- CMD_DONE:判断一条命令是否执行完毕;
- op_code:命令编码;
- RSTART:op_code 等于 0 时,用于控制 I2C 协议中 START 位以及 RESTART 位的
- 发送;
- WRITE:op_code等于1时,该命令表示当前主机将发送数据;
- READ:op_code等于2时,该命令表示当前主机将要接收数据;
- STOP:op_code等于3时,该命令用于控制协议中停止位的发送;
- END:op_code等于4时,该命令用于主机模式下连续发送数据,主要实现方式为关闭SCL 时钟,当数据准备完毕,继续上次传输。
- ack_value:当接收数据时,在字节被接收后,该位用于表示接收方将发送一个ACK位;
- ack_exp:该位用于设置发送方期望的ACK值;
- ack_check_en:该位用于控制发送方是否对ACK位进行检测;1:检测ACK值,0:不检测ACK值;
- byte_num:该寄存器用于说明读写数据的数据长度(单位字节),最大为255,最小为1。
2.2 从机控制器
从机控制器的SCL和SDA管脚会各自有一个滤波器,用于过滤通信中的噪声。
3. 例程
例程使用两个I2C外设互连,并相互收发数据。
#include "driver/i2c.h"
#include "driver/i2c_master.h"
#include "driver/i2c_slave.h"
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "esp_log.h"#include <string.h>#define TAG "app"#define WRITE_CMD 0x12
#define READ_CMD 0x13static i2c_master_bus_handle_t bus_handle;
static i2c_master_dev_handle_t dev_handle;
static i2c_slave_dev_handle_t slave_handle;static QueueHandle_t slave_recv_queue;
static QueueHandle_t slave_recv_notify;static const char *str = "Hello, World!";static IRAM_ATTR bool i2c_slave_rx_done_callback(i2c_slave_dev_handle_t slave, const i2c_slave_rx_done_event_data_t *edata, void *user_data)
{BaseType_t high_task_wakeup = pdFALSE;xQueueSendFromISR(slave_recv_queue, edata, &high_task_wakeup);return high_task_wakeup == pdTRUE;
}static void master_task(void *args)
{uint8_t *data_rd = (uint8_t *) malloc(128);while (1) {/* 主机写从机 */uint8_t data_sd[128] = {0};data_sd[0] = WRITE_CMD;strcpy((char *) &data_sd[1], str);ESP_ERROR_CHECK(i2c_master_transmit(dev_handle, data_sd, strlen(str) + 1, -1));ESP_LOGI(TAG, "[Master] Send %d bytes of data", strlen(str));xSemaphoreTake(slave_recv_notify, portMAX_DELAY); // 等待从机处理完数据接收vTaskDelay(1000 / portTICK_PERIOD_MS);/* 主机读从机 */uint8_t read_reg = READ_CMD;memset(data_rd, 0, 128);ESP_ERROR_CHECK(i2c_master_transmit_receive(dev_handle, &read_reg, 1, data_rd, strlen(str), -1));ESP_LOGI(TAG, "[Master] Receive %d bytes of data: %s", strlen((char *) data_rd), data_rd);xSemaphoreTake(slave_recv_notify, portMAX_DELAY); // 等待从机处理完数据接收vTaskDelay(1000 / portTICK_PERIOD_MS);}
}static void slave_task(void *args)
{uint8_t *data_rd = (uint8_t *) malloc(128);while (1) {/* 从机接收数据 */i2c_slave_rx_done_event_data_t rx_data;memset(data_rd, 0, 128);ESP_ERROR_CHECK(i2c_slave_receive(slave_handle, data_rd, 128));if (pdTRUE == xQueueReceive(slave_recv_queue, &rx_data, portMAX_DELAY)) {uint8_t *data = rx_data.buffer;if (data[0] == WRITE_CMD) {/* 主机写命令 */ESP_LOGI(TAG, "[Slave] Receive %d bytes of data: %s", strlen((char *) &data[1]), &data[1]);} else if (data[0] == READ_CMD) {/* 主机读命令 */ESP_ERROR_CHECK(i2c_slave_transmit(slave_handle, (const uint8_t *) str, strlen(str), -1));ESP_LOGI(TAG, "[Slave] Send %d bytes of data", strlen(str));}xSemaphoreGive(slave_recv_notify);}}
}void app_main()
{/* 初始化I2C主机 */i2c_master_bus_config_t i2c_bus_config = {0};i2c_bus_config.clk_source = I2C_CLK_SRC_DEFAULT; // 默认时钟,APB时钟i2c_bus_config.i2c_port = I2C_NUM_0;i2c_bus_config.scl_io_num = 17;i2c_bus_config.sda_io_num = 18;i2c_bus_config.glitch_ignore_cnt = 7;i2c_bus_config.flags.enable_internal_pullup = 1; // 使用内部上拉电阻ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_config, &bus_handle));i2c_device_config_t dev_cfg = {0};dev_cfg.dev_addr_length = I2C_ADDR_BIT_LEN_7; // 7位地址dev_cfg.device_address = 0x58;dev_cfg.scl_speed_hz = 400000; // 400kHzESP_ERROR_CHECK(i2c_master_bus_add_device(bus_handle, &dev_cfg, &dev_handle));slave_recv_notify = xSemaphoreCreateBinary();xSemaphoreTake(slave_recv_notify, 0);/* 初始化I2C从机 */i2c_slave_config_t i2c_slv_config = {0};i2c_slv_config.addr_bit_len = I2C_ADDR_BIT_LEN_7;i2c_slv_config.clk_source = I2C_CLK_SRC_DEFAULT; // 默认时钟,APB时钟i2c_slv_config.i2c_port = I2C_NUM_1;i2c_slv_config.send_buf_depth = 256; // 发送队列深度i2c_slv_config.scl_io_num = 21;i2c_slv_config.sda_io_num = 22;i2c_slv_config.slave_addr = 0x58;/* 注册从机接收回调 */ESP_ERROR_CHECK(i2c_new_slave_device(&i2c_slv_config, &slave_handle));slave_recv_queue = xQueueCreate(1, sizeof(i2c_slave_rx_done_event_data_t));i2c_slave_event_callbacks_t cbs = {.on_recv_done = i2c_slave_rx_done_callback,};ESP_ERROR_CHECK(i2c_slave_register_event_callbacks(slave_handle, &cbs, NULL));/* 创建任务 */xTaskCreate(slave_task, "slave_task", 2048, NULL, 5, NULL);xTaskCreate(master_task, "master_task", 2048, NULL, 5, NULL);
}
1. I2C主机初始化
第一步,初始化I2C总线,初始化结构体如下。
typedef struct {i2c_port_num_t i2c_port;gpio_num_t sda_io_num;gpio_num_t scl_io_num;union {i2c_clock_source_t clk_source;
#if SOC_LP_I2C_SUPPORTEDlp_i2c_clock_source_t lp_source_clk;
#endif};uint8_t glitch_ignore_cnt;int intr_priority;size_t trans_queue_depth;struct {uint32_t enable_internal_pullup: 1;} flags;
} i2c_master_bus_config_t;
- i2c_port:I2C外设(0-1);
- sda_io_num:SDA管脚号;
- scl_io_num:SCL管脚号;
- clk_source:时钟源;
- lp_clk_source:低功耗时钟源;
- glitch_ignore_cnt:毛刺过滤周期,一般默认为7就好;
- intr_priority:中断优先级;
- trans_queue_depth:发送队列深度;
- enable_internal_pullup:内部上拉。
第二步,添加总线设备,初始化结构体如下。
typedef struct {i2c_addr_bit_len_t dev_addr_length;uint16_t device_address;uint32_t scl_speed_hz;uint32_t scl_wait_us;struct {uint32_t disable_ack_check:1;} flags;
} i2c_device_config_t;
- dev_addr_length:从机地址长度;
- device_address:从机地址;
- scl_speed_hz:通信速率;
- scl_wait_us:时钟线超时时间;
- disable_ack_check:禁用ACK检查。
2. I2C从机初始化
第一步,初始化从机设备,初始化结构体如下。
typedef struct {i2c_port_num_t i2c_port;gpio_num_t sda_io_num;gpio_num_t scl_io_num;i2c_clock_source_t clk_source;uint32_t send_buf_depth;uint16_t slave_addr;i2c_addr_bit_len_t addr_bit_len;int intr_priority;struct {
#if SOC_I2C_SLAVE_CAN_GET_STRETCH_CAUSEuint32_t stretch_en: 1;
#endif
#if SOC_I2C_SLAVE_SUPPORT_BROADCASTuint32_t broadcast_en: 1;
#endif
#if SOC_I2C_SLAVE_SUPPORT_I2CRAM_ACCESSuint32_t access_ram_en: 1;
#endif
#if SOC_I2C_SLAVE_SUPPORT_SLAVE_UNMATCHuint32_t slave_unmatch_en: 1;
#endif} flags;
} i2c_slave_config_t;
- i2c_port:I2C外设(0-1);
- sda_io_num:SDA管脚号;
- scl_io_num:SCL管脚号;
- clk_source:时钟源;
- send_buf_depth:发送队列深度;
- slave_addr:从机地址;
- addr_bit_len:从机地址长度;
- intr_priority:中断优先级;
- stretch_en:使能stretch功能;
- broadcast_en:使能从机广播;
- access_ram_en:使能I2C RAM直接访问;
- slave_unmatch_en:使能地址匹配错误中断。
第二步,注册从机接收回调。回调函数的内容比较简单,就是把接收到的内容通过消息队列发送给从机任务。
这个例程我创建了两个任务线程,一个主机任务,一个从机任务。
主机任务先向从机发送数据,再从从机中读数据,每次操作都通过二值信号量等待从机完成数据处理。
从机任务负责接收主机发来的数据,根据第一个字节(命令字节)的内容(读或写)执行相应的操作。
这里要注意的是,目前IDF的最新版本(v5.3.1)存在一个未修复的BUG,当我们调用i2c_slave_receive函数接收主机数据时会导致系统崩溃,我在另一篇置顶文章中提供了解决思路。
编译烧录程序,就能看到以下log。