第二十三章 IIC_EEPROM实验
本章将学习ESP32-P4的硬件IIC接口去驱动24C02器件。在本章节,实现和24C02之间的双向通信,并把结果显示在LCD模块上。
本章分为如下几个小节:
23.1 24C02介绍
23.2 硬件设计
23.3 程序设计
23.4 下载验证
23.1 24C02介绍
关于IIC的描述,请回顾第十九章IIC_EXIO实验,本小节主要介绍24C02,首先科普一下何为EEPROM存储器?
其实EEPROM全程是“电可擦除可编程只读存储器”,即“Electrically Erasable Programmable Read-Only Memory”,特性就是数据掉电不丢失。
24C02是一个2K bit的串行EEPROM存储器,内部含有256个字节。在24C02里面还有一个8字节的页写缓冲器。该设备的通信方式IIC,通过其SCL和SDA与其他设备通信,芯片的引脚图如下图所示。

图23.1.1 24C02引脚图
上图的WP引脚是写保护引脚,接高电平只读,接地允许读和写,我们的板子设计是把该引脚接地。每一个设备都有自己的设备地址,24C02也不例外。前面提及到有7位寻址、11位寻址,这里的位数就是设备地址位数,24C02的设备地址就是7位的,具体格式如下图所示。

图23.1.2 24C02地址格式
24C02的设备地址是包括不可编程部分和可编程部分,不可编程部分也就是“1010”,可编程部分是根据上图的硬件引脚A0、A1和A2所决定。根据我们的板子设计,A0、A1和A2均接地处理,所以24C02设备地址为“1010000”即0x50。
这里还会涉及到24C02通信地址的概念,通信地址就是写操作地址和读操作地址,简单来说,就是设备地址和一个读写位的配合。上图中的地址格式最后一位R/W用于设置数据的传输方向,即读操作/写操作,0是写操作,1是读操作,所以24C02的读操作地址为:0xA1(0x50 << 1 | 1),写操作地址为:0xA0(0x50 << 1 | 0)。
下面把实验中到的数据传输时序讲解一下,分别是对24C02的写时序和读时序。24C02写时序图如下图所示。

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

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

图23.1.5 24C02读时序图
24C02读取数据的过程是一个复合的时序,其中包含写时序和读时序。先看第一个通信过程,这里是写时序,起始信号产生后,主机发送24C02的写操作地址0xA0(设备地址0x50 << 1 | 0),获取从机应答信号后,接着发送需要读取的内存地址;在读时序中,起始信号产生后,主机发送24C02的读操作地址0xA1(设备地址0x50 << 1 | 1),获取从机应答信号后,接着从机返回刚刚在写时序中内存地址的数据,以字节为单位传输在总线上,假如主机获取数据后返回的是应答信号,那么从机会一直传输数据,当主机发出的是非应答信号并以停止信号发出为结束,从机就会结束传输。
23.2 硬件设计
23.2.1 例程功能
每按下KEY1,MCU通过IIC总线向24C02写入数据,通过按下KEY0来控制24C02读取数据。同时在LCD上面显示相关信息。LED0闪烁用于提示程序正在运行。
23.2.2 硬件资源
1)LED灯
LED 0 - IO51
2)RGBLCD / MIPILCD(引脚太多,不罗列出来)
3)XL9555
IIC_INT - IO36
IIC_SDA - IO33
IIC_SCL - IO32
EXIO_8 - KEY0
EXIO_9 - KEY1
4)24C02
IIC_SDA - IO33
IIC_SCL - IO32
23.2.3 原理图
24C02器件相关原理图,如下图所示。

图23.2.3.1 24C02硬件原理图
23.3 程序设计
IIC外设驱动已经在第十九章19.3.1小节做了说明,这里就不再赘述了。
23.3.1 程序流程图

图23.3.1.1 IIC_EEPROM实验程序流程图
23.3.2 程序解析
在14_iic_eeprom例程中,作者在14_iic_eeprom\components\BSP路径下新建了1个文件夹AT24C02,并且需要更改CMakeLists.txt内容,以便在其他文件上调用。
IIC驱动代码跟19.3.3小节的IIC驱动代码部分是一样的,这里就不再赘述了,直接对at24c02驱动做介绍。
1. AT24C02驱动代码
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。AT24CXX驱动源码包括两个文件:at24c02.c和at24c02.h。
下面先解析at24c02.h的程序。对AT24C02的器件地址和AT24C02芯片最大地址做了相关定义。
#define AT_ADDR 0x50 /* 24c02设备地址 */ #define AT24C02 255
接下来,解析一下at24c02.c的程序,首先来看一下AT24C02器件的初始化函数at24c02_init,代码如下:
/**
* @brief 初始化AT24C02
* @param 无
* @retval ESP_OK:初始化成功
*/
esp_err_t at24c02_init(void)
{
/* 未调用myiic_init初始化IIC */
if (bus_handle == NULL)
{
ESP_ERROR_CHECK(myiic_init());
}
i2c_device_config_t eeprom_i2c_dev_conf = {
.dev_addr_length = I2C_ADDR_BIT_LEN_7, /* 从机地址长度 */
.scl_speed_hz = IIC_SPEED_CLK, /* 传输速率 */
.device_address = AT_ADDR, /* 从机7位的地址 */
};
/* I2C总线上添加AT24C02设备 */
ESP_ERROR_CHECK(i2c_master_bus_add_device( bus_handle, &eeprom_i2c_dev_conf,
&eeprom_handle));
return ESP_OK;
}
在AT24C02初始化函数中,首先对eeprom_i2c_dev_conf变量的成员进行赋值,设置AT24C02的地址长度、设备地址以及传输速率,然后调用i2c_master_bus_add_device函数对AT24C02设备进行初始化。
下面先看一下at24c02_write_one_byte函数,实现在AT24C02芯片指定地址写入一个数据,代码如下:
/**
* @brief 在AT24C02指定地址写入一个数据
* @param addr: 写入数据的目的地址
* @param data: 要写入的数据
* @retval 无
*/
void at24c02_write_one_byte(uint8_t addr, uint8_t data)
{
uint8_t send_buf[2] = {0};
send_buf[0] = addr % 256;
send_buf[1] = data;
ESP_ERROR_CHECK(i2c_master_transmit(eeprom_handle, send_buf, 2, -1));
/* 由于AT24C02写入过慢,需延迟10ms左右时间 */
esp_rom_delay_us(10000);
}
该函数的实现,主要调用i2c_master_transmit函数。在这里需要进行数据整合,把写入数据的目的地址和要写入的数据重新存放到一个buf。在这里需要注意存放顺序,写入数据的目的地址要在要写入数据的前面,这样子通过i2c_master_transmit函数发送出去的数据才符合AT24C02的写数据操作。这里需要注意:EEPROM写入比较慢,所以需要延时10ms,等待eeprom写入完毕。
继续看一下at24c02_read_one_byte,实现从AT24C02内存地址读取数据,代码如下:
/**
* @brief 在AT24C02指定地址读出一个数据
* @param addr: 开始读数的地址
* @retval 读到的数据
*/
uint8_t at24c02_read_one_byte(uint8_t addr)
{
uint8_t data = 0;
ESP_ERROR_CHECK(i2c_master_transmit_receive(eeprom_handle, &addr, 1, &data,
1, -1));
return data;
}
该函数的实现主要调用i2c_master_transmit_receive函数。从AT24C02的addr内存地址处读出data数据,作为函数返回值返回。
有了基本的读写函数接口,就可以写一个比较简单的检测函数at24c02_check,用来测试IIC总线上是否存在24C02或者说器件是否正常,代码如下所示。
/**
* @brief 检查AT24C02是否正常
* @note 检测原理: 在器件的末地址写如0X55, 然后再读取, 如果读取值为0X55
* 则表示检测正常. 否则,则表示检测失败.
* @param 无
* @retval 检测结果
* 0: 检测成功
* 1: 检测失败
*/
uint8_t at24c02_check(void)
{
uint8_t temp;
uint16_t addr = AT24C02;
temp = at24c02_read_one_byte(addr); /* 避免每次开机都写AT24CXX */
if (temp == 0X55) /* 读取数据正常 */
{
return 0;
}
else /* 排除第一次初始化的情况 */
{
at24c02_write_one_byte(addr, 0X55); /* 先写入数据 */
temp = at24c02_read_one_byte(255); /* 再读取数据 */
if (temp == 0X55)
{
return 0;
}
}
return 1;
}
在这里,就是利用EEPROM芯片掉电不丢失的特性,在第一次写入了某个值之后,再去读一下看是否写入成功,这种方式就可以去检测芯片是否可以正常工作。
有时候操作单位往往不是单个字节,所以这里我们也提供了多字节写和多字节读的函数接口,代码如下所示。
/**
* @brief 在AT24C02里面的指定地址开始读出指定个数的数据
* @param addr : 开始读出的地址 对24c02为0~255
* @param pbuf : 数据数组首地址
* @param datalen : 要读出数据的个数
* @retval 无
*/
void at24c02_read(uint8_t addr, uint8_t *pbuf, uint8_t datalen)
{
while (datalen--)
{
*pbuf++ = at24c02_read_one_byte(addr++);
}
}
/**
* @brief 在AT24C02里面的指定地址开始写入指定个数的数据
* @param addr : 开始写入的地址 对24c02为0~255
* @param pbuf : 数据数组首地址
* @param datalen : 要写入数据的个数
* @retval 无
*/
void at24c02_write(uint8_t addr, uint8_t *pbuf, uint8_t datalen)
{
while (datalen--)
{
at24c02_write_one_byte(addr, *pbuf);
addr++;
pbuf++;
}
}
以上两个函数都是基于单个字节读和单个字节写函数实现的,这里就不多讲了。
3. CMakeLists.txt文件
本例程的功能实现主要依靠IIC驱动和AT24C02驱动。要在main函数中,成功调用AT24C02文件中的内容,就得需要修改BSP文件夹下的CMakeLists.txt文件,修改如下:
set(src_dirs
LED
LCD
MYIIC
XL9555
AT24C02)
set(include_dirs
LED
LCD
MYIIC
XL9555
AT24C02)
set(requires
driver
esp_lcd
esp_common)
idf_component_register( SRC_DIRS ${src_dirs} INCLUDE_DIRS ${include_dirs} REQUIRES ${requires})
component_compile_options(-ffast-math -O3 -Wno-error=format=-Wno-format)
4. main.c驱动代码
在main.c里面编写如下代码。
void app_main(void)
{
esp_err_t ret;
uint8_t key = 0;
uint16_t i = 0;
uint8_t datatemp[TEXT_SIZE];
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());
ESP_ERROR_CHECK(nvs_flash_init());
}
led_init(); /* LED初始化 */
lcd_init(); /* LCD屏初始化 */
xl9555_init(); /* 初始化按键 */
at24c02_init(); /* 初始化24CXX */
lcd_show_string(30, 50, 200, 16, 16, "ESP32-P4", RED);
lcd_show_string(30, 70, 200, 16, 16, "EEPROM TEST", RED);
lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);
lcd_show_string(30, 110, 200, 16, 16, "KEY1:Write KEY0:Read", RED);
while (at24c02_check()) /* 检测不到24c02 */
{
lcd_show_string(30, 130, 200, 16, 16, "24C02 Check Failed!", RED);
vTaskDelay(pdMS_TO_TICKS(500));
lcd_show_string(30, 130, 200, 16, 16, "Please Check! ", RED);
vTaskDelay(pdMS_TO_TICKS(500));
LED0_TOGGLE();
}
lcd_show_string(30, 130, 200, 16, 16, "24C02 Ready!", RED);
while (1)
{
key = xl9555_key_scan(0); /* 按键扫描 */
if (key == KEY1_PRES) /* KEY1按下,写入24C02 */
{
lcd_fill(0, 150, 239, 319, WHITE); /* 清除显示 */
lcd_show_string(30, 150, 200, 16, 16,"Start Write 24C02....", BLUE);
at24c02_write(0, (uint8_t *)g_text_buf, TEXT_SIZE); /* 写数据 */
lcd_show_string(30, 150, 200, 16, 16,"24C02 Write Finished!", BLUE);
}
if (key == KEY0_PRES) /* KEY0按下,读取字符串并显示 */
{
lcd_show_string(30, 150, 200, 16, 16,"Start Read 24C02.... ", BLUE);
at24c02_read(0, datatemp, TEXT_SIZE); /* 读取数据 */
lcd_show_string(30, 150, 200, 16, 16,"The Data Readed Is: ", BLUE);
lcd_show_string(30, 170, 200, 16, 16, (char *)datatemp, BLUE);
}
i++;
if (i == 20)
{
LED0_TOGGLE();
i = 0;
}
vTaskDelay(pdMS_TO_TICKS(10));
}
}
在app_main函数中,初始化AT24C02后,检测AT24C02是否存在。若AT24C02正常,则会不断等待按键输入。通过KEY0去读取0地址存放的数据并把数据显示在LCD上;通过KEY1向0地址处写入g_text_buf数据并在LCD上显示传输中,完成后并显示“24C02 Write Finished!”。
23.4 下载验证
将程序下载到开发板后,可以看到LED0不停闪烁,提示程序已经在运行了,先按下KEY1写入数据,然后再按KEY0读取数据,最终LCD显示的内容如图23.4.1所示:

图23.4.1 IIC_EEPROM实验程序运行效果图