ESP32 文件系统(LittleFS/FatFS/SPIFFS)对文件的读取写入列表删除操作

实验效果

凌顺实验室(lingshunlab.com)本次示例主要讲解LittleFS的使用方法,并且把示例程序上传到ESP32,看其输出。实现ESP32文件系统的读取,写入,创建目录,列表文件等操作。

现在文件系统的库兼容性非常好,对于FatFS和SPIFFS库的使用,和LittleFS基本无差别,通过切换不同库的头文件,就可以实现无痛无缝切换。但各子的文件系统里的文件,并不相通,清注意这一点。切换了使用的文件系统,之前保存的文件很有可能会丢失。

以下图片是ESP32运行「LittleFS_test」的串口输出信息:

87gff87d

可以看出,
把1048576 bytes的数据写入的时间,需要 11358 ms,
而读取1048576 bytes的数据的时间,需要 530 ms。

LittleFS介绍

这个库现在是Arduino esp32 core v2的一部分。

https://github.com/espressif/arduino-esp32/tree/master/libraries/LittleFS

注意,现在已经从LITTLEFS改名为LittleFS,请在那里(Arduino esp32 core v2)发表你的问题。这里是为Arduino esp32 core 1.x而保留的,已经停止更新了。

  • 该功能与FAT,SPIFFS类似,基本可以通过修改头文件即可切换使用。

ESP32功能模块图:

2880px-Espressif_ESP32_Chip_Function_Block_Diagram.svg

SPIFFS 是原始文件系统,非常适合空间和 RAM 受限的应用程序,这些应用程序使用许多小文件,关心静态和动态磨损均衡并且不需要真正的目录支持。闪存上的文件系统开销也很小。

LittleFS 专注于更高的性能和目录支持,但具有更高的文件系统和每个文件的开销(最小 4K 与 SPIFFS 的 256 字节最小文件分配单元相比)。

在这个闪存中,ESP 存储了程序。除了该程序,您还可以将文件存储在上面。此内存的限制是它只有 10000(一万)个写周期。

与 SPIFFS 的区别

  • LittleFS有文件夹,除非你设置,否则你需要迭代文件夹中的文件 #define CONFIG_LITTLEFS_SPIFFS_COMPAT 1
  • open 方法的行为与 SPIFFS 不同。当您遍历 a 时,SPIFFS 会返回“子目录”中的文件 File::openNextFile() (因为它们不是子目录,而是名称中带有“/”的文件),而 LittleFS 只会返回指定子目录中的文件。这模仿了大多数 C 程序员习惯的目录遍历的POSIX 行为。
  • 在根目录下有一个“/folder”=“folder”
  • 需要挂载点标签,NULL 无效。推荐使用默认的 LITTLEFS.begin()
  • maxOpenFiles 参数未使用,保留兼容性
  • LITTLEFS.mkdir(path) 和 LITTLEFS.rmdir(path) 可用
  • file.seek() 的行为类似于 FFat 查看更多详细信息
  • 分区空间结束时的 file.write() 和 file.print() 可能返回不同于实际写入的字节(在其他 FS 上也不一致)。
  • 基于LittleFS_test.ino草图的速度比较(对于 1048576 字节的文件):
文件系统 读取时间 [ms] 写入时间[ms]
FAT 276 14493
LITTLEFS 446* 16387
SPIFFS 767 65622

*以上测试数据仅供参考,以实际为准。

配置储存空间

可以在上传代码中配置LittleFS的储存空间,

WX20230309-2027252x

库安装

如果是使用Arduino ESP core v2.0之后的版本,LittleFS已经默认安装好。

image-20230310090046721

使用方法

使用与SPIFFS相同的方法的LITTLEFS,加上mkdir()和rmdir()。
在你现有的代码基础上快速启动,你可也以重新定义 SPIFFS

#define USE_LittleFS

#include <FS.h>
#ifdef USE_LittleFS
  #define SPIFFS LITTLEFS
  #include<LITTLEFS.h>,否则 
#else
  #include <SPIFFS.h
#endif 

注意,如果你的草图使用了其他自己使用SPIFFS的库,这可能就不奏效。
参见esp_partition.h 。LITTLEFS重新使用与SPIFFS相同的类型和子类型。

属性与方法

有一些标准命令或者方法可用于此文件系统

LITTLEFS.begin()

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

LITTLEFS.format()

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

LITTLEFS.open(path, mode)

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

LITTLEFS.exists(path)

如果给定路径的文件存在则返回真;否则为假。

LITTLEFS.remove(path)

删除给定绝对路径的文件。如果文件删除成功则返回真。

LITTLEFS.rename(pathFrom, pathTo)

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

LITTLEFS .mkdir(path):

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

LITTLEFS .rmdir(path):

*删除目录。 如果目录已成功删除,则返回 true ;*否则为假。

LITTLEFS.totalBytes()

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

LITTLEFS.usedBytes()

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

file.seek(offset, mode)

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

  • 如果模式是 SeekSet,则位置设置为从开头偏移字节。
  • 如果模式是 SeekCur,则当前位置移动偏移量字节。
  • 如果模式为 SeekEnd,则位置设置为距文件末尾的字节偏移量。
  • 如果位置设置成功则返回真。

file.position()

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

file.size()

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

file.name()

返回文件名,作为 const char*。

file.close()

关闭文件。

file.getLastWrite()

最后一次写入的纪元时间(使用内部时间来管理日期)。

file.isDirectory()

如果是目录则返回

file.openNextFile()

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

file.rewindDirectory()

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

示例代码

打开示例代码,菜单栏「File」->「Examples」->「LittleFS」->「LittleFS_test」

// welcome to www.lingshunlab.com

#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(){

}

参考:

https://www.mischianti.org/2021/04/01/esp32-integrated-littlefs-filesystem-5/