第四章:数据存储与管理

好,咱们进入第四章。这一章讲的是数据怎么存、怎么管。

你想想看,一个气象站放在野外,可能几个月都没人管。数据要是丢了,那前面所有工作都白干了。我见过不少项目,硬件做得挺扎实,结果存储方案没想好,最后数据全乱套了。

4.1 SPI Flash驱动:让芯片开口说话

先说说底层驱动。SPI Flash,说白了就是个能存东西的芯片。咱们通过SPI总线跟它通信。

我个人习惯,写驱动前先看数据手册。别嫌烦,这一步省不了。我踩过最大的坑,就是没仔细看时序图,结果读写总是不对。

核心操作就四个:

  • 读ID:确认芯片活着
  • 擦除:写之前必须擦,这是Flash的脾气
  • 写入:按页写,一页256字节
  • 读取:按地址读,想读多少读多少

嗯,这里要注意。擦除操作很慢,擦一个扇区可能要几百毫秒。你在写代码时,千万别在主循环里干等。我建议用状态机,或者干脆把擦除放到后台任务里。

// 一个简单的SPI Flash写页函数
// 我习惯把页写和页读封装好,上层不用管底层细节
uint8_t spi_flash_write_page(uint32_t addr, uint8_t *data, uint16_t len) {
    // 1. 写使能
    spi_flash_write_enable();
    // 2. 发送页写指令
    spi_flash_send_cmd(CMD_PAGE_PROGRAM);
    // 3. 发送地址(3字节)
    spi_flash_send_addr(addr);
    // 4. 发送数据
    spi_flash_send_data(data, len);
    // 5. 等待完成
    while(spi_flash_read_busy());
    return SUCCESS;
}

避坑指南:我曾经在批量写数据时,忘了检查写使能状态。结果数据写进去一半,芯片自己锁住了。后来我加了个超时重试机制,问题才解决。

4.2 FATFS文件系统移植:让数据像U盘一样管理

裸的Flash用起来太原始了。你想想,要记录温度、湿度、风速,还得按时间查。没有文件系统,光地址管理就能把你搞疯。

FATFS是个轻量级文件系统,非常适合嵌入式。我移植过很多次,说几个关键点:

  1. 底层接口要写对:disk_read、disk_write、disk_status,这三个必须实现
  2. 时间戳回调:get_fattime,这个不实现,文件时间全是0
  3. 缓冲区大小:_MAX_SS 设成4096,跟Flash扇区对齐,性能会好很多

我记得第一次移植FATFS时,死活挂载不上。查了半天,原来是disk_read返回了错误码。后来我加了个调试打印,才发现是SPI时钟极性配反了。

// FATFS底层接口示例
// 我习惯把Flash操作封装成块设备
DSTATUS disk_initialize(BYTE pdrv) {
    // 初始化SPI Flash
    spi_flash_init();
    return RES_OK;
}

DRESULT disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count) {
    // 注意:sector是512字节块,要转成Flash地址
    uint32_t addr = sector * 512;
    spi_flash_read(addr, buff, count * 512);
    return RES_OK;
}

注意:FATFS默认扇区大小是512字节。如果你的Flash擦除块是4K,建议把扇区大小也改成4096。否则每次写小文件,都要先读-改-写,效率极低。

4.3 数据日志格式设计:存得进去,读得出来

文件系统有了,接下来就是数据怎么组织。我见过最糟糕的设计,是把所有数据塞进一个TXT文件,一行接一行。结果文件越来越大,最后打开都卡死。

我个人推荐用二进制格式。为什么?

  • 体积小:一个浮点数用4字节,转成字符串要十几字节
  • 解析快:直接memcpy,不用字符串解析
  • 对齐好:结构体定义清楚,按字节对齐就行
// 我常用的日志结构体
// 每个记录固定长度,方便随机访问
typedef struct {
    uint32_t timestamp;   // Unix时间戳,4字节
    float    temperature; // 温度,4字节
    float    humidity;    // 湿度,4字节
    float    pressure;    // 气压,4字节
    uint16_t wind_speed;  // 风速,2字节
    uint16_t wind_dir;    // 风向,2字节
    uint8_t  crc8;        // 校验,1字节
} __attribute__((packed)) weather_record_t;
// 总大小:21字节

嗯,这里有个小技巧。我习惯在每条记录末尾加CRC校验。虽然Flash本身有ECC,但万一数据写一半掉电了呢?加个校验,读取时能发现坏数据。

文件命名规则:

我建议按日期分文件。比如:2024_01_15.log。这样查找某一天的数据,直接打开对应文件就行。别把所有数据塞一个文件里,那是给自己找麻烦。

4.4 循环覆盖存储策略:有限空间,无限记录

气象站要连续运行,但Flash空间有限。怎么办?循环覆盖。

说白了,就是存满之后,把最旧的数据删掉,腾出空间写新的。有点像环形缓冲区,只不过是在文件系统上实现的。

我常用的策略有两种:

策略 原理 适用场景
按文件覆盖 固定文件数量,写满后删除最旧文件 数据量不大,按天分文件
按扇区覆盖 在Flash上直接管理,像环形缓冲区 数据量大,需要高可靠性

我个人更推荐按文件覆盖。原因很简单:文件系统帮你管理了空间,你只需要维护一个文件列表就行。

// 循环覆盖策略伪代码
// 我习惯在系统启动时检查磁盘空间
void check_storage_space() {
    // 获取剩余空间
    uint32_t free_space = get_free_space();
    
    // 如果剩余空间小于阈值(比如10%)
    if (free_space < THRESHOLD) {
        // 找到最旧的文件
        char oldest_file[32];
        find_oldest_file(oldest_file);
        // 删除它
        f_unlink(oldest_file);
    }
}

避坑指南:我曾经在删除文件时,没考虑文件系统碎片。结果删了文件,但空间没完全释放。后来我改用f_mkfs定期格式化,虽然粗暴,但有效。

还有一个细节:写数据时,别一条一条写。攒够一批,比如10条记录,一次性写入。这样能减少文件系统操作次数,延长Flash寿命。

嗯,说到寿命。Flash有擦写次数限制,一般是10万次。你算算,如果每分钟写一次,能用多久?大概69天。所以循环覆盖不是无限循环,是有寿命的。我建议在系统里加个寿命统计,快到期时提醒更换。

最后提醒:掉电保护一定要做。写数据时突然断电,文件系统可能损坏。我常用的办法是:先写临时文件,写完后重命名。这样即使掉电,最多丢一条记录,不会损坏整个文件系统。

好了,这一章就到这里。数据存储这块,说白了就是「存得进去、读得出来、管得明白」。下一章咱们聊聊通信协议,怎么把数据传出去。