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