網頁最後修改時間:2016/12/04
- 【EEPROM 和 SPIFFS 都是 Flash】2016/12/04
外部 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 |
相對與使用 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 那麼多的空間可以用!扣除掉 EEPROM 和 System Param 和 SPIFFS 函式庫的計算,實際上只有少於 1004 KBytes 的空間可以用 (這也是我們下面要討論的東西),這裡的少於 1004 KBytes 就是算是裡面的 SPIFFS,也代表 SPIFFS 函式庫裡面可用的空間範圍,所以 #M SPIFFS 是燒錄時設定的 "Flash Size:",算式裡面的 SPIFFS 是函式庫可使用的 Flash 範圍。因為名稱一樣,所以為避免搞混,下面使用時都是使用全文:#M SPIFFS 和 SPIFFS,請記住它們兩者的差別 !
接著說明 EEPROM。要先知道的是:ESP8266 沒有所謂的 EEPROM 這個東西!
這裡的 EEPROM 是從 Flash 中取一塊磁區虛擬出來的,最大只有 4KBytes (4096) 可以使用,位在 SPIFFS (也就是 File System) 的後面,Flash 倒數的第 5 個磁區。
而 System Param 則占據 Flash 最後四個磁區,共 16KBytes (16384)。EEPROM 和 System 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 加強版輸出 |
- 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 筆資料 (當然可以存放更多的資料,只要在增加擦除磁區的位置和增加寫入與讀取的數量就可以 ),寫入之後還要讀出資料做比對,來確認是否真的寫入完成 !
![]() |
可自由使用空間的範例程式執行結果 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <ESP8266WiFi.h> | |
#include <ESP.h> | |
char text[] = "this is a test string"; | |
char buff[ sizeof(text) ]; | |
const uint32_t addrstart = 0x3EB000; | |
const uint32_t addrend = 0x3FB000; | |
void setup() { | |
Serial.begin(115200); | |
// ERASE | |
if( !ESP.flashEraseSector( addrstart >> 12 ) ) { | |
Serial.println( "\n\nErase error\n"); | |
return; | |
} else { | |
Serial.println( "\n\nErase OK\n----------------"); | |
} | |
// WRITE | |
uint32_t flash_address; | |
for( int i = 0; i < 64; i++ ) { | |
flash_address = addrstart + i * 64; | |
if( !ESP.flashWrite( flash_address, (uint32_t*)text, sizeof(text) - 1 ) ) { | |
Serial.printf( "%2d: [error] write addr: %p\n", i, flash_address ); | |
continue; | |
} else Serial.printf( "%2d: addr: %p write [%s] OK\n", i, flash_address, text ); | |
memset( buff, 0, sizeof( buff ) ); | |
if ( !ESP.flashRead( flash_address, (uint32_t*)buff, sizeof(text) - 1 ) ) { | |
printf( "%2d: [error] read addr: %p\n", i, flash_address ); | |
continue; | |
} else Serial.printf( "%2d: addr: %p read [%s] OK\n", i, flash_address, buff ); | |
if ( memcmp( text, buff, sizeof( buff ) ) != 0 ) { | |
printf( "%2d: addr: %p, In != Out\n", i, flash_address ); | |
continue; | |
} else Serial.printf( "%2d: addr: %p compare OK\n", i, flash_address ); | |
} | |
} | |
void loop() { | |
delay(500); | |
} |
上面就是一個簡單且很實用的例子 ! 延伸這個範例就可以在這些 "可自由使用的空間" 中儲存自己的資料,在需要的時後取出使用,但是一定要留意上面討論所提到要注意的地方,才不會導致掛機!
所以若是瞭解上面所說的東西之後,那麼 Q&A (1) 所提到的將 WiFI 連線的帳號跟密碼儲存到 Flash 應該就會寫了吧 !
這一篇 Q&A (2) 應該就到這邊結束了! 接下來就換到 Q&A (3) 繼續其他的話題,ESP8266 出現異常時,怎麼查到可能出錯的地方或是程式碼 ?
<< 相關網頁與連結 >>
- Google Spreadsheet(試算表)之 ESP8266 溫濕度紀錄與趨勢圖
- ESP8266 入門學習套件支援 Arduino IDE 開發環境之安裝、使用說明與範例
說明如何在 Arduino IDE 環境中開發 ESP8266 的程式。 - ESP8266 Arduino IDE 開發問與答 Q&A ( 1 )
- 【ESP8266 / 模組 / 開發板】
- 【Web of Thing (WoT, 萬物網)】
- 【聰明連線的方法:WPS / Smart Config】
- ESP8266 Arduino IDE 開發問與答 Q&A ( 2 )
- 【EEPROM 和 SPIFFS 都是 Flash】
- ESP8266 Arduino IDE 開發問與答 Q&A ( 3 )
- 【ESP8266 崩潰 (Crash) 的時候 !】
非常謝謝你的文章 ~ 非常非常受用, 講解也很清楚, 我C語言的基礎不夠int32_t與int8_t的指標的我有點看不懂, 但還是非常謝謝
回覆刪除