存储需求

PipeMonitor 需要在 SPI NOR Flash 上存储两类数据:

  1. CSV 历史记录/data/measurements.csv
  2. 上行帧缓存/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,用于 LittleFS
  • rawtest: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

特性LittleFSFatFSSPIFFS
断电安全原子 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 分区的寿命远超设备生命周期。

分区大小规划

分区大小用途
filesystem1MBLittleFS:CSV + pending
rawtest128KBFlash 测试(生产验证用)
剩余-未使用,留作扩展

如果将来需要存储固件升级包,可以再划一个 OTA 分区。

小结

FAL + LittleFS 的组合为 PipeMonitor 提供了:

  • 分区管理:一块 Flash 划分成多个逻辑区域
  • 断电安全:原子操作保证文件系统一致性
  • 磨损均衡:延长 Flash 寿命
  • 标准 POSIX 接口open/read/write/close,开发简单

后续阅读