存储需求
PipeMonitor 需要在 SPI NOR Flash 上存储两类数据:
- CSV 历史记录:
/data/measurements.csv - 上行帧缓存:
/data/pending.jsonl
这些数据需要在断电后保留,且不能因为写入过程中断电而损坏文件系统。
FAL:Flash Abstraction Layer
RT-Thread 的 FAL 组件提供了 Flash 设备的统一抽象:
/* 分区表定义(通常在 fal_cfg.h) */
const fal_partition_t fal_partition_table[] =
{
{ "filesystem", "norflash", 0, 1024*1024, 0 },
{ "rawtest", "norflash", 1024*1024, 128*1024, 0 },
};
分区表把一整块 SPI NOR Flash 划分成多个逻辑分区:
filesystem:1MB,用于 LittleFSrawtest:128KB,用于 Flash 原始读写测试
初始化流程
static int storage_service_init(void)
{
/* 1. 初始化 FAL */
int result = fal_init();
if (result <= 0)
{
LOG_E("fal_init failed: %d", result);
return -RT_ERROR;
}
/* 2. 创建 MTD NOR 设备 */
if (rt_device_find("filesystem") == RT_NULL)
{
if (fal_mtd_nor_device_create("filesystem") == RT_NULL)
{
LOG_E("failed to create MTD NOR device");
return -RT_ERROR;
}
}
/* 3. 挂载 LittleFS */
return storage_service_mount_filesystem();
}
INIT_ENV_EXPORT(storage_service_init);
INIT_ENV_EXPORT 让这个函数在系统启动时自动执行,早于所有业务线程。
LittleFS 挂载
static int storage_service_mount_filesystem(void)
{
int result;
/* 尝试挂载 */
result = dfs_mount("filesystem", "/", "lfs", 0, RT_NULL);
if (result == 0)
{
LOG_I("littlefs mounted");
return storage_service_prepare_data_directory();
}
/* 挂载失败,尝试格式化 */
LOG_W("mount failed, try formatting");
result = dfs_mkfs("lfs", "filesystem");
if (result != 0)
{
LOG_E("mkfs failed");
return -RT_ERROR;
}
/* 格式化后重新挂载 */
result = dfs_mount("filesystem", "/", "lfs", 0, RT_NULL);
if (result != 0)
{
LOG_E("mount failed after mkfs");
return -RT_ERROR;
}
LOG_I("littlefs formatted and mounted");
return storage_service_prepare_data_directory();
}
首次使用时 Flash 是空的,挂载会失败。此时自动格式化再挂载。
为什么选 LittleFS
| 特性 | LittleFS | FatFS | SPIFFS |
|---|---|---|---|
| 断电安全 | 原子 rename | 日志式 | 不保证 |
| 掉电恢复 | 自动 | 需要 fsck | 可能损坏 |
| RAM 占用 | ~1KB | ~4KB | ~2KB |
| 代码体积 | ~10KB | ~30KB | ~15KB |
LittleFS 的核心优势是 原子 rename 操作:rename("old", "new") 要么完全成功,要么完全回滚,不会出现”文件写了一半”的状态。
这对 pending 缓存很重要——我们用 rename 来删除已补传的帧:
rename("/data/pending.tmp", "/data/pending.jsonl");
即使 rename 过程中断电,下次启动时文件系统仍然一致。
数据目录准备
挂载完成后创建 /data 目录:
static int storage_service_prepare_data_directory(void)
{
struct stat st;
if (stat("/data", &st) == 0)
return RT_EOK; /* 已存在 */
if (mkdir("/data", 0) == 0)
{
LOG_I("created data directory");
return RT_EOK;
}
/* 并发创建时可能已存在 */
if (stat("/data", &st) == 0)
return RT_EOK;
LOG_E("create data directory failed");
return -RT_ERROR;
}
Flash 原始测试
storage_rawtest 命令直接操作 Flash 分区,验证底层读写:
static int storage_service_rawtest(void)
{
const struct fal_partition *part;
part = fal_partition_find("rawtest");
/* 擦除一个扇区 */
fal_partition_erase(part, 0, 4096);
/* 写入测试数据 */
fal_partition_write(part, 0, write_buf, 256);
/* 读回验证 */
fal_partition_read(part, 0, read_buf, 256);
if (memcmp(write_buf, read_buf, 256) != 0)
{
rt_kprintf("rawtest: verify mismatch\n");
return -RT_ERROR;
}
rt_kprintf("rawtest: PASS\n");
return RT_EOK;
}
部署时先跑这个测试,确认 Flash 硬件正常。
文件系统测试
storage_fstest 命令验证 LittleFS 的文件操作:
static int storage_service_fstest(void)
{
/* 创建文件、写入、读回、删除 */
fd = open("/data/storage_test.bin", O_WRONLY | O_CREAT | O_TRUNC, 0);
write(fd, write_buf, 256);
close(fd);
fd = open("/data/storage_test.bin", O_RDONLY, 0);
read(fd, read_buf, 256);
close(fd);
/* 验证数据一致性 */
if (memcmp(write_buf, read_buf, 256) != 0)
return -RT_ERROR;
/* 清理 */
unlink("/data/storage_test.bin");
rt_kprintf("fstest: PASS\n");
return RT_EOK;
}
Flash 磨损均衡
SPI NOR Flash 的擦写寿命通常在 10 万到 100 万次。LittleFS 内部实现了磨损均衡——每次写入时选择擦写次数最少的块,避免某个块被过度使用。
对于 PipeMonitor:
- CSV 追加:每 8-10 秒写一次,每次 ~500 字节
- pending 追加:只在网络中断时写入
按最坏情况估算,1MB 分区的寿命远超设备生命周期。
分区大小规划
| 分区 | 大小 | 用途 |
|---|---|---|
filesystem | 1MB | LittleFS:CSV + pending |
rawtest | 128KB | Flash 测试(生产验证用) |
| 剩余 | - | 未使用,留作扩展 |
如果将来需要存储固件升级包,可以再划一个 OTA 分区。
小结
FAL + LittleFS 的组合为 PipeMonitor 提供了:
- 分区管理:一块 Flash 划分成多个逻辑区域
- 断电安全:原子操作保证文件系统一致性
- 磨损均衡:延长 Flash 寿命
- 标准 POSIX 接口:
open/read/write/close,开发简单