图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总线,该函数原型如下所示:
esp_err_t i2c_param_config(i2c_port_t i2c_num, const i2c_config_t *i2c_conf);
该函数的形参描述如下表所示:
参数 | 描述 |
| IIC端口号,有I2C_NUM_0、I2C_NUM_1两个端口可供配置 |
| 指向IIC配置的指针。(在i2c.h中有定义) |
表20.3.2.1 i2c_param_config函数形参描述
返回值:ESP_OK表示配置成功。其他表示配置失败。
该函数使用i2c_config_t类型的结构体变量传入,该结构体的定义如下所示:
| 成员变量 | |
| | 这里设置为 I2C_MODE_MASTER,表示主设备模式。还有其他选项 I2C_MODE_SLAVE 表示从设备模式。 |
| |
conf.sda_pullup_en: SDA 引脚是否启用内部上拉 | 这里设置为 GPIO_PULLUP_ENABLE,启用内部上拉,避免悬空。 |
| |
conf.scl_pullup_en: SCL 引脚是否启用内部上拉 | 这里设置为 GPIO_PULLUP_ENABLE,启用内部上拉,避免悬空。 |
conf.master.clk_speed: 以主设备模式工作时,IIC 的时钟速度 | 这里设置为 400000,即 400kHz,是 IIC的常用速度。 |
表20.3.2.2 i2c_config_t结构体参数值描述
完成上述结构体参数配置之后,可以将结构传递给 i2c_param_config () 函数,用以实例化IIC并返回IIC句柄。
2,安装IIC驱动
该函数设置某个管脚的中断服务函数,该函数原型如下所示:
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);
该函数的形参描述如下表所示:
参数 | 描述 |
| IIC端口号,有I2C_NUM_0、I2C_NUM_1两个端口可供配置 |
| I2C 工作模式,包括 I2C_MODE_MASTER(主模式)、I2C_MODE_SLAVE(从模式)和 I2C_MODE_MASTER_SLAVE(主从模式),类型为 i2c_mode_t。 |
| I2C 接收缓冲区长度,单位为字节,只有在从模式才用到,缓存大小必须大于 0,此处使用一个常量 |
| I2C 发送缓冲区长度,单位为字节,只有在从模式才用到,缓存大小必须大于 0,此处使用一个常量 |
| I2C 驱动中断标志,用于控制是否支持 I2C 中断处理程序。此处使用一个常量 0 表示不使用中断。 |
表20.3.2.3 i2c_driver_install()函数形参描述
返回值:ESP_OK表示配置成功。其他表示配置失败。
3,IIC读写操作
根据函数功能,以下函数可以归为一类进行讲解,下面将以表格的形式逐个介绍这些函数的作用与参数。
| |
| 此函数用于创建一个新的 IIC 命令链,并返回其句柄。在发送或接收 IIC数据时,需要先建立一个命令链,然后添加相应的命令。 |
| 向 IIC命令链中添加开始信号。函数参数如下: cmd:要添加的IIC命令链句柄 |
| 向 IIC命令链中添加停止信号。函数参数如下: cmd:要添加的IIC命令链句柄 |
| 向IIC命令链中添加一个字节的写操作 函数参数如下: cmd_handle:要添加的IIC命令链句柄 data:要发送的数据 ack_en:使能ACK信号 |
| cmd_handle:要添加的IIC命令链句柄 *data:将存储接收到的字节的指针 data_len:数据大小 ack:ACK信号 |
| cmd_handle:要添加的IIC命令链句柄 *data:将存储要发送的字节的指针 data_len:数据大小 ack:ACK信号 |
| 执行之前添加到 IIC命令链中的操作。函数参数如下:i2c_num:需要操作的 I2C 端口号 cmd:要添加的IIC命令链句柄 ticks_to_wait :超时时间,单位为操作系统 tick |
| 删除 IIC命令链。在执行完命令链后,需要调用此函数释放资源。函数参数如下: cmd:要删除的IIC命令链句柄 |
表20.3.2.4 IIC读写操作函数描述
4,初始化24C02的IIC引脚
在配置24C02芯片引脚之前,需要对IIC初始化进行一个判断。因为ESP32系统不支持同一个外设进行两次初始化,否则会出现系统不断复位的现象。因此,我们需要在24C02的初始化前面添加IIC端口判断。
5,在指定地址写入数据
该函数用于在24C02指定地址写入一个数据,其函数原型如下所示:
void at24cxx_write_one_byte(uint16_t addr, uint8_t data);
该函数的形参描述,如下表所示:
表20.3.2.5 函数at24cxx_write_one_byte ()形参描述
返回值:无。
6,在指定地址读出数据
该函数用于在24C02指定地址读出一个数据,其函数原型如下所示:
uint8_t at24cxx_read_one_byte(uint16_t addr);
该函数的形参描述,如下表所示:
表20.3.2.6 函数at24cxx_read_one_byte ()形参描述
返回值:读取的数据。
7,检查24C02是否正常
在器件的末地址写如:0X55, 然后再读取, 如果读取值为0X55则表示检测正常。否则,则表示检测失败,其函数原型如下所示:
uint8_t at24cxx_check(void);
返回值:1表示检验成功。0表示检验失败。
8,在指定地址读出数据
在24C02里面的指定地址开始读出指定个数的数据,其函数原型如下所示:
void at24cxx_read(uint16_t addr, uint8_t *pbuf, uint16_t datalen);
该函数的形参描述,如下表所示:
表20.3.2.7 函数at24cxx_read()形参描述
返回值:无。
9,在指定地址读出数据
在24C02里面的指定地址开始读出指定个数的数据,其函数原型如下所示:
void at24cxx_write(uint16_t addr, uint8_t *pbuf, uint16_t 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系列型号,具体定义如下:
#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
2,24cxx.c文件
在24cxx.c文件中,读/写操作函数对于不同容量大小的24Cxx芯片都有相对应的代码块解决处理。下面先看一下at24cxx_write_one_byte函数,实现在AT24Cxx芯片指定地址写入一个数据,代码如下:
/**
* @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所示。
芯片 | 页数 | 每页字节数 | 总的字节数 | 字节寻址地址线数量 |
AT24C01A | 16 | 8 | 128 | 7 |
AT24C02 | 32 | 8 | 256 | 8 |
AT24C04 | 32 | 16 | 512 | 9 |
AT24C08A | 64 | 16 | 1024 | 10 |
AT24C16A | 128 | 16 | 2048 | 11 |
AT24C32 | 128 | 32 | 4096 | 12 |
AT24C64A | 256 | 32 | 8192 | 13 |
表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;
}
这里的函数的实现跟前面第20.1小节24C02数据传输中的读时序一致,主机首先调用i2c_master_start()函数产生起始信号,然后调用i2c_master_write_byte()函数发送第1个字节数据设备写地址;收到应答信号后,继续发送第2个1字节数据内存地址addr;等待接收应答后,重新调用i2c_master_start()函数产生起始信号,这一次的设备方向改变了,调用i2c_master_write_byte()函数发送设备读地址,同时使用i2c_master_read_byte()去读取从从机发出来的数据。由于i2c_master_read_byte函数的参数是指针类型&data,所以在获取完1个字节的数据后,主机发送非应答信号,停止数据传输,最终调用i2c_master_stop()函数产生停止信号,返回由从机addr中读取到的数据。
为了方便检测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;
}
这里利用的是EEPROM芯片掉电不丢失的特性,在第一次写入了某个值之后,再去读一下是否写入成功,这种方式去检测芯片是否正常工作。
此外方便多字节写入和读取,还定义了在指定地址读取指定个数的函数以及在指令地址写入指定个数的函数,代码如下:
/**
* @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++;
}
}
对于这两个函数都是调用前面的单字节操作函数去实现的,利用while循环,连续调用单字节操作函数去实现,这里就不多讲。
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)
上述的红色24CXX驱动需要由开发者自行添加,以确保24C02驱动能够顺利集成到构建系统中。这一步骤是必不可少的,它确保了24C02驱动的正确性和可用性,为后续的开发工作提供了坚实的基础。
20.3.5 实验应用代码
打开main/main.c文件,该文件定义了工程入口函数,名为app_main。该函数代码如下。
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中读回的数据。