ESP8266 自定EEPROM起始位址存資料

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中的不同就將_dirtytrue,之後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就有機會指定位址來另外產生一組類似EEPROMclass,所以接下來就是要建立自已專屬的位址並操作方式與 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,整體下來的完整性也有,也能避免一些問題的產生。

參考資料

SPI Flash Mapping

ESP8266/Arduino