ESP8266 自定EEPROM起始位址存资料
撰写ESP8266 library时需要将设定资透过EEPROM library储存,但又想要避免使用者在使用我写的library配合EEPROM library时将资料盖掉而兴起研究ESP8266 EEPROM library的念头,看看是否有可以利用且不需要自行重写一个将资料储存的方式。
原理
EEPROM library在Arduino中是经常使用于断电储资料,相容Arduino的ESP8266也不例外,但ESP8266所用的是将flash中某一块4K的连续位址给予模拟成EEPROM library,至于为什么是4K呢?主要原因是flash删除时是以sector为单位1 sector等于4096Bytes(4K),透过ESP8266 SDK提供的API将flash中的资料一次读取至Buffer中是没有限制一次就要将4K全读进Buffer,而Buffer大小由EEPROM.begin()决定,但Buffer大小会占用记忆体,务必依照自行使用的大小进行宣告才能节省记忆体。
写入的动作透过commit
将flash定址的4K资料删除后才将Buffer中写入资料,原理大致上如下图:
所以要确保资料都会存到flash中,请自行考量commit
指令的时机,让Buffer会写入flash中。
读/写/提交
例如,我确定使用的空间为256bytes时,像下宣告:
EEPRON.begin(256);
读位址0的资料:
byte value = EEPROM.read(0);
写入资料至位址0:
byte value; value = 'a'; EEPROM.write(0,value);
范例中的读、写资料动作是不会写入flash,而是对Buffer进行操作,只有commit()时才会写入flash,这点需要再度强调。
节录EEPROM.cpp中的read()内容:
uint8_t EEPROMClass::read(int address) { if (address < 0 || (size_t)address >= _size) return 0; if(!_data) return 0; return _data[address]; }
读取是由address当索引至_data
阵列中也就是EEPROM Buffer取得资料,写入也是写到阵列中:
void EEPROMClass::write(int address, uint8_t value) { if (address < 0 || (size_t)address >= _size) return; if(!_data) return; // Optimise _dirty. Only flagged if data written is different. uint8_t* pData = &_data[address]; if (*pData != value) { *pData = value; _dirty = true; } }
如果变动的资料与Buffer中的不同就将_dirty
设true
,之后commit()
就会对flash进行删除后再将Buffer写入flash。
bool EEPROMClass::commit() { bool ret = false; if (!_size) return false; if(!_dirty) return true; if(!_data) return false; noInterrupts(); if(spi_flash_erase_sector(_sector) == SPI_FLASH_RESULT_OK) { if(spi_flash_write(_sector * SPI_FLASH_SEC_SIZE, reinterpret_cast<uint32_t*>(_data), _size) == SPI_FLASH_RESULT_OK) { _dirty = false; ret = true; } } interrupts(); return ret; }
以上理解原理后在资料储存至flash的时机点应该就能掌握,避免增加资料遗失的风险。
位址/初始化
Arduino中使用EEPROM是利用EEPROM.begin(size)
进行宣告容量,先前已提到这是使用Buffer模拟的,那我们来看一下原始码:
void EEPROMClass::begin(size_t size) { if (size <= 0) return; if (size > SPI_FLASH_SEC_SIZE) size = SPI_FLASH_SEC_SIZE; size = (size + 3) & (~3); if (_data) { delete[] _data; } _data = new uint8_t[size]; _size = size; noInterrupts(); spi_flash_read(_sector * SPI_FLASH_SEC_SIZE, reinterpret_cast<uint32_t*>(_data), _size); interrupts(); }
整个EEPROM.cpp主要Class为EEPROMClass
之后再宣告成EEPROM
,不过先看一下主要的EEPROMClass
内容。
上面程式内容知道,经由size
设定至内部变数_size
,并且由spi_flash_read
读取flash中的内容至_data
阵列中,所以_data
大小由size
来决定, 这也就是为什么要强调必需注意你宣告的大小,避免记忆体不足的现像产生。
当看到spi_flash_read
的第一个参数_sector * SPI_FLASH_SEC_SIZE
则是决定位址,从内容一追之后发现_sector
由下列程式内容决定:
EEPROMClass::EEPROMClass(uint32_t sector) : _sector(sector) , _data(0) , _size(0) , _dirty(false) { }
一开始宣告Class时的预设值只有sector
需要被指派,接下去再看一下是在哪决定的?程式中的最下面一行能得到答案:
EEPROMClass EEPROM((((uint32_t)&_SPIFFS_end - 0x40200000) / SPI_FLASH_SEC_SIZE));
0x40200000
代表的是flash的0x00000
,所以EEPROM是接续在_SPIFFS_end
之后,而_SPIFFS_end
定义为你经过Arduino IDE选择SPIFFS
的大小后,再从设定档中取得已设定好的值:
对于_SPIFFS_end
的值想要了解请查阅ESP8266/Arduino中的ld宣告, 这里选择一个eagle.flash.4m.ld内容来理解(此为上图中所选择的4M(3M SPIFSS):
/* Flash Split for 4M chips */ /* sketch 1019KB */。 /* spiffs 3052KB */ /* eeprom 20KB */ MEMORY { dport0_0_seg : org = 0x3FF00000, len = 0x10 dram0_0_seg : org = 0x3FFE8000, len = 0x14000 iram1_0_seg : org = 0x40100000, len = 0x8000 irom0_0_seg : org = 0x40201010, len = 0xfeff0 } PROVIDE ( _SPIFFS_start = 0x40300000 ); PROVIDE ( _SPIFFS_end = 0x405FB000 ); PROVIDE ( _SPIFFS_page = 0x100 ); PROVIDE ( _SPIFFS_block = 0x2000 ); INCLUDE "../ld/eagle.app.v6.common.ld"
经转换后的宣告为:
EEPROMClass EEPROM(((0x405FB000 - 0x40200000) / SPI_FLASH_SEC_SIZE));
因必需由位址转换成sector,所以(起始位址-结束位址)后必需要再除于每1个sector大小,SPI_FLASH_SEC_SIZE
值能从spi_flash.h定义中取得值为4096(4K),经过计算后才是sector位址:
EEPROMClass EEPROM(1019);
整个结论可以得到,重新宣告EEPROMClass
就有机会指定位址来另外产生一组类似EEPROM
class,所以接下来就是要建立自已专属的位址并操作方式与 EEPROM相同。
## 自定EEPROM位址
Danny将自定位址定于SPIFFS
中的最后一个sector,也就是之前结论得到的值:
EEPROMClass EEPROM(1019 - 1);
那反推回去就会变成:
EEPROMClass EEPROM(((0x405FB000 - 0x40200000) / SPI_FLASH_SEC_SIZE) - 1);
再往回推:
EEPROMClass EEPROM((((uint32_t)&_SPIFFS_end - 0x40200000) / SPI_FLASH_SEC_SIZE) - 1);
最后就得到整个公式算法,EEPROM已经被宣告过了,此时改成你要选宣告的名称就完成,Danny命名为DYEEPROM
EEPROMClass DYEEPROM((((uint32_t)&_SPIFFS_end - 0x40200000) / SPI_FLASH_SEC_SIZE) - 1);
未来SPIFFS
最后一个sector的整个4Kbytes就被DYEEPROM所使用,也不会与EEPROM重复使用,在使用时只要与使用者提醒会占用SPIFSS
最后的4Kbytes,整体下来的完整性也有,也能避免一些问题的产生。