STM32片内Flash读写测试实战:从解锁擦除到掉电保存避坑指南
简介这份资源面向嵌入式开发初学者与STM32应用工程师围绕片内Flash的非易失性读写展开帮助解决数据掉电丢失、扇区擦写策略与编程保护等实际问题。压缩包共140个文件约2.96MB以c与h源码为主配合uvprojx、uvoptx工程文件、sct分散加载脚本、hex与axf固件、map映射文件及bat批处理脚本构成一套可直接编译调试的Keil工程。内容涵盖Flash控制器初始化、扇区地址定位、HAL_FLASH_Program编程、状态错误检查与数据读取等关键环节并涉及备份寄存器与RTC掉电保护思路测试程序包含读写循环、异常处理与性能验证。已有1035人学习下载适合希望掌握底层存储操作、提升系统可靠性的开发者参考借鉴。1. STM32片内flash读写测试为什么你的第一个扇区就翻车了很多人第一次做 STM32 片内 flash 读写测试代码烧进去串口打印“写入成功”复位再读数据变了——不是写进去的值是 0xFF 或者干脆是乱码。这不是玄学是 STM32 的 flash 控制器在教你做人。片内 flash 读写测试这件事本质是验证三件事你能不能正确解锁控制器、能不能按扇区/页粒度擦除、能不能在擦除后的区域按正确位宽写入并回读校验。它适合所有需要在 MCU 上做参数存储、OTA 备份、日志记录、掉电保存的嵌入式从业者。你不需要外部 EEPROM也不需要 SPI flash一颗 STM32 自带的 flash 就能干这些活但前提是你得先跑通这个测试。我见过太多项目因为 flash 读写没测透量产时参数丢失返修率直接拉满。这一章先把边界划清楚片内 flash 不是 RAM写之前必须擦擦的单位比写的单位大写的时候位宽和地址对齐有硬性要求任何一条不满足测试就翻车。2. 片内 flash 读写测试的硬件前提与最小工程搭建2.1 先确认你的芯片 flash 页/扇区划分STM32 不同系列的 flash 组织方式差异很大。F1 系列按页擦除小容量页 1KB中容量 2KBF4 系列按扇区擦除扇区大小从 16KB 到 128KB 不等L4 系列又回到 2KB 页。做读写测试前第一件事是翻参考手册的 flash 章节把擦除粒度、写入位宽、地址范围抄下来。我一般会在工程里建一个flash_layout.h把关键参数写成宏后面所有代码都引用这个头文件换芯片只改这一处。/* flash_layout.h - 片内 flash 布局参数按实际芯片修改 */ #ifndef FLASH_LAYOUT_H #define FLASH_LAYOUT_H #include stm32f4xx.h /* 按实际系列替换头文件 */ /* 测试用扇区选最后一个扇区避免覆盖程序区 */ #define TEST_FLASH_SECTOR FLASH_SECTOR_11 #define TEST_FLASH_ADDR 0x080E0000U /* 扇区 11 起始地址 */ #define TEST_FLASH_SIZE 0x00020000U /* 128KB */ /* 写入位宽STM32F4 必须按字节、半字、字写入 */ #define FLASH_WRITE_WORD 1U #endif逻辑说明TEST_FLASH_ADDR必须落在你选定的扇区起始地址上不能随便取一个中间地址否则擦除会连带清掉前面的数据。TEST_FLASH_SIZE用来做边界检查防止写越界。参数怎么改如果你用的是 F103把TEST_FLASH_SECTOR换成FLASH_PAGE_63之类的页号地址按页大小算如果你用的是 L4擦除函数换成FLASH_PageErase页大小 2KB。2.2 解锁、擦除、写入、回读的最小闭环最小测试流程就四步解锁 flash 控制器、擦除目标扇区、写入测试数据、回读比对。每一步都有对应的 HAL 库函数但 HAL 库把很多细节藏起来了测试阶段我建议直接用寄存器操作或者 HAL 底层函数把状态标志看清楚。/* flash_test.c - 片内 flash 读写测试最小闭环 */ #include flash_layout.h #include string.h static uint32_t test_buf[4] {0x12345678, 0xAABBCCDD, 0xDEADBEEF, 0x5A5A5A5A}; static uint32_t read_buf[4] {0}; int flash_test_run(void) { HAL_FLASH_Unlock(); /* 1. 擦除目标扇区 */ FLASH_EraseInitTypeDef erase {0}; uint32_t sector_error 0; erase.TypeErase FLASH_TYPEERASE_SECTORS; erase.Sector TEST_FLASH_SECTOR; erase.NbSectors 1; erase.VoltageRange FLASH_VOLTAGE_RANGE_3; /* 2.7V-3.6V */ if (HAL_FLASHEx_Erase(erase, sector_error) ! HAL_OK) { HAL_FLASH_Lock(); return -1; /* 擦除失败检查 sector_error */ } /* 2. 按字写入 */ for (uint32_t i 0; i 4; i) { if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, TEST_FLASH_ADDR i * 4, test_buf[i]) ! HAL_OK) { HAL_FLASH_Lock(); return -2; /* 写入失败检查 FLASH-SR */ } } /* 3. 回读比对 */ memcpy(read_buf, (void *)TEST_FLASH_ADDR, sizeof(read_buf)); for (uint32_t i 0; i 4; i) { if (read_buf[i] ! test_buf[i]) { HAL_FLASH_Lock(); return -3; /* 数据不一致记录 i 和两个值 */ } } HAL_FLASH_Lock(); return 0; /* 全部通过 */ }逻辑说明擦除前必须解锁擦除后目标区域全变 0xFF。写入时FLASH_TYPEPROGRAM_WORD表示按 32 位字写地址必须 4 字节对齐。回读直接用指针访问 flash 地址空间不需要额外解锁。参数怎么改如果你要按半字写把FLASH_TYPEPROGRAM_WORD换成FLASH_TYPEPROGRAM_HALFWORD地址按 2 字节对齐如果你要写多个扇区把NbSectors改成实际数量但注意Sector要按连续扇区号填。失败时看什么擦除失败查sector_error和FLASH-SR的OPERR、WRPERR位写入失败查FLASH-SR的PGERR、WRPERR位回读不一致先确认擦除是否真的成功再确认写入地址有没有算错。2.3 用串口打印把测试结果变成可观测数据测试代码跑在 MCU 里你看不到变量必须把关键状态打出来。我一般会在每个失败分支里把FLASH-SR寄存器的值、失败地址、期望值和实际值都通过串口输出这样一次就能定位问题。/* 在失败分支里加打印printf 重定向到串口 */ printf(erase fail, SR0x%08X, sector_err%u\r\n, FLASH-SR, sector_error); printf(write fail at addr0x%08X, SR0x%08X\r\n, TEST_FLASH_ADDR i * 4, FLASH-SR); printf(read mismatch at idx%u, exp0x%08X, act0x%08X\r\n, i, test_buf[i], read_buf[i]);逻辑说明FLASH-SR是状态寄存器每一位对应一种错误打印出来直接对照参考手册查。参数怎么改如果你没有串口可以用 SWO 或者把错误码存到一个全局变量用调试器看。注意 printf 本身可能占用较大栈空间测试阶段够用量产代码里要删掉。3. 擦除粒度、写入位宽与地址对齐三个参数决定测试成败3.1 擦除粒度选错整个扇区的数据都没了STM32 片内 flash 的擦除粒度远大于写入粒度。F4 一个扇区 128KB你只想改 4 个字节也得把整个 128KB 擦掉。这意味着如果你的参数区和程序区在同一个扇区擦参数就会把程序擦没。我一般会把参数区放在最后一个扇区并且在链接脚本里把程序区限制在前面留出最后一个扇区专门做数据存储。测试阶段先确认TEST_FLASH_ADDR和TEST_FLASH_SIZE不会碰到任何代码段。/* 检查测试地址是否与程序区重叠编译期断言 */ _Static_assert(TEST_FLASH_ADDR 0x08000000U APP_SIZE, test flash addr overlaps application);逻辑说明APP_SIZE是你程序实际占用的 flash 大小可以从 map 文件里读出来。参数怎么改如果你不确定程序占多大先编译一次看 map 文件最后的地址然后把测试地址放到它后面。注意有些芯片的扇区不是从低地址到高地址均匀分布的F4 的扇区 0 到 3 是 16KB扇区 4 是 64KB扇区 5 到 11 是 128KB选扇区时一定要看表。3.2 写入位宽不匹配写进去的数据高位全是 0STM32F4 的 flash 编程只支持字节、半字、字三种位宽而且地址必须按位宽对齐。你用字写地址必须是 4 的倍数你用半字写地址必须是 2 的倍数。如果地址不对齐HAL 库会返回错误但有些老系列的库函数不检查直接写进去结果高位被截断。我见过一个案例开发者用uint8_t指针往 flash 写地址是 0x080E0001写进去的数据只有低 8 位有效高 24 位全是 0回读比对时才发现。/* 按字写入时地址必须 4 字节对齐 */ if ((TEST_FLASH_ADDR i * 4) % 4 ! 0) { return -4; /* 地址不对齐直接报错 */ }逻辑说明对齐检查放在写入之前比写完再查更省事。参数怎么改如果你要写非对齐数据先把数据拼成 32 位再写或者改用字节写入模式但字节写入速度慢而且不是所有系列都支持。注意 F1 系列只支持半字写入F4 支持字节、半字、字L4 支持双字写入具体看参考手册。3.3 擦除后的 0xFF 不是随便写的回读校验要按位宽比擦除后 flash 里的值是 0xFF这是硬件特性不是软件设置的。回读校验时如果你按字节比每个字节都是 0xFF如果你按字比就是 0xFFFFFFFF。写入数据后只有被写的位会从 1 变 0没写的位保持 1。所以如果你写 0x12345678回读时高 4 位和低 4 位可能还是 0xF因为擦除后是 0xFF写入只改了部分位。这不是错误是 flash 的物理特性。校验时一定要按你写入的位宽和长度比不要多比也不要少比。/* 回读校验按写入长度逐字比较 */ for (uint32_t i 0; i 4; i) { uint32_t addr TEST_FLASH_ADDR i * 4; uint32_t val *(volatile uint32_t *)addr; if (val ! test_buf[i]) { printf(mismatch at 0x%08X: exp0x%08X act0x%08X\r\n, addr, test_buf[i], val); return -3; } }逻辑说明volatile防止编译器优化掉回读操作。参数怎么改如果你写入的是半字回读时用*(volatile uint16_t *)addr长度按半字算。注意有些编译器会对 flash 地址空间的访问做优化加volatile是必须的。4. 片内 flash 读写测试的避坑与排查记录4.1 现象擦除成功但写入返回 HAL_ERROR原因flash 控制器在擦除后没有完全回到就绪状态或者电压范围设置不对。F4 的VoltageRange如果设成FLASH_VOLTAGE_RANGE_11.8V-2.1V而实际供电是 3.3V写入会失败。解决把VoltageRange改成FLASH_VOLTAGE_RANGE_3并在擦除后加一个短延时或者轮询FLASH-SR的BSY位确认控制器空闲再写。4.2 现象回读数据正确但复位后数据变了原因写完之后没有锁定 flash或者程序里其他地方又擦了这个扇区。有些 RTOS 的启动代码会初始化整个 flash 区域把你的测试数据覆盖了。解决写完立即HAL_FLASH_Lock()并检查链接脚本和启动文件确认没有其他代码操作测试扇区。如果用了 bootloader确认 bootloader 不会擦除 app 区后面的扇区。4.3 现象写入速度极慢一次写 4 个字要几百毫秒原因每次写入都在等 flash 控制器完成而 flash 写入本身需要几十微秒擦除一个 128KB 扇区需要几百毫秒到一秒。这是正常物理特性不是代码问题。解决测试阶段可以接受量产时如果要频繁写改用双 bank 交替写或者加外部 flash。注意不要在中断里做 flash 写入擦除期间 CPU 取指会停顿中断响应会超时。4.4 现象读出来的数据和写入的完全不一样但地址没错原因flash 地址映射错了。STM32 的 flash 在 0x08000000 开始映射但有些系列支持地址重映射或者你用了 MPU 把 flash 区域设成了不可缓存但实际访问到了别名地址。解决确认你访问的是 0x08000000 段不是 0x00000000 段。用调试器直接看内存窗口输入地址看数据和代码里读出来的对比。4.5 现象擦除时程序跑飞看门狗复位原因擦除操作会阻塞 CPU 取指如果程序存在 flash 里擦除期间 CPU 无法取指只能靠指令预取缓冲撑一小段时间。如果擦除时间超过预取缓冲CPU 就会取到无效指令。解决把擦除和写入函数放到 RAM 里执行或者确保擦除期间不取指。常见做法是用__attribute__((section(.ramfunc)))把 flash 操作函数放到 RAM。/* 把 flash 操作函数放到 RAM 执行 */ __attribute__((section(.ramfunc))) void flash_erase_sector(uint32_t sector) { /* 擦除代码执行期间不访问 flash */ }逻辑说明.ramfunc段需要在链接脚本里映射到 RAM启动时从 flash 拷贝到 RAM。参数怎么改如果你用的 IDE 自动处理 ramfunc直接加属性即可如果没有手动在链接脚本里加一段。注意 RAM 执行代码会增加 RAM 占用测试阶段够用。5. 进阶把读写测试变成掉电保存的可靠参数区跑通最小闭环之后下一步是把测试代码变成可用的参数存储方案。我一般会做一个简单的双备份参数区两个扇区交替写每个扇区头部加魔数和 CRC上电时读两个扇区选 CRC 正确的那个写入时擦另一个扇区再写。这样即使写入过程中掉电至少有一个扇区的数据是完整的。/* 参数区头部结构 */ typedef struct { uint32_t magic; /* 0x50415241 PARA */ uint32_t version; uint32_t crc; /* 覆盖 data 区域 */ uint32_t data[16]; } param_block_t; /* 上电读取选 CRC 正确的块 */ int param_load(param_block_t *out) { param_block_t *blk_a (param_block_t *)PARAM_A_ADDR; param_block_t *blk_b (param_block_t *)PARAM_B_ADDR; if (blk_a-magic 0x50415241 crc_check(blk_a)) { memcpy(out, blk_a, sizeof(param_block_t)); return 0; } if (blk_b-magic 0x50415241 crc_check(blk_b)) { memcpy(out, blk_b, sizeof(param_block_t)); return 0; } return -1; /* 两个块都无效用默认参数 */ }逻辑说明magic用来判断扇区是否被写过crc用来判断数据是否完整。写入时先擦目标扇区写新数据再更新另一个扇区的备份。参数怎么改data数组大小按你的参数数量调整注意不要超过扇区大小。CRC 可以用硬件 CRC 或者软件查表测试阶段用软件 CRC32 就够。验证方法写完参数后手动断电再上电看串口打印的参数值是否和写入一致。反复擦写 100 次确认没有扇区损坏。我一般会写一个循环测试每次写入递增的计数器跑一晚上第二天看计数器是否连续。这个习惯帮我提前发现过好几次 flash 写入时序问题。希望帮到你。本文还有配套的精品资源点击获取