简介:这份资源面向嵌入式开发初学者与STM32工程师,聚焦片内Flash非易失性数据存储这一核心技能,帮助解决掉电数据丢失、扇区擦写策略与编程保护等实际问题。压缩包共140个文件,约2.96MB,以34个c源文件与35个h头文件为主体,涵盖Flash、TIM、RCC、ADC、I2C、CAN等外设驱动,并附带uvprojx、uvoptx工程文件、sct分散加载脚本、hex与axf可执行文件及map、lst等编译产物,构成一套可直接编译运行的Keil工程。资源围绕Flash初始化、地址定位、编程操作、错误检查与数据读取等环节展开,包含读写循环、异常处理与性能测试示例,并涉及备份寄存器、RTC区域等掉电保护思路。目前已有1035人学习下载,适合希望掌握底层存储操作、提升系统可靠性的开发者参考借鉴。
1. STM32片内flash读写测试:为什么你的第一个扇区总是擦不掉
很多人第一次做STM32片内flash读写测试,代码烧进去,串口打印“写入成功”,再读出来一看,数据全对,于是觉得这事成了。但换一块板子,或者把测试地址从0x08010000挪到0x08000000附近,程序直接跑飞,调试器连不上,只能靠BOOT引脚救砖。这不是玄学,是片内flash的物理特性在教你做人。
STM32片内flash读写测试,本质是在没有外部存储芯片的情况下,用MCU自带的非易失存储保存参数、日志或标志位。它适合做小数据量掉电保存,比如校准系数、设备序列号、运行小时数。但片内flash的擦除粒度是扇区,写入前必须先擦,擦除期间CPU取指可能停顿,操作不当还会把正在运行的程序擦掉。这篇笔记按“先搞懂物理限制,再跑通最小读写,最后处理边界和掉电”的顺序展开,中间会给出可直接复现的代码和参数表,也会把我在实际项目里踩过的坑拆开讲。
2. 片内flash的物理边界:扇区、页和写入粒度
2.1 为什么擦除单位是扇区,写入单位却是半字
STM32的片内flash不是EEPROM,不能按字节随意改写。以常见的STM32F1系列为例,flash按页组织,每页1KB或2KB;F4系列按扇区组织,扇区大小从16KB到128KB不等。擦除的最小单位是页或扇区,写入的最小单位是半字(16位)或字(32位),具体取决于型号。这意味着你无法只改一个字节,必须把整个扇区读到RAM,修改目标字节,擦除扇区,再写回去。
这个特性直接决定了片内flash读写测试的代码结构:读可以按字节随机读,写必须按半字或字对齐,擦必须整扇区。很多新手写测试代码时,定义一个uint8_t buf[10],直接调用写函数往奇数地址写,结果硬件报错或者写入值错乱。不是库函数有问题,是硬件要求地址和长度都对齐。
常见做法是先用HAL_FLASH_Unlock()解锁,再配置擦除结构体,调用HAL_FLASHEx_Erase(),然后按半字循环调用HAL_FLASH_Program()。每一步都要检查返回值,不能只看最后一句。
2.2 不同系列的扇区划分差异与选型影响
STM32F1和F4的flash组织方式差别很大,测试代码不能直接照搬。F1的页大小固定,主存储区从0x08000000开始,页地址连续;F4的扇区大小不均匀,前几个扇区小,后面扇区大,擦除一个128KB扇区耗时可能超过1秒。F0和L0系列页更小,但写入粒度可能是双字。做片内flash读写测试前,必须查对应型号的参考手册,确认三件事:扇区起始地址、扇区大小、编程粒度。
我一般会在代码里用一个宏定义表把扇区信息列出来,测试时只操作最后一个扇区,避免动到程序区。比如某型号有8个扇区,程序编译后占用前6个,第7和第8个空闲,那就把测试地址定在第7个扇区起始。这样即使擦除失败,也不会影响程序运行。
注意:不要凭记忆写扇区地址,不同型号的flash容量和扇区划分可能差很多,查手册是最省时间的做法。
2.3 读写测试前必须确认的三个参数
动手写代码前,先填一张参数表。这张表决定了后面所有操作的正确性。
| 参数项 | 获取方式 | 常见值示例 |
|---|---|---|
| flash起始地址 | 参考手册存储映射章节 | 0x08000000 |
| 测试扇区起始地址 | 起始地址 + 扇区偏移 | 0x08040000 |
| 扇区大小 | 参考手册flash章节 | 128KB |
| 编程粒度 | 参考手册编程章节 | 半字(16位) |
| 供电电压范围 | 数据手册 | 2.7V ~ 3.6V |
| 擦除后默认值 | 实测或手册 | 0xFF |
这张表里最容易忽略的是供电电压。片内flash编程时如果电压低于2.7V,写入可能失败或者数据保持时间缩短。用USB转串口供电的板子,在大电流外设启动瞬间电压跌落,flash写入就会随机翻车。血泪经验是:做片内flash读写测试时,尽量用稳压电源或者电池供电,别用电脑USB口直接带电机。
3. 用HAL库跑通最小读写:从解锁到校验的完整流程
3.1 解锁、擦除、写入、上锁的四步骨架
下面这段代码在STM32F4系列上实测通过,测试地址选在最后一个扇区,写入两个半字,再读回比较。代码里保留了必要的错误处理,实际项目可以把错误码打印到串口。
#include "stm32f4xx_hal.h" #define TEST_FLASH_ADDR 0x080E0000U // 最后一个扇区起始地址,按实际型号修改 #define TEST_FLASH_SECTOR FLASH_SECTOR_11 #define TEST_FLASH_VOLTAGE_RANGE FLASH_VOLTAGE_RANGE_3 static uint32_t test_write_data[2] = {0x12345678, 0xABCDEF01}; static uint32_t test_read_data[2] = {0}; int flash_test(void) { HAL_StatusTypeDef status; FLASH_EraseInitTypeDef erase_init; uint32_t sector_error = 0; // 1. 解锁flash status = HAL_FLASH_Unlock(); if (status != HAL_OK) { return -1; // 解锁失败,检查是否被选项字节保护 } // 2. 擦除测试扇区 erase_init.TypeErase = FLASH_TYPEERASE_SECTORS; erase_init.Sector = TEST_FLASH_SECTOR; erase_init.NbSectors = 1; erase_init.VoltageRange = TEST_FLASH_VOLTAGE_RANGE; status = HAL_FLASHEx_Erase(&erase_init, §or_error); if (status != HAL_OK) { HAL_FLASH_Lock(); return -2; // 擦除失败,sector_error里是出错扇区号 } // 3. 按字写入两个32位数据 for (int i = 0; i < 2; i++) { status = HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, TEST_FLASH_ADDR + i * 4, test_write_data[i]); if (status != HAL_OK) { HAL_FLASH_Lock(); return -3; // 写入失败,检查地址对齐和电压 } } // 4. 上锁并读回校验 HAL_FLASH_Lock(); for (int i = 0; i < 2; i++) { test_read_data[i] = *(volatile uint32_t *)(TEST_FLASH_ADDR + i * 4); } if (test_read_data[0] != test_write_data[0] || test_read_data[1] != test_write_data[1]) { return -4; // 校验失败,数据不一致 } return 0; // 测试通过 }这段代码的逻辑很直白:先解锁,再擦除,再写入,最后上锁并读回。关键点在于每一步都检查返回值,而不是一口气跑完再看结果。擦除失败最常见的原因是扇区号写错或者电压范围配置不对;写入失败最常见的原因是地址没有按编程粒度对齐。
参数说明:TEST_FLASH_ADDR必须是你选定的空闲扇区起始地址,不能落在程序代码区。FLASH_VOLTAGE_RANGE_3对应2.7V到3.6V供电,如果你的板子是1.8V供电,需要改成对应的范围。FLASH_TYPEPROGRAM_WORD表示按32位字编程,有些型号只支持半字,那就改成FLASH_TYPEPROGRAM_HALFWORD并调整数据长度。
3.2 读回校验时为什么必须用volatile指针
上面代码里读回数据用了*(volatile uint32_t *),这个volatile不是装饰。编译器优化时可能认为这块内存没有被修改过,直接把第一次写入的值当成读取结果,导致校验永远通过。加上volatile告诉编译器每次都从内存地址重新加载,才能真实反映flash里的数据。
另一个容易翻车的地方是读回时地址没有按字对齐。如果你用uint8_t *去读一个32位数据,在某些内核上会触发硬件异常。片内flash读写测试的读操作虽然支持字节访问,但为了和写入粒度一致,建议统一按字或半字读。
3.3 把测试结果打印到串口的最小重定向
测试代码跑完后,需要知道返回值。最省事的方法是把printf重定向到串口,然后在主循环里调用flash_test()并打印结果。
#include <stdio.h> int __io_putchar(int ch) { HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY); return ch; } int main(void) { HAL_Init(); SystemClock_Config(); MX_USART1_UART_Init(); int ret = flash_test(); printf("flash test result: %d\r\n", ret); while (1) { HAL_Delay(1000); } }__io_putchar是ARM GCC工具链里printf的底层输出函数,不同工具链名字可能不同,比如Keil里是fputc。重定向后,串口助手就能看到返回值:0表示通过,负数表示对应步骤失败。如果串口没有输出,先检查波特率和接线,再检查printf是否被优化掉。
提示:有些IDE默认使用微库,需要勾选“Use MicroLIB”才能让
printf正常工作,否则程序会卡在__io_putchar里。
4. 擦除时间、中断响应与掉电保护:三个容易翻车的点
4.1 擦除一个扇区到底要多久,中断会不会丢
片内flash擦除不是瞬间完成的。F1系列擦除一页大约20ms到40ms,F4系列擦除一个128KB扇区可能超过1秒。在这段时间里,如果代码从flash取指,CPU会停顿。更麻烦的是,如果擦除过程中来了中断,中断向量表也在flash里,CPU可能无法及时响应,导致中断丢失或者程序跑飞。
常见做法是把擦除和写入操作放在RAM里执行,或者关掉全局中断再擦除。但关中断会影响实时性,所以更稳妥的方式是:擦除前把中断处理函数和向量表重映射到RAM,或者选择在系统空闲时擦除。我一般会在擦除前调用__disable_irq(),擦完立刻__enable_irq(),同时确保擦除期间没有看门狗喂狗操作。如果看门狗超时时间小于擦除时间,就会复位。
另一个坑是擦除时间随温度变化。低温下擦除时间可能变长,高温下变短。做片内flash读写测试时,如果只在室温下验证,到了现场低温环境可能擦除超时。稳妥的做法是留足余量,或者用状态寄存器轮询代替固定延时。
4.2 掉电保护:写一半断电,数据还能救吗
片内flash读写测试最怕的是写入过程中断电。如果只写了一个半字就掉电,这个半字可能处于不确定状态,读出来既不是旧值也不是新值。对于参数保存场景,这会导致设备启动后读到非法参数。
常见的保护策略是“双备份加校验”:把参数存两份,每份带一个CRC校验值。写入时先擦除备份区,写入新数据,再擦除主区,写入新数据。读取时先读主区,CRC不对再读备份区。这样即使写入过程中掉电,至少有一份数据是完整的。
typedef struct { uint32_t magic; uint32_t param_value; uint32_t crc; } flash_param_t; static uint32_t calc_crc32(uint8_t *data, uint32_t len) { // 简化的CRC32实现,实际项目可以用硬件CRC外设 uint32_t crc = 0xFFFFFFFF; for (uint32_t i = 0; i < len; i++) { crc ^= data[i]; for (int j = 0; j < 8; j++) { crc = (crc >> 1) ^ (0xEDB88320 & -(crc & 1)); } } return ~crc; }这个结构体里magic用于快速判断数据是否有效,crc覆盖前面所有字段。写入时先算好CRC再写,读取时重新算一遍比较。如果CRC不匹配,就认为这份数据无效,切换到备份区。双备份的代价是占用两倍flash空间,但对于小数据量参数来说完全可以接受。
4.3 选项字节和写保护:为什么你的flash突然写不进去了
有时候代码没改,之前能写,后来突然写不进去了。常见原因是选项字节里的写保护被意外使能,或者读保护等级被改过。选项字节是独立于主存储区的一块特殊区域,控制着读写保护和看门狗硬件行为。如果写保护生效,擦除和写入操作会返回错误,但读操作正常。
排查方法是读取选项字节状态,确认WRP和RDP位。如果写保护被使能,需要先解除保护再操作。但解除读保护会触发全片擦除,程序会丢失,所以操作前一定要确认。我一般会在测试代码里加一句打印选项字节状态的语句,方便定位。
注意:不要在主程序里随意修改选项字节,一旦改错可能导致芯片无法再次烧录,只能通过BOOT模式或者专用工具恢复。
5. 避坑与排查:片内flash读写测试的五个血泪记录
5.1 现象:写入返回成功,读出来全是0xFF
原因:擦除后没有等待擦除完成就调用写入函数。HAL库的HAL_FLASHEx_Erase是阻塞式的,但有些底层驱动或者自己写的寄存器操作没有轮询BSY位,导致写入时flash还在忙,数据被丢弃。
解决:擦除后读一次FLASH_SR寄存器的BSY位,确认为0再写入。用HAL库的话,检查HAL_FLASHEx_Erase返回值是否为HAL_OK,不要只看函数有没有卡住。
5.2 现象:程序烧录后第一次运行正常,复位后数据丢了
原因:测试地址选在了程序区,擦除时把程序代码擦掉了。复位后CPU从0x08000000取指,取到的是0xFF,直接硬件异常。
解决:用链接脚本或者编译器生成的map文件确认程序占用的地址范围,测试地址必须落在空闲扇区。可以在代码里加一个编译期断言,如果测试地址小于程序结束地址就报错。
5.3 现象:写入的数据偶尔错一个位
原因:供电电压波动。flash编程时对电压敏感,如果板子上有电机或者无线模块,瞬间电流拉低电压,写入就会出错。
解决:编程期间关闭大功率外设,或者增加电源滤波电容。测试时用示波器看VDD引脚,确认编程瞬间电压不低于2.7V。如果无法保证,改用外部EEPROM或者带掉电检测的flash操作。
5.4 现象:擦除一个扇区耗时超过2秒,看门狗复位
原因:看门狗超时时间设置太短,而擦除大扇区本身就需要较长时间。F4系列擦除128KB扇区在低温下可能超过1.5秒。
解决:擦除前喂一次狗,擦除后立刻再喂一次,或者把看门狗超时时间设为擦除时间的两倍以上。更稳妥的做法是把擦除操作放在启动阶段,此时看门狗还没使能。
5.5 现象:读回校验通过,但重启后数据变了
原因:没有上锁。写入完成后没有调用HAL_FLASH_Lock(),后续程序意外修改了flash控制寄存器,导致数据被篡改。
解决:写入完成后立即上锁,并在上锁后读回校验。如果校验失败,说明上锁前数据已经被改,需要检查是否有其他代码在操作flash。
6. 进阶技巧:用备份寄存器加CRC做掉电安全参数存储
片内flash读写测试跑通后,下一步是把它用到实际项目里。我一般会结合备份寄存器(BKP)和CRC做一个轻量级参数存储方案。备份寄存器在主电源掉电后由VBAT供电保持,容量很小但读写快,适合存一个“写入中”标志。流程是:写flash前先在备份寄存器里置位,写完清除;启动时检查备份寄存器,如果标志还在,说明上次写入过程中掉电,此时读flash数据并校验CRC,不通过就加载默认值。
这个方案的好处是不需要双备份,节省flash空间。代价是备份寄存器需要VBAT引脚接电池或者超级电容,没有VBAT的板子用不了。另一个技巧是把CRC校验值存在flash最后一个字,写入时先写数据再写CRC,读取时先读CRC再校验数据。如果CRC位置是0xFFFFFFFF,说明数据区被擦除过,直接加载默认值。
验证方法很简单:在写入过程中手动断电,重新上电后看设备是否加载默认值而不是乱码。反复测试十次,如果十次都能正确恢复,说明掉电保护逻辑可靠。我自己的习惯是每次修改flash操作代码后,至少做三次掉电测试,确认没有引入新的边界问题。希望帮到你。
本文还有配套的精品资源,点击获取