不能直接用 fopen + fseek 循环写 Flash,因为 SPI NOR 必须先擦除整个扇区(如 4KB)才能写入,而 C 库不感知擦除逻辑,会导致写失败、位翻转或提前损坏;需手写日志系统,严格对齐扇区、手动管理擦除与写位置、带 CRC 校验和序列号,并全盘扫描恢复。

为什么不能直接用 fopen + fseek 循环写 Flash?
Flash 存储(尤其是 SPI NOR)有物理擦除粒度限制:必须先擦除整个扇区(常见 4KB),才能写入新数据。而 fopen("log.bin", "r+b") 后反复 fseek 写入,会隐式覆盖旧字节——这在 NAND/NOR 上实际触发的是“读-改-擦-写”流程,但标准 C 库完全不感知擦除逻辑,最终导致写失败、位翻转或提前损坏 Flash。
手写轻量级日志系统的核心约束是:所有写操作必须对齐扇区边界,且每次写前确保目标扇区已擦除。
- 必须自己管理“当前写位置”和“有效日志头尾”,不能依赖文件系统元数据
- 日志条目需带校验(如 CRC16)和时间戳/序列号,用于崩溃后扫描恢复
- 避免在中断上下文调用擦除函数(SPI 操作通常阻塞且耗时)
如何设计日志块头与循环写结构?
每个扇区开头放一个固定格式的 log_block_header_t,标识该扇区是否有效、写入偏移、总长度、CRC:
struct log_block_header_t {
uint32_t magic; // 固定值 0x4C4F4742 ('LOGB')
uint32_t seq; // 递增序列号,用于判断新旧扇区
uint32_t write_off; // 当前扇区内已写入的字节数(从 header 后开始计)
uint16_t crc16; // header 自身 CRC(不含 magic)
};
日志内容紧随 header 之后,按条目追加;到达扇区末尾时,跳到下一个扇区并擦除它,重置 write_off = sizeof(header)。
立即学习“C++免费学习笔记(深入)”;
- magic 和 seq 共同判定扇区有效性:magic 错则跳过;seq 更大的扇区为最新
-
write_off必须是 2/4 字节对齐(取决于 Flash 编程粒度),否则写入可能失败 - 扇区大小建议选 4KB 或 64KB,太小导致擦除频繁,太大则重启后扫描慢
崩溃后如何安全定位最新日志起始位置?
上电后不能假设任何扇区干净,必须全盘扫描所有扇区,找出最后一个有效的 log_block_header_t,再从其 write_off 处向后解析日志条目(每条含长度字段+CRC)。
- 扫描顺序应从高地址扇区往低地址扫,第一个 magic+seq 验证通过的即为最新块
- 若两个扇区 seq 相同(比如写 seq 时掉电),以
write_off更大的为准 - 每条日志建议以
uint16_t len开头,后跟 payload 和uint16_t crc,便于跳过损坏条目 - 扫描过程要避开正在擦除/编程中的扇区(可通过硬件 BUSY 引脚或状态寄存器判断)
关键操作必须封装为原子函数
所有涉及 Flash 的操作都不可拆分:erase_sector()、program_page()、read_page() 必须对应硬件手册的最小操作单元(如 STM32 HAL 中的 HAL_FLASHEx_Erase() 或裸寄存器操作)。
-
log_write(const void* data, size_t len)内部要检查剩余空间:不足时先擦除下一扇区,再写 header,最后写 data - 写 header 前必须确认目标地址所在扇区已擦除,否则
program_page()会静默失败 - 强烈建议在
log_write()开头加static_assert(sizeof(log_block_header_t) ,防止 header 跨页 - 不要在
log_write()中做浮点运算或动态内存分配——嵌入式 Flash 日志必须确定性执行
最易被忽略的是擦除后的验证读:某些 Flash 在擦除后需主动读一次确认全 0xFF,否则后续编程可能出错。这个步骤不能省。


















