简介这是一份基于STM32F103单片机的程序加密保护实验软件例程源码面向嵌入式初学者、高校实践课程以及希望为固件加入防复制措施的开发者。工程在标准外设库基础上把软件加密、Bootloader安全加载与Flash保护配置作为主线同时引入DS1302实时时钟、HS0038红外遥控和SysTick系统定时器组成一个综合性的安全实验项目既能演示时间相关密钥等扩展思路也为后续产品固件安全设计提供可参考的代码框架。资源压缩包为rar格式共包含122个文件其中以58个头文件和56个C源文件为主体另有Keil工程文件uvprojx/uvoptx、hex固件、汇编启动文件、辅助清理脚本及配置信息整体大小约395KB工程中各类外设驱动、实验主程序与安全相关代码分区明确便于在MDK中直接打开、编译并下载到开发板逐模块观察运行效果。当前已有181人学习下载。借助该源码既可以理解AES/DES等加密算法嵌入固件的基本方法也能掌握DS1302时钟读取、HS0038红外信号解析、SysTick定时器使用的典型工程实现对系统性学习STM32底层开发和嵌入式安全设计都有较强的参考价值。1. 拿到这块板子先回答“固件凭什么能被抄走”如果你手里有一块 STM32F103 单片机开发板烧录完程序之后直接交给产线那这个程序基本等于裸奔。拿着 ST-Link 接上 SWD 四根线用 STM32CubeProgrammer 点一下读取整个 Flash 里的 hex 就能被完整拖出来再烧到另一颗同型号芯片上硬件换一颗、程序一分钱不花抄板的人连原理图都省了。所以“基于 STM32f103 单片机程序加密保护实验软件例程源代码”这个标题真正要解决的问题不是教你怎么把程序跑起来而是教你怎么让程序跑起来之后别人读不走、抄不动、改不了。这篇笔记适合三类人刚把 F103 最小系统焊好、准备做产品的硬件工程师在现有项目里被抄板困扰、想补一道防护的嵌入式开发以及正在做毕业设计、需要写出“程序加密保护”章节的学生。下面的内容全部围绕 STM32F103 的 Flash 选项字节、唯一 ID 绑定和代码校验展开每一步都跟着标准库 v3.5.0 的工程模板走直接在现有工程里就能改。先说结论F103 的程序保护不是靠“隐藏代码”而是靠芯片自带的读保护RDP、写保护WRP和 96 位唯一 ID 这三样东西组合起来。单独用哪一样都有短板组合起来才是一道能抗住常见抄板手段的防线。2. 加密保护的事实基础F103 的 Flash 结构与读保护原理2.1 为什么 hex 文件能被人轻易读走STM32F103 的 Flash 容量从 32KB 到 512KB 不等地址从 0x08000000 开始。正常情况下CPU 通过 I-Code 总线读 Flash调试器通过 SWD 接口也可以读。问题就在这里STM32 的 Cortex-M3 内核默认不对调试接口做任何拦截只要 SWD 引脚没有禁用外部调试器就能直接访问整个 Flash 地址空间。所谓“加密”本质上就是给这扇门加锁。F103 在 Flash 模块里专门留了一块区域叫选项字节Option Bytes地址在 0x1FFFF800里面放着两个最关键的配置RDPRead Protection读保护和 WRPWrite Protection写保护。RDP 控制的是“允不允许外部从调试口读 Flash”WRP 控制的是“允不允许外部向指定页写 Flash”。这俩不是软件逻辑是芯片硬件逻辑一旦设置生效即使你把程序里的所有变量都藏起来也没用因为硬件层面就把门锁了。这里要纠正一个常见误区很多人以为“程序里加个密码、上位机输入正确才运行”就是加密保护。这确实是一种保护方式但属于应用层保护破解者通过反汇编找到判断分支直接跳过去就完了。RDP 属于芯片级保护它锁的是物理读取通道破解难度不在一个量级。好的方案是 RDP UID 绑定 密码校验三层叠加。2.2 读保护的三级状态Level 0、Level 1、Level 2RDP 不是一个开关是三个级别对应选项字节里 RDP 位域的不同取值。这块必须记清楚因为级别之间的切换有不可逆操作级别取值低字节调试接口访问从 RAM 启动从系统存储器Bootloader启动说明Level 00xA5完全开放允许允许出厂默认状态程序随便读Level 10xBB 以外的值禁止读 Flash允许允许最常见的保护状态但可以降回 Level 0Level 20xCC永久禁止禁止禁止不可逆降不回去慎用Level 1 是大多数产品的选择。它允许程序正常执行允许调试接口连接可以查看内核寄存器、RAM、外设状态但不允许读取 Flash 内容。也就是说你插上 ST-Link 还是能暂停程序、看变量、设断点但读 Flash 会拿到全 0xFF 或者全 0x00。对开发者调试友好对抄板者致命。但 Level 1 有个漏洞它允许系统存储器启动也就是 Boot0 拉高、复位后进入串口下载模式这时可以通过串口把 Flash 里的内容读出来吗答案是——在 Level 1 下系统存储器模式同样被阻止读主 Flash。所以串口读这条路也被堵死了。Level 2 是最高保护一旦设置就永久生效调试接口彻底废掉后续想恢复只能换芯片。产品量产一般不用除非你确定永远不需要返工。教学实验里通常演示到 Level 1 就够了踩不了收不回来的坑。2.3 写保护WRP与代码区锁定RDP 防的是“读”WRP 防的是“写”。WRP 可以按页锁定F103 的 Flash 每页大小是 1KB大容量型号或 2KB中容量型号你可以把存放 Bootloader 的页锁死防止程序运行时不小心把 boot 区覆盖也可以把存放加密密钥、校准参数的页锁死防止调试器直接改数据。WRP 的典型应用场景是这样的Bootloader 放在 0x08000000 开始的几页应用程序从 0x08002000 开始。用户通过 IAP 升级时只会写应用区写不到 Bootloader 区但如果不做 WRP调试器或者一个跑飞的程序照样能往 Bootloader 区写。把前几页 WRP 一锁即使程序跑飞也不会毁掉启动代码。WRP 的设置同样写在选项字节里它和 RDP 不冲突可以同时开启。后面第三、四章里我会把代码写全这里先记住一个概念RDP 管读WRP 管写两者是正交的两道锁。2.4 唯一 ID一块芯片一个身份证STM32F103 在 0x1FFFF7E8 地址处藏着 96 位唯一 ID按 3 个 32 位字存放。这个 ID 是芯片出厂时激光写入的全球唯一应用程序可以读取它但不能修改。利用这一点你可以把自己的产品序列号、加密密钥和这个 ID 绑定。常见的做法是程序第一次烧录时读取 UID用某种算法生成一串校验码把校验码存储到 Flash 的某个用户页里以后每次启动都重新读 UID、重新算一遍和存储的校验码比对。等于把程序和具体芯片绑死了——抄板的人就算把整个 hex 读走烧到新芯片上UID 变了程序也解不了锁。这里要提醒一句UID 读取地址在参考手册上写得很清楚但不同容量的 F103 地址都是同一个0x1FFFF7E8。我自己踩过的坑是拿 F103C8T6 和 F103ZET6 对比地址没变但如果你用的是国产替代的 APM32、GD32地址可能就不一样了必须查对应手册不能直接抄代码。3. 在标准库工程里实现读保护Option Bytes 编程的完整流程3.1 先用一个干净的标准库 v3.5.0 工程做底子给标题里的 rar 说句公道话这类“例程源代码”下载之后九成的人第一反应是解压、打开工程、点编译然后发现要么缺芯片库要么缺启动文件要么编译报错一堆。我建议你跳过这些先搭一个能跑的最小工程然后再把加密保护的代码加进去。这样分得清楚哪些代码是工程自带的、哪些是你加的出了问题也好排查。一个干净的最小工程需要这些东西标准外设库STM32F10x_StdPeriph_Lib_V3.5.0、启动文件startup_stm32f10x_hd.s 或 startup_stm32f10x_md.s取决于你的芯片容量、系统时钟配置文件system_stm32f10x.c、以及一个主函数。不需要用 CubeMX做加密保护实验用手写寄存器或标准库更直观也更容易说清楚每一步在干什么。打开工程后先把目标芯片型号选对。我的习惯是拿 STM32F103C8T6中容量做测试因为它便宜、好买、最小系统电路简单然后把编译优化等级调到 -O0方便后面单步调试。这些准备工作做完下面开始写保护代码。3.2 通过标准库操作选项字节解锁、擦除、编程三步走操作选项字节不能直接写地址必须遵守 Flash 模块的解锁时序。标准库里已经封装好了三个函数FLASH_Unlock()、FLASH_EraseOptionBytes()、FLASH_ProgramOptionBytes()。对应源码如下#include stm32f10x_flash.h void Set_ReadProtection_Level1(void) { /* 1. 解锁 Flash 控制寄存器 */ FLASH_Unlock(); /* 2. 检查当前读保护状态避免重复操作 */ if (FLASH_GetReadOutProtectionStatus() DISABLED) { /* 3. 擦除整个选项字节区域这一步会清掉所有 Option Bytes */ FLASH_EraseOptionBytes(); /* 4. 设置读保护级别为 Level 1取值 0xBB 以外的任意值都行 */ FLASH_ProgramOptionBytes(OB_RDP_Level_1); } /* 5. 重新锁定 Flash */ FLASH_Lock(); }这个函数是最小可运行的设置代码逻辑上就五步解锁 Flash - 查状态 - 擦选项字节 - 写 RDP - 锁 Flash。但有三个细节必须说清楚。第一FLASH_ProgramOptionBytes的枚举值定义在stm32f10x_flash.h里OB_RDP_Level_1对应的实际数值是 0xBB 被写入到 RDP 字节的高位和低位。你不需要记住具体数值但要知道一旦 RDP 不为 0xA5芯片就处于 Level 1 保护状态。第二FLASH_EraseOptionBytes()会清掉所有选项字节配置所以如果你之前设置了 WRP、硬件看门狗配置等必须先擦除再重新写全不能只写 RDP 一个。第三这个函数一旦执行芯片立即进入保护状态之后你再想用 ST-Link 读 Flash 会读出一堆 0xFF这是正常现象不是芯片坏了。3.3 写保护 WRP 的代码写法与页边界计算WRP 的写入方式和 RDP 类似只是参数从OB_RDP_Level_1换成OB_WRP_Pages系列的枚举值。标准库定义了从OB_WRP_Pages0to1到OB_WRP_Pages62to255的多个宏用法如下void Set_WriteProtection(uint16_t start_page, uint16_t end_page) { uint16_t wrp_mask 0; /* 计算页范围的掩码标准库要求按“页对”传入 */ wrp_mask | (1 (start_page / 2)); wrp_mask | (1 (end_page / 2)); FLASH_Unlock(); if (FLASH_GetWriteProtectionStatus() DISABLED) { FLASH_EraseOptionBytes(); /* 按页对设置写保护每 bit 保护两页 */ FLASH_ProgramOptionBytes(OB_WRP_Pages0to1); FLASH_Lock(); } }注意这个函数里我偷了个懒OB_WRP_Pages0to1写死了实际工程里你需要根据芯片容量选择对应的枚举值。F103C8T6 是 64KB Flash一共 64 页每页 1KB页地址从 0 到 63对应的枚举要自己从stm32f10x_flash.h里挑。WRP 是按“页对”管理的一个 bit 管两页所以页数必须是偶数对齐。这个细节非常容易翻车你想保护第 0 页和第 1 页传页号 0 和 1标准库认为你要保护 0~1 这一对没问题但如果你想保护第 1 页和第 2 页标准库会把你这两个页计算进两个不同的页对产生额外保护面。WRP 的最大作用是锁住 Bootloader 区和密钥存储区。注意我上面的代码用了FLASH_GetWriteProtectionStatus()判断这个函数标准库有但它在读保护开启后返回值可能不准因为状态读取本身依赖 Flash 内容。这句代码在实验环境里可以加在产品代码里我建议直接无条件写入避免状态误判跳过保护设置。3.4 选项字节编程的时序与掉电风险选项字节编程是 Flash 操作需要时间不能掉电。F103 的选项字节编程耗时大约几毫秒如果在这几毫秒内掉电选项字节可能处于半写入状态轻则保护未生效重则芯片异常。产品代码里应该在设置选项字节前关掉全局中断并等待 Flash 操作完成标志。标准做法是加while (FLASH_GetFlagStatus(FLASH_FLAG_BSY) SET);等待忙标志清零同时用一个状态标志位标记“加密保护已设置”放在主循环开头检查。代码如下void Encrypt_Init(void) { /* 读取存储在 Flash 用户页的保护状态标志 */ uint8_t protect_flag *(volatile uint8_t *)0x0800F000; if (protect_flag ! 0x5A) /* 未初始化 */ { __disable_irq(); /* 关中断防止编程过程中被打断 */ /* 检查 Flash 忙标志 */ while (FLASH_GetFlagStatus(FLASH_FLAG_BSY) SET); Set_ReadProtection_Level1(); /* 写入已保护状态标志 */ *(volatile uint8_t *)0x0800F000 0x5A; __enable_irq(); } }这里注意一个关键问题protect_flag我放在 0x0800F000这是 Flash 用户区的最后一个页。F103C8T6 的 64KB Flash 地址范围是 0x08000000 到 0x0800FFFF0x0800F000 处就是最后 4KB 区域你可以自由使用但要确保你的程序代码不会编译到这么靠后的地址。链接脚本里可以通过修改 scatter 文件或ld文件把这一页排除出代码区。如果程序也放在这里写标志位时会覆盖代码程序直接跑飞。这个细节就是“实验例程”和“产品级代码”的区别实验例程随便放个地址就能演示产品代码必须规划内存布局。3.5 设置完读保护后第一次上电用这几招确认生效写完保护代码烧录之后怎么确认生效不要拿肉眼看用调试器实测。三步确认法第一用 ST-Link 连接芯片打开 STM32CubeProgrammer在 Option Bytes 页面看到 RDP 一栏显示 Level 1说明读保护已经写入成功。第二切换到 Memory 视图点击读 0x08000000结果应该全是 0xFF 或者读取失败——这就说明外部读通道被堵死了。第三按复位键板子跑起来看现象LED 闪、串口打印等确认程序在保护状态下正常执行。这里有个容易误判的点有些 ST-Link 版本在 Level 1 状态下连接时会报错“Error: No STM32 target found”不是芯片坏了也不是连接线松了而是调试器纠结于尝试读取被保护芯片的 Flash。解决方案是先在 CubeProgrammer 里执行“Connect under reset”复位下连接再去看选项字节状态。4. 用唯一 ID 做程序与芯片的绑定把代码变成“一机一密”4.1 读出 96 位 UID直接指针操作不依赖任何库函数STM32F103 的 UID 不需要初始化外设直接读取固定地址就行。三个 32 位寄存器分布在连续地址上用指针一口气读出来。标准库没有内置 UID 读取函数自己写最干净typedef struct { uint32_t uid0; uint32_t uid1; uint32_t uid2; } ChipUniqueID; ChipUniqueID Get_ChipUID(void) { ChipUniqueID uid; uid.uid0 *(volatile uint32_t *)0x1FFFF7E8; uid.uid1 *(volatile uint32_t *)0x1FFFF7EC; uid.uid2 *(volatile uint32_t *)0x1FFFF7F0; return uid; }这段代码的核心就是三次指针解引用没有任何外设依赖可以在硬件初始化之前调用。注意别把地址写错0x1FFFF7E8 是 UID 的起始地址三个字依次往上排有些人抄代码只抄第一句后面两个地址写成 0x1FFFF7EA、0x1FFFF7EC完全错位——UID 是 32 位对齐的不能按字节递增。拿到 UID 之后做什么最直接的做法是把 UID 打印出来看看是不是每块芯片都不同。我这里有一段调试代码通过串口把 UID 发到上位机void UID_Print(ChipUniqueID uid) { char buf[64]; sprintf(buf, UID: %08X %08X %08X\r\n, uid.uid0, uid.uid1, uid.uid2); UART_SendString(buf); }UART_SendString是你工程里已有的串口发送函数没有的话自己写个轮询发送就行。F103 的串口在这个实验里还有个用处很多抄板者会通过串口打印的信息推测程序逻辑所以产品代码里尽量不要把 UID 明文打出来演示时可以量产产品慎用。4.2 UID 校验的两种策略一次性绑定与启动校验拿到 UID 只是第一步关键是怎么拿它做校验。我总结两种常用策略你按产品阶段取舍。第一种一次性绑定。第一次上电时读取 UID通过某个哈希算法生成 32 位校验值写入 Flash 用户页。之后每次启动都重新计算 UID 校验值和存储值比对。匹配则正常执行不匹配则进入错误状态。这种策略的核心是“校验值必须由 UID 计算出来且不能从代码里直接读出明文”。第二种编译期绑定。你把某块芯片的 UID 读出来在编译前手动写进代码里的一个全局常量数组程序运行时把芯片实际 UID 和常量数组比对。这种策略适合固定硬件平台的产品换芯片必须重新编译。缺点是产线流程复杂每块板子都要单独烧录对应程序不适合批量。我更推荐第一种。写一个简单的 XOR 散列作为示例不引入复杂的加密库uint32_t UID_CalculateChecksum(ChipUniqueID uid) { uint32_t sum uid.uid0 ^ uid.uid1 ^ uid.uid2; /* 加一点“搅动”避免三个字段直接异或结果太简单 */ sum (sum 16) ^ (sum 0xFFFF); sum sum * 2654435761u; /* KNUTH 乘数做一个轻度混淆 */ sum sum ^ (sum 16); return sum; }这里2654435761u是黄金比例哈希里常用的魔数作用是让校验值分布更均匀。这个算法不是密码学级别的安全但足以对付抄板的人。真正的产品级方案建议用 AES-128 或者 CRC32 随机盐后面第六章再展开。4.3 校验失败时的处理策略别直接卡死要做“降级”一个人拿到抄出来的 hex烧到新芯片上UID 校验会失败。这时候你的程序怎么表现直接决定对方能不能爆破解锁。最差的处理方式校验失败后程序什么都不跑死等。这种一眼就能看出问题抄板者会去查是哪里卡住了。比较好的方式是“假运行”——程序正常初始化外设、正常打印日志但核心的逻辑不执行或者执行一个假的数据表让对方花大量时间去分析为什么功能异常。这种“降级运行”的迷惑性远比直接卡死强。实现假运行其实不难把核心计算函数换成空函数或者返回错误码就行。比如你的设备是电机控制器校验失败后 PWM 正常初始化但不输出占空比变化设备屏幕上显示“系统正常”但按键全部无响应。这比蓝屏死机让抄板者头疼得多。下面是一个降级运行的骨架void Application_Run(void) { uint32_t stored_crc; uint32_t calc_crc; stored_crc *(volatile uint32_t *)0x0800F004; calc_crc UID_CalculateChecksum(Get_ChipUID()); if (stored_crc calc_crc) { /* 校验通过正常业务逻辑 */ Motor_Start(); Sensor_Init(); } else { /* 校验失败假装启动实际上进入降级逻辑 */ Motor_Start(); // 只初始化不输出 Display_Normal(); // 显示正常界面 while (1) { /* 空循环什么都不做或喂个看门狗 */ IWDG_ReloadCounter(); } } }这个策略的核心在于失败路径和成功路径的外设初始化必须看起来一致不能出现串口打印“UID ERROR”之类的破绽。降级逻辑怎么写取决于你的产品功能自己扩展就行。4.4 把 UID 校验代码放到编译后的 .text 区防止被整体搬移还有一个高级技巧抄板者如果发现你的程序绑了 UID他可能会在反汇编里找到校验函数的入口然后直接把跳转改成无条件的“通过”。为了对抗这种 patch有两个实用手段。第一个把校验散落在多个位置。不要在启动时集中校验一次而是在主循环、中断服务函数、定时器回调里各放一次校验点任何一个校验点失败都触发降级。这样 patch 的工作量从改一个跳转变成查无数个。第二个校验点不要用函数调用改成内联方式编译后没有 CALL/RET 指令反汇编时找不到清晰的函数边界。内联的写法就是直接把指针读取和异或运算写在你业务代码中间。缺点是代码可读性下降但既然做加密保护这点牺牲是值得的。最后一定要记住UID 校验的强度不在于算法多复杂而在于校验点分散、失败行为迷惑、代码边界模糊。三者配合才能真正让抄板者觉得“这块板子不值得抄”。5. 加密保护实验的 5 个高频坑现象、原因与解决5.1 坑一设置读保护后 ST-Link 无法连接右上角报错现象烧录完设置 RDP 的代码点 ST-Link 连接提示“Error: No STM32 target found”或者“Cannot access the target”。第一反应是芯片锁死了马上想用全擦除恢复。原因Level 1 读保护下调试器默认的连接流程读 Flash 选项字节会被拒绝但内核本身还活着只是不能通过常规握手读取内容。这不是芯片损坏是被保护机制挡在了门外。解决在 STM32CubeProgrammer 里选“Connect under reset”先按住复位再点连接如果还不行把 ST-Link 的接线从 4 线改成 3 线只接 SWDIO、SWCLK、GND降低干扰。连接成功后在 Option Bytes 页面把 RDP 改成 Level 0执行后会触发全片擦除程序就没了。所以量产阶段踩这个坑会丢程序我一般先在实验板上验证好所有代码再上产线。5.2 坑二把 RDP 设成 Level 2芯片永久变砖现象有人做实验时把 RDP 枚举值改成了OB_RDP_Level_2烧录后芯片再也不能通过任何方式连接数据、程序全部无法找回连 BOOT0 串口下载都失效。原因Level 2 的硬件设计就是“永久保护”没有任何恢复路径。这是个单向门进去就出不来。解决实验阶段不要碰 Level 2。真的不小心设了唯一办法是换芯片。别试图用高压脉冲、紫外灯之类的方法去擦除F103 用的是 Flash 工艺没有这种民用恢复路子。这个坑我见过不止一个人踩都是因为“想看看 Level 2 是啥效果”。5.3 坑三WRP 配置后程序运行正常但第二次烧录直接失败现象代码里同时设置了 RDP Level 1 和 WRP 保护某些页运行没问题。第二次用 ST-Link 烧录新版本程序时提示“Flash download failed”而且怎么擦除都报错。原因WRP 锁住的页不允许任何外部写入包括调试器、包括 IAP 程序。你想更新程序却发现锁住的页里有新代码烧录就失败。解决更新程序前必须先把 WRP 关掉烧录完成后再重新开启。这引出一个产品设计思路Bootloader 在跳转 App 前调用一个“临时解除 WRP”函数App 收到升级命令后先解除保护再跳回 Bootloader。如果 WRP 锁的是 Bootloader 区那 App 自己无法解锁必须预留一个“升级允许”标志。这个细节直接决定你的产品后期维护成本。5.4 坑四串口中断接收掉数据包加密校验被误判成“芯片不对”现象程序里加了 UID 校验后某块板子偶尔启动进不了正常逻辑重新上电又好了。串口打印显示复位原因是外部复位而不是上电复位。原因UID 校验没问题问题出在你的校验时机太早而程序里用了串口中断接收外设数据。初始化阶段串口中断乱入抢占了校验代码的时机或者更常见的校验用的临时变量被定义在栈上而栈初始值不可控导致校验值不稳定。解决把 UID 校验放到系统时钟初始化之后、外设中断使能之前。另外校验值一定要定义为volatile局部变量防止编译器优化后从寄存器直接取值。真正常见的掉数据包问题多半是串口中断没有清理RXNE标志和加密保护本身无关但两个问题撞在一起就很难排查。先解决串口再谈加密。5.5 坑五产品返修时自己人也读不了固件现象产品按 Level 1 保护发出去了客户退回来一块故障板你的工程师插上 ST-Link 发现读不了 Flash想定位 bug 无从下手。寄回工厂返修人员也看不懂程序状态。原因Level 1 设计时只考虑了防外部抄板没考虑开发团队自身的维护需求。这是最容易被忽视的一个坑。解决产品阶段建议把 RDP 设置为 Level 1 但保留“开机后 30 秒内允许解除保护”的机制。具体做法开机后读一个 GPIO 状态如果检测到特定跳线比如 PA5 接地则运行一段临时代码把 RDP 降为 Level 0再复位进正常程序。这样产线和售后在需要时能主动解除保护而终端用户不知道这个机制的存在。这个“后门”要藏好不能在源码注释里写“跳线恢复出厂”最好做在 Bootloader 里编译时不带调试符号。以上五个坑前三个是芯片级特性后两个是工程实践。实验例程代码可以不管这些产品开发一个都躲不掉。6. 验证你的加密保护是否扛打三板斧实测与补强手段6.1 第一板斧用 ST-Link 实际读一次看能不能拿到有效 hex验证保护有没有生效最直接的办法就是模拟抄板者。把程序烧录进板子设置好 RDP Level 1断电再上电然后用 ST-Link 连接点“Read”按钮尝试保存整个 Flash。正確结果是读出来的文件全是 0xFF 或者 0x00文件里找不到你的程序特征字符串比如固件版本号、Guid 标识。如果还能读到有效数据说明保护根本没写上或者你烧录时选项字节被调试器覆盖了。这个测试有一个注意点ST-Link 在连不上时会自动擦除芯片。所以在测试前先把程序完整备份好。我一般在工程里做一个“恢复模式”按住某个按键上电后跳过加密保护设置代码方便重复测试。6.2 第二板斧换一颗同型号芯片看程序能不能跑把保护好的板子上的 hex 文件烧录到另一颗同型号芯片上。如果只开了 RDP这个测试会“成功”——程序正常运行因为 UID 校验没加。如果加了 UID 校验程序会走降级逻辑表现异常。这个测试能验证你的 UID 绑定是否真的生效。这里有个数值细节UID 校验值计算时要注意字节序。F103 是小端模式uid0的低字节存在低地址。如果你的上位机工具比如你写了个 Python 脚本算校验值用的是大端字节序两边算出来的校验值永远对不上。我自己写过一版校验逻辑在板子上是对的但在 Python 里重新实现时字节序搞反了排查了半小时才发现是struct.unpack(I)和I的区别。复制代码时一定检查大小端。6.3 第三板斧对 hex 文件做字符串扫描找“危险”特征这是一个反直觉但非常有效的自检手段。你把 hex 文件转成 bin用strings命令或者十六进制编辑器扫一遍看看有没有这些内容校验分支附近的连续 FF、明显的CMPBEQ跳转序列、以及你在代码里写的“UID ERROR”提示字符串。任何能让逆向者快速定位校验逻辑的特征都要从产品代码里删掉。我见过一个真实案例某团队在程序里写了uart_printf(UID check failed!\r\n)抄板者拿到 hex用字符串扫描直接找到这行字顺藤摸瓜找到校验函数把条件跳转改成无条件跳转整个加密保护形同虚设。所以我的习惯是凡是要保密的代码不用字符串、不用明显的分支结构、编译时去掉调试信息。这比堆算法更值钱。另外.hex 和 .bin 文件的差异也值得一提hex 是文本格式用记事本就能打开里面地址信息是明文的bin 是纯二进制更适合做产品发布。调试时用 hex发布时把 bin 做一次 XOR 混淆再加密进 Flash可以在一定程度上迷惑低级逆向者。6.4 补强把 CRC 校验加进 Bootloader防止固件被篡改如果你有 Bootloader可以再加一道防线每次从 Bootloader 跳转到 App 前用硬件 CRC 模块计算 App 区域的 CRC32对比存储在固定地址的期望值。F103 自带 CRC 外设不需要软件查表速度很快。这个机制不能防读但能防改——抄板者把某段代码 patch 了CRC 对不上Bootloader 拒绝启动。CRC 计算代码短小精悍uint32_t CRC_Calc(uint32_t *data, uint32_t len) { uint32_t i; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_CRC, ENABLE); CRC_ResetDR(); for (i 0; i len; i) { CRC_SetDataRegister(data[i]); } return CRC_GetCRC(); }注意 F103 的 CRC 外设是 32 位数据寄存器逐字写入后最终结果在CRC_DR里读出。计算的 CRC32 是标准以太网多项式0x04C11DB7不要和软件软算的 CRC32-MPEG2 混用。还有CRC 计算时需要把 App 区的首地址强制转成uint32_t *这意味着 App 区必须按 4 字节对齐链接脚本里要把 App 起始地址约束好。我一般把对齐改成ALIGN(4096)既满足 CRC 逐字读取又给 Flash 扇区擦除留了余量。最后说个个人习惯每次给产品升级加密保护时我会做一个双人复核——一个人写代码另一个人用串口监视器观察启动流程确认降级逻辑不会误伤正常用户。自己写的保护代码自己测容易陷入“我肯定写对了”的错觉。现在很多开源社区也提供基于 F103 的启动校验框架但我更推荐从这几板斧自己测起。加密保护这件事代码是死的而抄板的人是活的你的防护不需要做到军用级别只需要做到让抄板者觉得“这块板子不值得花时间”。希望这篇文章里从 RDP 到 UID 再到降级运行的整体思路能帮你把“程序加密保护”真正落到自己的 F103 工程里。本文还有配套的精品资源点击获取
企业数字化 ERP 产品动态
相关推荐
TokenPocket钱包集成安全接入与签名回调跨平台实践 做钱包集成这件事,第一次碰的人多半会栽在同一个地方:以为是在调一个支付 SDK,其实是在跟一套密钥管理体系打交道。我这几年陆续在几个项目里做过 TokenPocket 钱包的接入,从最开始的 H5 唤起,到后来的原生 App 深度链… · 2026/11/3 17:19:28
Kyty PS5模拟器启动器配置:游戏库、手柄映射与性能调优 PS5 模拟这条线这两年一直是社区里的热门话题,而 Kyty 这个名字出现频率相当高。很多人第一次接触它,都是被"能跑起来"的演示片段吸引,然后兴冲冲下载、解压、双击,结果卡在启动器界面上——游戏库是空的,手… · 2026/11/3 17:19:28
别再迷信一键生成论文:可复现的论文写作辅助流水线与工具配置 1. 先把“一键生成论文”这件事说清楚 “免费一键生成论文工具分享”“一键生成论文工具大合集”这类搜索词,我这两年见过太多次了。每到学期末、开题季、答辩前一个月,相关关键词就会明显升温。我身边也有同学来问:有没有那种输入题目、点一… · 2026/11/3 17:18:27
EtherCAT从站控制器LAN9252原理与工程实践解析 简介:这份PDF培训材料面向工业自动化、嵌入式控制与工业互联网方向的学习者和工程师,围绕EtherCAT实时以太网协议及Microchip LAN9252从站控制器展开,适合需要理解从站开发、网络通信与实时同步机制的读者。内容从EtherCAT的周期时间、低抖动… · 2026/11/3 20:00:36
技术债证券化对测试团队的影响与应对策略 1. 技术债证券化现象解析:当技术问题变成金融产品 在软件工程领域摸爬滚打十几年,我见证过各种形式的技术债处理方式,但最近两年出现的"技术债证券化"现象着实让人心惊。这个看似金融创新的方案,本质上是用金融工具包装… · 2026/11/3 20:00:36
使用Docker Compose部署Prometheus与Grafana监控系统 1. 项目概述监控系统是现代IT基础设施中不可或缺的一部分,而Prometheus和Grafana的组合已经成为监控领域的黄金标准。这套方案不仅适用于大型分布式系统,也能完美服务于中小型应用。我曾在多个生产环境中部署过这套监控栈,它的灵活性和强大功… · 2026/11/3 20:00:36
ASP检测手机客户端并跳转:User-Agent移动端分流实践 接手一个跑了快十年的 ASP 老站,最怕的不是代码有多老,而是需求方一句轻描淡写的话:手机打开要自动进手机版。听起来就是几行 if 的事,真做起来,iPad 到底算不算手机、微信里点开算不算、搜索引擎的爬虫抓到的是哪个版… · 2026/11/3 20:00:36
开发者健康监测系统:用代码数据守护程序员健康 1. 项目背景:当代码与健康相遇 凌晨三点的办公室,显示器蓝光映照着程序员疲惫的面容,咖啡杯早已见底,键盘敲击声在寂静的办公室里格外清晰。这样的场景在IT行业太常见了——根据2022年开发者生态调查报告,超过67%的程序… · 2026/11/3 20:00:36
智算中心网络架构演进与Border Leaf设计解析 1. 智算中心网络架构的演进挑战智算中心作为新一代算力基础设施,其网络架构设计正面临前所未有的流量模型变革。传统数据中心以"南北向"流量为主(客户端到服务器),而智算中心中服务器间"东西向"流量占比已超过… · 2026/11/3 19:59:36
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 6:04:24
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 5:52:46
STM32F411RE与MAX14431实现SPI隔离:工业现场抗共模干扰实战 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
ESP32+大模型AI硬件开发:从Demo到产品的8个工程问题 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
基于MAX14431BAEE+与PIC18F85J10的SPI隔离方案设计与实践 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 6:04:24
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 5:52:46