第四章:数据存储与管理
好,咱们进入第四章。这一章讲的是数据怎么存、怎么管。
你想想看,一个气象站放在野外,可能几个月都没人管。数据要是丢了,那前面所有工作都白干了。我见过不少项目,硬件做得挺扎实,结果存储方案没想好,最后数据全乱套了。
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是个轻量级文件系统,非常适合嵌入式。我移植过很多次,说几个关键点:
- 底层接口要写对:disk_read、disk_write、disk_status,这三个必须实现
- 时间戳回调:get_fattime,这个不实现,文件时间全是0
- 缓冲区大小:_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天。所以循环覆盖不是无限循环,是有寿命的。我建议在系统里加个寿命统计,快到期时提醒更换。
最后提醒:掉电保护一定要做。写数据时突然断电,文件系统可能损坏。我常用的办法是:先写临时文件,写完后重命名。这样即使掉电,最多丢一条记录,不会损坏整个文件系统。
好了,这一章就到这里。数据存储这块,说白了就是「存得进去、读得出来、管得明白」。下一章咱们聊聊通信协议,怎么把数据传出去。