SPI NOR Flash掉电保存方案:时间戳恢复与日志记录
2026/9/9 2:12:50 网站建设 项目流程

简介:Flash时钟是一份面向Flash动画初学者与ActionScript编程入门者的实践型资源,展示了如何用AS脚本编写一个可实时走时的桌面时钟。压缩包共3个文件、仅562KB,包括FLA源文件、SWF成品和HTML预览页,从源码到运行效果一应俱全,很适合边学边改。作者caoping8708实现了完整的时钟逻辑:用Date类读取系统时间,通过getHours、getMinutes、getSeconds取得时分秒,再将其换算为指针旋转角度,如时针每小时30度、分针秒针每单位6度,利用ENTER_FRAME事件逐帧刷新时针、分针和秒针角度,从而形成连续走针动画。整个过程覆盖时间获取、角度换算、事件驱动三大环节,能够帮助读者快速理解Flash动画中对象旋转与帧更新的配合方式。通过这份资源,读者既能获得可直接运行的SWF动画,也能从FLA源文件中拆解出时针、分针、秒针的实例命名与代码写法,练习难度适中。目前已有176人浏览学习,适合作为课堂案例、课后练手或经典Flash技术复习的参考。 去年做户外数据采集器那阵子,我接了个不大不小的改造需求:设备放在野外的机箱里,主控是STM32F407,偏偏板子上没打算装RTC后备电池。一旦断电再上电,系统时间直接退回1970年,客户的售后电话能被打爆。需求本身很朴素——每次掉电的准确时刻必须能恢复,还要能查最近一个月的运行记录。我翻了一圈板卡资源,发现预留了一颗SPI NOR Flash的位置,干脆把时间戳、运行日志、传感器数据全塞进这颗芯片。项目内部代号就叫"Flash时钟"。

名字有点双关:硬件上靠Flash解决时间数据的掉电保存,调试过程里又反复跟Flash的时钟信号、时序命令较劲。这篇文章就是把这套方案从选型思路、读写驱动、坏块处理到烧录报错的完整排查链路整理一遍。适合手里正拿着SPI Flash数据手册发愁的人,也适合被"Flash Download failed"这类报错卡住的朋友,看完你至少能少走我一半弯路。

1. 为什么不用RTC电池,偏要用Flash存时间

1.1 需求拆解:这个项目真正要解决的事

很多人一看到"保存时间"就条件反射想到RTC加电池,但这个方案在户外机箱场景下有个现实问题:锂电池在高温、低温环境里衰减很快,野外换电池的人工成本比电池本身贵得多,客户根本不想维护。所以核心需求其实是"断电后非易失地记录时间点",而不是"保持时钟一直走"。

想通这一点,方向就清晰了:掉电前把当前时间戳写入非易失存储,上电后读出来作为基准继续跑。记录一条掉电事件只需要几字节,日志按天轮转,一个月大概也就几十KB,正好在SPI NOR Flash的舒适区。数据手册里那些容量、擦写次数、擦除时间参数,这下全有了实际用武之地。

1.2 内部Flash和外部SPI Flash怎么选

很多初学者第一反应是直接写STM32内部Flash,我一开始也这么想。但细算之后就放弃了,给你看个对比:

对比项STM32内部Flash外部SPI Flash(以W25Q128JV为例)
容量F407是1MB,代码区占了大部分128Mbit即16MB,独立空间
擦写寿命典型1万次10万次,数据区用更放心
擦除单元扇区最小4KB,操作有风险区4KB扇区/64KB块,管理灵活
掉电影响擦写中掉电可能伤及程序区只伤数据,不影响代码
代码复杂度涉及内部Flash解锁、擦写保护标准SPI命令,简单

最终选外部Flash,一个是容量弹性大,另一个是把风险区隔离开——程序写挂在Flash数据区,顶多丢日志,不会让整台设备变砖。当然还有一层私心:W25Q128这颗料在市场上量大、便宜、处处买得到,兼容替换型号极多,哪怕原厂缺货也不会被卡脖子。

1.3 "时钟"在这个项目里到底是什么

这里的"Clock"有三层含义。最表面的是系统时间的读写功能,其次是指Flash芯片工作时的SCK时钟信号,深层则是指擦写时序里的各种时间参数。这个项目做完,我对"时钟"的理解从应用层深入到了波形层,收获比预期大得多。

2. 选型先行的道理:NOR与NAND之争,以及SPI时钟上限

2.1 为什么数据记录场景选NOR而不是NAND

搜索"Flash时钟"相关的技术词时,很大一部分人是在纠结NAND和NOR怎么选。这个问题的标准答案我背得很熟,但做项目之后体会更深:NOR Flash随机读快、擦写单元小、没有出厂坏块,SOP8封装两毛钱的PCB面积就能放下,上电就能读代码执行;NAND Flash容量大、价格低,但需要坏块管理、纠错算法,控制器复杂度上一个台阶。

我这台设备单日日志量撑死几百字节,一个月不到100KB,NOR的容量完全够。要是换成NAND,光坏块管理代码的维护成本就够喝一壶。所以选型逻辑不是"谁先进选谁",而是让容量需求、可靠性需求、开发成本三条曲线取交集。

2.2 SCK时钟频率不能拍脑袋拉满

SPI Flash的时钟频率是第一个容易踩的坑。W25Q128JV的数据手册里,快速读(0x0B)最高能到133MHz,普通读数据(0x03)一般限制在50MHz左右。很多朋友看了最高频率就兴奋,直接把SPI预分频设为最高,结果高速下发命令时好时坏。

我这个项目里SPI时钟跑的是50MHz,不是不能更高,是没必要。原因有三条:第一,机箱内走线长且没有做阻抗控制,频率上去之后反射、串扰会一起冒出来;第二,主控和Flash之间还隔着排针和杜邦线,这种链路跑高频就是自找麻烦;第三,日志写入量很小,时钟快一点慢一点对整个系统性能根本没影响。实测下来50MHz非常稳,连续读写一晚上没有出现一比特错误。

2.3 CS和CLK引脚的电路处理

原理图上两个细节值得强调。CS引脚必须接上拉电阻到VCC,上电瞬间主控引脚还没初始化,如果不做上拉,Flash可能被随机电平误选中,在启动阶段就收到一条残缺命令。CLK时钟线尽量靠近Flash摆放,远离电源和继电器这类干扰源,我板子上还加了一颗22Ω的串阻,用于抑制振铃信号。

供电去耦也别忘了,VCC引脚旁边放一颗0.1uF陶瓷电容,靠近引脚放置。这些看起来都是基本功,但我在调试阶段确实遇到过CS悬空导致Flash偶发进入怪状态的问题,补上拉之后故障就消失了。

3. 读Flash ID不能省:型号识别里的门道

3.1 上电第一件事为什么是读JEDEC ID

SPI NOR Flash的厂商和容量识别有一套标准机制:发送0x9F命令,芯片会回传3字节JEDEC ID,分别是Manufacturer ID、Memory Type、Capacity Code。上电后第一件事读ID,不是走形式,而是为了确认软件配置跟实际芯片一致。

我习惯在系统初始化里专门写一个Flash自检函数:读ID、解析厂商、比对容量,如果发现和编译时期望值不一致,直接打错误日志并禁止写入操作。这个习惯救过我一次——采购换了一批物料,板子上标的型号是W25Q64,实际焊上去的是GD25Q64,主控读ID才发现厂商码不一样。有了这层防护,软件用的容量、擦除算法都按新颗粒重新适配,避免了后续大量数据写飞。

3.2 读取JEDEC ID的参考代码

用STM32 HAL库实现很简单:

uint32_t spi_flash_read_jedec_id(void) { uint8_t tx_data[4] = {0x9F, 0x00, 0x00, 0x00}; uint8_t rx_data[4] = {0x00}; FLASH_CS_LOW(); HAL_SPI_TransmitReceive(&hspi1, tx_data, rx_data, 4, 100); FLASH_CS_HIGH(); return ((uint32_t)rx_data[1] << 16) | ((uint32_t)rx_data[2] << 8) | ((uint32_t)rx_data[3]); }

常见颗粒的ID码对照表,建议收藏:

厂商首字节常见型号示例
Winbond0xEFW25Q16/32/64/128,容量码常见0x15/16/17/18
Macronix0xC2MX25L系列
GD0xC8GD25Q系列,和Winbond引脚兼容
ISSI0x9DIS25系列
EON0x1CEN25系列

3.3 读ID异常时的排查方向

如果读回来的ID是全0xFF或者全0x00,先别急着怀疑芯片坏了,按这个顺序排查:供电电压是否在2.7V到3.6V之间,CS引脚初始化状态有没有拉高,SPI时钟极性和相位配置是否和芯片匹配,接线有没有断路。我遇到过一次奇怪问题,读ID时好时坏,最后发现是Flash的WP引脚悬空导致芯片状态不受控,补上拉以后恢复稳定。读ID是所有后续操作的前提,这个环节稳了,后面读写才有意义。

4. 页编程、扇区擦除与状态轮询:时序和时钟模式里的坑

4.1 基本命令集家的理解

SPI NOR Flash的读写命令不复杂,绕不开五条:0x06写使能、0x05读状态寄存器1、0x03读数据、0x02页编程、0x20扇区擦除。页编程一次最多写256字节,扇区擦除一次清4KB,这是硬件物理结构决定的——NOR Flash的存储阵列按页组织,擦除按扇区块组织。写之前必须先擦,擦完读出来是全0xFF,这是无数新手写飞数据的根源。

4.2 为什么必须轮询BUSY位

Flash执行擦写是自定时的,时间还很长,扇区擦除典型值在45ms到400ms之间,页编程也有1到3毫秒。代码里不能发完命令就以为完成了,必须通过0x05命令读状态寄存器1,检查bit0的BUSY标志,为0才表示操作结束。

我最初图省事,擦除后直接Delay了100ms,结果批量生产时发现个别芯片擦除时间超过标称值,日志写入就错乱了。后来老老实实改成轮询BUSY,兼容性一下子提升了,不同批次、不同厂商的芯片都能稳定跑。这里给个参考实现:

uint8_t spi_flash_read_status(void) { uint8_t tx_buf[2] = {0x05, 0xFF}; uint8_t rx_buf[2] = {0x00}; FLASH_CS_LOW(); HAL_SPI_TransmitReceive(&hspi1, tx_buf, rx_buf, 2, 100); FLASH_CS_HIGH(); return rx_buf[1]; } void spi_flash_wait_busy(void) { while (spi_flash_read_status() & 0x01); }

4.3 跨页写和缓存对齐

页编程最大长度是256字节,而且绝对不能跨页。意思是如果一页只剩50字节空间,你要写100字节,就必须拆成两条命令,第一条写50字节,第二条从下一页开头继续写。如果无视边界硬写,有些芯片会把命令拒掉,有些会滚回到当前页开头覆盖数据,两种结果都很恶心。

我做日志记录时撞上过一次"数据写进去读出来不完整"的问题,排查半天才意识到是日志结构跨页导致的。解决思路是:写前先计算当前页剩余空间,做拆分写入,所有日志结构体都按256字节对齐设计,让单条记录尽量不要横跨页边界。

4.4 时钟模式(CPOL/CPHA)的波形陷阱

SPI有四个模式,NOR Flash通常支持Mode 0(CPOL=0, CPHA=0)和Mode 3(CPOL=1, CPHA=1)。很多人觉得不都是标准SPI吗,配上哪组都一样。实际调试中,读ID正常但读写数据全乱的案例,相当一部分出在这里。用逻辑分析仪看波形,时钟极性和相位不对,接收端采样到的就是错位的数据。

我自己的习惯是统一用Mode 3,数据手册推荐、主从两边稳定。切换模式时要注意,Flash芯片如果之前用Mode 0通信过,直接切Mode 3大概率第一次操作会失败,因为芯片内部状态逻辑里时钟两个边沿的关系变了,稳妥做法是每次通信前把CS拉高复位一下,确保芯片回到命令接收初始态。

5. 坏块管理:Flash不可能永远可靠,系统得学会自救

5.1 NOR Flash也会出现坏块

一提到坏块,很多人的第一反应是"NAND才有坏块,NOR出厂保证没有"。这句话只说对了一半。NOR出厂时确实没有坏块,但使用中仍然会累积坏块,尤其擦写次数接近寿命极限,或者掉电发生在擦写过程中间,扇区数据就可能损坏。只读应用的设备可能一辈子碰不上,但像我这种循环写入日志的场景,必须当成必然会遇到来处理。

网上搜"flash bad block too much"能搜出一大片设备变砖的求助帖,基本都是从不做坏块管理开始的。存储设备有没有做坏块管理的区别,就像硬盘坏道处理的好坏,一个能自愈,一个只能返厂。

5.2 系统自救的两个基本动作

我的方案不复杂,两个动作。第一个是每次擦除后做校验:擦除完读回整片扇区,如果发现不是全0xFF,就认定坏块,标记到Flash末尾的坏块表里,后续分配逻辑跳过这个扇区。第二个是写入后立即回读:比较读回的数据和写入缓冲是否一致,不一致就换一个扇区重新写,同时更新坏块表。

标记表本身也要考虑磨损和掉电问题,我用的办法是在Flash末尾固定区域保存一份8字节的表项,包含扇区号、标记时间、CRC,每次更新前先擦除再写。这个区域相对独立,正常使用下磨损很慢。

5.3 日志环形缓冲和时间戳格式

数据记录我设计成环形缓冲,这是做日志存储最实用的结构。整块存储区划分为多个4KB扇区,头部记录写指针位置,尾部记录擦除进度,循环覆盖最旧的数据。时间戳格式固定为4字节Unix时间,加上2字节传感器数据、1字节长度和1字节CRC,单条记录8字节,正好对齐到偶数边界。

关键优化是:每满一个扇区才执行一次擦除,而不是每条记录擦一次。这样既减少擦除次数,又让坏块检测能按扇区维度处理。这个设计跑下来,十万次擦写寿命实际能撑很多年,客户再也不用担心设备用着用着就死掉了。

6. 烧录调试阶段卡住我最久的三个报错

6.1 Error: Flash Download failed - Target DLL has been cancelled

这个报错折腾了我将近一天,症状是MDK里点击下载,烧录进度条跑到一半就弹这个对话框。查资料时发现遇到的人特别多,但每个人原因还不太一样。排掉供电和接线问题后,我用排除法定位到真正的祸首:JLink和板子之间的SWD线太长,而且时钟速率配置得太高。

SWD协议对时序很敏感,线材超过20厘米、时钟超过1MHz就容易出问题。解决办法是把JLink的SWD速度从默认的4MHz降到1MHz,同时把杜邦线剪短。如果降速还不行,下一个怀疑对象就是目标板Flash算法和实际芯片不匹配,需要去MDK的Flash Download配置页重新选择正确的编程算法文件。

6.2 Cannot load flash device description / device description文件缺失

J-Flash烧录时提示找不到Flash设备描述,一般是J-Flash安装目录下的Device Description文件里没有对应Flash型号。国产Flash芯片特别常见,新出的型号在J-Link的数据库里还没有更新,就会报这个错。

解决方案是手动添加:在J-Flash里选择"Create new project",手动指定SPI Flash的容量、页大小、擦除命令。或者去J-Link官网下载最新的Device Support包安装。说到底,这个报错的本质是烧录器不认识你的芯片,你把芯片参数告诉它就行。同类报错还有"no loader specified",意思是烧录器没有给你的内核配置Flash Loader,在项目配置里指一下FLM文件路径即可。

6.3 内部Flash容量不足和烧录中途失败

把日志改到外部Flash之前,我也遇到过STM32内部Flash容量不足的报错。代码体积逼近极限时,下载固件会在链接阶段就报错。解决方案不复杂:工程里把不必要的调试输出关掉、优化编译等级,但治本的方法还是把大数据挪走。这个项目里日志和参数区全部移到SPI Flash后,内部Flash一下子释放出几十KB空间,再也没为容量发过愁。

搜索相关技术词时看到有人在折腾Cisco交换机升级IOS时遇到Flash容量不足,思路其实互通:删除不需要的镜像文件,腾出空间,或者外接存储介质扩充容量。嵌入式里的存储规划,本质上都是在做同一道算术题。

6.4 一次完整的排查链路参考

上面提到的报错组合出现时,我建议按这个顺序排查:

  • 第一步:用万用表确认目标板供电电压稳定,Flash芯片VCC两端量到3.3V
  • 第二步:确认SWD四根线(SWDIO、SWCLK、GND、VCC)连接可靠,线长尽量短
  • 第三步:把烧录时钟降到1MHz,排除时序问题
  • 第四步:读芯片ID,确认烧录器识别到的内核和实际一致
  • 第五步:在烧录软件里重新选择匹配的Flash编程算法文件
  • 第六步:如果还不行,把Flash算法里的Erase Full Chip选项勾上,先全片擦除再烧录

这套链路帮我解决过至少三块不同板子的烧录问题,翻了车不要慌,按顺序来大概率能定位到根因。

6.5 关于烧录时的掉电保护

最后补一句:烧录过程中断电是最伤Flash的,可能造成老旧数据残留、状态寄存器异常等问题。工业级做法是给烧录环节加一个独立的电源开关和状态指示灯,提醒操作人员烧录过程中不要断电。实测有一次同事急着拔电,再上电后Flash进入了一种"既不响应擦除也不响应写入"的异常状态,只能换芯片。这个经验是拿一片芯片换来的,希望对你有用。

7. 掉电检测与时间戳恢复的完整链路

7.1 硬件上的掉电检测电路

软件做得再好,硬件上也要给掉电留出反应窗口。我在系统电源输入端加了一个分压电阻网络,连到主控的ADC引脚,监控电压变化。正常工作时电压稳定在3.3V,一旦检测到电压跌到3.0V以下,立即触发外部中断,进入掉电保存流程。

这个窗口时间很紧张,主控必须在几毫秒内写完时间戳并执行Flash的写使能、页编程命令。好在我用的日志结构够精简,8字节数据一条命令就能搞定,实际测试从检测到掉电到完成写入,耗时不到2毫秒。

7.2 上电恢复的完整流程

上电后主控执行的步骤是固定的:初始化SPI、读Flash ID、读取头部写指针、加载最新时间戳、继续正常调度。关键点是如果断电发生在Flash擦除过程中,上电后要能检测到擦除未完成的状态,我通过在扇区头添加状态字段(Erased/Programming/Complete)来区分,一旦发现处在编程中状态,直接回退到上一个已完成扇区继续。

这个流程跑了大半年,模拟断电测试几百次,没有出现一次数据丢失或时间错乱。

7.3 回头再看这个项目的几点体会

整个"Flash时钟"项目做下来,我对Flash的认识从"一颗能存数据的芯片"变成了"一个有生命周期、有时序脾气、需要细心伺候的器件"。最难的问题往往不在应用层,而在你忽略的时钟极性、状态轮询、边界条件这些细节上。做嵌入式的朋友应该都明白,这种从报错到根因的排查过程,才是项目真正值钱的部分。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询