2016年12月4日 星期日

ESP8266 Arduino IDE 開發問與答 Q&A ( 2 )

網頁最後修改時間:2016/12/04
  • 【EEPROM 和 SPIFFS 都是 Flash】2016/12/04
ESP8266 模組使用外部的 Flash 作為存放韌體程式與相關資料用,但是並不是所有的空間都被消耗殆盡;而是有剩餘很多部分都是空閒的,而這些部分就是可被使用者自行利用的空間。

外部 Flash 使用 SPI 的方式與 ESP8266 做溝通,所以也稱為 SPI Flash,大小由 512KBytes 到最大 16 MBytes。像最早的 ESP-01 (藍色板) 就都是 512 KBytes 的 Flash,現在 (黑色板) 都是 1MBytes 的 Flash 設置;ESP-12F 就使用 4MByte 的 Flash ... 等 (相關識別 Flash 大小的方法在 Q&A (1) 已經介紹過了)。

ESP8266 的 Flash 對於前面 1MByte 的位址可以使用記憶體映射 (0x40200000 ~ 0x40300000) 的方式加快存取速度,但後面的部分就不行 ! 但整段區域都可以使用 ESP8266 SDK 裡面的 SPI Flash 特定函式來作為擦除 (Erase)、讀取 (Read)和寫入 (Write) 的動作,就這三個函式就可以玩弄整個 ESP8266 SPI Flash !

但是,使用這三個指令之前卻是有一些事必須要先了解 ! 畢竟函式只是工具,怎麼使用這些工具以及在什麼時候用 ? 用的時候的注意事項為何 ... 等等,都是非常重要的 ! 因為我們現在是在操作 Flash,弄錯有可能會讓 ESP8266 掛機,要重新燒錄程式 !

【EEPROM 和 SPIFFS 都是 Flash】
ESP8266 SDK 對於 Flash 的規劃如下圖所示,分為支援雲端韌體升級 (FOTA, Firmwaire Over-The-Air) 和不支援雲端韌體升級 (Non-FOTA) 兩種。主要是 ESP8266 SDK 編譯之後產生的韌體檔案名稱與大小不同,所以有這兩種配置,分別將所需要的檔案名稱列在下面對應的規劃區之內
FLASH MAP for ESP8266 FOTA and Non-FOTA
上面的這些區域都有對應的燒錄位址,檔案大小也有最大的限制,使用之後剩餘的綠色區域 (User Data),這就是我們 "可自由使用的空間"。

相對與使用 Arduino ESP8266 Core,編譯出來的韌體只有一個,燒錄的位址是由 SPI Flash 0x000000 開始燒錄,且編譯完成之後自動燒錄,不需要再去掙扎要燒到哪個位址去。
Flash 不管大小為何,最後面的四個磁區 (Sector) 是保留區共 16 KBytes (System Param);ESP8266 core for Arduino 更把前面一個磁區 (4KBytes) 作為 EEPROM 使用。
對比於 ESP8266 SDK 的 Flash 規劃,ESP8266 Core for Arduino 的 Flash 規劃如下圖所示,粉紅色與藍色的部份是我的理解與補充 (這邊改了很多次,希望這樣的方式能夠更加容易理解 ! )
|--------------|-------|-----------------|---------|----------------|
^              ^       ^                 ^         ^
Sketch    OTA update   File system       EEPROM    WiFi config (SDK)
|----------------------|-----------------|-4KBytes-|---- 16Kytes----|
|         {!}          |----- SPIFF -----| EEPROM  |  System Param  |
                       |--- {Arduino} Tools/Flsh Size: #M SPIFFS ---|
要解釋上圖,先看一下 "{Arduino IDE}/Tools/Flash Size:" 列在後面的是什麼 ?
Arduino IDE: Flash Size 選擇
/*-/--*-*/*/*/*/***//-*-*-**-*/*-*-/*/*/*-*-/-////--/**/**--**/--///--//**----**//--**//**----***//*-**//*
4M (1M SPIFFS)
ESP8266 模組內,外接的 SPI Flash 大小為 4MBytes,規劃裡面的 1MBytes 為 SPIFFS ( SPI Flash File System )
/*-/--*-*/*/*/*/***//-*-*-**-*/*-*-/*/*/*-*-/-////--/**/**--**/--///--//**----**//--**//**----***//*-**//*
SPIFFS (SPI Flash File Systm) 這一段區域是規劃用來給使用者存放檔案的,可以利用軟體或是自己撰寫上傳的網頁介面 (這不在這一篇的討論範圍內) 上傳檔案,也就是說使用者登陸 WebServer 的時候,我們可以根據他所要求的網頁網址回傳 SPIFFS 裡面 (例如:http://esp8266.local/index.html 所要求的 index.html ) 檔案文件一般,以及回傳相關的 *.css 和 *.js ... 等輔助文件,就像一個遠端的伺服器一樣,不需要再特別去處理包裝 HTML 的回傳,這非常方便 ! 但這裡只是提及,不在這一篇網頁的討論範圍。我們要討論的是,Flash "自由使用的空間",可用來永久存放數據的地方在哪裡 ? 怎麼用 ?

看一下上面 ESP8266 core for Arduino 的 Flash 規劃 (如藍色標示),#M SPIFFS 由這三部分構成:

#M SPIFFS= SPIFFS + EEPROM + System Param

上面有兩個 SPIFFS,這是怎麼回事 !

#M SPIFFS 是在 Arduino IDE 上面所設定的 "Flash Size: " 的參數 (假設使用 "4M (1M SPIFFS",那麼 # 就是 1),但實際上並沒有到 1M 那麼多的空間可以用!扣除掉 EEPROMSystem ParamSPIFFS 函式庫的計算,實際上只有少於 1004 KBytes 的空間可以用 (這也是我們下面要討論的東西),這裡的少於 1004 KBytes 就是算是裡面的 SPIFFS,也代表 SPIFFS 函式庫裡面可用的空間範圍,所以 #M SPIFFS 是燒錄時設定的 "Flash Size:",算式裡面的 SPIFFS 是函式庫可使用的 Flash 範圍。因為名稱一樣,所以為避免搞混,下面使用時都是使用全文:#M SPIFFSSPIFFS,請記住它們兩者的差別 !

接著說明 EEPROM。要先知道的是:ESP8266 沒有所謂的 EEPROM 這個東西!

這裡的 EEPROM 是從 Flash 中取一塊磁區虛擬出來的,最大只有 4KBytes (4096) 可以使用,位在 SPIFFS (也就是 File System) 的後面,Flash 倒數的第 5 個磁區。

而  System Param 則占據 Flash 最後四個磁區,共 16KBytes (16384)。EEPROMSystem Param 這兩個區域佔據的位置不管 Flash 多大,都是固定大小與位置的。

SPIFFS 這個空間中,除了可以放置檔案之外,剩餘的空間還可以額外做使用,這就是上面說到的 "可自由使用的空間",這個空間裡的磁區大小、擦除、讀和寫的方式都是跟 EEPROM 和 SPIFFS 使用相同的 SPI Flash 基本操作函式就可以實現。例如在這空間中,劃分一塊 256 KBytes 的空間來儲存個人通訊資料,只要 SPIFFS 扣除掉已使用的檔案大小還有足夠的空間,就可以自由規劃來使用。所以知道哪裡是 "可自由使用的空間" 就能獲得更多的儲存資源可以做應用。

ESP8266 的 SPI Flash 讀取很簡單,只要知道讀取的位址與讀取的數量就可以;但是要寫入資料,則是必須要做磁區擦除的動作才能呼叫寫入的函式寫入資料,不然就會錯亂,WHY ???

簡單解釋就是:擦除時將磁區裡面的資料全部擦除 1 ( 0x11111111),在寫入資料時 (例如 0x00001010) 進行 AND 計算 (得 0x00001010);如果不進行擦除就直接寫入的話,答案就不會是 0x00001010。

另外,Flash 是有壽命的 ! 大概是 100,000+ ~ 1,000,000 才會失效 ! 所以存放的數據就比較適合不經常改變的資料。

了解 SPI Flash 操作最好的例子,就是 EEPROM 函式庫 ! 雖然它的最大可使用的大小只有 4KBytes,但是它怎麼對 Flash 擦除、讀取和寫入卻是可以作為 SPI Flash 操作函式的學習與瞭解。

/*--*//**---/*///**---*-*////***--*/*///***----*///--*/*///**--*/*//**--**/*//
* 舉例說明 4M (1M SPIFFS):

SPI Flash 讀取時可以使用記憶體映射的方式讀取前面 1MByte 的位址,但是對於後面的部分必須要以真實的 Flash 位址才行。

SPI Flash 在寫入時必須先進行擦除的動作,每次擦除以 4KBytes (一個磁區 (sector) ) 為單位,所以計算 EEPROM 所處的磁區位址就是第 1019 個磁區

{ 4MBytes (0x40 0000) - 16KByte (System Param, 0x4000) - 4KBytes (EEPROM, 0x1000) } ÷ 4KBytes (sector) = 1019

spi_flash_erase_sector(1019);

整個計算方式看一下 EEPROM.cpp 程式碼就不難了解,而且從中也可以發現,最大的 EEPROM 設定的大小為 4096 bytes,每次的讀取與寫入都是在一個 unsigned char 的緩衝陣列中,只有當下達 EEPROM.commit() 時,才會真正的將資料寫入到 Flash 中 ! WHY ? 這是因為 Flash 的讀比較快,但是寫慢很多,如果每次寫入都要擦除一次磁區,那樣太浪費了 !

------------------------
* 4-byte aligned:
------------------------
再深入看一下 EEPROM.begin() 這個函式。其中,size 的計算觀念上很重要,因為 Flash 的讀寫必須要 4-byte aligned (4 位元對齊) !
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();
}

簡單說:uint8_t = unsigned char 為 1-byte 大小,當要將 uint8_t 的陣列寫進去 Flash 的時候,如果沒有事先對陣列數量進行過 4-byte 對齊,寫進去的數據即便經過型態轉換為 uint32_t,寫進去的位址也會出現錯誤,所以經過上面的計算式計算之後可以得到對其之後的 size 大小,如此才能正確的將資料寫進去和讀出來
EEPROM.begin(248);  // size = (248 + 3) & (~3) = 0xFB  & 0xFC  = 0xFB  = 248
EEPROM.begin(249);  // size = (249 + 3) & (~3) = 0xFC  & 0xFC  = 0xFC  = 252
EEPROM.begin(250);  // size = (250 + 3) & (~3) = 0xFD  & 0xFC  = 0xFC  = 252
EEPROM.begin(251);  // size = (251 + 3) & (~3) = 0xFE  & 0xFC  = 0xFC  = 252
EEPROM.begin(252);  // size = (252 + 3) & (~3) = 0xFF  & 0xFC  = 0xFC  = 252
EEPROM.begin(253);  // size = (253 + 3) & (~3) = 0x100 & 0xFFC = 0x100 = 256

下面是一個轉換 uint8_t 到 uint32_t 在轉換回 uint8_t 的例子,若對上面不清楚可以看一下。旁邊是 online 編譯器的網址,把程式碼丟上去可以看到執行後輸出的結果 !
#include <stdint.h>
#include <stdio.h>
int main()
{
        uint8_t a[4]={0x01, 0x02, 0x03, 0x04};
        uint32_t b = *((uint32_t*) a);
        printf( "b=0x%x\n", b );
        uint8_t c[4];
        for (int i=0; i<4 ;++i)
                c[i] = ((uint8_t*)&b)[i];
        printf( "[0]0x%x, [1]0x%x, [2]0x%x, [3]0x%x\n", c[0], c[1], c[2], c[3] );
}
[ 執行之後的輸出 ]
b=0x4030201
[0]0x1, [1]0x2, [2]0x3, [3]0x4

當 a[] 陣列不是 3 的時候 (沒有 4-byte aligned ),當指定 a[] b 就會少一位,而這一位就是未知的數據,重複編譯執行,b ( = ##030201, ## 的部分) 和 c[3] 都會不一樣,這就會導致 b 的數值與實際的數值不同,如果是要做數值轉換的話就會產生錯誤
#include <stdint.h>
#include <stdio.h>
int main()
{
        uint8_t a[3]={0x01, 0x02, 0x03};
        uint32_t b = *((uint32_t*) a);
        printf( "b=0x%x\n", b );
        uint8_t c[4];
        for (int i=0; i<4 ;++i)
                c[i] = ((uint8_t*)&b)[i];
        printf( "[0]0x%x, [1]0x%x, [2]0x%x, [3]0x%x\n", c[0], c[1], c[2], c[3] );
}
[其中一個輸出的結果]
b=0xe5030201
[0]0x1, [1]0x2, [2]0x3, [3]0xe5

但是若是將 a[] 陣列大小設為 4,b##c[3] 就會預期的結果,這才是我們要的結果 !
#include <stdint.h>
#include <stdio.h>
int main()
{
        uint8_t a[4]={0x01, 0x02, 0x03};
        uint32_t b = *((uint32_t*) a);
        printf( "b=0x%x\n", b );
        uint8_t c[4];
        for (int i=0; i<4 ;++i)
                c[i] = ((uint8_t*)&b)[i];
        printf( "[0]0x%x, [1]0x%x, [2]0x%x, [3]0x%x\n", c[0], c[1], c[2], c[3] );
}
[輸出的結果]
b=0x30201
[0]0x1, [1]0x2, [2]0x3, [3]0x0

EEPROM.cpp 程式中,因為未知使用者會開啟多少的緩衝區做使用,所以必須在內部做 4-byte 對齊計算的動作。但如果要寫入的資料數量是一個可以控制的變數,那麼對於寫入 Flash 時需要考慮的 4-byte 對齊動作,就可以在符合下面兩個原則完全忽略不用去管
  • 緩衝陣列的數量必須要可用 4 做整除
  • 緩衝陣列的數量必須要是磁區大小的因數
關於上面兩個原則,後面會給出一個實例。

現在還有一個地方沒做說明: "可自由使用的空間"

輔助資料:
------------------------------
* 可自由使用的空間:
------------------------------
扣除掉 EEPROM System Param 兩個區域 20KByte 之後,剩下的 SPIFFS 空間計算可參照下面程式中列出的部分,再深入查閱 Arduino ESP8266 Core 裡的 spiffs 相關的檔案,可總結出 Q&A (1) 提過 Flash 大小是怎麼算出來的
checkFlashConfig 加強版輸出
Flash 可用空間為 957314 Bytes 來自於:
  • block size 設置為 8192 Bytes,1004 KBytes 除以 8192 Bytes 可得到 125.5,無條件捨去之後得 125 個可用 block count 在 SPIFFS 空間
  • page size 設置為 256 Bytes,可得每一個 block 有 32 個 pages
  • 每一個 page header length 佔據 5-byte,所以每一個 data page size 剩下 256 - 5 = 251 bytes 可用
  • SPIFFS 函式庫需要保留 2 個 block 和 1 個 object lookup page,為了緊急狀況使用額外提供 1 個 page,由這些資訊可得到 SPIFFS 1004 KByte 總共可用的 page 數量為 3814
  • 總和上面各像結果,就可以得到 SPIFFS 可用的 total bytes 為 3814 * 251 = 957, 314 bytes
了不了解中間的過程其實並不是那麼的重要,結果才是我們必須要注意的!

SPIFFS 存放上傳檔案的方式,應該是從 SPI Flash 3M 的地方開始放,扣除掉上傳到 SPIFFS 檔案的大小,剩下的空間就是 "可自由使用的空間",在這個空間進行任何讀寫的動作必須特別注意資料的邊界,不可跨界動作 ! 一旦出錯就是系統重啟,掛機的意思 !

為了避免與 SPIFFS 檔案系統重疊使用,建議從尾部取用 ! 以現在使用的例子,就是從第 1019 個磁區往前取用,每次取用以一個磁區做為最小單位,最大可取用到第 768 磁區。

------------------------------------------
* 可自由使用的空間範例程式:
------------------------------------------
下面是一個簡單的範例程式,取用 EEPROM 前面的一個磁區作為存放資料之用,每一筆資料的長度不超過 64 的大小,要存放 64 筆資料 (當然可以存放更多的資料,只要在增加擦除磁區的位置和增加寫入與讀取的數量就可以 ),寫入之後還要讀出資料做比對,來確認是否真的寫入完成 !
可自由使用空間的範例程式執行結果
SPI Flash 測試程式碼下載

上面就是一個簡單且很實用的例子 ! 延伸這個範例就可以在這些 "可自由使用的空間" 中儲存自己的資料,在需要的時後取出使用,但是一定要留意上面討論所提到要注意的地方,才不會導致掛機!

所以若是瞭解上面所說的東西之後,那麼 Q&A (1) 所提到的將 WiFI 連線的帳號跟密碼儲存到 Flash 應該就會寫了吧 !

這一篇 Q&A (2) 應該就到這邊結束了! 接下來就換到 Q&A (3) 繼續其他的話題,ESP8266 出現異常時,怎麼查到可能出錯的地方或是程式碼 ?


<< 相關網頁與連結 >>

1 則留言:

  1. 非常謝謝你的文章 ~ 非常非常受用, 講解也很清楚, 我C語言的基礎不夠int32_t與int8_t的指標的我有點看不懂, 但還是非常謝謝

    回覆刪除

留言屬名為"Unknown"或"不明"的用戶,大多這樣的留言都會直接被刪除掉,不會得到任何回覆!

發問問題,請描述清楚你(妳)的問題,別人回答前不會想去 "猜" 問題是什麼?

不知道怎麼發問,請看 [公告] 部落格提問須知 - 如何問問題 !