1. 头文件包含

#include "driver/i2c.h"
#include "driver/gpio.h"

2. I2C的配置

使用如下函数配置I2C外设:

esp_err_t i2c_param_config(i2c_port_t i2c_num, const i2c_config_t *i2c_conf);

各个参数的含义:

2.1 i2c_num 的选择

参数 i2c_num 用于指定要配置的I2C外设。ESP32-S3有2个I2C外设。选择 I2C_NUM_0I2C_NUM_1

typedef enum {
    I2C_NUM_0 = 0,              /*!< I2C port 0 */
#if SOC_HP_I2C_NUM >= 2
    I2C_NUM_1,                  /*!< I2C port 1 */
#endif /* SOC_HP_I2C_NUM >= 2 */
#if SOC_LP_I2C_NUM >= 1
    LP_I2C_NUM_0,               /*< LP_I2C port 0 */
#endif /* SOC_LP_I2C_NUM >= 1 */
    I2C_NUM_MAX,                /*!< I2C port max */
} i2c_port_t;

2.2 i2c_conf 的设定

第二个参数 i2c_conf 用于配置I2C的工作参数。结构体 i2c_config_t 的定义为:

typedef struct {
    i2c_mode_t mode;     /*!< I2C mode */
    int sda_io_num;      /*!< GPIO number for I2C sda signal */
    int scl_io_num;      /*!< GPIO number for I2C scl signal */
    bool sda_pullup_en;  /*!< Internal GPIO pull mode for I2C sda signal*/
    bool scl_pullup_en;  /*!< Internal GPIO pull mode for I2C scl signal*/

    union {
        struct {
            uint32_t clk_speed;      /*!< I2C clock frequency for master mode, (no higher than 1MHz for now) */
        } master;                    /*!< I2C master config */
#if SOC_I2C_SUPPORT_SLAVE
        struct {
            uint8_t addr_10bit_en;   /*!< I2C 10bit address mode enable for slave mode */
            uint16_t slave_addr;     /*!< I2C address for slave mode */
            uint32_t maximum_speed;  /*!< I2C expected clock speed from SCL. */
        } slave;                     /*!< I2C slave config */
#endif // SOC_I2C_SUPPORT_SLAVE
    };
    uint32_t clk_flags;              /*!< Bitwise of ``I2C_SCLK_SRC_FLAG_**FOR_DFS**`` for clk source choice*/
} i2c_config_t;

typedef void *i2c_cmd_handle_t;    /*!< I2C command handle  */

2.2.1 mode/设置模式

参数 mode 用于设置I2C的模式。一般是使用 I2C_MODE_MASTER

typedef enum{
#if SOC_I2C_SUPPORT_SLAVE
    I2C_MODE_SLAVE = 0,   /*!< I2C slave mode */
#endif
    I2C_MODE_MASTER,      /*!< I2C master mode */
    I2C_MODE_MAX,
} i2c_mode_t;

2.2.2 sda_io_num、scl_io_num/设置SDA和SCL的GPIO

2.2.3 sda_pullup_en、scl_pullup_en/设置SDA和SCL的上拉使能

硬件设计上,I2C的两个信号线一般在远端使用电阻上拉。这里就不需要使能了。如果外面没有上拉电阻,这里也可以使能内部上拉以补救。

2.2.4 clk_speed/设置I2C速率

如设置为100000,则表示100Kbps的I2C速率。

2.2.5 clk_flags/标记位

设置为 I2C_SCLK_SRC_FLAG_FOR_NOMAL 即可。

3. 驱动的安装

使用如下函数安装I2C驱动:

esp_err_t i2c_driver_install(i2c_port_t i2c_num, i2c_mode_t mode, size_t slv_rx_buf_len, size_t slv_tx_buf_len, int intr_alloc_flags);
  1. i2c_num:指定I2C外设,I2C_NUM_0 或者 I2C_NUM_1
  2. mode:指定I2C模式,I2C_MODE_MASTER
  3. slv_rx_buf_lenslv_tx_buf_len:用于指定slave模式时的接收和发送缓存大小。对于master模式,设置为0即可。
  4. intr_alloc_flags:设定中断优先级之类的标记。定义在 esp_intr_alloc.h 文件中。

这一步完成之后,I2C就可以准备读写其他设备了。

4. 读或写

ESP32-S3的I2C驱动,使用一种链接的方式来实现读写。程序首先要创建一个链接,然后将I2C访问过程中的每一个步骤,按类型不同,使用不同的函数压入到链中,最后在命令驱动按照链去执行每一个步骤。使用起来还是很方便的。

4.1 创建命令序列/链

使用如下命令创建一个命令序列。返回值要保留,后面压入命令和执行命令都要用到。

i2c_cmd_handle_t i2c_cmd_link_create(void);

4.2 添加start/start-repeated命令

I2C访问的开始,不论是读还是写,都是以一个start条件开始的。因此读写序列的第一个命令,一定是添加一个start命令。
另外I2C读取数据的时候,在总线地址和目标地址发送完成后,需要重新发送一个start(start-repeated)命令。
这两种情况都可以通过如下函数添加。

esp_err_t i2c_master_start(i2c_cmd_handle_t cmd_handle);

其中,第一个参数 cmd_handle,即是由函数 i2c_cmd_link_create 创建返回的句柄。

4.3 添加单个字节写入

I2C主设备,需要向从设备写入总线地址、寄存器地址、或者单个字节的数据时,都可以添加单个字节写入命令。通过如下函数实现该功能:

esp_err_t i2c_master_write_byte(i2c_cmd_handle_t cmd_handle, uint8_t data, bool ack_en);

其中,第二个参数 data 是要写入的数据。第三个参数 ack_en ,用于设定是否需要检查从机的ACK。正常应该是需要的。

4.4 添加多个字节写入

也可以一次添加多个数据写入,例如写入EEPROM数据等。通过如下函数来实现该功能:

esp_err_t i2c_master_write(i2c_cmd_handle_t cmd_handle, const uint8_t *data, size_t data_len, bool ack_en);

第二个参数 data ,指向一个保存多个数据的缓存。data_len 用于指定写入数据量。

4.5 读取单个字节数据

使用如下函数向命令序列中添加单个字节读取命令。

esp_err_t i2c_master_read_byte(i2c_cmd_handle_t cmd_handle, uint8_t *data, i2c_ack_type_t ack);

这个命令和单个字节写入有点类似,但是第三个参数,即是否需要ACK不同。其定义为:

typedef enum {
    I2C_MASTER_ACK = 0x0,        /*!< I2C ack for each byte read */
    I2C_MASTER_NACK = 0x1,       /*!< I2C nack for each byte read */
    I2C_MASTER_LAST_NACK = 0x2,   /*!< I2C nack for the last byte*/
    I2C_MASTER_ACK_MAX,
} i2c_ack_type_t;

如何设置,需要根据应用而定。它控制读取数据时,每个字节接收完成后,是否需要向从机发送ACK响应。一般来说,连续读取一串数据,都需要发送ACK,但是最后一个数据读完后应该发送NACK。

4.6 读取多个字节数据

使用如下函数向命令序列中添加多个字节的读取命令。

esp_err_t i2c_master_read(i2c_cmd_handle_t cmd_handle, uint8_t *data, size_t data_len, i2c_ack_type_t ack);

这种情况下,最后一个参数ack,一般应设置为 I2C_MASTER_LAST_NACK ,以指示最后一个字节接收完成后,不发送ACK。除非这条读命令添加后,还需要再添加读取命令。

4.7 添加stop命令

使用如下函数添加stop命令,以结束I2C传输:

esp_err_t i2c_master_stop(i2c_cmd_handle_t cmd_handle);

4.8 执行命令序列

使用如下命令执行已经构建好的命令序列:

esp_err_t i2c_master_cmd_begin(i2c_port_t i2c_num, i2c_cmd_handle_t cmd_handle, TickType_t ticks_to_wait);

ticks_to_wait 用于设置超时时间。以FreeRTOS的systick跳动为单位。

4.9 删除命令序列

序列执行完成后,需要删除命令序列。因为命令序列是动态分配的。

void i2c_cmd_link_delete(i2c_cmd_handle_t cmd_handle);

5. 示例

以下示例,先向24C02 EEPROM写入一些字节,然后读取这些数据并打印。

test_i2c.h文件:

#define TEST_I2C_GPIO_SCL        (GPIO_NUM_42)
#define TEST_I2C_GPIO_SDA        (GPIO_NUM_41)

void TEST_I2C_I2CConfig(void) ;
void TEST_I2C_eeprom_wr_test(void) ;
void TEST_I2C_eeprom_rd_test(void) ;

test_i2c.c文件:

#include "driver/i2c.h"

#include "test_i2c.h"


void TEST_I2C_I2CConfig(void)
{
    esp_err_t        iErrCode ;
    i2c_config_t     stI2CConfigInfo ;

    stI2CConfigInfo.mode             = I2C_MODE_MASTER ;
    stI2CConfigInfo.sda_io_num       = TEST_I2C_GPIO_SDA ;
    stI2CConfigInfo.scl_io_num       = TEST_I2C_GPIO_SCL ;
    stI2CConfigInfo.sda_pullup_en    = true ;
    stI2CConfigInfo.scl_pullup_en    = true ;
    stI2CConfigInfo.master.clk_speed = 100000 ;
    stI2CConfigInfo.clk_flags        = I2C_SCLK_SRC_FLAG_FOR_NOMAL ;

    iErrCode = i2c_param_config(I2C_NUM_0, &stI2CConfigInfo) ;
    
    if(ESP_OK != iErrCode)
    {
        printf("i2c_param_config error. Return value is %d.\n", iErrCode) ;
    }
    
    iErrCode = i2c_driver_install(I2C_NUM_0, I2C_MODE_MASTER, 0, 0, 0) ;

    if(ESP_OK != iErrCode)
    {
        printf("i2c_driver_install error. Return value : %d.\n ", iErrCode) ;
    }
    
    return ;
}

void TEST_I2C_eeprom_wr_test(void)
{
    esp_err_t        iErrCode ;
    i2c_cmd_handle_t pvCmd ;
    unsigned char    aucSendBuf[8] = {0xAA,0x11,0xBB,0x22,0xCC,0x33,0xDD,0x44} ;

    /* 创建I2C命令序列 */
    pvCmd = i2c_cmd_link_create() ;

    /* 添加I2C起始命令 */
    i2c_master_start(pvCmd) ;

    /* 添加I2C单字节发送:设备总线地址 */
    i2c_master_write_byte(pvCmd, 0xA0, true) ;

    /* 添加I2C单字节发送:寄存器地址 */
    i2c_master_write_byte(pvCmd, 0x10, true) ;

    /* 添加I2C多字节发送:数据 */
    i2c_master_write(pvCmd, aucSendBuf, sizeof(aucSendBuf), true) ;

    /* 添加I2C停止命令 */
    i2c_master_stop(pvCmd) ;

    /* 启动命令序列,并等待完成 */
    iErrCode = i2c_master_cmd_begin(I2C_NUM_0, pvCmd, 500) ;

    if(ESP_OK != iErrCode)
    {
        printf("i2c_master_cmd_begin error. Return value : %d. \n ", iErrCode) ;

        while(1) ;
    }

    /* 命令执行完成,删除序列 */
    i2c_cmd_link_delete(pvCmd) ;

    printf("24C02 Write done.\n") ;

    return ;
}

void TEST_I2C_eeprom_rd_test(void)
{
    esp_err_t        iErrCode ;
    i2c_cmd_handle_t pvCmd ;
    unsigned char    aucRecvBuf[8] = {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00} ;

    /* 创建I2C命令序列 */
    pvCmd = i2c_cmd_link_create() ;

    /* 添加I2C起始命令 */
    i2c_master_start(pvCmd) ;

    /* 添加I2C单字节发送:设备总线地址(写入) */
    i2c_master_write_byte(pvCmd, 0xA0, true) ;

    /* 添加I2C单字节发送:寄存器地址 */
    i2c_master_write_byte(pvCmd, 0x10, true) ;

    /* 添加I2C重复起始命令 */
    i2c_master_start(pvCmd) ;

    /* 添加I2C单字节发送:设备总线地址(读出) */
    i2c_master_write_byte(pvCmd, 0xA1, true) ;

    /* 添加I2C多字节接收:数据 */
    i2c_master_read(pvCmd, aucRecvBuf, sizeof(aucRecvBuf), I2C_MASTER_LAST_NACK) ;

    /* 添加I2C停止命令 */
    i2c_master_stop(pvCmd) ;

    /* 启动命令序列,并等待完成 */
    iErrCode = i2c_master_cmd_begin(I2C_NUM_0, pvCmd, 1000) ;

    if(ESP_OK != iErrCode)
    {
        printf("i2c_master_cmd_begin failed. Return value : %d. \n", iErrCode) ;
        
        while(1) ;
    }

    /* 命令执行完成,删除序列 */
    i2c_cmd_link_delete(pvCmd) ;

    printf("24C02 read done. Value is %02x %02x %02x %02x %02x %02x %02x %02x.\n", aucRecvBuf[0], aucRecvBuf[1], aucRecvBuf[2], aucRecvBuf[3], 
        aucRecvBuf[4], aucRecvBuf[5], aucRecvBuf[6], aucRecvBuf[7]) ;

    return ;
}

main.c文件:

#include <stdint.h>
#include <stdio.h>
#include <stdbool.h>
#include <unistd.h>

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "freertos/semphr.h"

#include "test_i2c.h"

void app_main(void)
{
    TEST_I2C_I2CConfig() ;
    
    TEST_I2C_eeprom_wr_test() ;
    vTaskDelay(50) ;
    TEST_I2C_eeprom_rd_test() ;
    
    while (true)
    {
        vTaskDelay(50) ;
    }
}

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐