简介:这份资源面向使用STM32F407进行嵌入式开发的工程师与学习者,聚焦FLASH读写保护与解除这一程序安全核心主题。STM32F407基于ARM Cortex-M4内核,其FLASH存储着代码、配置数据与启动代码,读保护可阻止敏感信息被非法读取,写保护则防止代码被篡改或擦除,二者均通过选项字节与特定寄存器序列配置。资源包共129个文件,以58个h头文件与49个c源文件为主体,辅以12个s汇编启动文件、5个txt说明及uvprojx、uvoptx、sct等Keil工程与链接脚本文件,压缩包约771KB,结构完整可直接导入编译。内容涵盖FLASH接口初始化、保护等级配置、状态验证以及解锁与锁定流程,并涉及选项字节的启动地址、BootPin选择等高级用法。目前已有1176人学习下载,适合希望掌握固件安全机制、提升项目可靠性的开发者参考实践。
1. 从一次量产翻车说起:STM32F407 的 FLASH 读写保护到底在防谁
某次小批量试产,两百块板子烧完固件、跑完老化,回读校验时发现其中三块的主程序区被改写了两个字节,功能表现为上电偶发卡死。排查到最后,问题不在电源也不在晶振,而是产线工装误触了调试口,把运行中的参数区当成下载区刷了一遍。这件事之后,我把 STM32F407 固件库里的 FLASH 读写保护重新捡起来,做成了产线必过的工序。读写保护不是玄学,它解决的就是两个很具体的问题:一是防止外部调试器或误操作改写片上 FLASH,二是防止别人通过调试口把固件整片读走。它适合所有用 STM32F407 做量产固件、又不想让代码和标定参数裸奔的团队。这一篇就按固件库的路子,把设置、解除、参数和踩坑一次讲透。
2. 先把 F407 的 FLASH 保护机制拆开:读保护和写保护不是一回事
2.1 写保护作用在扇区,读保护作用在整片
STM32F407 的片上 FLASH 分成若干扇区,写保护(WRP)是按扇区粒度生效的,你可以只保护存放 bootloader 的扇区 0 到 3,而让参数区所在的扇区 11 保持可写。读保护(RDP)则是整片级别的,一旦开启,通过调试口读 FLASH 会返回全 0 或直接报错,同时也会顺带禁止从 RAM 启动调试。很多人把两者混为一谈,结果设了写保护却发现固件还是能被读出来,或者开了读保护导致产线无法回读校验。常见做法是:写保护用来锁住不该被改的代码区,读保护用来挡住整片提取,两者配合使用。
固件库操作 FLASH 的核心是FLASH_OBProgramInitTypeDef这个结构体,它同时承载了读写保护、用户选项字节和 BOR 电平的配置。理解它的字段含义,比死记函数名重要得多。
2.2 选项字节、RDP 等级与解除时的整片擦除
F407 的读保护分等级,通过选项字节里的 RDP 字节表示。未保护时该字节为 0xAA,一旦写入其他值即进入保护状态。这里有个必须提前知道的硬规则:从保护状态解除回 0xAA 时,硬件会自动触发一次整片 FLASH 擦除,包括你辛苦标定的参数区。这不是 bug,是设计上的安全策略,防止有人先读保护再偷偷解除后读取残留数据。所以解除读保护之前,一定要确认参数能重新写入,或者参数根本不放在内部 FLASH。
写保护的解除相对温和,它只改选项字节,不会擦除主存储区。但写保护的生效和解除都需要经过解锁、选项字节编程、重新上电或系统复位这几个步骤,少了任何一步都可能出现“设置了但没生效”的假象。
2.3 用固件库读取当前保护状态的完整代码
在动手改之前,先读清楚当前芯片处于什么状态,这是避免翻车的第一步。下面这段代码用固件库把选项字节和读写保护状态打印出来。
#include "stm32f4xx.h" #include "stm32f4xx_flash.h" #include <stdio.h> /* 读取并打印当前 FLASH 选项字节与保护状态 */ void FLASH_PrintProtectionState(void) { FLASH_OBProgramInitTypeDef ob; /* 获取选项字节当前配置,包含 RDP 等级和 WRP 扇区 */ FLASH_OBGetConfig(&ob); printf("RDP level byte: 0x%02X\r\n", ob.RDPLevel); printf("WRP sectors mask: 0x%08X\r\n", ob.WRPSector); printf("BOR level: 0x%02X\r\n", ob.BORLevel); /* RDPLevel 为 0xAA 表示未开启读保护 */ if (ob.RDPLevel == 0xAA) { printf("Read protection: DISABLED\r\n"); } else { printf("Read protection: ENABLED\r\n"); } /* WRPSector 按位对应扇区,位为 1 表示该扇区被写保护 */ if (ob.WRPSector == OB_WRP_SECTOR_All) { printf("Write protection: ALL sectors\r\n"); } else if (ob.WRPSector == OB_WRP_SECTOR_0) { printf("Write protection: sector 0 only\r\n"); } else { printf("Write protection: mask=0x%08X\r\n", ob.WRPSector); } }逻辑说明:FLASH_OBGetConfig会把当前选项字节解析到结构体里,RDPLevel字段直接反映读保护字节,WRPSector是按位或的扇区掩码。参数说明:OB_WRP_SECTOR_0到OB_WRP_SECTOR_11是固件库定义的扇区宏,F407 的 1MB 版本共 12 个扇区,前 4 个各 16KB,第 5 个 64KB,后 7 个各 128KB。打印时注意%02X保证字节宽度,避免看到 0xAA 被截断成 0xA 而误判。
3. 用固件库设置写保护与读保护:从解锁到生效的完整流程
3.1 写保护的最小操作序列与扇区掩码选择
设置写保护的标准流程是:解锁 FLASH 和选项字节、填充结构体、调用编程函数、锁定、复位。下面以保护扇区 0 到 3(通常放 bootloader)为例。
#include "stm32f4xx.h" #include "stm32f4xx_flash.h" /* 对扇区 0~3 设置写保护,保护 bootloader 区域 */ void FLASH_SetWriteProtection(void) { FLASH_OBProgramInitTypeDef ob; /* 解锁 FLASH 控制器和选项字节区域 */ FLASH_Unlock(); FLASH_OB_Unlock(); /* 先读当前配置,避免覆盖已有的其他选项字节设置 */ FLASH_OBGetConfig(&ob); /* 在原有掩码基础上追加扇区 0~3,防止误清除已有保护 */ ob.WRPSector |= OB_WRP_SECTOR_0 | OB_WRP_SECTOR_1 | OB_WRP_SECTOR_2 | OB_WRP_SECTOR_3; ob.OptionType = OPTIONBYTE_WRP; /* 执行选项字节编程,内部会处理擦除与写入 */ if (FLASH_OBProgram(&ob) != FLASH_COMPLETE) { /* 编程失败,通常是电压不稳或未解锁 */ printf("WRP program failed\r\n"); } /* 锁定选项字节和 FLASH,防止误改 */ FLASH_OB_Lock(); FLASH_Lock(); /* 写保护需要系统复位后才真正生效 */ NVIC_SystemReset(); }逻辑说明:FLASH_OBProgram会根据OptionType决定改哪部分选项字节,这里只改 WRP,不动 RDP 和 BOR。参数说明:OB_WRP_SECTOR_0等宏在固件库头文件里定义,直接按位或即可组合多个扇区。关键点是先FLASH_OBGetConfig再追加,而不是直接赋值,否则会把之前设过的其他扇区保护清掉,这是血泪经验。复位后可以用第 2 章的打印函数验证掩码是否生效。
3.2 读保护的设置与那个绕不开的整片擦除
设置读保护比写保护更简单,但代价更大。下面代码把 RDP 设为等级 1。
/* 开启读保护,等级 1,调试口将无法读取 FLASH */ void FLASH_SetReadProtection(void) { FLASH_OBProgramInitTypeDef ob; FLASH_Unlock(); FLASH_OB_Unlock(); FLASH_OBGetConfig(&ob); ob.RDPLevel = OB_RDP_LEVEL_1; /* 等级 1,最常用 */ ob.OptionType = OPTIONBYTE_RDP; if (FLASH_OBProgram(&ob) != FLASH_COMPLETE) { printf("RDP program failed\r\n"); } FLASH_OB_Lock(); FLASH_Lock(); NVIC_SystemReset(); }逻辑说明:OB_RDP_LEVEL_1对应写入非 0xAA 的读保护值,等级 2 会永久锁死调试口且不可解除,量产中极少使用。参数说明:设置读保护本身不会擦除 FLASH,但解除时会。所以如果你的参数存在内部 FLASH 且没有备份,开启前务必先导出。复位后再次连接调试器,会发现无法读取 FLASH 内容,这就是生效的标志。
3.3 解除读保护:接受整片擦除并重建参数
解除读保护就是把 RDP 写回 0xAA,硬件随即擦除整片主存储区。代码很短,但心理准备要足。
/* 解除读保护,注意:此操作会触发整片 FLASH 擦除 */ void FLASH_DisableReadProtection(void) { FLASH_OBProgramInitTypeDef ob; FLASH_Unlock(); FLASH_OB_Unlock(); FLASH_OBGetConfig(&ob); ob.RDPLevel = OB_RDP_LEVEL_0; /* 0xAA,解除保护 */ ob.OptionType = OPTIONBYTE_RDP; FLASH_OBProgram(&ob); FLASH_OB_Lock(); FLASH_Lock(); NVIC_SystemReset(); }逻辑说明:OB_RDP_LEVEL_0即写回 0xAA。执行后芯片会自动擦除主存储区,程序区变空,需要重新烧录固件。参数说明:如果解除后不重新烧录,芯片会停在空 FLASH 状态,表现为无法启动。所以解除操作一般只在返修或重新产线烧录时做,且要配合完整的烧录流程。
4. 避坑与排查:读写保护设置中最容易翻车的五个点
4.1 设置了写保护但还能写入,现象与原因
现象:调用FLASH_SetWriteProtection后复位,用调试器或程序仍能擦写被保护扇区。原因通常是漏掉了NVIC_SystemReset,选项字节虽然写进去了,但保护逻辑要复位后才加载。解决:确认复位,再用第 2 章打印函数核对WRPSector掩码是否真的包含目标扇区。另一个可能是只调了FLASH_OBProgram却没先FLASH_OB_Unlock,编程被静默拒绝。
4.2 解除读保护后程序没了,参数区也空了
现象:执行解除读保护后,设备无法启动,之前存的标定参数全部丢失。原因就是前面反复强调的整片擦除机制。解决:解除前把参数导出到外部存储或上位机,解除并重新烧录后再写回。如果参数必须留在内部 FLASH,考虑把参数区放在独立的外部 EEPROM 或 FRAM,避免和读保护绑定。
4.3 选项字节编程返回失败但没报错
现象:FLASH_OBProgram返回值不是FLASH_COMPLETE,但代码里没处理,导致以为设置成功。原因可能是供电电压低于 2.7V,选项字节编程对电压敏感;也可能是 FLASH 处于锁定状态。解决:在编程前检查FLASH_GetStatus,确保供电稳定,并在失败分支里加打印或 LED 指示,不要静默忽略。
4.4 误把等级 2 当成等级 1 用
现象:调试口彻底连不上,芯片无法再烧录。原因:OB_RDP_LEVEL_2是不可逆的,一旦写入,调试口永久关闭,只能通过用户程序里的特定逻辑才能改回。解决:量产中除非有明确的安全需求,否则只用等级 1。如果不小心写了等级 2,基本只能换芯片,这是后悔药也救不回来的操作。
4.5 写保护和读保护同时设置时的顺序问题
现象:先设读保护再设写保护,结果写保护编程失败。原因:读保护开启后,选项字节的某些操作会受到限制,部分固件库版本要求先设写保护再设读保护。解决:推荐顺序是先设写保护、复位验证,再设读保护、复位验证。如果必须同时设,把两个OptionType合并到一次FLASH_OBProgram调用里,减少中间状态。
5. 产线落地的一个实用技巧:用状态机管理保护与烧录流程
把读写保护做进产线,最怕的是流程混乱:有的工位先烧录后保护,有的先保护后烧录,结果良率忽高忽低。我一般会在工装脚本里用一个简单的状态机来管:上电先读保护状态,如果未保护则烧录固件、写参数、设写保护、复位、再设读保护、复位、最后回读校验。回读校验在开启读保护后无法读 FLASH,所以校验要放在设读保护之前,或者只校验参数区的 CRC。
下面是一个状态判断的片段,用来决定当前芯片该走哪条分支。
/* 根据当前保护状态决定产线动作 */ typedef enum { STATE_UNPROTECTED, /* 未保护,可烧录 */ STATE_WRP_ONLY, /* 仅写保护,需补读保护 */ STATE_RDP_ON, /* 已读保护,需解除后重烧 */ } FlashState; FlashState FLASH_GetState(void) { FLASH_OBProgramInitTypeDef ob; FLASH_OBGetConfig(&ob); if (ob.RDPLevel != 0xAA) { return STATE_RDP_ON; /* 读保护已开,先解除 */ } if (ob.WRPSector != 0x00000000) { return STATE_WRP_ONLY; /* 写保护已开,补读保护 */ } return STATE_UNPROTECTED; /* 干净芯片,正常烧录 */ }逻辑说明:RDPLevel非 0xAA 优先判断,因为读保护开启时写保护状态可能读不全。参数说明:WRPSector为 0 表示无写保护。产线脚本拿到状态后,STATE_RDP_ON走解除加重烧,STATE_WRP_ONLY直接补设读保护,STATE_UNPROTECTED走完整流程。这样无论芯片之前被谁动过,都能收敛到同一个终态。
一个具体技巧是:把参数区的 CRC 和版本号存在外部 EEPROM,内部 FLASH 只放代码。这样即使读保护解除触发整片擦除,参数也不受影响,返修时只需重烧代码。我吃过一次亏,参数存在内部 FLASH 最后一个扇区,解除读保护后整批返修板子的标定数据全没了,重新标定花了两天。从那以后,参数和代码物理分离成了我的默认习惯。希望帮到你。
本文还有配套的精品资源,点击获取