资讯动态

STM32读保护RDP与PCROP详解:从Level 0到Level 2的固件安全配置

发布时间:2026/8/29 13:45:39 来源:尧图企业网站定制
1. 看到“代码读取保护”先想清楚它在保护什么做过嵌入式产品量产的人都有这种体会固件就是产品的命根子。费了好几个月调出来的算法、通信协议、UI逻辑一旦被竞争对手通过调试接口把Flash内容读出来逆向工程的门槛就低了一大截。STM32L4、STM32L4和STM32G4系列微控制器在国内用量非常大从表计、传感器、工业控制到便携医疗设备都能看到它们的身影所以很多人拿到芯片的第一步就是研究怎么把代码锁死。这个主题里提到的“专利代码读取保护”英文原文其实叫 Protected Code Readout Protection不过绝大多数场合你看到的缩写是RDPReadout Protection也就是读保护。市面上也有人把PCROPProprietary Code Readout Protection专有代码读取保护跟RDP混着叫这俩确实是两套机制一个管全部Flash一个管指定区域我后面会详细拆开讲。简单一句话先记住RDP管的是“整颗芯片能不能被人把固件读走”PCROP管的是“即使能读到Flash某些关键区域的代码也读不出来”。这篇文章适合刚从F1/F0系列转过来做新项目的朋友也适合已经在用L4系列、但还没仔细研究过Option Bytes的人。读完你至少能搞清楚三个问题RDP到底有几个等级、设置之后会发生什么不可逆的变化、量产时怎么用STM32CubeProgrammer或HAL库把保护配好。与此同时我会把L4、L4、G4三个系列之间的差异标注出来毕竟它们的双Bank结构、Option Bytes布局和PCROP能力不完全一样。2. RDP三个等级背后的设计逻辑2.1 等级0出厂默认裸奔状态Level 0是芯片出厂时的状态意味着没有任何读保护任何人都可以通过SWD接口连接调试器读取Flash、SRAM、OTP区域的内容也可以随意擦除和重新烧写。这个状态适合开发调试阶段因为你可以随时读回Flash验证数据可以在断点处查看内存不会受到任何阻碍。但量产烧录之后如果仍停留在Level 0风险极大。测试治具、返修板、废弃板只要流到外部别人拿一根ST-Link线就能把整颗Flash倒出来。更麻烦的是很多硬件设计会把SWD接口引到测试点或排针上这等于把固件裸送出去。所以Level 0只适合开发期和内部测试产品出厂前必须升到Level 1或Level 2。2.2 等级1最常用也是最容易被误解的一级Level 1是对外展示固件的首选保护等级。设置Level 1之后调试接口仍然可以连接这意味着你能在开发阶段发现问题、继续调试但调试器去读Flash时会得到无效数据或者直接读不出来。与此同时从RAM中启动的程序、系统Bootloader也被禁止访问主Flash。这套逻辑的本质是CPU可以正常执行Flash里的代码但任何外部调试器都不能直接抓取Flash内容。这里有一个特别关键的细节也是很多新手踩坑的地方Level 1状态下如果通过调试器发起连接并请求解除保护芯片会先执行一次全片擦除然后再把RDP降回Level 0。这是ST有意设计的安全机制目的是防止有人拿到Level 1的板子之后通过修改Option Bytes来绕过保护、再慢慢读Flash。所以你在STM32CubeProgrammer里选择“Remove read protection”时工具一定会弹窗提醒你数据会被擦掉。这个不是工具bug是芯片硬件的保护策略。很多项目选择在生产时直接设Level 1既能挡住绝大多数抄袭者又保留了一定的现场维护能力。比如设备返修时通过串口IAP或者RDP降级重烧只要接受全片擦除这个代价就行。2.3 等级2不可逆的“终局锁”Level 2是我在项目里说得最多的一个等级因为它的代价非常“昂贵”。Level 2一旦使能芯片的调试接口SWD/JTAG会永久禁用Cortex-M内核的调试寄存器也全部锁死调试器连芯片ID都读不到。所有从系统Bootloader启动的入口、RAM启动的入口也会被禁用相当于整个芯片只剩一个功能执行Flash里已有的应用程序。代价是这种保护不可逆。一旦芯片处于Level 2不管用什么工具、什么连接方式都别想再通过SWD口把保护降级或擦除Flash。如果想通过系统Bootloader也就是自举程序来恢复也不行因为Level 2下Bootloader入口直接被硬件禁止。这就意味着烧录Level 2之前你必须把后续所有可能需要的升级功能都固化在应用代码里比如IAP应用内程序升级、通过UART/CAN/USB接收新固件等。否则芯片就只能报废。从安全性角度Level 2相当于把芯片变成了一次性容器。很多行业客户对安全等级有硬性要求比如某些计量类产品、金融终端设备强制要求固件不可被外部读取同时防止未经授权的调试。这时候Level 2几乎是唯一选择。2.4 级别切换时Flash数据会发生什么搞清楚了等级定义还要把切换过程的数据变化捋顺否则量产时手一抖就是一片板子报废。Level 0 切到 Level 1Flash内容保留。这是量产烧录最常见路径先烧固件再设Level 1然后测试最后发货。Level 1 切到 Level 0芯片自动执行全片擦除然后RDP回到Level 0。所以这种切换只适用于返修重烧不适用于“只看一眼”的场景。Level 1 切到 Level 2Flash内容保留之后不可降级。这一点要在烧录Level 2前做足测试因为一旦切过去无法再回到Level 1做现场调试。Level 0 直接切到 Level 2芯片会把Flash内容保留如果之前有内容但不可逆。真正量产的时候我建议把“设置Level 1”作为默认动作等到整机测试全部通过、确定不再需要SWD调试之后再在产线最后一道工序设置Level 2。这样做的好处是前面任何返修都还能通过全片擦除重来后期成品则彻底锁死。3. Option Bytes操作与烧录流程实战3.1 通过STM32CubeProgrammer搞定Level 1和Level 2先讲图形化操作因为这是多数产线工程最常用也最不容易出错的方式。用STM32CubeProgrammer连接芯片之后切到左侧的“Option Bytes”页面。STM32L4、L4、G4系列的Option Bytes布局基本类似在“Read protection”区域会看到一个下拉框里面有三个选项AA表示Level 055表示Level 1CC表示Level 2。不同批次/不同手册里显示值可能略有差异但原理一致。选择Level 1后点击“Apply”工具会写入Option Bytes并发起一次系统复位保护立即生效。选择Level 2时软件会有红色警告提示此操作不可逆。这时千万不要直接点确定先把工位上的接线和设备状态确认一遍再执行。有一个产线经常忽略的点如果板子上有独立的外部看门狗、电源管理芯片或其他外设在Option Bytes写入并复位期间要确保供电稳定。因为Option Bytes写入过程涉及Flash控制器操作如果刚好在写入瞬间掉电Option Bytes的值可能处于中间状态导致芯片启动异常。虽然大部分情况下重新烧写能恢复但产线上能少一例异常就少一例。建议所有操作Option Bytes的工位都使用稳定电源不要依靠USB口供电。3.2 在应用代码里用HAL库修改RDP在某些场景下你希望在固件运行时自动完成RDP升级。比如设备出厂前自动运行自检自检通过后自动把Level 0切到Level 1这样产线就不需要单独执行一次CubeProgrammer操作。思路很直接在代码里调用HAL库的Flash Option Bytes编程接口。以STM32L4系列为例先把Option Bytes解锁然后配置RDP级别并触发加载#include stm32l4xx_hal.h void Set_RDP_Level(uint8_t level) { FLASH_OBProgramInitTypeDef OBInit; HAL_StatusTypeDef status; // 1. 解锁Flash HAL_FLASH_Unlock(); // 2. 解锁Option Bytes区域 HAL_FLASH_OB_Unlock(); // 3. 准备写入RDP配置 OBInit.OptionType OPTIONBYTE_RDP; OBInit.RDPLevel level; // OB_RDP_LEVEL_0 / OB_RDP_LEVEL_1 / OB_RDP_LEVEL_2 // 4. 执行Option Bytes编程 status HAL_FLASHEx_OBProgram(OBInit); if (status ! HAL_OK) { // 处理失败注意RDP配置失败时必须复位重试 } // 5. 触发Option Bytes加载 HAL_FLASH_OB_Launch(); // 6. 锁定Flash HAL_FLASH_OB_Lock(); HAL_FLASH_Lock(); // 7. 系统复位让保护生效 NVIC_SystemReset(); }这里有个容易被坑的点HAL_FLASH_OB_Launch()会触发一次系统复位复位之后代码会重新开始执行。如果你的保护升级是在主程序的某个任务里做的一定要保证复位后不要再走一遍“连接调试器”的逻辑否则一旦保护生效调试器连接行为会异常。另外Level 2在代码里设置完以后芯片基本就断开了所有外部调试可能性。所以在量产固件里做自动升级Level 2必须确保固件本身足够稳定否则后面想通过SWD修bug就完全没门了。3.3 L4、L4、G4系列在RDP上的差异虽然这三个系列同源参考手册都可以看RM0351和RM0432但细节上还是有区别STM32L4比如L476、L496单Bank或者双Bank取决于型号配置RDP作用于整个Flash双Bank模式下两个Bank一起受保护。STM32L4比如L4R9、L4S9容量更大双Bank支持更灵活在Level 1下可通过选项字节配置不同的Bank映射方式。STM32G4比如G431、G474定位是电机控制、数字电源Flash容量相对较小但RDP机制和L4基本一致且多数型号支持双BankG4的选项字节中还有额外的CCM SRAM保护选项但和RDP是独立的两回事。实际操作时我强烈建议把具体型号对应参考手册里“Flash option bytes”章节翻出来对照。因为即便是L4系列内部不同子系列对Option Bytes的bit定义也可能有一两个差异比如某些保留位在新版本芯片上有了新功能。如果只凭经验不看手册遇到新批次的芯片很容易踩坑。4. PCROP把指定区域的代码彻底“隐形”4.1 为什么有了RDP还需要PCROPRDP Level 1虽然挡住了调试器但有一个绕不开的场景如果攻击者已经能够执行任意代码比如利用你已经开放的IAP接口把一段自定义程序加载进RAM然后让CPU运行这段代码那么这段代码还是可以正常读取Flash中的内容。因为在芯片自身看来主CPU读自己的Flash是天经地义的事硬件不会区分读的人到底是谁。PCROPProprietary Code Readout Protection解决的就是这个问题。它的核心机制是把某个连续地址区间标记为PCROP区域主CPU可以从这个区域取指令执行但任何针对这个区域的数据读取都会被硬件阻止写入也被阻止。这意味着即使攻击者获得了任意代码执行能力也没办法把PCROP区域里的二进制内容dump出来。这就等于把关键算法所在的代码段变成了“只能跑、不能看”的黑盒。4.2 PCROP配置与限制PCROP需要在Option Bytes里指定起始地址和结束地址并配合RDP等级使用。在STM32L4系列上PCROP区域通常按页对齐且两个Bank都有对应的起始/结束寄存器。以L4为例PCROP1A_STRT、PCROP1A_END控制Bank1的PCROP范围PCROP1B_STRT、PCROP1B_END控制Bank2的PCROP范围。和RDP一样PCROP可以通过CubeProgrammer的Option Bytes页面配置也可以在代码里用HAL的HAL_FLASHEx_OBProgram接口写入FLASH_OBProgramInitTypeDef OBInit; OBInit.OptionType OPTIONBYTE_PCROP; OBInit.PCROPConfig FLASH_BANK_1; OBInit.PCROPStartAddr 0x08008000; OBInit.PCROPEndAddr 0x0800BFFF; OBInit.PCROP_RDP OB_PCROP_RDP_NOT_ENABLE; HAL_FLASHEx_OBProgram(OBInit); HAL_FLASH_OB_Launch();这里的PCROPStartAddr和PCROPEndAddr必须遵循页对齐要求如果地址设错工具会直接报错或者写入无效值。我建议把算法函数放在一个独立的链接段里比如__attribute__((section(.pcrop_text))) void Crypto_Algorithm(void) { // 这里放关键算法代码 }然后在链接脚本里把.pcrop_text段安排到固定地址再把该地址范围配为PCROP区域。这样编译出来的固件结构清晰不会出现关键代码散落在各个地址的问题。需要注意的是PCROP区域不能包含中断向量表因为L4系列的中断向量表必须能被正常读取。如果向量表落在PCROP区域芯片启动后取中断向量就会失败直接进HardFault。这是个非常隐蔽的坑我在一个原型项目上排查了两天才发现是PCROP把向量表盖住了。从整体安全性角度PCROP和RDP是互补关系RDP把整颗芯片的读保护等级拉上来PCROP则把最关键的一段代码单独藏起来。即使RDP被某种方式绕过PCROP区域仍然能守住最后一道防线。5. 常见问题与排查技巧实录5.1 问题速查表现象可能原因处理方式CubeProgrammer提示“Read protection Level 1”并拒绝读取Flash芯片处于RDP Level 1属正常保护不要慌确认是否有解除保护的权限接受全片擦除后降级设置Level 2之后调试器完全连不上这是Level 2的预期行为无法通过SWD恢复。若此前没有固化IAP芯片只能报废设置Level 1后程序运行异常Option Bytes里其他选项被误改重新读取Option Bytes比对检查nBOOT0、nBOOT1是否被改动PCROP区域代码执行正常但特定函数调用返回错误可能是PCROP边界未对齐或把常量数据放进了PCROP区确认PCROP区域内只放代码常量和只读数据要放到其他段批量产线中少量板子设置RDP后无法启动写入Option Bytes时供电不稳或接线过长先Full Chip Erase再重新烧录检查产线供电和SWD线质量忘记Level 2前的IAP会导致无法升级没有在烧录Level 2前固化Bootloader无解只能换芯片。这也是为什么必须在Level 2前把Bootloader写入Flash5.2 调试中的几个独家心得第一Level 1状态下调试器连接出现“connect failed”不代表芯片坏了。很多时候是因为上一次调试会话没有正常关闭或者调试器在连接时发起了memory read请求被硬件拒绝了。解决方法是先确保芯片供电再点击CubeProgrammer的“Connect under reset”按钮或者手动拉低NRST同时连接给调试器一个干净的上电复位时机。第二在代码里通过HAL库设置RDP Level 2时有一个细节容易被忽略HAL_FLASHEx_OBProgram函数返回成功之后RDP并不立即生效要等HAL_FLASH_OB_Launch()并复位。如果你在函数返回后还继续执行大量Flash读取操作可能读到的内容并没有被保护覆盖。更稳妥的做法是设置完Level 2后立即执行NVIC_SystemReset()不要做其他多余的事。第三关于L4系列的双Bank选项和RDP的关系。如果项目启用了双Bank启动模式并且通过DBANK选项字节切换了Bank映射RDP的Level 1保护是作用于两个Bank的但PCROP区域则是按Bank单独配置的。在Bank切换后PCROP的起始/结束地址需要重新确认否则可能出现“保护了A区域实际代码跑在B区域”的尴尬情况。5.3 量产与后期维护的整体建议我做过几个带无线升级功能的表计产品最终的固件保护策略是这样的Bootloader区域放在Flash起始位置负责启动和IAP接收。应用固件放在Bootloader之后正常运行时执行应用。关键加密算法、AES密钥派生函数放在PCROP区域。RDP设置为Level 1保证调试接口还能用于产测。所有产测通过后在最终出货前把RDP升到Level 2。这套组合里Level 2牺牲了现场返修的便利性但换来了极高的安全性。如果产品后续有固件升级需求Bootloader里一定要预留IAP机制并且IAP接收固件后要校验签名防止攻击者利用升级通道往里塞恶意固件。否则你设了Level 2反而把自己的产品入口也封死了而攻击者如果找到了IAP漏洞却发现Flash随便读那才是灾难。6. 从参考手册到实际项目建议的资料清单很多朋友会在网上翻“stm32l4中文参考手册”这里我给个明确的索引建议。STM32L4系列的官方参考手册编号是RM0351STM32L4系列是RM0432STM32G4系列是RM0440。虽然名字叫“参考手册”但其实它就是最完整、最权威的寄存器级说明RDP、PCROP、Option Bytes的bit定义和操作流程都写在里面。中文版手册在ST官网或者第三方社区可以下载但注意版本日期尽量与你的芯片批次对应。官方应用笔记方面重点关注AN4712STM32L4系列的安全它对RDP、PCROP、OTP、加密引擎等做了系统性的梳理。另外AN4992和AN5052也有关于代码保护的部分适合深度阅读。在实际项目里我的建议是先看参考手册当中“Flash option bytes”这一章把Option Bytes的bit图大概扫一遍然后去数据手册里查芯片具体支持多少个PCROP区域、每页多大。不同型号的Flash页大小可能不同比如L4系列很多是2KB一页G4系列是2KBL4部分型号是4KB。页大小直接影响PCROP边界的对齐要求这个细节直接影响链接脚本的编写。通读这些资料确实需要时间但回报也很大当你真正把RDP和PCROP吃透之后才能在设计阶段就把Flash布局、Bootloader、IAP、产线烧录流程一次性定好。否则等你画完了板子、写完了代码才想起保护机制会发现很多功能被自己的保护策略堵死了。7. 几个值得再展开的关键小细节最后分享几个我在项目里折腾过、觉得值得专门记录的小细节。关于RDP对调试口的隐藏STM32L4设Level 2之后除了SWD不可用连芯片内部的“系统内存自举模式”也会失效。这意味着如果外部把BOOT0引脚拉高想让芯片进入内置bootloader是进不去的。不要把这个当成bug这是Level 2的预期设计。你只能靠自己的固件IAP升级。关于Option Bytes的“预编程”验证在代码里写入RDP之前最好先读取一次当前Option Bytes值确认芯片确实处于预期的保护状态。有些旧版HAL库在HAL_FLASH_OBProgram前没有做电源调整在低电压下直接写Option Bytes有可能产生校验失败。如果遇到这种情况检查供电电压是否在芯片允许范围内尤其当芯片带USB外设时3.0V以下的电压对Flash写操作不太友好。关于产线软件自动化的容错如果产线是用STM32CubeProgrammer命令行脚本批量烧录建议在脚本里对“设置Level 2”做二次确认。命令行工具一旦执行不会像GUI那样弹窗提示很容易出现误操作。比如你把烧录命令写错把-ob RDP0xCC当成默认参数下发一批板子全变砖。我见过不止一次这种事故都是因为脚本里Level 2的参数没有做分段隔离。关于PCROP与加密库的配合如果你准备在PCROP区域放一个AES解密函数注意不要把密钥常量也放进同一个PCROP区域。因为PCROP区域禁止读取但大多数ARM Cortex-M的指令立即数都可以被反汇编工具识别如果密钥以普通常量形态存在于代码里即使不能通过调试器读Flash也可能通过侧信道分析或者其他漏洞泄露。更稳妥的做法是把密钥存入OTP区域或者使用芯片内置的硬件加密单元。关于“先设RDP还是先写PCROP”的顺序ST官方推荐流程是先写完整个Flash包括PCROP区域内的代码然后统一配置PCROP和RDP。如果你在PCROP已使能的状态下尝试去更新PCROP区域内的代码会发现写入被禁止因为PCROP区域对写入也是封锁的。这会让开发流程很别扭所以开发阶段最好保持RDP和PCROP都不开启全部调试完成后再做最后保护配置。8. 结束前说一点个人的实际体会我自己的习惯是每个项目里所有代码保护相关的配置都写成一个独立的烧录脚本或固件模块并且每一步都加日志输出。这样出问题时至少能知道保护失败发生在“写入Option Bytes时”还是“触发加载时”排查路径会清晰很多。踩过最痛的一次坑是在一个已经批量出货的项目里发现某个批次的芯片通过USART的IAP接口升级时总是下载到一半就失败。后来查下来原因是固件里既有PCROP区域又把IAP接收缓冲区的校验逻辑放在了PCROP区域边界附近导致DMA读取PCROP区域数据时被硬件拦截数据校验永远失败。从那以后我就坚决不在PCROP区域里放任何需要被DMA、外设直接读取的数据。这种小细节文档里不会明确写“你不能这样用”但实际硬件行为就是这么硬核。代码读取保护这东西说到底是给产品上一把锁。锁的级别越高安全越好但自己操作起来也越不自由。提前想清楚产品的维护方式、升级路径、返修策略再决定用Level 1还是Level 2才是最务实的做法。希望这篇笔记能帮你少走几步弯路。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价