目录

第二十章-ESP32S3-IIC_EEPROM-实验

第二十章 ESP32S3 IIC_EEPROM 实验

本章使用 ESP32-S3 驱动板载的 EEPROM 进行读写操作。通过本章的学习使用 IIC 接口驱动 24C02 器件。
本章分为如下几个小节:
20.1 24C02 简介
20.2 硬件设计
20.3 程序设计
20.4 下载验证

20.1 24C02 简介

        24C02 是一个 2K bit 的串行 EEPROM 存储器,内部含有 256 个字节。在 24C02 里面还有一个 8 字节的页写缓冲器。该设备的通信方式 IIC,通过其 SCL 和 SDA 与其他设备通信,芯片的
引脚图如图 35.1.2.1 所示。

https://i-blog.csdnimg.cn/direct/a2e1fd30d5064f04baae74f7b8f92282.png

图 20.1.1 24C02 引脚图

核心特性:

​特性维度​​说明​
​存储容量​​2 Kbit​​,相当于​​256字节​​ (Byte)。
​通信接口​​I2C总线​​(双线制:SCL时钟线,SDA数据线),支持​​标准模式(100kHz)和快速模式(400kHz)​
​工作电压​范围宽泛:​​1.8V 至 6.0V​,使其能适应多种电源环境,包括电池供电的低功耗设备。
​关键特性​- ​​低功耗CMOS技术​​ - ​​自定时擦写周期​​ - ​​写保护功能(WP引脚)​​ - ​​16字节页写缓冲器​
​耐久性与可靠性​- ​​1,000,000 次编程/擦除周期​​ - ​​数据保存时间:100年

表20.1.1 24C02核心特性

        上图的 WP 引脚是写保护引脚,接高电平只读,接地允许读和写,我们的板子设计是把该引脚接地。每一个设备都有自己的设备地址, 24C02 也不例外,但是 24C02 的设备地址是包括不可编程部分和可编程部分,可编程部分是根据上图的硬件引脚 A0、 A1 和 A2 所决定。设备地址最后一位用于设置数据的传输方向,即读操作/写操作, 0是写操作, 1是读操作,具体格式如下图所示:

https://i-blog.csdnimg.cn/direct/552f260b00984bde8836840027e393ee.png

图 20.1.2 24C02 设备地址格式图

        当前板子设计A0、 A1 和 A2 均接地处理,所以 24C02 设备的读操作地址为:0xA1;写操作地址为: 0xA0。
        在上一章已经说过 IIC 总线的基本读写操作,那么就可以基于 IIC 总线的时序的上,理解24C02 的数据传输时序。
        下面介绍实验中数据传输时序,分别是对 24C02 的写时序和读时序。 24C02 写时序下图:

https://i-blog.csdnimg.cn/direct/2552bf471014405a861251a7bc10e516.png

图 20.1.3 24C02 写时序图

        上图展示的主机向 24C02 写操作时序图,主机在 IIC 总线发送第 1 个字节的数据为 24C02的设备地址 0xA0,用于寻找总线上找到 24C02,在获得 24C02 的应答信号之后,继续发送第 2个字节数据,该字节数据是 24C02 的内存地址,再等到 24C02 的应答信号,主机继续发送第 3字节数据,这里的数据即是写入在第 2 字节内存地址的数据。主机完成写操作后,可以发出停止信号,终止数据传输。
        上面的写操作只能单字节写入到 24C02,效率比较低,所以 24C02 有页写入时序,大大提
高了写入效率,下面看一下 24C02 页写时序图,图 20.1.4 所示。

https://i-blog.csdnimg.cn/direct/d17b09262ee947dbb4aeff7e1017eb15.png

图 20.1.4 24C02 页写时序

        在单字节写时序时,每次写入数据时都需要先写入设备的内存地址才能实现,在页写时序中,只需要告诉 24C02 第一个内存地址 1,后面数据会按照顺序写入到内存地址 2,内存地址 3
等,大大节省了通信时间,提高了时效性。因为 24C02 每次只能 8bit 数据,所以它的页大小也就是 1 字节。页写时序的操作方式跟上面的单字节写时序差不多,所以不作过多解释了。参考以上说明去理解页写时序。
        说完两种写入方式之后,下面看一下图 24C02 的读时序:

https://i-blog.csdnimg.cn/direct/7b978694ab1f4b02a0a4b913ef9d37f1.png

图 20.1.5 24C02 读时序图

        24C02 读取数据的过程是一个复合的时序,其中包含写时序和读时序。先看第一个通信过程,这里是写时序,起始信号产生后,主机发送 24C02 设备地址 0xA0,获取从机应答信号后,接着发送需要读取的内存地址;在读时序中,起始信号产生后,主机发送24C02设备地址0xA1,获取从机应答信号后,接着从机返回刚刚在写时序中内存地址的数据,以字节为单位传输在总线上,假如主机获取数据后返回的是应答信号,那么从机会一直传输数据,当主机发出的是非应答信号并以停止信号发出为结束,从机就会结束传输。

20.2 硬件设计

20.2.1 例程功能

        通过按下 KEY0~4 按键来控制蜂鸣器和 LED 灯开关状态, KEY0 和 KEY1 控制蜂鸣器开与
关; KEY2 和 KEY3 控制 LED 灯开与关。
20.2.2 硬件资源
        1. LED
                LED - IO1
        2. 独立按键
                KEY0(XL9555) - IO1_7
                KEY1(XL9555) - IO1_6
        3. UART_NUM_0(U0TX、 U0RX 连接至板载 USB 转串口芯片上)
                U0TXD - IO43
                U0RXD - IO44
        4. 24C02
                IIC_SCL - IO42
                IIC_SDA - IO41

20.2.3 原理图
        本章实验使用了板载的 24C02 芯片,该芯片是一个 EEPROM, MCU 是通过两个 GPIO 与该EEPROM 进行连接与通信的,该 EEPROM 与 MCU 的连接原理图,如下图所示:

https://i-blog.csdnimg.cn/direct/0d7718ded03a44b6b88c54ecc81b23ea.png

图 20.1.3.1 24C02 与 MCU 的连接原理图

20.3 程序设计

20.3.1 程序流程图
        程序流程图能帮助我们更好的理解一个工程的功能和实现的过程,对学习和设计工程有很好的主导作用。下面看看本实验的程序流程图:

https://i-blog.csdnimg.cn/direct/88535e89cb2c4264bbc7bd659c32ad1c.png

图 20.3.1.1 24C02 实验程序流程图

20.3.2 IIC_EEPROM 函数解析
        ESP-IDF 提供了一套 API 来配置 IIC。要使用此功能,需要导入必要的头文件:

#include "driver/i2c.h"

接下来,作者将介绍一些常用的 ESP32-S3 中的 IIC 函数,以及 24C02 中用到的函数,这些函数的描述及其作用如下:
(1) IIC 相关函数

         IIC 相关函数已在上一章(  ,19.3.2 IIC_EXIO 函数解析)做了详细介绍,可以参考上一章。

(2)初始化 24C02 的 IIC 引脚

        在配置 24C02 芯片引脚之前,需要对 IIC 初始化进行一个判断。因为 ESP32 系统不支持同一个外设进行两次初始化,否则会出现系统不断复位的现象。因此,我们需要在 24C02 的初始化前面添加 IIC 端口判断。

(3)在指定地址写入数据

        该函数用于在 24C02 指定地址写入一个数据,其函数原型如下所示:

void at24cxx_write_one_byte(uint16_t addr, uint8_t data);

        该函数的形参描述,如下表所示:

形参描述
addr写入数据的目的地址
data要写入的数据

表 20.3.2.5 函数 at24cxx_write_one_byte ()形参描述

        返回值:无。

(4)在指定地址读出数据

        该函数用于在 24C02 指定地址读出一个数据,其函数原型如下所示:

uint8_t at24cxx_read_one_byte(uint16_t addr);

        该函数的形参描述,如下表所示:

形参描述
addr开始读数的地址

表 20.3.2.6 函数 at24cxx_read_one_byte ()形参描述

        返回值:读取的数据。

(5) 检查 24C02 是否正常

        在器件的末地址写如: 0X55, 然后再读取, 如果读取值为 0X55 则表示检测正常。 否则, 则
表示检测失败,其函数原型如下所示:

uint8_t at24cxx_check(void);

        返回值: 1 表示检验成功。 0 表示检验失败。

(6)在指定地址读出数据

        在 24C02 里面的指定地址开始读出指定个数的数据,其函数原型如下所示:

void at24cxx_read(uint16_t addr, uint8_t *pbuf, uint16_t datalen);

        该函数的形参描述,如下表所示:

形参描述
addr开始读出的地址 对 24c02 为 0~255
*pbuf数据数组首地址
datalen要读出数据的个数

表 20.3.2.7 函数 at24cxx_read()形参描述

        返回值:无。

(7)在指定地址读出数据

        在 24C02 里面的指定地址开始读出指定个数的数据,其函数原型如下所示:

void at24cxx_write(uint16_t addr, uint8_t *pbuf, uint16_t datalen);

        该函数的形参描述,如下表所示:

形参描述
addr开始写入的地址 对 24c02 为 0~255
*pbuf数据数组首地址
datalen要写入数据的个数

表 20.3.2.8 函数 at24cxx_write ()形参描述

        返回值:无。

20.3.3 IIC_EEPROM 驱动解析

        在 IDF 版的 10_iic_eeprom 例程中,作者在 10_iic_eeprom \components\BSP 路径下新增了一个 24CXX 文件夹,用于存放 24cxx.c 和 24cxx.h 这两个文件。其中, 24cxx.h 文件负责声明24CXX 相关的函数和变量,而 24cxx.c 文件则实现了 24CXX 的驱动代码。下面,介绍这两个文件的实现内容。

(1)24cxx.h 文件

        为了使代码功能更加健全,所以在 24cxx.h中宏定义了不同容量大小的 24C系列型号,具体
定义如下:

/* 24c02设备地址 */
#define AT_ADDR     (0x50)

#define AT24C01     127
#define AT24C02     255
#define AT24C04     511
#define AT24C08     1023
#define AT24C16     2047
#define AT24C32     4095
#define AT24C64     8191
#define AT24C128    16383
#define AT24C256    32767

/* 开发板使用的是24c02,所以定义EE_TYPE为AT24C02 */
#define EE_TYPE     AT24C02

/* 函数声明 */
void at24cxx_init();                                               /* 初始化IIC */
uint8_t at24cxx_check(void);                                        /* 检查器件 */
uint8_t at24cxx_read_one_byte(uint16_t addr);                       /* 指定地址读取一个字节 */
void at24cxx_write_one_byte(uint16_t addr,uint8_t data);            /* 指定地址写入一个字节 */
void at24cxx_write(uint16_t addr, uint8_t *pbuf, uint16_t datalen); /* 从指定地址开始写入指定长度的数据 */
void at24cxx_read(uint16_t addr, uint8_t *pbuf, uint16_t datalen);  /* 从指定地址开始读出指定长度的数据 */

(2) 24cxx.c 文件
        在 24cxx.c 文件中,读/写操作函数对于不同容量大小的 24Cxx 芯片都有相对应的代码块解决处理。下面先看一下 at24cxx_write_one_byte 函数,实现在 AT24Cxx 芯片指定地址写入一个数
据,代码如下:

/**
 * @brief       在AT24CXX指定地址写入一个数据
 * @param       addr: 写入数据的目的地址
 * @param       data: 要写入的数据
 * @retval      无
 */
void at24cxx_write_one_byte(uint16_t addr, uint8_t data)
{
    i2c_cmd_handle_t cmd = i2c_cmd_link_create();
    i2c_master_start(cmd);
        
    if(EE_TYPE > AT24C16)
    {
        i2c_master_write_byte(cmd, (AT_ADDR << 1) | I2C_MASTER_WRITE, ACK_CHECK_EN);    /* 发送写命令 */
        i2c_master_write_byte(cmd, addr >> 8, ACK_CHECK_EN);                            /* 发送高地址 */
    }
    else
    {
        i2c_master_write_byte(cmd, 0XA0 + ((addr/256) << 1), ACK_CHECK_EN);             /* 发送器件地址0XA0,写数据 */
    }

    i2c_master_write_byte(cmd, addr % 256, ACK_CHECK_EN);                               /* 发送低地址 */
    i2c_master_write_byte(cmd, data, ACK_CHECK_EN);
    i2c_master_stop(cmd);
    i2c_master_cmd_begin(at24cxx_master.port, cmd, 1000);
    i2c_cmd_link_delete(cmd);
    vTaskDelay(10);
}

        该函数的操作流程跟前面已经分析过的24C02单字节写时序一样,首先调用i2c_master_start()函数产生起始信号,然后调用i2c_master_write_byte()函数发送第1个字节数据设备地址;收到应答信号后,继续发送第2个1字节数据内存地址addr,最后发送第3个字节数据写入内存地址的数据data,24Cxx设备接收完数据,返回应答信号,主机调用i2c_master_stop()函数产生停止信号终止数据传输,最终需要延时10ms,等待eeprom写入完毕。
        函数兼容24Cxx系列多种容量,就在发送设备地址处做了处理,先看一下24Cxx芯片内存组织表,见表35.2.2.1所示。

https://i-blog.csdnimg.cn/direct/d1b31c7febb54ccc878e93709c3c0388.png

表 20.3.3.1 24Cxx 芯片内存组织表

        主机发送的设备地址和内存地址共同确定了要写入的地方,这里分析一下 24C16 的使用的是 i2c_master_write_byte(cmd, 0XA0 + ((addr/256) « 1), ACK_CHECK_EN);和 i2c_master_write_byte(cmd, addr %256), ACK_CHECK_EN)确定写入位置,由于它内存大小一共 2048 字节,所以只需要定义 11 个寻址地址线, 2048 = 2^11。主机下发读写命令的时候带了 3位,后面再跟 1 个字节(8 位)的地址,正好 11 位,就不需要再发后续的地址字节了。

        容量大于 24C16 的芯片,需要单独发送 2 个字节(甚至更多)的地址,如 24C32,它的大小为 4096,需要 12 个寻址地址线支持, 4096 = 2^12。 24C16 是 2 个字节刚刚好,而它需要三个字节才能确定写入的位置。 24C32 芯片规定设备写地址 0xA0/读地址 0xA1,后面接着发送 8位高地址,最后才发送 8 位低地址。与函数里面的操作是一致。

        接下来看一下 at24cxx_read_one_byte 函数,其定义如下:

/**
 * @brief       在AT24CXX指定地址读出一个数据
 * @param       addr: 开始读数的地址
 * @retval      读到的数据
 */
uint8_t at24cxx_read_one_byte(uint16_t addr)
{
    uint8_t data = 0;

    i2c_cmd_handle_t cmd = i2c_cmd_link_create();
    i2c_master_start(cmd);
    /* 根据不同的24CXX型号, 发送高位地址
     * 1, 24C16以上的型号, 分2个字节发送地址
     * 2, 24C16及以下的型号, 分1个低字节地址 + 占用器件地址的bit1~bit3位 用于表示高位地址, 最多11位地址
     *    对于24C01/02, 其器件地址格式(8bit)为: 1  0  1  0  A2  A1  A0  R/W
     *    对于24C04,    其器件地址格式(8bit)为: 1  0  1  0  A2  A1  a8  R/W
     *    对于24C08,    其器件地址格式(8bit)为: 1  0  1  0  A2  a9  a8  R/W
     *    对于24C16,    其器件地址格式(8bit)为: 1  0  1  0  a10 a9  a8  R/W
     *    R/W      : 读/写控制位 0,表示写; 1,表示读;
     *    A0/A1/A2 : 对应器件的1,2,3引脚(只有24C01/02/04/8有这些脚)
     *    a8/a9/a10: 对应存储整列的高位地址, 11bit地址最多可以表示2048个位置,可以寻址24C16及以内的型号
     */
    if(EE_TYPE > AT24C16)
    {
        i2c_master_write_byte(cmd, (AT_ADDR << 1) | I2C_MASTER_WRITE, ACK_CHECK_EN);    /* 发送写命令 */
        i2c_master_write_byte(cmd, addr >> 8, ACK_CHECK_EN);                            /* 发送高地址 */
    }
    else
    {
        i2c_master_write_byte(cmd, 0XA0 + ((addr / 256) << 1), ACK_CHECK_EN);           /* 发送器件地址0XA0,写数据 */
    }
    i2c_master_write_byte(cmd, addr % 256, ACK_CHECK_EN);                               /* 发送低地址 */
    i2c_master_start(cmd);
    i2c_master_write_byte(cmd, (AT_ADDR << 1) | I2C_MASTER_READ, ACK_CHECK_EN);
    i2c_master_read_byte(cmd, &data, ACK_CHECK_EN);
    i2c_master_stop(cmd);
    i2c_master_cmd_begin(at24cxx_master.port, cmd, 1000);
    i2c_cmd_link_delete(cmd);
    vTaskDelay(10);

    return data;
}

        检测 24Cxx 芯片是否正常工作,在这里也定义了一个检测函数,代码如下:

/**
 * @brief       检查AT24CXX是否正常
 * @note        检测原理: 在器件的末地址写如0X55, 然后再读取, 如果读取值为0X55
 *              则表示检测正常. 否则,则表示检测失败.
 * @param       无
 * @retval      检测结果
 *              0: 检测成功
 *              1: 检测失败
 */
uint8_t at24cxx_check(void)
{
    uint8_t temp;
    uint16_t addr = EE_TYPE;

    temp = at24cxx_read_one_byte(addr);     /* 避免每次开机都写AT24CXX */

    if (temp == 0X55)                       /* 读取数据正常 */
    {
        return 0;
    }
    else                                    /* 排除第一次初始化的情况 */
    {
        at24cxx_write_one_byte(addr, 0X55); /* 先写入数据 */
        temp = at24cxx_read_one_byte(255);  /* 再读取数据 */

        if (temp == 0X55)
        {
            return 0;
        }
    }

    return 1;
}

        多字节写入和读取,还定义了在指定地址读取指定个数的函数以及在指令地址写入指定个数的函数,代码如下:

/**
 * @brief       在AT24CXX里面的指定地址开始读出指定个数的数据
 * @param       addr    : 开始读出的地址 对24c02为0~255
 * @param       pbuf    : 数据数组首地址
 * @param       datalen : 要读出数据的个数
 * @retval      无
 */
void at24cxx_read(uint16_t addr, uint8_t *pbuf, uint16_t datalen)
{
    while (datalen--)
    {
        *pbuf++ = at24cxx_read_one_byte(addr++);
    }
}

/**
 * @brief       在AT24CXX里面的指定地址开始写入指定个数的数据
 * @param       addr    : 开始写入的地址 对24c02为0~255
 * @param       pbuf    : 数据数组首地址
 * @param       datalen : 要写入数据的个数
 * @retval      无
 */
void at24cxx_write(uint16_t addr, uint8_t *pbuf, uint16_t datalen)
{
    while (datalen--)
    {
        at24cxx_write_one_byte(addr, *pbuf);
        addr++;
        pbuf++;
    }
}

        其中多字节写入也可以用这个函数i2c_master_write_to_device。

20.3.4 CMakeLists.txt 文件

        打开本实验 BSP 下的 CMakeLists.txt 文件,其内容如下所示:

set(src_dirs
            24CXX
            IIC
            LED
            XL9555)

set(include_dirs
            24CXX
            IIC
            LED
            XL9555)

set(requires
            driver)

idf_component_register(SRC_DIRS ${src_dirs} INCLUDE_DIRS ${include_dirs} REQUIRES ${requires})

component_compile_options(-ffast-math -O3 -Wno-error=format=-Wno-format)

20.3.5 实验应用代码

i2c_obj_t i2c0_master;

const uint8_t g_text_buf[] = {"ESP32-S3 EEPROM"};   /* 要写入到24c02的字符串数组 */
#define TEXT_SIZE   sizeof(g_text_buf)              /* TEXT字符串长度 */

/**
 * @brief       显示实验信息
 * @param       无
 * @retval      无
 */
void show_mesg(void)
{
    /* 串口输出实验信息 */
    printf("\n");
    printf("********************************\n");
    printf("ESP32\n");
    printf("IIC EEPROM TEST\n");
    printf("ATOM@ALIENTEK\n");
    printf("KEY0:Write Data, KEY1:Read Data\n");
    printf("********************************\n");
    printf("\n");
}

/**
 * @brief       程序入口
 * @param       无
 * @retval      无
 */
void app_main(void)
{
    uint16_t i = 0;
    uint8_t err = 0;
    uint8_t key;
    uint8_t datatemp[TEXT_SIZE];
    esp_err_t ret;
    
    ret = nvs_flash_init();             /* 初始化NVS */

    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND)
    {
        ESP_ERROR_CHECK(nvs_flash_erase());
        ret = nvs_flash_init();
    }

    led_init();                         /* 初始化LED */
    i2c0_master = iic_init(I2C_NUM_0);  /* 初始化IIC0 */
    xl9555_init(i2c0_master);           /* IO扩展芯片初始化 */
    at24cxx_init(i2c0_master);          /* 初始化24CXX */
    show_mesg();                        /* 显示实验信息 */

    err = at24cxx_check();              /* 检测AT24C02 */
    
    if (err != 0)
    {
        while (1)                       /* 检测不到24c02 */
        {
            printf("24C02 check failed, please check!\n");
            vTaskDelay(500);
            LED_TOGGLE();               /* LED闪烁 */
        }
    }

    printf("24C02 Ready!\n");
    printf("\n");

    while(1)
    {
        key = xl9555_key_scan(0);
        
        switch (key)
        {
            case KEY0_PRES:
            {
                at24cxx_write(0, (uint8_t *)g_text_buf, TEXT_SIZE);
                printf("The data written is:%s\n", g_text_buf);
                break;
            }
            case KEY1_PRES:
            {
                at24cxx_read(0, datatemp, TEXT_SIZE);
                printf("The data read is:%s\n", datatemp);
                break;
            }
            default:
            {
                break;
            }
        }

        i++;

        if (i == 20)
        {
            LED_TOGGLE();               /* LED闪烁 */
            i = 0;
        }
        
        vTaskDelay(10);
    }
}

        在初始化完 EEPROM 后,会检测与 EEPROM 的连接是否正常,若与 EEPROM 的连接正常,则会不断地等待按键输入,若检测到 KEY0 按键被按下,则会往 EEPROM的指定地址中写入指定的数据,若检测到 KEY1按键被按下,则会从 EEPROM的指定地址中读取数据,并通过串口或者 VSCode 终端进行打印。

20.4 下载验证

        在完成编译和烧录操作后,若 MCU 与 EEPROM 的连接无误,则可以在串口助手或者VSCode 终端上看到“24C02 Ready!”的提示信息,此时可以按下 KEY0 按键往 EEPROM 的指定地址写入指定数据,然后再按下 KEY1 按键从 EEPROM 的指定地址将写入的数据读回来在串口助手或者 VSCode 终端上进行显示,此时便可以看到在串口助手或者 VSCode 终端上显示了“ESP32-S3 EEPROM”的提示信息,该提示信息就是从 EEPROM 中读回的数据。

https://i-blog.csdnimg.cn/direct/eefb1ce7cc9b48d58af8cb31bdbd2268.png