Flash 分区表
ESP32 的 Flash(通常 4MB)通过分区表划分不同用途区域。分区表位于 Flash 的固定地址(0x8000),记录每个分区的类型、子类型、起始地址和大小。
NVS — 非易失性存储
NVS(Non-Volatile Storage)是 ESP-IDF 提供的键值对存储系统,数据存储在 Flash 的专用分区,断电后保留。它是存储 Wi-Fi 密码、设备配置、校准参数等小量数据的最佳方式。
NVS 核心概念
NVS 读写示例
#include "nvs_flash.h"
#include "nvs.h"
#include "esp_log.h"
#define NVS_NS "app_config" // 命名空间
static const char *TAG = "NVS";
void nvs_write_config(const char *ssid, const char *password, int32_t interval)
{
nvs_handle_t handle;
ESP_ERROR_CHECK(nvs_open(NVS_NS, NVS_READWRITE, &handle));
nvs_set_str(handle, "wifi_ssid", ssid);
nvs_set_str(handle, "wifi_pass", password);
nvs_set_i32(handle, "report_interval", interval);
ESP_ERROR_CHECK(nvs_commit(handle)); // 必须 commit 才真正写入 Flash
nvs_close(handle);
ESP_LOGI(TAG, "配置已保存");
}
void nvs_read_config(void)
{
nvs_handle_t handle;
esp_err_t err = nvs_open(NVS_NS, NVS_READONLY, &handle);
if (err == ESP_ERR_NVS_NOT_FOUND) {
ESP_LOGW(TAG, "命名空间不存在,使用默认配置");
return;
}
char ssid[33] = {0};
size_t ssid_len = sizeof(ssid);
nvs_get_str(handle, "wifi_ssid", ssid, &ssid_len);
int32_t interval = 60; // 默认值
nvs_get_i32(handle, "report_interval", &interval);
ESP_LOGI(TAG, "SSID: %s, 上报间隔: %" PRId32 "秒", ssid, interval);
nvs_close(handle);
}
void app_main(void)
{
/* 初始化 NVS(必须在 Wi-Fi 初始化前调用)*/
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase()); // 清空后重试
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
nvs_write_config("HomeWiFi", "password123", 30);
nvs_read_config();
}
LittleFS — 推荐文件系统
LittleFS 是 ARM Mbed 团队开发的嵌入式文件系统,专为 Flash 存储设计,具有断电安全、磨损均衡、压缩等特性。ESP-IDF v5 起推荐使用 LittleFS 替代老旧的 SPIFFS。
LittleFS 优点
- 支持真正的目录结构
- 原子操作(断电安全)
- 更好的磨损均衡
- 更快的文件系统修复
SPIFFS 局限性
- 不支持目录(所有文件在根)
- 断电可能损坏文件
- 大量小文件时性能下降
- 官方建议迁移到 LittleFS
#include "esp_littlefs.h"
#include "esp_log.h"
#include <stdio.h>
#include <sys/stat.h>
static const char *TAG = "LFS";
void littlefs_init(void)
{
esp_vfs_littlefs_conf_t conf = {
.base_path = "/littlefs", // 挂载点
.partition_label = "littlefs", // 分区表中的名称
.format_if_mount_failed = true, // 挂载失败时自动格式化
};
ESP_ERROR_CHECK(esp_vfs_littlefs_register(&conf));
size_t total = 0, used = 0;
esp_littlefs_info("littlefs", &total, &used);
ESP_LOGI(TAG, "文件系统: 总计 %d KB, 已用 %d KB", total/1024, used/1024);
}
void write_sensor_log(float temp, float hum, uint32_t timestamp)
{
FILE *f = fopen("/littlefs/sensor.csv", "a");
if (!f) {
ESP_LOGE(TAG, "无法打开文件");
return;
}
fprintf(f, "%" PRIu32 ",%0.1f,%0.1f\n", timestamp, temp, hum);
fclose(f);
}
void read_sensor_log(void)
{
FILE *f = fopen("/littlefs/sensor.csv", "r");
if (!f) return;
char line[64];
while (fgets(line, sizeof(line), f)) {
ESP_LOGI(TAG, "%s", line);
}
fclose(f);
}
将本地文件(HTML/配置文件/证书)打包到 LittleFS 镜像中:
1. 将文件放入 data/ 目录
2. CMakeLists.txt 中添加 littlefs_create_partition_image(littlefs ../data FLASH_IN_PROJECT)
3. idf.py flash 时会自动打包上传
SD 卡(SPI 模式)
#include "driver/spi_common.h"
#include "driver/sdspi_host.h"
#include "sdmmc_cmd.h"
#include "esp_vfs_fat.h"
#define SD_CS GPIO_NUM_5
#define SD_CLK GPIO_NUM_18
#define SD_MOSI GPIO_NUM_23
#define SD_MISO GPIO_NUM_19
void sd_card_init(void)
{
sdmmc_host_t host = SDSPI_HOST_DEFAULT();
spi_bus_config_t bus_cfg = {
.mosi_io_num = SD_MOSI,
.miso_io_num = SD_MISO,
.sclk_io_num = SD_CLK,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
};
spi_bus_initialize(host.slot, &bus_cfg, SDSPI_DEFAULT_DMA);
sdspi_device_config_t slot_config = SDSPI_DEVICE_CONFIG_DEFAULT();
slot_config.gpio_cs = SD_CS;
esp_vfs_fat_sdmmc_mount_config_t mount_config = {
.format_if_mount_failed = false,
.max_files = 5,
.allocation_unit_size = 16 * 1024,
};
sdmmc_card_t *card;
esp_vfs_fat_sdspi_mount("/sdcard", &host, &slot_config, &mount_config, &card);
sdmmc_card_print_info(stdout, card);
/* 之后可以用标准 POSIX 文件 API */
FILE *f = fopen("/sdcard/log.txt", "a");
fprintf(f, "ESP32 启动\n");
fclose(f);
}
SPI 模式下 SD 卡最高约 20~25 MHz,实际读写速度约 2~4 MB/s。若需更高速度(如连续录制音频/视频),应选用 SDMMC 4-bit 接口,但需注意引脚冲突(GPIO 12/13/14/15 与 JTAG 和 Flash 共用)。
NVS 存储 Flash 寿命管理
ESP32 内置 Flash 的擦写寿命约 100,000 次。NVS 通过磨损均衡(Wear Leveling)将写操作分散到整个分区,而不是重复写同一个扇区,大幅延长 Flash 使�命:
/* NVS 高级用法:批量操作与错误处理 */
#include "nvs_flash.h"
#include "nvs.h"
#include "esp_log.h"
typedef struct {
float temp_threshold;
uint32_t report_interval_s;
char server_url[128];
bool led_enabled;
} DeviceConfig_t;
/* 将整个结构体写入 NVS(使用 blob 类型)*/
esp_err_t config_save(const DeviceConfig_t *cfg)
{
nvs_handle_t h;
esp_err_t err = nvs_open("device_cfg", NVS_READWRITE, &h);
if (err != ESP_OK) return err;
err = nvs_set_blob(h, "config", cfg, sizeof(DeviceConfig_t));
if (err == ESP_OK) {
err = nvs_commit(h); /* commit 确保数据持久化到 Flash */
}
nvs_close(h);
return err;
}
/* 读取结构体,不存在则使用默认值 */
esp_err_t config_load(DeviceConfig_t *cfg)
{
/* 默认配置 */
*cfg = (DeviceConfig_t){
.temp_threshold = 30.0f,
.report_interval_s = 60,
.server_url = "mqtt://192.168.1.1",
.led_enabled = true
};
nvs_handle_t h;
esp_err_t err = nvs_open("device_cfg", NVS_READONLY, &h);
if (err == ESP_ERR_NVS_NOT_FOUND) {
ESP_LOGW("NVS", "无配置,使用默认值");
return ESP_OK; /* 不算错误 */
}
if (err != ESP_OK) return err;
size_t sz = sizeof(DeviceConfig_t);
err = nvs_get_blob(h, "config", cfg, &sz);
nvs_close(h);
return err;
}
/* NVS 诊断:检查分区使用情况 */
void nvs_stats_print(void)
{
nvs_stats_t stats;
nvs_get_stats(NULL, &stats);
ESP_LOGI("NVS", "已用条目: %d / 总计: %d(%d%% 使用率)",
stats.used_entries, stats.total_entries,
stats.used_entries * 100 / stats.total_entries);
ESP_LOGI("NVS", "可用命名空间: %d", stats.available_entries);
}
掉电时如果恰好在 Flash 写操作中,可能造成 NVS 页损坏(Page State = CORRUPT)。NVS 会自动标记损坏页并跳过,但如果损坏严重(所有页都损坏),nvs_flash_init() 会返回 ESP_ERR_NVS_NO_FREE_PAGES。此时正确处理方式是调用 nvs_flash_erase() 格式化 NVS 分区后再 init(会丢失所有 NVS 数据):
esp_err_t err = nvs_flash_init();
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
err = nvs_flash_init();
}
ESP_ERROR_CHECK(err);
ESP32 提供三种持久化存储方案:NVS(键值存储,适合配置参数和少量数据,内置磨损均衡)、文件系统(SPIFFS/LittleFS,适合网页、证书等文件,LittleFS 更稳定)、SD 卡(大容量日志和媒体文件)。NVS 使用 namespace 隔离不同组件的数据,写入后必须调用 nvs_commit 才能持久化。LittleFS 比 SPIFFS 抗掉电能力更强,新项目推荐使用。文件系统通过 VFS(虚拟文件系统)挂载到路径(如 /spiffs),可用标准 C 文件 API 读写。