STM32片内FLASH读写实战:替代EEPROM的完整方案与避坑指南
1. 项目缘起为什么要在STM32上操作FLASH如果你用过STM32大概率接触过它的EEPROM或者至少听说过。但很多人在项目做到一半发现数据需要掉电保存时第一个念头是“加个外置EEPROM芯片”。这个想法没错但对于成本敏感、PCB空间有限或者只是想“偷个懒”的工程师来说STM32片内自带的FLASH其实是一个被严重低估的宝藏。我最初接触片内FLASH操作是因为一个电池供电的传感器节点项目。节点需要每小时记录一次环境数据并在上位机查询时通过无线方式上传。如果使用外置EEPROM不仅增加了几毛钱的BOM成本还多占了两个I/O口和PCB面积。而STM32F103系列片上有64K甚至128K的FLASH程序只用了不到一半剩下的空间“荒着”也是浪费为何不拿来存数据呢这个想法很美好但一脚踩进去才发现坑不少。官方手册关于FLASH操作的章节往往写得比较“硬件”直接上手HAL库函数又可能会遇到各种“玄学”问题比如写进去的数据读出来不对或者操作一次后芯片莫名其妙死机。网上找的代码片段质量参差不齐有的甚至存在严重隐患。这次我就把基于HAL库的STM32 FLASH读写从原理到避坑彻底讲清楚并附上经过大量项目验证的稳健代码。简单来说这个实验的核心价值是教你安全、高效地利用STM32片内剩余的FLASH空间实现类似EEPROM的掉电存储功能省下外置芯片的成本和空间。无论你是正在做毕业设计的学生还是从事产品开发的工程师掌握这项技能都能让你的方案更灵活。2. 核心原理FLASH与EEPROM、RAM的根本区别在写代码之前必须搞清楚我们在操作的是一个什么样的存储器。很多人对FLASH、EEPROM、RAM的概念是模糊的用错特性就会导致程序崩溃。RAM (随机存取存储器) 这好比你的办公桌面。数据放上去、拿下来都特别快纳秒级可以随意修改桌面任何位置的一张纸字节级修改。但一旦下班断电桌面就被清空了易失性。在STM32里变量、数组、堆栈都是放在RAM里的。EEPROM (电可擦除可编程只读存储器) 这好比你办公室里的一个档案柜。存放东西比较慢毫秒级但断电后数据依然在非易失性。它的关键优势是可以按字节擦写。你想修改档案柜里A档案中的某一页可以直接取出那一页修改后放回不影响其他档案。很多STM32型号内部就集成了这样的“小档案柜”即真正的EEPROM如STM32L系列或者通过模拟实现如STM32F1的Data EEPROM。FLASH (闪存存储器) 这是我们今天的主角也是存放你程序代码的地方。它更像是一本已经印刷成册的书。非易失成本低容量大。但它有两个关键限制理解不透一定会出问题必须先擦后写你不能直接拿笔在书的某一页上修改几个字。你必须先把整一个扇区Sector或整页Page的纸全部撕掉擦除为0xFF然后再用笔重新写入新的内容写操作将某些位从1变为0。擦除操作很慢几十到几百毫秒且是以“块”为单位的。写操作只能将‘1’变为‘0’在FLASH的物理层面擦除后的状态是全‘1’0xFF。写入操作实际上是施加电压将特定的“浮栅晶体管”的阈值电压拉高在逻辑上表现为将对应的位从‘1’变成‘0’。你无法通过写操作将‘0’变回‘1’这个“由0复1”的过程只能通过擦除整个块来完成。在STM32上我们运行的程序本身就在FLASH里。现在我们要把数据也存进去这就意味着我们和程序代码共享了同一个物理存储器。所以绝对绝对不能去擦写当前正在运行的程序所在的扇区否则处理器会取指失败立即死机。我们的操作必须严格限定在程序未使用的、地址空间靠后的FLASH区域。HAL库提供了一组函数HAL_FLASH_Program,HAL_FLASHEx_Erase等帮我们封装了底层复杂的时序和控制寄存器操作。我们的任务就是正确地、安全地调用这些函数。3. 实战准备划定安全区与关键参数确定动手写代码前需要做两件至关重要的事划定安全操作的FLASH地址范围以及理解你所用芯片的具体参数。3.1 如何划定FLASH操作安全区这是整个项目安全性的基石。你不能假设程序永远只占用固定的空间。第一步查看链接脚本.ld文件或.sct文件在IDEKeil MDK或STM32CubeIDE中编译工程后会生成一个链接脚本文件。它定义了程序代码、数据、堆栈等在内存中的布局。你需要找到FLASH区域的定义并重点关注程序实际占用的长度。例如在Keil MDK中编译后可以在.map文件里看到Code (inc. data) RO Data RW Data ZI Data Debug 12345 6789 1024 500 2000 xxxxx Object Totals这里的RO Data(只读数据如const常量)和Code(代码)都是要烧录到FLASH中的。但最准确的方法是看.map文件末尾的Memory Map of the image找到FLASH部分的起始和结束地址。一个更工程化的做法是在代码中声明一个特殊的变量来标记程序结束的地址。例如在链接脚本中定义一个符号_App_End然后在C代码中声明extern uint32_t _App_End;。(uint32_t)_App_End就是程序结束后的第一个地址。第二步选择操作起始地址为了安全起见我们不会紧挨着程序结束地址开始存数据。应该留出一定的余量例如1KB或一个扇区大小。假设我们决定从地址0x08010000开始作为数据存储区。第三步确认结束地址结束地址不能超过芯片FLASH的总容量。以STM32F103C8T6为例标称64KB FLASH地址范围是0x08000000~0x0800FFFF。那么0x08010000就已经超出了范围这里是一个经典坑点STM32F103C8T6实际上有128KB的FLASH但官方按64KB销售和标注。很多开发板使用的正是这个“隐藏容量”的版本。为了代码的通用性强烈建议以芯片手册和CubeMX中定义的容量为准不要依赖“隐藏容量”。更稳妥的做法是使用HAL库提供的函数HAL_FLASHEx_GetError和芯片定义#include “stm32f1xx_hal.h” // 根据你的系列引入 // 假设我们使用STM32F103C8 查看 stm32f103xe.h 中的定义 #define FLASH_END_ADDR (FLASH_BASE FLASH_SIZE - 1)FLASH_SIZE是在编译时根据芯片型号定义的一个值单位是字节。第四步对齐到扇区边界FLASH擦除以扇区为单位。STM32不同系列、不同容量的扇区大小差异很大。F1的小容量产品扇区是1KB大容量是2KB。F4的扇区更大从16KB到128KB不等。F0/F3的页大小可能是1KB或2KB。你必须根据你的具体芯片型号在数据手册或CubeMX中查明你要操作的地址范围属于哪个些扇区以及扇区大小。你的数据存储起始地址最好就是一个扇区的起始地址这样管理起来最方便。注意在调试阶段可以在main函数开始时通过printf打印出计算出的起始地址、结束地址、扇区信息确保万无一失。3.2 关键HAL库函数解析HAL库将FLASH操作简化为几个核心函数理解它们的参数和状态至关重要。解锁与锁定 (HAL_FLASH_Unlock/HAL_FLASH_Lock)FLASH的写/擦除控制寄存器是受保护的防止程序跑飞意外修改FLASH。在操作前必须解锁操作后建议立刻锁定。HAL_StatusTypeDef status; status HAL_FLASH_Unlock(); if (status ! HAL_OK) { // 处理错误可能是FLASH操作正在进行或HAL库状态不对 } // ... 执行擦除或编程操作 ... HAL_FLASH_Lock();扇区擦除 (HAL_FLASHEx_Erase)这个函数需要一个FLASH_EraseInitTypeDef结构体来配置擦除参数并返回擦除失败的扇区信息。FLASH_EraseInitTypeDef EraseInitStruct; uint32_t SectorError 0; EraseInitStruct.TypeErase FLASH_TYPEERASE_PAGES; // F1是PAGESF4是SECTORS EraseInitStruct.Banks FLASH_BANK_1; // 对于有多个BANK的芯片需要指定 EraseInitStruct.PageAddress DataStartAddr; // 要擦除的起始地址 EraseInitStruct.NbPages 1; // 要擦除的页数/扇区数 if (HAL_FLASHEx_Erase(EraseInitStruct, SectorError) ! HAL_OK) { // 擦除失败SectorError包含了出错的扇区号 // 可以调用 HAL_FLASH_GetError() 获取详细错误码 uint32_t flashError HAL_FLASH_GetError(); // 处理错误常见错误FLASH_ERROR_PGP编程并行度错误、FLASH_ERROR_WRP写保护错误 }关键点PageAddress必须是扇区的起始地址。NbPages是连续擦除的数量。擦除后整个扇区所有地址的数据都变为0xFFFFFFFF。编程写入 (HAL_FLASH_Program)擦除完成后才能进行写操作。HAL库提供了按不同数据宽度编程的函数最常用的是双字64位和字32位编程因为STM32的FLASH编程接口通常以这些宽度为单位效率最高。// 编程一个32位数据一个字 uint32_t Address 0x08010000; uint32_t Data 0x12345678; if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, Address, Data) ! HAL_OK) { // 编程失败 uint32_t flashError HAL_FLASH_GetError(); // 处理错误 } // 对于F4等支持双字编程的系列 // uint64_t Data64 0x123456789ABCDEF0; // HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, Address, Data64);重要限制你必须确保写入的地址是擦除后的值为0xFFFFFFFF并且写入的数据只能将‘1’变为‘0’。例如地址原始是0xFFFFFFFF写入0xFFFFFF00是允许的高24位1变1低8位1变0。但如果原始是0xFFFFFF00你想写入0xFFFFFFFF这是不可能的必须先擦除整个扇区。4. 代码实现一个健壮的FLASH管理模块直接调用HAL函数很底层我们需要封装一个更友好、更健壮的模块。这个模块需要解决几个实际问题磨损均衡可选、数据校验、写保护、以及最重要的——确保不会误擦程序区。以下是一个针对STM32F103系列1KB页的简化版模块flash_manager.c/hflash_manager.h#ifndef __FLASH_MANAGER_H #define __FLASH_MANAGER_H #include “stdint.h” #include “stdbool.h” // 定义FLASH数据存储区的起始地址和大小 // 假设程序占用空间小于60KB我们从第60个页0x0800F000开始预留4个页4KB用于存储 // 请务必根据你的实际工程链接结果修改此地址 #define DATA_FLASH_START_ADDR ((uint32_t)0x0800F000) #define DATA_FLASH_PAGE_SIZE (1024) // STM32F103C8T6 页大小为1KB #define DATA_FLASH_PAGE_NUM (4) #define DATA_FLASH_END_ADDR (DATA_FLASH_START_ADDR DATA_FLASH_PAGE_SIZE * DATA_FLASH_PAGE_NUM - 1) // 错误码定义 typedef enum { FLASH_MGR_OK 0, FLASH_MGR_ERROR_UNLOCK, FLASH_MGR_ERROR_ERASE, FLASH_MGR_ERROR_WRITE, FLASH_MGR_ERROR_ADDR_INVALID, FLASH_MGR_ERROR_DATA_CORRUPT, } FlashMgr_Status_t; // 初始化函数检查FLASH区域是否可用 FlashMgr_Status_t FlashMgr_Init(void); // 在指定偏移地址相对于DATA_FLASH_START_ADDR写入一段数据 FlashMgr_Status_t FlashMgr_Write(uint32_t offset, const void *pData, uint32_t size); // 从指定偏移地址读取一段数据 FlashMgr_Status_t FlashMgr_Read(uint32_t offset, void *pBuffer, uint32_t size); // 擦除整个数据FLASH区域慎用 FlashMgr_Status_t FlashMgr_EraseAll(void); #endif /* __FLASH_MANAGER_H */flash_manager.c- 核心实现与避坑点#include “flash_manager.h” #include “stm32f1xx_hal.h” // 包含HAL FLASH驱动 #include “string.h” // 用于memcpy // 内部函数声明 static bool is_addr_valid(uint32_t addr); static FlashMgr_Status_t flash_erase_page(uint32_t page_addr); // 初始化检查起始地址是否对齐是否在有效范围内 FlashMgr_Status_t FlashMgr_Init(void) { // 1. 检查地址对齐对于F1页起始地址应对齐到1KB边界 if ((DATA_FLASH_START_ADDR % DATA_FLASH_PAGE_SIZE) ! 0) { return FLASH_MGR_ERROR_ADDR_INVALID; } // 2. 检查地址是否超出芯片FLASH范围这里简化处理实际应根据FLASH_SIZE计算 if (DATA_FLASH_END_ADDR (FLASH_BASE FLASH_SIZE)) { return FLASH_MGR_ERROR_ADDR_INVALID; } // 3. 可选可以读取第一个字检查是否为已擦除状态(0xFFFFFFFF)初步判断区域是否可用 // if (*(__IO uint32_t*)DATA_FLASH_START_ADDR ! 0xFFFFFFFF) { // // 可能需要先擦除或者区域被程序占用 // } return FLASH_MGR_OK; } // 写入函数这是最复杂的部分需要处理跨页、数据覆盖等问题 FlashMgr_Status_t FlashMgr_Write(uint32_t offset, const void *pData, uint32_t size) { if (pData NULL || size 0) { return FLASH_MGR_OK; // 或返回一个无效参数错误 } // 计算绝对地址并检查整个写入范围是否有效 uint32_t start_abs_addr DATA_FLASH_START_ADDR offset; uint32_t end_abs_addr start_abs_addr size - 1; if (!is_addr_valid(start_abs_addr) || !is_addr_valid(end_abs_addr)) { return FLASH_MGR_ERROR_ADDR_INVALID; } // 解锁FLASH if (HAL_FLASH_Unlock() ! HAL_OK) { return FLASH_MGR_ERROR_UNLOCK; } FlashMgr_Status_t status FLASH_MGR_OK; uint32_t current_addr start_abs_addr; const uint8_t *pSrc (const uint8_t*)pData; uint32_t bytes_remaining size; // **关键循环逐字32位或逐双字编程** while (bytes_remaining 0 status FLASH_MGR_OK) { // 检查目标地址当前内容是否允许写入即要写入的位当前必须是1 // 例如我们要写入一个32位数Data32那么要求 (*(uint32_t*)current_addr Data32) Data32 // 这意味着Data32中所有为0的位在目标地址当前也必须为0实际上要求目标地址对应位为1因为只能1-0 // 更简单的判断如果目标地址当前值不是0xFFFFFFFF且我们要写入的值不是其子集则需要先擦除页。 // 这是一个简化处理我们假设每次写入都是对“干净”区域0xFF的写入。 // 如果遇到非0xFF区域本示例直接返回错误。实际产品中你可能需要实现更复杂的逻辑 // 例如将整个受影响页的数据读出来在RAM中修改然后擦除页再写回整个页。 uint32_t current_value *(__IO uint32_t*)current_addr; uint32_t data_to_write; uint32_t write_size; // 决定本次写入的数据宽度和大小 // 为了简单和兼容性我们使用32位编程。地址必须4字节对齐。 if ((current_addr % 4 0) (bytes_remaining 4)) { // 地址对齐且剩余数据4字节按字写入 memcpy(data_to_write, pSrc, 4); write_size 4; if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, current_addr, data_to_write) ! HAL_OK) { status FLASH_MGR_ERROR_WRITE; break; } } else { // 地址未对齐或剩余数据不足4字节需要按字节拼凑。 // **注意STM32 HAL库不直接支持字节编程。** // 一种常见做法是将包含目标地址的一个完整32位字读出来在RAM中修改对应的字节 // 然后擦除整个页再将修改后的32位字写回去。但这涉及读-改-擦-写非常复杂且影响寿命。 // **因此强烈建议在应用层设计时保证所有写入操作都是32位对齐的并且一次写入4字节的倍数。** // 这里为了演示我们假设传入的offset和size总是4字节对齐的。 // 如果不对齐我们返回错误。 status FLASH_MGR_ERROR_ADDR_INVALID; // 实际上应是“未对齐错误” break; } current_addr write_size; pSrc write_size; bytes_remaining - write_size; } // 锁定FLASH HAL_FLASH_Lock(); // 如果中途出错可以在这里获取详细错误码 if (status ! FLASH_MGR_OK) { uint32_t error HAL_FLASH_GetError(); // 可以打印或记录error帮助调试 } return status; } // 读取函数相对简单直接内存映射读取 FlashMgr_Status_t FlashMgr_Read(uint32_t offset, void *pBuffer, uint32_t size) { if (pBuffer NULL || size 0) { return FLASH_MGR_OK; } uint32_t start_abs_addr DATA_FLASH_START_ADDR offset; uint32_t end_abs_addr start_abs_addr size - 1; if (!is_addr_valid(start_abs_addr) || !is_addr_valid(end_abs_addr)) { return FLASH_MGR_ERROR_ADDR_INVALID; } // FLASH是内存映射的可以直接像读取数组一样读取 memcpy(pBuffer, (void*)start_abs_addr, size); return FLASH_MGR_OK; } // 擦除所有数据页谨慎使用 FlashMgr_Status_t FlashMgr_EraseAll(void) { if (HAL_FLASH_Unlock() ! HAL_OK) { return FLASH_MGR_ERROR_UNLOCK; } FlashMgr_Status_t status FLASH_MGR_OK; uint32_t page_addr; uint32_t sector_error; FLASH_EraseInitTypeDef EraseInitStruct; EraseInitStruct.TypeErase FLASH_TYPEERASE_PAGES; EraseInitStruct.Banks FLASH_BANK_1; // F1只有一个BANK // 计算起始页号。F1的页号从0开始。 // 公式Page (Address - FLASH_BASE) / PAGE_SIZE EraseInitStruct.PageAddress DATA_FLASH_START_ADDR; EraseInitStruct.NbPages DATA_FLASH_PAGE_NUM; if (HAL_FLASHEx_Erase(EraseInitStruct, §or_error) ! HAL_OK) { status FLASH_MGR_ERROR_ERASE; } HAL_FLASH_Lock(); return status; } // 内部辅助函数检查地址是否在数据FLASH区域内 static bool is_addr_valid(uint32_t addr) { return (addr DATA_FLASH_START_ADDR addr DATA_FLASH_END_ADDR); }5. 避坑指南与高级话题上面的代码提供了一个基础框架但在实际产品中你还需要考虑更多。5.1 数据校验与掉电保护FLASH写入过程中如果发生掉电数据可能处于半写状态部分位写成功部分位没写。为了检测这种情况需要引入数据校验机制。常用方法CRC校验在存储数据时额外存储一个CRC校验码。读取时重新计算CRC并与存储的对比。HAL库提供了硬件CRC计算函数HAL_CRC_Calculate速度很快。校验和简单求和抗干扰能力弱但计算快。版本号备份扇区更高级的做法。将一份数据存到两个不同的扇区A和B并附带版本号。每次更新数据时写到另一个扇区如当前在A则写到B并增加版本号。完成后将旧扇区擦除。这样即使写B时掉电A中仍然是上一版完好数据。读取时比较两个扇区的版本号取版本号最新的有效数据。5.2 磨损均衡FLASH每个扇区有擦写次数限制通常是10万次左右。如果你频繁更新同一个地址的数据会导致该地址所在的扇区很快损坏。简易磨损均衡策略在数据存储区内部分成多个“槽位”(slots)。每次写数据时写到下一个空闲槽位并更新一个“当前有效槽位”的索引这个索引本身也需要存储在FLASH中并且其磨损也需要均衡。当所有槽位快满时进行一次“垃圾回收”将有效数据整理到前面擦除后面的扇区。这其实就是简易文件系统或EEPROM模拟库如STM32自带的EEPROM Emulation中间件的工作原理。5.3 中断与看门狗FLASH擦写操作耗时很长几十ms这会阻塞CPU。在此期间所有中断都会被挂起因为CPU在执行擦写指令序列。这会导致通信超时如UART、I2C。系统定时器不准。看门狗可能超时复位。解决方案在擦写FLASH前暂停关键业务和通信或者确保它们能容忍短时间阻塞。喂好看门狗在启动长耗时擦写操作前先喂一次看门狗。如果擦写时间超过看门狗超时时间必须在擦写循环中插入喂狗操作。但注意FLASH操作期间代码执行在特定序列中直接调用HAL_IWDG_Refresh()可能不安全。一个可行的方法是利用STM32的IWDG独立看门狗其由独立时钟驱动即使核心暂停只要在超时前“喂狗”即可。可以在擦写函数的循环中每擦除一小块或每写入几个字后插入一个喂狗操作。while (bytes_remaining 0) { // ... 执行一部分FLASH编程 ... HAL_IWDG_Refresh(hiwdg); // 喂独立看门狗 // ... 继续 ... }使用RTOS在RTOS中可以将FLASH操作放在一个低优先级的线程中避免阻塞高优先级任务。但同样需要注意中断屏蔽和看门狗问题。5.4 调试常见错误与HAL_FLASH_GetError()当你调用HAL库函数失败时一定要检查错误码uint32_t error HAL_FLASH_GetError(); if (error HAL_FLASH_ERROR_PGP) { // 编程并行度错误通常是因为编程数据宽度不对如地址未对齐 } if (error HAL_FLASH_ERROR_WRP) { // 写保护错误尝试写入了受写保护的扇区可能是前部的程序区 } if (error HAL_FLASH_ERROR_OPTV) { // 选项字节错误与选项字节读保护、写保护配置有关 } // ... 其他错误“no algorithm found for: xxxxh”错误这个错误通常出现在使用调试器如ST-Link下载或擦除芯片时而不是在代码运行时。它意味着你的IDE如Keil没有为芯片的这片FLASH区域配置正确的编程算法。你需要确认在IDE的工程配置中正确选择了你的芯片型号并且FLASH算法文件存在。对于自定义的FLASH操作代码这个错误一般不会出现。6. 项目集成与测试例程最后我们写一个简单的main.c来测试这个FLASH管理模块。#include “main.h” #include “flash_manager.h” #include “stdio.h” // 用于printf // 假设已初始化USART用于打印调试信息 extern UART_HandleTypeDef huart1; // 重写fputc使printf能通过串口输出 int __io_putchar(int ch) { HAL_UART_Transmit(huart1, (uint8_t*)ch, 1, HAL_MAX_DELAY); return ch; } // 要存储的数据结构 typedef struct { uint32_t magic; // 魔数用于识别数据是否有效例如 0xAA55CC33 uint32_t counter; // 一个计数器 float sensor_value; // 一个传感器值 uint8_t checksum; // 简单的校验和 } MyData_t; // 计算校验和简单示例 uint8_t calculate_checksum(MyData_t *data) { uint8_t *p (uint8_t*)data; uint8_t sum 0; for (size_t i 0; i (sizeof(MyData_t) - sizeof(data-checksum)); i) { sum p[i]; } return ~sum; // 取反作为校验和 } int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USART1_UART_Init(); // 初始化串口 printf(“\r\n STM32 FLASH R/W Test \r\n”); // 1. 初始化FLASH管理器 FlashMgr_Status_t status FlashMgr_Init(); if (status ! FLASH_MGR_OK) { printf(“FlashMgr Init Failed! Error: %d\r\n”, status); Error_Handler(); } printf(“FlashMgr Init OK.\r\n”); // 2. 准备测试数据 MyData_t data_to_write, data_read; data_to_write.magic 0xAA55CC33; data_to_write.counter 0; data_to_write.sensor_value 25.6f; data_to_write.checksum 0; // 先置零 data_to_write.checksum calculate_checksum(data_to_write); // 3. 擦除整个数据区域首次运行或需要清空时 printf(“Erasing Flash Data Sector...\r\n”); status FlashMgr_EraseAll(); if (status ! FLASH_MGR_OK) { printf(“Erase Failed! Error: %d\r\n”, status); } else { printf(“Erase Success.\r\n”); } // 4. 写入数据从偏移地址0开始写 printf(“Writing Data to Flash...\r\n”); status FlashMgr_Write(0, data_to_write, sizeof(MyData_t)); if (status ! FLASH_MGR_OK) { printf(“Write Failed! Error: %d\r\n”, status); } else { printf(“Write Success.\r\n”); } // 5. 读取数据 printf(“Reading Data from Flash...\r\n”); memset(data_read, 0, sizeof(MyData_t)); // 清空读取缓冲区 status FlashMgr_Read(0, data_read, sizeof(MyData_t)); if (status ! FLASH_MGR_OK) { printf(“Read Failed! Error: %d\r\n”, status); } else { printf(“Read Success.\r\n”); } // 6. 验证数据 printf(“\r\n— Data Verification —\r\n”); printf(“Magic (Expected: 0x%08lX, Read: 0x%08lX) %s\r\n”, data_to_write.magic, data_read.magic, (data_to_write.magic data_read.magic) ? “[OK]” : “[FAIL]”); printf(“Counter (Expected: %lu, Read: %lu) %s\r\n”, data_to_write.counter, data_read.counter, (data_to_write.counter data_read.counter) ? “[OK]” : “[FAIL]”); // 比较浮点数不能直接用这里简化处理 printf(“Sensor Value (Expected: %.2f, Read: %.2f)\r\n”, data_to_write.sensor_value, data_read.sensor_value); // 计算读取数据的校验和 uint8_t read_checksum data_read.checksum; data_read.checksum 0; // 计算前清零存储的校验和 uint8_t calc_checksum calculate_checksum(data_read); printf(“Checksum (Stored: 0x%02X, Calculated: 0x%02X) %s\r\n”, read_checksum, calc_checksum, (read_checksum calc_checksum) ? “[OK]” : “[FAIL]”); // 7. 模拟“更新”操作增加计数器并写回 printf(“\r\n— Updating Counter —\r\n”); data_to_write.counter; data_to_write.checksum 0; data_to_write.checksum calculate_checksum(data_to_write); // **注意这里直接覆盖写。如果原地址不是0xFF会失败** // 因为我们之前擦除了整个区域所以第一次更新是OK的。 // 第二次运行程序时这里就会失败因为数据已经不是0xFF了。 // 实际应用中你需要实现前面提到的“读-改-擦-写”或磨损均衡逻辑。 status FlashMgr_Write(0, data_to_write, sizeof(MyData_t)); if (status ! FLASH_MGR_OK) { printf(“Update Write Failed! Error: %d\r\n”, status); printf(“This is EXPECTED if you run this code multiple times without erase.\r\n”); printf(“It demonstrates the ‘write after write’ limitation of FLASH.\r\n”); } else { printf(“Update Write Success.\r\n”); } while (1) { HAL_Delay(1000); // 主循环 } }运行这个程序通过串口助手观察输出。第一次运行所有操作应该成功。第二次运行不重新擦除你会发现“更新计数器”的写操作失败了。这正是FLASH“只能从1写0不能从0写1”特性的直接体现。要解决这个问题你就需要设计更复杂的数据管理策略例如每次更新都将数据写到新的位置或者定期擦除整个扇区。通过这个完整的实验你不仅学会了调用HAL库函数更重要的是理解了片内FLASH作为数据存储器的特性、限制和工程实践中的应对策略。这远比单纯复制一段代码更有价值。下次当你的项目需要节省那几毛钱的EEPROM时你会更有底气地选择使用片内FLASH。