第四章 数据存储与文件系统:让黑匣子学会“记笔记”
大家好,欢迎来到第四章。这一章我们聊聊黑匣子的“记忆”问题。
说白了,黑匣子就是个记录仪。它得把车速、刹车、转向、加速度这些数据,老老实实地存下来。万一出了事故,这些数据就是“铁证”。
那怎么存?用什么存?存成什么格式?断电了会不会丢?这些问题,就是本章要解决的。
我个人习惯把这一章分成四块: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操作完成后,需要检查状态寄存器。一般流程是:
- 发送命令(写使能、擦除、写入等)
- 等待芯片忙状态结束(轮询状态寄存器)
- 检查操作结果
下面是一个简化的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 几种常见的保护策略
我总结了几种实用的方法:
- 双备份策略:同时写两份数据,一份主文件,一份备份。掉电后启动时检查,哪个完整用哪个。
- 日志预写(WAL):先写日志,再写数据。如果掉电,恢复时重放日志。
- 电容保持:在电源端加一个大电容,掉电后还能维持几十毫秒,足够完成最后一次写入。
- 文件系统“只读挂载”:正常运行时只读,需要写入时临时挂载为读写,写完立即切回只读。
我个人最推荐的是“双备份 + 电容保持”的组合。成本不高,但效果很好。
实战经验:电容的容量怎么选?我一般按“掉电后需要维持20ms”来算。假设系统功耗是500mA,电压5V,那电容至少需要:C = I * t / V = 0.5 * 0.02 / 5 = 2000μF。实际选2200μF或4700μF比较稳妥。
知识体系总览
下面这张图,把本章的核心逻辑串起来了:
好了,这一章的内容就到这里。数据存储这块,说白了就是“怎么存、存什么、丢了怎么办”。你只要把Flash驱动调稳了,FATFS移植对了,日志格式设计合理了,掉电保护做到位了,黑匣子的“记忆”功能就基本靠谱了。
下一章我们会聊聊数据采集与信号处理,到时候见。