前言
stm32与esp32的flash区别?
特性 | ESP32 | STM32 |
---|---|---|
Flash物理位置 | 集成SPI Flash(非CPU直连) | 内部Flash(CPU直接访问) |
管理方式 | 通过分区表动态划分 | 固定地址链接脚本分配 |
灵活性 | 可动态调整分区大小 | 需重新编译修改链接脚本 |
- ESP32可额外连接外部SPI Flash(如扩展存储),但分区表默认不管理外部Flash。需手动通过SPI驱动或文件系统(如LittleFS)访问。
一、分区表的作用
-
Flash 物理特性: ESP32 的 SPI Flash 存储芯片本质上是连续的线性地址空间。分区表将这个物理空间逻辑划分成多个独立的区域,每个区域有特定的用途。
-
功能隔离: 固件包含多个功能组件(如 Bootloader、应用程序、文件系统、配置数据、OTA 更新数据等)。分区表将它们隔离在不同区域,防止互相覆盖或干扰。
-
灵活管理: 允许开发者根据项目需求(如应用程序大小、文件系统大小、OTA 支持等)灵活配置各分区的大小和位置。
-
启动流程: Bootloader 依赖分区表来确定从哪个分区加载应用程序并启动。
-
OTA: 它定义了主应用程序分区、OTA 数据分区以及至少一个 OTA 应用程序分区(用于存储新固件)。
-
数据持久化: 为 NVS (Non-Volatile Storage)、FATFS、SPIFFS 等文件系统或数据存储提供专用的、受保护的空间。
-
安全: 支持定义安全相关的分区(如 NVS 加密密钥分区)。
二、分区表的结构
1. 默认bootloader区域 (0x0000 - 0x9000)
地址 | 描述 | 大小 | 说明 |
---|---|---|---|
0x0000 | Bootloader 头部信息 | 4KB | 包含 Bootloader 的元数据(如 Flash 加密密钥、芯片兼容性标志等) |
0x1000 | Bootloader 代码 | 28KB | 实际的 Bootloader 可执行代码(负责初始化硬件、加载分区表、启动应用程序) |
0x8000 | 分区表 (Partition Table) | 4KB | 定义 Flash 的分区结构(如 app、data 等分区的偏移和大小) |
0x9000 | 第一个用户分区(如 nvs) | - | 用户自定义分区的起始地址(如 nvs 分区) |
-
默认情况下,不要手动修改 0x0 - 0x9000 的内容:
错误的写入可能导致设备无法启动(需重新烧写 Bootloader 和分区表)。 -
扩展 Bootloader 需谨慎:
如果自定义 Bootloader 超过 28KB,需调整分区表偏移(但需确保与 ESP-IDF 兼容)。 -
加密影响:
启用 Flash 加密后,0x0 - 0x9000 的内容会被加密,但 Bootloader 会先解密再执行。
1. Bootloader 区域 (0x1000 - 0x8000)
- 大小:
固定为 0x7000(28KB),由编译时 bootloader.bin 决定。 - 作用:
初始化硬件(CPU 时钟、SPI Flash、串口等)。从 0x8000 读取分区表。根据分区表加载应用程序(从 app 分区)或进入 OTA 流程。 - 不可修改:
除非重新编译并烧写 Bootloader,否则此区域内容不会变化。
2. 分区表区域 (0x8000 - 0x9000)
就是这个文件partitions.csv的内容
- 大小:
固定 4KB(即使分区表实际内容不足 4KB,也会占用完整 Sector)。 - 内容:
二进制格式的分区表条目(每个条目 32 字节)。包含所有分区的名称、类型、子类型、偏移、大小和标志。 - 查看方法:
使用 esptool.py 读取并解析:
# 连接ESP32,然后读取0x8000位置,大小0x1000,生成文件partitions.bin,参考下午
esptool.py read_flash 0x8000 0x1000 test.bin
# 然后解析文件
gen_esp32part.py test.bin
3. 0x9000 之前未使用的空间
- 0x8000 - 0x9000 之间的剩余空间:
分区表仅占用 0x8000 开始的 4KB,但 0x9000 是下一个分区的起始地址,因此中间可能有未使用的空间(具体取决于分区表定义)。 - 示例:
如果分区表实际只占用 1KB,剩余的 0x8000 + 0x1000 到 0x9000(即 3KB)是空闲的,但无法直接利用(因为 Flash 擦除最小单位为 4KB Sector)。
2. partitions.csv 文件结构 (0x9000 - )
这个文件partitions.csv的内容存放在(0x8000 - 0x9000) 内
1. menuconfig里分区相关配置
- Single factory app, no OTA (默认)
适用于 无 OTA 功能 的固件,仅包含一个 factory 应用分区
# ESP-IDF Partition Table
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 24K,
phy_init, data, phy, 0xf000, 4K,
factory, app, factory,0x10000, 2M,
storage, data, fat, 0x210000, 1M,
- Single factory app (large), no OTA
适用于 无 OTA 功能 的固件,仅包含一个 factory 应用分区
# ESP-IDF Partition Table
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 24K,
phy_init, data, phy, 0xf000, 4K,
factory, app, factory,0x10000, 1500K,
- Factory app, two OTA definitions
支持 OTA 升级,预留 ota_0 和 ota_1 分区
# ESP-IDF Partition Table
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 16K,
otadata, data, ota, 0xd000, 8K,
phy_init, data, phy, 0xf000, 4K,
factory, app, factory,0x10000, 1M,
ota_0, app, ota_0, 0x110000, 1M,
ota_1, app, ota_1, 0x210000, 1M,
- Custom partition table CSV
需要配置自定义分区CSV文件的名称。此路径默认在项目根目录。但是,如果提供了CSV文件的绝对路径,则配置绝对路径。
下面是一个参考例子
- Bootloader: 0x1000 - 0x8000 (隐含)。
- 分区表: 0x8000 (固定)。
- nvs (加密): 0x9000 (紧接分区表后), 24KB。存储加密的设备配置。
- otadata: 0xf000 (自动计算 0x9000+0x6000=0xF000), 8KB。管理 OTA 状态。
- phy_init: 0x11000 (0xF000+0x2000=0x11000), 4KB。RF 参数。
- factory: 0x12000 (0x11000+0x1000=0x12000), 1MB。出厂应用/回退。
- ota_0 / ota_1: 各 1MB。OTA 应用。
- spiffs: 0x312000 (0x212000+1M=0x312000), 3MB。文件系统存储网页、日志等。
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x6000, encrypted
otadata, data, ota, 0xf000, 0x2000,
phy_init, data, phy, 0x11000, 0x1000,
factory, app, factory, 0x12000, 1M,
ota_0, app, ota_0, 0x112000,1M,
ota_1, app, ota_1, 0x212000,1M,
spiffs, data, spiffs, 0x312000,3M,
总大小: 0x312000 (3, 145, 728) + 3MB (3, 145, 728) = 6, 291, 456 Bytes ≈ 6MB。剩余约 2MB 未使用(在 0x612000 - 0x800000 之间),可作为未来扩展预留。
三、关键字段说明
1. Name: 分区的逻辑名称。
- 在代码中可以通过这个名称来操作分区(如挂载文件系统、获取分区信息)。在同一张分区表中必须唯一。常见名称示例: nvs, otadata, factory, ota_0, ota_1, storage, coredump。
2. Type: 定义分区的主要类型。
ESP-IDF 预定义了几种核心类型:
app: 应用程序分区。
- 用于存储可执行的 ESP-IDF 应用程序固件。Bootloader 会从这里加载并运行代码。至少需要一个 app 分区(通常是 factory 或 ota_X)。
data: 数据分区。
- 用于存储非执行代码的数据,如配置文件、文件系统、OTA 元数据、NVS 数据、PHY 初始化数据等。
(0x40-0xFE): 自定义类型。
- 用户可以根据需要定义自己的类型(使用十六进制值)。
3. SubType: 子类型
[app]factory: 出厂应用程序分区。
- 通常是 Bootloader 的默认启动选择(如果没有 OTA 数据或 OTA 数据无效)。
[app]ota_0 到 ota_15: OTA 应用程序分区。
- 用于存储通过 OTA 更新的应用程序固件。ota_data 分区记录当前活动的 OTA 槽位 (ota_0 或 ota_1 等)。
[app]test: 用于工厂测试程序。
[data]ota: OTA 数据分区。
- 存储 OTA 升级过程的元数据,最重要的是记录当前活动的 OTA 应用程序 (ota_0, ota_1 等)。大小通常为 0x2000 (8KB)。
[data]phy: PHY 初始化数据分区。
- 存储 RF 物理层 (PHY) 的初始化参数。如果未提供,使用编译到 Bootloader 中的默认 PHY 参数。大小通常为 0x1000 (4KB)。
[data]nvs: NVS (Non-Volatile Storage) 分区。
- 用于存储键值对形式的配置数据(Wi-Fi SSID/密码、设备参数等)。ESP-IDF 的 nvs_flash 库使用它。推荐最小 0x3000 (12KB)。
[data]nvs_keys: NVS 密钥分区。
- 如果启用了 NVS 加密,此分区存储用于加密 NVS 分区的密钥。大小通常为 0x1000 (4KB)。
[data]spiffs, fat, littlefs: 文件系统分区。
- 用于存储 SPIFFS、FATFS 或 LittleFS 文件系统的数据。
[data]coredump: Core Dump 分区。
- 用于存储发生严重错误(如崩溃)时的内存转储信息,便于后续调试。
[data]efuse_em: 模拟 eFuse 分区 (高级用法)。
[data]undefined / (0x00): 通用数据分区,子类型未指定。
- 用户可以在代码中自定义其用途。
[ (0x40-0xFE)]: 自定义子类型
- 用户可自定义子类型含义
4. Offset: 起始地址
-
该分区在 Flash 中的起始地址(十六进制表示)。绝对关键参数! 必须指定。必须满足:
- 对齐: 偏移地址必须对齐到 0x1000 (4KB) 边界(Flash 擦除操作的最小单位通常是 Sector=4KB)。gen_esp32part.py 会强制对齐,但最好在 CSV 中明确指定对齐的地址。
- 顺序: 分区在 Flash 中的物理顺序应与它们在表中定义的顺序一致(偏移地址递增)。
- 不重叠: 分区的偏移+大小不能超出 Flash 总大小,且分区之间不能有重叠区域。
-
0xNNNN 或 ‘-’: 如果设置为 -,工具会自动计算偏移,使其紧接在前一个分区之后(考虑对齐)。
5. Size: 分区的大小
- 必须指定大小
示例: 0x1000 (4KB), 16K (16KB), 1M (1MB), 0x200000 (2MB)。 - 大小也必须满足 4KB 对齐(工具会自动向上取整到最近的 4KB 边界)。
6. Flags (可选):
- 为分区设置附加标志(可选字段)。当前主要支持的标志是 encrypted。
- encrypted: 标记该分区在 Flash 加密启用时需要被加密。Bootloader 在加载 app 分区前会自动解密它。数据分区如果包含敏感信息(如 NVS),也应标记为 encrypted。
- 多个标志用逗号分隔(目前只有 encrypted 有意义)。
四、 实际应用案例
1. esptool.py和gen_esp32part.py工具
- esptool.py:烧录分区表到 Flash
# 烧录分区表
esptool.py -p /dev/ttyUSB0 -b 460800 write_flash 0x8000 partitions.bin
# 读取现有分区表
esptool.py -p /dev/ttyUSB0 read_flash 0x8000 0x1000 partition_table.bin
# 验证烧录内容
esptool.py -p /dev/ttyUSB0 verify_flash 0x8000 partitions.bin
# 擦除分区表区域
esptool.py -p /dev/ttyUSB0 erase_region 0x8000 0x1000
- gen_esp32part.py:
- 将 CSV 格式的分区表 转换为 二进制文件(供烧录到 Flash)。
- 将 二进制分区表 反向解析为 CSV 文件(用于查看或修改)。
# 将 partitions.csv 转换为二进制文件
gen_esp32part.py partitions.csv partitions.bin
# 从 Flash 中读取的分区表二进制文件解析为 CSV
gen_esp32part.py partition_table.bin partition_table.csv
# 检查 CSV 文件是否有错误(如偏移重叠、未对齐等)
gen_esp32part.py --verify partitions.csv
2. idf.py flash工具
- 用于将编译生成的固件(包括 Bootloader、分区表、应用程序、文件系统等)一键烧录到 ESP32 的 Flash 中
默认烧写固件
- -p:指定串口 -b:设置烧录波特率
idf.py flash -p /dev/ttyUSB0 -b 921600
仅烧录应用程序
- 不更新 Bootloader 或分区表
- 适用场景:代码修改后快速测试,节省烧录时间。
idf.py app-flash
仅烧录分区表
idf.py partition-table-flash
仅烧录文件系统
- 前提:项目中配置了文件系统分区(如 SPIFFS、FATFS)。
idf.py storage-flash
3. 读取 NVS、SPIFFS 和 FATFS 数据
#include "nvs_flash.h"
#include "esp_spiffs.h"
#include "esp_vfs_fat.h"
#include "sdmmc_cmd.h"
// 读取 NVS 键值对数据
void read_nvs_data() {
nvs_handle_t nvs_handle;
esp_err_t ret;
// 初始化 NVS
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);
// 打开命名空间
ret = nvs_open("storage", NVS_READONLY, &nvs_handle);
if (ret != ESP_OK) {
printf("NVS 打开失败: %s\n", esp_err_to_name(ret));
return;
}
// 读取整数
int32_t restart_count = 0;
ret = nvs_get_i32(nvs_handle, "restart_count", &restart_count);
printf("设备重启次数: %d\n", restart_count);
// 读取字符串
char ssid[32] = {0};
size_t ssid_len = sizeof(ssid);
ret = nvs_get_str(nvs_handle, "wifi_ssid", ssid, &ssid_len);
printf("WiFi SSID: %s\n", ssid);
nvs_close(nvs_handle);
}
// 读取 SPIFFS 文件数据
void read_spiffs_data() {
// 配置并挂载 SPIFFS
esp_vfs_spiffs_conf_t conf = {
.base_path = "/spiffs",
.partition_label = "spiffs_storage",
.max_files = 5,
.format_if_mount_failed = false
};
esp_err_t ret = esp_vfs_spiffs_register(&conf);
if (ret != ESP_OK) {
printf("SPIFFS 挂载失败: %s\n", esp_err_to_name(ret));
return;
}
// 打开并读取文件
FILE* f = fopen("/spiffs/config.json", "r");
if (f == NULL) {
printf("无法打开配置文件\n");
} else {
char buffer[256];
while (fgets(buffer, sizeof(buffer), f) != NULL) {
printf("%s", buffer);
}
fclose(f);
}
// 卸载 SPIFFS
esp_vfs_spiffs_unregister(conf.partition_label);
}
// 读取 FATFS 数据 (SD卡示例)
void read_fatfs_data() {
sdmmc_host_t host = SDMMC_HOST_DEFAULT();
sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT();
// FATFS 挂载配置
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_err_t ret = esp_vfs_fat_sdmmc_mount("/sdcard", &host, &slot_config, &mount_config, &card);
if (ret != ESP_OK) {
printf("SD卡挂载失败: %s\n", esp_err_to_name(ret));
return;
}
// 读取日志文件
FILE* f = fopen("/sdcard/system.log", "r");
if (f == NULL) {
printf("无法打开日志文件\n");
} else {
char line[128];
printf("系统日志内容:\n");
while (fgets(line, sizeof(line), f) != NULL) {
printf("%s", line);
}
fclose(f);
}
// 卸载文件系统
esp_vfs_fat_sdmmc_unmount();
}
void app_main() {
// 读取三种存储的数据
read_nvs_data();
read_spiffs_data();
read_fatfs_data();
}
五、相关问题
1. OTA1 OTA2与factory的关系
分区 | 子类型 | 作用 |
---|---|---|
factory | app, factory | 出厂固件,作为最终回退版本(安全备份),通常不参与常规OTA更新。 |
ota_0 | app, ota_0 | 第一个OTA(OTA1),用于存储通过OTA更新的固件。 |
ota_1 | app, ota_1 | 第二个OTA(OTA2),用于交替存储OTA更新的固件。 |
otadata | data, ota | 记录当前活动的OTA分区(如选择 ota_0 或 ota_1),大小固定为 8KB。 |