简介:这份STM32F407固件库示例压缩包聚焦FLASH读写保护的设置与解除,面向嵌入式开发者和安全设计人员,帮助理解选项字节与读写保护等级配置,解决程序被非法读取或篡改的隐患,适用于Keil MDK开发环境,特别适合有基础STM32开发经验、正在考虑固件安全防护的工程师。压缩包整体仅771KB,共129个文件,以固件库中的58个h头文件和49个c源文件为主体,另含12个汇编启动文件、5个txt说明、Keil工程文件及清理脚本,其中uvprojx/sct用于工程管理,bat脚本可一键清理中间文件,可直接导入MDK查看与编译。已有1170人浏览学习,对于正在调试STM32F4系列FLASH保护机制的开发者具有直接参考价值。示例中包含FLASH驱动源码和常用外设模块,既能学习FLASH读保护/写保护使能与解除的完整流程。也可对照选项字节寄存器与校验步骤,快速迁移到实际项目的安全启动与固件防篡改设计中。 做嵌入式这些年,我经常碰到一个尴尬的场景:板子发给客户试用,对方拿个J-Link把固件读出来,没几天市场上就出现了同款产品。反过来,也有客户自己的工程师在调试时误开了Flash写保护,整块板子直接连不上调试器,只能返厂处理。这两类问题本质上都指向同一件事——STM32F407的Flash读写保护该怎么设置、该怎么解除。
在STM32F407固件库(标准外设库)里,读写保护的核心操作集中在选项字节(Option Bytes)的配置上,代码量不大,但一旦操作顺序错误,轻则功能失效,重则芯片直接“锁死”。我早年在这上面栽过跟头,所以这篇文章把设置的完整流程、解除的正确姿势、以及各种“救砖”手段都整理出来,希望对正在做量产固件保护、BootLoader升级、或者调试时不小心把保护开错的朋友有帮助。
1. 为什么要给STM32F407的Flash加读写保护
1.1 读保护(RDP)与写保护(WRP)到底在保护什么
读保护对应的是RDP(Read Protection)选项位,它管的是“别人能不能把Flash里的代码读出来”。F407的读保护分三档:
Level 0表示没有任何保护,调试接口可以直接读Flash内容,这也是芯片出厂时的默认状态。Level 1是最常用的一档,开启后外部调试接口无法读取Flash,但程序内部仍然可以正常访问Flash,换句话说,固件能跑、能读写自己的存储区,但外部工具看不到代码内容。Level 2则是最严格的档位,它会永久禁止调试接口访问,同时程序本身也无法再读Flash,而且Level 2一旦写入,连回退到低等级的可能性都没有,基本等于把芯片变成一次性的。开发调试阶段千万不要碰Level 2,除非你有换芯片的预算。
写保护对应的是WRP(Write Protection)选项位,它管的是“能不能往Flash里写数据和擦除扇区”。F407的Flash主存储区共有12个扇区(Sector 0到Sector 11),写保护可以按扇区单独配置。开启写保护后,硬件会拦截对应扇区的编程和擦除操作,寄存器里会留下写保护违例标志。
这两种保护的侧重点完全不同:读保护防的是“抄代码”,写保护防的是“破坏代码”。实际项目中两者通常是配合使用,形成一个完整的固件安全策略。
1.2 典型应用场景:防抄板、防误擦写、量产保护
先说说读保护最常见的用途。产品固件里往往包含私有通信协议、加密算法、服务器对接地址和密钥信息,这些内容一旦被调试器导出,对手不需要逆向整个程序,只需要在数据段里翻一翻,可能就能拿到关键凭证。所以量产板上,在烧录完成并通过全部产测后,最后一步就是设置RDP Level 1,把代码锁住。
写保护最典型的场景是保护BootLoader。我一般把BootLoader放在Sector 0,应用程序从Sector 2开始放,Sector 1留作参数存储。然后把Sector 0设置成写保护,这样应用固件无论怎么跑飞、地址指针怎么错乱,都不可能把BootLoader区域覆盖掉。哪怕OTA升级失败,设备上电后依然能从BootLoader进入恢复菜单,重新下载固件,不至于变成一块砖头。
还有一种场景是防止误擦写。有些工装板、测试台的程序需要长期稳定运行,不希望操作人员或者上位机软件误触发固件升级指令。把关键扇区写保护之后,即使升级流程出现异常,核心代码也不会被破坏。
1.3 锁死后会付出什么代价
这里必须先说清楚一个很多新手不知道的机制:RDP Level 1往Level 0降级时,芯片硬件会自动执行全片擦除。注意,这不是软件调用了擦除函数,而是硬件层面的强制行为,整个用户Flash区都会被清掉,固件、参数、日志全部消失。
所以“解除读保护”不等于“恢复原样”,它背后是“清空整个Flash”的代价。如果你想保留固件内容,必须在开启保护之前先用调试工具把Flash完整备份成bin文件,解除保护后再重新烧录回去。这个特性在AN4750等应用笔记里写得很明白,但实际踩坑的人还是很多,主要是因为没有预料到“解保护会擦除全片”。
2. 开发环境与工程准备
2.1 标准外设库版本与工具链
这个场景用的是STM32F4xx标准外设库(StdPeriph_Driver),对应的版本是STM32F4xx_DSP_StdPeriph_Lib V1.8.0或V1.8.x。这套库虽然官方已经停止更新了,但胜在函数结构清晰、寄存器操作直观,很多在产的老项目依然在用它。如果你现在用的是HAL库,也不用担心,底层寄存器和操作流程完全一致,只是函数名换了一套,理解了本文的寄存器级逻辑,切到HAL库只是查一下API映射表的事。
IDE方面我用的是Keil MDK 5.x,调试器是ST-Link,备用的辅助工具是STM32CubeProgrammer。STM32CubeProgrammer这个工具非常关键,后面“救砖”环节会专门用到它,建议提前装好并熟悉一下它的界面。
2.2 需要准备的四样东西
动手前把下面这些准备好,能少走不少弯路:
- 一块可用的STM32F407开发板,且SWD引脚(PA13、PA14)已引出。
- ST-Link调试器,推荐正版或正规仿制版本,部分劣质J-Link在对选项字节写入时会出现概率性失败。
- STM32CubeProgrammer,用于验证保护状态和强制解除保护。
- 一份RM0090参考手册,重点是Option Bytes相关的章节,英文版或中文版均可。
2.3 压缩包里的工程结构怎么组织
这个压缩包我实际整理成了标准的Keil工程结构:核心目录包含MDK-ARM(存放工程文件)、User(存放main.c和应用代码)、Libraries(存放标准外设库源码),其中CMSIS设备头文件和启动文件放在Libraries/CMSIS/Device路径下。最关键的是,工程里必须加入stm32f4xx_flash.c和对应的stm32f4xx_flash.h,同时在stm32f4xx_conf.h中打开FLASH模块的include宏开关。很多新手编译时总是报“找不到stm32f4xx_flash.h”,十有八九就是漏掉了这个配置开关。
3. 设置读写保护的代码与关键逻辑
3.1 读保护Level 1设置:三行核心调用
在标准外设库中,设置读保护Level 1的完整函数如下:
void Flash_Set_RDP_Level1(void) { FLASH_Unlock(); // 解锁Flash主控制寄存器 FLASH_OB_Unlock(); // 解锁选项字节区域 FLASH_OB_RDPConfig(OB_RDP_Level_1); // 配置读保护等级为Level 1 FLASH_OB_Launch(); // 加载选项字节并触发系统复位 FLASH_OB_Lock(); // 锁定选项字节区域 FLASH_Lock(); // 锁定Flash主控制寄存器 }FLASH_OB_Launch()是整段代码的重心。它会把当前配置真正写入选项字节区,然后触发一次系统复位,让保护配置立即生效。执行完这行后,开发板会重启,重启之后调试接口就再也读不到Flash内容了。这个函数执行期间不能发生中断,否则选项字节写入时序可能被打断,所以调用前最好加一下中断屏蔽。
RDP的值需要注意:标准库里OB_RDP_Level_0对应0xAA,OB_RDP_Level_1对应0x55,除了这两个值之外的任何数都可能被硬件视为Level 2。因此不要为了“做成其他保护级别”而手动改配置值,很容易把芯片搞成永久锁定。
3.2 写保护设置:扇区配置与ENABLE陷阱
写保护的设置比读保护稍复杂一点,因为涉及扇区选择:
void Flash_Set_WRP(void) { FLASH_Unlock(); FLASH_OB_Unlock(); // 保护Sector 0 ~ Sector 3 FLASH_OB_WriteProtectionConfig( OB_WRP_Sector_0 | OB_WRP_Sector_1 | OB_WRP_Sector_2 | OB_WRP_Sector_3, ENABLE ); FLASH_OB_Launch(); FLASH_OB_Lock(); FLASH_Lock(); }这里有一个非常容易踩的坑:FLASH_OB_WriteProtectionConfig的第二个参数,ENABLE表示使能写保护,DISABLE表示解除写保护。表面看语义很直接,但标准库内部实现里,这个参数最终会被取反后写入选项字节寄存器。如果你在调试时发现“我明明传了ENABLE,为什么选项字节里显示的保护位反而被清除了”,就是因为没有跟踪到内部的取反逻辑。所以调试时别只看函数名,一定要看库函数的具体实现。
另一个细节是写保护的生效时机。FLASH_OB_Launch()之后并不是立刻对所有已配置扇区生效,而是在选项字节加载完成、芯片复位完成之后,被保护扇区才真正进入只读状态。如果你在OB_Launch之后立刻尝试擦除被保护扇区,操作可能仍然会成功,容易造成保护未生效的错觉。正确做法是每次设置完写保护后,等待复位完成,再尝试执行一次擦除来验证。
3.3 为什么OB_Launch之后必须复位
很多初学者会问:FLASH_OB_Launch()和普通的复位有什么区别?它不只是复位,它还会重新加载选项字节的配置值。选项字节是芯片上电时被硬件读取到配置寄存器里的,运行时改寄存器配置并不能让新的保护值生效,必须通过这个函数触发一次“配置重载”过程。这也是为什么我习惯在设置完保护后,用一句while(1)死循环等用户主动断电复位,保证保护状态完整建立起来。
3.4 工程中加入用户交互入口
保护逻辑不要在main()里裸调,否则代码一旦上线,正常逻辑误调用了这个函数,后果非常严重。我自己的习惯是把它封装成一个独立的命令接口,由串口或者上位机触发:
int main(void) { // 初始化UART等外设 uint8_t cmd; while (1) { cmd = uart_get_char(); if (cmd == 'L') { Flash_Set_RDP_Level1(); } else if (cmd == 'W') { Flash_Set_WRP(); } } }通过串口下发指令来触发保护锁定,而不是在开机时自动执行,这样既方便产线操作,也避免了开发阶段“程序上电就把自己锁死”的惨剧。
4. 解除读写保护的代码与恢复流程
4.1 解除读保护:安全降级到Level 0
解除读保护级别1回到Level 0的标准库代码如下:
void Flash_Unset_RDP(void) { FLASH_Unlock(); FLASH_OB_Unlock(); FLASH_OB_RDPConfig(OB_RDP_Level_0); // 配置回Level 0 FLASH_OB_Launch(); FLASH_OB_Lock(); FLASH_Lock(); }看起来和设置代码几乎一样,只是参数从Level 1换成了Level 0,但执行时的副作用是一个巨大的“全片擦除”。我在前面已经强调过,硬件会在OB_Launch时自动擦除整个用户Flash区域。换句话说,执行这个函数后,原来的固件会全部消失。如果你只是想解除保护而保留固件,那是不现实的。
所以实际操作中,解除保护前必须先备份固件。我的做法是用STM32CubeProgrammer的Read功能,把0x08000000开始的Flash内容导出为一个bin文件,然后再执行解保护,最后把bin烧回去。整个过程要保证板子供电稳定,如果中途断电,芯片可能停留在未知状态,后续只能靠外部工具强制恢复。
4.2 解除写保护:注意被保护扇区的擦除问题
解除写保护相对简单,把所有扇区的保护位恢复为未保护状态:
void Flash_Unset_WRP(void) { FLASH_Unlock(); FLASH_OB_Unlock(); // 对所有扇区解除写保护 FLASH_OB_WriteProtectionConfig(OB_WRP_AllSectors, DISABLE); FLASH_OB_Launch(); FLASH_OB_Lock(); FLASH_Lock(); }与解除读保护类似,解除写保护时,F407也要求硬件对曾经被保护的扇区做擦除处理。所以同样存在“解保护=数据丢失”的代价。在实际的BootLoader升级流程里,我通常会把需要升级的应用区设置为不受保护状态,BootLoader区始终保持写保护,这样既保证了升级的灵活性,又不会让核心引导代码暴露在风险中。
4.3 程序无法运行时,用STM32CubeProgrammer强制解除
这是最常遇到的局面:芯片里的固件开启了读保护,现在你想重新下载新固件,但在Keil里一烧录就报“Flash Download failed - Target DLL has been cancelled”,调试器根本连不上目标芯片。这时程序里的解除函数根本跑不起来,怎么办?
用STM32CubeProgrammer可以绕过程序,直接通过调试接口操作选项字节寄存器:
- 用ST-Link连接芯片,软件识别到设备型号。
- 如果芯片处于保护状态,软件会弹窗警告或直接提示“Device protected”。
- 切换到Option Bytes页面,把RDP等级从Level 1改为Level 0。
- 点击Apply,软件会再次提示这次操作会擦除全片Flash,确认即可。
这个流程的本质,是通过调试接口直接改写选项字节配置,不依赖目标固件里的任何代码。因此,即使芯片里的程序已经跑飞或循环死机,只要SWD端口还处于可用状态,就有机会强制解除。
这里要特别强调:很大一部分“无法连接调试器”的板子,并不是保护等级的问题,而是SWD引脚被复用。如果代码在启动后立即把PA13/PA14配置成了普通GPIO,调试器也无法连接。常见的恢复技巧是按住板子上的复位键,在Keil点击下载的瞬间松开复位键,让内核在复位向量处停留,调试器抢在应用程序运行之前建立连接。这个方法对很多“死锁”情况都有效。
5. 完整实操流程与验证方法
5.1 一次完整的保护与解除验证流程
把代码准备好之后,我建议按下面的流程在开发板上完整走一遍,这个过程会帮你建立对选项字节机制的整体认知:
- 用ST-Link连接F407开发板,保证PA13/PA14接线正确。
- 在Keil中编译一个带串口打印的基础工程,烧录后确认板子能正常运行。
- 执行设置读保护函数,等待芯片复位,再用STM32CubeProgrammer尝试连接。此时你会看到软件无法正常读取Flash内容,或者读取结果全是无效数据。这就是保护已生效的直接验证。
- 再次烧录固件,会触发“Flash Download failed - Target DLL has been cancelled”之类的错误,这属于正常现象。
- 用CubeProgrammer将RDP降至Level 0,确认芯片被全片擦除,然后重新烧录备份的固件。
把整个流程走一遍,你对“保护到底锁住了什么、解保护会丢什么”会有一个非常直观的理解,后续遇到类似问题就不会手忙脚乱了。
5.2 三种验证保护是否生效的方法
第一是重新下载法。设置完写保护后,在Keil里重新下载一次工程。如果写保护生效,擦除阶段就会报错,提示Flash算法失败或写入超时。如果读保护生效,连接阶段就会失败,或者在读取内存时出现异常。这个方法最快速,适合日常开发验证。
第二是外部工具读取法。用STM32CubeProgrammer连接芯片后,尝试读取0x08000000地址的数据。读保护开启时,读取结果会变得不可理解,并且软件通常会弹出警告或提示错误。
第三是程序自检法。在设置写保护后,程序里尝试对受保护扇区执行一次FLASH_EraseSector(),然后读取FLASH状态寄存器的写保护违例标志位。如果这个标志位变为1,说明写保护确实阻止了非法擦写。这个方法适合做产线自动测试,通过代码直接判断保护状态。
5.3 量产产线上怎么用这套逻辑
量产环境中,我不建议让工人手动用CubeProgrammer去调选项字节,效率太低且容易出错。我的习惯是:产测上位机通过串口和待测板通信,在烧录完成后自动下发“锁定读保护”指令,然后回读保护状态,确认成功后打标“已锁定”。整个过程记录进生产日志,返修时通过特定工装对板卡解除保护并重新擦写。
这个流程的关键点是:产线上的每一块板子在出厂前必须完成一次完整的读写验证,确认固件功能正常后再锁定保护,否则一旦锁死后才发现问题,返工成本会非常高。
6. 常见问题速查与避坑实录
这一节把我实际项目中踩过的坑整理成速查表,每一条都对应一个真实的调试故事。
| 现象 | 可能原因 | 处理方法 |
|---|---|---|
| Keil下载报“Flash Download failed - Target DLL has been cancelled” | 芯片处于RDP Level 1保护状态 | 用STM32CubeProgrammer将RDP降为Level 0,注意会全片擦除 |
| 下载时报“No target connected” | SWD引脚被复用、芯片进入低功耗模式 | 按住复位键,在下载瞬间松开,尝试连接时复位 |
| 设置写保护后,程序仍能正常写入受保护扇区 | OB_Launch后未实际复位,或WRP配置位未正确写入 | 确保OB_Launch执行后等待完整复位,再重新测试 |
| 读取Flash内容全为0xFF或0x00 | 读保护开启,或芯片刚被全片擦除 | 确认RDP等级,重新烧录备份固件 |
| 设置读保护后,无法连接任何调试工具 | 可能误入RDP Level 2,或SWD被禁用 | Level 2无法恢复,只能更换芯片;SWD被禁用则尝试复位时序 |
| ST-Link连接时提示“Cannot access target” | 供电不稳、复位电路异常、接线不良 | 优先检查电源和SWD接线,排除硬件问题后再排查保护配置 |
除了表格里的问题,还有一些容易被忽略的细节:
第一,FLASH_Unlock()和FLASH_OB_Unlock()的顺序不能乱。先解锁Flash主区,再解锁选项字节区。反过来操作时,FLASH_OB_RDPConfig()会静默失败,不报错、不弹警告,但保护根本没写进去。调试时如果发现保护状态怪异,先检查解锁顺序。
第二,OB_Launch会触发系统复位,此时如果看门狗在工作,要小心复位又被看门狗接管。我遇到过一例,设置了读保护后,芯片不断重启,原因是看门狗在OB_Launch触发的复位过程中又产生了一次复位,导致选项字节加载不完整。解决方法是在操作选项字节之前临时关闭看门狗,或者在OB_Launch失败后重新初始化看门狗。
第三,ST-Link的固件版本会影响选项字节操作的成功率。老版本的ST-Link固件对F407的选项字节写入存在兼容性问题,表现为偶尔写入失败,或者读到的状态与配置不符。遇到这种情况,先用STM32CubeProgrammer升级一下ST-Link固件,很多时候能解决一些看似玄学的问题。
第四,调试时用“仿真但不下载”或“直接运行”的方式,可能会绕过Keil对Flash下载流程的保护检查,导致你误以为读保护没开启。建议关闭不必要的调试优化选项,确保每次运行都是完整下载流程。
7. 说点实际的总结与经验
读写保护配置最稳妥的做法是:固件烧录后,通过产测上位机下发命令触发“保护锁定”,锁定成功后回读确认保护状态,整个流程计入生产记录。不要把这个功能烧死在BootLoader里自动执行,否则后续一旦需要返修、升级,你连恢复通道都没有,只能拆壳飞线用外部工具硬解,既费时间又容易伤板子。
再补充一个小技巧:写保护一定要做好扇区规划,BootLoader占哪几个扇区、应用固件占哪几个扇区、数据存储占哪几个扇区,用一张分区表先画清楚,再决定保护范围。我见过把数据存储区和代码区一起保护的方案,结果每次升级都要先解保护、备份数据、再擦除写入,流程变得极其复杂。保护是为了省事,不是为了给自己添堵。
本文还有配套的精品资源,点击获取