ESP8266開發之旅 基礎篇④ ESP8266與EEPROM

授人以魚不如授人以漁,目的不是爲了教會你具體項目開發,而是學會學習的能力。但願你們分享給你周邊須要的朋友或者同窗,說不定大神成長之路有博哥的奠定石。。。git

QQ技術互動交流羣:ESP8266&32 物聯網開發 羣號622368884,不喜勿噴github

1、你若是想學基於Arduino的ESP8266開發技術

1、基礎篇web

  1. ESP8266開發之旅 基礎篇① 走進ESP8266的世界
  2. ESP8266開發之旅 基礎篇② 如何安裝ESP8266的Arduino開發環境
  3. ESP8266開發之旅 基礎篇③ ESP8266與Arduino的開發說明
  4. ESP8266開發之旅 基礎篇④ ESP8266與EEPROM
  5. ESP8266開發之旅 基礎篇⑤ ESP8266 SPI通訊和I2C通訊
  6. ESP8266開發之旅 基礎篇⑥ Ticker——ESP8266定時庫

2、網絡篇編程

  1. ESP8266開發之旅 網絡篇① 認識一下Arduino Core For ESP8266
  2. ESP8266開發之旅 網絡篇② ESP8266 工做模式與ESP8266WiFi庫
  3. ESP8266開發之旅 網絡篇③ Soft-AP——ESP8266WiFiAP庫的使用
  4. ESP8266開發之旅 網絡篇④ Station——ESP8266WiFiSTA庫的使用
  5. ESP8266開發之旅 網絡篇⑤ Scan WiFi——ESP8266WiFiScan庫的使用
  6. ESP8266開發之旅 網絡篇⑥ ESP8266WiFiGeneric——基礎庫
  7. ESP8266開發之旅 網絡篇⑦ TCP Server & TCP Client
  8. ESP8266開發之旅 網絡篇⑧ SmartConfig——一鍵配網
  9. ESP8266開發之旅 網絡篇⑨ HttpClient——ESP8266HTTPClient庫的使用
  10. ESP8266開發之旅 網絡篇⑩ UDP服務
  11. ESP8266開發之旅 網絡篇⑪ WebServer——ESP8266WebServer庫的使用
  12. ESP8266開發之旅 網絡篇⑫ 域名服務——ESP8266mDNS庫
  13. ESP8266開發之旅 網絡篇⑬ SPIFFS——ESP8266 Flash文件系統
  14. ESP8266開發之旅 網絡篇⑭ web配網
  15. ESP8266開發之旅 網絡篇⑮ 真正的域名服務——DNSServer
  16. ESP8266開發之旅 網絡篇⑯ 無線更新——OTA固件更新

3、應用篇網絡

  1. ESP8266開發之旅 應用篇① 局域網應用 ——炫酷RGB彩燈
  2. ESP8266開發之旅 應用篇② OLED顯示天氣屏
  3. ESP8266開發之旅 應用篇③ 簡易版WiFi小車

4、高級篇app

  1. ESP8266開發之旅 進階篇① 代碼優化 —— ESP8266內存管理
  2. ESP8266開發之旅 進階篇② 閒聊Arduino IDE For ESP8266配置
  3. ESP8266開發之旅 進階篇③ 閒聊 ESP8266 Flash
  4. ESP8266開發之旅 進階篇④ 常見問題 —— 解決困擾
  5. ESP8266開發之旅 進階篇⑤ 代碼規範 —— 像寫文章同樣優美
  6. ESP8266開發之旅 進階篇⑥ ESP-specific APIs說明

    EEPROM(Electrically Erasable Programmable Read-Only Memory),電可擦可編程只讀存儲器——一種掉電後數據不丟失的存儲芯片。
    EEPROM能夠在不使用文件和文件系統的狀況下用來固化一些數據,常見的好比用來保存SSID或者Password,保存用戶設置等數據,這樣就能夠不用每次都經過燒寫程序來改變系統運行時的初始值。
    Arduino提供了完善的eeprom庫,不過須要注意的是ESP8266沒有硬件EEPROM,使用的是flash模擬的EEPROMdom

1. 原理

    EEPROM庫在Arduino中常常用於存儲設定數據。固然基於Arduino的ESP8266也不例外。可是,和真正的Arduino板子不同的是,ESP8266採用的方式是將flash中某一塊4K的存儲模擬成EEPROM。至於爲何是4K呢?主要緣由是flash是以sector爲一個單位,1 sector等於4096Bytes(4KB),操做flash時是以sector爲一個總體來操做。
    讀取操做是經過ESP8266 SDK提供的API將flash中的內容讀取到Buffer中是沒有限制一次就要將4K全讀完,Buffer的大小由EEPROM.begin(size)決定,可是因爲Buffer大小會佔用內存RAM,因此務必按照實際須要來定義大小。
    寫入操做是經過commit將flash eeprom地址的4K 存儲內容刪除後纔將Buffer寫入flash中(也就是說就算你buffer只有4個字節,可是最終仍是會刷新整個sector),原理大體以下圖:
image
    因此要確保內容被保存到flash中,須要考慮commit的時機。函數

2. 官方介紹

    下圖來源於Arduino For ESP8266對於EEPROM的介紹:
image
    具體意思能夠理解爲如下幾點:oop

  • ESP8266 EEPROM的操做其實和Arduino EEPROM的操做核心思想很像,可是又有所不一樣。
  • 和標準的EEPROM庫不同的是,你須要在讀或者寫操做以前先經過 EEPROM.begin(size) 來聲明你須要操做的存儲大小,size取值範圍爲4~4096字節。
  • EEPROM.write() 不會馬上把內容寫進flash,若是你但願保持到flash去,那麼你必須調用 EEPROM.commit()。固然, EEPROM.end() 不只也能完成commit,同時會釋放申請的eeprom ram資源。
  • EEPROM庫跟在SPIFFS文件系統的後面(那麼讀者就得考慮不一樣的SPIFFS大小對應的地址是不同)。

3. 庫介紹

    EEPROM庫很是簡單,請看博主總結的百度腦圖:
image
    僅僅有5個方法,可是博主在這裏仍是帶讀者深刻去理解一下它們。
    Arduino Core For ESP8266的源碼在github上能夠查找到,讀者能夠把它下載下來以便後續深刻開發,連接位置爲 ESP8266源碼
    而後請找到下圖位置:
image
    性能

3.1 begin()

    該功能用於申請具體大小的ram內存空間。
    函數: begin(size)
    參數:
        size:要申請的內存大小。
    返回值: 無;
    注意點:

  • 入參size必須大於0。
void EEPROMClass::begin(size_t size) {
  //size 必須大於0
  if (size <= 0)
    return;
  if (size > SPI_FLASH_SEC_SIZE)
    //超過4096的size,都強制變成4096
    size = SPI_FLASH_SEC_SIZE;

  //size最終的大小都是4個倍數,好比輸入1,最終size是4
  size = (size + 3) & (~3);

  //In case begin() is called a 2nd+ time, don't reallocate if size is the same
  if(_data && size != _size) {
    delete[] _data;
    _data = new uint8_t[size];
  } else if(!_data) {
    //建立內存buffer空間 這裏須要注意
    _data = new uint8_t[size];
  }

  _size = size;

  noInterrupts();
  //把具體內容讀取出來
  spi_flash_read(_sector * SPI_FLASH_SEC_SIZE, reinterpret_cast<uint32_t*>(_data), _size);
  interrupts();

  _dirty = false; //make sure dirty is cleared in case begin() is called 2nd+ time
}
  • 從begin源碼解析能夠看出,雖然咱們能夠輸入自定義size,可是最終會通過計算獲得真正的size(4的倍數),並申請對應的內存空間,這也驗證了博主上面說的flash模擬EEPROM。
  • 因此 begin(1) 等同於 begin(4)。

3.2 write()

    該功能用於往內存空間去寫入數據。
    函數: write(address,value)
    參數:
        address:要寫入的地址位置,取值範圍爲內存空間的地址0~size。
        val:寫入的數據。
    返回值: 無;
    注意點:

void EEPROMClass::write(int const address, uint8_t const 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;
  }
}

從源碼能夠看出,寫入的數據只是寫入到申請的內存空間,並非馬上寫入到flash中。

3.3 read()

    該功能用於讀取數據操做。
    函數: read(address)
    參數:
        address:要讀取的地址位置,取值範圍爲內存空間的地址0~size。
    返回值: 返回存儲數據;
    注意點:

uint8_t EEPROMClass::read(int const address) {
  if (address < 0 || (size_t)address >= _size)
    return 0;
  if(!_data)
    return 0;
  //讀取內存數據
  return _data[address];
}

從源碼看出,讀取的數據也是從begin中生成的內存空間中去獲取,並不會直接操做flash。操做內存的一個好處就是快。

3.4 commit()

    該功能用於把內存空間的數據覆蓋到flash eeprom塊去。
    函數: commit()
    參數: 無;
    返回值: 返回bool值,表示是否覆蓋成功;
    注意點:

bool EEPROMClass::commit() {
  bool ret = false;
  if (!_size)
    return false;
  if(!_dirty)
    return true;
  if(!_data)
    return false;

  noInterrupts();
  //是否擦除eeprom sector成功
  if(spi_flash_erase_sector(_sector) == SPI_FLASH_RESULT_OK) {
    //把內存空間數據寫入到eeprom sector
    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空間;
  • 並且,寫回flash以前會把整一塊sector所有擦除掉,也就意味着就算咱們begin(1)最終也是會擦除4096字節空間。可是size的大小決定了內存空間的剩餘量以及回寫的快慢,因此根據具體狀況來設置size。

3.5 end()

    該功能用於寫入flash,而且釋放內存空間。
    函數: end()
    參數: 無;
    返回值: 無;
    注意點:

void EEPROMClass::end() {
  if (!_size)
    return;
  //寫入flash
  commit();
  if(_data) {
    //回收內存空間
    delete[] _data;
  }
  _data = 0;
  _size = 0;
  _dirty = false;
}
  • 從源碼看,end包含了寫入flash,而且回收內存空間。
  • 建議讀者操做完EEPROM以後,必須調用這個方法,回收內存空間很重要。

4. EEPROM位置

4.1 EEPROM官方定義

    前面,咱們說到,ESP8266採用的方式是將flash中某一塊4K的存儲模擬成EEPROM。那麼它到底在哪個位置呢?請看看源碼:

EEPROMClass::EEPROMClass(uint32_t sector)
: _sector(sector)
, _data(0)
, _size(0)
, _dirty(false)
{
}

EEPROMClass::EEPROMClass(void)
: _sector((((uint32_t)&_SPIFFS_end - 0x40200000) / SPI_FLASH_SEC_SIZE))
, _data(0)
, _size(0)
, _dirty(false)
{
}
  • _sector表明的是具體哪一塊4K sector。
  • 重點代碼在 **(uint32_t)&_SPIFFS_end - 0x40200000) / SPI_FLASH_SEC_SIZE)** ,0x40200000表明的是flash的0x00000,SPIFFS_end定義爲你設置Arduino IDE的SPIFFS的大小後,再從設置中取得已設定好的值:
    image
        對於_SPIFFS_end的值具體能夠參考 地址定義,這裏選擇其中一個 eagle.flash.4m3m.ld 來說解怎麼計算(這個爲4M(3M SPIFSS)):
/* Flash Split for 4M chips */
/* sketch @0x40200000 (~1019KB) (1044464B) */
/* empty  @0x402FEFF0 (~4KB) (4112B) */
/* spiffs @0x40300000 (~3052KB) (3125248B) */
/* eeprom @0x405FB000 (4KB) */
/* rfcal  @0x405FC000 (4KB) */
/* wifi   @0x405FD000 (12KB) */

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 "local.eagle.app.v6.common.ld"

    代入上面的公式變成:

EEPROMClass EEPROM(((0x405FB000 - 0x40200000) / SPI_FLASH_SEC_SIZE));

其中 SPI_FLASH_SEC_SIZE定位爲 4096(4K),具體定義可參考 spi_flash.h
    因此最終獲得的結果是:

EEPROMClass EEPROM(1019);

4.2 EEPROM自定義

    從上一節的計算,咱們能夠知道,根據計算公式,咱們會最終獲得一個具體位置的sector來描述eeprom。那麼,反過來思考一下,既然官方的eeprom對應的sector地址是SPIFFS_END的下一個sector,那麼在官方eeprom存儲不夠用的前提下,咱們是否能夠本身定義一個sector來繼續存儲更多的內容?若是能夠,那麼這個sector該取哪一部分呢?
    很顯然,若是咱們沒有用到SPIFFS,徹底能夠利用這一塊區域去作咱們自定義的EEPROM。這裏咱們選擇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);

注意點:

  • 咱們定義了SPIFFS最後一個sector的整個4Kbytes做爲自定義EEPROM,若是使用到了SPIFFS,須要考慮是否會覆蓋它。

5. 實例講解

5.1 寫數據

/*
 * 功能描述:該代碼向EEPROM寫入100字節數據
 */
#include <EEPROM.h>

int addr = 0; //EEPROM數據地址

void setup() 
{
  Serial.begin(9600);
  Serial.println("");
  Serial.println("Start write");

  EEPROM.begin(100);
  for(addr = 0; addr<100; addr++)
  {
    int data = addr;
    EEPROM.write(addr, addr); //寫數據
  }
  EEPROM.end(); //保存更改的數據

  Serial.println("End write");
}

void loop() 
{
}

5.2 讀數據

/*
 * 功能描述:該代碼從EEPROM讀取100字節數據
 */
#include <EEPROM.h>

int addr = 0;

void setup() 
{
  Serial.begin(9600);
  Serial.println("");
  Serial.println("Start read");

  EEPROM.begin(100); 
  for(addr = 0; addr<100; addr++)
  {
    int data = EEPROM.read(addr); //讀數據
    Serial.print(data);
    Serial.print(" ");
    delay(2);
  }
  //釋放內存
  EEPROM.end();
  Serial.println("End read");
}

void loop() 
{
}

5.3 清除數據

/*
   EEPROM Clear

   Sets all of the bytes of the EEPROM to 0.
   This example code is in the public domain.

*/

#include <EEPROM.h>

void setup() {
  EEPROM.begin(100);
  // write a 0 to all 512 bytes of the EEPROM
  for (int i = 0; i < 100; i++) {
    EEPROM.write(i, 0);
  }
  //釋放內存
  EEPROM.end();
}

void loop() {
}

5.4 結構體操做

    在沒有應用結構體以前,不論是寫入仍是讀取操做,咱們都須要記住具體的存儲位置。特別是當配置數據愈來愈多的時候或者別人維護的時候,很是容易出錯。那麼有沒有辦法優雅地解決這種問題呢?固然有,那就是結構體的妙用,咱們不須要關注具體的位置,只須要關注數據自己。看如下代碼:

/*
 * 功能描述:eeprom結構體操做
 */
#include <EEPROM.h>

#define DEFAULT_STASSID "danpianjicainiao"
#define DEFAULT_STAPSW  "boge"

struct config_type
{
  char stassid[32];
  char stapsw[64];
};

config_type config;

/*
 * 保存參數到EEPROM
*/
void saveConfig()
{
  Serial.println("Save config!");
  Serial.print("stassid:");
  Serial.println(config.stassid);
  Serial.print("stapsw:");
  Serial.println(config.stapsw);

  EEPROM.begin(1024);
  uint8_t *p = (uint8_t*)(&config);
  for (int i = 0; i < sizeof(config); i++)
  {
    EEPROM.write(i, *(p + i));
  }
  EEPROM.commit();
}

/*
 * 從EEPROM加載參數
*/
void loadConfig()
{
  EEPROM.begin(1024);
  uint8_t *p = (uint8_t*)(&config);
  for (int i = 0; i < sizeof(config); i++)
  {
    *(p + i) = EEPROM.read(i);
  }
  EEPROM.commit();
  Serial.println("-----Read config-----");
  Serial.print("stassid:");
  Serial.println(config.stassid);
  Serial.print("stapsw:");
  Serial.println(config.stapsw);
}

/*
*初始化
*/
void setup() {
  ESP.wdtEnable(5000);
  strcpy(config.stassid, DEFAULT_STASSID);
  strcpy(config.stapsw, DEFAULT_STAPSW);
  saveConfig();
}
/*
*主循環
*/
void loop() {
  ESP.wdtFeed();
  loadConfig();
}

結構體與EEPROM的結合使用,使咱們脫離了存儲位置的限制,就算後期須要加多一個配置,咱們只須要在結構體上加上相應的字段,徹底不用改動其餘代碼。

6. 總結

     這一章,講解了ESP8266 EEPROM的底層設計原理,講述了內存和flash之間的關係,也講解了方法使用,雖然簡單,可是對於底層的認知,會讓咱們優化代碼性能更加便捷。

相關文章
相關標籤/搜索