STM32G070 Flash分块存储:一次擦除多次写入的掉电参数保存方案
2026/9/17 7:49:35 网站建设 项目流程

前几天一个做智能断路器项目的朋友跑过来找我,说设备跑一阵子后参数莫名其妙丢了,而且整页Flash的擦写次数消耗得特别快。我一看他的代码,每次改一个uint16_t配置,都是整页擦掉重写,能不丢吗?这其实是嵌入式开发里非常经典的存储问题,尤其在STM32G070xx这种Cortex-M0+内核、Flash容量几十到上百KB的超值型MCU上,很多人还是会把Flash用得很浪费。

STM32G070xx内部Flash虽然比EEPROM快,但它擦除是整页整页来的,写入又只能在“1变0”的方向上进行。如果每次都图省事整页擦掉再写,寿命和效率都扛不住。今天我想好好聊聊我在这颗芯片上常用的一套办法——把Flash页分块存储,用固定数据块的方式管理,做到一次擦除、多次写入。这个方案不需要外挂存储芯片,不增加BOM成本,代码量也不算大,非常适合用来保存校准参数、运行状态、累计数值这类掉电不能丢的数据。

1. 整体设计与思路拆解

1.1 为什么Flash不能像EEPROM那样“改了再存”

很多人用惯了外部EEPROM或者模拟EEPROM,总觉得存储就是“把数据写到某个地址”,下次直接覆盖就好。但内部Flash的物理特性决定了它完全不是这么回事。

Flash存储单元的特点是:擦除操作会把一整页的存储单元全部恢复为0xFF,写入操作只能把某些位从1写成0,反过来是做不到的。也就是说,你写过一个字节之后,如果想把某个位从0改回1,只能把整个页擦除一遍,否则无论你怎么写,0还是0,改不回来。

这就带来两个直接后果。第一个后果是修改一个很小的参数也要擦除整页,比如你只想改一个字节,却必须把一页数据全读出来、擦掉、改好、再写回去,整个操作既慢又危险。第二个后果是Flash擦写寿命被快速消耗,STM32G070xx这类MCU的内部Flash擦除寿命典型值在1万次左右,如果你一天校准两次设备,全页擦写的方式几年下来就到寿命了,Flash一旦达到寿命,存储单元开始不稳定,数据丢失、校验失败这些怪问题就都来了。

我可以用一个生活化的类比:Flash就像一张只能涂黑、不能擦掉的答题卡。第一次写错答案时,最蠢的办法是把整张卡扔掉重来;稍微聪明的做法是在旁边找个空格重新涂一遍。分块存储就是给这张答题卡画好格子,一个格子一个格子用,用完了再换下一张卡。

1.2 固定数据块方案的核心思想

分块存储的思路其实不复杂,核心就三句话:把一页里面切成若干个固定大小的数据块;新数据永远写到下一个还没用过的空闲块;所有块都用完之后才做一次整页擦除,然后从头再来。

假设一页是1KB,如果你把数据产品定义为64字节一个块,那么一页能放16个块。原来一页只能支持1次有效数据更新(改一次擦一次),现在可以支持连续写16次不同的值才需要擦除一次。如果配置数据很少,比如只有16字节一个块,一页就能放64个块,写64次才擦一次,寿命摊薄得很明显。

这里有个容易被忽略的点:既然每个块的大小是固定的,那块的“状态”也需要管理。哪些块是空闲的,哪些块已经写了有效数据,哪些块里的数据已经作废,这些信息本身也要存在Flash里。通常做法是在每个数据块的开头或结尾放一个固定长度的状态字,状态字初始为0xFFFFFFFF,写入有效数据后把它写为0x00000000,或者按位写入特定的标记值,通过状态字来区分块是否被占用。

另外,读取的时候不能只看块状态是“有效”就认为它是最终数据。因为同一页里可能有好几个块都被写入了不同版本的数据,最理想的做法是给每个块加入一个递增的序号,读取时扫描整页,找到序号最大、校验通过的那个块作为最新数据。这套逻辑稍复杂一点,但能应对掉电中断等异常情况,具体我在后面实操部分展开。

1.3 为什么不用外部EEPROM或者SPI Flash

可能有朋友会问,既然内部Flash存储这么麻烦,为什么不上一个AT24C02或者W25Q16?这要分场景看。

外部EEPROM确实有它的优势,按字节擦写、不占主控Flash、容量可以选。但同时也要多一颗物料、多占PCB面积、多一路I2C或SPI通信,还得考虑I2C总线上拉电阻、时序稳定性、驱动代码调试。很多消费类产品对BOM成本敏感,一颗EEPROM看起来只有几毛钱,但加上贴片、测试、组装成本,一年几百万的量就是不小的数字。

SPI Flash容量大、价格便宜,但它同样存在按扇区擦除的问题,而且需要额外驱动,启动阶段如果要从外部Flash加载代码或数据,还要自己处理映射和缓存逻辑,工程复杂度上了一个台阶。

分块存储方案的优势在于:完全使用MCU内部资源,零额外成本;擦写通过HAL库函数直接操作,逻辑清晰;写入速度通常比外部I2C EEPROM快;配合序号和校验,掉电安全性做得好的话,可靠性也足够。它的局限也很明显,容量有限,不适合存大量数据;Flash擦写次数上限固定,写入频率极高的场景不适用;代码需要仔细设计,不能像读RAM变量那样随意。

所以我的建议是:如果只是存几十到几百字节的配置参数、校准值、累计计数、设备状态,优先用本文这套内部Flash分块存储方案;如果数据量达到KB级别且频繁追加,才考虑外挂SPI Flash;如果应用需要频繁改写单字节且对实时性要求高,外挂EEPROM可能更省心。

2. 硬件基础与底层原理解析

2.1 STM32G070xx的Flash硬件特性

先明确一个概念,STM32G070xx属于STM32G0系列,Cortex-M0+内核,最高主频64MHz。这颗芯片的一大特点就是性价比高、外设丰富,但Flash部分和F0系列有些差异,用之前一定要看参考手册里的Flash章节,别凭老经验想当然。

以常见的STM32G070CBT6为例,它集成128KB Flash和36KB RAM。具体到Flash方面,有几个关键参数会直接影响你的存储设计:

  • 页大小:型号不同页大小可能不同,G0系列里不同容量的页大小不完全一致,我自己的工程按1KB一页处理过,但你在动手前一定要打开参考手册RM0444的Memory Map章节确认你的具体型号。页大小决定了你一次擦除的最小物理单位,也决定了分块数量的上限。
  • 编程最小单位:Flash写入不是按字节随便写,而要按照一定的对齐宽度操作,常见的是32位字(word)或者64位双字(double word)。写入地址必须对齐到对应的宽度边界,数据宽度也要匹配,否则HAL库会直接返回错误。下面代码示例我以32位字为准演示。
  • 擦除时间:整页擦除的典型时间大约在十几到几十毫秒级别,具体数值看手册的电气特性表。虽然看起来不长,但如果擦除过程中发生掉电,就会出现部分擦除、数据不一致的情况,所以擦除前后的状态管理必须做。
  • 擦写寿命:典型10000次,这不是说只能用一万次,而是指在数据保持能力和擦写次数之间有个平衡,超过这个范围后Flash单元可能出现位翻转、写不进、读不稳等现象。分块存储的本质,就是把这个一万次擦除寿命通过“分块”放大成几万甚至几十万次的数据更新次数。

还有一点非常容易被忽略:Flash的等待周期和电压范围。主频跑上去之后,Flash访问需要插入等待周期(wait states),如果供电电压偏低或者等待周期配置不对,程序跑起来会出现随机死机,甚至下载调试都失败。这类问题看起来是Flash“坏了”,其实根本不是,后面我单独讲排查。

2.2 Flash访问接口与HAL库函数

MCU内部的Flash是通过AHB总线访问的,CPU取指令、读常量、读数据都会走这条路。写Flash和擦除Flash时,Flash控制器需要处于解锁状态,操作完成后要重新上锁,防止程序跑飞时意外修改Flash内容。

HAL库把底层寄存器操作封装好了,常用的API就几个:

  • HAL_FLASH_Unlock() / HAL_FLASH_Lock():解锁和上锁。
  • HAL_FLASH_Program(typeProgram, Address, DataAddress):写入数据,typeProgram指定写入宽度,Address是目标Flash地址,DataAddress是源数据地址。
  • HAL_FLASHEx_Erase(&EraseInitStruct, &PageError):擦除页面,需要先填好结构体,比如Bank、Page、NbPages。
  • HAL_FLASH_OB_Unlock() / HAL_FLASH_OB_Lock():操作选项字节时用,平时不用动。

一个典型的写Flash流程大概是这样:

FLASH_EraseInitTypeDef eraseInit = {0}; uint32_t pageError = 0; HAL_FLASH_Unlock(); eraseInit.TypeErase = FLASH_TYPEERASE_PAGES; eraseInit.Banks = FLASH_BANK_1; eraseInit.Page = targetPage; eraseInit.NbPages = 1; if (HAL_FLASHEx_Erase(&eraseInit, &pageError) != HAL_OK) { // 擦除失败,pageError会指出是哪一页出错 } // 以32位字为单位写入 uint32_t data = 0x12345678; HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, targetAddress, (uint32_t)&data); HAL_FLASH_Lock();

这里有个工程上的要点:擦除和写入Flash时,CPU必须保证不访问正在被擦除的Flash页。如果CPU正在从Flash取指令,而这条指令恰好位于你正在擦除的页里,总线就会出问题,轻则操作卡死,重则触发硬件错误。所以我在实际项目里,要么把擦写函数放在固定的一块不会被擦除的Flash区域,要么直接放到RAM里执行,后者更稳妥,后面代码里我会给出具体做法。

2.3 状态字与块结构设计

分块存储不是简单地把一页切成几段直接用,数据格式一定要设计好。我一般会把一个块设计成“块头 + 数据区”的结构:

字段大小说明
Magic4字节固定魔数,比如0xA5A5A5A5,用于判断块是否已初始化
Seq4字节序号,每次写入递增,用于判断哪块是最新数据
DataN字节实际数据,固定大小
CRC162字节对Data区的校验,检测写入异常或数据损坏

块大小尽量对齐到4字节,这样用32位字编程时不会出现跨字的问题。状态位我不单独用一个字节存“空闲/有效/作废”,而是结合Magic和Seq来区分,这样设计逻辑更自然,也不浪费空间。

具体约定如下:

  • 整页擦除后,所有字节都是0xFF,此时Magic为0xFFFFFFFF,表示块空闲未使用。
  • 写入时先写Magic和Seq,再写Data,最后写CRC。全部写完后,Magic变成0xA5A5A5A5,表示块已经有效。
  • 读取时从头扫描每个块,Magic等于0xA5A5A5A5且CRC校验通过,视为有效块;多个有效块时取Seq最大的那个。
  • 如果需要作废旧数据(比如要回滚或标记无效),可以在状态字段中按位写0,人为破坏CRC,或者在块头增加一个Flag位,用写0方式标记。

这里必须强调一个Flash操作铁律:任何时候都不要尝试去修改一个已经有内容的数据块里的数据。因为0不能写回1,你不可能通过写入来“更新”旧值。正确做法永远是:新数据写新块,旧块通过标记作废。这也是“一次擦除多次写入”能够成立的根本逻辑。

2.4 擦写寿命与“摊薄”计算

分块存储带来的直接收益是寿命放大。假设你的产品数据是32字节一个块,一页1KB可以放约30个块(扣掉块头开销,按32字节对齐算大概28~30个)。原来1万次擦除只能支持1万次数据更新,现在一页写满才擦一次,最多可以支持约30万次更新,寿命放大了快一个数量级。

但是要注意,这是理想情况。实际产品中如果数据写入频率不稳定,或者你做了磨损均衡(多页轮换),寿命计算还要更精细。我的习惯是先估算最恶劣情况,比如设备每10秒存一次数据,一天8640次,如果用全页擦写方案,1万次寿命一天多就完了,这显然不现实;换了分块方案后能顶一个月左右,但如果产品要跑十年,那还得继续优化,比如减少写入频率、增大块数量、用多页轮换磨损均衡。

计算擦写寿命没有捷径,核心公式就一个:可用寿命 = Flash擦除寿命 × 每页块数量 × 页数 ÷ 每天写入次数。根据结果决定你的块大小、页面数量,以及是否需要磨损均衡。把这些想清楚了再写代码,比上来就调HAL函数靠谱得多。

3. 实操实现:STM32G070xx分块存储完整流程

3.1 存储区布局与基础宏定义

先规划好存储区。我建议把Flash分区为“程序区”和“参数区”,参数区固定使用最后的1~2页。为什么用最后一页?因为应用代码从起始地址开始烧录,容量是慢慢增长的,用最后一页做存储,程序扩容时不容易覆盖到数据区。

下面是一份可以直接移植到工程里的基础定义,以1KB一页为例(具体页大小按你的芯片手册确认):

#define FLASH_PARAM_PAGE (63) // 使用最后一页,128KB Flash的话是Page63 #define FLASH_PARAM_BASE (0x08000000UL + FLASH_PARAM_PAGE * 1024UL) #define FLASH_PAGE_SIZE (1024UL) // 页面大小,以手册为准 #define FLASH_BLOCK_DATA_SIZE (32UL) // 数据区固定大小,建议4字节对齐 #define FLASH_BLOCK_COUNT ((FLASH_PAGE_SIZE - 8UL) / (sizeof(BlockHeader_t) + FLASH_BLOCK_DATA_SIZE)) typedef struct { uint32_t magic; // 魔数,0xFFFFFFFF为空闲,0xA5A5A5A5为有效 uint32_t seq; // 序号,越大越新 uint8_t data[FLASH_BLOCK_DATA_SIZE]; uint16_t crc; // 数据区CRC校验 uint16_t reserved; // 保留,对齐到4字节 } BlockHeader_t; #define FLASH_BLOCK_SIZE (sizeof(BlockHeader_t)) #define FLASH_BLOCK_MAGIC_FREE (0xFFFFFFFFUL) #define FLASH_BLOCK_MAGIC_VALID (0xA5A5A5A5UL)

块大小里面放了BlockHeader_t,注意数据区固定32字节,加上Magic、Seq、CRC、保留字段,总大小会在44字节左右,整块对齐到4字节。实际一页能放多少块,用上面的宏计算,代码里不要写死块数,这样即使后续调页大小也不用改太多。

3.2 核心函数:扫描块、写入、擦除

先写扫描函数,读取出当前页面里最新有效的块。因为Flash读取可以直接按指针访问,不需要HAL函数,所以这个函数很轻量。

int flash_param_find_latest(uint8_t *out_data, uint32_t *out_seq) { BlockHeader_t *block; uint8_t *base = (uint8_t *)FLASH_PARAM_BASE; uint32_t best_seq = 0; int found = 0; for (int i = 0; i < FLASH_BLOCK_COUNT; i++) { block = (BlockHeader_t *)(base + i * FLASH_BLOCK_SIZE); if (block->magic != FLASH_BLOCK_MAGIC_VALID) { continue; // 空闲块,跳过 } uint16_t cal = crc16(block->data, FLASH_BLOCK_DATA_SIZE); if (cal != block->crc) { continue; // 校验失败,数据损坏,跳过 } // 序号最大的有效块就是最新数据 if (block->seq > best_seq) { best_seq = block->seq; memcpy(out_data, block->data, FLASH_BLOCK_DATA_SIZE); out_seq = best_seq; found = 1; } } return found ? 0 : -1; }

写入函数要复杂一些,因为要先找到下一个空闲块,如果整页都写满了,就需要先擦除再从头写。写入之前务必要计算CRC,并把数据组织成BlockHeader_t结构。

int flash_param_write(const uint8_t *in_data) { BlockHeader_t header; BlockHeader_t *block; uint8_t *base = (uint8_t *)FLASH_PARAM_BASE; uint32_t last_seq = 0; int free_index = -1; // 先扫描,找到已有的最大序号和第一个空闲块位置 for (int i = 0; i < FLASH_BLOCK_COUNT; i++) { block = (BlockHeader_t *)(base + i * FLASH_BLOCK_SIZE); if (block->magic == FLASH_BLOCK_MAGIC_VALID) { if (block->seq > last_seq) { last_seq = block->seq; } } else { if (free_index < 0) { free_index = i; // 记录第一个空闲块 } } } // 如果没有空闲块,整页擦除,从头开始 if (free_index < 0) { flash_param_erase_page(); last_seq = 0; free_index = 0; } // 填充块头 header.magic = FLASH_BLOCK_MAGIC_VALID; header.seq = last_seq + 1; memcpy(header.data, in_data, FLASH_BLOCK_DATA_SIZE); header.crc = crc16(header.data, FLASH_BLOCK_DATA_SIZE); header.reserved = 0; // 逐个32位字写入Flash uint32_t *src = (uint32_t *)&header; uint32_t *dst = (uint32_t *)(base + free_index * FLASH_BLOCK_SIZE); HAL_FLASH_Unlock(); for (int i = 0; i < FLASH_BLOCK_SIZE / 4; i++) { if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, (uint32_t)&dst[i], src[i]) != HAL_OK) { HAL_FLASH_Lock(); return -2; // 写入失败 } } HAL_FLASH_Lock(); return 0; }

擦除函数里面有一个很多新手会踩的坑:擦除过程中CPU执行的代码不能落在被擦除页里。在我这个设计里,参数区放在最后一页,擦除函数和调用它的代码都在前面几页,所以CPU取指令不会跑到被擦除的页里,算是安全的。如果你把参数区放在代码附近,或者你的代码量已经非常靠近最后一页,那就要小心了。

int flash_param_erase_page(void) { FLASH_EraseInitTypeDef eraseInit = {0}; uint32_t pageError = 0; HAL_FLASH_Unlock(); eraseInit.TypeErase = FLASH_TYPEERASE_PAGES; eraseInit.Banks = FLASH_BANK_1; eraseInit.Page = FLASH_PARAM_PAGE; eraseInit.NbPages = 1; if (HAL_FLASHEx_Erase(&eraseInit, &pageError) != HAL_OK) { HAL_FLASH_Lock(); return -1; } HAL_FLASH_Lock(); return 0; }

这个实现已经能跑,但我还建议加一层保险:在写到最后一个空闲块时,不要等下次写数据才发现满了再去擦除,而是在写入时就主动检查,如果本次写入后空闲块数为0,马上做一次擦除并把当前数据写到新块。这样能避免“擦除时突然掉电,数据全丢”的极端情况。当然,真遇到擦除时掉电,整页数据本来也保不住,所以更专业的做法用双页备份,我放到3.4节讲。

3.3 读取与掉电安全设计

分块存储的读取逻辑本身就自带掉电恢复能力。因为每次写入都是“新数据写新块”,而不是修改旧块,所以Flash里永远保存着历史版本。掉电只可能发生在几个窗口期:

  • 写入过程中掉电,块写了一半,Magic要么还是0xFFFFFFFF,要么已经写成0xA5A5A5A5但CRC不对,扫描时会跳过,不影响旧数据。
  • 写Magic和Seq成功、Data写了一半掉电,CRC校验也能发现数据损坏,同样跳过。
  • 写入完整成功后掉电,不影响。

所以只要扫描逻辑严格检查Magic和CRC,掉电导致的半写块就不会被当成有效数据。

但这里有一个细节必须注意:CRC校验要在写入前计算好,写入时要把CRC放在最后写,或者至少保证CRC写入完成前,这个块不会被判定为有效。上面代码的字段排列顺序是Magic、Seq、Data、CRC,写入时按顺序从低地址向高地址写,最后一个才写CRC,这正好保证了“CRC完整写入”是块有效的最后一步。顺序写反了,功能也正常,但掉电安全性会差不少。

有些项目只用一块数据,不需要关心历史版本。但如果是多字段参数,比如存储一组传感器校准系数,我的建议是把所有参数打包成一个固定结构体,整体当作一个块的数据区写入。这样能保持单块结构简单,避免为每个参数分配一个块,导致扫描逻辑膨胀。

3.4 磨损均衡扩展:多页轮换

如果产品的写入频率很高,单页分块还是不够,可以扩展到多页轮换。思路是把参数区扩大到N页,每次先从所有页中找出当前活跃页(存放最新数据的页),写满后跳到下一页,所有页都写满后再擦除最旧的一页。

多页轮换的实现复杂度主要在活跃页的识别和擦除时机控制上。我常用一个简单办法:在页头的固定偏移处记录本页起始序号,扫描时比较各页起始序号,序号最大的页就是活跃页。

方案单页分块多页轮换
代码复杂度
寿命放大倍数块数倍块数 × 页数倍
掉电恢复简单,扫描最新有效块即可需要处理活跃页切换瞬间的掉电
适用场景低频参数保存高频计数值、日志追加

多页轮换看起来好,但引入的问题也不少,比如擦除旧页的时机要避开活跃页切换窗口,否则掉电后可能找不到活跃页。我的建议是:先用单页分块方案跑通,确认真有寿命需求再加多页轮换,不要一开始就把复杂度拉满。

4. 常见问题与排查技巧实录

4.1 调试下载失败:Flash Download Failed

很多人在调试STM32G070时遇到报错,比如 error: flash download failed - target dll has been cancelled,或者在Keil、STM32CubeProgrammer里下载到一半提示失败。第一反应是代码有问题,其实十有八九是调试器连接和Flash保护状态的问题。

排查顺序我建议这样来:

  1. 检查供电是否稳定。G070是低功耗MCU,调试器供电能力不足时擦写Flash瞬间电流波动大,会直接中断下载过程,换一根短USB线或者外接3.3V电源先排除供电因素。
  2. 检查调试接口速度。SWD时钟频率设太高容易在不稳定连接下失败,把SWD频率从4MHz降到1MHz试一下。
  3. 检查芯片读保护。如果之前烧录过程序,并且不小心开了RDP(读保护),调试器无法正常读写Flash。这时用STM32CubeProgrammer连接,如果提示读保护,先做Full Chip Erase解除保护再重新下载。
  4. 检查复位电路。调试器需要控制复位引脚进行下载,复位引脚被强制拉低或者外接大电容导致复位信号太慢,都会导致下载失败。

这类问题不是分块存储代码本身的锅,但很多人因为在调Flash写函数时频繁触发硬件错误,然后就开始“Flash下载失败”,所以我把这个放在第一位提醒。

4.2 擦写Flash时程序跑飞或进入HardFault

写入和擦除Flash时出现死机、HardFault,最常见的原因是CPU正在从被擦除或正在写的Flash页取指令。前面已经提过,G070内部Flash是统一编址的,CPU取指令也在同一地址空间。当你调用HAL_FLASHEx_Erase擦除某一页时,如果当前正在执行的代码恰好位于这一页,硬件就会出现总线错误,直接进入HardFault。

要避免这个问题,最简单的办法是让擦写相关代码所在区域和参数区分离。比如参数区固定放在最后一页,而所有Flash相关函数放在程序的早期地址区域,这样函数永远不会落在被擦除的页里。如果你的程序比较大,代码段已经快占满整个Flash,那就要考虑把Flash操作函数放到RAM里执行:

__attribute__((section(".ramfunc"))) void flash_operation_code(void) { // Flash擦除和写入相关函数 }

把擦写函数放进RAM执行还有一个额外好处:擦写Flash期间,CPU不需要从Flash取指令,从根上避免了取指冲突。代价是RAM占用增加,一般几十到一两百字节,对36KB RAM的G070来说完全可以接受。

4.3 写完的数据读出来还是0xFF或校验不对

如果写入后读取,发现数据全0xFF或者CRC校验失败,首先别怀疑Flash坏了,多半是下面几个原因:

  • 没有调用HAL_FLASH_Unlock就直接写。Flash控制器处于锁定状态时,写操作会被忽略,读出来自然还是原来的0xFF。对应处理是确保每次写前Unlock,写完Lock。
  • 写入地址没有按32位字对齐。Flash写入最小单位是字,地址必须是4的倍数,如果结构体大小不对齐,或者计算偏移时出现1字节偏差,写入结果就会异常。
  • 写入数据里本身包含0xFF。Flash只能把1写成0,如果你要写的数据本来就是0xFF,那跟没写一样,读出来自然还是0xFF。这种情况在状态字段设计时要留意,比如有效标志不要用0xFF,否则擦除后状态和有效状态就混淆了。
  • 时钟或电压不稳定导致编程时序失败。HAL库的Program函数调用后返回HAL_OK才算成功,如果你忽略返回值,写入失败后继续往下走,读出的数据当然不对。务必要检查返回值,并在失败时做错误处理。

4.4 写满后的擦除时机与历史数据保留

分块存储虽然能多次写入,但擦除时机选择不好,会掉进“写满才发现没地方写,结果擦除时掉电,数据全丢”的坑。我在项目里的做法是:写入时提前判断空闲块数量,如果剩余块小于等于1,先把当前要写的数据写到新块,然后立即安排一次全页擦除,并把新数据重新写入。这样即使擦除后掉电,最坏情况也只是丢一次更新,而不是把之前所有有效数据都弄丢,损失可控。

还有一点,在生产测试时,设备可能频繁写入测试配置,导致存储页快速写满并反复擦除。如果测试脚本没有做“恢复到出厂设置”的操作,用户拿到手的产品可能已经消耗了一部分Flash寿命。建议生产测试流程的最后一步,主动调用一次参数区的整页擦除,把Flash恢复到出厂状态,避免测试数据残留。

4.5 常见问题速查表

现象可能原因解决建议
下载失败 target dll cancelled供电不稳、SWD速度过高、读保护换线、降速、CubeProgrammer全片擦除
擦写时HardFault执行代码位于被擦除页擦写函数放RAM或固定区域执行
读回0xFF未解锁、地址未对齐、数据本身为0xFF检查Unlock、对齐、数据结构
CRC校验错数据写入不完整、半字写入检查块字段排列,最后写CRC
数据丢得莫名其妙页擦除时机未管理好提前判断空闲块,写前预留擦除窗口
寿命不够写频率太高、块数太少减小块大小、增加页数、多页轮换

5. 最后一点实操心得

分块存储这套东西,我最早是在做一个带掉电保存功能的电动工具控制器时用起来的,那时候没有经验,第一版就是直接整页擦写,结果现场设备用了一段时间后参数频繁丢失,售后压力很大。后来痛定思痛,重新设计了分块存储逻辑,再加上掉电检测和延迟处理,问题才算彻底解决。

如果你正准备在STM32G070xx上做类似功能,我的建议是:先别急着抄完整代码,花半天时间把芯片手册的Flash章节翻一遍,搞懂页大小、编程宽度、擦除时间这三个参数。然后把数据结构设计好,尤其是Magic、Seq、CRC这三个字段一个都不能少,它们是掉电安全的基石。最后才是写HAL函数调用。

还有一个容易踩的小坑:调试时如果老是在写Flash函数上打断点,单步调试会导致Flash进入奇怪状态,建议在调试阶段先做一个“只读模式”,真正要验证写入时,再打开写功能并用串口打印日志来观察,不要反复单步进入Flip操作。我试过好几次,单步进HAL_FLASH_Program之后程序就不正常了,不是代码问题,是调试器在单步时对Flash控制器的干扰。

这套方案做下来,整个Flash操作部分我控制在三百行代码以内,裸机工程里跑得很稳。后续如果写入频率继续提升,我打算把双页备份和磨损均衡加进去,但目前这个量级的项目确实用不上了。希望这篇分享对你有帮助,也欢迎在实际项目里踩到坑后回来交流。

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

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

立即咨询