“固件保护”这四个字在STM32圈子里经常被一句话带过开个读保护不就行了。可当你真正拿到STM32L4、STM32L4和STM32G4这几个系列的数据手册会发现事情没那么简单。它们用的保护机制不只是RDP选项字节还叠加了一层面向厂商量产场景的“专利代码读取保护”也就是应用笔记里常说的PCROP。我见过好几个团队在样机阶段把RDP设为Level 1就以为Flash里的核心算法万无一失结果评审时被问到“DMA能不能读”“调试器还能不能看到外设寄存器”才意识到漏了一堆细节。这篇笔记不是念手册而是把STM32L4、L4和G4上的保护机制拆开揉碎RDP三个等级到底各自锁什么PCROP和RDP怎么配合以及在CubeProgrammer、调试器、量产烧录这些实际场景里怎么操作、怎么测试、怎么避坑。适合刚接触STM32保护特性的工程师也适合已经在用L4/G4但准备把固件保护升一个档次的团队。1. 为什么这几条产品线的保护逻辑和以前的F1/F0完全不一样1.1 选项字节只是入口闪存控制器才是真正把关的人很多老工程师对读保护的理解还停留在STM32F1时代有个叫RDP的选项字节0xAA是没锁0xBB是锁上锁上之后调试器读不到Flash大概就是这么回事。但从STM32L4开始在L4、L4和G4这条产品线里Flash控制器本身的权限管理比F1精细得多。RDP只是一个总开关真正决定“哪条总线可以访问哪块Flash区域”的是闪存控制器内部的那张访问权限表。CPU可以访问DMA可能被拦调试器可能被拦系统存储器的启动代码可能被拦这几类访问是分开管控的。所以同样一个RDP Level 1在F1上理解成“整个芯片锁死”到了L4/G4上更准确的理解是外部调试器对主Flash的读取权限被切断了但CPU运行用户代码时的正常访问不受影响。这种细粒度权限拆分是后面PCROP能够单独锁某几块区域的前提。1.2 PCROP的“专利代码”到底是什么标题里的“专利代码读取保护”简单说不是保护你的“专利证书”而是保护你写在Flash里那些不想被别人抄走的固件块比如音视频处理算法、步进电机加减速曲线、加密密钥派生函数。芯片原厂把这类代码统称为proprietary code翻译过来就成了“专有代码”或“专利代码”。以前在F1上想把某一段算法保护起来没有太好的办法。RDP一开整片Flash全锁调试器进不来但你在自己代码里用DMA读这段算法区域照样能读甚至还能把数据从外设接口直接搬运出去。换句话说F1上的保护是“对外防”但“对内”基本不设防。而L4/L4/G4引入的PCROP逻辑完全不同它允许你在Flash里画几个保护区这些区域CPU可以取指执行但DMA不能读调试器不能读其他总线主设备也不能读。这样一来算法本身是能在CPU里跑的但别人既没法通过调试器把镜像拉下来也没法用DMA把数据偷偷拷到外部接口。这才是“专利代码读取保护”真正要解决的事。1.3 老系列与新系列的差异对照我整理了一张简化对照表方便你看清大概差异项目STM32F1/F4旧思路STM32L4/L4/G4新思路保护开关RDP选项字节RDP PCROP选项字节组合RDP Level 1调试器禁止访问主Flash调试器禁止访问主Flash但权限粒度更细独立代码区保护基本没有可以指定PCROP区域只锁指定页DMA对受保护区域的访问RDP开启后仍可能访问PCROP区域内DMA访问被硬件拦截等级回退Level 1回Level 0需全片擦除同样需要全片擦除但G4/L4有更严格约束调试口永久关闭无对应机制Level 2可永久关闭调试口这颗钉子钉下去就好理解了以前我们习惯把“代码保护”当成一个开关开了就安全现在在L4/G4上得把保护拆成“外防调试器”和“内防外设泄露”两层来看。2. RDP三级保护Level 0、Level 1、Level 2的取舍RDP在L4、L4和G4上依然是三个等级但每个等级的边界条件比老系列写得更细。下面不是照抄参考手册而是讲人话拆给你听。2.1 Level 0默认状态裸奔没商量Level 0是芯片出厂时的默认状态调试器可以完整访问Flash、SRAM、外设寄存器所有读保护功能全部关闭。在这个状态下如果你的板子被竞争对手拿到大概率可以直接用ST-LINK把整片固件二进制文件拉出来再用IDA慢慢逆向。很多团队在开发阶段会一直停留在Level 0这没问题因为要调试、要烧录、要反复改代码开放状态最省事。但如果你把Level 0状态的产品直接发出去量产那基本等于把源代码的二进制可执行文件送到别人手上。一个容易被忽略的点Level 0下PCROP也处于休眠状态。要配置PCROP区域通常也得先把RDP维持在Level 0或1但真正让PCROP对调试器和DMA生效往往需要把RDP提升到Level 1及以上。确切关系在不同系列上略有差别启动时务必看一眼对应参考手册的Option bytes章节。2.2 Level 1日常开发和量产的首选Level 1是大多数工程师口里说的“开了读保护”的状态。在这个等级下外部调试器对主Flash的访问被阻断。你用ST-LINK去连芯片读FLASH时要么返回全0要么直接被拒绝。SRAM和外设寄存器的调试访问也受到限制具体取决于芯片配置。这会直接影响调试体验第一次连上时调试器会弹“Device is protected”之类的提示让你决定要不要unlock而unlock动作会触发全片擦除。但Level 1并不影响你自己代码的正常运行。CPU可以继续从Flash取指令、读常量、启动Bootloader用户自定义的IAP程序也可以正常读写Flash。因为这是“内部用户代码”访问不是“外部调试器”访问属于两个权限域。所以我会给团队定一条规矩一切准备参与硬件测试的板子默认全部烧到Level 1。原因很简单开发板丢在会议室被人顺手摆弄太常见了Level 1至少保证别人把固件拿走后不能立刻读出一份完整的映像。而且从Level 1回到Level 0是全片擦除操作不影响后面继续开发最多重新烧一遍成本和风险都可控。2.3 Level 2烧下去之前先想好“后事”Level 2是最高等级特点是不可逆。一旦把RDP设置为Level 2调试口从物理层面被关闭JTAG/SWD再也不可能连接芯片系统存储器里的Bootloader也拒绝启动。更关键的是你不能用任何软件方法从Level 2回到Level 1。这句话在量产后意味着什么意味着你所有后续升级通道都必须依赖自己在用户固件里写的IAP逻辑比如通过UART、USB、CAN或无线方式接收新固件再写入Flash。如果没设计这套通道芯片等于一次性烧录之后只能回原厂或者直接报废。所以我的建议很明确验证阶段的样机RDP不要设Level 2最多Level 1。真正要量产的版本在量产治具上最后一步再切Level 2且前提是产品逻辑里有可靠的OTA或IAP。想验证Level 2效果用一片不心疼的样片去试不要拿第一批小批量成品当小白鼠。3. PCROP只锁关键函数不锁整片Flash3.1 PCROP和RDP的分工RDP是“门卫”管的是外部调试器能不能进来。PCROP是“保险柜”管的是芯片内部各种总线主设备能不能读指定区域。这句话说出来很多人才意识到PCROP和RDP其实是两码事。RDP关的是外部通道但芯片内部用户代码之间、DMA和外设之间并不受RDP约束。举个例子你有一段AES密钥派生函数放在Flash地址0x08010000RDP开到了Level 1调试器读不到没错但如果你的程序里某个DMA通道配置错误完全可能把这段密钥数据搬运到串口发出去。这时候RDP是完全失效的因为访问者是芯片内部的合法主设备不是外部调试器。PCROP就是来解决这个内防问题的。把0x08010000区域设置为PCROP后DMA访问会被硬件拒绝调试器访问被拒绝其他非CPU总线主设备也不行。唯一能读取这块区域的就是CPU本身而且CPU也只能以执行代码或读取数据的方式访问不能通过调试接口把这些内容导出。3.2 在L4/L4/G4上怎么划PCROP区域L4和G4的PCROP配置核心是往Flash选项字节里写几组边界寄存器比如PCROP1_STRT和PCROP1_END以及可选的PCROP2_STRT和PCROP2_END。这些边界以Flash页或扇区为单位不同型号页大小不一样配置前先查清楚型号对应的组织方式。配置流程大致是这个套路先确认芯片处于Level 0或可修改选项字节的状态。把目标算法函数链接到固定地址段比如用链接脚本划一个独立的段出来。将被保护区域涉及的页首地址和末页地址换算成寄存器值。在选项字节里使能PCROP_RDP位并设置边界。重新上电或复位让选项字节生效。这条流程里最容易翻车的是链接脚本。很多人只把代码放到保护区却忘了把该函数用到的只读常量表、查表数据也放进去。函数一旦执行到保护区外的数据CPU本身当然能读但如果你打算把这个区域彻底做成“私有函数库”就得把代码、常量、向量表相关部分一起规划清楚。另一个细节如果PCROP区域跨越了两个不连续页你需要根据硬件是否支持多个区域来安排。部分L4型号只支持一个连续PCROP区域而G4系列对区域数量和边界的灵活性更高。设计前别只凭标题就动手一定要打开对应型号的参考手册翻到Flash选项字节章节。3.3 PCROP和硬件加密引擎的配合很多L4系列自带AES硬件加密引擎G4系列同样有相关加密外设。如果你手里有算法密钥常规做法是把密钥放在Flash普通区域但这相当于把“钥匙”和“锁”放在同一个房间。更稳妥的组合是PCROP保护区里放一段用AES硬件引擎解密的引导代码启动时先从保护区里取出解密后的密钥/配置完成外设初始化再把密钥原地擦掉。这样即使别人拿到Flash镜像也拿不到保护区里的敏感逻辑。配合方案里我强调一个原则PCROP区域里不放“真密钥”只放“取密钥的逻辑”。因为任何软件保护只要CPU能执行理论上都能被动态分析攻破PCROP只是大幅提高了门槛不是绝对防住所有攻击这个预期一定要建立好。4. 用CubeProgrammer/CubeIDE配置保护的完整操作链路4.1 在STM32CubeProgrammer里设置RDPSTM32CubeProgrammer是最直观的工具。连接芯片后左侧导航栏里找到Option Bytes页面一般会有一个Read Out Protection下拉框里面列出DisabledLevel 0、Level 1、Level 2三个选项。需要特别注意的是当你把Level从0切到1CubeProgrammer会弹一段警告提示“该操作将限制调试访问”。正常点确定即可。但如果你是想从Level 1切回Level 0弹窗里会明确提示“此操作会执行Flash全片擦除”因为这是硬件层面的强制要求从Level 1降级必须触发mass erase避免你“先打开保护、又悄无声息地取消保护”留下读取后门。我自己做批量爬坑时习惯先在接线端子旁贴个标签写明“当前芯片RDP Level 1勿按Unlock”免得隔几天别人也拿CubeProgrammer点一下固件全没。4.2 在Option Bytes里配置PCROP区域在CubeProgrammer的Option Bytes界面里PCROP相关配置通常在Flash区域设置里能看到类似“PCROP Start address”“PCROP End address”的输入框。你只需要填入受保护区域的起始地址和结束地址工具会自动换算成边界值然后点击Apply。注意如果该芯片型号支持PCROP_RDP位界面上一般还会有一个“Enable PCROP_RDP”的勾选项。勾选它意味着PCROP区域即使在RDP Level 0下也受保护否则PCROP的强保护要等RDP升到Level 1才完全生效。这部分不同型号的默认行为会有差异不要在没有看手册的情况下靠猜。我有一次想省事用了CubeProgrammer脚本命令直接改选项字节结果地址写错了半页把一段启动代码划进了PCROP区域复位后芯片直接HardFault很长一段时间没找到原因。后来恢复Level 0检查边界才发现是PCROP区域起点算错。PCROP的边界按页对齐如果你的函数起始地址在页中间硬件会把整页都保护起来结果就是保护区比你预期的大隔壁正常代码也跟着遭殃。4.3 调试器连接受保护芯片时先学会拒绝自动解锁Keil MDK、IAR Embedded Workbench以及STM32CubeIDE自带的调试器遇到受保护芯片时通常会提示“是否执行全片擦除以解除保护”。很多人没看清就点了Yes结果Flash里的旧固件没了保护也没了。正确做法是如果只是想确认保护是否生效直接选No然后断开连接用CubeProgrammer检查选项字节状态如果确实需要重新烧录必须在确认代码已经备份的前提下才选择执行解锁。调试项目的日常建议是开发阶段保持Level 0要交给测试或准备做小批量时才升到Level 1。一旦升到Level 1调试器就再也看不到Flash里的反汇编代码了这对排查某些低层问题时很不方便。所以保护这件事不该是最后拍脑袋加的而是要在项目计划里提前分配好时间节点。5. 量产环境下的保护策略升级通道和覆盖烧录如何共存5.1 先设计IAP再谈Level 2很多产品有远程升级需求类似L4/G4这类带大量Flash的芯片通常会做两个Boot区一个放Bootloader一个放App。如果你把整个芯片直接锁到Level 2系统存储器自带的Bootloader就废了你自己在App里写的Bootloader却还能运行因为它是用户固件。所以量产时推荐这个结构用户Bootloader放在Flash开头一个固定分区不被PCROP覆盖。App区放在另一段整个芯片RDP设为Level 1保证外部调试器读不到App内容。核心算法库单独放到PCROP保护区既防调试器也防DMA。如果产品安全等级要求极高再考虑在Bootloader固件里写一段“只有在收到特定安全命令时才允许切换RDP”的逻辑把Level 2的触发放到升级流程里。直接出厂Level 2且没有IAP通道是很多小团队的灾难我身边就有人因此返工过。5.2 从Level 1退回Level 0的全片擦除是帮你也不是帮你全片擦除是RDP降级时的硬件自毁机制目的很直接防止有人把设备从Level 1解锁到Level 0然后慢慢读出升级前残留的内存数据。它的副作用是你升级、解锁、重新烧录、再升回Level 1这个过程耗时比直接烧录多不少。量产烧录场景下一般节奏是先用烧录器空白芯片状态写入Bootloader和App。写入后校验一遍CRC或SHA算法。最后把RDP切到Level 1或Level 2。再读一遍选项字节确认状态。这四步每一步都能用自动化脚本完成。CubeProgrampreter支持命令行批处理模式可以用cmd命令把option字节设置和下载动作串起来。5.3 保护状态下读UID和Flash校验千万别踩权限误区L4/G4每个芯片都有唯一UID这个UID本身并不受RDP保护在任何保护等级下都能用库函数读取。很多产品用UID做序列号或者加密绑定这个不会因为读保护而失效。但有一点要留意如果你的保护等级是Level 1调试器无法直接读Flash但你的代码可以正常读。这意味着你可以在固件里做一个“运行时自校验”——启动时计算Flash关键区域的CRC和出厂时存在OTP或Flash尾部的校验值比对如果发现被覆盖或者被篡改就直接进入错误处理流程。这在量产保护里是很实用的一招成本低效果直观比单纯依赖RDP更抗物理拆解。6. 我踩过的三个坑写出来给你当路标6.1 第一个坑Keil自动解锁把固件清了这大概是所有玩STM32的人都会遇到一次的经典事故。当时我拿到一块L4板子前任工程师已经把RDP升到Level 1但我不知道。我打开Keil点了一下Download调试器探测到芯片受保护弹出提示问是否解锁。我手一滑点了Yes紧接着全片擦除固件彻底没了而且芯片里原本烧好的校准参数也没了。从那以后我坚持在项目根目录放一个README里面写明白“当前板级保护等级”并且在CubeProgrammer里把Option Bytes的状态截图归档。操作任何板子前第一件事不是下载而是先读一下选项字节确认对方当前在哪个Level。6.2 第二个坑PCROP区域边界算错启动即HardFault这个坑前面提过值得再单独强调一次。L4系列的Flash页大小可能是2KB、4KB或者更大PCROP边界按页对齐而不是按字节对齐。我有一版算法函数开头正好落在页起始偏移0x800处我把边界写到实际函数地址而不是页首地址结果保护区域把启动向量表所在的扇区盖进去了一半。复位后负责挑转函数调用的代码进了保护区自身却可执行但保护区外的函数跳转过来时触发总线错误MCU一直HardFault。解决办法是明确记录保护区段地址PCROP Start地址填写第一页页首地址PCROP End地址填写最后页页尾地址杜绝“大概差不多的地址”。6.3 第三个坑PCROP区域里的常量表被DMA当成普通数据访问PCROP真正防的是非法总线访问我排查过一个诡异现象系统运行一段时间后串口数据偶发异常后来抓逻辑分析仪发现是一个DMA通道错误地把PCROP区域地址当成内存地址源读操作被硬件总线层拒绝然后触发了DMA传输错误中断。表面上看这是优化问题实际根因是代码里写了“内存到内存”的DMA传输把函数指针强行当作源地址。PCROP不是洪水猛兽但它确实会改变总线权限边界。凡是启用了PCROP的工程我建议在代码评审阶段重点审查所有DMA描述符的源地址确保它们不会指向保护区。这个低成本检查能省掉后面几周的抓狂调试。限制要写就写彻底Level 1加PCROP组合拳已经能挡住绝大多数常规破解手段。再往上走Level 2就得先想清楚升级路怎么留。毕竟保护的目的从来都是让产品在被攻击时多一道门槛而不是把自己的维护通道也一起焊死。