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同时不影响该页原有的其他数据,那么原有的其他数据如果要保留怎么办,有两种方式:
-
找一个空的页,将原有的有用数据的页整个复制过去,将原来的页擦除,将1byte的数据变化+复制过去的页复制到原来的flash,最后临时使用的这个空的页擦除。这个方法省MCU的内存,不需要在SRAM中存Flash页里的所有相关数据作为复制操作的缓冲区
-
在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的扇区做复制备份,才是行得通的方法
可见,如果需要频繁地修改非易失性存储器的数据,而且单次改动的数据量又很小,比如只占最小擦除粒度的 1/100,如果使用方式1,得磨损2次,如果使用方式2磨损1次,而且,STM32的Flash是有擦写寿命的,例如G031最坏情况只有1万次:
对于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 | 3 | 0 | 128 | 8 | |
AT24C02 | 256 x 8 (2K) | 8-byte page | 32 | 8bit | 3 | 0 | 256 | 8 | |
AT24C04 | 512 x 8 (4K) | 16-byte page | 32 | 8bit | 2 | 1 | 256 x 21 | 4 | |
AT24C08 | 1,024 x 8 (8K) | 16-byte page | 64 | 8bit | 1 | 2 | 256 x 22 | 2 | |
AT24C16 | 2,048 x 8 (16K) | 16-byte page | 128 | 8bit | 0 | 3 | 256 x 23 | 1 | |
AT24C32 | 4,096 x 8 (32K) | 32-byte page | 128 | 16bit | 3 | 0 | 4096 | 8 | |
AT24C64 | 8,192 x 8 (64K) | 32-byte page | 256 | 16bit | 3 | 0 | 8192 | 8 | |
AT24C128 | 16,384 x 8 (128K) | 64-byte page | 256 | 16bit | 3 | 0 | 16384 | 8 | |
AT24C256 | 32,768 x 8 (256K) | 64-byte page | 512 | 16bit | 3 | 0 | 32768 | 8 | |
AT24C512 | 65,536 x 8 (512K) | 128-byte page | 512 | 16bit | 3 | 0 | 65536 | 8 | |
AT24C1024 | 131,072 x 8 (1024K) | 256-byte page | 512 | 16bit | 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有关的代码大都是以此库的进行分析
EEPROM擦除
一般擦除法
ee24库在erasefull函数内创建了一个:256byte大小全为0xff的常量,多次写这个数据以达到擦除整片EEPROM
1 | bool ee24_eraseChip(void) |
特殊擦除法
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 | //ee24Config.h |
但对于 安森美 - 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 | bool ee24_write(uint16_t address, uint8_t *data, size_t len, uint32_t timeout); |
读写函数体内部是多次while的形式,C预处理器根据EEPROM的页大小,计算出每次while的地址指针增量,即传入HAL_I2C_Mem_Read的MemAddSize参数,非常优雅,比如ee24_write:
1 | bool ee24_write(uint16_t address, uint8_t *data, size_t len, uint32_t timeout){ |
分析为AT24C02的情况:
EEPROM型号 | 容量 | 页大小 | 页数 | 发送地址位数 | I2C地址结构 | A(n)位数 | P(n)位数 | 可寻址数 | 一条I2C总线最多 挂载同型号数量 |
---|---|---|---|---|---|---|---|---|---|
AT24C02 | 256 x 8 (2K) | 8-byte page | 32 | 8bit | 3 | 0 | 256 | 8 |
1 | while (1) |
根据AT24Cxx系列参数表格,AT24C02为 8-byte-page,发送地址为8bit,片内子地址寻址(地址指针)可对内部 256 Bytes中的任一个进行读/写操作,其寻址范围为00~FF,共256个寻址单位
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 | 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 | bool ee24_write(uint16_t address, uint8_t *data, size_t len, 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 | bool ee24_write_test_A() |
每次循环的记录当运行传到“断点" “ 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或连续读写
以上情况24C16的 DevAddress
和address
的高位没有发生变化,如果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 | //address升级为uint16_t, 添加#elif (_EEPROM_SIZE_KBIT == 1024)部分,函数其余部分不变 |
模拟一下写入43个1byte数据到AT24C1024,address
为0xfff0
第 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.cpp的I2C_eeprom::determineSize()
分析
1 | // returns size in bytes |
注意,抬头的注释:对于24C16及以下容量的型号,使用1byte address检测,24C32及以上容量的型号,使用
2byte address检测,地址长度的变化根据I2C_eeprom类的成员_isAddressSizeTwoWords
的值在I2C_eeprom::_beginTransmission()
中更改,相关代码节选:
1 | // supports one and two bytes addresses |
_isAddressSizeTwoWords
默认未初始化,必须在I2C_eeprom类其中一个构造函数中传入deviceSize,才会初始化:
1 | class I2C_eeprom { |
这个检测逻辑如果不给定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 |
|
修改arduino库的实现到EE24库
以下是检测页和Page大小的函数(我用 class 将 ee24 库套了一层)
1 |
|
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类型的数据(使用共用体)
🐂🍺结构体内嵌联合体 在数据组包和解包的应用、Ti单片寄存器封装:union内嵌位域结构体、管理状态变量
1 | struct sensor_rom_t |
把 union 的 str 成员用 eeprom_write 写到eeprom里就可以了
1 | eeprom_write((uint8_t *)&structx, sizeof(structx)); |
不过有个问题,,结构体有不同类型的成员,编译器可能插入填充字节进行字节对齐,结构体实际大小可能大于其成员大小的总和,这种情况的结构体如果是union 的成员,那么用union的另一个成员保存到EEPROM,会造成EEPROM空间浪费?先Mark一下,以后再想想
SAU-G0的系统设置的存储结构
1 | //路径 Settings.h |
如何节约标志位的空间?
注:”Colum“ 的正确拼写是 "Column",后文都是用 ”Colum“ 是我的历史遗留问题,见笑了
SAU-G0的系统设置里有很多“开启”或”关闭“的设置,如果用uint8_t,或uint16_t存放,非常耗费EEPROM容量,使用前面提到的技巧
系统设置是systemStorageType类型,它的data成员的settingsBits[2]数组,是settingsBitsType类型,其使用union内嵌位域结构体,每个settings_Bits结构可存8个标志位(0或1):
1 | //路径 Settings.h |
在实现多级菜单的Page类中也实现了对位域的修改,例如构造colum时可传入 指向settingsBitsType的指针
+ 对应bit的掩码
:
1 | //路径 colum.hpp |
changeSettingsBitByMask()实现修改位,类似于I2C读写寄存器指定位:
1 | //路径 Page.cpp |
测试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 | | 时间 | 温度(℃) | 湿度(%) | |
不算表格抬头,一次输出一行,至少得分配42byte作为TxBuffer
1 | | 2022/5/10 08:00 | 18.7 | 65.8 | \r \n \0 |
相关实现在 columsDataCollect_Export
附
EEPROM 字节对齐?
I2C_EEPROM/I2C_eeprom.cpp,这个库实现了EEPROM字节对齐
stm32使用#pragma pack(非常详细的字节对齐用法说明)
STM32 FATFS结构体的字节对齐问题.所以建议大家在自己建立结构体的时候,最好加上__packed关键字
Excel 技巧
- 本文链接: http://oldgerman.github.io/74fbdec2/
- 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!