12. 存储

Flash结构

即使文件系统与程序存储在同一 Flash 芯片中,编写 Arduino SKetch 程序也不会修改文件系统内容。这允许使用文件系统来存储草图数据,配置文件或 Web 服务器的内容。

下图说明了 Arduino 环境中使用的 Flash 布局:

|--------------|-------|---------------|--|--|--|--|--|
^              ^       ^               ^     ^
Sketch    OTA update   File system   EEPROM  WiFi config (SDK)

包括 Arduino SKetch APP 程序空间、OTA空间、文件系统、EEPROM 参数存储、WiFi config。

文件系统的大小取决于闪存芯片的大小。根据在 Arduino IDE 中选择 Flash Size 、Partition Scheme 分区方案等选项。

12.1 Preferences 配置参数存储

当前不建议使用 EEPROM 库。 对于 Edge101WE 主板上的新应用程序,请使用下面的 Preferences 方式。

Preferences 库使用主板中的 FLASH 作为 NVS(Non-volatile storage 非易失性存储)。

API参考

begin() - 开始使用NVS

开始使用 NVS(Non-volatile storage 非易失性存储)。

语法

#include <Preferences.h>

bool Preferences::begin(const char * name, bool readOnly = false, const char* partition_label=NULL);

参数

传入值 说明 值范围
name 要使用的命名空间。最多可以使用15个字符。
readOnly 如果为true,则为只读。如果为false,则可读/写。默认为false
partition_label 分区标卷。如果省略,则为Null

返回

返回值 说明 值范围
bool 执行结果。如果成功,则为true。如果不成功则为false。

clear() - 删除所有键值

从NVS中删除当前使用的命名空间中的所有键值。

语法

#include <Preferences.h>

bool Preferences::clear();

参数

返回

返回值 说明 值范围
bool 执行结果。如果成功,则为true。如果不成功则为false。

end() - 停止使用NVS

语法

#include <Preferences.h>

void Preferences::end();

参数

返回

Preferences::get() - 获取指定键对应的值

Preferences::getChar()
Preferences::getUChar()
Preferences::getShort()
Preferences::getUShort()
Preferences::getInt()
Preferences::getUInt()
Preferences::getLong()
Preferences::getULong()
Preferences::getLong64()
Preferences::getULong64()
Preferences::getFloat()
Preferences::getDouble()
Preferences::getBool()
Preferences::getString()

从NVS中获取与当前使用的命名空间中的指定键对应的值。

语法

#include <Preferences.h>

int8_t Preferences::getChar(const char* key, const int8_t defaultValue);
uint8_t Preferences::getUChar(const char* key, const uint8_t defaultValue);
int16_t Preferences::getShort(const char* key, const int16_t defaultValue);
uint16_t Preferences::getUShort(const char* key, const uint16_t defaultValue);
int32_t Preferences::getInt(const char* key, const int32_t defaultValue);
uint32_t Preferences::getUInt(const char* key, const uint32_t defaultValue);
int32_t Preferences::getLong(const char* key, const int32_t defaultValue);
uint32_t Preferences::getULong(const char* key, const uint32_t defaultValue);
int64_t Preferences::getLong64(const char* key, const int64_t defaultValue);
uint64_t Preferences::getULong64(const char* key, const uint64_t defaultValue);
float_t Preferences::getFloat(const char* key, const float_t defaultValue);
double_t Preferences::getDouble(const char* key, const double_t defaultValue);
bool Preferences::getBool(const char* key, const bool defaultValue);
size_t Preferences::getString(const char* key, char* value, const size_t maxLen);
String Preferences::getString(const char* key, const String defaultValue);
size_t Preferences::getBytesLength(const char* key);
size_t Preferences::getBytes(const char* key, void * buf, size_t maxLen) ;

参数

传入值 说明 值范围
key 要获取的数据的键名。最多可以使用15个字符
defaultValue 发生错误时要返回的值
value, buf 获得的值
maxLen 获得字符串的最大长度

返回

返回值 说明 值范围
如果返回类型不是size_t:如果读取成功,则该值对应于指定的键。如果失败,则为defaultValue指定的值。
如果返回类型为size_t:如果读取失败,则为0。

注意

由于发生错误时会返回defaultValue,因此无法以编程方式检查错误是否确实发生。错误级别日志被输出。

put() - 将与指定键对应的值写入当前正在使用的名称空间中的NVS中

Preferences::putChar()
Preferences::putUChar()
Preferences::putShort()
Preferences::putUShort()
Preferences::putInt()
Preferences::putUInt()
Preferences::putLong()
Preferences::putULong()
Preferences::putLong64()
Preferences::putULong64()
Preferences::putFloat()
Preferences::putDouble()
Preferences::putBool()
Preferences::putString()

将与指定键对应的值写入当前正在使用的名称空间中的NVS中。

语法

#include <Preferences.h>

size_t Preferences::putChar(const char* key, int8_t value);
size_t Preferences::putUChar(const char* key, uint8_t value);
size_t Preferences::putShort(const char* key, int16_t value);
size_t Preferences::putUShort(const char* key, uint16_t value);
size_t Preferences::putInt(const char* key, int32_t value);
size_t Preferences::putUInt(const char* key, uint32_t value);
size_t Preferences::putLong(const char* key, int32_t value);
size_t Preferences::putULong(const char* key, uint32_t value);
size_t Preferences::putLong64(const char* key, int64_t value);
size_t Preferences::putULong64(const char* key, uint64_t value);
size_t Preferences::putFloat(const char* key, const float_t value);
size_t Preferences::putDouble(const char* key, const double_t value);
size_t Preferences::putDouble(const char* key, const double_t value);
size_t Preferences::putBool(const char* key, const bool value);
size_t Preferences::putString(const char* key, const char* value);
size_t Preferences::putString(const char* key, const String value);
size_t Preferences::putBytes(const char* key, const void* value, size_t len);

参数

传入值 说明 值范围
key 要写入的数据的键名。最多可以使用15个字符。
value 要写入的值。
len 数据长度。

返回

返回值 说明 值范围
size_t 执行结果。如果成功,则为true。如果不成功则为false。

remove() - 从NVS中删除当前使用的命名空间中的指定键/值

语法

#include <Preferences.h>

bool Preferences::remove(const char * key);

参数

传入值 说明 值范围
key 要删除的数据的键名。最多可以使用15个字符。

返回

返回值 说明 值范围
bool 执行结果。如果成功,则为true。如果不成功则为false。

例程: 配置参数存储 Prefs2Struct

(参考Arduino IDE例程 Examples -> Examples for Edge101WE ->Preferences\examples\Prefs2Struct)

/*
This example shows how to use Preferences (nvs) to store a
structure.  Note that the maximum size of a putBytes is 496K
or 97% of the nvs partition size.  nvs has signifcant overhead,
so should not be used for data that will change often.
*/ 
#include <Preferences.h>
Preferences prefs;

typedef struct {
  uint8_t hour;
  uint8_t minute;
  uint8_t setting1;
  uint8_t setting2;
} schedule_t;

void setup() {
  Serial.begin(115200);
  prefs.begin("schedule"); // use "schedule" namespace
  uint8_t content[] = {9, 30, 235, 255, 20, 15, 0, 1}; // two entries
  prefs.putBytes("schedule", content, sizeof(content));
  size_t schLen = prefs.getBytesLength("schedule");
  char buffer[schLen]; // prepare a buffer for the data
  prefs.getBytes("schedule", buffer, schLen);
  if (schLen % sizeof(schedule_t)) { // simple check that data fits
    log_e("Data is not correct size!");
    return;
  }
  schedule_t *schedule = (schedule_t *) buffer; // cast the bytes into a struct ptr
  Serial.printf("%02d:%02d %d/%d\n", 
    schedule[1].hour, schedule[1].minute,
    schedule[1].setting1, schedule[1].setting2);
  schedule[2] = {8, 30, 20, 21}; // add a third entry (unsafely)
// force the struct array into a byte array
  prefs.putBytes("schedule", schedule, 3*sizeof(schedule_t)); 
  schLen = prefs.getBytesLength("schedule");
  char buffer2[schLen];
  prefs.getBytes("schedule", buffer2, schLen);
  for (int x=0; x<schLen; x++) Serial.printf("%02X ", buffer[x]);
  Serial.println(); 
}

void loop() {}

例程在 NVS 中存储数据,然后将数据读取后通过串口输出。

串口输出结果

20:15 0/1
09 1E EB FF 14 0F 00 01 08 1E 14 15 

例程:启动次数计数器 Start Counter

(参考Arduino IDE例程 Examples -> Examples for Edge101WE ->Preferences\examples\StartCounter)

/*
 ESP32 startup counter example with Preferences library.

 This simple example demonstrates using the Preferences library to store how many times
 the ESP32 module has booted. The Preferences library is a wrapper around the Non-volatile
 storage on ESP32 processor.

 created for arduino-esp32 09 Feb 2017
 by Martin Sloup (Arcao)
*/

#include <Preferences.h>

Preferences preferences;

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

  // Open Preferences with my-app namespace. Each application module, library, etc
  // has to use a namespace name to prevent key name collisions. We will open storage in
  // RW-mode (second parameter has to be false).
  // Note: Namespace name is limited to 15 chars.
  preferences.begin("my-app", false);

  // Remove all preferences under the opened namespace
  //preferences.clear();

  // Or remove the counter key only
  //preferences.remove("counter");

  // Get the counter value, if the key does not exist, return a default value of 0
  // Note: Key name is limited to 15 chars.
  unsigned int counter = preferences.getUInt("counter", 0);

  // Increase counter by 1
  counter++;

  // Print the counter to Serial Monitor
  Serial.printf("Current counter value: %u\n", counter);

  // Store the counter to the Preferences
  preferences.putUInt("counter", counter);

  // Close the Preferences
  preferences.end();

  // Wait 10 seconds
  Serial.println("Restarting in 10 seconds...");
  delay(10000);

  // Restart ESP
  ESP.restart();
}

void loop() {}

程序读取 NVS 中保存的启动次数计数器,加一后通过串口打印,同时将加一的值保存到NVS中存储。等待10秒中后重启。

串口输出数据

rst:0xc (SW_CPU_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
configsip: 0, SPIWP:0xee
clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
mode:DIO, clock div:1
load:0x3fff0030,len:1252
load:0x40078000,len:12716
load:0x40080400,len:3068
entry 0x400805e4

Current counter value: 8
Restarting in 10 seconds...
ets Jul 29 2019 12:21:46

rst:0xc (SW_CPU_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
configsip: 0, SPIWP:0xee
clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
mode:DIO, clock div:1
load:0x3fff0030,len:1252
load:0x40078000,len:12716
load:0x40080400,len:3068
entry 0x400805e4

Current counter value: 9
Restarting in 10 seconds...

12.2 FFat 文件系统

FatFs 是用于小型嵌入式系统的通用 FAT / exFAT文件系统模块。FatFs 模块是按照 ANSI C(C89)编写的,并且与磁盘 I / O 层完全分开。因此,它独立于平台。可以将其合并到资源有限的小型微控制器中。

FFat 模块使用每个打开的并发文件 8KB 加上 4KB。默认情况下,它允许打开 10 个文件,这意味着它使用 48KB。如果要减少其内存使用量,则可以告诉它仅支持一个文件,这样可以节省 36KB,而仅使用 12KB。

if(!FFat.begin()){
        Serial.println("FFat Mount Failed");
        return;
    }

由于存在目录,因此该 open 方法的行为不同于 SPIFFS。遍历时,SPIFFS将返回“子目录”中的文件 File::openNextFile() (因为它们实际上不是子目录,而只是名称中带有“ /”的文件),而 FFat 仅返回特定子目录中的文件。这模仿了大多数C程序员习惯的目录遍历的 POSIX 行为。

主板在此闪存中存储程序。连同程序一起,您可以在其中存储文件。该内存的局限性在于它只有10000(一万)个写周期。

API参考

FFat.begin()

此方法将挂载 FFat 文件系统,并且必须在使用任何其他 FS API 之前调用它。如果文件系统安装成功,则返回 true,否则返回 false。

bool F_Fat::begin(bool formatOnFail, const char * basePath, uint8_t maxOpenFiles, const char * partitionLabel)

FFat.format()

格式化文件系统。如果格式化成功,则返回 true。

bool F_Fat::format(bool full_wipe, char* partitionLabel)

FFat.open(path,mode)

打开一个文件。path应该是一个以斜杠开头的绝对路径(例如/dir/filename.txt)。mode 是指定访问模式的字符串。它可以是“ r”,“ w”,“ a”之一。这些模式的含义与 fopen C 函数相同。 返回 File 对象。要检查文件是否成功打开,请使用布尔运算符。

FFat.exists(path)

如果存在具有给定路径的文件,则返回true,否则返回false。

FFat.remove(path)

删除具有绝对路径的文件。如果成功删除文件,则返回true。

FFat.rename(pathFrom,pathTo)

将文件从 pathFrom 重命名为 pathTo。路径必须是绝对的。如果文件已成功重命名,则返回 true。

FFat .mkdir(path)

创建一个新文件夹。 如果目录创建成功,则返回true, 否则返回 false

FFat .rmdir(path)

删除目录。 如果目录已成功删除,则返回true, 否则返回 false

FFat.totalBytes()

返回在 FFat 上启用的总字节数。返回字节。

FFat.usedBytes()

返回在 FFat 上启用的已使用字节总数。返回字节。

file.seek(offset,mode)

此函数的行为类似于 fseek C 函数。根据 mode 的值,它会如下移动文件中的当前位置:

  • 如果mode为SeekSet,则将position设置为从头开始偏移字节。

  • 如果mode为SeekCur,则当前位置移动偏移字节。

  • 如果mode为SeekEnd,则将position设置为从文件末尾偏移字节。

  • 如果位置设置成功,则返回true。

file.position()

返回文件中的当前位置,以字节为单位。

file.size()

返回文件大小,以字节为单位。

file.name()

返回文件名,为const char *。

file.close()

关闭文件。

file.getLastWrite()

上次写入的时间(使用内部时间来管理日期)。

file.isDirectory()

如果它是目录,则返回

file.openNextFile()

设置目录中的下一个文件指针。

file.rewindDirectory()

重新启动指向目录的第一个文件的指针。

例程:FFat_Test

注意:下载例程时 Partition Scheme 分区方案 选择带FAT系统的分区。

(参考Arduino IDE例程 Examples -> Examples for Edge101WE ->FFat\examples\FFat_Test)

#include "FS.h"
#include "FFat.h"

// This file should be compiled with 'Partition Scheme' (in Tools menu)
// set to 'Default with ffat' if you have a 4MB ESP32 dev module or
// set to '16M Fat' if you have a 16MB ESP32 dev module.

// You only need to format FFat the first time you run a test
#define FORMAT_FFAT true

void listDir(fs::FS &fs, const char * dirname, uint8_t levels){
    Serial.printf("Listing directory: %s\r\n", dirname);

    File root = fs.open(dirname);
    if(!root){
        Serial.println("- failed to open directory");
        return;
    }
    if(!root.isDirectory()){
        Serial.println(" - not a directory");
        return;
    }

    File file = root.openNextFile();
    while(file){
        if(file.isDirectory()){
            Serial.print("  DIR : ");
            Serial.println(file.name());
            if(levels){
                listDir(fs, file.path(), levels -1);
            }
        } else {
            Serial.print("  FILE: ");
            Serial.print(file.name());
            Serial.print("\tSIZE: ");
            Serial.println(file.size());
        }
        file = root.openNextFile();
    }
}

void readFile(fs::FS &fs, const char * path){
    Serial.printf("Reading file: %s\r\n", path);

    File file = fs.open(path);
    if(!file || file.isDirectory()){
        Serial.println("- failed to open file for reading");
        return;
    }

    Serial.println("- read from file:");
    while(file.available()){
        Serial.write(file.read());
    }
    file.close();
}

void writeFile(fs::FS &fs, const char * path, const char * message){
    Serial.printf("Writing file: %s\r\n", path);

    File file = fs.open(path, FILE_WRITE);
    if(!file){
        Serial.println("- failed to open file for writing");
        return;
    }
    if(file.print(message)){
        Serial.println("- file written");
    } else {
        Serial.println("- write failed");
    }
    file.close();
}

void appendFile(fs::FS &fs, const char * path, const char * message){
    Serial.printf("Appending to file: %s\r\n", path);

    File file = fs.open(path, FILE_APPEND);
    if(!file){
        Serial.println("- failed to open file for appending");
        return;
    }
    if(file.print(message)){
        Serial.println("- message appended");
    } else {
        Serial.println("- append failed");
    }
    file.close();
}

void renameFile(fs::FS &fs, const char * path1, const char * path2){
    Serial.printf("Renaming file %s to %s\r\n", path1, path2);
    if (fs.rename(path1, path2)) {
        Serial.println("- file renamed");
    } else {
        Serial.println("- rename failed");
    }
}

void deleteFile(fs::FS &fs, const char * path){
    Serial.printf("Deleting file: %s\r\n", path);
    if(fs.remove(path)){
        Serial.println("- file deleted");
    } else {
        Serial.println("- delete failed");
    }
}

void testFileIO(fs::FS &fs, const char * path){
    Serial.printf("Testing file I/O with %s\r\n", path);

    static uint8_t buf[512];
    size_t len = 0;
    File file = fs.open(path, FILE_WRITE);
    if(!file){
        Serial.println("- failed to open file for writing");
        return;
    }

    size_t i;
    Serial.print("- writing" );
    uint32_t start = millis();
    for(i=0; i<2048; i++){
        if ((i & 0x001F) == 0x001F){
          Serial.print(".");
        }
        file.write(buf, 512);
    }
    Serial.println("");
    uint32_t end = millis() - start;
    Serial.printf(" - %u bytes written in %u ms\r\n", 2048 * 512, end);
    file.close();

    file = fs.open(path);
    start = millis();
    end = start;
    i = 0;
    if(file && !file.isDirectory()){
        len = file.size();
        size_t flen = len;
        start = millis();
        Serial.print("- reading" );
        while(len){
            size_t toRead = len;
            if(toRead > 512){
                toRead = 512;
            }
            file.read(buf, toRead);
            if ((i++ & 0x001F) == 0x001F){
              Serial.print(".");
            }
            len -= toRead;
        }
        Serial.println("");
        end = millis() - start;
        Serial.printf("- %u bytes read in %u ms\r\n", flen, end);
        file.close();
    } else {
        Serial.println("- failed to open file for reading");
    }
}

void setup(){
    Serial.begin(115200);
    Serial.setDebugOutput(true);
    if (FORMAT_FFAT) FFat.format();
    if(!FFat.begin()){
        Serial.println("FFat Mount Failed");
        return;
    }

    Serial.printf("Total space: %10u\n", FFat.totalBytes());
    Serial.printf("Free space: %10u\n", FFat.freeBytes());
    listDir(FFat, "/", 0);
    writeFile(FFat, "/hello.txt", "Hello ");
    appendFile(FFat, "/hello.txt", "World!\r\n");
    readFile(FFat, "/hello.txt");
    renameFile(FFat, "/hello.txt", "/foo.txt");
    readFile(FFat, "/foo.txt");
    deleteFile(FFat, "/foo.txt");
    testFileIO(FFat, "/test.txt");
    Serial.printf("Free space: %10u\n", FFat.freeBytes());
    deleteFile(FFat, "/test.txt");
    Serial.println( "Test complete" );
}

void loop(){

}

12.3 SPIFFS 文件系统

SPIFFS

在Arduino IDE安装 SPIFFS工具

ESP32 Web Server using SPIFFS

SPIFFS (SPI Flash Filing System)是一个用于 SPI NOR flash 设备的嵌入式文件系统,支持磨损均衡、文件系统一致性检查等功能,非常适合空间和 RAM 受限的应用程序,这些应用程序使用许多小文件并关心静态和动态损耗平衡,并且不需要真正的目录支持。闪存上的文件系统开销也最小。

说明

  • 目前,SPIFFS 尚不支持目录,但可以生成扁平结构。如果 SPIFFS 挂载在 /spiffs 下,在 /spiffs/tmp/myfile.txt 路径下创建一个文件则会在 SPIFFS 中生成一个名为 /tmp/myfile.txt 的文件,而不是在 /spiffs/tmp 下生成名为 myfile.txt 的文件;

  • SPIFFS 并非实时栈,每次写操作耗时不等;

  • 目前,SPIFFS 尚不支持检测或处理已损坏的块。

  • 文件名总共不能超过 32 个字符。'\0'为 C 字符串终止保留了一个 字符,因此我们剩下31个可用字符。由于在编译或运行时都不会出现错误消息,因此可能不会引起注意。

API 参考

文件操作相关API

write()

size_t write(uint8_t)
size_t write(const uint8_t *buf, size_t size)

向文件中写入数据,该操作会移动文件指针;

int available()

返回当前指针下可读取字节数;

int read()

size_t read(uint8_t* buf, size_t size)
size_t readBytes(char *buffer, size_t length)

读取数据,该操作会移动文件指针;

int peek()

在不移动文件指针的情况下读取一个字节数据;

seek()

bool seek(uint32_t pos, SeekMode mode)
bool seek(uint32_t pos)

移动文件指针,mode可选SeekSet、SeekCur、SeekEnd,分别为正常移动、移动到文件头、移动到文件尾;

size_t position()

返回当前文件指针位置;

size_t size()

返回当前文件的大小;

void close()

关闭当前文件;

operator bool()

返回当前文件是否有效;

time_t getLastWrite()

返回最后修改文件时间;

const char* name()

返回当前文件名

boolean isDirectory(void)

返回当前文件是否为目录;

File openNextFile(const char* mode = FILE_READ)

打开下一个文件;

void rewindDirectory(void)

返回到目录中首文件位置;

另外也可以使用print等方法;

文件系统通用API

open()

File open(const char* path, const char* mode = FILE_READ)
File open(const String& path, const char* mode = FILE_READ)

打开一个文件,输入参数分别为路径,打开方式; mode可选FILE_READ、FILE_WRITE、FILE_APPEND,即”r”、”w”、”a”,只读模式、写入模式、追加模式; 只读模式:打开一个文件用于读取,指针位于文件头; 写入模式:打开一个文件用于写入,指针位于文件头,如果文件不存在则建立文件; 追加模式:打开一个文件用于写入,指针位于文件尾;

exists()

bool exists(const char* path)
bool exists(const String& path)

检查文件或路径是否存在;

remove()

bool remove(const char* path)
bool remove(const String& path)

移除文件;

rename()

bool rename(const char* pathFrom, const char* pathTo)
bool rename(const String& pathFrom, const String& pathTo)

重命名文件,依次输入旧的、新的包含完整文件名的路径;

mkdir()

bool mkdir(const char *path)
bool mkdir(const String &path)

创建目录;

rmdir()

bool rmdir(const char *path)
bool rmdir(const String &path)

删除目录;

SPIFFS文件系统API

begin()

bool begin(bool formatOnFail=false, const char * basePath="/spiffs", uint8_t maxOpenFiles=10)

挂载文件系统,输入参数分别为当挂载失败是否格式化、挂载点、文件最大同时打开数;

调用成功将会返回true,否则返回false

bool format()

格式化文件系统。返回true表示格式化成功。

size_t totalBytes()

返回文件系统总字节数;

size_t usedBytes()

返回文件系统已用字节数;

void end()

取消挂载;

例程:SPIFFS_Test

注意:下载例程时 Partition Scheme 分区方案 选择带 SPIFFS 系统的分区

(参考Arduino IDE例程 Examples -> Examples for Edge101WE ->SPIFFS\examples\SPIFFS_Test)

#include "FS.h"  // 在使用SPIFFS功能之前需要在文件内引用FS头文件
#include "SPIFFS.h"

/* You only need to format SPIFFS the first time you run a
   test or else use the SPIFFS plugin to create a partition
   https://github.com/me-no-dev/arduino-esp32fs-plugin */
#define FORMAT_SPIFFS_IF_FAILED true

void listDir(fs::FS &fs, const char * dirname, uint8_t levels){
    Serial.printf("Listing directory: %s\r\n", dirname);

    File root = fs.open(dirname);
    if(!root){
        Serial.println("- failed to open directory");
        return;
    }
    if(!root.isDirectory()){
        Serial.println(" - not a directory");
        return;
    }

    File file = root.openNextFile();
    while(file){
        if(file.isDirectory()){
            Serial.print("  DIR : ");
            Serial.println(file.name());
            if(levels){
                listDir(fs, file.name(), levels -1);
            }
        } else {
            Serial.print("  FILE: ");
            Serial.print(file.name());
            Serial.print("\tSIZE: ");
            Serial.println(file.size());
        }
        file = root.openNextFile();
    }
}

void readFile(fs::FS &fs, const char * path){
    Serial.printf("Reading file: %s\r\n", path);

    File file = fs.open(path);
    if(!file || file.isDirectory()){
        Serial.println("- failed to open file for reading");
        return;
    }

    Serial.println("- read from file:");
    while(file.available()){
        Serial.write(file.read());
    }
    file.close();
}

void writeFile(fs::FS &fs, const char * path, const char * message){
    Serial.printf("Writing file: %s\r\n", path);

    File file = fs.open(path, FILE_WRITE);
    if(!file){
        Serial.println("- failed to open file for writing");
        return;
    }
    if(file.print(message)){
        Serial.println("- file written");
    } else {
        Serial.println("- write failed");
    }
    file.close();
}

void appendFile(fs::FS &fs, const char * path, const char * message){
    Serial.printf("Appending to file: %s\r\n", path);

    File file = fs.open(path, FILE_APPEND);
    if(!file){
        Serial.println("- failed to open file for appending");
        return;
    }
    if(file.print(message)){
        Serial.println("- message appended");
    } else {
        Serial.println("- append failed");
    }
    file.close();
}

void renameFile(fs::FS &fs, const char * path1, const char * path2){
    Serial.printf("Renaming file %s to %s\r\n", path1, path2);
    if (fs.rename(path1, path2)) {
        Serial.println("- file renamed");
    } else {
        Serial.println("- rename failed");
    }
}

void deleteFile(fs::FS &fs, const char * path){
    Serial.printf("Deleting file: %s\r\n", path);
    if(fs.remove(path)){
        Serial.println("- file deleted");
    } else {
        Serial.println("- delete failed");
    }
}

void testFileIO(fs::FS &fs, const char * path){
    Serial.printf("Testing file I/O with %s\r\n", path);

    static uint8_t buf[512];
    size_t len = 0;
    File file = fs.open(path, FILE_WRITE);
    if(!file){
        Serial.println("- failed to open file for writing");
        return;
    }

    size_t i;
    Serial.print("- writing" );
    uint32_t start = millis();
    for(i=0; i<2048; i++){
        if ((i & 0x001F) == 0x001F){
          Serial.print(".");
        }
        file.write(buf, 512);
    }
    Serial.println("");
    uint32_t end = millis() - start;
    Serial.printf(" - %u bytes written in %u ms\r\n", 2048 * 512, end);
    file.close();

    file = fs.open(path);
    start = millis();
    end = start;
    i = 0;
    if(file && !file.isDirectory()){
        len = file.size();
        size_t flen = len;
        start = millis();
        Serial.print("- reading" );
        while(len){
            size_t toRead = len;
            if(toRead > 512){
                toRead = 512;
            }
            file.read(buf, toRead);
            if ((i++ & 0x001F) == 0x001F){
              Serial.print(".");
            }
            len -= toRead;
        }
        Serial.println("");
        end = millis() - start;
        Serial.printf("- %u bytes read in %u ms\r\n", flen, end);
        file.close();
    } else {
        Serial.println("- failed to open file for reading");
    }
}

void setup(){
    Serial.begin(115200);
    if(!SPIFFS.begin(FORMAT_SPIFFS_IF_FAILED)){
        Serial.println("SPIFFS Mount Failed");
        return;
    }
    
    listDir(SPIFFS, "/", 0);
    writeFile(SPIFFS, "/hello.txt", "Hello ");
    appendFile(SPIFFS, "/hello.txt", "World!\r\n");
    readFile(SPIFFS, "/hello.txt");
    renameFile(SPIFFS, "/hello.txt", "/foo.txt");
    readFile(SPIFFS, "/foo.txt");
    deleteFile(SPIFFS, "/foo.txt");
    testFileIO(SPIFFS, "/test.txt");
    deleteFile(SPIFFS, "/test.txt");
    Serial.println( "Test complete" );
}

void loop(){

}

例程测试对 SPIFFS 系统进行一系列操作。

例程:SPIFFS_time

注意:下载例程时 Partition Scheme 分区方案 选择带 SPIFFS 系统的分区

(参考Arduino IDE例程 Examples -> Examples for Edge101WE ->SPIFFS\examples\SPIFFS_time)

#include "FS.h"
#include "SPIFFS.h"
#include <time.h> 
#include <WiFi.h>

const char* ssid     = "your-ssid";
const char* password = "your-password";

long timezone = 1; 
byte daysavetime = 1;

void listDir(fs::FS &fs, const char * dirname, uint8_t levels){
    Serial.printf("Listing directory: %s\n", dirname);

    File root = fs.open(dirname);
    if(!root){
        Serial.println("Failed to open directory");
        return;
    }
    if(!root.isDirectory()){
        Serial.println("Not a directory");
        return;
    }

    File file = root.openNextFile();
    while(file){
        if(file.isDirectory()){
            Serial.print("  DIR : ");
            Serial.print (file.name());
            time_t t= file.getLastWrite();
            struct tm * tmstruct = localtime(&t);
            Serial.printf("  LAST WRITE: %d-%02d-%02d %02d:%02d:%02d\n",(tmstruct->tm_year)+1900,( tmstruct->tm_mon)+1, tmstruct->tm_mday,tmstruct->tm_hour , tmstruct->tm_min, tmstruct->tm_sec);
            if(levels){
                listDir(fs, file.name(), levels -1);
            }
        } else {
            Serial.print("  FILE: ");
            Serial.print(file.name());
            Serial.print("  SIZE: ");
            Serial.print(file.size());
            time_t t= file.getLastWrite();
            struct tm * tmstruct = localtime(&t);
            Serial.printf("  LAST WRITE: %d-%02d-%02d %02d:%02d:%02d\n",(tmstruct->tm_year)+1900,( tmstruct->tm_mon)+1, tmstruct->tm_mday,tmstruct->tm_hour , tmstruct->tm_min, tmstruct->tm_sec);
        }
        file = root.openNextFile();
    }
}

void createDir(fs::FS &fs, const char * path){
    Serial.printf("Creating Dir: %s\n", path);
    if(fs.mkdir(path)){
        Serial.println("Dir created");
    } else {
        Serial.println("mkdir failed");
    }
}

void removeDir(fs::FS &fs, const char * path){
    Serial.printf("Removing Dir: %s\n", path);
    if(fs.rmdir(path)){
        Serial.println("Dir removed");
    } else {
        Serial.println("rmdir failed");
    }
}

void readFile(fs::FS &fs, const char * path){
    Serial.printf("Reading file: %s\n", path);

    File file = fs.open(path);
    if(!file){
        Serial.println("Failed to open file for reading");
        return;
    }

    Serial.print("Read from file: ");
    while(file.available()){
        Serial.write(file.read());
    }
    file.close();
}

void writeFile(fs::FS &fs, const char * path, const char * message){
    Serial.printf("Writing file: %s\n", path);

    File file = fs.open(path, FILE_WRITE);
    if(!file){
        Serial.println("Failed to open file for writing");
        return;
    }
    if(file.print(message)){
        Serial.println("File written");
    } else {
        Serial.println("Write failed");
    }
    file.close();
}

void appendFile(fs::FS &fs, const char * path, const char * message){
    Serial.printf("Appending to file: %s\n", path);

    File file = fs.open(path, FILE_APPEND);
    if(!file){
        Serial.println("Failed to open file for appending");
        return;
    }
    if(file.print(message)){
        Serial.println("Message appended");
    } else {
        Serial.println("Append failed");
    }
    file.close();
}

void renameFile(fs::FS &fs, const char * path1, const char * path2){
    Serial.printf("Renaming file %s to %s\n", path1, path2);
    if (fs.rename(path1, path2)) {
        Serial.println("File renamed");
    } else {
        Serial.println("Rename failed");
    }
}

void deleteFile(fs::FS &fs, const char * path){
    Serial.printf("Deleting file: %s\n", path);
    if(fs.remove(path)){
        Serial.println("File deleted");
    } else {
        Serial.println("Delete failed");
    }
}

void setup(){
    Serial.begin(115200);
    // We start by connecting to a WiFi network
    Serial.println();
    Serial.println();
    Serial.print("Connecting to ");
    Serial.println(ssid);

    WiFi.begin(ssid, password);

    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
    }
    Serial.println("WiFi connected");
    Serial.println("IP address: ");
    Serial.println(WiFi.localIP());
    Serial.println("Contacting Time Server");
	configTime(3600*timezone, daysavetime*3600, "time.nist.gov", "0.pool.ntp.org", "1.pool.ntp.org");
	struct tm tmstruct ;
    delay(2000);
    tmstruct.tm_year = 0;
    getLocalTime(&tmstruct, 5000);
	Serial.printf("\nNow is : %d-%02d-%02d %02d:%02d:%02d\n",(tmstruct.tm_year)+1900,( tmstruct.tm_mon)+1, tmstruct.tm_mday,tmstruct.tm_hour , tmstruct.tm_min, tmstruct.tm_sec);
    Serial.println("");
    
    if(!SPIFFS.begin()){
        Serial.println("Card Mount Failed");
        return;
    }

    listDir(SPIFFS, "/", 0);
    removeDir(SPIFFS, "/mydir");
    createDir(SPIFFS, "/mydir");
    deleteFile(SPIFFS, "/hello.txt");
    writeFile(SPIFFS, "/hello.txt", "Hello ");
    appendFile(SPIFFS, "/hello.txt", "World!\n");
	listDir(SPIFFS, "/", 0);
}

void loop(){

}

例程通过 WiFi 获取 NTP 服务器当前时间,然后新建 hello.txt 文件,将时间戳写入到文件中。

串口打印信息

Connecting to dfrobotOffice
.......WiFi connected
IP address: 
192.168.0.221
Contacting Time Server

Now is : 2021-12-23 09:29:15

Listing directory: /
  FILE: /hello.txt  SIZE: 13  LAST WRITE: 2021-12-23 09:24:16
Removing Dir: /mydir
rmdir failed
Creating Dir: /mydir
Dir created
Deleting file: /hello.txt
File deleted
Writing file: /hello.txt
File written
Appending to file: /hello.txt
Message appended
Listing directory: /
  FILE: /hello.txt  SIZE: 13  LAST WRITE: 2021-12-23 09:29:16

12.4 LittleFS 文件系统

Github地址为:https://github.com/lorol/LITTLEFS

LittleFS 相比 SPIFFS,它支持真实目录,并且对于大多数操作而言,速度要快许多倍。推荐新项目使用 LitteFS 文件系统。

LittleFS 实现支持最多 31 个字符的文件名+零结尾(即 ‘\0’),并在空间允许的情况下提供尽可能多的子目录。

如果不存在首字母“ /”,则假定文件名位于根目录中。

在子目录中打开文件需要指定文件的完整路径(即open(”/sub/dir/file.txt”);)。当您尝试在子目录中创建文件时,会自动创建子目录;当删除子目录中的最后一个文件时,会自动删除子目录本身。这是因为 mkdir() 现有的 SPIFFS 文件系统中没有任何方法。

与 SPIFFS 不同,实际文件描述符是根据应用程序的请求分配的,因此在内存不足的情况下,您可能无法打开新文件。相反,这也意味着只有使用的文件描述符实际上会在堆上占用空间。

由于存在目录,因此该 openDir 方法的行为不同于 SPIFFS。当您遍历 a 时,SPIFFS 将返回“子目录”中的文件 Dir::next()(因为它们实际上不是子目录,而只是名称中带有“ /”的文件),而 LittleFS 仅返回特定子目录中的文件。这模仿了大多数 C 程序员习惯的目录遍历的 POSIX 行为。

API参考

从 SPIFFS 转换为 LittleFS ,只需更改 SPIFFS.begin()toLittleFS.begin()SPIFFS.open()to LittleFS.open(),而其余代码保持不变。

API 的使用请参考 SPIFFS 部分。

Arduino ESP32文件系统上传器介绍

地址:https://github.com/lorol/arduino-esp32fs-plugin

  • Arduino plugin 可将Sketch数据文件夹打包到 SPIFFS,LittleFS 或 FatFS 文件系统 image 中,并将 image 上传到 ESP32 闪存中。

  • 添加了自定义**“ partition.csv”**文件处理(如果它位于 Sketch 文件夹中)。

  • 添加了基于 Arduino IDE 选择的 esp32 / esp32s2 芯片检测。

  • 在“擦除所有闪存”中添加了一个选项。

  • 同一 Arduino 项目上只能有三个文件系统之一作为数据分区。

  • 请参阅Mac OS的Bergahl说明

SPIFFS的注意事项

  • 这是在 esp-32 核心中为 / data文件夹实现的默认文件系统

  • 转到 Arduino IDE 菜单:***工具>分区方案,***然后选择带有 SPIFFS 分区的条目

LittleFS的注意事项

  • 与 SPIFFS 相同的分区方案

  • 在完全实现到 esp-32 内核之前,它需要一个额外的库。 已经考虑将其用于下一个核心版本。mklittlefs 工具从那里提供。

  • 有关参考,请参阅LITTLEFS esp32库以获取更多详细信息

  • 如果您需要mklittlefs工具,请下载该版本在此处找到以前版本中的存档。

  • mklittlefs [.exe] 复制到 espotaesptool(.py或.exe)工具所在的 esp32 平台的 / tools文件夹中

FatFS的注意事项

  • 转到 Arduino IDE 菜单:***工具>分区方案,***然后选择具有 FAT 分区的条目

  • 如果内核未提供,则可能需要用于 Windows 或 Linux 的其他二进制文件,感谢@lbernstone进行编译-或从此处获取它们-mkfatfs工具,感谢labplus-cn或从此处存档的先前发行版中获取

  • 如果丢失,则需要将 mkfatfs [.exe]复制到espotaesptool(.py或.exe)工具所在的 esp32 平台的 / tools 文件夹中

  • FAT 分区的可用大小减少了1个4096字节(0x1000)的扇区,以解决磨损均衡空间的要求。映像文件以 csv表条目的分区地址偏移+4096字节(0x1000)刷新

  • 您可能需要在草图的 FFat.begin()处减少 maxOpenFiles请参阅此注释

FFAT 模块使用每个打开的并发文件8KB加上4KB。默认情况下,它允许打开10个文件,这意味着它使用48KB。如果要减少其内存使用量,则可以告诉它仅支持一个文件,这样可以节省36KB,而仅使用12KB。

if (!FFat.begin(0, "", 1)) die("Fat FS mount failed. Not enough RAM?");
  • 要通过网络端口将数据文件夹作为 FAT 分区刷新(使用espota),请在此处将 esp32-core 更新库替换为修改后的文件

安装

  • 确保使用受支持的 Arduino IDE 版本之一,并已安装 ESP32 内核。

  • 最新版本下载esp32fs.zip压缩工具

    image-20210513173004079

  • 在您的 Arduino sketchbook 目录中,创建一个 tools 目录(如果尚不存在)。

  • 将工具解压缩到 “ Arduino安装目录-> Arduino” / tools目录中。例如:<home_dir>/Arduino/tools/ESP32FS/tool/esp32fs.jar

    image-20210513180126454

    或在OSX上/Applications/Arduino.app/Contents/Java/tools/ESP32FS

  • 确保在 esp32 核心安装文件夹中有 mklittlefs [.exe]mkfatfs [.exe]。在**\ AppData \ Local \ Arduino15 …内部**或在 zip IDE 安装上查看,请参阅 “Setup->sketchbook location”hardware\espressif\esp32\tools

  • 作为参考,请参阅以前的发行版,以获取有关已归档二进制文件的副本。

  • 您还可以使用提供的 package_esp32_index.template.json 来运行 get.py 并下载缺少的二进制文件

  • 重新启动 Arduino IDE。

用法

  • 打开一个 sketch(或创建一个新sketch并保存)。

  • 转到 sketch目录(选择“sketch”>“Show Sketch Folder”)。

  • 在该文件系统中创建一个名为data的目录,里面存放想要存储到文件系统的文件。

    image-20210513181521525

  • 确保已选择 board,port,分区方案(选择带 SPIFFS 的选项),并关闭了串行监视器。

  • 选择 Tools > ESP32 Sketch Data Upload菜单项。

    image-20210513181021479

  • 在下拉列表中,从 / data文件夹中选择要创建的 SPIFFS,LittleFS或FatFS。这里选择 LittleFS。

    image-20210513181125125

  • 单击“确定”应开始将文件上传到 ESP32 Flash 文件系统中。

  • 最后还有一个 !Erase Flash!选项允许在必要时清除整个flash,请谨慎使用。

    完成后,IDE状态栏将显示“图像已上传”消息的状态。对于大型文件系统,可能需要几分钟的时间。

例程:LittleFS_test

注意:下载例程时 Partition Scheme 分区方案 选择带 SPIFFS 系统的分区

(参考Arduino IDE例程 Examples -> Examples for Edge101WE ->LITTLEFS\examples\LITTLEFS_test)

#include <Arduino.h>
#include "FS.h"
#include <LITTLEFS.h>

/* You only need to format LITTLEFS the first time you run a
   test or else use the LITTLEFS plugin to create a partition
   https://github.com/lorol/arduino-esp32littlefs-plugin
   
   If you test two partitions, you need to use a custom
   partition.csv file, see in the sketch folder */

//#define TWOPART

#define FORMAT_LITTLEFS_IF_FAILED true

void listDir(fs::FS &fs, const char * dirname, uint8_t levels){
    Serial.printf("Listing directory: %s\r\n", dirname);

    File root = fs.open(dirname);
    if(!root){
        Serial.println("- failed to open directory");
        return;
    }
    if(!root.isDirectory()){
        Serial.println(" - not a directory");
        return;
    }

    File file = root.openNextFile();
    while(file){
        if(file.isDirectory()){
            Serial.print("  DIR : ");
            Serial.println(file.name());
            if(levels){
                listDir(fs, file.path(), levels -1);
            }
        } else {
            Serial.print("  FILE: ");
            Serial.print(file.name());
            Serial.print("\tSIZE: ");
            Serial.println(file.size());
        }
        file = root.openNextFile();
    }
}

void createDir(fs::FS &fs, const char * path){
    Serial.printf("Creating Dir: %s\n", path);
    if(fs.mkdir(path)){
        Serial.println("Dir created");
    } else {
        Serial.println("mkdir failed");
    }
}

void removeDir(fs::FS &fs, const char * path){
    Serial.printf("Removing Dir: %s\n", path);
    if(fs.rmdir(path)){
        Serial.println("Dir removed");
    } else {
        Serial.println("rmdir failed");
    }
}

void readFile(fs::FS &fs, const char * path){
    Serial.printf("Reading file: %s\r\n", path);

    File file = fs.open(path);
    if(!file || file.isDirectory()){
        Serial.println("- failed to open file for reading");
        return;
    }

    Serial.println("- read from file:");
    while(file.available()){
        Serial.write(file.read());
    }
    file.close();
}

void writeFile(fs::FS &fs, const char * path, const char * message){
    Serial.printf("Writing file: %s\r\n", path);

    File file = fs.open(path, FILE_WRITE);
    if(!file){
        Serial.println("- failed to open file for writing");
        return;
    }
    if(file.print(message)){
        Serial.println("- file written");
    } else {
        Serial.println("- write failed");
    }
    file.close();
}

void appendFile(fs::FS &fs, const char * path, const char * message){
    Serial.printf("Appending to file: %s\r\n", path);

    File file = fs.open(path, FILE_APPEND);
    if(!file){
        Serial.println("- failed to open file for appending");
        return;
    }
    if(file.print(message)){
        Serial.println("- message appended");
    } else {
        Serial.println("- append failed");
    }
    file.close();
}

void renameFile(fs::FS &fs, const char * path1, const char * path2){
    Serial.printf("Renaming file %s to %s\r\n", path1, path2);
    if (fs.rename(path1, path2)) {
        Serial.println("- file renamed");
    } else {
        Serial.println("- rename failed");
    }
}

void deleteFile(fs::FS &fs, const char * path){
    Serial.printf("Deleting file: %s\r\n", path);
    if(fs.remove(path)){
        Serial.println("- file deleted");
    } else {
        Serial.println("- delete failed");
    }
}

// SPIFFS-like write and delete file, better use #define CONFIG_LITTLEFS_SPIFFS_COMPAT 1

void writeFile2(fs::FS &fs, const char * path, const char * message){
    if(!fs.exists(path)){
		if (strchr(path, '/')) {
            Serial.printf("Create missing folders of: %s\r\n", path);
			char *pathStr = strdup(path);
			if (pathStr) {
				char *ptr = strchr(pathStr, '/');
				while (ptr) {
					*ptr = 0;
					fs.mkdir(pathStr);
					*ptr = '/';
					ptr = strchr(ptr+1, '/');
				}
			}
			free(pathStr);
		}
    }

    Serial.printf("Writing file to: %s\r\n", path);
    File file = fs.open(path, FILE_WRITE);
    if(!file){
        Serial.println("- failed to open file for writing");
        return;
    }
    if(file.print(message)){
        Serial.println("- file written");
    } else {
        Serial.println("- write failed");
    }
    file.close();
}

void deleteFile2(fs::FS &fs, const char * path){
    Serial.printf("Deleting file and empty folders on path: %s\r\n", path);

    if(fs.remove(path)){
        Serial.println("- file deleted");
    } else {
        Serial.println("- delete failed");
    }

    char *pathStr = strdup(path);
    if (pathStr) {
        char *ptr = strrchr(pathStr, '/');
        if (ptr) {
            Serial.printf("Removing all empty folders on path: %s\r\n", path);
        }
        while (ptr) {
            *ptr = 0;
            fs.rmdir(pathStr);
            ptr = strrchr(pathStr, '/');
        }
        free(pathStr);
    }
}

void testFileIO(fs::FS &fs, const char * path){
    Serial.printf("Testing file I/O with %s\r\n", path);

    static uint8_t buf[512];
    size_t len = 0;
    File file = fs.open(path, FILE_WRITE);
    if(!file){
        Serial.println("- failed to open file for writing");
        return;
    }

    size_t i;
    Serial.print("- writing" );
    uint32_t start = millis();
    for(i=0; i<2048; i++){
        if ((i & 0x001F) == 0x001F){
          Serial.print(".");
        }
        file.write(buf, 512);
    }
    Serial.println("");
    uint32_t end = millis() - start;
    Serial.printf(" - %u bytes written in %u ms\r\n", 2048 * 512, end);
    file.close();

    file = fs.open(path);
    start = millis();
    end = start;
    i = 0;
    if(file && !file.isDirectory()){
        len = file.size();
        size_t flen = len;
        start = millis();
        Serial.print("- reading" );
        while(len){
            size_t toRead = len;
            if(toRead > 512){
                toRead = 512;
            }
            file.read(buf, toRead);
            if ((i++ & 0x001F) == 0x001F){
              Serial.print(".");
            }
            len -= toRead;
        }
        Serial.println("");
        end = millis() - start;
        Serial.printf("- %u bytes read in %u ms\r\n", flen, end);
        file.close();
    } else {
        Serial.println("- failed to open file for reading");
    }
}

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

#ifdef TWOPART
    if(!LITTLEFS.begin(FORMAT_LITTLEFS_IF_FAILED, "/lfs2", 5, "part2")){
    Serial.println("part2 Mount Failed");
    return;
    }
    appendFile(LITTLEFS, "/hello0.txt", "World0!\r\n");
    readFile(LITTLEFS, "/hello0.txt");
    LITTLEFS.end();

    Serial.println( "Done with part2, work with the first lfs partition..." );
#endif

    if(!LITTLEFS.begin(FORMAT_LITTLEFS_IF_FAILED)){
        Serial.println("LITTLEFS Mount Failed");
        return;
    }
    Serial.println( "SPIFFS-like write file to new path and delete it w/folders" );
    writeFile2(LITTLEFS, "/new1/new2/new3/hello3.txt", "Hello3");
    listDir(LITTLEFS, "/", 3);
    deleteFile2(LITTLEFS, "/new1/new2/new3/hello3.txt");
    
    listDir(LITTLEFS, "/", 3);
	createDir(LITTLEFS, "/mydir");
	writeFile(LITTLEFS, "/mydir/hello2.txt", "Hello2");
	listDir(LITTLEFS, "/", 1);
	deleteFile(LITTLEFS, "/mydir/hello2.txt");
	removeDir(LITTLEFS, "/mydir");
	listDir(LITTLEFS, "/", 1);
    writeFile(LITTLEFS, "/hello.txt", "Hello ");
    appendFile(LITTLEFS, "/hello.txt", "World!\r\n");
    readFile(LITTLEFS, "/hello.txt");
    renameFile(LITTLEFS, "/hello.txt", "/foo.txt");
    readFile(LITTLEFS, "/foo.txt");
    deleteFile(LITTLEFS, "/foo.txt");
    testFileIO(LITTLEFS, "/test.txt");
    deleteFile(LITTLEFS, "/test.txt");
	
    Serial.println( "Test complete" ); 
}

void loop(){

}

从串口打印出新建文件的信息,说明文件已经保存到文件系统

SPIFFS-like write file to new path and delete it w/folders
Create missing folders of: /new1/new2/new3/hello3.txt
Writing file to: /new1/new2/new3/hello3.txt
- file written
Listing directory: /
  FILE: firebeetle.txt	SIZE: 15

12.5 SD SPI Host 驱动程序

Edge101WE 主板可以使用SPI接口访问 SD 卡,占用用4个IO口。

sdCardPin

SPI接线

SD卡SPI引脚 主板GPIO
DO GPIO12
DI GPIO39
CLK GPIO14
CS GPIO5

SD API参考

SD.begin() - 挂载SD卡

语法

bool begin(uint8_t ssPin=SS, SPIClass &spi=SPI, uint32_t frequency=4000000, const char * mountpoint="/sd", uint8_t max_files=5)
  if (!SD.begin())
  {
    Serial.print(".");
  }
  Serial.println("SD card Ready!");

挂载存储卡,输入参数分别为SS引脚号、SPI对象、时钟频率、挂载点、文件最大同时打开数; 默认IO口连接为:CS - IO5DI - IO39SCLK - IO14DO - IO12

初始化SD库和卡。这开始使用SPI总线和芯片选择引脚,该引脚默认为硬件SS引脚。成功返回true;失败时为假。

参数

传入值 说明 值范围
ssPin 连接到SD卡芯片选择线的引脚(默认为SPI总线的硬件SS线)
spi SPI对象(默认 SPI)
frequency 时钟频率(默认 4000000)
mountpoint 挂载点(默认 "/sd")
max_files 文件最大同时打开数(默认 5)

返回

返回值 说明 值范围
bool true:挂载成功。
false:挂载失败

SD.end() - 取消SD卡挂载

void end()

SD.cardSize() - 获取卡的大小

返回存储卡大小字节数,返回值是个64位数值

uint64_t cardSize()
Serial.printf("SD.cardSize = %lld \r\n", SD.cardSize());

SD.cardType() - 获取卡的类型

sdcard_type_t cardType()

返回值:sdcard_type_t类型 该类型定义如下:

typedef enum {
	CARD_NONE,
	CARD_MMC,
	CARD_SD,
	CARD_SDHC,
	CARD_UNKNOWN,
} sdcard_type_t;
返回值 枚举值 说明
0 CARD_NONE 未连接存储卡;
1 CARD_MMC mmc卡;
2 CARD_SD sd卡,最大2G;
3 CARD_SDHC sdhc卡,最大32G;
4 CARD_UNKNOWN 未知存储卡;
Serial.printf("SD.cardType = %d \r\n", SD.cardType());

SD.exists(”/test.txt”) - 是否存在文件

测试SD卡上是否存在文件或目录。如果文件或目录存在,则返回true,否则返回false。

SD.mkdir(”/doc1”) - 创建目录;

在SD卡上创建目录。这还将创建任何尚不存在的中间目录。例如SD.mkdir(“ a / b / c”)将创建a,b和c。如果目录创建成功,则返回true;否则,返回false。

SD.rmdir(”/doc1”) - 删除目录

从SD卡中删除目录。该目录必须为空。如果删除目录成功,则返回true;否则,返回false。(如果目录不存在,则返回值未指定)

SD.remove(filename) - 删除文件

SD.remove("/test.txt")  

从SD卡中删除文件。如果删除文件成功,则返回true;否则,返回false。(如果文件不存在,则返回值未指定)

SD.open() - 创建/打开文件

SD.open(filepath) 

SD.open(filepath, mode)

打开SD卡上的文件。如果打开该文件进行写入,则将创建该文件(如果尚不存在)(但是包含该文件的目录必须已经存在)。参数模式(可选):打开文件的模式,默认为FILE_READ- byte。FILE_READ之一:从文件的开头开始打开文件进行读取。FILE_WRITE:从文件末尾开始打开文件进行读写。返回引用打开的文件的File对象;如果无法打开文件,则此对象在布尔上下文中将评估为false,即,您可以使用“ if(f)”测试返回值。

File file = SD.open("/test.txt", FILE_WRITE);

SD.rename(filenameFrom, filenameTo) - 重命名

SD.rename("/doc1","/doc") 

重命名或移动SD卡中的文件。如果重命名工作则返回true,否则返回false

SD.totalBytes()

返回在SD上启用的总字节数。返回字节。

SD.usedBytes()

返回在SD上启用的已使用字节总数。返回字节。

SD.cardSize()

返回SD的大小。返回字节

FS API参考

file.name()

返回文件名

file.available()

检查是否有任何字节可用于从文件读取。返回字节数。

file.close()

关闭文件,并确保将写入其中的所有数据物理保存到SD卡。

file.flush()

确保写入文件的所有字节都物理保存到SD卡中。关闭文件后,此操作会自动完成。

file.peek()

从文件读取一个字节,而不会前进到下一个字节。也就是说,对peek()的连续调用将返回相同的值,与对下一个read()的调用相同。

file.position()

获取文件中的当前位置(即,下一个字节将被读取或写入的位置)。返回文件中的位置(unsigned long)。

file.print(data)

file.print(data,base)

将数据打印到文件中,该文件必须已打开才能进行写入。将数字打印为数字序列,每个数字为一个ASCII字符(例如,数字123作为三个字符“ 1”,“ 2”,“ 3”发送)。参数数据:要打印的数据(字符,字节,整数,长整数或字符串),BASE(可选):要打印数字的基数:BIN(对于二进制)(以2为底),DEC(十进制)(以10为底),OCT代表八进制(基数8),十六进制代表十六进制(基数16)。返回写入的字节数,尽管读取该数字是可选的。

file.println()

file.println(data)

file.println(data,base)

作为打印但最终返回

file.seek(pos)

在文件中寻找新位置,该位置必须在0到文件的大小(含)之间。参数:pos:要搜索的位置(无符号long)。如果成功,则返回true;如果失败,则返回false(布尔值)

file.size()

获取文件的大小。返回文件的大小(以字节为单位)(无符号long)。

file .read()

file.read(buf,len)

从文件读取。返回下一个字节(或字符);如果没有可用字节,则返回-1。

file .write(数据)

file .write(buf,len)

将数据写入文件。返回写入的字节数,尽管读取该数字是可选的

file.isDirectory()

目录(或文件夹)是特殊类型的文件,此功能报告当前文件是否为目录。如果是目录,则返回true。

file.openNextFile()

报告目录中的下一个文件或文件夹。返回路径中的下一个文件或文件夹。

file.rewindDirectory()

将带您回到目录中的第一个文件,与openNextFile()结合使用。

file.getLastWrite()

返回时期中最后一次写入/更改的数据。

例程: SD卡常规操作

注意:下载例程时 Partition Scheme 分区方案 选择带FAT系统的分区。

#include <Arduino.h>
#include "SD.h"

void setup()
{
  Serial.begin(115200);
  if (!SD.begin(5)) // GPIO5连接SD卡CS 
  {
    Serial.print(".");
  }
  Serial.println("SD card Ready!");
  Serial.printf("SD.cardSize = %lld \r\n", SD.cardSize());
  Serial.printf("SD.totalBytes = %lld \r\n", SD.totalBytes());
  Serial.printf("SD.usedBytes = %lld \r\n", SD.usedBytes());
  Serial.printf("SD.cardType = %d \r\n", SD.cardType());
  Serial.printf("is there /test.txt? :%d \r\n", SD.exists("/sd/doc1/test.txt"));
  Serial.println(SD.mkdir("/doc1"));
  Serial.printf("is there /doc1? :%d \r\n", SD.exists("/doc1"));
  Serial.printf("is there /test.txt? :%d \r\n", SD.exists("/test.txt"));
  File file = SD.open("/test.txt", FILE_WRITE);
  Serial.printf("is there /test.txt? :%d \r\n", SD.exists("/test.txt"));
  file.printf("hello!!!");
  file.close();
  file = SD.open("/test.txt", FILE_READ);
  Serial.println(file.readString());
  file.close();
  Serial.printf("is there /doc1/test1.txt? :%d \r\n", SD.exists("/doc1/test1.txt"));
  File file2 = SD.open("/doc1/test1.txt", FILE_WRITE);
  Serial.printf("is there /doc1/test1.txt? :%d \r\n", SD.exists("/doc1/test1.txt"));
  file2.printf("hello!!!");
  file2.close();
  file2 = SD.open("/test.txt", FILE_READ);
  Serial.println(file2.readString());
  file2.close();
  SD.end();
}

void loop()
{
}

串口打印信息如下:

SD card Ready!
SD.cardSize = 1990197248 
SD.totalBytes = 1989902336 
SD.usedBytes = 294912 
SD.cardType = 2 
is there /test.txt? :0 
1
is there /doc1? :1 
is there /test.txt? :1 
is there /test.txt? :1 
hello!!!
is there /doc1/test1.txt? :1 
is there /doc1/test1.txt? :1 
hello!!!

例程:SD_Test

注意:下载例程时 Partition Scheme 分区方案 选择带FAT系统的分区。

(参考Arduino IDE例程 Examples -> Examples for Edge101WE ->SD/examples/SD_Test)

/*
 * Connect the SD card to the following pins:
 *
 * SD Card | FireBeetle MESH 
 *    D2       -
 *    D3       GPIO5
 *    CMD      MOSI
 *    VSS      GND
 *    VDD      3.3V
 *    CLK      SCK
 *    VSS      GND
 *    D0       MISO
 *    D1       -
 */
#include "FS.h"
#include "SD.h"
#include "SPI.h"

void listDir(fs::FS &fs, const char * dirname, uint8_t levels){
    Serial.printf("Listing directory: %s\n", dirname);

    File root = fs.open(dirname);
    if(!root){
        Serial.println("Failed to open directory");
        return;
    }
    if(!root.isDirectory()){
        Serial.println("Not a directory");
        return;
    }

    File file = root.openNextFile();
    while(file){
        if(file.isDirectory()){
            Serial.print("  DIR : ");
            Serial.println(file.name());
            if(levels){
                listDir(fs, file.name(), levels -1);
            }
        } else {
            Serial.print("  FILE: ");
            Serial.print(file.name());
            Serial.print("  SIZE: ");
            Serial.println(file.size());
        }
        file = root.openNextFile();
    }
}

void createDir(fs::FS &fs, const char * path){
    Serial.printf("Creating Dir: %s\n", path);
    if(fs.mkdir(path)){
        Serial.println("Dir created");
    } else {
        Serial.println("mkdir failed");
    }
}

void removeDir(fs::FS &fs, const char * path){
    Serial.printf("Removing Dir: %s\n", path);
    if(fs.rmdir(path)){
        Serial.println("Dir removed");
    } else {
        Serial.println("rmdir failed");
    }
}

void readFile(fs::FS &fs, const char * path){
    Serial.printf("Reading file: %s\n", path);

    File file = fs.open(path);
    if(!file){
        Serial.println("Failed to open file for reading");
        return;
    }

    Serial.print("Read from file: ");
    while(file.available()){
        Serial.write(file.read());
    }
    file.close();
}

void writeFile(fs::FS &fs, const char * path, const char * message){
    Serial.printf("Writing file: %s\n", path);

    File file = fs.open(path, FILE_WRITE);
    if(!file){
        Serial.println("Failed to open file for writing");
        return;
    }
    if(file.print(message)){
        Serial.println("File written");
    } else {
        Serial.println("Write failed");
    }
    file.close();
}

void appendFile(fs::FS &fs, const char * path, const char * message){
    Serial.printf("Appending to file: %s\n", path);

    File file = fs.open(path, FILE_APPEND);
    if(!file){
        Serial.println("Failed to open file for appending");
        return;
    }
    if(file.print(message)){
        Serial.println("Message appended");
    } else {
        Serial.println("Append failed");
    }
    file.close();
}

void renameFile(fs::FS &fs, const char * path1, const char * path2){
    Serial.printf("Renaming file %s to %s\n", path1, path2);
    if (fs.rename(path1, path2)) {
        Serial.println("File renamed");
    } else {
        Serial.println("Rename failed");
    }
}

void deleteFile(fs::FS &fs, const char * path){
    Serial.printf("Deleting file: %s\n", path);
    if(fs.remove(path)){
        Serial.println("File deleted");
    } else {
        Serial.println("Delete failed");
    }
}

void testFileIO(fs::FS &fs, const char * path){
    File file = fs.open(path);
    static uint8_t buf[512];
    size_t len = 0;
    uint32_t start = millis();
    uint32_t end = start;
    if(file){
        len = file.size();
        size_t flen = len;
        start = millis();
        while(len){
            size_t toRead = len;
            if(toRead > 512){
                toRead = 512;
            }
            file.read(buf, toRead);
            len -= toRead;
        }
        end = millis() - start;
        Serial.printf("%u bytes read for %u ms\n", flen, end);
        file.close();
    } else {
        Serial.println("Failed to open file for reading");
    }


    file = fs.open(path, FILE_WRITE);
    if(!file){
        Serial.println("Failed to open file for writing");
        return;
    }

    size_t i;
    start = millis();
    for(i=0; i<2048; i++){
        file.write(buf, 512);
    }
    end = millis() - start;
    Serial.printf("%u bytes written for %u ms\n", 2048 * 512, end);
    file.close();
}

void setup(){
    Serial.begin(115200);
    // GPIO5连接SD卡CS 
    if(!SD.begin(5)){
        Serial.println("Card Mount Failed");
        return;
    }
    uint8_t cardType = SD.cardType();

    if(cardType == CARD_NONE){
        Serial.println("No SD card attached");
        return;
    }

    Serial.print("SD Card Type: ");
    if(cardType == CARD_MMC){
        Serial.println("MMC");
    } else if(cardType == CARD_SD){
        Serial.println("SDSC");
    } else if(cardType == CARD_SDHC){
        Serial.println("SDHC");
    } else {
        Serial.println("UNKNOWN");
    }

    uint64_t cardSize = SD.cardSize() / (1024 * 1024);
    Serial.printf("SD Card Size: %lluMB\n", cardSize);

    listDir(SD, "/", 0);
    createDir(SD, "/mydir");
    listDir(SD, "/", 0);
    removeDir(SD, "/mydir");
    listDir(SD, "/", 2);
    writeFile(SD, "/hello.txt", "Hello ");
    appendFile(SD, "/hello.txt", "World!\n");
    readFile(SD, "/hello.txt");
    deleteFile(SD, "/foo.txt");
    renameFile(SD, "/hello.txt", "/foo.txt");
    readFile(SD, "/foo.txt");
    testFileIO(SD, "/test.txt");
    Serial.printf("Total space: %lluMB\n", SD.totalBytes() / (1024 * 1024));
    Serial.printf("Used space: %lluMB\n", SD.usedBytes() / (1024 * 1024));
}

void loop(){

}

例程首先挂载SD卡,如果挂载失败会打印错误,然后检测SD卡类型。接下来对SD卡进行一系列的操作。

串口打印类如如下:

SD Card Type: SDSC
SD Card Size: 1898MB
Listing directory: /
  DIR : /System Volume Information
Creating Dir: /mydir
Dir created
Listing directory: /
  DIR : /mydir
  DIR : /System Volume Information
Removing Dir: /mydir
Dir removed
Listing directory: /
  DIR : /System Volume Information
Listing directory: /System Volume Information
  FILE: /System Volume Information/WPSettings.dat  SIZE: 12
  FILE: /System Volume Information/IndexerVolumeGuid  SIZE: 76
Writing file: /hello.txt
File written
Appending to file: /hello.txt
Message appended
Reading file: /hello.txt
Read from file: Hello World!
Deleting file: /foo.txt
Delete failed
Renaming file /hello.txt to /foo.txt
File renamed
Reading file: /foo.txt
Read from file: Hello World!
Failed to open file for reading
1048576 bytes written for 5804 ms
Total space: 1897MB
Used space: 1MB

例程:SD_time

注意:下载例程时 Partition Scheme 分区方案 选择带FAT系统的分区。

(参考Arduino IDE例程 Examples -> Examples for Edge101WE ->SD/examples/SD_time)

/*
 * Connect the SD card to the following pins:
 *
 * SD Card | FireBeetle MESH 
 *    D2       -
 *    D3       GPIO5
 *    CMD      MOSI
 *    VSS      GND
 *    VDD      3.3V
 *    CLK      SCK
 *    VSS      GND
 *    D0       MISO
 *    D1       -
 */

#include "FS.h"
#include "SD.h"
#include "SPI.h"
#include <time.h> 
#include <WiFi.h>

const char* ssid     = "your-ssid";
const char* password = "your-password";

long timezone = 8; 	// 中国时区 8
byte daysavetime = 0; //夏令时 0

void listDir(fs::FS &fs, const char * dirname, uint8_t levels){
    Serial.printf("Listing directory: %s\n", dirname);

    File root = fs.open(dirname);
    if(!root){
        Serial.println("Failed to open directory");
        return;
    }
    if(!root.isDirectory()){
        Serial.println("Not a directory");
        return;
    }

    File file = root.openNextFile();
    while(file){
        if(file.isDirectory()){
            Serial.print("  DIR : ");
            Serial.print (file.name());
            time_t t= file.getLastWrite();
            struct tm * tmstruct = localtime(&t);
            Serial.printf("  LAST WRITE: %d-%02d-%02d %02d:%02d:%02d\n",(tmstruct->tm_year)+1900,( tmstruct->tm_mon)+1, tmstruct->tm_mday,tmstruct->tm_hour , tmstruct->tm_min, tmstruct->tm_sec);
            if(levels){
                listDir(fs, file.name(), levels -1);
            }
        } else {
            Serial.print("  FILE: ");
            Serial.print(file.name());
            Serial.print("  SIZE: ");
            Serial.print(file.size());
            time_t t= file.getLastWrite();
            struct tm * tmstruct = localtime(&t);
            Serial.printf("  LAST WRITE: %d-%02d-%02d %02d:%02d:%02d\n",(tmstruct->tm_year)+1900,( tmstruct->tm_mon)+1, tmstruct->tm_mday,tmstruct->tm_hour , tmstruct->tm_min, tmstruct->tm_sec);
        }
        file = root.openNextFile();
    }
}

void createDir(fs::FS &fs, const char * path){
    Serial.printf("Creating Dir: %s\n", path);
    if(fs.mkdir(path)){
        Serial.println("Dir created");
    } else {
        Serial.println("mkdir failed");
    }
}

void removeDir(fs::FS &fs, const char * path){
    Serial.printf("Removing Dir: %s\n", path);
    if(fs.rmdir(path)){
        Serial.println("Dir removed");
    } else {
        Serial.println("rmdir failed");
    }
}

void readFile(fs::FS &fs, const char * path){
    Serial.printf("Reading file: %s\n", path);

    File file = fs.open(path);
    if(!file){
        Serial.println("Failed to open file for reading");
        return;
    }

    Serial.print("Read from file: ");
    while(file.available()){
        Serial.write(file.read());
    }
    file.close();
}

void writeFile(fs::FS &fs, const char * path, const char * message){
    Serial.printf("Writing file: %s\n", path);

    File file = fs.open(path, FILE_WRITE);
    if(!file){
        Serial.println("Failed to open file for writing");
        return;
    }
    if(file.print(message)){
        Serial.println("File written");
    } else {
        Serial.println("Write failed");
    }
    file.close();
}

void appendFile(fs::FS &fs, const char * path, const char * message){
    Serial.printf("Appending to file: %s\n", path);

    File file = fs.open(path, FILE_APPEND);
    if(!file){
        Serial.println("Failed to open file for appending");
        return;
    }
    if(file.print(message)){
        Serial.println("Message appended");
    } else {
        Serial.println("Append failed");
    }
    file.close();
}

void renameFile(fs::FS &fs, const char * path1, const char * path2){
    Serial.printf("Renaming file %s to %s\n", path1, path2);
    if (fs.rename(path1, path2)) {
        Serial.println("File renamed");
    } else {
        Serial.println("Rename failed");
    }
}

void deleteFile(fs::FS &fs, const char * path){
    Serial.printf("Deleting file: %s\n", path);
    if(fs.remove(path)){
        Serial.println("File deleted");
    } else {
        Serial.println("Delete failed");
    }
}

void setup(){
    Serial.begin(115200);
    // We start by connecting to a WiFi network
    Serial.println();
    Serial.println();
    Serial.print("Connecting to ");
    Serial.println(ssid);

    WiFi.begin(ssid, password);

    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
    }
    Serial.println("WiFi connected");
    Serial.println("IP address: ");
    Serial.println(WiFi.localIP());
    Serial.println("Contacting Time Server");
	configTime(3600*timezone, daysavetime*3600, "time.nist.gov", "0.pool.ntp.org", "1.pool.ntp.org");
	struct tm tmstruct ;
    delay(2000);
    tmstruct.tm_year = 0;
    getLocalTime(&tmstruct, 5000);
	Serial.printf("\nNow is : %d-%02d-%02d %02d:%02d:%02d\n",(tmstruct.tm_year)+1900,( tmstruct.tm_mon)+1, tmstruct.tm_mday,tmstruct.tm_hour , tmstruct.tm_min, tmstruct.tm_sec);
    Serial.println("");
    
    // GPIO5连接SD卡CS 
    if(!SD.begin(5)){
        Serial.println("Card Mount Failed");
        return;
    }
    uint8_t cardType = SD.cardType();

    if(cardType == CARD_NONE){
        Serial.println("No SD card attached");
        return;
    }

    Serial.print("SD Card Type: ");
    if(cardType == CARD_MMC){
        Serial.println("MMC");
    } else if(cardType == CARD_SD){
        Serial.println("SDSC");
    } else if(cardType == CARD_SDHC){
        Serial.println("SDHC");
    } else {
        Serial.println("UNKNOWN");
    }

    uint64_t cardSize = SD.cardSize() / (1024 * 1024);
    Serial.printf("SD Card Size: %lluMB\n", cardSize);

    listDir(SD, "/", 0);
    removeDir(SD, "/mydir");
    createDir(SD, "/mydir");
    deleteFile(SD, "/hello.txt");
    writeFile(SD, "/hello.txt", "Hello ");
    appendFile(SD, "/hello.txt", "World!\n");
	listDir(SD, "/", 0);
}

void loop(){

}

例程将通过WiFi 获取NTP服务器当前时间,然后新建mydir文件夹和hello.txt文件,然后将时间戳写入到文件中。

串口打印信息如下:

Connecting to dfrobotOffice
.......WiFi connected
IP address: 
192.168.0.60
Contacting Time Server

Now is : 2021-08-13 11:26:00

SD Card Type: SDSC
SD Card Size: 1898MB
Listing directory: /
  FILE: /test.txt  SIZE: 1048576  LAST WRITE: 1980-01-01 00:00:06
  FILE: /foo.txt  SIZE: 13  LAST WRITE: 1980-01-01 00:00:00
  DIR : /mydir  LAST WRITE: 2021-08-13 05:22:28
  FILE: /hello.txt  SIZE: 13  LAST WRITE: 2021-08-13 05:22:28
  DIR : /System Volume Information  LAST WRITE: 2018-08-09 15:34:32
Removing Dir: /mydir
Dir removed
Creating Dir: /mydir
Dir created
Deleting file: /hello.txt
File deleted
Writing file: /hello.txt
File written
Appending to file: /hello.txt
Message appended
Listing directory: /
  FILE: /test.txt  SIZE: 1048576  LAST WRITE: 1980-01-01 00:00:06
  FILE: /foo.txt  SIZE: 13  LAST WRITE: 1980-01-01 00:00:00
  DIR : /mydir  LAST WRITE: 2021-08-13 11:26:00
  FILE: /hello.txt  SIZE: 13  LAST WRITE: 2021-08-13 11:26:00
  DIR : /System Volume Information  LAST WRITE: 2018-08-09 15:34:32

12.6 通过SD卡本地更新固件

部署在远端的设备如果不具备联网功能,也可以通过 SD 卡进行本地更新程序,这样可减少跑现场。您只需要将编译好的新版本固件发给客户,客户将固件拷贝到SD 卡中,然后将 SD 卡插入 Edge101WE 主板的 SD卡槽,主板重启后即可更新到新版本的固件。我们需要在程序中增加 SD_Update 的代码功能。

这里我们通过 SD_Update 例程来说明如何实现 SD卡更新固件,首先我们将 SD_Update 烧写到 Edge101WE 主板。

例程:SD_Update

(参考Arduino IDE例程 Examples -> Examples for Edge101WE ->Update\examples\SD_Update)

/* 
 Name:      SD_Update.ino
 Created:   12.09.2017 15:07:17
 Author:    Frederik Merz <frederik.merz@novalight.de>
 Purpose:   Update firmware from SD card

 Steps:
   1. Flash this image to the ESP32 an run it
   2. Copy update.bin to a SD-Card, you can basically
      compile this or any other example
      then copy and rename the app binary to the sd card root
   3. Connect SD-Card as shown in SD example, 
      this can also be adapted for SPI
   3. After successfull update and reboot, ESP32 shall start the new app
*/

#include <Update.h>
#include <FS.h>
#include <SD.h>

// perform the actual update from a given stream
void performUpdate(Stream &updateSource, size_t updateSize) {
   if (Update.begin(updateSize)) {      
      size_t written = Update.writeStream(updateSource);
      if (written == updateSize) {
         Serial.println("Written : " + String(written) + " successfully");
      }
      else {
         Serial.println("Written only : " + String(written) + "/" + String(updateSize) + ". Retry?");
      }
      if (Update.end()) {
         Serial.println("OTA done!");
         if (Update.isFinished()) {
            Serial.println("Update successfully completed. Rebooting.");
         }
         else {
            Serial.println("Update not finished? Something went wrong!");
         }
      }
      else {
         Serial.println("Error Occurred. Error #: " + String(Update.getError()));
      }

   }
   else
   {
      Serial.println("Not enough space to begin OTA");
   }
}

// check given FS for valid update.bin and perform update if available
void updateFromFS(fs::FS &fs) {
   File updateBin = fs.open("/update.bin");
   if (updateBin) {
      if(updateBin.isDirectory()){
         Serial.println("Error, update.bin is not a file");
         updateBin.close();
         return;
      }

      size_t updateSize = updateBin.size();

      if (updateSize > 0) {
         Serial.println("Try to start update");
         performUpdate(updateBin, updateSize);
      }
      else {
         Serial.println("Error, file is empty");
      }

      updateBin.close();
    
      // whe finished remove the binary from sd card to indicate end of the process
      fs.remove("/update.bin");      
   }
   else {
      Serial.println("Could not load update.bin from sd root");
   }
}

void setup() {
   uint8_t cardType;
   Serial.begin(115200);
   Serial.println("Welcome to the SD-Update example!");

   // You can uncomment this and build again
   // Serial.println("Update successfull");

   //first init and check SD card
   if (!SD.begin()) {
      rebootEspWithReason("Card Mount Failed");
   }

   cardType = SD.cardType();

   if (cardType == CARD_NONE) {
      rebootEspWithReason("No SD_MMC card attached");
   }else{
      updateFromFS(SD);
  }
}

void rebootEspWithReason(String reason){
    Serial.println(reason);
    delay(1000);
    ESP.restart();
}

//will not be reached
void loop() {
  
}

修改 SD_Update.ino 代码,在loop循环中增加循环打印的语句。

//will not be reached
void loop() {
  Serial.println("The code has changed");
  delay(500);
}

点击 Sketch - > Export compiled Binary 导出编译后的二进制文件。

image-20210816153513968

我们可以在 gcc编译行后面找到编译后的bin文件地址

image-20210816154140715

"C:\\Users\\DFRobot-DFTV\\AppData\\Local\\Arduino15\\packages\\FireBeetleMESH\\tools\\xtensa-esp32-elf-gcc\\gcc8_4_0-esp-2020r3/bin/xtensa-esp32-elf-size" -A "C:\\Users\\DFROBO~1\\AppData\\Local\\Temp\\arduino_build_822742/SD_Update.ino.elf"

image-20210816154608185

将 SD_Update.ino.bin 文件修改为和 SD_Update.ino 中代码一致的路径和文件名。

下面的代码显示 升级二进制bin文件名称必须是 update.bin 并且放到 SD卡的根目录下。

// check given FS for valid update.bin and perform update if available
void updateFromFS(fs::FS &fs) {
   File updateBin = fs.open("/update.bin");

SD_Update.ino.bin 名称修改为 update.bin,并拷贝到SD卡中。

将SD卡插入到 Edge101WE 主板的SD卡座,按下复位按钮。

升级成功,串口打印的信息如下:

Welcome to the SD-Update example!
Try to start update
Written : 309568 successfully
OTA done!
Update successfully completed. Rebooting.


Welcome to the SD-Update example!
Could not load update.bin from sd root
The code has changed
The code has changed
The code has changed
The code has changed

也可以将SD卡升级和OTA升级功能合并到产品中,这样产品既可以通过SD卡进行升级也可以通过无线进行升级。