Appearance
第八章:存储与 NVS
8.1 ESP32-S3 存储体系
存储层次(从快到慢,从小到大):
内部 SRAM(512KB)
└─ 运行时变量、栈、堆
└─ 断电丢失
外部 Flash(16MB,N16)
├─ 分区表(Partition Table)
│ ├─ bootloader(64KB)
│ ├─ nvs(16KB) ← 键值存储
│ ├─ phy_init(4KB)
│ ├─ app(主程序,~3MB)
│ └─ spiffs(剩余空间) ← 文件系统
└─ 断电保留
外部 PSRAM(8MB,R8)
└─ 大缓冲区(图像、音频)
└─ 断电丢失
SD 卡(可选,通过 SPI/SDMMC)
└─ 大容量文件存储8.2 NVS(非易失性存储)
NVS 特性
NVS = Non-Volatile Storage(非易失性存储)
类似于 Windows 注册表 / Linux /etc 配置文件
存储键值对:key → value
支持类型:int8/16/32/64, uint8/16/32/64, float, string, blob
命名空间:类似文件夹,隔离不同模块的数据
断电保留,支持磨损均衡基础读写
c
#include "nvs_flash.h"
#include "nvs.h"
#include "esp_log.h"
static const char *TAG = "NVS";
// 初始化 NVS(app_main 开头必须调用)
void nvs_init(void)
{
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES ||
ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
// NVS 分区损坏或版本不匹配,擦除重建
ESP_LOGW(TAG, "NVS 分区异常,正在擦除...");
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
ESP_LOGI(TAG, "NVS 初始化完成");
}
// 写入整数
esp_err_t nvs_write_int(const char *ns, const char *key, int32_t val)
{
nvs_handle_t handle;
esp_err_t ret;
ret = nvs_open(ns, NVS_READWRITE, &handle);
if (ret != ESP_OK) return ret;
ret = nvs_set_i32(handle, key, val);
if (ret == ESP_OK) {
ret = nvs_commit(handle); // 必须 commit 才真正写入!
}
nvs_close(handle);
return ret;
}
// 读取整数
esp_err_t nvs_read_int(const char *ns, const char *key,
int32_t *val, int32_t default_val)
{
nvs_handle_t handle;
esp_err_t ret;
ret = nvs_open(ns, NVS_READONLY, &handle);
if (ret != ESP_OK) {
*val = default_val;
return ret;
}
ret = nvs_get_i32(handle, key, val);
if (ret == ESP_ERR_NVS_NOT_FOUND) {
*val = default_val; // 键不存在,使用默认值
ret = ESP_OK;
}
nvs_close(handle);
return ret;
}
// 写入字符串
esp_err_t nvs_write_str(const char *ns, const char *key, const char *val)
{
nvs_handle_t handle;
esp_err_t ret = nvs_open(ns, NVS_READWRITE, &handle);
if (ret != ESP_OK) return ret;
ret = nvs_set_str(handle, key, val);
if (ret == ESP_OK) ret = nvs_commit(handle);
nvs_close(handle);
return ret;
}
// 读取字符串
esp_err_t nvs_read_str(const char *ns, const char *key,
char *buf, size_t buf_size)
{
nvs_handle_t handle;
esp_err_t ret = nvs_open(ns, NVS_READONLY, &handle);
if (ret != ESP_OK) return ret;
size_t required_size = buf_size;
ret = nvs_get_str(handle, key, buf, &required_size);
nvs_close(handle);
return ret;
}实用示例:保存配置
c
// 配置结构体
typedef struct {
char wifi_ssid[32];
char wifi_pass[64];
uint8_t led_brightness;
uint32_t boot_count;
} app_config_t;
#define NVS_NS "app_config"
// 保存配置
void config_save(const app_config_t *cfg)
{
nvs_handle_t h;
nvs_open(NVS_NS, NVS_READWRITE, &h);
nvs_set_str(h, "wifi_ssid", cfg->wifi_ssid);
nvs_set_str(h, "wifi_pass", cfg->wifi_pass);
nvs_set_u8(h, "led_bri", cfg->led_brightness);
nvs_set_u32(h, "boot_cnt", cfg->boot_count);
nvs_commit(h);
nvs_close(h);
ESP_LOGI(TAG, "配置已保存");
}
// 加载配置(带默认值)
void config_load(app_config_t *cfg)
{
nvs_handle_t h;
esp_err_t ret = nvs_open(NVS_NS, NVS_READONLY, &h);
if (ret == ESP_OK) {
size_t len;
len = sizeof(cfg->wifi_ssid);
if (nvs_get_str(h, "wifi_ssid", cfg->wifi_ssid, &len) != ESP_OK)
strcpy(cfg->wifi_ssid, "");
len = sizeof(cfg->wifi_pass);
if (nvs_get_str(h, "wifi_pass", cfg->wifi_pass, &len) != ESP_OK)
strcpy(cfg->wifi_pass, "");
if (nvs_get_u8(h, "led_bri", &cfg->led_brightness) != ESP_OK)
cfg->led_brightness = 50;
if (nvs_get_u32(h, "boot_cnt", &cfg->boot_count) != ESP_OK)
cfg->boot_count = 0;
nvs_close(h);
} else {
// 首次运行,使用默认值
memset(cfg, 0, sizeof(*cfg));
cfg->led_brightness = 50;
cfg->boot_count = 0;
}
}
void app_main(void)
{
nvs_init();
app_config_t cfg;
config_load(&cfg);
cfg.boot_count++;
ESP_LOGI(TAG, "第 %"PRIu32" 次启动", cfg.boot_count);
config_save(&cfg);
}8.3 SPIFFS 文件系统
c
#include "esp_spiffs.h"
#include <stdio.h>
#include <dirent.h>
// 挂载 SPIFFS
void spiffs_init(void)
{
esp_vfs_spiffs_conf_t conf = {
.base_path = "/spiffs", // 挂载点
.partition_label = NULL, // 使用默认分区
.max_files = 5,
.format_if_mount_failed = true, // 挂载失败则格式化
};
ESP_ERROR_CHECK(esp_vfs_spiffs_register(&conf));
size_t total = 0, used = 0;
esp_spiffs_info(NULL, &total, &used);
ESP_LOGI(TAG, "SPIFFS: 总 %d KB,已用 %d KB",
total / 1024, used / 1024);
}
// 写文件
void spiffs_write_file(const char *path, const char *content)
{
FILE *f = fopen(path, "w");
if (f == NULL) {
ESP_LOGE(TAG, "无法创建文件: %s", path);
return;
}
fprintf(f, "%s", content);
fclose(f);
ESP_LOGI(TAG, "文件写入: %s", path);
}
// 读文件
void spiffs_read_file(const char *path)
{
FILE *f = fopen(path, "r");
if (f == NULL) {
ESP_LOGE(TAG, "文件不存在: %s", path);
return;
}
char line[128];
while (fgets(line, sizeof(line), f)) {
printf("%s", line);
}
fclose(f);
}
// 列出目录
void spiffs_list_dir(const char *path)
{
DIR *dir = opendir(path);
if (!dir) return;
struct dirent *entry;
while ((entry = readdir(dir)) != NULL) {
ESP_LOGI(TAG, " %s", entry->d_name);
}
closedir(dir);
}
void app_main(void)
{
nvs_init();
spiffs_init();
// 写入配置文件
spiffs_write_file("/spiffs/config.txt",
"brightness=80\nwifi_ssid=MyWiFi\n");
// 读取
spiffs_read_file("/spiffs/config.txt");
// 列出文件
spiffs_list_dir("/spiffs");
}8.4 分区表配置
自定义分区表(partitions.csv)
csv
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x6000,
phy_init, data, phy, 0xf000, 0x1000,
factory, app, factory, 0x10000, 3M,
spiffs, data, spiffs, , 12M,在 CMakeLists.txt 中指定
cmake
set(PARTITION_TABLE_CSV_PATH "${CMAKE_CURRENT_LIST_DIR}/partitions.csv")或在 menuconfig 中: Partition Table → Custom partition table CSV
📝 第八章练习题
练习 8-1:开机计数器(基础)
目标:掌握 NVS 基础读写
任务:
- 每次开机读取 NVS 中的计数值
- 计数 +1 后写回 NVS
- 打印:
这是第 N 次开机 - 断电重启后计数不丢失
练习 8-2:配置管理器(基础)
目标:NVS 结构化数据存储
任务:实现配置管理模块,支持:
config set <key> <value>→ 保存配置config get <key>→ 读取配置config list→ 列出所有配置config reset→ 恢复默认值
通过 UART 命令行交互,配置断电保留。
练习 8-3:日志文件(进阶)
目标:SPIFFS 文件操作
任务:
- 每次开机在
/spiffs/log.txt追加一条记录 - 格式:
[开机次数] [时间戳] 系统启动 - 文件超过 10KB 时,自动删除旧文件重建
- 通过 UART 命令
log show打印日志内容
练习 8-4:参数存储(进阶)
目标:NVS Blob 存储结构体
任务:
- 定义一个传感器校准参数结构体(包含偏移量、增益等)
- 使用 NVS Blob 存储整个结构体
- 实现校准流程:采集 10 次数据,计算平均值作为偏移量
- 保存校准参数,下次开机自动加载
NVS Blob API:
c
nvs_set_blob(handle, "calib", &calib_data, sizeof(calib_data));
nvs_get_blob(handle, "calib", &calib_data, &size);练习 8-5:思考题
NVS 写入时为什么必须调用
nvs_commit()?如果不调用会发生什么?Flash 的擦写次数有限(通常 10 万次),NVS 如何通过磨损均衡延长寿命?
SPIFFS 和 FAT 文件系统各有什么优缺点?什么时候选择 SPIFFS,什么时候选择 FAT(SD 卡)?
NVS 分区默认只有 16KB,能存多少数据?如果需要存储更多配置怎么办?
本章小结
| 知识点 | 掌握程度自评 |
|---|---|
| NVS 初始化 | ⬜⬜⬜⬜⬜ |
| NVS 键值读写 | ⬜⬜⬜⬜⬜ |
| NVS Blob 存储 | ⬜⬜⬜⬜⬜ |
| SPIFFS 挂载 | ⬜⬜⬜⬜⬜ |
| 文件读写操作 | ⬜⬜⬜⬜⬜ |
| 分区表配置 | ⬜⬜⬜⬜⬜ |
上一章:← FreeRTOS 基础下一章:Wi-Fi 编程 →