SAU-G0 EEPROM笔记

前言

为什么选择EEPROM?EEPROM与Flash有何不同?

既然STM32有那么大的Flash,为什么还要使用EEPROM?好问题,这得从几个方面来回答

SAU-G0 是我学习STM32低功耗时做的传感器采集单元(就是个屑温湿度计罢了),使用STM32G031G8U6(之后简称G031)作为主控芯片,根据ST G0的中文手册,其Flash结构如下:

其中G031存储器的64KB分为32页,每页2KB,最小擦除单位为1页

根据Flash的擦写特性,如果有一个1byte的数据变化需求,那它存储所在的页(或者扇区、或者最小擦除粒度)需要先整块擦除再写入,不能只修改该页的1个byte同时不影响该页原有的其他数据,那么原有的其他数据如果要保留怎么办,有两种方式:

  1. 找一个空的页,将原有的有用数据的页整个复制过去,将原来的页擦除,将1byte的数据变化+复制过去的页复制到原来的flash,最后临时使用的这个空的页擦除。这个方法省MCU的内存,不需要在SRAM中存Flash页里的所有相关数据作为复制操作的缓冲区

  2. 在SRAM中总是存在一个flash修改页数据的全局变量,当该页发生一个1byte的数据变化需求,将该页的数据整个拷贝到SRAM中,擦除该页,再将1byte的数据变化+SRAM中的flash数据副本复制到原来的flash,这个方法耗费MCU的内存,需要在SRAM中存待修改的Flash页的所有相关数据,当页大小比较大时,比如STM32F4 Flash分布和G031不一样,在STM32F4的编程手册上可找到FLASH是按照Sector划分的:F4系列最小擦除粒度为扇区(最小的16K,但有16K、64K、128K三种大小),那么当你修改的扇区中的有用数据有比如14K时,正好F4的SRAM剩余空间不够14KB,那么使用方式1,留一个16KB的扇区做复制备份,才是行得通的方法

F4_FLASH的Sector划分

可见,如果需要频繁地修改非易失性存储器的数据,而且单次改动的数据量又很小,比如只占最小擦除粒度的 1/100,如果使用方式1,得磨损2次,如果使用方式2磨损1次,而且,STM32的Flash是有擦写寿命的,例如G031最坏情况只有1万次:

6B509ED7C29973CE1FBBE74496ABB86B

对于SAU-G0,若使用flash的一个2KB页每小时存一次采集数据,每天改动24次flash数据,那么416天后,G031的该页将面临报废(实际上2KB存不下这些采集数据hhh)

可能又有疑问,计算机的固态硬盘不也是Flash吗,每天面临比stm32还要多得多的擦写需求,那为啥没有那么容易报废?

好问题,因为固态硬盘除了存储介质是NAND Flash,每一块盘都带有闪存控制器,闪存翻译层的平均磨损逻辑,可以将擦除平均分布在所有的块上,最大化每个块的寿命,对于坏块自动屏蔽处理,从预留OP中开辟新的块来代替,而STM32是NOR Flash,没有(这么nb的)闪存主控,无法整这些高级骚操作,只能按照产品规格书的Flash最坏擦除寿命进行保守的应用

EEPROM最小擦写粒度是1byte,可以按1byte读写,也可以多个byte读写,不需要地址对齐,擦写寿命100万次,相比G031的 Flash,对于频繁的小容量数据存取EEPROM再合适不过

EEPROM参数表格(AT24Cxx系列)

所有的这些型号都支持字节读写,即每个byte都有独立的地址,若地址指针寻址位数不够,使用I2C地址的P位来倍增地址指针寻址数量,例如1片24C16,I2C地址就占用0x50~0x57(nnd居然跟我用的PCF2129的0x51地址冲突了一个),还要注意发送的地址指针地址与同一I2C总线上的设备是否冲突,例如:X1226 和 AT24C16 地址冲突问题

EEPROM型号 容量 页大小 页数 发送地址位数 I2C地址结构 A(n)位数 P(n)位数 可寻址数 一条I2C总线最多
挂载同型号数量
AT24C01 128 x 8 (1K) 8-byte page 16 8bit AT24C01-02-32-64-128-256-512_Dev_Addr 3 0 128 8
AT24C02 256 x 8 (2K) 8-byte page 32 8bit AT24C01-02-32-64-128-256-512_Dev_Addr 3 0 256 8
AT24C04 512 x 8 (4K) 16-byte page 32 8bit AT24C04-1024_Dev_Addr 2 1 256 x 21 4
AT24C08 1,024 x 8 (8K) 16-byte page 64 8bit AT24C08_Dev_Addr 1 2 256 x 22 2
AT24C16 2,048 x 8 (16K) 16-byte page 128 8bit AT24C16_Dev_Addr 0 3 256 x 23 1
AT24C32 4,096 x 8 (32K) 32-byte page 128 16bit AT24C01-02-32-64-128-256-512_Dev_Addr 3 0 4096 8
AT24C64 8,192 x 8 (64K) 32-byte page 256 16bit AT24C01-02-32-64-128-256-512_Dev_Addr 3 0 8192 8
AT24C128 16,384 x 8 (128K) 64-byte page 256 16bit AT24C01-02-32-64-128-256-512_Dev_Addr 3 0 16384 8
AT24C256 32,768 x 8 (256K) 64-byte page 512 16bit AT24C01-02-32-64-128-256-512_Dev_Addr 3 0 32768 8
AT24C512 65,536 x 8 (512K) 128-byte page 512 16bit AT24C01-02-32-64-128-256-512_Dev_Addr 3 0 65536 8
AT24C1024 131,072 x 8 (1024K) 256-byte page 512 16bit AT24C04-1024_Dev_Addr 2 1 65536 x 21 4

P位的有无比较特殊:

  • AT24C01 - AT24C02:没有P位

  • AT24C08 - AT24C16:有P位

  • AT24C32 - AT24C512:没有P位

  • AT24C1024:有1位P位,与AT24C04的I2C地址结构一样

ee24(开源EEPROM驱动库 STM32 HAL)

支持I2C EEPROM:24C01/02/04/08/16...到512

  • 不支持AT24C1024,因为AT24C1024地址有P0位,实际是17位寻址空间,而该库读写函数的第一个参数是uint16_t address,无法在不溢出的情况下接受17bit地址参数,无法检测bit[16]位而改变P0位,除非升级类型为uint32_t

  • 后续文章EEPROM有关的代码大都是以此库的进行分析

  • SAU-G0 使用的 ee24 库是我魔改过的,链接

EEPROM擦除

一般擦除法

ee24库在erasefull函数内创建了一个:256byte大小全为0xff的常量,多次写这个数据以达到擦除整片EEPROM

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
bool ee24_eraseChip(void)
{
//创建擦除数据,元素值全为11111111,总大小等于24C02 的 256Byte
const uint8_t eraseData[32] = {
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
};
uint32_t bytes = 0;
//反复写它以达到全片擦除,写N次32组0xff
while ( bytes < (_EEPROM_SIZE_KBIT * 256))
{
if (ee24_write(bytes, (uint8_t*)eraseData, sizeof(eraseData), 100) == false)
return false;
bytes += sizeof(eraseData);
}
return true;
}

特殊擦除法

EEPROM写入数据之前需要先进行擦除操作嘛(例如25LC512)?

对于Flash,在写入之前必须先擦除(也就是写入0xFF,因为Flash只能从1修改为0),对于EEPROM,写之前不需要擦除的,可以直接写入数据,支持按字节或页写

Microchip - 25LC512 比较特殊,是SPI EEPROM,支持擦除指令,这里的擦除并非类似于Flash的写操作,手册(2.8 PAGE ERASE、2.9 SECTOR ERASE、2.10 CHIP ERASE)

EEPROM页大小

比如 ATMEL - AT24C01A/02/04/08/16Kb ,不同容量的页大小不同

8-byte Page (1K, 2K), 16-byte Page (4K, 8K, 16K) Write Modes

也有另类,比如 安森美 - CAT24C01 - 2/4/8/16 Kb中的2Kbit款也是16-byte-Page,而上面的AT24C01和AT24C02是16-byte-Page

The CAT24C02/04/08/16 are 2−Kb, 4−Kb, 8−Kb and 16−Kb respectively I2C Serial EEPROM devices organized internally as 16/32/64 and 128 pages respectively of 16 bytes each

ee24库根据宏定义的EEPROM容量,自动选择页大小,与ATMEL - AT24Cxx的变化一致

1
2
3
4
5
6
7
8
9
10
11
//ee24Config.h
#define _EEPROM_SIZE_KBIT 2 //eeprom容量

//ee24.c
#if (_EEPROM_SIZE_KBIT == 1) || (_EEPROM_SIZE_KBIT == 2)
#define _EEPROM_PSIZE 8 //eeprom page大小
#elif (_EEPROM_SIZE_KBIT == 4) || (_EEPROM_SIZE_KBIT == 8) || (_EEPROM_SIZE_KBIT == 16)
#define _EEPROM_PSIZE 16
#else
#define _EEPROM_PSIZE 32
#endif

但对于 安森美 - CAT24C02 是16-byte-Page特殊情况,并不是说选 _EEPROM_SIZE_KBIT 为 2而配置 _EEPROM_PSIZE 为 8 ,8-byte Page Write Mode 就不适用了,而是通信次数会多一倍,以等价于16-byte-Page的写入数据量,宏这里这么搞是因为越大容量的EEPROM在一个读写时支持的字节数更多,可以减少通信初始化I2C的次数,那么如果对 24C02以 _EEPROM_PSIZE 为 32 读写会发生什么?根据手册PAGE WRITE部分的节选,数据地址会回到开头覆盖原来的数据:

"When the word address, internally generated, reaches the page boundary, the following byte is placed at the beginning of the same page. If more than eight (1K/2K) or sixteen (4K, 8K, 16K) data words are transmitted to the EEPROM, the data word address will “roll over” and previous data will be overwritten."

当内部生成的字地址到达页边界时,下一个字节被放置在同一页的开头。如果超过八个 (1K/2K) 或十六个 (4K, 8K, 16K) 数据字被传输到 EEPROM,数据字地址将“翻转”并且之前的数据将被覆盖。 这个是页覆盖特性

EEPROM寻址

以下是安森美 - CAT24C的页写模式序列图,每次I2C地址应答后,下一个从Master发来的8bit数据改变地址指针的位置

EEPROM的地址指针从0x00开始寻址, 容量 2/4/8/16Kbit 的EEPROM的Address Byte只需要 1Byte,而I2C EEPROM最大有1024Kbit,8bit寻址能寻多大容量的EEPROM?比如FMD(辉芒微) FT24C16A-ELR-T,可以简单计算一下,容量16Kbit就是2048Byte,每页16byte,需要128个地址,也就是7bit寻址,那么ATMEL - AT24C512T呢?以下是手册节选:有512页,就是9bit寻址

The 512K is internally organized as 512 pages of 128-bytes each. Random word addressing requires a 16-bit data word address.

它的页写模式序列图如下,可见每次I2C地址应答后,后面两个从Master发来的8bit数据 改变 16bit地址 指针的位置

回来看看ee24库,读写函数第一个参数是EPPROM内部寻址地址,接受16bit参数:

1
2
bool ee24_write(uint16_t address, uint8_t *data, size_t len, uint32_t timeout);
bool ee24_read(uint16_t address, uint8_t *data, size_t len, uint32_t timeout);

读写函数体内部是多次while的形式,C预处理器根据EEPROM的页大小,计算出每次while的地址指针增量,即传入HAL_I2C_Mem_Read的MemAddSize参数,非常优雅,比如ee24_write:

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
bool ee24_write(uint16_t address, uint8_t *data, size_t len, uint32_t timeout){
......
while (1)
{
w = _EEPROM_PSIZE - (address % _EEPROM_PSIZE);
if (w > len)
w = len;
#if ((_EEPROM_SIZE_KBIT==1) || (_EEPROM_SIZE_KBIT==2))
if (HAL_I2C_Mem_Write(&_EEPROM_I2C, _EEPROM_ADDRESS, address, I2C_MEMADD_SIZE_8BIT, data, w, 100) == HAL_OK)
#elif (_EEPROM_SIZE_KBIT==4)
if (HAL_I2C_Mem_Write(&_EEPROM_I2C, _EEPROM_ADDRESS | ((address & 0x0100) >> 7), (address & 0xff), I2C_MEMADD_SIZE_8BIT, data, w, 100) == HAL_OK)
#elif (_EEPROM_SIZE_KBIT==8)
if (HAL_I2C_Mem_Write(&_EEPROM_I2C, _EEPROM_ADDRESS | ((address & 0x0300) >> 7), (address & 0xff), I2C_MEMADD_SIZE_8BIT, data, w, 100) == HAL_OK)
#elif (_EEPROM_SIZE_KBIT==16)
if (HAL_I2C_Mem_Write(&_EEPROM_I2C, _EEPROM_ADDRESS | ((address & 0x0700) >> 7), (address & 0xff), I2C_MEMADD_SIZE_8BIT, data, w, 100) == HAL_OK)
#else
if (HAL_I2C_Mem_Write(&_EEPROM_I2C, _EEPROM_ADDRESS, address, I2C_MEMADD_SIZE_16BIT, data, w, 100) == HAL_OK)
#endif
{
ee24_delay(10);
len -= w;
data += w;
address += w;
if (len == 0)
}
......
}
......
}

分析为AT24C02的情况:

EEPROM型号 容量 页大小 页数 发送地址位数 I2C地址结构 A(n)位数 P(n)位数 可寻址数 一条I2C总线最多
挂载同型号数量
AT24C02 256 x 8 (2K) 8-byte page 32 8bit AT24C01-02-32-64-128-256-512_Dev_Addr 3 0 256 8
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
while (1)
{
w = _EEPROM_PSIZE - (address % _EEPROM_PSIZE);
if (w > len)
w = len;
#if ((_EEPROM_SIZE_KBIT==1) || (_EEPROM_SIZE_KBIT==2))
if (HAL_I2C_Mem_Write(&_EEPROM_I2C, _EEPROM_ADDRESS, address, I2C_MEMADD_SIZE_8BIT, data, w, 100) == HAL_OK)
......
{
ee24_delay(10);
len -= w;
data += w;
address += w;
if (len == 0){
ee24_lock = 0;
return true;
}
......
}
......
}

//相关宏和函数位置:
//stm32xxxx_hal_i2c.h
#define I2C_MEMADD_SIZE_8BIT (0x00000001U)
//stm32xxxx_hal_i2c.c
HAL_StatusTypeDef HAL_I2C_Mem_Write(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress,
uint16_t MemAddSize, uint8_t *pData, uint16_t Size, uint32_t Timeout);

根据AT24Cxx系列参数表格,AT24C02为 8-byte-page,发送地址为8bit,片内子地址寻址(地址指针)可对内部 256 Bytes中的任一个进行读/写操作,其寻址范围为00~FF,共256个寻址单位

AT24C02、04、08、16 操作说明

1010是固定的,A表示器件地址,可以拉高和拉低,I2C总线上可以并接2的n次方个器件。P表示具体的内部地址数,比如at24c02共有256个字节,第二个地址字节完全可以满足,不用P。但是at24c04一个有512个字节,需要9位地址线,第一个字节中的p就表示地址线了,p=0表示低256字节,1表示高256字节。

那么HAL_I2C_Mem_Read的MemAddSize参数传入I2C_MEMADD_SIZE_8BIT没有问题,那么参数_EEPROM_SIZE_KBIT = 2_EEPROM_PSIZE = 8 (8-byte-page),模拟一下写入过程,比如从EEPROM第一个页开始写,即address = 0,需要写入1byte的数据长度 len = 13,由array[13]存储

循环次数 w address data len
1 8 0 &array[0] 13
2 5 8 &array[8] 5

分析为AT24C16的情况:

EEPROM型号 容量 页大小 页数 发送地址位数 I2C地址结构 A(n)位数 P(n)位数 可寻址数 一条I2C总线最多
挂载同型号数量
AT24C16 2,048 x 8 (16K) 16-byte page 128 8bit AT24C16_Dev_Addr 0 3 256 x 23 1

根据AT24Cxx系列参数表格,AT24C16为 16-byte-page,发送地址为8bit,那么HAL_I2C_Mem_Read的MemAddSize参数传入I2C_MEMADD_SIZE_8BIT没有问题,那么参数_EEPROM_SIZE_KBIT = 2_EEPROM_PSIZE = 16 (16-byte-page)

注意24C16的I2C地址bit[1:3]用作P位,也就是一片24C16占用 8个7bit地址: 1010,000 - 1010,111,即0x50 - 0x57,bit[0]用作读写(R/W)标识

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
bool ee24_write(uint16_t address, uint8_t *data, size_t len, uint32_t timeout) {
......
while (1)
{
w = _EEPROM_PSIZE - (address % _EEPROM_PSIZE);
if (w > len)
w = len;
......
#elif (_EEPROM_SIZE_KBIT==16)
if (HAL_I2C_Mem_Write(&_EEPROM_I2C, _EEPROM_ADDRESS | ((address & 0x0700) >> 7), (address & 0xff), I2C_MEMADD_SIZE_8BIT, data, w, 100) == HAL_OK)
#else
......
{
ee24_delay(10);
len -= w;
data += w;
address += w;
if (len == 0){
ee24_lock = 0;
return true;
}
......
}
......
}
}
//相关宏和函数位置:
//stm32xxxx_hal_i2c.h
#define I2C_MEMADD_SIZE_8BIT (0x00000001U)
//stm32xxxx_hal_i2c.c
HAL_StatusTypeDef HAL_I2C_Mem_Write(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress,
uint16_t MemAddSize, uint8_t *pData, uint16_t Size, uint32_t Timeout);

模拟一下写入过程:

  • 24C16 i2C 7bit地址有8个,但当所有P(n)位都为0时,8bit地址就是 _EEPROM_ADDRESS = 1010,0000

  • 假设:地址指针从EEPROM内部的0x17开始写,即二级制address = 0001,0111

  • 假设:需要写入1byte的数据长度 len = 43

  • 代码段中的常量:

    _EEPROM_PSIZE = 16

    0x0700二进制为0000,0111,0000,0000

手算太麻烦了,使用等效的测试代码打断点记录数据:

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
bool ee24_write_test_A()
{
uint16_t address = 0x17;
size_t len = 43;
uint8_t array[43] = {0};
uint8_t data = array;
int i = 0;
uint16_t w;
while (1)
{
i;
w = 16 - (address % 16);
if (w > len)
w = len;

{
//等效HAL_I2C_Mem_Write()的传入参数
uint8_t DevAddress = _EEPROM_ADDRESS | ((address & 0x0700) >> 7);
uint16_t MemAddress = (address & 0xff);
data;
w;
}
{
/*断点位置*/ ee24_delay(10);
len -= w;
data += w;
address += w;
if (len == 0)
{

return true;
}
}
++i;
}
}

每次循环的记录当运行传到“断点" “ HAL_I2C_Mem_Write() ”时,各个相关值:

第 1 次 第 2 次 第 3 次 第 4 次
address (BIN) (DEC) 0000,0000,0001,0111 (23) 0000,0000,0010,0000 (32) 0000,0000,0011,0000 (48) 0000,0000,0100,0000 (64)
w 或 size (BIN) (DEC) 0000,1001 (9) 0001,0000 (16) 0001,0000 (16) 0000,0010 (2)
DevAddress (BIN) 1010,0000 1010,0000 1010,0000 1010,0000
MemAddress (BIN) 0000,0000,0001,0111 0000,0000,0010,0000 0000,0000,0011,0000 0000,0000,0100,0000
data 或 pData &array[0] &array[9] &array[25] &array[41]
len 43 34 18 2

24C16每页16yte,直觉感觉写入1byte的数据长度 len = 43只需要3次循环(16x3 = 48)就好了,但实际循环了4次!这与假设的address初始值为0x17有关!

该段数据在EEPROM的分布:EEPROM的字节寻址特性,不必像Flash一样地址对齐就能读写1byte或连续读写

IMG_0895

以上情况24C16的 DevAddressaddress的高位没有发生变化,如果address变为0x1fa,I2C地址的P0和P1、address的高位发生变化:

第 1 次 第 2 次 第 3 次 第 4 次
address(BIN) (DEC) 0000,0000,1111,1010 (506) 0000,0010,0000,0000 (512) 0000,0010,0001,0000 (528) 0000,0010,0010,0000 (544)
w 或 size (BIN) (DEC) 0000,0110 (6) 0001,0000 (16) 0001,0000 (16) 0000,0101 (5)
DevAddress (BIN) 1010,0010 1010,0100 1010,0100 1010,0100
MemAddress (BIN) 0000,0000,1111,1010 0000,0000,0000,0000 0000,0000,0001,0000 0000,0000,0010,0000
data 或 pData &array[0] &array[6] &array[22] &array[38]
len 43 37 21 5

修改ee24库以支持24C1024

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//address升级为uint16_t, 添加#elif (_EEPROM_SIZE_KBIT == 1024)部分,函数其余部分不变
bool ee24_write(uint32_t address, uint8_t *data, size_t len, uint32_t timeout) {
......
#elif (_EEPROM_SIZE_KBIT == 1024)
if (HAL_I2C_Mem_Write(&_EEPROM_I2C, _EEPROM_ADDRESS | ((address & 0x10000) >> 15), address, I2C_MEMADD_SIZE_16BIT, data, w, 100) == HAL_OK)
#else
......
}

//address升级为uint16_t, 添加#elif (_EEPROM_SIZE_KBIT == 1024)部分,函数其余部分不变
bool ee24_read(uint32_t address, uint8_t *data, size_t len, uint32_t timeout) {
......
#elif (_EEPROM_SIZE_KBIT==1024)// AT24C1024又有P位了
if (HAL_I2C_Mem_Read(&_EEPROM_I2C, _EEPROM_ADDRESS | ((address & 0x10000) >> 15), address, I2C_MEMADD_SIZE_16BIT, data, len, 100) == HAL_OK)
#else
......
}

模拟一下写入43个1byte数据到AT24C1024,address0xfff0

第 1 次 第 2 次 第 3 次
address(BIN) (DEC) 0,1111,1111,1111,0000 (65520) 1,0000,0000,0000,0000 (65536) 1,0000,0000,0001,0000 (65552)
w 或 size (BIN) (DEC) 0001,0000 (16) 0001,0000 (16) 0000,1011 (11)
DevAddress (BIN) 1010,0000 1010,0010 1010,0010
MemAddress (BIN) 0000,0000,1111,0000 0000,0000,0000,0000 0000,0000,0001,0000
data 或 pData &array[0] &array[16] &array[32]
len 43 27 11

OK大功告成!

EEPROM检测大小

检测总大小

利用地址指针溢出后回到第一页的特性

给你一片不知道多大的EEPROM(比如丝印没了),如何快速得知它的容量?

I2C_EEPROM/I2C_eeprom.cppI2C_eeprom::determineSize()分析

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
69
70
// returns size in bytes
// returns 0 if not connected
//
// tested for
// 2 byte address
// 24LC512 64 KB YES
// 24LC256 32 KB YES
// 24LC128 16 KB YES
// 24LC64 8 KB YES
// 24LC32 4 KB YES* - no hardware test, address scheme identical to 24LC64.
//
// 1 byte address (uses part of deviceAddress byte)
// 24LC16 2 KB YES
// 24LC08 1 KB YES
// 24LC04 512 B YES
// 24LC02 256 B YES
// 24LC01 128 B YES
uint32_t I2C_eeprom::determineSize(const bool debug)
{
// try to read a byte to see if connected
if (! isConnected()) return 0;

uint8_t patAA = 0xAA; //1010,1010
uint8_t pat55 = 0x55; //0101,0101

/*
* 每次倍增size,注意size即作检测的EEPROM大小,也作为地址指针位置
* 对于24C01,size = 127是最末尾的byte,size = 128 会循环覆盖到 0
* 对于24C02,size = 255是最末尾的byte,size = 256 会循环覆盖到 0
* ......
*/
for (uint32_t size = 128; size <= 65536; size *= 2)
{
bool folded = false;

// store old values
//bool addressSize = _isAddressSizeTwoWords; //依赖构造函数传是否是2byte地址, 不依赖,可以不初始化该值
_isAddressSizeTwoWords = size > I2C_DEVICESIZE_24LC16; //如果size倍增迭代大于2048Kbit,那么_isAddressSizeTwoWords = true
uint8_t buf = readByte(size); //去读地址指针=size处地址的1byte值,临时存起来

// test folding 测试折叠
uint8_t cnt = 0; //统计页的大小,以byte为单位
writeByte(size, pat55); //对size地址写入 0101,0101
/*
* 如果首地址的值与写入的值相同,说明地址指针溢出后回到第一页地址,
* 第一页之前的数据将被覆盖,不过没关系,我们事先存了副本到buf中
*/
if (readByte(0) == pat55) cnt++;

writeByte(size, patAA); //对size地址继续写入 1010,1010,地址指针会自增吗?
/*
* 如果首地址的值与写入的值相同,说明页发生了折叠
*/
if (readByte(0) == patAA) cnt++;
folded = (cnt == 2); //如果cnt==2,那么发生了折叠
if (debug)
{
Serial.print(size, HEX);
Serial.print('\t');
Serial.println(readByte(size), HEX);
}

// restore old values //恢复原有的数据,也就是说这个测试不会破坏原有的数据,考虑很周到
writeByte(size, buf);
//_isAddressSizeTwoWords = addressSize;

if (folded) return size; //发生了折叠,返回size就是检测的EEPROM大小
}
return 0;
}

注意,抬头的注释:对于24C16及以下容量的型号,使用1byte address检测,24C32及以上容量的型号,使用 2byte address检测,地址长度的变化根据I2C_eeprom类的成员_isAddressSizeTwoWords 的值在I2C_eeprom::_beginTransmission()中更改,相关代码节选:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//  supports one and two bytes addresses
void I2C_eeprom::_beginTransmission(const uint16_t memoryAddress)
{
if (this->_isAddressSizeTwoWords)
{
_wire->beginTransmission(_deviceAddress);
// Address High Byte
_wire->write((memoryAddress >> 8)); //左移8bit,发送高位8bit
}
else
{
uint8_t addr = _deviceAddress | ((memoryAddress >> 8) & 0x07);
_wire->beginTransmission(addr);
}

// Address Low Byte (or single byte for chips 16K or smaller that have one-word addresses)
_wire->write((memoryAddress & 0xFF)); //将bit[15:8]置0, 发送低位8bit
}

_isAddressSizeTwoWords 默认未初始化,必须在I2C_eeprom类其中一个构造函数中传入deviceSize,才会初始化:

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
class I2C_eeprom {
......
private:
// 24LC32..24LC512 use two bytes for memory address
// 24LC01..24LC16 use one-byte addresses + part of device address
bool _isAddressSizeTwoWords;
......
}

I2C_eeprom::I2C_eeprom(const uint8_t deviceAddress, TwoWire *wire)
{
I2C_eeprom(deviceAddress, I2C_PAGESIZE_24LC256, wire);
}

//根据传入的deviceSize初始化_isAddressSizeTwoWords
I2C_eeprom::I2C_eeprom(const uint8_t deviceAddress, const uint32_t deviceSize, TwoWire *wire)
{
_deviceAddress = deviceAddress;
_deviceSize = deviceSize;
_pageSize = getPageSize(_deviceSize);
_wire = wire;

// Chips 16Kbit (2048 Bytes) or smaller only have one-word addresses.
this->_isAddressSizeTwoWords = deviceSize > I2C_DEVICESIZE_24LC16;
}

这个检测逻辑如果不给定deviceSize,就没法得知_isAddressSizeTwoWords,假真不给定deviceSize那如何检测容量?很简单,让uint32_t I2C_eeprom::determineSize()以1byte和2byte长度的地址各测试一遍,取计算结果偏大的,我分别进行了以下测试,可以看出该方法理论上可行:

被检测型号 address长度 检测的size 检测page
24C02 1byte 256 8
24C02 2byte 128 8
24C128 1byte 0 8
24C128 2byte 16384 64

对于24C1024,地址指针有1位在I2C P0位,实际是17bit寻址,所有该库uint16 address无法检测,需要升级类型到32bit,或者在I2C_eeprom::determineSize()检测到为65536时,更改I2C地址bit[1],再次检测,看还是不是65536

检测页大小

根据之前的总大小自动返回页大小就行,没有用到页覆盖特性(不搞这么麻烦,简单粗暴)

另外,24C1024的Page size是256byte,还可以加一行if

1
2
3
4
5
6
7
8
9
10
11

uint8_t I2C_eeprom::getPageSize(uint32_t deviceSize)
{
// determine page size from device size - based on Microchip 24LCXX data sheets.
if (deviceSize <= I2C_DEVICESIZE_24LC02) return 8;
if (deviceSize <= I2C_DEVICESIZE_24LC16) return 16;
if (deviceSize <= I2C_DEVICESIZE_24LC64) return 32;
if (deviceSize <= I2C_DEVICESIZE_24LC256) return 64;
// I2C_DEVICESIZE_24LC512
return 128;
}

修改arduino库的实现到EE24库

以下是检测页和Page大小的函数(我用 class 将 ee24 库套了一层)

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112

uint32_t EE24::determine_memsize()
{
// try to read a byte to see if connected
if (! isConnected()) return 0;

// 设为测试值
_sizePageByte = 8;
_addrBitAndDev = 0xff;
_addrBitRightOffSet = 0;
_addrBitAndMem = 0x1ffff;
_memAddSize = I2C_MEMADD_SIZE_8BIT;

uint8_t patAA = 0xAA; //1010,1010
uint8_t pat55 = 0x55; //0101,0101
/*
* 每次倍增size,注意size即作检测的EEPROM大小,也作为地址指针位置
* 对于24C01,size = 127是最末尾的byte,size = 128 会循环覆盖到 0
* 对于24C02,size = 255是最末尾的byte,size = 256 会循环覆盖到 0
* ......
*/
uint32_t size;
uint32_t sizePrev = 0;
uint32_t sizeLater = 0;
bool folded;

for(int j = 0; j < 2; j++){
if(j == 1)
_memAddSize = I2C_MEMADD_SIZE_16BIT;

for (size = 128; size <= 65536; size *= 2)
{
folded = false;
// uint8_t buf;
// readByte(size,&buf); //去读地址指针=size处地址的1byte值,临时存起来

// test folding 测试折叠
uint8_t cnt = 0; //统计页的大小,以byte为单位
writeByte(size, &pat55); //对size地址写入 0101,0101

uint8_t readBuffer;
readByte( 0, &readBuffer);

if (readBuffer == pat55) cnt++; //如果首地址的值与写入的值相同,说明地址指针溢出后回到第一页地址, 第一页之前的数据将被覆盖,不过没关系,我们事先存了副本到buf中
writeByte(size, &patAA); //对size地址继续写入 1010,1010,地址指针会自增吗?

readByte( 0, &readBuffer);
if (readBuffer == patAA) cnt++; //如果首地址的值与写入的值相同,说明页发生了折叠

folded = (cnt == 2); //如果cnt==2,那么发生了折叠

readByte(size, &readBuffer);
DBG_EE24("size = %d,read address size: %d ", size, readBuffer);
// writeByte(size, &buf); // restore old values //恢复原有的数据,也就是说这个测试不会破坏原有的数据,但对8bit地址指针使用16bit搞不一定
if (folded) {
return size;
if(j == 0) {
if(size > 65535) size = 0; //说明16bit地址指针溢出
sizePrev = size;
}else{ //j == 1
if(size > 65535) size = 0; //说明16bit地址指针溢出
sizeLater = size;
}
}
}
}

//如果至少一个size非零, 那肯定发生了折叠,可以确定地址值位数
if(sizePrev | sizeLater){
if(sizePrev < sizeLater) {
_memAddSize = I2C_MEMADD_SIZE_16BIT;
size = sizeLater;
}
else {
_memAddSize = I2C_MEMADD_SIZE_8BIT;
size = sizePrev;
}
return size / 128; //以Kbit单位返回,例如24C128返回128
}
return 0;
}

// tested for 2 byte address
// 24LC1024 128 KB 待测试
uint32_t EE24::determineMemSize()
{
uint32_t size = determine_memsize();
if(size == I2C_DEVICESIZE_24LC512){
uint8_t devAddrOld = _devAddress;
_devAddress = _devAddress | 0x2; //更改P0位以检测24C1024
if(determine_memsize() == I2C_DEVICESIZE_24LC512)
size = I2C_DEVICESIZE_24LC1024;
_devAddress = devAddrOld; //还原地址
}
_sizeMemKbit = size;
return size;
}

uint16_t EE24::determinePageSize()
{
uint16_t size;
// determine page size from device size - based on Microchip 24LCXX data sheets.
if (_sizeMemKbit <= I2C_DEVICESIZE_24LC02) size = 8;
else if (_sizeMemKbit <= I2C_DEVICESIZE_24LC16) size = 16;
else if (_sizeMemKbit <= I2C_DEVICESIZE_24LC64) size = 32;
else if (_sizeMemKbit <= I2C_DEVICESIZE_24LC256) size = 64;
else if (_sizeMemKbit <= I2C_DEVICESIZE_24LC512) size = 128;
// I2C_DEVICESIZE_24LC1024
else size = 256;
_sizePageByte = size;
return size;
}

EEPPROM存储数据

将结构体存到EEPROM

union结合struct的技巧

EEPROM不论是1byte、多个byte、页的读写,每一次目标地址的迭代单位都是1byte,也就是说对于2byte及以上的单个类型(例如 uint16_t、int16_t、float、long、double等)不能直接用1byte迭代的读写函数,需要将这种类型拆分成多个1byte,存进去。取出来,也要再经历组合操作,转换成原来的类型,标准方法是使用 union:

联合体保存float到eeprom的方法

联合体看似与结构体相似,但有不同之处,结构体中每个变量占用不同的内存,而联合体共用一段内存

联合体同一时间只用到一个成员

结构体变量所占内存长度是其中最大字段大小的整数倍

共用体变量所占的内存长度等于最长的成员变量的长度

在EEPROM中保存浮点数的方法_mrwangwang的博客

Arduino中利用EEPROM存储double和float类型的数据(使用共用体)

union_Microsoft_Docs

🐂🍺结构体内嵌联合体 在数据组包和解包的应用、Ti单片寄存器封装:union内嵌位域结构体、管理状态变量

amobbs:如何方便的读写结构体到EEPROM中?

1
2
3
4
5
6
7
8
9
10
11
12
13
struct sensor_rom_t
{
unsigned int addr;
unsigned char altimes;
unsigned char exist;//设置的状态
unsigned char ctr;
unsigned char relay;
};
union cf_page
{
struct sensor_rom_t s[ALMMAX];//ALMMAX是存成结构体数组 //获取结构体值时,使用s
unsigned char str[sizeof(struct sensor_rom_t) * ALMMAX]; //读写EEPROM时,使用str
} cf;

把 union 的 str 成员用 eeprom_write 写到eeprom里就可以了

1
eeprom_write((uint8_t *)&structx, sizeof(structx));

不过有个问题,,结构体有不同类型的成员,编译器可能插入填充字节进行字节对齐,结构体实际大小可能大于其成员大小的总和,这种情况的结构体如果是union 的成员,那么用union的另一个成员保存到EEPROM,会造成EEPROM空间浪费?先Mark一下,以后再想想

SAU-G0的系统设置的存储结构

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
//路径 Settings.h
struct orgData{
//版本信息
uint16_t FWversion;
//数据采集
uint32_t TimeRUN; // 累计运行时间--RUN
uint32_t TimeLPW_RUN; // 累计运行时间--LPW_RUN
uint32_t TImeSTOP1; // 累计运行时间--STOP1
uint16_t NumOfDataCollected; // 已采集的数据组个数(也用于下次写EEPROM地址的指针偏移)
uint16_t NumOfDataWillCollect; // 将采集的数据组个数
//任务开始日期
uint16_t STyy;
uint16_t STMM;
uint16_t STdd;
uint16_t SThh;
uint16_t STmm;
uint16_t STss;
//任务采集周期(+ 任务开始日期,可以配合RTClib的opertor算出结束日期)
uint16_t Thh; //>24小时后, 换算为天
uint16_t Tmm;
uint16_t Tss;
//每个周期采集样本数(给滤波器的处理为一组数据,不会存未经滤波的多个数据组)
uint16_t TSamples; //暂时不支持单独设置某一对象的样本数
//这个其实当全局变量好了,不需要存在eeprom
//日期时间
uint16_t yy; //0~99
uint16_t MM;
uint16_t dd;
uint16_t hh;
uint16_t mm;
uint16_t ss;
//显示设置
uint16_t ScreenBrightness; // 0~100% 屏幕亮度
//熄屏唤醒
uint16_t Sensitivity; // 0~100% 动作阈值
uint16_t SleepTime; // 0~999S 亮屏时间
//开关标志
settingsBitsType settingsBits[2];
};

/*
* systemStorageType
* 用于片外 EEPROM 储存设置信息和数据采集信息
*/
typedef union {
struct orgData data; //运行时使用
uint8_t ctrl[sizeof(orgData)]; //向eeprom读写时使用
} systemStorageType;

如何节约标志位的空间?

注:”Colum“ 的正确拼写是 "Column",后文都是用 ”Colum“ 是我的历史遗留问题,见笑了

SAU-G0的系统设置里有很多“开启”或”关闭“的设置,如果用uint8_t,或uint16_t存放,非常耗费EEPROM容量,使用前面提到的技巧

系统设置是systemStorageType类型,它的data成员的settingsBits[2]数组,是settingsBitsType类型,其使用union内嵌位域结构体,每个settings_Bits结构可存8个标志位(0或1):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//路径 Settings.h
#define sysBits 0 //表示系统设置
#define colBits 1 //表示采样设置
struct settings_Bits{
uint8_t bit0 :1;
uint8_t bit1 :1;
uint8_t bit2 :1;
uint8_t bit3 :1;
uint8_t bit4 :1;
uint8_t bit5 :1;
uint8_t bit6 :1;
uint8_t bit7 :1;
};

typedef union{
settings_Bits bits;
uint8_t ctrl; // colum对象成员prBits和mask修改bits时使用
}settingsBitsType;

在实现多级菜单的Page类中也实现了对位域的修改,例如构造colum时可传入 指向settingsBitsType的指针 + 对应bit的掩码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//路径 colum.hpp
Class colum {
......
colum(const char *Str, settingsBitsType* Bits, uint8_t Mask) :
str(Str), ptrBits(Bits), mask(Mask){}
......
};

//路径 CutomPageObjects.cpp,实例化colum对象
std::vector<colum> columsScreenOffAndWKUP = {
......
colum("自动休眠", &systemSto.data.settingsBits[sysBits], B00000001),
......
};

changeSettingsBitByMask()实现修改位,类似于I2C读写寄存器指定位:

1
2
3
4
5
6
7
8
9
10
//路径 Page.cpp
void changeSettingsBitByMask(settingsBitsType * ptrBits, uint8_t mask){
uint8_t b = ptrBits->ctrl; //先备份一个
(b & mask)? //若b对应mask为1的位为1
(b = b & ~mask) //不改变b对应mask为0位的值,将mask为1的值改为0
: //b对应mask为1的位为0
(b = b | mask); //不改变b对应mask为0位的值,将mask为0的值改为1
//参考 b = (data != 0) ? (b | mask) : (b & ~mask);
ptrBits->ctrl = b; //保存b到结构体
}

测试union内嵌位域结构体作为colum构造函数参数,在Debug下查看位域修改情况:

union内嵌位域结构体作为Colum构造函数参数在Debug下查看位域修改情况

SAU-G0采集数据的格式

存SAU-G0采集数据的EEPROM建议24C16(16Kbit容量)以上,因为 24C16刚好是2KB,与G031 Flash的页大小一样,存满了可以拷贝到G031的Flash的一个页里

对于24C16,每小时存一组(温度16bit+湿度16bit)的采集数据,可以存20天,消耗1920byte,余下的128byte,支持每天用6byte存一次日期,还余下8byte,6byte日期数据如下:

大小 1byte 1byte 1byte 1byte 1byte 1byte
类型 uint8_t uint8_t uint8_t uint8_t uint8_t uint8_t
举个栗子 22 5 23 16 04 23
单位

4byte温湿度采集数据如下:

大小 2byte 2byte
类型 uint16_t uint16_t
举个栗子 25.7 63.2
单位 ℃或℉ %

即使24C16只有2KB最多存20天,但拷贝到flash后,只要G031 的 Flash空间还有,每页就能多存20天

串口输出EEPROM存储的采集数据

流程:每24次里 从EEPEOM读1次日期,读24次温湿度数据,转成字符串,分配一个缓冲区,将热乎的字符串数据格式化成MarkDown表格的编码结构

举个例子,想要串口输出成这样子的:(注意时间和日期没有前导0)

Date & Time T(℃) H(%)
2022/5/10 8:00 8.72 5.83
2022/5/10 10:00 22.54 58.44
2022/5/10 12:00 25.67 53.12
... ... ...
1
2
3
4
5
6
| 时间            | 温度(℃) | 湿度(%) |
| --------------- | ------- | ------- |
| 2022/5/10 08:00 | 18.7 | 65.8 |
| 2022/5/10 10:00 | 22.5 | 58.4 |
| 2022/5/10 12:00 | 25.6 | 53.1 |
| ... | ... | ... |

不算表格抬头,一次输出一行,至少得分配42byte作为TxBuffer

1
2
3
| 2022/5/10 08:00 | 18.7    | 65.8    |  \r \n \0
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
012345678901234567890123456789012345678 9 0 1

相关实现在 columsDataCollect_Export

EEPROM 字节对齐?

I2C_EEPROM/I2C_eeprom.cpp,这个库实现了EEPROM字节对齐

stm32使用#pragma pack(非常详细的字节对齐用法说明)

STM32 FATFS结构体的字节对齐问题.所以建议大家在自己建立结构体的时候,最好加上__packed关键字

Excel 技巧

excel录入技巧:如何进行日期格式的转换,真日期vs假日期

Excel时间计算公式大全

计算两个时间之间的差值 support.microsoft

如何在excel中求气温距平序列