news 2026/10/26 11:33:49

STM32F103 FLASH读写失败的5大硬核原因与解决方法

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32F103 FLASH读写失败的5大硬核原因与解决方法

简介:本资源是一份面向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从0x08000400(+0x400 = 1024字节),依此类推,Page 127止于0x0801FC00。

这种设计带来两个硬性约束:

  • 擦除不可逆:每页擦除后,所有位变为0xFF(高电平),写入只能将1→0,不能0→1;
  • 写入需对齐:HAL库要求写入地址必须是半字(16-bit)对齐(即地址低1位为0),且每次写入长度为2字节(Half Word)。试图向奇数地址写入或写入单字节,硬件直接忽略,FLASH->SR的PGERR(Programming 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寄存器的PG(Program)或PER(Page Erase)位,否则在下一条指令执行前,LOCK位可能被硬件自动重置(尤其在中断打断时)。这就是为什么裸写FLASH->KEYR后直接调用HAL_FLASH_Program()仍可能失败——中间插入了其他代码,触发了自动锁死。

更隐蔽的是:FLASH->SR(Status Register)的BSY(Busy)位在解锁后仍可能为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->AR(Address Register)读取地址,并自动截取低1位。若addr为奇数(如0x08001235),硬件实际使用0x08001234,导致数据写入错误位置。更严重的是:当addr低1位为1时,FLASH->SR的WRPRTERR(Write 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控制器的编程状态机。编译器生成的STRH(Store Halfword)指令会经由AHB总线送达FLASH接口,硬件据此启动高压编程脉冲。

3.2FLASH_WaitForLastOperation():超时机制与错误分类的真相

该函数轮询FLASH->SR,但只检查三个标志位:EOP(End of Operation)、PGERR(Programming Error)、WRPRTERR(Write 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可能只有BSY=1,其余位全0 }

这意味着:HAL_TIMEOUT返回 ≠ 编程失败,而是“等待响应超时”。常见于VDD低于2.7V、晶振不稳、或FLASH处于异常状态(如页被写保护但未解除)。

3.3 写保护(WRP)机制:如何用FLASH_OBR和FLASH_WRPR寄存器锁定特定页

F103的写保护是页级的,通过FLASH_WRPR(Write Protection Register)的bit位控制。每个bit对应一页:bit0控制Page 0,bit1控制Page 1,... bit127控制Page 127。WRPRTERR标志被置位,说明你试图写入的页对应的WRPR bit为0(即受保护)。

解除写保护需两步:

  1. 解锁FLASH->OPTKEYR(非FLASH->KEYR!):
    FLASH->OPTKEYR = FLASH_OPTKEY1; // 0x08192A3B FLASH->OPTKEYR = FLASH_OPTKEY2; // 0x4C5D6E7F
  2. 修改FLASH_WRPR:将目标页对应bit写为1(1=不保护,0=保护);
  3. 触发选项字节加载:设置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),而实际擦除需20ms(1KB页),则函数提前返回,FLASH->SR的EOP尚未置位,数据未真正擦除。
解决:增大FLASH_TIMEOUT_VALUE(建议≥50000),并在擦除后手动验证:

uint16_t *page_ptr = (uint16_t*)page_address; for(int i=0; i<512; 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),或显式复制到RAM:

uint16_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的RDP(Read Out Protection)位。F103的RDP Level 1(0xAA)启用后,调试接口被禁用,需通过特定方式(如Bootloader串口)解除。
解决:

  • 开发阶段禁用RDP(RDP = 0xFF);
  • 若已锁死,使用STM32 ST-LINK Utility的“Unlock”功能(需芯片处于系统存储器启动模式);
  • 生产固件中RDP应设为Level 1,并确保有可靠的固件升级通道。

5. “模拟EEPROM”方案的设计取舍:为什么不用外部I2C EEPROM,而要在FLASH上造轮子?

5.1 成本与可靠性权衡:省掉一颗芯片,换来更复杂的软件逻辑

某图像处理Demo项目曾面临选择:用1片AT24C02(I2C EEPROM,¥0.8)还是复用F103内部FLASH?硬件BOM成本看,省¥0.8很诱人。但深入分析发现,FLASH方案需解决三大难题:

  • 磨损均衡(Wear Leveling):FLASH页擦写寿命约10k次,而EEPROM达1M次。若参数每天更新10次,10k次≈3年,远低于设备生命周期;
  • 掉电保护(Power Loss Protection):擦除页需20ms,期间断电会导致页损坏。EEPROM写入仅5ms,且多数支持页写模式;
  • 原子性(Atomicity):保存一组关联参数(如PID系数Kp/Ki/Kd)需保证全部写入或全部失败。FLASH页擦除是原子操作,但编程是半字级,需设计状态标记(如VALID/INVALID flag)。

最终方案采用双页轮换(Two-Page Swap):

  • Page A 和 Page B 交替使用;
  • 每次写入前,先擦除“备用页”,写入新数据+状态头(含CRC、版本号、VALID标志);
  • 全部写入成功后,将“当前页”的状态头改为INVALID;
  • 上电时扫描两页,选VALID且CRC正确的页为有效数据。

此方案牺牲了1KB空间(两页共2KB),但避免了单页频繁擦写导致的早期失效。

5.2 性能瓶颈实测:擦除 vs 编程的时间开销对比

在F103@72MHz下,实测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内放置中断向量表或关键函数)。

我们最终选择后者:将中断向量表重映射到SRAM(SYSCFG->MEMRMP = SYSCFG_MEMRMP_FB_MODE),并将所有FLASH操作封装为独立的SRAM函数。虽然增加200字节SRAM占用,但彻底规避了硬件陷阱。

从那以后我每次设计FLASH存储方案,都强制走一遍“擦除-写入-验证-掉电测试”全流程,哪怕只是存一个版本号。因为硬件不会说谎,它只会在量产前夜,用最沉默的方式告诉你:那个没加while(BSY)的HAL_FLASH_Unlock(),才是真正的黑匣子。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/26 11:33:28

TurboQuant算法解析:8bit无损量化与AI模型加速

1. TurboQuant算法技术解析&#xff1a;当Google遇上DeepSeek 上周在GitHub Trending上看到多个与DeepSeek相关的项目突然爆火&#xff0c;仔细研究后发现这背后是Google最新发布的TurboQuant算法引发了技术圈地震。这个号称能同时实现bit无损、计算加速、模型压缩三大特性的技…

作者头像 李华
网站建设 2026/10/26 11:32:20

VC6 MFC读写Mifare IC卡实战:封闭环境下的射频通信方案

简介&#xff1a;这是一套基于VC开发的非接触式射频IC卡读写程序源码&#xff0c;面向嵌入式系统初学者、智能卡应用开发者及高校电子/自动化专业学生&#xff0c;解决RFID卡片底层通信与数据交互的实际开发需求。资源包含64个文件&#xff0c;涵盖10个头文件&#xff08;如dci…

作者头像 李华
网站建设 2026/10/26 11:32:17

图形化Git工具提升开发效率:从入门到实战

1. 为什么开发者需要图形化Git工具&#xff1f; 作为一个在代码版本控制领域摸爬滚打多年的老手&#xff0c;我清楚地记得第一次接触Git命令行时的茫然无措。 git rebase -i HEAD~3 这样的命令对新手来说简直就是天书&#xff0c;更别提解决合并冲突时的 <<<<&…

作者头像 李华
网站建设 2026/10/26 11:32:14

端到端测试实战:从CI摆设到质量信任锚点

1. 项目概述&#xff1a;当“e2e”不再只是缩写&#xff0c;而是一套可落地的验证逻辑最近在多个技术团队的日常沟通里&#xff0c;“e2e”这个词出现频率陡然升高——不是作为某个工具名或平台代号&#xff0c;而是作为一种明确的行为动词和质量承诺。比如&#xff1a;“这个功…

作者头像 李华
网站建设 2026/10/26 11:32:09

无标题博文创作:从零散素材到优质内容的方法论

1. 项目概述作为一名从业多年的内容创作者&#xff0c;我经常遇到一个看似简单却困扰许多同行的难题——如何在没有明确标题的情况下&#xff0c;依然能够创作出结构完整、内容充实的优质博文。这种情况在实际工作中并不少见&#xff0c;可能是临时接到的紧急任务&#xff0c;或…

作者头像 李华
网站建设 2026/10/26 11:31:54

数据可视化核心原则与最佳实践指南

1. 数据可视化的本质与价值 数据可视化早已超越了简单的图表展示阶段&#xff0c;成为现代商业决策和科学研究的核心工具。记得我第一次接触数据可视化是在2012年参与一个零售业销售分析项目&#xff0c;当时用Excel制作的柱状图和折线图就让客户惊叹不已。十年后的今天&#x…

作者头像 李华