(1)最近刚做完ESP32的一个模块的驱动移植,使用到了I2C。感觉ESP32的硬件I2C还是挺容易使用的。
(2)本文将只会介绍ESP32的硬件I2C使用,如果想知道软件I2C使用,可看其他的任意一款芯片软件I2C实现流程,都是一样的东西。
(3)注意,本人只会介绍常用的函数接口。其他的可自行阅读乐鑫官方I2C驱动文档。
(1)如果玩过多款MCU会发现,一般来说,一款MCU只有指定的引脚支持硬件I2C的。所以,根据惯性思维,我认为要使用ESP32的硬件I2C,就需要先看看datasheet,看看这块芯片的哪些引脚支持硬件I2C。
注:看datasheet的3.10章节。
(2)后面发现如下图,任意GPIO管脚!!!当时我是懵逼的,啥玩意?!任意管脚是啥东西,根据我接触过的这么多款芯片经验来说,还是头一次遇到这种情况。
(3)后面在交流群里面说了这个发现之后,一位大佬就提出了,ESP32的GPIO 交换矩阵。这个GPIO 交换矩阵能够让所有的引脚实现复用功能,猛的鸭皮。
(4)后面我找到了ESP32S3的技术参考手册,对GPIO交换矩阵感兴趣的可以查看第6章节。
(5)因此,外面可以从手册上得知ESP32S3拥有两个I2C,他们支持任意引脚的复用,并且可以当从机,也可以当主机使用。
(1)初始化I2C配置
/**
* @brief 初始化I2C配置
*
* @param i2c_num 配置的I2C端口
* -i2c_conf 指向I2C配置的结构体指针
*
* @return ESP_OK 配置成功
* -ESP_ERR_INVALID_ARG 传入参数错误
*/
esp_err_t i2c_param_config(i2c_port_t i2c_num, const i2c_config_t *i2c_conf);
(1)注册I2C,因为我们这里的ESP32是用于当主机使用,所以最后三个参数都传入0。第二个参数,
mode
传入I2C_MODE_MASTER
。
/**
* @brief 注册I2C
*
* @param i2c_num 配置的I2C端口
* -mode 设置主机模式还是从机模式
* -slv_rx_buf_len 接收缓冲区大小,单位字节。只有从机模式才会使用该值,主站模式下该值被忽略,一般写0。
* -slv_tx_buf_len 发送缓冲区大小,单位字节。只有从机模式才会使用该值,主站模式下该值被忽略,一般写0。
* -intr_alloc_flags 用于分配中断的标志。如果不使用I2C中断功能,写0
*
* @return ESP_OK 注册成功
* -ESP_ERR_INVALID_ARG 传入参数错误
* -ESP_FAIL 注册失败
*/
esp_err_t i2c_driver_install(i2c_port_t i2c_num, const i2c_config_t *i2c_conf);
(1)使用给定的缓冲区创建并初始化 I2C 命令缓冲区。完成 I2C 事务后,需要调用
i2c_cmd_link_delete()
释放并返回资源。
(2)在ESP32中,如果我们想要使用硬件I2C传输数据,需要先向一个缓冲区写入自己要发送的指令,然后再调用i2c_master_cmd_begin()
将这个缓冲区的数据输出。
(3)需要注意的一点是,调用i2c_master_cmd_begin()
将这个缓冲区的数据输出之后,i2c_cmd_link_create()
创建的命令缓冲区的数据不会被清除,依旧存在,所以需要调用i2c_cmd_link_delete()
函数,将这个命令缓冲区数据手动清空。
/**
* @brief 动态创建一个I2C命令缓冲区
*
* @param 无
*
* @return 如果成功创建I2C命令缓冲区,返回i2c_cmd_handle_t句柄。否则返回NULL
*/
i2c_cmd_handle_t i2c_cmd_link_create(void);
(1)根据I2C的协议,每次通讯前,主机需要发送起始信号。I2C总线上的设备才会被唤醒,开始通讯。因此,每次通讯这个函数是必须放在写/读数据函数前面的。
/**
* @brief I2C协议的起始信号
*
* @param cmd_handle 传入i2c_cmd_handle_t句柄
*
* @return ESP_OK 起始信号成功写入缓冲区
* -ESP_ERR_INVALID_ARG 传入参数错误
* -ESP_ERR_NO_MEM cmd_handle静态缓冲区太小。如果是调用的i2c_cmd_link_create()动态创建的I2C命令缓冲区,这个不用管。
* -ESP_FAIL 堆上不再有内存
*/
esp_err_t i2c_master_start(i2c_cmd_handle_t cmd_handle);
(1)这个函数必须是在
i2c_master_start()
之后调用,他就是向I2C命令缓冲区写入一个uint8_t 的数据。
(2)因为I2C的协议要求,每次从机设备接受到主机信号都会发送一个ACK回应。所以主机可以根据这个ACK回应来判断从机是否收到了信号,但是主机可以不判断ACK回应。
<1>因此,第三个参数ack_en
传入true
表示启用ACK回应,只有主机收到ACK回应才会继续发送下一个数据。个人建议还是启用ACK回应。
<2>但是如果第三个参数ack_en
传入false
不启用ACK回应。那么主机就会一股脑的传输数据,不管你从机是否真正收到了信号。
/**
* @brief I2C协议的起始信号
*
* @param cmd_handle 传入i2c_cmd_handle_t句柄
* -data 端口上要发送的字节
* -ack_en 启用 ACK 信号
*
* @return ESP_OK 成功写入缓冲区
* -ESP_ERR_INVALID_ARG 传入参数错误
* -ESP_ERR_NO_MEM 创建的I2C命令缓冲区静态缓冲区太小。如果是调用的i2c_cmd_link_create()动态创建的I2C命令缓冲区,这个不用管。
* -ESP_FAIL 堆上不再有内存
*/
esp_err_t i2c_master_write_byte(i2c_cmd_handle_t cmd_handle, uint8_t data, bool ack_en);
(1)这个函数就是对
i2c_master_write_byte()
进行了再一次的封装,唯一需要注意的是,多了一个传入参数data_len
。他需要知道要写入的数据有多少个。
/**
* @brief I2C协议的起始信号
*
* @param cmd_handle 传入i2c_cmd_handle_t句柄
* -data 端口上要发送的字节
* -data_len 发送的数据长度
* -ack_en 启用 ACK 信号
*
* @return ESP_OK 成功写入缓冲区
* -ESP_ERR_INVALID_ARG 传入参数错误
* -ESP_ERR_NO_MEM 创建的I2C命令缓冲区静态缓冲区太小。如果是调用的i2c_cmd_link_create()动态创建的I2C命令缓冲区,这个不用管。
* -ESP_FAIL 堆上不再有内存
*/
esp_err_t i2c_master_write(i2c_cmd_handle_t cmd_handle, const uint8_t *data, size_t data_len, bool ack_en);
(1)这里就是传入一个1字节大小的内存
data
,最后读取到的数据会自动存入data
中。
注意:data
必须是uint8_t *
类型指针!
(2)关于ACK 信号如何配置。
<1>因为I2C协议规定了,如果主机想读取从机信号,每次读取到了数据都需要返回一个ACK回应,告诉从机我收到信号了,你继续发。
<2>但是,主机有时候只想读取3个字节数据,有时候又只想读取2个字节数据。从机是无法知道主机要读取多少个数据的,因此,当主机不想读取数据的时候,主机需要发送一个NACK回应给从机,我读取到完数据了,你可以休息了。
<3>所以从上述分析即可知道,如果是主机读取数据,第三个参数ack
应该传入的是I2C_MASTER_LAST_NACK
。
/**
* @brief I2C协议的起始信号
*
* @param cmd_handle 传入i2c_cmd_handle_t句柄
* -data 端口上要读取的字节
* -ack ACK 信号
*
* @return ESP_OK 成功写入缓冲区
* -ESP_ERR_INVALID_ARG 传入参数错误
* -ESP_ERR_NO_MEM 创建的I2C命令缓冲区静态缓冲区太小。如果是调用的i2c_cmd_link_create()动态创建的I2C命令缓冲区,这个不用管。
* -ESP_FAIL 堆上不再有内存
*/
esp_err_t i2c_master_read_byte(i2c_cmd_handle_t cmd_handle, uint8_t *data, i2c_ack_type_t ack);
(1)这里就是传入一个data_len字节大小的内存
data
,最后读取到的数据会自动存入data
中。
注意:data
必须是uint8_t *
类型指针!
/**
* @brief I2C协议的起始信号
*
* @param cmd_handle 传入i2c_cmd_handle_t句柄
* -data 端口上要读取的字节
* -data_len 要读取的数据长度,单位是字节
* -ack ACK 信号
*
* @return ESP_OK 成功写入缓冲区
* -ESP_ERR_INVALID_ARG 传入参数错误
* -ESP_ERR_NO_MEM 创建的I2C命令缓冲区静态缓冲区太小。如果是调用的i2c_cmd_link_create()动态创建的I2C命令缓冲区,这个不用管。
* -ESP_FAIL 堆上不再有内存
*/
esp_err_t i2c_master_read(i2c_cmd_handle_t cmd_handle, uint8_t *data, size_t data_len, i2c_ack_type_t ack);
(1)根据I2C的协议,每次通讯结束,主机需要发送停止信号。I2C总线上的设备就会从工作状态进入休眠,停止通讯。因此,每次这个函数是必须放在写/读数据函数后面的。
/**
* @brief I2C协议的停止信号
*
* @param cmd_handle 传入i2c_cmd_handle_t句柄
*
* @return ESP_OK 起始信号成功写入缓冲区
* -ESP_ERR_INVALID_ARG 传入参数错误
* -ESP_ERR_NO_MEM cmd_handle静态缓冲区太小。如果是调用的i2c_cmd_link_create()动态创建的I2C命令缓冲区,这个不用管。
* -ESP_FAIL 堆上不再有内存
*/
esp_err_t i2c_master_start(i2c_cmd_handle_t cmd_handle);
(1)我们调用了上面这么多函数之后,真正的硬件I2C并没有开始做任何工作。只有使用
i2c_master_cmd_begin()
这个函数之后,硬件I2C才会开始工作,并且将缓冲区的数据输出到总线上。
(2)需要注意的是,ticks_to_wait
作为超时等待时间是什么意思,这个可能会有同学不明白,我简单提一下。
<1>我们上面说了,无论是主机或者从机,收到信号之后都会发送一个ACK或者NACK的回应信号。如果设备没有收到回应信号,那么就会进入等待状态。
<2>那么假设因为什么原因,从机一直不发送回应信号,难道我主机就一直死等着吗?显然是不合理的,所以第三个参数需要设置一个等待的最长时间。
<3>因为ESP-IDF 默认的 FreeRTOS 实现,所以第三个参数需要使用到pdMS_TO_TICKS()
这个宏,将毫秒为单位表示的时间转换为FreeRTOS中的时钟滴答数(ticks)。例如第三个参数传入pdMS_TO_TICKS(10)
表示超时等待时间为10ms。
(3)再次强调i2c_master_cmd_begin()
这个函数并不会清空命令缓冲区的数据。
/**
* @brief 触发 I2C 控制器发送命令缓冲区
*
* @param i2c_num 进行数据传输的I2C端口
* -cmd_handle 传入i2c_cmd_handle_t句柄
* -ticks_to_wait 超时前的最大等待时间
*
* @return ESP_OK 成功写入缓冲区
* -ESP_ERR_INVALID_ARG 传入参数错误
* -ESP_FAIL 发送命令错误,从属设备未返回ACK信号
* -ESP_ERR_INVALID_STATE 未安装 I2C 驱动程序或未处于主模式
* -ESP_ERR_TIMEOUT 由于总线繁忙,操作超时
*/
esp_err_t i2c_master_cmd_begin(i2c_port_t i2c_num, i2c_cmd_handle_t cmd_handle, TickType_t ticks_to_wait);
(1)这个函数有两个调用的可能:
<1>这次I2C通讯结束
<2>利用I2C和某些模块通讯,他们会返回一个status
,我们需要根据status
进行后续的操作。因为我上述说了i2c_master_cmd_begin()
这个函数并不会清空命令缓冲区的数据,所以需要调用i2c_cmd_link_delete()
清除缓冲区,再调用i2c_cmd_link_create()
重新创建一个缓冲区。
/**
* @brief 释放I2C命令缓冲区使用的资源
*
* @param cmd_handle 传入i2c_cmd_handle_t句柄
*
* @return 无
*/
void i2c_cmd_link_delete(i2c_cmd_handle_t cmd_handle);
(1)如下为一个简单的I2C主机测试流程,因为代码太长了,所以只截取了部分。想知道更多的使用,可查看ESP-IDF的I2C部分例程。
/**
* @brief I2C0的主机初始化程序
*
* @param i2c_sda1 I2C0的SDA引脚
* -i2c_scl1 I2C0的SCL引脚
*
* @return 无
*/
void i2c_bus_0_master_init(int i2c_sda1, int i2c_scl1)
{
i2c_config_t conf;
conf.mode = I2C_MODE_MASTER;
conf.sda_io_num = (gpio_num_t)i2c_sda1;
conf.sda_pullup_en = GPIO_PULLUP_ENABLE;
conf.scl_io_num = (gpio_num_t)i2c_scl1;
conf.scl_pullup_en = GPIO_PULLUP_ENABLE;
conf.master.clk_speed = 100000;
conf.clk_flags = I2C_SCLK_SRC_FLAG_FOR_NOMAL;
esp_err_t ret = i2c_param_config(I2C_NUM_0, &conf);
TEST_ASSERT_EQUAL_MESSAGE(ESP_OK, ret, "I2C0 config returned error");
ret = i2c_driver_install(I2C_NUM_0, conf.mode, 0, 0, 0);
TEST_ASSERT_EQUAL_MESSAGE(ESP_OK, ret, "I2C0 install returned error");
ESP_LOGI(TAG, "i2c_0_master_init OK");
}
/*=================== 一次I2C通讯流程 ==========================*/
typedef struct
{
i2c_port_t bus;
uint8_t dev_addr;
uint8_t cmd_init[3];
uint8_t trigger_buff[3];
uint8_t status;
uint8_t readbuff[6];
}aht20_dev_t;
i2c_cmd_handle_t cmd;
cmd = i2c_cmd_link_create();
vTaskDelay(pdMS_TO_TICKS(40));
ret = i2c_master_start(cmd);
assert(ESP_OK == ret);
ret = i2c_master_write_byte(cmd, sens->dev_addr | I2C_MASTER_READ, true);
assert(ESP_OK == ret);
ret = i2c_master_read(cmd, &sens->status, 1, I2C_MASTER_LAST_NACK);
assert(ESP_OK == ret);
ret = i2c_master_stop(cmd);
assert(ESP_OK == ret);
ret = i2c_master_cmd_begin(sens->bus, cmd, pdMS_TO_TICKS(10));
i2c_cmd_link_delete(cmd);
(1)初始化I2C配置。
<1>和ESP32当主机是使用的相同的API函数,不过区别在于初始化i2c_config_t
结构体时候,mode
参数传入的是I2C_MODE_SLAVE
。
<2>同时,这里需要设置ESP32当从机时候的地址信息。从机有两种地址信息,一种是7bit,一种是10bit。slave_addr配置7bit的地址,addr_10bit_en配置10bit的地址。一般都是使用的7bit的地址,所以addr_10bit_en=0即可。ESP32的从机7bit地址可以为任意值。
/**
* @brief 初始化I2C配置
*
* @param i2c_num 配置的I2C端口
* -i2c_conf 指向I2C配置的结构体指针
*
* @return ESP_OK 配置成功
* -ESP_ERR_INVALID_ARG 传入参数错误
*/
esp_err_t i2c_param_config(i2c_port_t i2c_num, const i2c_config_t *i2c_conf);
(1)因为是从机,所以
mode
需要配置为I2C_MODE_SLAVE
。
(2)主机的slv_rx_buf_len
和slv_tx_buf_len
是不需要配置的,但是如果是从机,你就需要根据需求配置缓冲区大小。如果不知道为多少,一般128即可。
/**
* @brief 注册I2C
*
* @param i2c_num 配置的I2C端口
* -mode 设置主机模式还是从机模式
* -slv_rx_buf_len 接收缓冲区大小,单位字节。只有从机模式才会使用该值,主站模式下该值被忽略,一般写0
* -slv_tx_buf_len 发送缓冲区大小,单位字节。只有从机模式才会使用该值,主站模式下该值被忽略,一般写0
* -intr_alloc_flags 用于分配中断的标志。如果不使用I2C中断功能,写0
*
* @return ESP_OK 注册成功
* -ESP_ERR_INVALID_ARG 传入参数错误
* -ESP_FAIL 注册失败
*/
esp_err_t i2c_driver_install(i2c_port_t i2c_num, const i2c_config_t *i2c_conf);
(1)读取主机数据
/**
* @brief 读取主机数据
*
* @param i2c_num I2C端口
* -data 存放读取到主机信号的缓冲区
* -max_size 读取的最大字节数
* -ticks_to_wait 最大等待时间
*
* @return ESP_FAIL(-1) 传入参数错误
* -Others(>=0) 从 I2C 从站缓冲区读取的数据字节数
*/
int i2c_slave_read_buffer(i2c_port_t i2c_num, uint8_t *data, size_t max_size, TickType_t ticks_to_wait);
(1)向主机写入数据
/**
* @brief 向主机写入数据
*
* @param i2c_num I2C端口
* -data 向主机写入数据的缓冲区
* -max_size 写入数据的字节数
* -ticks_to_wait 最大等待时间
*
* @return ESP_FAIL(-1) 传入参数错误
* -Others(>=0) 推送到 I2C 从缓冲区的数据字节数
*/
int i2c_slave_write_buffer(i2c_port_t i2c_num, const uint8_t *data, int size, TickType_t ticks_to_wait);
(1)注意:ESP32C2没有从机功能。
(2)从机初始化时候,接受和发送缓冲区是实际数据长度的2倍,这个设置是为了在I2C从机模式下提供足够的缓冲区空间,以存储来自主机的数据。
/*=================== ESP32从机初始化 ==========================*/
static esp_err_t i2c_slave_init(void)
{
int i2c_slave_port = I2C_NUM_0;
i2c_config_t conf_slave = {
.sda_io_num = GPIO_NUM_5,
.sda_pullup_en = GPIO_PULLUP_ENABLE,
.scl_io_num = GPIO_NUM_4,
.scl_pullup_en = GPIO_PULLUP_ENABLE,
.mode = I2C_MODE_SLAVE,
.slave.addr_10bit_en = 0,
.slave.slave_addr = 0x28,
};
esp_err_t err = i2c_param_config(i2c_slave_port, &conf_slave);
if (err != ESP_OK) {
return err;
}
return i2c_driver_install(i2c_slave_port, conf_slave.mode, 2*128, 2*128, 0);
}
/*=================== ESP32向主机写入数据 ==========================*/
uint8_t *data = (uint8_t *)malloc(128);
size_t d_size = i2c_slave_write_buffer(I2C_NUM_0, data, 128, pdMS_TO_TICKS(10));
/*=================== ESP32向主机读取数据 ==========================*/
size = i2c_slave_read_buffer(I2C_NUM_0, data, 128, pdMS_TO_TICKS(10));