ESP32 Flash 存储实战:从分区、FATFS 到数据流的完整指南
前言:给数据一个可靠的家
在上一篇文章中,我们记录了如何克服重重困难,最终获取到正确的 IMU 数据。但数据的正确性仅仅是第一步,如何将这些高速产生的数据流(本项目中 IMU 数据帧约为 47 字节/帧,频率可达数百赫兹)高效、可靠地存储下来,是所有物联网数据采集项目的核心挑战。
本文将详细剖析本项目中的数据存储方案:从最底层的 Flash 分区规划,到文件系统的选择与挂载,再到最终的数据读写策略。
第一章:规划蓝图 - partitions.csv
ESP32 的内部 Flash 并非一整块可随意使用的空间。它被一张“分区表”划分为不同的区域,用于存放 Bootloader、应用程序、NVS 数据等。要存储我们自己的大文件,第一步就是在这张蓝图上,为我们的数据规划出一块专属的“地皮”。
这一切都在 partitions.csv 文件中定义。我们为 IMU 数据添加了一个名为 storage_part 的自定义分区:
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x6000,
phy_init, data, phy, 0xf000, 0x1000,
factory, app, factory, 0x10000, 2M,
storage_part, data, fat, , 1M,
让我们来解读 storage_part 这一行:
- Name (
storage_part): 为这个分区起一个独一无二的名字,我们将在代码中通过这个名字来找到它。 - Type (
data): 表明这是一个数据分区,而非可执行的应用程序分区。 - SubType (
fat): 指定这个分区的用途是存放一个 FAT 文件系统。 - Offset (): 我们将偏移量留空,构建系统会自动计算并放置在上一个分区(
factory)之后,避免了手动计算的麻烦。 - Size (
1M): 我们为它分配了 1MB 的空间,足以存储大量的 IMU 数据。
有了这个规划,ESP-IDF 在编译和烧录时,就会在 Flash 的指定位置,为我们预留出这 1MB 的空间。
第二章:搭建文件系统 - FATFS 与磨损均衡
有了“地皮”,我们还需要建立一个“文件管理系统”,而不是直接在原始的 Flash 地址上“堆放”数据。文件系统为我们带来了创建、删除、读写文件和目录的便利。
本项目选用了 ESP-IDF 官方支持的 FATFS 文件系统,并搭配了**磨损均衡(Wear Levelling)**层。
- FATFS: 一个轻量级、兼容性极佳的文件系统,非常适合资源有限的嵌入式设备。
- 磨损均衡 (Wear Levelling): SPI Flash 芯片的每个块都有读写次数的寿命限制。如果我们总是在同一个位置写入数据(例如日志文件),那个位置的 Flash 块会很快损坏。磨损均衡层就像一个聪明的管家,它会将写入操作均匀地分布到整个分区的不同物理块上,从而极大延长 Flash 的使用寿命。
在 main.cpp 的 init_fatfs 函数中,我们完成了文件系统的初始化和挂载:
void init_fatfs()
{
// 1. 通过名字找到我们定义的分区
const esp_partition_t* fat_partition = esp_partition_find_first(
ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_FAT, "storage_part");
if (!fat_partition) {
ESP_LOGE(TAG, "Failed to find FATFS partition 'storage_part'");
return;
}
// 2. 定义挂载配置
const esp_vfs_fat_mount_config_t mount_config = {
.format_if_mount_failed = true, // 关键!如果挂载失败(例如首次使用),则自动格式化
.max_files = 4, // 最大同时打开文件数
.allocation_unit_size = CONFIG_WL_SECTOR_SIZE
};
// 3. 将分区挂载到虚拟文件系统(VFS)的 "/storage" 路径下
// 并启用磨损均衡(WL)
wl_handle_t wl_handle = WL_INVALID_HANDLE;
esp_err_t err = esp_vfs_fat_spiflash_mount_rw_wl(
"/storage", "storage_part", &mount_config, &wl_handle);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to mount FATFS (%s)", esp_err_to_name(err));
return;
}
ESP_LOGI(TAG, "FATFS mounted successfully at /storage");
}
执行完这段代码后,我们就可以像操作电脑上的文件一样,通过标准 C 文件 I/O 函数(fopen, fwrite 等)来读写 /storage 目录下的文件了。
第三章:数据 I/O 策略
写入数据:直接、原子化
在经历了惨痛的内存损坏 bug 后,我们最终采用了一种最直接、最不容易出错的数据写入策略。该逻辑位于 imu_calibration.cpp 中。
-
开始采集 (
imu_start_calibration):const char* file_path = "/storage/imu_data.bin"; // 以 "wb" 模式打开文件。'w' 会清空旧文件,'b' 代表二进制模式。 g_calib_imu_file = fopen(file_path, "wb"); if (g_calib_imu_file == NULL) { ESP_LOGE(TAG, "Failed to open file!"); }关键在于使用
"wb"模式。这确保了每一次新的采集任务都会创建一个全新的、干净的文件,避免了旧数据的干扰。 -
处理数据流 (
imu_process_data_stream):// 数据在内存中一经验证,就立刻写入文件 if (fwrite(data, 1, len, g_calib_imu_file) != len) { ESP_LOGE(TAG, "Failed to write complete IMU packet to file!"); }这里是整个方案的核心。我们放弃了所有复杂的内存缓存机制,在数据被验证为正确的下一刻,就调用
fwrite将其直接、原子化地写入 Flash。这彻底杜绝了数据在内存中被其他任务破坏的可能性。 -
停止采集 (
imu_stop_calibration):if (g_calib_imu_file != NULL) { fclose(g_calib_imu_file); g_calib_imu_file = NULL; }调用
fclose至关重要。它不仅会释放文件句柄,更重要的是会将文件系统缓冲区中剩余的、尚未完全写入物理 Flash 的数据,强制刷入(Flush) 到磁盘,确保数据的完整性。
读取数据:为可靠传输而设计
当 PC 端需要下载 imu_data.bin 文件时,我们面临一个新的挑战:如何通过一个主要用于打印日志的文本串口,来传输一个二进制文件?
直接发送二进制数据是不可行的,因为很多字节可能会被串口终端误解为控制字符(如换行、回车等),导致数据截断或乱码。
我们的解决方案是设计一个健壮的、基于文本的传输协议:
-
AT 指令: PC 端发送
AT+READFILE=<offset>,<len>指令,请求从文件的某个偏移量开始,读取特定长度的数据块。 -
分块读取与 Base64 编码: ESP32 的
send_file_chunk函数负责响应。// main.cpp -> send_file_chunk FILE* f = fopen("/storage/imu_data.bin", "rb"); fseek(f, offset, SEEK_SET); // 定位到请求的偏移 // 读取一小块数据(例如256字节)到内存缓冲区 size_t read_len = fread(buf.data(), 1, len, f); fclose(f); // 将二进制数据块编码为 Base64 字符串 size_t enc_len = 0; unsigned char* enc = base64_encode(buf.data(), read_len, &enc_len);Base64 是一种将任意二进制数据,转换为只包含
A-Z,a-z,0-9,+,/,=等安全字符的文本编码。这使得我们的二进制数据块可以安全地在任何文本通道中传输。 -
自定义帧格式: 为了让 PC 端的 Python 脚本能从混杂的日志中准确地“捞出”我们需要的数据,我们将 Base64 字符串用一个自定义的“帧”包裹起来再发送:
printf("###BEGIN###%lu,%u,", (unsigned long)offset, (unsigned)read_len); printf("%.*s", (int)j, enc); // j 是移除换行符后的编码长度 printf("###END###\r\n");这个
###BEGIN###...###END###格式,就像给我们的数据包裹加上了独一无二的“信封”。Python 脚本read_imu_data.py只需在串口数据流中搜索这两个标记,就能精确地提取出有效的数据载荷,而忽略所有的系统日志。
结语
通过 partitions.csv 的精心规划、FATFS 与磨损均衡的强力支持,以及针对性设计的直接写入和 Base64 分块读取策略,我们为 IMU 数据构建了一个既高效又极其可靠的存储和传输方案。这套方案不仅解决了我们项目中遇到的所有数据损坏问题,也为其他类似的嵌入式数据采集项目,提供了一套可供参考的实践范例。
更多推荐



所有评论(0)