Flash写入前必须先擦除,最小擦除单位为扇区(1KB~4KB),远大于日志条目;正确做法是固定使用1~2个扇区作为循环缓冲区,仅在空间不足或跨边界时触发擦除,并避开死机瞬间,在重启初始化阶段完成擦除与写入。

Flash写入前必须先擦除,且最小擦除单位远大于日志条目
单片机Flash不能像RAM一样按字节写入,每次写入前必须擦除对应扇区——而常见MCU(如STM32F1/F4、GD32、NXP Kinetis)的扇区大小是1KB~4KB。如果你为10条死机日志单独分配10个地址,每次只写32字节,却要为每条日志擦除整个扇区,不仅速度慢,还会在几次死机后就耗尽Flash寿命(典型耐久度仅1万~10万次擦除)。
正确做法是:固定使用1~2个扇区(例如 2KB),把日志当作循环缓冲区管理,所有写入都集中在这个区域里,只在空间不足或跨扇区边界时才触发擦除。擦除动作本身应尽量避开死机发生瞬间(比如放在重启后的初始化阶段,而非死机中断中)。
- 不要在
HardFault_Handler或NMI_Handler中调用Flash擦除函数(如HAL_FLASHEx_Erase),它可能因总线锁死或电压不稳失败 - 用一个标志位(如备份寄存器
BKP_DR1或独立Flash页)标记“有未保存日志”,重启后由main()开头检查并完成擦除+写入 - 每条日志结构建议包含:
uint32_t magic(校验)、uint32_t tick(SysTick计数)、uint32_t sp、uint32_t pc、uint32_t lr、uint32_t cfsr等关键寄存器快照
用“头尾指针+魔数校验”实现无磨损循环覆盖
不依赖Flash文件系统(如FatFS),也不用维护复杂索引表。直接在固定扇区内用两个偏移量模拟队列:一个指向最新日志起始地址(log_head),一个指向下一个空闲位置(log_tail)。每次写入时,先跳过已存在的有效日志(通过检查每条开头的 magic 值),找到第一个无效位置,写入新日志;写满后回到起始位置继续覆盖——前提是该位置已被擦除。
关键点在于:擦除只在检测到“即将写入位置未擦除”时触发,且只擦除当前扇区(不提前擦整个区)。这样即使设备频繁死机,只要不是连续高频死机,擦除次数可控制在极低水平。
立即学习“C++免费学习笔记(深入)”;
- 日志结构体末尾加1字节
0xFF作为填充,确保每条日志长度对齐(如64字节),方便地址计算 -
log_head不是实时更新的指针,而是扫描Flash后动态确定的——每次启动时从扇区起始逐条读取,直到遇到第一个magic != 0xCAFEBABE的位置,其前一条即为最新日志 - 避免用
sizeof(log_entry_t)直接做指针偏移,Flash地址需按实际存储布局对齐(例如某些MCU要求半字/字对齐写入)
HardFault中安全捕获寄存器快照的三个硬约束
在 HardFault_Handler 里能可靠读取的寄存器非常有限:SP(此时是MSP或PSP,需先判断)、PC、LR、CFSR 是核心;但不能调用任何C库函数(如 printf、memcpy)、不能访问未初始化的全局变量、不能触发任何额外异常(包括未对齐访问)。
- 用内联汇编读取
SP和SCB->CFSR,避免编译器优化干扰:__asm volatile ("MRS %0, psp" : "=r"(psp) :: "r0"); - 区分MSP/PSP:检查
CONTROL寄存器第0位,若为0则当前用MSP,否则用PSP(FreeRTOS等RTOS常切换) - 不要尝试读取R0–R3等通用寄存器——它们在进入Handler时已被压栈,但栈帧结构依赖于编译器和调用约定,不可靠;专注保存
SP、PC、LR、CFSR、HFSR、DFSR这6个最能定位问题的寄存器
重启后保存日志时必须关闭全局中断并检查Flash状态
很多开发者把日志保存逻辑放在 main() 开头,却忘了此时系统时钟、外设可能尚未初始化完毕,或者Flash控制器时钟没使能。更危险的是:如果上次死机发生在Flash操作中途(如擦除一半断电),再次上电后直接写入会失败甚至锁死Flash。
- 务必在调用
HAL_FLASH_Unlock()前确认RCC已开启Flash接口时钟(如__HAL_RCC_FLASH_CLK_ENABLE()) - 写入前用
HAL_FLASHEx_Erase(&eraseInitStruct, &error)擦除,并检查返回值;若失败(如error == HAL_FLASH_ERROR_PROG),说明该扇区已损坏,应跳转到备用扇区(如有)或停止写入 - 每次写入单条日志后,调用
FLASH_WaitForLastOperation(FLASH_TIMEOUT_VALUE)并检查状态,避免后续写入覆盖未完成的前一次操作 - 最后记得
HAL_FLASH_Lock()—— 忘记它会导致后续任何Flash操作(包括IAP升级)失败,且错误难以排查
真正难的不是写代码,是让这套机制在电压跌落、时钟抖动、中断嵌套、RTOS任务切换等各种边缘场景下仍保持日志不丢、不乱、不毁。尤其是擦除时机和寄存器快照完整性之间存在根本矛盾:早擦,可能覆盖未提交的日志;晚擦,可能没机会擦。最终方案往往要妥协——接受某次死机日志丢失,但确保其余9次绝对可靠。


















