ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32F103内部Flash读写实战:掉电保存不靠外部EEPROM

STM32F103内部Flash读写实战:掉电保存不靠外部EEPROM 简介STM32F103内部Flash读写例程是一份面向嵌入式开发者和初学者的实用代码包帮助理解基于HAL库的Flash解锁、扇区擦除、编程、验证与锁定完整流程并可延伸到固件升级、参数存储等应用场景。压缩包共72个文件包含29个.h头文件、28个.c源文件、8个.s启动汇编文件以及uvproj工程文件、PDF说明文档等整体体积仅948KB目录按CMSIS、FWlib、User等模块划分便于对照工程学习。目前已有10726人学习浏览这份例程是实践Flash读写时高频参考的例子。开发者可从示例中直接获取可编译的完整工程代码包含擦除前解锁、按扇区写入、读取校验及操作后锁定等关键环节配合datasheet与工程文件可以快速移植到自己项目中满足设备配置掉电保存、运行日志滚动存储等实际需求。 做嵌入式开发和掉电打交道多了你就明白一个道理凡是能让设备记住状态和数据的地方都是金贵资源。很多人一说到“掉电保存”第一反应就是外挂AT24C02或者W25Q64整天跟I2C、SPI的外部存储芯片较劲。实际上STM32F103内部Flash的读写这件事可能被很多人低估了。我今天要聊的这套内部Flash读写例子已经在我的好几个量产项目上跑了好久稳定、省事、不需要额外器件。如果你的需求只是存几个参数、几十个校准值或者运行状态完全不用上外部芯片直接操作F103内部Flash就够了。这篇文章适合正在做掉电保存数据、想做参数存储的开发者原理和可复制的代码我都会给到。1. 为什么我放弃了外部EEPROM改用内部Flash1.1 内部Flash不是“只读空间”很多从51单片机或者Arduino转过来的朋友潜意识里把Flash当成“烧程序用的仓库”只能通过下载器写进去运行的时候只能read。其实STM32这类Cortex-M3内核的单片机设计的时候就允许你在程序里通过寄存器操作来擦写自己的Flash这就是IAPIn-Application Programming的基础。也就是说代码一边跑一边可以把数据写进Flash掉电之后数据还在。从这天开始我就把片内Flash当作一块“非易失RAM”来用了。代码占了低地址区高地址区的空余页就是天然的存储空间不花一分钱BOM成本。1.2 不同容量的Flash页大小差别很大使用内部Flash之前第一件事是搞清楚你手上这颗芯片的Flash总容量和页大小。STM32F103分小容量、中容量、大容量三档小容量16KB~32KB页大小1KB中容量64KB~128KB页大小1KB大容量256KB~512KB页大小2KB我用的最多的是F103C8T6中容量64KB Flash总共64页每页1KB。如果你用的是F103ZET6512KB Flash页大小就变成了2KB。页大小直接影响擦除的颗粒度擦除操作只能按页来不能按字节来。这个差异在选型号时一定要考虑。我曾经在F103C8T6上预留了Flash末尾的4页做数据存储代码写好后切到F103RBT6也是中容量没问题一旦换到大容量芯片页大小变了你按1KB设计的擦除逻辑就需要重新计算地址。1.3 省成本、省面积可靠性还不差拿一个已经量产的温控器来说当时需要保存PID参数、温度校准值和累计运行时间总共不到100字节。用AT24C02的话芯片价格几毛钱还要路过I2C时序和上拉电阻PCB上多两个器件位置。改用内部Flash后这几个参数的存取就变成调用两个函数的事整板BOM少了一颗料生产贴片也少一道工序。有些人担心内部Flash写多了会坏这里说个实话F103官方的擦写寿命典型值是10,000次。如果你产品一天掉电保存10次一年也就3650次用两三年完全没问题。如果遇到频繁上断电的场景比如每隔几分钟就重启一次那就得做轮转写入后面第四部分我会专门说甚至这时候才应该考虑外挂EEPROM。2. 内部Flash读写的底层逻辑2.1 三个动作解锁、擦除、编程在STM32F103标准库里Flash写入看着简单底层是有一套严格顺序的。第一步是解锁。F103的Flash控制寄存器FLASH_CR默认是锁住的不让你直接改必须先往FLASH_KEYR寄存器依次写入两个固定的密钥KEY10x45670123和KEY20xCDEF89AB顺序错了或者少了锁就打不开。标准库里直接调用FLASH_Unlock()就行了。第二步是擦除。Flash只能把1写成0不能把0写成1。所以写入之前必须先执行擦除操作把目标页的所有字节全部恢复成0xFF。标准库函数是FLASH_ErasePage(uint32_t Page_Address)传入页的首地址一次擦一整页。第三步是编程也就是真正往里写数据。F103的Flash编程接口是16位半字Half Word为单位操作的标准库函数FLASH_ProgramHalfWord(uint32_t Address, uint16_t Data)。每次写2字节写完要等硬件忙完再写下一字节。这三步走完最后调用FLASH_Lock()把控制寄存器重新锁上免得程序跑飞的时候误改Flash。2.2 为什么总有“半个字”的坑我见过很多新手直接调用FLASH_ProgramWord()这个函数在标准库v3.5里确实存在能一次写32位数据。但你要知道STM32F103内部Flash物理编程宽度是16位库函数里的ProgramWord其实是帮你拆成两次ProgramHalfWord来做的。如果你边写边做Flash状态等待用ProgramWord不会有问题但如果你的数据没有4字节对齐ProgramWord内部就可能在地址上出错。所以我个人建议宁可自己用ProgramHalfWord按2字节来写也不要图省事。Flash地址必须是偶数地址奇数地址写入会直接报错这是F103芯片的硬件限制。2.3 读取比写入简单得多读Flash不需要解锁不需要擦除直接取地址里的值就行。标准写法是uint32_t data *(volatile uint32_t *)0x0800FC00;如果读字符串也可以直接把这个地址当成const char *指针来用。注意加volatile防止编译器把重复访问优化成一个固定的值。3. 一套可以直接抄的读写实现3.1 地址规划数据放在哪里最安全程序编译出来的代码是从0x08000000开始的。你的Flash存储区不能和代码区重叠否则要么烧写失败要么跑了Flash擦除直接程序崩溃。我一般这样规划先用编译工具的.map文件确认当前程序占用的最大Flash地址。比如一个产测程序编译后占用了0x08000000到0x0800E000那从0x0800F000开始的最后4页就是安全的。为了给后续固件更新留空间我通常从Flash的末尾往前用一页一页做数据区。拿F103C8T6举例Flash从0x08000000到0x0800FFFF共64KB。我预留最后4页做参数存储#define DATA_AREA_BASE 0x0800F000 #define DATA_AREA_PAGES 4 #define DATA_PAGE_SIZE 1024注意如果你的程序用了Bootloader那应用Flash和数据Flash的边界更要仔细计算别让Bootloader升级时把数据区也擦掉。3.2 读写函数实现下面是我在标准库v3.5环境下的通用Flash读写代码。用起来就两个函数往指定地址连续写N个字节从指定地址连续读N个字节。/* flash_rw.h */ #ifndef __FLASH_RW_H #define __FLASH_RW_H #include stm32f10x.h void F103Flash_WriteBytes(uint32_t addr, uint8_t *data, uint16_t len); void F103Flash_ReadBytes(uint32_t addr, uint8_t *buf, uint16_t len); #endif/* flash_rw.c */ #include flash_rw.h static void F103Flash_Unlock(void) { FLASH_Unlock(); /* 清除可能残留的标志位避免误判 */ FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR); } static void F103Flash_Lock(void) { FLASH_Lock(); } void F103Flash_WriteBytes(uint32_t addr, uint8_t *data, uint16_t len) { uint16_t i; uint16_t halfWordCnt (len 1) / 2; /* 向上取整 */ uint32_t pageStart; uint16_t temp; /* 计算目标地址所在页首地址 */ pageStart addr (~(DATA_PAGE_SIZE - 1)); F103Flash_Unlock(); /* 擦除整页 */ FLASH_ErasePage(pageStart); FLASH_WaitForLastOperation(); /* 按16位半字写入 */ for (i 0; i halfWordCnt; i) { if (2 * i 1 len) { temp data[2 * i] | (data[2 * i 1] 8); } else { temp data[2 * i]; /* 最后一个奇数长度高字节填充0xFF */ } FLASH_ProgramHalfWord(addr 2 * i, temp); FLASH_WaitForLastOperation(); } F103Flash_Lock(); } void F103Flash_ReadBytes(uint32_t addr, uint8_t *buf, uint16_t len) { uint16_t i; for (i 0; i len; i) { buf[i] *(volatile uint8_t *)(addr i); } }这段代码我自己用得很顺手几个关键点在注释里写清楚了。我的习惯是“即使数据长度是奇数也直接把Flash读回的空间填满”因为擦除后Flash全部是0xFF读取的时候天然补了0xFF不会导致校验逻辑误判。3.3 掉电保存的实战写法说一个我经常用到的场景保存两个校准系数每个是float附带一个累加校验和。#define SYS_PARAM_ADDR 0x0800FC00 /* 最后一页的尾部区域 */ typedef struct { float calib_kp; float calib_ki; uint8_t valid_flag; /* 固定写0x5A表示数据有效 */ } SysParam; void SaveSysParam(const SysParam *param) { uint8_t buf[16]; uint8_t i; uint32_t checksum 0; /* 结构体转字节流注意每个字段单独拷贝避免结构体对齐问题 */ memcpy(buf[0], param-calib_kp, 4); memcpy(buf[4], param-calib_ki, 4); buf[8] param-valid_flag; /* 用前面的字节算一个简单的32位累加和 */ for (i 0; i 9; i) { checksum buf[i]; } buf[9] (uint8_t)(checksum 0xFF); buf[10] (uint8_t)((checksum 8) 0xFF); /* 统一写成12字节实际只写一页效率高 */ F103Flash_WriteBytes(SYS_PARAM_ADDR, buf, 12); } uint8_t LoadSysParam(SysParam *param) { uint8_t buf[16]; uint8_t i; uint32_t checksum 0; F103Flash_ReadBytes(SYS_PARAM_ADDR, buf, 12); memcpy(param-calib_kp, buf[0], 4); memcpy(param-calib_ki, buf[4], 4); param-valid_flag buf[8]; for (i 0; i 9; i) { checksum buf[i]; } if (buf[9] ! (uint8_t)(checksum 0xFF)) { return 0; /* 校验失败 */ } if (param-valid_flag ! 0x5A) { return 0; /* 数据无效 */ } return 1; }这样的好处是不需要每次都擦除整个页如果你只需要更新单个参数而页里存了好几组数据可以先整个页面都读回一个数组然后在内存里改好再一次性把整页擦除、整页写回。这样能避免频繁擦写同一个页也是Flash存储设计的常规做法。4. 常见问题与避坑经验4.1 写入失败的排查流程如果你发现数据死活写不进去先别怀疑代码按下面顺序查最终写入地址是否对齐到偶数地址F103硬件要求半字地址必须是偶数奇数地址会直接导致FLASH_FLAG_PGERR。是否擦除了目标页很多人只写了ProgramHalfWord忘了擦除结果Flash里残留0和1混合写入就有随机错误。写保护是否打开标准库默认FLASH_Lock()只是写保护寄存器但如果你在程序里调用了FLASH_ReadOutProtection之类的函数整片Flash可能处于读保护状态写入会被拒绝。地址是否超出了当前芯片的Flash范围比如F103C8T6只有64KB你写0x0802F000那已经跑到不存在的地方了硬件根本不会响应函数也不会报错但数据就是写不进去。排查的时候可以在写入后马上把Flash读回来打印到串口和源数据做对比这一步比什么调试器都好用。4.2 中断与耗时问题Flash操作期间别乱跑STM32F103擦除或者写入Flash的时候整个Flash模块会阻塞CPU读取代码区。如果此时中断触发中断服务函数里的代码同样要从Flash取指执行这就相当于死锁CPU在等Flash操作完成Flash又等CPU响应中断完成。实测下来F103擦除一页1KB的耗时大约20ms~40ms写入一个半字大约40us~70us几十字节写下来整个Flash总线要锁住几毫秒到几十毫秒。所以这个阶段一定要提前把中断屏蔽掉等Flash写完再恢复。我的习惯是__disable_irq(); F103Flash_WriteBytes(...); __enable_irq();如果系统里有定时器、串口这类持续中断建议在Flash操作期间挂起相关的处理或者把操作放到主循环的空闲时刻去做不要在按键消抖这种对时序敏感的地方顺手就写Flash。4.3 结构体直接memcpy写入的坑有朋友直接把一个结构体用memcpy到Flash然后发现读出来数据是“花”的。原因多半是结构体字节对齐造成的空洞。比如typedef struct { uint8_t flag; /* 地址0 */ uint32_t value; /* 编译器可能会放到地址4 */ } MyStruct;在默认对齐规则下value会从地址4开始中间空出3个字节。memcpy进Flash之后读回来表面上字节数对但内部布局和你想的完全不一样。如果只是自己用这个结构体倒腾问题不大一旦和上位机协议、版本升级后的结构体版本兼容挂钩就是灾难。我的建议有两个一是用#pragma pack(push, 1)强制结构体1字节对齐但访问效率会低一点二是干脆像我上面的例子一样把结构体字段逐个拷贝到byte数组里自己控制偏移这样最保险。4.4 频繁擦写寿命不足怎么办如果一个设备每天要上电几十次每次都要写Flash那1万次寿命确实有点悬。我的做法是把数据区拆成多个页采用轮转写入比如拆4页写到一个页的末尾后换到下一页继续写同时用“页头”里的序号标记当前最新的一页。启动读取的时候扫描所有页里的序号找到最大的那一页就是最新数据。这套逻辑本质上就是模拟EEPROM的均衡磨损算法网上也有现成的开源方案。如果连这个都嫌麻烦那才应该老老实实外挂AT24C02。顺便说一句W25Q64这类SPI Flash同样有擦写寿命不存在谁完爆谁关键看使用场景。5. 最后再说两句实操心得写内部Flash这件事技术上不复杂但细节确实能绊倒人。我个人在项目里用这套读写的核心心得是先规划好地址写一个简短的单元测试每次烧录后先写完再读回校验把读写逻辑固定下来之后所有产品的参数存储都复用同一份代码极少出问题。如果你项目中需要做掉电保存数据、参数存储、校准值保留这些功能很建议先把这套F103内部Flash读写例子跑通试过你就知道省下来的不仅仅是几毛钱的外部存储芯片成本还有你调试I2C、SPI时序的那几个晚上。本文还有配套的精品资源点击获取
返回列表