STM32读保护RDP与PCROP详解:从Level 0到Level 2的固件安全配置
2026/8/29 13:45:25 网站建设 项目流程

1. 看到“代码读取保护”,先想清楚它在保护什么

做过嵌入式产品量产的人都有这种体会:固件就是产品的命根子。费了好几个月调出来的算法、通信协议、UI逻辑,一旦被竞争对手通过调试接口把Flash内容读出来,逆向工程的门槛就低了一大截。STM32L4、STM32L4+和STM32G4系列微控制器在国内用量非常大,从表计、传感器、工业控制到便携医疗设备都能看到它们的身影,所以很多人拿到芯片的第一步就是研究怎么把代码锁死。

这个主题里提到的“专利代码读取保护”,英文原文其实叫 Protected Code Readout Protection,不过绝大多数场合你看到的缩写是RDP(Readout Protection),也就是读保护。市面上也有人把PCROP(Proprietary 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 1:Flash内容保留。这是量产烧录最常见路径:先烧固件,再设Level 1,然后测试,最后发货。
  • Level 1 切到 Level 0:芯片自动执行全片擦除,然后RDP回到Level 0。所以这种切换只适用于返修重烧,不适用于“只看一眼”的场景。
  • Level 1 切到 Level 2:Flash内容保留,之后不可降级。这一点要在烧录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 0,55表示Level 1,CC表示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基本一致,且多数型号支持双Bank;G4的选项字节中还有额外的CCM SRAM保护选项,但和RDP是独立的两回事。

实际操作时,我强烈建议把具体型号对应参考手册里“Flash option bytes”章节翻出来对照。因为即便是L4系列内部,不同子系列对Option Bytes的bit定义也可能有一两个差异,比如某些保留位在新版本芯片上有了新功能。如果只凭经验不看手册,遇到新批次的芯片很容易踩坑。

4. PCROP:把指定区域的代码彻底“隐形”

4.1 为什么有了RDP还需要PCROP

RDP Level 1虽然挡住了调试器,但有一个绕不开的场景:如果攻击者已经能够执行任意代码,比如利用你已经开放的IAP接口把一段自定义程序加载进RAM,然后让CPU运行这段代码,那么这段代码还是可以正常读取Flash中的内容。因为在芯片自身看来,主CPU读自己的Flash是天经地义的事,硬件不会区分读的人到底是谁。

PCROP(Proprietary Code Readout Protection)解决的就是这个问题。它的核心机制是:把某个连续地址区间标记为PCROP区域,主CPU可以从这个区域取指令执行,但任何针对这个区域的数据读取都会被硬件阻止,写入也被阻止。这意味着即使攻击者获得了任意代码执行能力,也没办法把PCROP区域里的二进制内容dump出来。这就等于把关键算法所在的代码段变成了“只能跑、不能看”的黑盒。

4.2 PCROP配置与限制

PCROP需要在Option Bytes里指定起始地址和结束地址,并配合RDP等级使用。在STM32L4系列上,PCROP区域通常按页对齐,且两个Bank都有对应的起始/结束寄存器。以L4为例,PCROP1A_STRTPCROP1A_END控制Bank1的PCROP范围,PCROP1B_STRTPCROP1B_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();

这里的PCROPStartAddrPCROPEndAddr必须遵循页对齐要求,如果地址设错,工具会直接报错或者写入无效值。我建议把算法函数放在一个独立的链接段里,比如:

__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写入Flash

5.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系列的官方参考手册编号是RM0351,STM32L4+系列是RM0432,STM32G4系列是RM0440。虽然名字叫“参考手册”,但其实它就是最完整、最权威的寄存器级说明,RDP、PCROP、Option Bytes的bit定义和操作流程都写在里面。中文版手册在ST官网或者第三方社区可以下载,但注意版本日期,尽量与你的芯片批次对应。

官方应用笔记方面,重点关注AN4712(STM32L4系列的安全),它对RDP、PCROP、OTP、加密引擎等做了系统性的梳理。另外AN4992和AN5052也有关于代码保护的部分,适合深度阅读。

在实际项目里,我的建议是先看参考手册当中“Flash option bytes”这一章,把Option Bytes的bit图大概扫一遍,然后去数据手册里查芯片具体支持多少个PCROP区域、每页多大。不同型号的Flash页大小可能不同,比如L4系列很多是2KB一页,G4系列是2KB,L4+部分型号是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 RDP=0xCC当成默认参数下发,一批板子全变砖。我见过不止一次这种事故,都是因为脚本里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,才是最务实的做法。希望这篇笔记能帮你少走几步弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询