ESP32IDF — 硬件I2C使用教程
  rWLfpslpeeJP 2023年11月02日 90 0


前言

(1)最近刚做完ESP32的一个模块的驱动移植,使用到了I2C。感觉ESP32的硬件I2C还是挺容易使用的。
(2)本文将只会介绍ESP32的硬件I2C使用,如果想知道软件I2C使用,可看其他的任意一款芯片软件I2C实现流程,都是一样的东西。
(3)注意,本人只会介绍常用的函数接口。其他的可自行阅读乐鑫官方I2C驱动文档

ESP32S3的I2C引脚简单介绍

(1)如果玩过多款MCU会发现,一般来说,一款MCU只有指定的引脚支持硬件I2C的。所以,根据惯性思维,我认为要使用ESP32的硬件I2C,就需要先看看datasheet,看看这块芯片的哪些引脚支持硬件I2C。
注:看datasheet的3.10章节。
(2)后面发现如下图,任意GPIO管脚!!!当时我是懵逼的,啥玩意?!任意管脚是啥东西,根据我接触过的这么多款芯片经验来说,还是头一次遇到这种情况。

ESP32IDF — 硬件I2C使用教程_单片机

(3)后面在交流群里面说了这个发现之后,一位大佬就提出了,ESP32的GPIO 交换矩阵。这个GPIO 交换矩阵能够让所有的引脚实现复用功能,猛的鸭皮。
(4)后面我找到了ESP32S3的技术参考手册,对GPIO交换矩阵感兴趣的可以查看第6章节
(5)因此,外面可以从手册上得知ESP32S3拥有两个I2C,他们支持任意引脚的复用,并且可以当从机,也可以当主机使用。

ESP32当主机的API函数介绍

i2c_param_config()初始化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);

i2c_driver_install()注册I2C

(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);

i2c_cmd_link_create()动态创建一个I2C命令缓冲区

(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);

i2c_master_start()协议起始信号

(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);

i2c_master_write_byte()写一个字节数据

(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);

i2c_master_write()写任意字节数据

(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);

i2c_master_read_byte()读一个字节数据

(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);

i2c_master_read()读任意字节数据

(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);

i2c_master_stop()协议停止信号

(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);

i2c_master_cmd_begin()触发 I2C 控制器发送命令缓冲区

(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);

i2c_cmd_link_delete()释放I2C命令缓冲区使用的资源

(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);

ESP32主机示例

(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);

ESP32当从机的API函数介绍

i2c_param_config()初始化I2C配置

(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);

i2c_driver_install()注册I2C

(1)因为是从机,所以mode需要配置为I2C_MODE_SLAVE
(2)主机的slv_rx_buf_lenslv_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);

i2c_slave_read_buffer()读取主机数据

(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);

i2c_slave_write_buffer()向主机写入数据

(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);

ESP32从机示例

(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));


【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月08日 0

暂无评论

推荐阅读
rWLfpslpeeJP