文章程式碼顯示

2018年3月3日 星期六

NodeMCU 教學 - 13:NodeMCU 的 Flash 資料 讀取/寫入

本文原文連結為以下連結,我將該作者的部分內容進行節錄並且做一些小小的修正,若原文作者對此有意見請與我聯繫,將立刻刪除此文章


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



ESP8266 模組內,外接的 SPI Flash 大小為 4MBytes ( 下圖五個區間總共 ),規劃裡面的 1MBytes 為 SPIFFS ( SPI Flash File System ),SPIFFS (SPI Flash File Systm) 這一段區域是規劃用來給使用者存放檔案的


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

#M SPIFFS= SPIFFS + EEPROM + System Param

上面有兩個 SPIFFS,這是怎麼回事 !?? ( 上圖中原作者應該是漏了 SPIFFS 的 S )

#M SPIFFS 是在 Arduino IDE 上面所設定的 "Flash Size: " 的參數 (假設使用 "4M (1M SPIFFS",那麼 # 就是 1),但實際上並沒有到 1M Bytes 那麼多的空間可以用(上圖右邊三塊)

扣除掉 EEPROM ( 4K Bytes ) 和 System Param ( 16K Bytes )和 SPIFFS 函式庫的計算,實際上只有少於 1004 K Bytes 的空間可以用,這裡的少於 1004 K Bytes 就算是裡面的 SPIFFS也代表 SPIFFS 函式庫裡面可用的空間範圍

所以 #M SPIFFS 是燒錄時設定的 "Flash Size:",算式裡面的 SPIFFS 是函式庫可使用的 Flash 範圍

因為名稱一樣,所以為避免搞混,下面使用時都是使用全文:#M SPIFFS 和 SPIFFS,請記住它們兩者的差別 !

========================================================================

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

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

而  System Param 則占據 Flash 最後四個磁區,共 16KBytes (1024*16 = 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 操作函式的學習與瞭解。

練習計算 EEPROM 的位址

SPI Flash 讀取時可以使用記憶體映射的方式讀取前面 1MByte 的位址,但是對於後面的部分必須要以真實的 Flash 位址才行。(此段話我不是很理解,後面的部分是指什麼?)

SPI Flash 在寫入時必須先進行擦除的動作,每次擦除以 4KBytes (一個磁區 (sector) ) 為單位

因為整個 SPI FLASH 有 4M Bytes (0x40 0000),往回扣除 System Param 的 16K Bytes (0x4000)再扣除 EEPROM 的 4K Bytes (0x1000) ,最後除上磁區的單位( 4K Bytes ) ,就算出 EEPROM 所處的磁區位址是第 1019 個磁區。

存取 SPIFFS

SPIFFS 存放資料的部分,應該是從 SPI Flash 3M 的地方開始放,在這個空間(約957, 314 bytes) 進行任何讀寫的動作。

必須特別注意資料的邊界,不可跨界動作 ! 一旦出錯就是系統重啟,掛機的意思 !

為了避免掛機,建議從尾部取用 ! 以現在使用的例子,就是從第 1019 個磁區( EEPROM 的頭)往前取用,每次取用以一個磁區做為最小單位,最大可取用到第 768 磁區



原作者給岀了一個程式碼,取用 EEPROM 前面的一個磁區作為存放資料之用,每一筆資料的長度不超過 64 的大小,要存放 64 筆資料 (當然可以存放更多的資料,只要在增加擦除磁區的位置和增加寫入與讀取的數量就可以 ),寫入之後還要讀出資料做比對,來確認是否真的寫入完成 !

Hint : 原文作者在下方的程式碼中 addrstart 為 0x3EB000,但文字敘述是取前面一個磁區,應為誤植(0x3FB000 往前回推 4K Bytes 應是 0x3FA000)。


#include "ESP8266WiFi.h"
#include "ESP.h"

char text[]    = "this is a test string";
char buff[ sizeof(text) ];
const uint32_t addrstart = 0x3FA000; 
const uint32_t addrend   = 0x3FB000;

void setup() {
    Serial.begin(115200);

    // ERASE 。 flashEraseSector 的輸入參數需為 "磁區" ,故將 addrstart/4096 ,也就是右移 12 位
    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);
}





一個磁區有 4K Bytes(4096 Bytes) ,原作者將其在細分割為 64 個小區段,每個區段就會是 64 Bytes 的長度。

補充:傳數値



#include "ESP8266WiFi.h"
#include "ESP.h"

uint32_t text0    = 1;
uint32_t buff0;
char text[] = "Ephone";
char text1[] = "evan6060";
char buff[ sizeof(text) ];
char buff1[ sizeof(text1) ];
const uint32_t addrstart = 0x3FA000; 

void setup() {
    Serial.begin(115200);

    // ERASE 。 flashEraseSector 的輸入參數需為 "磁區" ,故將 addrstart/4096 ,也就是右移 12 位
    if( !ESP.flashEraseSector( addrstart >> 12 ) ) {
        Serial.println( "\n\nErase error\n");
        return;
    } else {
        Serial.println( "\n\nErase OK\n----------------");
    }
    
    // WRITE
    uint32_t flash_address;
    int i = 0;

        flash_address = addrstart + i*64;        
        
        if( !ESP.flashWrite( flash_address, (uint32_t*)&text0, 32 ) ) {
            Serial.printf( "%2d: [error] write addr: %p\n", i, flash_address );
        } else Serial.printf( "%2d: addr: %p write [%d] OK\n", i, flash_address, text0 );

        if ( !ESP.flashRead( flash_address, (uint32_t*)&buff0, 32 ) ) {
            printf( "%2d: [error] read addr: %p\n", i, flash_address );
        } else Serial.printf( "%2d: addr: %p read [%d] OK\n", i, flash_address, buff0 );

        i = 1;
        flash_address = addrstart + i*64;        
        
        if( !ESP.flashWrite( flash_address, (uint32_t*)text, 64 ) ) {
            Serial.printf( "%2d: [error] write addr: %p\n", i, flash_address );
        } 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, 64 ) ) {
            printf( "%2d: [error] read addr: %p\n", i, flash_address );
        } 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 );
        } else Serial.printf( "%2d: addr: %p compare OK\n", i, flash_address );

        i = 2;
        flash_address = addrstart + i*64;
        
        if( !ESP.flashWrite( flash_address, (uint32_t*)text1, 64 ) ) {
            Serial.printf( "%2d: [error] write addr: %p\n", i, flash_address );
        } else Serial.printf( "%2d: addr: %p write [%s] OK\n", i, flash_address, text1 );

        memset( buff1, 0, sizeof( buff1 ) );
        if ( !ESP.flashRead( flash_address, (uint32_t*)buff1, 64 ) ) {
            printf( "%2d: [error] read addr: %p\n", i, flash_address );
        } else Serial.printf( "%2d: addr: %p read [%s] OK\n", i, flash_address, buff1 );

        if ( memcmp( text1, buff1, sizeof( buff1 ) ) != 0 ) {
            printf( "%2d: addr: %p, In != Out\n", i, flash_address );
        } else Serial.printf( "%2d: addr: %p compare OK\n", i, flash_address );
    
}

void loop() {
    delay(500);
}





↓↓↓ 連結到部落格方針與索引 ↓↓↓

Blog 使用方針與索引