# 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 非易失性存储)。 **语法** ```c++ #include 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中删除当前使用的命名空间中的所有键值。 **语法** ```c++ #include bool Preferences::clear(); ``` **参数** 无 **返回** | 返回值 | 说明 | 值范围 | | :----: | :-------------------------------------------------- | ------ | | bool | 执行结果。如果成功,则为true。如果不成功则为false。 | | #### end() - 停止使用NVS **语法** ```c++ #include void Preferences::end(); ``` **参数** 无 **返回** 无 #### Preferences::get() - 获取指定键对应的值 ```c++ 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中获取与当前使用的命名空间中的指定键对应的值。 **语法** ```c++ #include 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中。 **语法** ```c++ #include 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中删除当前使用的命名空间中的指定键/值 **语法** ```c++ #include bool Preferences::remove(const char * key); ``` **参数** | 传入值 | 说明 | 值范围 | | :----: | :----------------------------------------: | :----: | | key | 要删除的数据的键名。最多可以使用15个字符。 | | **返回** | 返回值 | 说明 | 值范围 | | :----: | :-------------------------------------------------- | ------ | | bool | 执行结果。如果成功,则为true。如果不成功则为false。 | | ### 例程: 配置参数存储 Prefs2Struct (参考Arduino IDE例程 Examples -> Examples for Edge101WE ->Preferences\examples\Prefs2Struct) ```c++ /* 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 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 Examples for Edge101WE ->Preferences\examples\StartCounter) ```c++ /* 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 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。 ```c++ 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。 ```c++ bool F_Fat::begin(bool formatOnFail, const char * basePath, uint8_t maxOpenFiles, const char * partitionLabel) ``` #### FFat.format() 格式化文件系统。如果格式化成功,则返回 true。 ```c++ 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) ```c++ #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](https://github.com/espressif/arduino-esp32/tree/master/libraries/SPIFFS) [在Arduino IDE安装 SPIFFS工具](https://randomnerdtutorials.com/install-esp32-filesystem-uploader-arduino-ide/) [ESP32 Web Server using SPIFFS](https://randomnerdtutorials.com/esp32-web-server-spiffs-spi-flash-file-system/) 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() ```c++ size_t write(uint8_t) size_t write(const uint8_t *buf, size_t size) ``` 向文件中写入数据,该操作会移动文件指针; #### int available() 返回当前指针下可读取字节数; #### int read() ```c++ size_t read(uint8_t* buf, size_t size) size_t readBytes(char *buffer, size_t length) ``` 读取数据,该操作会移动文件指针; #### int peek() 在不移动文件指针的情况下读取一个字节数据; #### seek() ```c++ 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() ```c++ 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() ```c++ bool exists(const char* path) bool exists(const String& path) ``` 检查文件或路径是否存在; #### remove() ```c++ bool remove(const char* path) bool remove(const String& path) ``` 移除文件; #### rename() ```c++ bool rename(const char* pathFrom, const char* pathTo) bool rename(const String& pathFrom, const String& pathTo) ``` 重命名文件,依次输入旧的、新的包含完整文件名的路径; #### mkdir() ```c++ bool mkdir(const char *path) bool mkdir(const String &path) ``` 创建目录; #### rmdir() ```c++ bool rmdir(const char *path) bool rmdir(const String &path) ``` 删除目录; ### SPIFFS文件系统API #### begin() ```c++ 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) ```c++ #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) ```c++ #include "FS.h" #include "SPIFFS.h" #include #include 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()`to`LittleFS.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说明](https://github.com/bergdahl/arduino-esp32fs-plugin/blob/2800b79970d09b3ab39fa4e774ee59bfcb92e036/README.md) #### SPIFFS的注意事项 - 这是在 esp-32 核心中为 / data文件夹实现的默认文件系统 - 转到 Arduino IDE 菜单:***工具>分区方案,***然后选择带有 SPIFFS 分区的条目 #### LittleFS的注意事项 - 与 SPIFFS 相同的分区方案 - 在完全实现到 esp-32 内核之前,它需要一个额外的库。 已经考虑将其用于下一个核心版本。mklittlefs 工具从那里提供。 - 有关参考,请参阅[LITTLEFS esp32库](https://github.com/lorol/LITTLEFS)以获取更多详细信息 - 如果您需要[mklittlefs工具,请](https://github.com/earlephilhower/mklittlefs)下载该[版本](https://github.com/earlephilhower/mklittlefs/releases)或[在此处](https://github.com/lorol/arduino-esp32fs-plugin/releases)找到[以前版本中的存档。](https://github.com/lorol/arduino-esp32fs-plugin/releases) - 将 **mklittlefs [.exe]** 复制到 **espota** 和 **esptool**(.py或.exe)工具所在的 esp32 平台的 **/ tools**文件夹中 #### FatFS的注意事项 - 转到 Arduino IDE 菜单:***工具>分区方案,***然后选择具有 FAT 分区的条目 - 如果内核未提供,则可能需要用于 Windows 或 Linux 的其他二进制文件,感谢[@lbernstone](https://github.com/lbernstone)进行编译-或从[此处](https://github.com/labplus-cn/mkfatfs/releases/tag/v1.0)获取它们[-mkfatfs工具](https://github.com/labplus-cn/mkfatfs/releases/tag/v1.0),感谢[labplus-cn](https://github.com/labplus-cn/mkfatfs)或从[此处存档的先前发行版中获取](https://github.com/lorol/arduino-esp32fs-plugin/releases) - 如果丢失,则需要将 **mkfatfs [.exe]**复制到**espota**和**esptool**(.py或.exe)工具所在的 esp32 平台的 **/ tools** 文件夹中 - FAT 分区的可用大小减少了1个4096字节(0x1000)的扇区,以解决磨损均衡空间的要求。映像文件以 csv表条目的分区地址偏移+4096字节(0x1000)刷新 - 您可能需要在草图的 FFat.begin()处减少 **maxOpenFiles**,[请参阅此注释](http://marc.merlins.org/perso/arduino/post_2019-03-30_Using-FatFS-FFat-on-ESP32-Flash-With-Arduino.html) > FFAT 模块使用每个打开的并发文件8KB加上4KB。默认情况下,它允许打开10个文件,这意味着它使用48KB。如果要减少其内存使用量,则可以告诉它仅支持一个文件,这样可以节省36KB,而仅使用12KB。 ``` if (!FFat.begin(0, "", 1)) die("Fat FS mount failed. Not enough RAM?"); ``` - 要通过**网络端口**将数据文件夹作为 FAT 分区**刷新(使用espota)**,请[在此处](https://github.com/lorol/arduino-esp32fatfs-plugin/tree/master/extra/esp32-modified-Update-lib-ffat-espota.zip)将 esp32-core 更新库替换为[修改后的文件](https://github.com/lorol/arduino-esp32fatfs-plugin/tree/master/extra/esp32-modified-Update-lib-ffat-espota.zip) #### 安装 - 确保使用受支持的 Arduino IDE 版本之一,并已安装 ESP32 内核。 - 从[最新版本](https://github.com/lorol/arduino-esp32fs-plugin/releases)下载**esp32fs.zip**压缩工具 ![image-20210513173004079](./pictures/image-20210513173004079.png) - 在您的 Arduino sketchbook 目录中,创建一个 tools 目录(如果尚不存在)。 - 将工具解压缩到 “ Arduino安装目录-> Arduino” **/ tools**目录中。例如:`/Arduino/tools/ESP32FS/tool/esp32fs.jar` ![image-20210513180126454](./pictures/image-20210513180126454.png) 或在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** - 作为参考,请参阅[以前的发行版,](https://github.com/lorol/arduino-esp32fs-plugin/releases)以获取有关已归档二进制文件的副本。 - 您还可以使用提供的 **package_esp32_index.template.json** 来运行 **get.py** 并下载缺少的二进制文件 - 重新启动 Arduino IDE。 #### 用法 - 打开一个 sketch(或创建一个新sketch并保存)。 - 转到 sketch目录(选择“sketch”>“Show Sketch Folder”)。 - 在该文件系统中创建一个名为`data`的目录,里面存放想要存储到文件系统的文件。 ![image-20210513181521525](./pictures/image-20210513181521525.png) - 确保已选择 board,port,分区方案(选择带 SPIFFS 的选项),并关闭了串行监视器。 - 选择 **Tools > ESP32 Sketch Data Upload**菜单项。 ![image-20210513181021479](./pictures/image-20210513181021479.png) - 在下拉列表中,从 / data文件夹中选择要创建的 SPIFFS,LittleFS或FatFS。这里选择 LittleFS。 ![image-20210513181125125](./pictures/image-20210513181125125.png) - 单击“确定”应开始将文件上传到 ESP32 Flash 文件系统中。 - 最后还有一个 !Erase Flash!选项允许在必要时清除整个flash,请谨慎使用。 完成后,IDE状态栏将显示“图像已上传”消息的状态。对于大型文件系统,可能需要几分钟的时间。 ### 例程:LittleFS_test **注意:下载例程时 Partition Scheme 分区方案 选择带 SPIFFS 系统的分区** (参考Arduino IDE例程 Examples -> Examples for Edge101WE ->LITTLEFS\examples\LITTLEFS_test) ```c++ #include #include "FS.h" #include /* 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](./pictures/sdCardPin.webp) **SPI接线** | SD卡SPI引脚 | 主板GPIO | | :---------: | :------: | | DO | GPIO12 | | DI | GPIO39 | | CLK | GPIO14 | | CS | GPIO5 | ### SD API参考 #### SD.begin() - 挂载SD卡 **语法** ```cpp bool begin(uint8_t ssPin=SS, SPIClass &spi=SPI, uint32_t frequency=4000000, const char * mountpoint="/sd", uint8_t max_files=5) ``` ```go if (!SD.begin()) { Serial.print("."); } Serial.println("SD card Ready!"); ``` 挂载存储卡,输入参数分别为SS引脚号、SPI对象、时钟频率、挂载点、文件最大同时打开数; 默认IO口连接为:`CS - IO5`、`DI - IO39`、`SCLK - IO14`、`DO - 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卡挂载 ```cpp void end() ``` #### SD.cardSize() - 获取卡的大小 返回存储卡大小字节数,返回值是个64位数值 ```c++ uint64_t cardSize() ``` ```cpp Serial.printf("SD.cardSize = %lld \r\n", SD.cardSize()); ``` #### SD.cardType() - 获取卡的类型 ```c++ 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 | 未知存储卡; | ```cpp 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) - 删除文件 ```c++ SD.remove("/test.txt") ``` 从SD卡中删除文件。如果删除文件成功,则返回true;否则,返回false。(如果文件不存在,则返回值未指定) #### SD.open() - 创建/打开文件 ```c++ SD.open(filepath) SD.open(filepath, mode) ``` 打开SD卡上的文件。如果打开该文件进行写入,则将创建该文件(如果尚不存在)(但是包含该文件的目录必须已经存在)。参数模式(*可选*):打开文件的模式,默认为FILE_READ- *byte*。FILE_READ之一:从文件的开头开始打开文件进行读取。FILE_WRITE:从文件末尾开始打开文件进行读写。返回引用打开的文件的File对象;如果无法打开文件,则此对象在布尔上下文中将评估为false,即,您可以使用“ if(f)”测试返回值。 ```kotlin File file = SD.open("/test.txt", FILE_WRITE); ``` #### SD.rename(filenameFrom, filenameTo) - 重命名 ```c++ 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) ```c++ file.print(data,base) ``` 将数据打印到文件中,该文件必须已打开才能进行写入。将数字打印为数字序列,每个数字为一个ASCII字符(例如,数字123作为三个字符“ 1”,“ 2”,“ 3”发送)。参数数据:要打印的数据(字符,字节,整数,长整数或字符串),BASE(可选):要打印数字的基数:BIN(对于二进制)(以2为底),DEC(十进制)(以10为底),OCT代表八进制(基数8),十六进制代表十六进制(基数16)。返回写入的字节数,尽管读取该数字是可选的。 #### file.println() ```c++ file.println(data) file.println(data,base) ``` 作为打印但最终返回 #### file.seek(pos) 在文件中寻找新位置,该位置必须在0到文件的大小(含)之间。参数:pos:要搜索的位置(无符号long)。如果成功,则返回true;如果失败,则返回false(布尔值) #### file.size() 获取文件的大小。返回文件的大小(以字节为单位)(无符号long)。 #### file .read() ```c++ file.read(buf,len) ``` 从文件读取。返回下一个字节(或字符);如果没有可用字节,则返回-1。 #### file .write(数据) ```c++ file .write(buf,len) ``` 将数据写入文件。返回写入的字节数,尽管读取该数字是可选的 #### file.isDirectory() 目录(或文件夹)是特殊类型的文件,此功能报告当前文件是否为目录。如果是目录,则返回true。 #### file.openNextFile() 报告目录中的下一个文件或文件夹。返回路径中的下一个文件或文件夹。 #### file.rewindDirectory() 将带您回到目录中的第一个文件,与openNextFile()结合使用。 #### file.getLastWrite() 返回时期中最后一次写入/更改的数据。 ### 例程: SD卡常规操作 **注意:下载例程时 Partition Scheme 分区方案 选择带FAT系统的分区。** ```cpp #include #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() { } ``` 串口打印信息如下: ```c++ 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) ```c++ /* * 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卡进行一系列的操作。 串口打印类如如下: ```c++ 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) ```c++ /* * 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 #include 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文件,然后将时间戳写入到文件中。 串口打印信息如下: ```c++ 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) ```c++ /* Name: SD_Update.ino Created: 12.09.2017 15:07:17 Author: Frederik Merz 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 #include #include // 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循环中增加循环打印的语句。 ```c++ //will not be reached void loop() { Serial.println("The code has changed"); delay(500); } ``` 点击 Sketch - > Export compiled Binary 导出编译后的二进制文件。 ![image-20210816153513968](./pictures/image-20210816153513968.png) 我们可以在 gcc编译行后面找到编译后的bin文件地址 ![image-20210816154140715](./pictures/image-20210816154140715.png) ``` "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](./pictures/image-20210816154608185.png) 将 SD_Update.ino.bin 文件修改为和 SD_Update.ino 中代码一致的路径和文件名。 下面的代码显示 升级二进制bin文件名称必须是 update.bin 并且放到 SD卡的根目录下。 ```c++ // 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卡进行升级也可以通过无线进行升级。