LV16-19-I2C-02-读写EEPROM

本文主要是STM32开发——I2C读写EE2PROM AT24C02的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
Windows windows11
Ubuntu Ubuntu16.04的64位版本
VMware® Workstation 16 Pro 16.2.3 build-19376536
SecureCRT Version 8.7.2 (x64 build 2214) - 正式版-2020年5月14日
开发板 正点原子 i.MX6ULL Linux阿尔法开发板
uboot NXP官方提供的uboot,NXP提供的版本为uboot-imx-rel_imx_4.1.15_2.1.0_ga(使用的uboot版本为U-Boot 2016.03)
linux内核 linux-4.15(NXP官方提供)
STM32开发板 正点原子战舰V3(STM32F103ZET6)
点击查看本文参考资料
  • 通用
分类 网址说明
官方网站https://www.arm.com/ARM官方网站,在这里我们可以找到Cotex-Mx以及ARMVx的一些文档
https://www.st.com/content/st_com/zh.htmlST官方网站,在这里我们可以找到STM32的相关文档
https://www.stmcu.com.cn/意法半导体ST中文官方网站,在这里我们可以找到STM32的相关中文参考文档
http://elm-chan.org/fsw/ff/00index_e.htmlFatFs文件系统官网
教程书籍《ARM Cortex-M3权威指南》ARM公司专家Joseph Yiu(姚文祥)的力作,中文翻译是NXP的宋岩
《ARM Cortex-M0权威指南》
《ARM Cortex-M3与Cortex-M4权威指南》
开发论坛http://47.111.11.73/forum.php开源电子网,正点原子的资料下载及问题讨论论坛
https://www.firebbs.cn/forum.php国内Kinetis开发板-野火/秉火(刘火良)主持的论坛,现也做STM32和i.MX RT
https://www.amobbs.com/index.php阿莫(莫进明)主持的论坛,号称国内最早最火的电子论坛,以交流Atmel AVR系列单片机起家,现已拓展到嵌入式全平台,其STM32系列帖子有70W+。
http://download.100ask.net/index.html韦东山嵌入式资料中心,有些STM32和linux的相关资料也可以来这里找。
博客参考http://www.openedv.com/开源网-原子哥个人博客
http://blog.chinaaet.com/jihceng0622博主是原Freescale现NXP的现场应用工程师
cortex-m-resources这其实并不算是一个博客,这是ARM公司专家Joseph Yiu收集整理的所有对开发者有用的官方Cortex-M资料链接(也包含极少数外部资源链接)
  • STM32
STM32STM32 HAL库开发实战指南——基于F103系列开发板野火STM32开发教程在线文档
STM32库开发实战指南——基于野火霸道开发板野火STM32开发教程在线文档
  • SD卡
SD Association提供了SD存储卡和SDIO卡系统规范
点击查看相关文件下载
STM32F103xx英文数据手册STM32F103xC/D/E系列的英文数据手册
STM32F103xx中文数据手册STM32F103xC/D/E系列的中文数据手册
STM32F10xxx英文参考手册(RM0008)STM32F10xxx系列的英文参考手册
STM32F10xxx中文参考手册(RM0008)STM32F10xxx系列的中文参考手册
Arm Cortex-M3 处理器技术参考手册-英文版Cortex-M3技术参考手册-英文版
STM32F10xxx Cortex-M3编程手册-英文版(PM0056)STM32F10xxx/20xxx/21xxx/L1xxxx系列Cortex-M3编程手册-英文版
SD卡相关资料——最新版本有关SD卡的一些资料可以从这里下载
SD卡相关资料——历史版本有关SD卡的一些历史版本资料可以从这里下载,比如后边看的SD卡2.0协议
SD 2.0 协议标准完整版这是一篇关于SD卡2.0协议的中文文档,还是比较有参考价值的,可以一看

一、EEPROM简介

EEPROM (Electrically Erasable Programmable read only memory)是指带电可擦可编程只读存储器。是一种掉电后数据不丢失的存储芯片。 EEPROM 可以在电脑上或专用设备上擦除已有信息,重新编程。一般用在即插即用。

我们这一节的笔记主要是关于AT24C02的使用,它是一个串行电可擦除和可编程只读存储器(EEPROM),总容量是256(2K/8)个字节,通过IIC通信。它是Atmel公司的一款芯片,我们我使用的战舰V3的开发板上时AT24C02BN,我们可以在这里找到它的数据手册:AT24C01A/02/04/08/16 (microchip.com)

二、AT24C02功能

1. 框图

我们可以看 AT24C01A/02/04/08/16 (microchip.com)

image-20230507122145166

引脚功能描述如下:

引脚 说明
A0~A2 Address Input
SDA Serial Data
SCL Serial Clock Input
WP Write Protect
VCC Supply Voltage(VCC = 1.8V to 5.5V)
GND Ground

2. 设备地址

我们知道I2C总线是通过设备地址来区分不同的设备,我们使用的AT24C02有三根地址线,它的地址是这样的:

image-20230507125856205

A0A2可以接GND(代表0)也可以接VCC(代表1),这样根据A0A2的接线的不同,可以有不同的地址,比如我们把A0~A2全接地,那么设备地址就是1010000x。(我们可以看 AT24C01A/02/04/08/16 (microchip.com)的 6. Device Addressing)

那么当我们要读数据的时候,发送的地址就是10100001 = 0xA1,当我们要写数据的时候发送的地址就是10100000 = 0xA0。

3. 多少页?

我们需要知道的一些信息:

1
AT24C02 一共是 2Kb = 2048/8B = 256B

我们看芯片手册有这么一句: 8-byte Page (2K) Write Modes,也就是说,这个256B按照8字节一页的方式,被分为32页:

1
AT24C02 一共是 256B/8 = 32

三、读写数据

1. 写操作

1.1 BYTE WRITE

image-20230507131203330

1.2 PAGE WRITE

image-20230507131219435

2. 读操作

2.1 CURRENT ADDRESS READ

image-20230507131440446

2.2 RANDOM READ

image-20230507131453955

2.3 SEQUENTIAL READ

image-20230507131505419

3. 怎么找到要读写的位置?

上边是读写操作的过程,那么我们在实际读写过程中,怎么确定我们要往那个地址写数据呢?AT24C02一共是2Kbit,也就是256x8bit,也就是一共256页,每一页是1字节(8位),地址在实际读写的过程中是这样的:

地址 数据
0x00 xxxx xxxx
0x01 xxxx xxxx
0x02 xxxx xxxx
…… ……
0xFF xxxx xxxx

四、I2C读写实例

1. 硬件设计

image-20230507133622454

可以看到这里的AT24C02接在了PB6和PB7,也就是I2C1上边。

2. 物理I2C读写AT24C02

一般步骤如下:

(1)配置通讯使用的目标引脚为开漏模式;

(2)使能 I2C 外设的时钟;

(3)配置 I2C 外设的模式、地址、速率等参数并使能 I2C 外设;

(4)编写基本 I2C 按字节收发的函数;

(5)编写读写 EEPROM 存储内容的函数;

(6)编写测试程序,对读写数据进行校验。

2.1 STM32CubeMX配置

image-20230507134551105

2.2 宏定义

我们先来定义一些要用的宏:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define EEPROM_PAGESIZE  8    // AT24C01/02每页有8个字节,一共是32页
#define I2C_OWN_ADDRESS7 0X0A // STM32自己的地址,这个地址只要与STM32外挂的I2C器件地址不一样即可

/*
* AT24C02 2kb = 2048bit = 2048/8 B = 256 B
* 32 pages of 8 bytes each
*
* Device Address
* 1 0 1 0 A2 A1 A0 R/W
* 1 0 1 0 0 0 0 0 = 0XA0
* 1 0 1 0 0 0 0 1 = 0XA1
*/

#define EEPROM_ADDRESS 0xA0 // AT24C02的地址(A0~A2全部接0了)

2.3 写入一个字节数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
uint32_t I2C_EE_ByteWrite(uint8_t* pBuffer, uint8_t WriteAddr)
{
HAL_StatusTypeDef status = HAL_OK;

status = HAL_I2C_Mem_Write(&I2C_Handle, EEPROM_ADDRESS, (uint16_t)WriteAddr, I2C_MEMADD_SIZE_8BIT, pBuffer, 1, 100);

/* Check the communication status */
if(status != HAL_OK)
{
/* Execute user timeout callback */
//I2Cx_Error(Addr);
}
while (HAL_I2C_GetState(&I2C_Handle) != HAL_I2C_STATE_READY);

/* Check if the EEPROM is ready for a new operation */
while (HAL_I2C_IsDeviceReady(&I2C_Handle, EEPROM_ADDRESS, EEPROM_MAX_TRIALS, I2Cx_TIMEOUT_MAX) == HAL_TIMEOUT);

/* Wait for the end of the transfer */
while (HAL_I2C_GetState(&I2C_Handle) != HAL_I2C_STATE_READY);
return status;
}

这里只是简单调用库函数 HAL_I2C_Mem_Write 就可以实现,通过封装一次使用更方。在这个通讯过程中, STM32 实际上通过 I2C 向 EEPROM 发送了两个数据,但为何第一个数据被解释为 EEPROM 的内存地址?这是由 EEPROM 的自己定义的单字节写入时序,见下图:

image-20230507140607977

EEPROM 的单字节时序规定,向它写入数据的时候,第一个字节为内存地址,第二个字节是要写入的数据内容。所以我们需要理解:命令、地址的本质都是数据,对数据的解释不同,它就有了不同的功能。

2.4 按页写入数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
uint32_t I2C_EE_PageWrite(uint8_t* pBuffer, uint8_t WriteAddr, uint8_t NumByteToWrite)
{
HAL_StatusTypeDef status = HAL_OK;
/* Write EEPROM_PAGESIZE */
status=HAL_I2C_Mem_Write(&I2C_Handle, EEPROM_ADDRESS,WriteAddr, I2C_MEMADD_SIZE_8BIT, (uint8_t*)(pBuffer),NumByteToWrite, 100);

while (HAL_I2C_GetState(&I2C_Handle) != HAL_I2C_STATE_READY);

/* Check if the EEPROM is ready for a new operation */
while (HAL_I2C_IsDeviceReady(&I2C_Handle, EEPROM_ADDRESS, EEPROM_MAX_TRIALS, I2Cx_TIMEOUT_MAX) == HAL_TIMEOUT);

/* Wait for the end of the transfer */
while (HAL_I2C_GetState(&I2C_Handle) != HAL_I2C_STATE_READY);
return status;
}

在以上的数据通讯中,每写入一个数据都需要向 EEPROM 发送写入的地址,我们希望向连续地址写入多个数据的时候,只要告诉 EEPROM 第一个内存地址 address1,后面的数据按次序写入到address2、 address3…这样可以节省通讯的内容,加快速度。为应对这种需求, EEPROM 定义了一种页写入时序 :

image-20230507140815497

根据页写入时序,第一个数据被解释为要写入的内存地址 address1,后续可连续发送 n 个数据,这些数据会依次写入到内存中。这段页写入函数主体跟单字节写入函数是一样的,只是它在发送数据的时候,使用 while 循环控制发送多个数据,发送完多个数据后才产生 I2C 停止信号,只要每次传输的数据小于等于 EEPROM时序规定的页大小,就能正常传输。

2.5 多字节数据写入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
void I2C_EE_BufferWrite(uint8_t* pBuffer, uint8_t WriteAddr, uint16_t NumByteToWrite)
{
uint8_t NumOfPage = 0, NumOfSingle = 0, Addr = 0, count = 0;

Addr = WriteAddr % EEPROM_PAGESIZE;
count = EEPROM_PAGESIZE - Addr;
NumOfPage = NumByteToWrite / EEPROM_PAGESIZE;
NumOfSingle = NumByteToWrite % EEPROM_PAGESIZE;

/* If WriteAddr is I2C_PageSize aligned */
if(Addr == 0)
{
/* If NumByteToWrite < I2C_PageSize */
if(NumOfPage == 0)
{
I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);
}
/* If NumByteToWrite > I2C_PageSize */
else
{
while(NumOfPage--)
{
I2C_EE_PageWrite(pBuffer, WriteAddr, EEPROM_PAGESIZE);
WriteAddr += EEPROM_PAGESIZE;
pBuffer += EEPROM_PAGESIZE;
}

if(NumOfSingle!=0)
{
I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);
}
}
}
/* If WriteAddr is not I2C_PageSize aligned */
else
{
/* If NumByteToWrite < I2C_PageSize */
if(NumOfPage== 0)
{
I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);
}
/* If NumByteToWrite > I2C_PageSize */
else
{
NumByteToWrite -= count;
NumOfPage = NumByteToWrite / EEPROM_PAGESIZE;
NumOfSingle = NumByteToWrite % EEPROM_PAGESIZE;

if(count != 0)
{
I2C_EE_PageWrite(pBuffer, WriteAddr, count);
WriteAddr += count;
pBuffer += count;
}

while(NumOfPage--)
{
I2C_EE_PageWrite(pBuffer, WriteAddr, EEPROM_PAGESIZE);
WriteAddr += EEPROM_PAGESIZE;
pBuffer += EEPROM_PAGESIZE;
}
if(NumOfSingle != 0)
{
I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);
}
}
}
}

多次写入数据时,利用 EEPROM 的页写入方式,避免单字节读写时候的等待。 其实它的主旨就是对输入的数据进行分页 (本型号芯片每页 8 个位)。通过“整除”计算要写入的数据 NumByteToWrite 能写满多少“完整的页”,计算得的值存储在 NumOfPage 中,但有时数据不是刚好能写满完整页的,会多一点出来,通过“求余”计算得出“不满一页的数据个数”就存储在 NumOfSingle 中。计算后通过按页传输 NumOfPage 次整页数据及最后的 NumOfSing 个数据,使用页传输,比之前的单个字节数据传输要快很多。

除了基本的分页传输,还要考虑首地址的问题。若首地址不是刚好对齐到页的首地址,会需要一个 count 值,用于存储从该首地址开始写满该地址所在的页,还能写多少个数据。实际传输时,先把这部分 count 个数据先写入,填满该页,然后把剩余的数据 (NumByteToWrite-count),再重复上述求出 NumOPage 及 NumOfSingle 的过程,按页传输到 EEPROM。

(1)若 writeAddress=16,计算得 Addr=16%8= 0 , count=8-0= 8;

(2)同时,若 NumOfPage=22,计算得 NumOfPage=22/8= 2, NumOfSingle=22%8= 6。

(3)数据传输情况如下表

不影响 0 1 2 3 4 5 6 7
不影响 8 9 10 11 12 13 14 15
第 1 页 16 17 18 19 20 21 22 23
第 2 页 24 25 26 27 28 29 30 31
NumOfSingle=6 32 33 34 35 36 37 38 39

(4)若 writeAddress=17,计算得 Addr=17%8= 1, count=8-1= 7;

(5)同时,若 NumOfPage=22,

(6)先把 count 去掉,特殊处理,计算得新的 NumOfPage=22-7= 15

(7)计算得 NumOfPage=15/8= 1, NumOfSingle=15%8= 7。

(8)数据传输情况如下表:

不影响 0 1 2 3 4 5 6 7
不影响 8 9 10 11 12 13 14 15
count=7 16 17 18 19 20 21 22 23
第 1 页 24 25 26 27 28 29 30 31
NumOfSingle=7 32 33 34 35 36 37 38 39

EEPROM 支持的页写入只是一种加速的 I2C 的传输时序,实际上并不要求每次都以页为单位进行读写, EEPROM 是支持随机访问的 (直接读写任意一个地址),如前面的单个字节写入。在某些存储器,如 NAND FLASH,它是必须按照 Block 写入的,例如每个 Block 为 512或 4096 字节,数据写入的最小单位是 Block,写入前都需要擦除整个 Block; NOR FLASH 则是写入前必须以 Sector/Block 为单位擦除,然后才可以按字节写入。而我们的 EEPROM 数据写入和擦除的最小单位是“字节”而不是“页”,数据写入前不需要擦除整页。

2.6 读取数据

1
2
3
4
5
6
7
8
uint32_t I2C_EE_BufferRead(uint8_t* pBuffer, uint8_t ReadAddr, uint16_t NumByteToRead)
{
HAL_StatusTypeDef status = HAL_OK;

status=HAL_I2C_Mem_Read(&I2C_Handle,EEPROM_ADDRESS,ReadAddr, I2C_MEMADD_SIZE_8BIT, (uint8_t *)pBuffer, NumByteToRead,1000);

return status;
}

我们只需要确定 I2C 的地址,数据格式,数据存储指针,数据大小,超时设置就可以把想要的数据读回来。

2.7 测试函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#define  DATA_Size			256
#define EEP_Firstpage 0x00
uint8_t I2c_Buf_Write[DATA_Size];
uint8_t I2c_Buf_Read[DATA_Size];

uint8_t I2C_Test(void)
{
uint16_t i;

printf("写入的数据\r\n");

for ( i=0; i<DATA_Size; i++ ) //填充缓冲
{
I2c_Buf_Write[i] =i;
printf("0x%02X ", I2c_Buf_Write[i]);
if(i%16 == 15)
printf("\r\n");
}

//将I2c_Buf_Write中顺序递增的数据写入EERPOM中
I2C_EE_BufferWrite( I2c_Buf_Write, EEP_Firstpage, DATA_Size);

printf("读出的数据\r\n");
//将EEPROM读出数据顺序保持到I2c_Buf_Read中
I2C_EE_BufferRead(I2c_Buf_Read, EEP_Firstpage, DATA_Size);
//将I2c_Buf_Read中的数据通过串口打印
for (i=0; i<DATA_Size; i++)
{
if(I2c_Buf_Read[i] != I2c_Buf_Write[i])
{
printf("0x%02X ", I2c_Buf_Read[i]);
printf("错误:I2C EEPROM写入与读出的数据不一致\r\n");
return 0;
}
printf("0x%02X ", I2c_Buf_Read[i]);
if(i%16 == 15)
printf("\r\n");

}
printf("I2C(AT24C02)读写测试成功\r\n");
return 1;
}

3. 模拟I2C读写AT24C02

我们还可以通过模拟I2C的方式来读写,模拟方式可以看另一篇专门写通信的笔记,这里就不多写了。