第四章 数据存储与文件系统:让黑匣子学会“记笔记”

大家好,欢迎来到第四章。这一章我们聊聊黑匣子的“记忆”问题。

说白了,黑匣子就是个记录仪。它得把车速、刹车、转向、加速度这些数据,老老实实地存下来。万一出了事故,这些数据就是“铁证”。

那怎么存?用什么存?存成什么格式?断电了会不会丢?这些问题,就是本章要解决的。

我个人习惯把这一章分成四块:Flash驱动、文件系统移植、日志格式设计、掉电保护。咱们一块一块啃。


4.1 Flash存储芯片驱动:SPI Nor Flash

4.1.1 为什么选SPI Nor Flash?

黑匣子对存储的要求其实挺苛刻的:

  • 容量不用太大:几十兆到几百兆就够了
  • 速度要够:写入不能太慢,否则数据来不及存
  • 可靠性要高:车规级,-40℃到85℃得扛得住
  • 掉电不能丢:非易失性存储

SPI Nor Flash正好满足这些要求。它接口简单(就4根线),驱动成熟,而且支持XIP(片上执行),虽然黑匣子用不上这个特性。

核心知识点:SPI Nor Flash的基本操作包括——读、写、擦除。注意,Nor Flash写入前必须先擦除,而且擦除是按“扇区”或“块”来的,不能按字节擦。

4.1.2 驱动实现要点

我在项目中遇到过一个问题:某款Flash芯片的擦除时间特别长,导致写入超时。后来发现是驱动里没做“忙等待”处理。

嗯,这里要注意:Flash操作完成后,需要检查状态寄存器。一般流程是:

  1. 发送命令(写使能、擦除、写入等)
  2. 等待芯片忙状态结束(轮询状态寄存器)
  3. 检查操作结果

下面是一个简化的SPI Nor Flash驱动框架:

// SPI Nor Flash 驱动示例(伪代码)
#define CMD_WRITE_ENABLE   0x06
#define CMD_SECTOR_ERASE   0x20
#define CMD_PAGE_PROGRAM   0x02
#define CMD_READ_DATA      0x03
#define CMD_READ_STATUS    0x05

// 等待芯片不忙
void flash_wait_busy(void) {
    uint8_t status;
    do {
        spi_cs_low();
        spi_transfer_byte(CMD_READ_STATUS);
        status = spi_transfer_byte(0xFF);
        spi_cs_high();
    } while (status & 0x01);  // BUSY位
}

// 扇区擦除(4KB)
void flash_sector_erase(uint32_t addr) {
    spi_cs_low();
    spi_transfer_byte(CMD_WRITE_ENABLE);
    spi_cs_high();

    spi_cs_low();
    spi_transfer_byte(CMD_SECTOR_ERASE);
    spi_transfer_byte((addr >> 16) & 0xFF);
    spi_transfer_byte((addr >> 8) & 0xFF);
    spi_transfer_byte(addr & 0xFF);
    spi_cs_high();

    flash_wait_busy();
}

// 页写入(256字节)
void flash_page_write(uint32_t addr, uint8_t *data, uint16_t len) {
    spi_cs_low();
    spi_transfer_byte(CMD_WRITE_ENABLE);
    spi_cs_high();

    spi_cs_low();
    spi_transfer_byte(CMD_PAGE_PROGRAM);
    spi_transfer_byte((addr >> 16) & 0xFF);
    spi_transfer_byte((addr >> 8) & 0xFF);
    spi_transfer_byte(addr & 0xFF);
    for (uint16_t i = 0; i < len; i++) {
        spi_transfer_byte(data[i]);
    }
    spi_cs_high();

    flash_wait_busy();
}

小技巧:SPI时钟频率别拉太高。我见过有人为了追求速度,把SPI时钟设到80MHz,结果Flash芯片扛不住,数据出错。车规级芯片一般建议20-40MHz,稳定第一。


4.2 FATFS文件系统移植

4.2.1 为什么需要文件系统?

你想想看,如果没有文件系统,数据就是“裸写”在Flash上的。你要读数据,得知道每个字节在哪个地址。这太痛苦了。

有了文件系统,你就可以像操作U盘一样:创建文件、写入数据、读取数据。而且FATFS是开源的,移植起来也不复杂。

4.2.2 移植步骤

FATFS的移植,说白了就是实现几个底层接口。我建议你重点关注这几个函数:

函数 功能 说明
disk_initialize 初始化磁盘 其实就是初始化SPI Flash
disk_read 读取扇区 Flash按扇区读,注意对齐
disk_write 写入扇区 需要先擦除再写
disk_ioctl 控制命令 获取扇区大小、总容量等
get_fattime 获取当前时间 用于文件时间戳

我曾经在移植时踩过一个坑:disk_write的缓冲区必须是4字节对齐的。因为SPI DMA传输要求对齐,否则会触发硬件错误。后来我在驱动里加了个临时缓冲区,专门处理非对齐数据。

警告:FATFS默认的扇区大小是512字节。如果你的Flash物理扇区是4KB,建议在ffconf.h中配置为4096,否则性能会受影响。


4.3 日志文件格式设计

4.3.1 二进制 vs CSV

这是个老生常谈的问题。我直接说结论:

  • 二进制格式:体积小、解析快、适合嵌入式。但可读性差,需要配套解析工具。
  • CSV格式:可读性好,直接用Excel打开。但体积大,解析慢。

我个人建议:黑匣子内部用二进制,导出时再转CSV。这样既节省存储空间,又方便事后分析。

4.3.2 二进制日志格式设计

下面是我常用的一个日志结构:

// 日志文件头
typedef struct {
    uint32_t magic;          // 魔数,用于校验
    uint32_t version;        // 格式版本
    uint32_t record_count;   // 记录总数
    uint32_t record_size;    // 单条记录大小
    uint32_t start_time;     // 开始时间戳
    uint32_t end_time;       // 结束时间戳
    uint32_t checksum;       // 文件头校验和
} log_file_header_t;

// 单条数据记录
typedef struct {
    uint32_t timestamp;      // 时间戳(ms)
    float    speed;          // 车速(km/h)
    float    acceleration;   // 加速度(m/s²)
    uint8_t  brake_status;   // 刹车状态
    uint8_t  steering_angle; // 转向角度
    uint16_t engine_rpm;     // 发动机转速
    uint32_t reserved;       // 保留字段
} log_record_t;

每条记录固定32字节。你想想看,如果每秒记录100条,一小时也就11MB左右。一个128MB的Flash,能存十几个小时的数据。

关键点:每条记录都带时间戳。这样即使掉电导致最后几条数据没写完,也能通过时间戳判断哪些数据是完整的。


4.4 掉电保护机制

4.4.1 掉电是黑匣子的“天敌”

汽车在行驶中,随时可能断电。如果正在写Flash时突然掉电,轻则丢数据,重则文件系统损坏。

我曾经遇到过一台测试车,连续掉电三次后,FATFS的文件分配表彻底乱了,整个盘都读不出来。从那以后,我对掉电保护格外重视。

4.4.2 几种常见的保护策略

我总结了几种实用的方法:

  1. 双备份策略:同时写两份数据,一份主文件,一份备份。掉电后启动时检查,哪个完整用哪个。
  2. 日志预写(WAL):先写日志,再写数据。如果掉电,恢复时重放日志。
  3. 电容保持:在电源端加一个大电容,掉电后还能维持几十毫秒,足够完成最后一次写入。
  4. 文件系统“只读挂载”:正常运行时只读,需要写入时临时挂载为读写,写完立即切回只读。

我个人最推荐的是“双备份 + 电容保持”的组合。成本不高,但效果很好。

实战经验:电容的容量怎么选?我一般按“掉电后需要维持20ms”来算。假设系统功耗是500mA,电压5V,那电容至少需要:C = I * t / V = 0.5 * 0.02 / 5 = 2000μF。实际选2200μF或4700μF比较稳妥。


知识体系总览

下面这张图,把本章的核心逻辑串起来了:

数据存储与文件系统知识体系 SPI Nor Flash驱动 FATFS文件系统移植 日志文件格式设计 掉电保护机制 读/写/擦除操作 状态寄存器轮询 SPI时序配置 disk_initialize/read/write 扇区大小配置 长文件名支持 二进制 vs CSV 固定长度记录结构 时间戳 + 校验 双备份策略 电容保持 日志预写(WAL) 核心目标:可靠、高效、可恢复 驱动是基础 → 文件系统是骨架 → 日志格式是血肉 → 掉电保护是灵魂

好了,这一章的内容就到这里。数据存储这块,说白了就是“怎么存、存什么、丢了怎么办”。你只要把Flash驱动调稳了,FATFS移植对了,日志格式设计合理了,掉电保护做到位了,黑匣子的“记忆”功能就基本靠谱了。

下一章我们会聊聊数据采集与信号处理,到时候见。

专注资料整理