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,整體下來的完整性也有,也能避免一些問題的產生。