【esp32s3】3-日志/分区表/nvs/spiffs
配套代码仓库: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
可选级别:None、Error、Warning、Info、Debug、Verbose。 -
作用规则:
只有等于或高于设定级别的日志才会被输出。例如:- 设为
Info时:Error、Warning、Info会输出,Debug和Verbose被过滤。 - 设为
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_TAG为Debug,实际仍只能输出Info及以上日志。 - 需在
menuconfig中启用CONFIG_LOG_ALLOW_OVERRIDE_LEVEL以允许覆盖默认级别。
(3). 临时禁用日志
若需完全关闭某个 TAG 的日志,将其级别设为 ESP_LOG_NONE:
esp_log_level_set("NOISY_TAG", ESP_LOG_NONE); // 禁用该 TAG 所有日志
5. 注意事项
- TAG 必须匹配:
esp_log_level_set()的 TAG 需与日志语句中的ESP_LOGx("TAG", ...)完全一致(区分大小写)。 - 实时生效:
修改日志级别后,后续日志会立即遵循新规则。 - 性能开销:
高频调用esp_log_level_set()可能影响性能,建议在初始化阶段设置。 - 动态调整:
通过代码动态调整日志级别,可以精准控制调试信息的输出范围,无需重新编译或修改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_ERROR或ESP_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 # 更新默认配置文件
- 编译下载监控能看到引导程序的打印内容。可以看到实际的
Type和ST的枚举类型。
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(非易失性存储)等。
- 子类型:
nvs、phy、fat、spiffs等。
- 子类型:
- 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. 关键注意事项
- OTA更新:至少需要2个
app类型分区(如ota_0和ota_1)。 - 文件系统:如FAT/SPIFFS,需确保分区足够大(例如>=1MB)。
- 偏移对齐:默认分区表从
0x8000开始,用户分区通常从0x10000开始。 - 动态大小:最后一个分区可用
-占满剩余空间。
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(键值对存储)
- 初学时使用的存储方式:
- 一个扇区存储几个变量;统一读写擦除;
- 几个扇区存储几个结构体;统一读写擦除;
- 一个扇区使用键值对;键为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. 代码测试
疑问:
- 可以同时打开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(如open、read、write、close等)屏蔽底层存储差异,允许开发者以相同的方式操作不同物理设备(如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)。
- 通过VFS将Socket抽象为文件(如Linux风格
- 多存储介质切换
- 动态切换不同存储设备而无需修改业务代码。
(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 适用于仅需管理少量(可能是大型)文件,并要求较高数据一致性的场景。通常,SPIFFS 比 FatFS 占用更少的 RAM 空间,并支持最大 128 MB 的 flash 芯片。需要注意的是,SPIFFS 已不再开发和维护,因此请仔细斟酌是否优先选择此文件系统。
3. 挂载路径
在 SPIFFS 文件系统挂载时,base_path(基础路径前缀)是可以自定义更改的。在嵌入式系统(如 ESP32)中使用 SPIFFS(或其他文件系统)时,挂载路径的前缀(base_path)本质是一个命名空间标识,用于隔离不同文件系统实例或逻辑分区的访问路径。不同的前缀通常代表以下含义:
(1) 物理分区隔离
- 若系统中有多个 SPIFFS 分区(如
storage1和storage2),不同的前缀可以区分它们:// 分区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) 多文件系统兼容
- 当同时使用 SPIFFS 和 FATFS(如 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 风格设备),前缀可能代表用户空间:
(注:ESP32 通常无多用户系统,此场景较少见)/user/alice/files // 用户Alice的文件 /user/bob/files // 用户Bob的文件
(6) 关键注意事项
-
路径一致性
所有文件操作(fopen、unlink等)必须基于完整前缀路径,否则会提示“文件不存在”。// 正确 FILE* f = fopen("/data/config/device.txt", "r"); // 错误(缺少前缀) FILE* f = fopen("/device.txt", "r"); -
避免冲突
前缀不可与系统保留路径(如/proc、/dev)或其他挂载点重复。 -
结尾斜杠
前缀通常不带结尾斜杠(/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 : 2025年7月7日22点27分
(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 : 2025年7月7日22点27分
I (1523) spiffs_test.c: /spiffs/bin.bin : 123456789
I (1523) spiffs_test.c: /spiffs/txt.txt : 2025年7月7日22点57分
4. 读写API
- 接口很多,不赘述了,基本上就分两种:
- 作为 普通二进制 按 字节长度 读取;
- 作为 文本字符串 按 回车换行 读取;
在 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. 注意事项
-
缓冲区溢出
fgets会保证字符串以\0结尾,但需确保缓冲区足够大。- 二进制模式下,
fread需检查实际读取的字节数。
-
性能优化
- 频繁读写小文件时,尽量 一次读取完整数据 到内存。
- SPIFFS 的随机访问性能较差,顺序读写更高效。
-
错误处理
- 所有操作后检查返回值(如
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 操作可能因存储损坏或空间不足失败。
更多推荐


所有评论(0)