配套代码仓库:Lovely_him/esp32s3_component_test
提交: ef00551845e5acb6246b6482f90554842fca6759 [ef00551]
标签:csdn_node_3

esp-idf手册:分区表
esp-idf手册:NVS 加密
推荐视频:第二十二讲 系统存储——ESP32中的分区表_哔哩哔哩_bilibili

一、LOG日志

ESP32S3 报错 未定义标识符 CONFIG_LOG_MAXIMUM_LEVEL
c_cpp_properties.json->configurations->includePath中添加"${workspaceFolder}/build/config"

在ESP(Espressif Systems,如ESP32/ESP8266)开发中,日志打印(Logging)是调试和问题排查的重要工具。ESP-IDF框架提供了不同级别的日志类型,每种类型对应不同的应用场景和严重程度。以下是常见的日志级别及其区别:


1. 日志类型

(1). 错误(Error)

  • 级别:最高优先级,标记为 ERROR(ESP-IDF)或 e(Arduino-ESP)。
  • 用途:记录严重错误,可能导致系统崩溃或功能完全失效(如硬件初始化失败、内存分配失败等)。
  • 输出:通常以红色显示(如果终端支持颜色)。

(2). 警告(Warning)

  • 级别WARN(ESP-IDF)或 w(Arduino-ESP)。
  • 用途:记录非致命但需要注意的问题,如参数超出范围、降级操作等。系统仍能运行,但可能存在隐患。

(3). 信息(Info)

  • 级别INFO(ESP-IDF)或 i(Arduino-ESP)。
  • 用途:记录常规运行信息,如状态更新、关键流程节点(如Wi-Fi连接成功)。
  • 注意:避免过度打印,否则会淹没关键日志。

(4). 调试(Debug)

  • 级别DEBUG(ESP-IDF)或 d(Arduino-ESP)。
  • 用途:记录详细的调试信息,如函数调用、变量值等。通常用于开发阶段,生产环境建议关闭。
  • 如何启用:需显式设置日志级别为 DEBUG(ESP-IDF中通过 menuconfig 配置)。

(5). 详细(Verbose)

  • 级别VERBOSE(仅ESP-IDF)。
  • 用途:记录最详细的日志(如高频数据、低级硬件操作)。对性能影响较大,仅用于深度调试。

(6). 其他类型

  • 断言(Assert)
    当条件不满足时触发致命错误,终止程序并打印堆栈(如 assert(param != NULL))。
  • 核心转储(Core Dump)
    ESP-IDF支持将崩溃时的内存状态保存到 flash,供后续分析。

2. 类型区别总结

级别 严重性 使用场景 生产环境是否启用
Error 致命错误
Warning 潜在问题 是(少量)
Info 正常运行状态 是(关键信息)
Debug 开发 详细的调试输出
Verbose 深度 高频/底层细节

3. 配置建议

  • 设置位置
    menuconfig → Component config → Log output → Default log verbosity
    可选级别:NoneErrorWarningInfoDebugVerbose

  • 作用规则
    只有等于或高于设定级别的日志才会被输出。例如:

    • 设为 Info 时:ErrorWarningInfo 会输出,DebugVerbose 被过滤。
    • 设为 Debug 时:Verbose 被过滤,其余均输出。
  • 代码中的优先级
    即使代码中调用了 ESP_LOGD,如果全局级别低于 Debug,日志会被编译器优化掉(不占用Flash和运行时间)。


4. 代码修改

(1). 查看当前日志优先级

使用 esp_log_level_get() 函数获取指定 TAG 的当前日志级别:

#include "esp_log.h"

void check_log_level() {
    esp_log_level_t level = esp_log_level_get("MY_TAG");
    printf("当前日志级别: ");
    switch (level) {
        case ESP_LOG_ERROR:    printf("Error\n"); break;
        case ESP_LOG_WARN:     printf("Warning\n"); break;
        case ESP_LOG_INFO:     printf("Info\n"); break;
        case ESP_LOG_DEBUG:    printf("Debug\n"); break;
        case ESP_LOG_VERBOSE:  printf("Verbose\n"); break;
        default:               printf("Unknown\n");
    }
}

注意:若未显式设置某个 TAG 的级别,返回的是 menuconfig 中配置的默认级别(CONFIG_LOG_DEFAULT_LEVEL)。


(2). 动态设置日志优先级

使用 esp_log_level_set() 修改指定 TAG 的日志级别:

#include "esp_log.h"

void set_log_level() {
    // 设置某个 TAG 的日志级别(高于全局默认级别时生效)
    esp_log_level_set("MY_TAG", ESP_LOG_DEBUG);  // 允许输出 DEBUG 级别日志
    
    // 示例:仅在调试时启用详细日志
    #ifdef CONFIG_DEBUG_MODE
        esp_log_level_set("NETWORK", ESP_LOG_VERBOSE);
    #endif
}

关键规则

  • 实际生效的日志级别 = min(全局默认级别, TAG设置的级别)。
    例如:全局默认为 Info,即使设置 MY_TAGDebug,实际仍只能输出 Info 及以上日志。
  • 需在 menuconfig 中启用 CONFIG_LOG_ALLOW_OVERRIDE_LEVEL 以允许覆盖默认级别。

(3). 临时禁用日志

若需完全关闭某个 TAG 的日志,将其级别设为 ESP_LOG_NONE

esp_log_level_set("NOISY_TAG", ESP_LOG_NONE);  // 禁用该 TAG 所有日志

5. 注意事项

  1. TAG 必须匹配
    esp_log_level_set() 的 TAG 需与日志语句中的 ESP_LOGx("TAG", ...) 完全一致(区分大小写)。
  2. 实时生效
    修改日志级别后,后续日志会立即遵循新规则。
  3. 性能开销
    高频调用 esp_log_level_set() 可能影响性能,建议在初始化阶段设置。
  4. 动态调整
    通过代码动态调整日志级别,可以精准控制调试信息的输出范围,无需重新编译或修改 menuconfig

6. 处理错误的宏

(1). ESP_ERROR_CHECK 的作用

ESP_ERROR_CHECK 是 ESP-IDF(Espressif IoT Development Framework)中用于错误处理的宏,主要用于快速检查函数返回值是否为错误代码,并在出错时触发系统级处理。其核心功能包括:

  • 错误检测:检查传入的表达式(通常是函数调用)的返回值是否为 ESP_OK(即 0)。若返回值非 ESP_OK,表示发生了错误。
  • 错误处理:触发以下行为:
    • 打印错误信息(包括错误代码、源码位置等)。
    • 调用 abort() 终止程序运行(默认行为,可配置为其他方式如重启)。

(2). 类似接口

ESP-IDF 提供了其他相关错误处理机制,适用于不同场景:

接口/宏 作用 区别
ESP_ERROR_CHECK_WITHOUT_ABORT 类似 ESP_ERROR_CHECK,但不终止程序,仅打印错误日志。 适合可恢复错误的场景。
ESP_RETURN_ON_ERROR 检查错误,若发生则返回当前函数的错误代码(需在函数内使用)。 用于函数内部的错误传递。
ESP_GOTO_ON_ERROR 检查错误,若发生则跳转到指定标签(通常用于资源清理)。 适合需要资源释放的场景。
assert() 标准 C 断言,检查表达式为真,否则终止程序(需包含 assert.h)。 通用断言,不特定于 ESP 错误码。

示例对比

// ESP_ERROR_CHECK:直接终止
ESP_ERROR_CHECK(esp_wifi_init(&config));

// ESP_RETURN_ON_ERROR:返回错误
esp_err_t my_function() {
    ESP_RETURN_ON_ERROR(esp_wifi_init(&config), TAG, "WiFi init failed");
    return ESP_OK;
}

// ESP_GOTO_ON_ERROR:跳转清理
esp_err_t my_function() {
    ESP_GOTO_ON_ERROR(esp_wifi_init(&config), err, TAG, "WiFi init failed");
    return ESP_OK;
err:
    cleanup_resources();
    return err_code;
}

(3). 应用场景建议

  • 致命错误:使用 ESP_ERROR_CHECK(如硬件初始化失败)。
  • 可恢复错误:使用 ESP_RETURN_ON_ERRORESP_GOTO_ON_ERROR
  • 调试阶段:结合 assert() 验证逻辑条件。

(4). 总结

ESP_ERROR_CHECK 是 ESP-IDF 中用于快速失败(Fail-Fast)的核心工具,强制开发者立即处理错误。类似的接口(如 ESP_RETURN_xxx)提供了更灵活的错误传播方式,适应不同层级的错误处理需求。


二、分区表(partition table)

1. 拷贝修改分区表文件

  • 从idf安装目录中拷贝所需分区表。并加个自己的数据分区。
# 分区表目录:esp_idf_541\frameworks\esp-idf-v5.4.1\components\partition_table
# Name,   Type, SubType, Offset,   Size, Flags
# Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap
nvs,      data, nvs,     ,        0x4000,
otadata,  data, ota,     ,        0x2000,
phy_init, data, phy,     ,        0x1000,
factory,  app,  factory, ,        1M,
ota_0,    app,  ota_0,   ,        1M,
ota_1,    app,  ota_1,   ,        1M,
data_him, 0x40, 0x01,    ,        0x400, 
  • 选择配置为自定义分区表。
idf.py menuconfig			# 打开配置页面
Partition Table				# 搜索分区表选项
Custom partition CSV file	# 选择自定义分区文件
idf.py save-menuconfig		# 更新默认配置文件
  • 编译下载监控能看到引导程序的打印内容。可以看到实际的TypeST的枚举类型。
I (50) boot: Partition Table:
I (52) boot: ## Label            Usage          Type ST Offset   Length
I (59) boot:  0 nvs              WiFi data        01 02 00009000 00004000
I (65) boot:  1 otadata          OTA data         01 00 0000d000 00002000
I (72) boot:  2 phy_init         RF data          01 01 0000f000 00001000
I (78) boot:  3 factory          factory app      00 00 00010000 00100000
I (85) boot:  4 ota_0            OTA app          00 10 00110000 00100000
I (91) boot:  5 ota_1            OTA app          00 11 00210000 00100000
I (98) boot:  6 data_him         unknown          40 01 00310000 00000400
I (104) boot: End of partition table

2. 分区表的核心内容

(1) 分区类型(Type)

定义分区的用途,常见类型包括:

  • app (0x00): 存放应用程序(固件),通常是主程序或OTA更新后的程序。
  • data (0x01): 存储数据,如配置文件、NVS(非易失性存储)等。
    • 子类型:nvsphyfatspiffs等。
  • 0x40: 自定义类型。

(2) 子类型(SubType)

进一步细化分区用途,例如:

  • ota_0 / ota_1: 用于OTA更新的两个镜像分区。
  • nvs: Non-Volatile Storage(键值对存储)。
  • fat: FAT文件系统。
  • phy: RF射频校准数据。
  • factory: 出厂默认固件(无OTA时使用)。
  • 当分区类型为0x40是,子类型也可自定义。

(3) 偏移地址(Offset)

分区在Flash中的起始地址:

  • 可显式指定(如 0x10000)。
  • 或留空自动计算(紧接上一个分区)。

(4) 分区大小(Size)

  • 单位:KB(如 16K)、MB(如 1M)。
  • 没自动计算,必须明确指明没有错误的大小。

(5) 标志(Flags)

  • encrypted: 分区是否加密(需启用Flash加密)。
  • readonly: 只读分区。
  • 一般留空,普通的可读写分区,app分区会默认强制加密。

3. 配置选项详解

字段 选项/示例 说明
Type app, data, storage 决定分区的主要功能。
SubType ota_0, nvs, fat 细化分区的具体用途。
Offset 0x10000 分区起始地址(Hex)或自动对齐。
Size 256K, 1M 分区大小或占用剩余空间。
Flags encrypted, readonly 加密或只读属性(可选)。

3. 关键注意事项

  1. OTA更新:至少需要2个app类型分区(如ota_0ota_1)。
  2. 文件系统:如FAT/SPIFFS,需确保分区足够大(例如>=1MB)。
  3. 偏移对齐:默认分区表从 0x8000 开始,用户分区通常从 0x10000 开始。
  4. 动态大小:最后一个分区可用 - 占满剩余空间。

4. 参考资料

  • 官方文档ESP32 Partition Tables
  • 生成工具idf.py partition-table 或手动编辑 partitions.csv
  • 总结:通过灵活配置分区表,可以优化ESP32的存储布局,适应不同应用场景(如OTA、文件系统、加密存储等)。

5. 代码测试

类比理解:
esp32将用分区表管理片内flash的划分,有完整的引导程序与之匹配,直接读写分区表内存的操作,和以前直接读写flash的操作一样,属于较直接的读写操作。跳转到读写api的实现,可以看到调用了 esp_flash_spi.c 驱动。

  • 创建分区表测试组件,组件调用 esp_partition.h 需要添加依赖 PRIV_REQUIRES esp_partition
// 在组件文件 Partition_Tables_Test.c 内写入
#include <stdio.h>
#include <string.h>
#include "Partition_Tables_Test.h"

#include "esp_log.h"
#include "esp_partition.h" // 需要添加组件依赖 PRIV_REQUIRES esp_partition

static const char TAG[] = "Partition_Tables_Test.c";

#define TEST_PARTITION_TYPE     (0x40)      // 主类型
#define TEST_PARTITION_SUBTYPE  (0x01)      // 子类型
#define TEST_PARTITION_NAME     "data_him"  // 名字(标签)类型如果有重复,可以使用标签进一步指定

static const esp_partition_t* partition_p = NULL; // 分区表指定

void Partition_Tables_Test_func(void)
{
    partition_p = esp_partition_find_first(TEST_PARTITION_TYPE, TEST_PARTITION_SUBTYPE, TEST_PARTITION_NAME); // 指定 类型和名字,返回第一个
    if (partition_p == NULL) {
        ESP_LOGE(TAG, "%d - %d - %s :分区表获取失败", TEST_PARTITION_TYPE, TEST_PARTITION_SUBTYPE, TEST_PARTITION_NAME);
        return;
    } else {
        ESP_LOGI(TAG, "%d - %d - %s :分区表获取成功", TEST_PARTITION_TYPE, TEST_PARTITION_SUBTYPE, TEST_PARTITION_NAME);
    }

    esp_partition_erase_range(partition_p, 0, 1024*4); // 擦除扇区,偏移0,大小4kb对齐

    char temp_str[] = "测试分区表直接读写操作";

    esp_partition_write(partition_p, 0, temp_str, sizeof(temp_str)); // 写入,偏移0,指定指针和大小

    memset(temp_str, 0, sizeof(temp_str)); // 清空字符串
    ESP_LOGI(TAG, "读取前: %s", temp_str);

    esp_partition_read(partition_p, 0, temp_str, sizeof(temp_str)); // 读取,偏移0,指定指针和大小
    ESP_LOGI(TAG, "读取后: %s", temp_str);
}

三、nvs(键值对存储)

  • 初学时使用的存储方式:
    1. 一个扇区存储几个变量;统一读写擦除;
    2. 几个扇区存储几个结构体;统一读写擦除;
    3. 一个扇区使用键值对;键为32位枚举索引,值为32位数据;分开读写,统一擦除;
  • 目前nvs就是类似第3种,提供字符串类型的键,值也改为任意类型。
  • 注意,部分组件 默认 使用nvs存储参数,所以在使用这些组件前 必须 先初始化配置nvs。

1. NVS的通用性设计

  • 跨组件存储:NVS的目标是为整个ESP32应用程序提供轻量级的键值(Key-Value)存储,无论是Wi-Fi/BT组件的配置(如SSID、密码)、用户应用程序数据(如设备参数、传感器校准值),还是系统运行时状态,均可通过NVS存储。
  • 组件的默认依赖:部分ESP-IDF组件(如wifi_provisioning)默认依赖NVS存储配置,这是因为NVS是ESP32推荐的通用存储方案,而非其专属设计。如果开发者不希望使用NVS,可以为组件配置其他存储方式(例如SPIFFS/LittleFS文件系统),但需自行实现接口适配。

2. NVS的核心特性

  • 键值存储模型:通过简单的set/get接口操作数据,支持整数、字符串、二进制块等多种类型。
  • 命名空间:在键Key的基础上进一步添加命名空间,避免重名,简单理解为命名前缀。
  • 分区管理:NVS存储在Flash的独立分区中(通常是nvs分区),与程序代码、文件系统等其他区域隔离,避免冲突。
  • 损耗均衡:NVS在底层自动处理Flash的写入均衡,延长Flash寿命。(大概是:更改值时写入最后位置,写满后才擦除
  • 加密支持:可启用NVS加密(基于AES-XTS),保护敏感数据(如Wi-Fi凭证)。

3. 代码测试

疑问

  1. 可以同时打开2个命名空间吗?
    没有任何影响,正常使用。
  2. 读出长度越位怎么办?
    实际上读取属于内部指定长度,长度是输出值,不是输入值。
  • 对于键值对于不同类型有不同API接口。可以选择最通用的,直接无类型指针写入指定字节即可。其实内部调用的就是字符串写入。
  • 创建nvs测试组件,组件调用 nvs_flash.h 需要添加依赖 PRIV_REQUIRES nvs_flash
// 在组件文件 nvs_test.c 内写入
#include <stdio.h>
#include <string.h>
#include "nvs_test.h"

#include "freertos/FreeRTOS.h"
#include "nvs_flash.h" // 需要添加依赖 PRIV_REQUIRES nvs_flash
#include "esp_log.h"

static const char TAG[] = "nvs_test.c";

void nvs_test_rw(char *namespace_name, char *key, void *value, size_t length)
{
    nvs_handle wifi = {0};
    nvs_handle temp_wifi = {0};
    size_t temp_len = 0;

    nvs_open("namespace_name", NVS_READWRITE, &temp_wifi);  // 打开另一个没用到的命名空间
    nvs_open(namespace_name, NVS_READWRITE, &wifi);         // 打开(挂载)命名空间,模式为读写,句柄

    ESP_LOGI(TAG, "%s write %s : %s", namespace_name, key, (char *)value); // 打印数据
    nvs_set_blob(wifi, key, value, length);                 // 写入
    nvs_commit(wifi);                                       // 立刻写入:将任何挂起的更改写入非易失性存储

    nvs_close(wifi);                                        // 关闭(卸载)命名空间

    vTaskDelay(pdMS_TO_TICKS(1000));                        // 延时1000ms

    nvs_open(namespace_name, NVS_READONLY, &wifi);          // 打开(挂载)命名空间,模式为只读,句柄

    nvs_get_blob(wifi, key, NULL, &temp_len);               // 先读取长度,确保有参数且长度不为0
    if (temp_len > 0)
    {
        temp_len += 20;                                     // 强制越位读取,实际上读取属于内部指定长度,长度是输出值,不是输入值
        char *temp_str = (char *)malloc(temp_len + 1);      // 长度不为空就申请内存,字符串加一
        memset(temp_str, 0, temp_len + 1);                  // 先清空
        nvs_get_blob(wifi, key, temp_str, &temp_len);       // 再读取数据
        ESP_LOGI(TAG, "%s read %s : %s, %d", namespace_name, key, temp_str, temp_len); // 打印数据
        free(temp_str);                                     // 释放申请的空间
    }

    nvs_close(wifi);        // 关闭(卸载)命名空间
    nvs_close(temp_wifi);   // 关闭(卸载)没用的命名空间,没有任何影响,正常使用
}

void nvs_test_func(void)
{
    esp_err_t err = nvs_flash_init(); // 初始化默认的NVS分区
    if (err != ESP_OK) {
        ESP_LOGI(TAG, "nvs 初始化失败, 擦除后重试");
        ESP_ERROR_CHECK(nvs_flash_erase()); // 擦除扇区
        ESP_ERROR_CHECK(nvs_flash_init()); // 再次尝试初始化
    } else {
        ESP_LOGI(TAG, "nvs 初始化成功");
    }

    nvs_test_rw("wifi_0", "ssid", "my_wifi", sizeof("my_wifi")); // 打包测试内容
    nvs_test_rw("wifi_0", "pass", "1234567", sizeof("1234567"));

    nvs_test_rw("wifi_1", "ssid", "him_wifi", sizeof("him_wifi"));
    nvs_test_rw("wifi_1", "pass", "98765432", sizeof("98765432"));
}

四、spiffs(文件系统)

1.vfs

(1). 什么是VFS?

VFS是操作系统或嵌入式框架中常用的抽象概念,它通过统一的API(如openreadwriteclose等)屏蔽底层存储差异,允许开发者以相同的方式操作不同物理设备(如Flash、SD卡、SPIFFS、FATFS等)或网络文件系统。


(2). VFS的作用

  • 统一接口:不同文件系统(如SPIFFS、FATFS)或设备(如Flash、SD卡)的底层实现差异大,VFS提供标准化接口,避免为每个设备编写重复代码。
  • 动态挂载:支持运行时挂载/卸载不同存储介质(如将FATFS挂载到SD卡,SPIFFS挂载到Flash)。
  • 设备抽象:允许非文件系统设备(如UART、Socket)通过文件接口(/dev/uart)访问,实现“一切皆文件”的Unix哲学。
  • 多文件系统共存:例如,ESP32可同时使用SPIFFS存储配置和FATFS访问SD卡中的大文件。

(3). ESP平台中的VFS应用

在ESP-IDF(Espressif官方开发框架)中,VFS核心功能包括:

  • 默认支持的标准接口:通过open()fopen()等标准C库函数访问文件。
  • 串口/UART重定向:将标准输入/输出(stdin/stdout)映射到UART,方便调试(如printf输出到串口)。
  • Flash文件系统集成:支持SPIFFS、LittleFS等嵌入式文件系统通过VFS挂载。
  • FATFS扩展:通过wear_levelling库将FATFS挂载到Flash或SD卡。

(4). 常见应用场景

  • 嵌入式存储管理
    • 读写Flash中的配置文件(如SPIFFS)。
    • 访问SD卡中的音频、视频文件(FATFS)。
  • 设备调试与日志
    • 重定向日志输出到文件或串口。
  • 网络协议栈集成
    • 通过VFS将Socket抽象为文件(如Linux风格/dev/socket)。
  • 多存储介质切换
    • 动态切换不同存储设备而无需修改业务代码。

(5). 对比其他平台

类似概念也存在于其他系统:

  • Linux VFS:统一EXT4、NTFS、NFS等文件系统。
  • 嵌入式RTOS:如FreeRTOS的FatFS + VFS封装。

在ESP中,VFS更加轻量化,专注于嵌入式场景的存储和设备抽象。

总结
ESP的VFS是连接应用程序与底层存储/设备的关键层,它通过标准化接口降低了开发复杂度,尤其适合需要多存储介质支持或设备抽象的物联网应用。


2. spiffs

开发指南:.SPIFFS 文件系统
开发指南: SPIFFS 注意事项
推荐笔记: 关于SPIFF的使用——esp32学习笔记
推荐笔记: 26-ESP32-S3 的 FLASH分区表以及 SPIFFS 文件系统 和spiffsgen.py工具

(1) 概述

SPIFFS 是一个用于 SPI NOR flash 设备的嵌入式文件系统,支持磨损均衡、文件系统一致性检查等功能。


(2) 说明

  • 目前,SPIFFS 尚不支持目录,但可以生成扁平结构。如果 SPIFFS 挂载在 /spiffs 下,在 /spiffs/tmp/myfile.txt 路径下创建一个文件则会在 SPIFFS 中生成一个名为 /tmp/myfile.txt 的文件,而不是在 /spiffs/tmp 下生成名为 myfile.txt 的文件;
  • SPIFFS 并非实时栈,每次写操作耗时不等;
  • 目前,SPIFFS 尚不支持检测或处理已损坏的块。
  • SPIFFS 只能稳定地使用约 75% 的指定分区容量。
  • 当文件系统空间不足时,垃圾收集器会尝试多次扫描文件系统来寻找可用空间。根据所需空间的不同,写操作会被调用多次,每次函数调用将花费几秒。同一操作可能会花费不同时长的问题缘于 SPIFFS 的设计。
  • 当垃圾收集器尝试多次(默认为 10 次)扫描整个文件系统以回收空间时,在每次扫描期间,如果有可用的数据块,则垃圾收集器会释放一个数据块。因此,如果为垃圾收集器设置的最大运行次数为 n(可通过 SPIFFS_GC_MAX_RUNS 选项配置,该选项位于 SPIFFS 配置 中),那么 n 倍数据块大小的空间将可用于写入数据。如果尝试写入超过 n 倍数据块大小的数据,写入操作可能会失败并返回错误。
  • 如果 ESP32 在文件系统操作期间断电,可能会导致 SPIFFS 损坏。但是仍可通过 esp_spiffs_check 函数恢复文件系统。

(3) 注意事项

SPIFFS 文件系统提供了一定程度的断电安全性(参见重启后修复函数 esp_spiffs_check()),并具有内置磨损均衡机制。由于其垃圾回收机制,当专用分区使用超过大约 70% 时,文件系统性能可能会下降,且不支持目录。SPIFFS 适用于仅需管理少量(可能是大型)文件,并要求较高数据一致性的场景。通常,SPIFFSFatFS 占用更少的 RAM 空间,并支持最大 128 MB 的 flash 芯片。需要注意的是,SPIFFS 已不再开发和维护,因此请仔细斟酌是否优先选择此文件系统。


3. 挂载路径

在 SPIFFS 文件系统挂载时,base_path(基础路径前缀)是可以自定义更改的。在嵌入式系统(如 ESP32)中使用 SPIFFS(或其他文件系统)时,挂载路径的前缀(base_path)本质是一个命名空间标识,用于隔离不同文件系统实例或逻辑分区的访问路径。不同的前缀通常代表以下含义:


(1) 物理分区隔离

  • 若系统中有多个 SPIFFS 分区(如 storage1storage2),不同的前缀可以区分它们:
    // 分区1: 存储配置
    conf1.base_path = "/config";
    conf1.partition_label = "config_partition";
    
    // 分区2: 存储日志
    conf2.base_path = "/logs";
    conf2.partition_label = "log_partition";
    
    • 含义
      /config 路径下的文件属于 config_partition 分区,而 /logs 属于 log_partition

(2) 逻辑功能分类

  • 即使只有一个物理分区,也可以通过前缀对文件进行逻辑分类,提高代码可读性:
    .base_path = "/data/sensors";  // 存储传感器数据
    .base_path = "/data/config";   // 存储设备配置
    
    • 含义
      通过路径前缀明确文件用途(类似目录结构),例如:
      • /data/sensors/temperature.txt
      • /data/config/network.json

(3) 多文件系统兼容

  • 当同时使用 SPIFFSFATFS(如 SD 卡)时,前缀可区分不同文件系统:
    // SPIFFS(片内Flash)
    spiffs_conf.base_path = "/flash";
    
    // FATFS(SD卡)
    fatfs_conf.base_path = "/sd";
    
    • 含义
      /flash 下的文件在内部 Flash,而 /sd 下的文件在外部 SD 卡。

(4) 动态加载与热切换

  • 前缀可用于动态切换存储后端(如 A/B 固件备份):
    // 正常模式
    .base_path = "/fw/active";
    
    // 回滚模式
    .base_path = "/fw/backup";
    

(5) 用户权限隔离

  • 在支持多用户的系统中(如 Linux 风格设备),前缀可能代表用户空间:
    /user/alice/files  // 用户Alice的文件
    /user/bob/files    // 用户Bob的文件
    
    (注:ESP32 通常无多用户系统,此场景较少见)

(6) 关键注意事项

  1. 路径一致性
    所有文件操作(fopenunlink 等)必须基于完整前缀路径,否则会提示“文件不存在”。

    // 正确
    FILE* f = fopen("/data/config/device.txt", "r");
    
    // 错误(缺少前缀)
    FILE* f = fopen("/device.txt", "r");
    
  2. 避免冲突
    前缀不可与系统保留路径(如 /proc/dev)或其他挂载点重复。

  3. 结尾斜杠
    前缀通常不带结尾斜杠(/data 而非 /data/),但实际文件路径需要包含斜杠:

    /data/file.txt  // 正确
    /datafile.txt   // 错误(可能误解析为另一前缀)
    

(7) 总结

前缀的本质是对存储资源的逻辑抽象,通过路径命名实现:

  • 物理分层(分区、存储介质)
  • 功能分类(配置、数据、日志)
  • 动态管理(A/B 更新、多用户)

设计时应遵循 语义清晰避免冲突与业务逻辑匹配 的原则。


3. 代码测试

(1) 挂载

  • 创建新组件填入代码:
// spiffs_test.c 的初步内容如下
#include <stdio.h>
#include "spiffs_test.h"

#include "esp_spiffs.h" // 需要添加依赖 PRIV_REQUIRES spiffs
#include "esp_log.h"

static const char TAG[] = "spiffs_test.c";

void spiffs_test_func(void)
{
    esp_vfs_spiffs_conf_t conf_t = {
        .base_path = "/spiffs",         // 与文件系统关联的文件路径前缀
        .format_if_mount_failed = true, // 如果为true,则在挂载失败时格式化文件系统
        .max_files = 5,                 // 可同时打开的最大文件数
        .partition_label = "ffs_him",   // 可选,要使用的SPIFFS分区的标签。如果设置为NULL,则将使用subtype=spiffs的第一个分区
    };
    ESP_ERROR_CHECK(esp_vfs_spiffs_register(&conf_t)); // 注册(挂载)文件系统
    ESP_ERROR_CHECK(esp_spiffs_check(conf_t.partition_label)); // 检查SPIFFS的完整性

    size_t total_bytes = 0, used_bytes = 0; // 文件系统的大小,文件系统中当前已使用的字节数
    ESP_ERROR_CHECK(esp_spiffs_info(conf_t.partition_label, &total_bytes, &used_bytes)); // 获取SPIFFS的信息
    ESP_LOGI(TAG, "%s - total: %zu, used: %zu", conf_t.partition_label, total_bytes, used_bytes); // 使用 %zu 打印 size_t 类型
    
    if ((total_bytes < used_bytes) || (total_bytes > 16*1024)) {
        ESP_ERROR_CHECK(ESP_FAIL); // 返回错误,代表异常,可能分配大小太小了
    }

	// 这里是放下面的一些测试函数 ...
	    
    esp_vfs_spiffs_unregister(conf_t.partition_label); // 卸载
}
  • 分区表设置如下,注意分配大小和对其问题。
  • 如果打印长度不但对,就是异常!
// partitions_test.csv 分区表末尾添加,大小不能太低,否者会错误
// 实测貌似最低8k,注册完显示还剩251字节
ffs_him,  data, spiffs,  ,        16k,

// 如果第一次下载没有看到打印格式化内容,就代表其实没有成功,然后打印的长度也是错的,是负数!!!
// 打印的大小比实际偏小,是因为 SPIFFS 只能稳定地使用约 75% 的指定分区容量
W (302) SPIFFS: mount failed, -10025. formatting...
I (6072) spiffs_test.c: ffs_him - total: 7781, used: 0

(2) 读写文件

  • 直接使用常规的打开文件,读写操作。
// 调用测试函数
spiffs_test_rw_txt("/spiffs/him.txt"); // 测试读写 txt 文件
...
// 读写
void spiffs_test_rw_txt(const char *dir)
{
    FILE *f = fopen(dir, "w"); // 以写方式打开文件
    if (f == NULL)
    {
        ESP_LOGI(TAG, "%s -> Fopen Failure!", dir); // 打印错误
        return;
    }
    fprintf(f, "2025年7月7日22点27分"); // 填写字符串
    fclose(f);                          // 关闭文件

    vTaskDelay(pdMS_TO_TICKS(1000));

    f = fopen(dir, "r"); // 以读方式打开文件
    if (f == NULL)
    {
        ESP_LOGI(TAG, "%s -> Fopen Failure!", dir); // 打印错误
        return;
    }
    char temp[64] = {0};
    fgets(temp, sizeof(temp), f);        // 填写字符串
    ESP_LOGI(TAG, "%s : %s", dir, temp); // 打印读取的内容
    fclose(f);                           // 关闭文件
}
  • 监控到打印以下内容,已经使用 0.5kb 了
I (443) spiffs_test.c: ffs_him - total: 7781, used: 502
I (1533) spiffs_test.c: /spiffs/him.txt : 2025772227

(3) 烧录文件

  • 再组件内创建需要烧录的文件目录 files

在这里插入图片描述

  • 修改编译文件 CMakeLists.txt
idf_component_register(
    SRCS "spiffs_test.c"
                    
    INCLUDE_DIRS "include"
                    
    PRIV_REQUIRES spiffs
    )

# 将工程下相对目录中的文件打包成 SPIFFS 镜像
# 将镜像烧录到名为 ffs_him 的分区中
spiffs_create_partition_image(ffs_him ./files FLASH_IN_PROJECT)
  • 编写测试函数并添加
// 添加测试函数
spiffs_test_rw_txt("/spiffs/him.txt"); // 测试读写 txt 文件
spiffs_test_r("/spiffs/bin.bin"); // 读取文件内容
spiffs_test_r("/spiffs/txt.txt"); // 读取文件内容

// 只读
void spiffs_test_r(const char *dir)
{
    FILE *f = fopen(dir, "r"); // 以读方式打开文件
    if (f == NULL)
    {
        ESP_LOGI(TAG, "%s -> Fopen Failure!", dir); // 打印错误
        return;
    }
    char temp[64] = {0};
    fgets(temp, sizeof(temp), f);        // 填写字符串
    ESP_LOGI(TAG, "%s : %s", dir, temp); // 打印读取的内容
    fclose(f);                           // 关闭文件
}
  • 编译下载监控,可以看到已使用内容变大了,直接读取并打印也成功。
I (473) spiffs_test.c: ffs_him - total: 7781, used: 1506
I (1523) spiffs_test.c: /spiffs/him.txt : 2025772227I (1523) spiffs_test.c: /spiffs/bin.bin : 123456789
I (1523) spiffs_test.c: /spiffs/txt.txt : 2025772257

4. 读写API

  • 接口很多,不赘述了,基本上就分两种:
    1. 作为 普通二进制 按 字节长度 读取;
    2. 作为 文本字符串 按 回车换行 读取;

SPIFFS(SPI Flash File System)中,常用的文件操作接口可以按 二进制模式文本模式 分为两类,以下是核心接口的简明总结和典型用法:


1. 二进制模式(按字节读写)

适用于非文本文件(如图片、固件、配置块等),直接操作字节流。

(1) 打开/关闭文件
// 打开文件("rb" 读取,"wb" 写入,"ab" 追加)
FILE *f = fopen("/spiffs/data.bin", "rb"); 
if (f == NULL) {
    ESP_LOGE("SPIFFS", "文件打开失败");
    return;
}

// 关闭文件
fclose(f);
(2) 读取数据
uint8_t buffer[128];
size_t bytes_read = fread(buffer, 1, sizeof(buffer), f);  // 读取字节
if (bytes_read != sizeof(buffer)) {
    ESP_LOGW("SPIFFS", "未完整读取数据");
}
(3) 写入数据
uint8_t data[] = {0x01, 0x02, 0x03};
size_t bytes_written = fwrite(data, 1, sizeof(data), f);
if (bytes_written != sizeof(data)) {
    ESP_LOGE("SPIFFS", "写入失败");
}
(4) 随机访问
fseek(f, 0, SEEK_SET);  // 跳转到文件开头
long pos = ftell(f);    // 获取当前偏移量

2. 文本模式(按行读写)

适用于文本文件(如配置文件、日志),自动处理换行符。

(1) 读取一行
char line[256];
while (fgets(line, sizeof(line), f) != NULL) {
    // 去除换行符(可选)
    line[strcspn(line, "\n")] = '\0';
    ESP_LOGI("SPIFFS", "行内容: %s", line);
}
(2) 写入一行
fprintf(f, "当前温度: %.1f°C\n", temperature);  // 自动添加换行符
(3) 格式化读取
int year;
char month[10];
fscanf(f, "%d %s", &year, month);  // 从文件解析数据

3. 其他实用接口

(1) 检查文件是否存在/属性
struct stat st;
if (stat("/spiffs/config.txt", &st) == 0) {
    ESP_LOGI("SPIFFS", "文件大小: %ld 字节", st.st_size);
} else {
    ESP_LOGW("SPIFFS", "文件不存在");
}
(2) 删除文件
unlink("/spiffs/obsolete.log");
(3) 目录操作
DIR *dir = opendir("/spiffs");
if (dir) {
    struct dirent *ent;
    while ((ent = readdir(dir)) != NULL) {
        ESP_LOGI("SPIFFS", "找到文件: %s", ent->d_name);
    }
    closedir(dir);
}

4. 选择模式的准则

场景 推荐模式 原因
图片/固件/加密数据 二进制模式 避免换行符转换导致数据损坏
配置文件/CSV/日志 文本模式 直接处理可读的文本行
混合内容文件 二进制模式 统一用 fread/fwrite 控制

5. 注意事项

  1. 缓冲区溢出

    • fgets 会保证字符串以 \0 结尾,但需确保缓冲区足够大。
    • 二进制模式下,fread 需检查实际读取的字节数。
  2. 性能优化

    • 频繁读写小文件时,尽量 一次读取完整数据 到内存。
    • SPIFFS 的随机访问性能较差,顺序读写更高效。
  3. 错误处理

    • 所有操作后检查返回值(如 fread 返回的字节数、fopen 是否返回 NULL)。

代码模板:二进制文件复制

void copy_file(const char *src_path, const char *dst_path) {
    FILE *src = fopen(src_path, "rb");
    FILE *dst = fopen(dst_path, "wb");
    uint8_t buffer[1024];
    
    while (1) {
        size_t bytes = fread(buffer, 1, sizeof(buffer), src);
        if (bytes == 0) break;
        fwrite(buffer, 1, bytes, dst);
    }
    
    fclose(src);
    fclose(dst);
}

总结

  • 二进制模式:用 fread/fwrite + "rb"/"wb",适合精准控制字节流。
  • 文本模式:用 fgets/fprintf,简化文本行处理。
  • 务必检查返回值:SPIFFS 操作可能因存储损坏或空间不足失败。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐