基于ESP32的桌面小屏幕实战[10]:使用文件系统存储文件

源码位置~/esp/esp-idf/examples/storage/spiffs

1. 基础知识

文件系统就是一个用于存储文件的系统模块,可以创建、打开、读写、关闭文件。后面的http service会用到文件系统。用手机连接桌面屏幕的热点,访问它的网页,这个功能需要文件系统。

SPIFFS(SPI Flash File System)是一种用于微控制器(MCU)和嵌入式系统的轻量级文件系统。它专为资源受限的设备设计,能够在 SPI 闪存设备上提供简单且可靠的文件存储方式。SPIFFS 不支持目录结构,因此所有文件都存储在一个扁平的空间中,但它在读写小文件时非常高效。

官方文档中说:“SPIFFS 是一个用于 SPI NOR flash 设备的嵌入式文件系统,支持磨损均衡、文件系统一致性检查等功能。”[1]

下面逐一介绍这句话里的专业名词:

  • SPI(Serial Peripheral Interface,串行外设接口)是一种用于短距离数据传输的高速串行通信协议,常用于在微控制器和外围设备之间(如闪存、传感器和显示器)建立连接。SPI 通信使用以下四条信号线:

    • MOSI(主输出、从输入):主设备(如微控制器)发送数据,从设备(如闪存)接收。
    • MISO(主输入、从输出):从设备发送数据,主设备接收。
    • SCLK(时钟):由主设备生成的时钟信号,用于同步数据传输。
    • SS/CS(从设备选择):选择从设备,确保只有被选中的从设备能进行数据传输。

    SPI 的特点是全双工通信,数据传输速度快,广泛用于嵌入式设备中,如传感器、存储器和显示屏。

  • SPI NOR Flash 是一种通过 SPI 接口连接的 NOR 闪存。NOR Flash 是一种非易失性存储器,与 NAND Flash 相比,它的特点是:

    • 字节级可寻址:可以直接访问特定字节地址,而 NAND Flash 是页级别访问。
    • 高读取速度:适合需要频繁读取的应用,例如存储固件或启动代码。
    • 较低写入速度和耐用性:通常 NAND Flash 写入速度更快、容量更大、成本更低,但 NOR Flash 更适合存储少量数据且需要快速读取的应用。

    SPI NOR Flash 通常用于嵌入式系统和微控制器上存储固件、配置文件和日志数据等。

  • 磨损均衡是一种管理闪存存储的技术,用于延长闪存的使用寿命。由于闪存具有有限的擦写次数,某些区域会因频繁擦写而过度磨损,导致数据可靠性下降。磨损均衡通过均匀分布写入操作,使所有区域的擦写次数接近,从而避免单一区域过早损坏。这通常通过以下方式实现:

    • 动态磨损均衡:在文件频繁更新时,将文件数据移动到不同区域,均匀分布写入负载。
    • 静态磨损均衡:不频繁更新的数据也会偶尔移动,以保证存储空间整体磨损分布均匀。

    在 SPIFFS 中,磨损均衡是通过管理页和块的方式实现的。

  • 文件系统一致性检查是指在文件系统初始化或异常(如断电、意外重启)后,对文件系统的元数据和存储内容进行检查,确保数据结构的完整性和一致性。例如,当设备断电后重新启动,文件系统会通过一致性检查验证文件结构、文件数据和元数据的正确性,防止损坏或数据丢失。

2. 源码

2.1 包含头文件以及变量定义

包含头文件

#include <stdio.h>
#include <string.h>
#include <sys/unistd.h>
#include <sys/stat.h>
#include "esp_err.h"
#include "esp_log.h"
#include "esp_spiffs.h"

"esp_spiffs.h"头文件定义了 SPIFFS 文件系统在 ESP32 环境中的接口和配置方式,帮助开发者在 SPI Flash 上方便地管理文件数据。

定义一个指向字符串 "example" 的常量指针 TAG,用于日志系统中标识该模块或文件的日志信息来源。

static const char *TAG = "example";
  • static 表示TAG变量的作用域限定在本文件内,其他文件不能直接访问它。
  • const 表示TAG是一个常量指针,指向的字符串 "example" 是只读的,防止代码意外修改其内容。

2.2 主函数

定义 esp_vfs_spiffs_conf_t 类型的结构体变量 conf,用于配置 SPIFFS 文件系统。

void app_main(void)
{
    ESP_LOGI(TAG, "Initializing SPIFFS");//打印日志

    esp_vfs_spiffs_conf_t conf = {
      .base_path = "/spiffs",           //挂载点路径
      .partition_label = NULL,          //不指定具体的分区标签
      .max_files = 5,                   //设置最多同时打开 5 个文件
      .format_if_mount_failed = true    //果挂载文件系统失败,尝试格式化并重新挂载
    };
//... ...
  • conf 是一个 esp_vfs_spiffs_conf_t 类型的结构体变量,用于配置 SPIFFS 文件系统。esp_vfs_spiffs_conf_t 的定义可以在 ESP-IDF 框架的 esp_spiffs.h 头文件中找到。
  • .base_path = "/spiffs":指定挂载点路径为 /spiffs,之后的文件操作将使用这个路径。
  • .partition_label = NULL:不指定具体的分区标签,使用默认的 SPIFFS 分区。
  • .max_files = 5:设置最多同时打开 5 个文件。
  • .format_if_mount_failed = true:如果挂载文件系统失败,尝试格式化并重新挂载。

使用上述配置初始化并挂载 SPIFFS 文件系统

    esp_err_t ret = esp_vfs_spiffs_register(&conf);

conf 配置来初始化和挂载 SPIFFS 文件系统。如果成功,ret 会返回 ESP_OK;否则返回错误代码。

错误处理

    if (ret != ESP_OK) {
        if (ret == ESP_FAIL) {
            ESP_LOGE(TAG, "Failed to mount or format filesystem");
        } else if (ret == ESP_ERR_NOT_FOUND) {
            ESP_LOGE(TAG, "Failed to find SPIFFS partition");
        } else {
            ESP_LOGE(TAG, "Failed to initialize SPIFFS (%s)", esp_err_to_name(ret));
        }
        return;
    }
  • ESP_FAIL:挂载失败,且文件系统格式化也失败。
  • ESP_ERR_NOT_FOUND:找不到指定的 SPIFFS 分区。
  • 其他错误:使用 esp_err_to_name(ret) 将错误代码转换为字符串并打印错误信息。

检查并获取 SPIFFS 文件系统的状态,包括是否有错误、存储空间的总量和已使用量。

#ifdef CONFIG_EXAMPLE_SPIFFS_CHECK_ON_START
    ESP_LOGI(TAG, "Performing SPIFFS_check().");
    /* 调用 esp_spiffs_check 函数检查文件系统一致性,传入分区标签 conf.partition_label */
    ret = esp_spiffs_check(conf.partition_label);
    if (ret != ESP_OK) {    // 如果检查结果不是 ESP_OK,则表示检查失败
        ESP_LOGE(TAG, "SPIFFS_check() failed (%s)", esp_err_to_name(ret));
        return; // 终止当前函数,返回调用者
    } else {
        ESP_LOGI(TAG, "SPIFFS_check() successful");
    }
#endif

    /* 定义两个变量 total 和 used,用于存储文件系统的总空间和已使用空间的大小 */
    size_t total = 0, used = 0;
    /* 获取 SPIFFS 文件系统的分区信息 */
    ret = esp_spiffs_info(conf.partition_label, &total, &used);
    if (ret != ESP_OK) {    // 如果返回值 ret 不是 ESP_OK,则表示获取分区信息失败
        ESP_LOGE(TAG, "Failed to get SPIFFS partition information (%s). Formatting...", esp_err_to_name(ret));
        /* 调用 esp_spiffs_format 函数格式化 SPIFFS 文件系统,使其恢复到初始状态 */
        esp_spiffs_format(conf.partition_label);
        return; // 终止当前函数,返回调用者
    } else {
        ESP_LOGI(TAG, "Partition size: total: %d, used: %d", total, used);
    }
//... ...
  • esp_spiffs_check
    • 功能:检查 SPIFFS 文件系统的一致性。
    • 函数原型:esp_err_t esp_spiffs_check(const char *partition_label);
    • 返回值为 ESP_OK 表示检查成功,文件系统一致性良好。返回值为其他错误代码(如 ESP_FAIL)表示文件系统出现了错误。
    • 使用场景:通常用于系统启动时,确认 SPIFFS 文件系统没有发生数据损坏或不一致的情况。
  • esp_spiffs_info
    • 功能:获取 SPIFFS 文件系统的分区信息,包括总空间和已使用空间。

    • 参数:

      • partition_label:要获取信息的 SPIFFS 分区标签。可以为 NULL 表示默认分区。
      • total_bytes:用于存储文件系统的总空间(字节数)。
      • used_bytes:用于存储文件系统的已使用空间(字节数)。
    • 返回值:

      • ESP_OK 表示成功获取分区信息。
      • 其他错误代码表示无法获取分区信息,可能是文件系统未挂载或指定的分区不存在。
    • 使用场景:可以用来监测文件系统的空间使用情况,为文件读写提供参考。

  • esp_spiffs_format
    • 功能:格式化 SPIFFS 文件系统,将其清空并恢复为初始状态。
    • 函数原型:esp_err_t esp_spiffs_format(const char *partition_label);
    • 返回值:
      • ESP_OK 表示格式化成功。
      • 其他错误代码表示格式化失败。
    • 使用场景:当文件系统出现严重错误或数据无法读取时,可以使用该函数进行格式化,以清除错误数据,恢复文件系统的正常状态。

检查已使用空间是否超过总空间

    if (used > total) { //如果 used(已使用空间)大于 total(总空间),表明存在文件系统不一致的情况,继续执行检查。
        ESP_LOGW(TAG, "Number of used bytes cannot be larger than total. Performing SPIFFS_check().");
        ret = esp_spiffs_check(conf.partition_label);   //检查文件系统一致性
        // Could be also used to mend broken files, to clean unreferenced pages, etc.
        // More info at https://github.com/pellepl/spiffs/wiki/FAQ#powerlosses-contd-when-should-i-run-spiffs_check
        if (ret != ESP_OK) {
            ESP_LOGE(TAG, "SPIFFS_check() failed (%s)", esp_err_to_name(ret));
            return;
        } else {
            ESP_LOGI(TAG, "SPIFFS_check() successful");
        }
    }

使用标准的 C 库函数来在 SPIFFS 文件系统上创建和写入一个文件

    ESP_LOGI(TAG, "Opening file");//日志记录
    FILE* f = fopen("/spiffs/hello.txt", "w");//打开文件进行写入
    /* 检查文件是否成功打开 */
    if (f == NULL) {
        ESP_LOGE(TAG, "Failed to open file for writing");
        return;
    }
    fprintf(f, "Hello World!\n");//使用 fprintf 函数将 "Hello World!\n" 字符串写入文件 f 中
    fclose(f);//关闭文件
    ESP_LOGI(TAG, "File written");//日志记录:文件写入成功
  • FILE* f 定义了一个指向 FILE 类型的指针变量 f,用于在 C 语言中处理文件操作。
    • FILE 类型:FILE 是一个结构体类型,通常用于表示打开的文件。它包含文件的状态信息、当前位置、读写模式等,由标准库定义。
    • FILE* 指针:FILE* 是一个指向 FILE 结构的指针,表示文件的引用,用来在代码中访问和操作该文件。
    • f 变量:在这里,f 是指向 FILE 的指针,用来保存 fopen 函数返回的文件指针。

在重命名文件前检查目标文件是否已存在。如果存在,则将其删除。

    struct stat st;//定义一个stat类型的结构体变量st,用于存储文件信息。
    if (stat("/spiffs/foo.txt", &st) == 0) {//使用 stat 函数检查 /spiffs/foo.txt 文件是否存在。stat 函数会将文件的信息存储到 st 中。
        // Delete it if it exists
        unlink("/spiffs/foo.txt");//如果文件存在,调用 unlink 函数删除该文件。
    }
  • stat 是 POSIX 标准的结构体类型,用于保存文件的元数据。
  • unlink 是 POSIX 标准中的文件删除函数,它会从文件系统中移除指定文件。

将文件重命名

    ESP_LOGI(TAG, "Renaming file");//输出一条信息日志
    if (rename("/spiffs/hello.txt", "/spiffs/foo.txt") != 0) {//重命名
        ESP_LOGE(TAG, "Rename failed");
        return;
    }

打开并读取重命名后的文件的内容,并将其打印到日志中

    ESP_LOGI(TAG, "Reading file");//输出日志,表示开始读取文件
    f = fopen("/spiffs/foo.txt", "r");//以只读模式打开文件,将文件指针赋给f
    if (f == NULL) {//如果 f 为 NULL,表示文件打开失败
        ESP_LOGE(TAG, "Failed to open file for reading");
        return;
    }
    char line[64];//定义字符数组,存储读取内容
    fgets(line, sizeof(line), f);//使用 fgets 读取文件内容到 line,只读取第一行
    fclose(f);//关闭文件,释放资源
    // strip newline
    char* pos = strchr(line, '\n');//查找 line 中的换行符(\n)位置
    if (pos) {//如果找到
        *pos = '\0';//将换行符替换为字符串终止符 '\0',去掉换行。
    }
    ESP_LOGI(TAG, "Read from file: '%s'", line);//打印文件内容

卸载 SPIFFS 文件系统并释放其占用的资源

    esp_vfs_spiffs_unregister(conf.partition_label);
    ESP_LOGI(TAG, "SPIFFS unmounted");//打印日志信息,确认 SPIFFS 文件系统已成功卸载
  • 调用 esp_vfs_spiffs_unregister 函数,可以卸载指定分区(即 conf.partition_label 指定的分区)上的 SPIFFS 文件系统。此函数会清理与 SPIFFS 相关的资源,将文件系统与虚拟文件系统 (VFS) 解除绑定,禁用 SPIFFS。

3. 编译工程

在编译工程之前,首先要做spiffs文件系统移植

在终端中使用

idf.py menuconfig

打开项目的配置界面

找到下面的Custom partition table CSV,并选择它
img

然后连上板子,在终端输入

idf.py -p /dev/ttyUSB0 flash monitor

下载程序。

打印出的日志如下
img

推荐链接

posted @ 2025-11-19 09:44  茴香豆的茴  阅读(121)  评论(0)    收藏  举报