
简介本资源是一份面向STM32F103初学者与嵌入式开发者的内部Flash读写实战工程聚焦非易失性数据存储核心需求解决程序运行中配置参数保存、断电记忆等典型应用难题。压缩包共156个文件以70个.h头文件和60个.c源文件为主体涵盖Flash驱动stm32f10x_flash.c、定时器与LCD显示支持、启动文件.ia、Keil工程配置uvprojx/uvoptx及批量编译脚本.bat结构完整便于理解底层操作逻辑与工程组织方式整体大小仅544KB轻量易导入。已有4153人学习下载资源亮点在于完整实现基于串口指令的Flash页擦除/写入/读取交互流程含EEPROM模拟逻辑与安全保护机制说明配套调试配置dbgconf和可直接烧录的hex文件大幅降低实操门槛是掌握STM32 Flash底层操作不可多得的入门级参考工程。1. STM32F103内部FLASH读写为什么你改了参数却死在HAL_FLASH_Program()里而HAL_FLASH_Unlock()没报错你刚把一个校准系数存进STM32F103的FLASH第64页烧录后运行一切正常可第二次上电系统在HAL_FLASH_Program()调用处卡死调试器显示PC停在while(FLASH-SR FLASH_SR_BSY)——FLASH状态寄存器BSY位永远为1。更诡异的是HAL_FLASH_Unlock()返回HAL_OK仿佛一切顺利。这不是玄学是FLASH擦除/编程时序与页保护状态被忽略的必然结果。STM32F103的内部FLASH不是EEPROM它不支持字节级写入也不允许在未擦除的区域覆盖数据一次写操作失败可能让整页进入不可恢复的“半擦除”状态后续所有操作都会被硬件锁死。搞懂它底层的解锁序列、页擦除粒度、写入对齐约束和状态机反馈机制不是为了炫技而是避免你在量产前夜发现固件无法升级、参数无法保存——那种翻车现场连后悔药都找不到源码入口。2. FLASH硬件架构与访问约束为什么必须先擦除才能写且不能跨页写入2.1 物理结构决定行为边界页Page与扇区Sector的本质区别STM32F103系列以中容量128KB FLASH为例的内部FLASH并非线性连续的“大硬盘”而是由128个1KB页Page组成的阵列。注意这里没有“扇区Sector”概念——F103的FLASH组织单位就是Page每页独立可擦除最小擦除单位即1KB。这与F4/F7系列按扇区如16KB/64KB划分有本质不同。页地址固定映射Page 0从0x08000000开始Page 1从0x080004000x400 1024字节依此类推Page 127止于0x0801FC00。这种设计带来两个硬性约束擦除不可逆每页擦除后所有位变为0xFF高电平写入只能将1→0不能0→1写入需对齐HAL库要求写入地址必须是半字16-bit对齐即地址低1位为0且每次写入长度为2字节Half Word。试图向奇数地址写入或写入单字节硬件直接忽略FLASH-SR的PGERRProgramming Error位会被置1但HAL默认不检查该标志。提示F103的FLASH控制器不提供“字节写入”模式。所谓“模拟EEPROM”方案本质是用多页轮换状态标记实现逻辑上的字节更新物理层仍是页擦除半字写入。2.2 解锁序列三步走缺一不可的硬件握手协议FLASH控制器默认锁定防止误操作。解锁不是简单写一个寄存器而是一套严格时序的密钥序列// HAL_FLASH_Unlock() 底层展开基于STM32F103xx HAL库 v1.8.5 FLASH-KEYR FLASH_KEY1; // 0x45670123 FLASH-KEYR FLASH_KEY2; // 0xCDEF89AB // 此时 FLASH-CR 的 LOCK 位清零但仅表示“可配置”不代表可编程关键点在于解锁后必须立即配置FLASH_CR寄存器的PGProgram或PERPage Erase位否则在下一条指令执行前LOCK位可能被硬件自动重置尤其在中断打断时。这就是为什么裸写FLASH-KEYR后直接调用HAL_FLASH_Program()仍可能失败——中间插入了其他代码触发了自动锁死。更隐蔽的是FLASH-SRStatus Register的BSYBusy位在解锁后仍可能为1例如前次操作未完成此时写FLASH_CR会被忽略。正确流程必须是// 安全解锁等待就绪的典型模式 HAL_FLASH_Unlock(); while (FLASH-SR FLASH_SR_BSY) {} // 等待前次操作结束 // 此刻才可安全配置 FLASH_CR FLASH-CR | FLASH_CR_PER; // 准备擦除 FLASH-AR page_address; // 设置页地址 FLASH-CR | FLASH_CR_STRT; // 启动擦除 while (FLASH-SR FLASH_SR_BSY) {} // 等待擦除完成 if (FLASH-SR FLASH_SR_EOP) { // 检查擦除完成标志 FLASH-SR | FLASH_SR_EOP; // 清除EOP标志写1清零 }2.3 写入对齐与数据宽度为什么HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, addr, data)中addr必须是偶数F103的FLASH编程电路设计为半字16-bit总线宽度。其FLASH-CR寄存器的PG位启用后硬件会从FLASH-ARAddress Register读取地址并自动截取低1位。若addr为奇数如0x08001235硬件实际使用0x08001234导致数据写入错误位置。更严重的是当addr低1位为1时FLASH-SR的WRPRTERRWrite Protection Error位可能被意外置位因为地址解码异常触发了写保护检测逻辑。验证方法在调试器中观察FLASH-AR值对比你传入的addr。你会发现奇数地址被强制向下对齐。// 错误示范addr为奇数 uint32_t addr 0x08001235; HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, addr, 0xABCD); // 实际写入地址0x08001234且可能触发 WRPRTERR // 正确做法强制对齐 uint32_t aligned_addr addr ~0x1; // 清除最低位 HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, aligned_addr, 0xABCD);HAL库本身不校验地址对齐它信任开发者。这也是为什么HAL_FLASH_Program()返回HAL_OK但数据根本没写进去——硬件静默丢弃了非法请求。3. HAL库封装下的真实状态机HAL_FLASH_Program()背后隐藏的5个关键检查点3.1 HAL层状态流转从API调用到寄存器操作的完整链路HAL_FLASH_Program()表面是一个函数实则是三层状态机嵌套应用层检查参数合法性地址范围、对齐、数据有效性驱动层配置FLASH_CR、写FLASH_AR、轮询FLASH_SR硬件层FLASH控制器执行编程时序约20μs/半字并更新状态寄存器。其核心逻辑可简化为HAL_StatusTypeDef HAL_FLASH_Program(uint32_t TypeProgram, uint32_t Address, uint64_t Data) { HAL_StatusTypeDef status HAL_ERROR; uint32_t address Address; // Step 1: 地址范围检查F103为0x08000000~0x0801FFFF if(address FLASH_BASE || address (FLASH_BASE FLASH_SIZE - 1)) { return HAL_ERROR; } // Step 2: 对齐检查HAL库只检查半字对齐不检查页边界 if((address 0x1) ! 0) { // 奇数地址 return HAL_ERROR; // 注意此处会直接返回错误 } // Step 3: 等待FLASH空闲 if(FLASH_WaitForLastOperation(FLASH_TIMEOUT_VALUE) ! HAL_OK) { return HAL_ERROR; } // Step 4: 配置编程模式并写入 __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR); FLASH-CR | FLASH_CR_PG; // 使能编程 *(__IO uint16_t*)address (uint16_t)Data; // 关键通过指针解引用触发写入 // Step 5: 等待编程完成 if(FLASH_WaitForLastOperation(FLASH_TIMEOUT_VALUE) ! HAL_OK) { status HAL_ERROR; } else { status HAL_OK; } FLASH-CR ~FLASH_CR_PG; // 关闭编程 return status; }注意*(__IO uint16_t*)address (uint16_t)Data;这行是精髓。它不是普通内存写而是触发FLASH控制器的编程状态机。编译器生成的STRHStore Halfword指令会经由AHB总线送达FLASH接口硬件据此启动高压编程脉冲。3.2FLASH_WaitForLastOperation()超时机制与错误分类的真相该函数轮询FLASH-SR但只检查三个标志位EOPEnd of Operation、PGERRProgramming Error、WRPRTERRWrite Protection Error。它忽略BSY位超时的底层原因——比如电压不稳导致编程脉冲失败此时BSY可能永远为1而PGERR未置位。其超时判断逻辑为// FLASH_WaitForLastOperation() 超时循环核心 timeout FLASH_TIMEOUT_VALUE; // 默认为10000 do { if(__HAL_FLASH_GET_FLAG(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR)) { break; } timeout--; } while(timeout ! 0); if(timeout 0) { return HAL_TIMEOUT; // 此时FLASH-SR可能只有BSY1其余位全0 }这意味着HAL_TIMEOUT返回 ≠ 编程失败而是“等待响应超时”。常见于VDD低于2.7V、晶振不稳、或FLASH处于异常状态如页被写保护但未解除。3.3 写保护WRP机制如何用FLASH_OBR和FLASH_WRPR寄存器锁定特定页F103的写保护是页级的通过FLASH_WRPRWrite Protection Register的bit位控制。每个bit对应一页bit0控制Page 0bit1控制Page 1... bit127控制Page 127。WRPRTERR标志被置位说明你试图写入的页对应的WRPR bit为0即受保护。解除写保护需两步解锁FLASH-OPTKEYR非FLASH-KEYRFLASH-OPTKEYR FLASH_OPTKEY1; // 0x08192A3B FLASH-OPTKEYR FLASH_OPTKEY2; // 0x4C5D6E7F修改FLASH_WRPR将目标页对应bit写为11不保护0保护触发选项字节加载设置FLASH-CR的OPTSTRT位并等待FLASH-SR的BSY清零。注意修改选项字节包括WRP会擦除整个选项字节区含RDP、USER、DATA等且需要重新加载。若RDP等级为Level 1此操作可能导致芯片锁死。生产环境务必慎用。4. 常见问题排查5个血泪经验总结的“必踩坑”清单4.1 现象HAL_FLASH_Erase()返回HAL_OK但擦除后读取仍是旧数据原因未等待FLASH_SR_EOP标志或擦除启动后未清除FLASH_CR_PER位。HAL_FLASH_Erase()内部调用FLASH_WaitForLastOperation()但若超时值过小如设为100而实际擦除需20ms1KB页则函数提前返回FLASH-SR的EOP尚未置位数据未真正擦除。解决增大FLASH_TIMEOUT_VALUE建议≥50000并在擦除后手动验证uint16_t *page_ptr (uint16_t*)page_address; for(int i0; i512; i) { // 1KB 512 halfwords if(page_ptr[i] ! 0xFFFF) { // 任意半字非0xFFFF说明未擦除 Error_Handler(); } }4.2 现象首次写入成功第二次写入时HAL_FLASH_Program()卡死在while(FLASH-SR FLASH_SR_BSY)原因未执行擦除操作。FLASH写入只能将1→0若原地址数据为0x0000全0再次写入0x1234时硬件需将某些位从0→1这违反物理规则直接触发PGERR但BSY位卡死。解决任何写入前必须确保目标地址所在页已擦除。即使只改一个参数也要先擦除整页再重新写入全部数据含未修改字段。4.3 现象调试器读取FLASH地址显示正确但复位后数据丢失原因写入地址超出FLASH物理范围或使用了__ram段变量作为写入源。F103的FLASH编程要求数据源必须在SRAM或寄存器中。若Data参数指向FLASH中的常量如const uint16_t val 0x1234;*(__IO uint16_t*)address val;会尝试从FLASH读取val而FLASH在编程时不可读导致总线错误BusFault。解决确保Data是栈/全局变量位于SRAM或显式复制到RAMuint16_t ram_data 0x1234; HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, addr, ram_data);4.4 现象HAL_FLASH_Unlock()后立即调用HAL_FLASH_Program()返回HAL_ERROR原因HAL_FLASH_Unlock()内部未检查FLASH-SR的BSY位。若前次操作如擦除未完成FLASH-CR的PG位写入会被忽略HAL_FLASH_Program()的地址检查通过但硬件无响应。解决解锁后强制等待空闲HAL_FLASH_Unlock(); while(__HAL_FLASH_GET_FLAG(FLASH_FLAG_BSY)) {} // 显式等待 HAL_FLASH_Program(...);4.5 现象使用HAL_FLASHEx_OBProgram()修改选项字节后芯片无法连接调试器原因错误设置了FLASH_OBR的RDPRead Out Protection位。F103的RDP Level 10xAA启用后调试接口被禁用需通过特定方式如Bootloader串口解除。解决开发阶段禁用RDPRDP 0xFF若已锁死使用STM32 ST-LINK Utility的“Unlock”功能需芯片处于系统存储器启动模式生产固件中RDP应设为Level 1并确保有可靠的固件升级通道。5. “模拟EEPROM”方案的设计取舍为什么不用外部I2C EEPROM而要在FLASH上造轮子5.1 成本与可靠性权衡省掉一颗芯片换来更复杂的软件逻辑某图像处理Demo项目曾面临选择用1片AT24C02I2C EEPROM¥0.8还是复用F103内部FLASH硬件BOM成本看省¥0.8很诱人。但深入分析发现FLASH方案需解决三大难题磨损均衡Wear LevelingFLASH页擦写寿命约10k次而EEPROM达1M次。若参数每天更新10次10k次≈3年远低于设备生命周期掉电保护Power Loss Protection擦除页需20ms期间断电会导致页损坏。EEPROM写入仅5ms且多数支持页写模式原子性Atomicity保存一组关联参数如PID系数Kp/Ki/Kd需保证全部写入或全部失败。FLASH页擦除是原子操作但编程是半字级需设计状态标记如VALID/INVALID flag。最终方案采用双页轮换Two-Page SwapPage A 和 Page B 交替使用每次写入前先擦除“备用页”写入新数据状态头含CRC、版本号、VALID标志全部写入成功后将“当前页”的状态头改为INVALID上电时扫描两页选VALID且CRC正确的页为有效数据。此方案牺牲了1KB空间两页共2KB但避免了单页频繁擦写导致的早期失效。5.2 性能瓶颈实测擦除 vs 编程的时间开销对比在F10372MHz下实测1KB页擦除耗时18~22ms受VDD波动影响而写入512个半字1KB耗时10~12ms。这意味着若采用“擦除-写入”单页模式每次参数更新平均耗时≈30ms双页轮换模式下99%的更新只需12ms直接写入备用页仅当两页均满时才触发擦除概率0.1%。更关键的是擦除时间不可分割而编程可分批进行。因此双页方案将长阻塞30ms转化为短阻塞12ms大幅提升实时性。5.3 一个被忽略的硬件特性FLASH的“预取缓冲区Prefetch Buffer”干扰F103的FLASH控制器包含预取缓冲区用于加速代码执行。但当FLASH正在编程时预取缓冲区可能因地址冲突产生不可预测行为。官方勘误表Errata Sheet明确指出在FLASH编程期间若CPU执行位于同一1KB页内的代码可能导致总线错误。解决方案只有两个将所有FLASH操作函数HAL_FLASH_Program,HAL_FLASH_Erase放入SRAM中执行使用__attribute__((section(.ramfunc)))或确保操作期间CPU不跳转到被操作页的代码对F103而言意味着不要在0x08000000~0x0801FFFF内放置中断向量表或关键函数。我们最终选择后者将中断向量表重映射到SRAMSYSCFG-MEMRMP SYSCFG_MEMRMP_FB_MODE并将所有FLASH操作封装为独立的SRAM函数。虽然增加200字节SRAM占用但彻底规避了硬件陷阱。从那以后我每次设计FLASH存储方案都强制走一遍“擦除-写入-验证-掉电测试”全流程哪怕只是存一个版本号。因为硬件不会说谎它只会在量产前夜用最沉默的方式告诉你那个没加while(BSY)的HAL_FLASH_Unlock()才是真正的黑匣子。希望帮到你。本文还有配套的精品资源点击获取