STM32 实战复盘:一次烧录验证串起的整条工具链
STM32 实战复盘:一次烧录验证串起的整条工具链
新电脑部署完 STM32 开发环境(过程见《换机不翻车:嵌入式开发环境整机迁移复盘》),光"编译通过"还不算完——得拿一块真板子,从编译、烧录到串口输出完整跑一遍才算数。这次用洋桃 1 号开发板(STM32F103C8T6)自带的 48_CHIPID 模块做端到端验证,一路踩了三个坑,也顺手把整条工具链的验证做扎实了。
#一、连接验证:CubeProgrammer 探测链路
烧录之前,先确认"线是通的"。打开 STM32CubeProgrammer v2.23.0,连上 ST-Link 点 Probe,结果全部正常:
| 探测项 | 结果 | 说明 |
|---|---|---|
| ST-LINK SN | 37FF7106... | 调试器序列号(前缀) |
| 固件 | V2J37S7 | ST-LINK 固件版本 |
| 目标电压 | 3.24V | SWD 供电正常 |
| Device ID | 0x410 | STM32F103 中容量(Medium-density)器件 |
| SWD 频率 | 4MHz | 调试时钟 |
目标电压 3.24V 说明 SWD 供电正常,Device ID 0x410 是 STM32F103 中容量器件的标准 ID。调试器、驱动、接线、供电这一段全绿——坑都不在连接层,全在后面。
#二、坑一:中文路径让 CubeProgrammer 找不到 hex
第一个坑来得毫无悬念。hex 文件放在中文目录(例如 D:\嵌入式\烧录\)下,CubeProgrammer 加载时直接报错:
txtFile does not exist: 48_CHIPID.hex
文件明明就在那里,路径一个字符不差。这不是 CubeProgrammer 一家的毛病——之前 GNU ld 链接器也不认中文路径,同款问题同款结局:嵌入式工具链里一堆老工具对非 ASCII 路径的支持约等于零。
解法:不跟工具较劲,把 hex 拷到纯 ASCII 路径(比如 C:\stm32_tmp\)再加载烧录,一次通过。
一句话经验:给嵌入式工具链喂的任何文件,路径一律纯 ASCII,能省下无数排查时间。
#三、坑二:改一个字符拿烧录成功的铁证
第二个坑更隐蔽:这块板子出厂就带着旧程序,OLED 显示的内容和"我以为刚烧进去的程序"看起来一模一样。CubeProgrammer 说烧录 OK,可万一烧进去的是旧 hex 呢?板子上看得见的变化,才是能让人睡踏实的证据。
解法:从源码下手,把 OLED 显示的字符串从 "CHIP ID TEST1" 改成 "TEST2",重新编译烧录。烧完 OLED 显示 TEST2——铁证:板子上跑的就是这次新编出来的程序。
编译没有点 GUI,直接用 Keil UV4 命令行:
txtUV4.exe -b demo48_CHIPID.uvproj -j0 -o build.log
-b 批量构建,-j0 不弹 GUI 窗口,-o 把编译输出写进 build.log,编完翻日志确认 error/warning 即可。
这里有个值得复用的工程思路:改一个字符,验证整条链路。完整链条是"源码 → Keil 编译 → hex → CubeProgrammer 烧录 → Flash → 复位运行 → OLED 显示",中间任何一环断了,TEST2 都出不来。挑一个链条末端、肉眼可见、且只可能由新程序产生的信号当验证标志,比盯着工具日志踏实得多。
#四、坑三:GBK 源码用 GB18030 读写保编码
第三个坑在源码本身:洋桃这套例程是 GBK 编码。如果用默认按 UTF-8 处理的编辑器或脚本直接改文件,保存的瞬间中文注释就变乱码,Keil 里打开一团糟。
解法:用脚本改源码时显式按 GB18030 读写(.NET 里 Encoding.GetEncoding("GB18030"),它是 GBK 的超集,向下兼容),改完编码不变,Keil 打开一切正常。
顺带记一笔:博客文件是 UTF-8,洋桃源码是 GBK,两套编码体系并存。处理哪套文件就用哪套编码,工具的默认值只能信一半,动手前先确认。
#五、实验升级:串口每 5 秒打印一次 ChipID
验证通过之后顺手做了个升级:原程序主循环 delay_ms(1000) 每秒打一次 ID,节奏太快不好观察,改成 delay_ms(5000),重编重烧。收串口这次不借助任何串口助手软件,直接用 PowerShell 的 System.IO.Ports.SerialPort 连 COM3、115200:
powershell$p = New-Object IO.Ports.SerialPort COM3,115200,None,8,One
$p.Open()
for(;;){ Start-Sleep -m 200; $p.ReadExisting() }
收到的输出:
txt[4s] ChipID: 0670FF54 54537851 87152711
[9s] ChipID: 0670FF54 54537851 87152711
[14s] ChipID: 0670FF54 54537851 87152711
三次输出间隔正好 5 秒,节奏完美;三次 UID 完全一致,稳定可靠。到这里,"写进去的程序"和"跑出来的行为"终于对上了。
#六、ChipID 知识点:96 位 UID 全球唯一
这次实验的主角 ChipID 值得单独记一笔。STM32 的 96 位 UID(Unique Device ID)出厂时固化在芯片里,全球唯一,F103 从固定地址(如 0x1FFFF7E8)连续读 3 个 32 位字就能拿到。四个典型用途:
- 产品防伪与授权:固件绑定 UID 做授权校验,克隆板跑不起来。
- 通信地址派生:多节点组网时用 UID 派生地址,省掉人工分配。
- 加密密钥种子:配合板载私密数据派生密钥,每台设备各不相同。
- 免贴标序列号:出厂不用人工贴标,读 UID 即可追溯。
还有一个反直觉的点:程序里出现 "chipID error!" 是正常的。代码里硬编码的比对 UID 是杜洋那块板子的(066EFF34 开头),你手里任何一块别的板子 UID 必然不同,比对自然失败——这是演示程序的"特性",不是 bug,换上自己板子的 UID 就不报了。
写完上面这段后,我们顺手把剧情走完了:把比对值改成自己板子的 UID(0x0670FF54 / 0x54537851 / 0x87152711),UV4 重编、CubeProgrammer 烧录、COM3 先开端口再让 ST-Link 复位,抓到开机瞬间——
text[ 0s] ChipID: 0670FF54 54537851 87152711
[ 1s] chipID OK!
[ 1s] ChipID: 0670FF54 54537851 87152711
[ 6s] ChipID: 0670FF54 54537851 87152711
这一下完成了防伪逻辑的最小闭环:程序只认原板 UID → 换成自己的 UID → 重新授权通过。把 UID 比对换成密钥校验,就是真实产品里的防克隆方案。另一个细节:抓启动打印要先开串口再复位芯片,顺序反了就只能看到主循环的重复输出。
#七、闭环复盘:整条工具链脚本化可复用
复盘到最后,这台新电脑的 STM32 工具链已经形成完整闭环:
- 编译:Keil 5.25.3.0 命令行,
UV4.exe -b 工程.uvproj -j0 -o build.log。 - 烧录:CubeProgrammer CLI,hex 从纯 ASCII 路径加载。
- 验证:PowerShell 串口直收,System.IO.Ports.SerialPort 打印输出。
三步各自独立、全部可脚本化,下次换任何一个工程,改一下工程名和 hex 路径就能整套复用。这次 48_CHIPID 验证的价值也在这里:环境部署的终点不是"软件都装好了",而是"从源码到串口输出,一条命令跑通"。
#八、实战延伸:用 UID 给固件上锁
防抄袭的本质是让固件离开这颗芯片就活不下去。按防护强度从低到高,有四档做法:
| 档位 | 做法 | 防住谁 | 防不住谁 |
|---|---|---|---|
| L1 裸比对 | if(ID==...) 不匹配就死循环 | 直接拷 hex 到别的板 | 会逆向的人(nop 掉跳转即可) |
| L2 密钥派生 | 固件不存明文密钥,用 UID 异或/运算出密钥去解密 flash 里的授权块,解错了进残废模式 | 拷贝固件 + 不会逆向的人 | 耐心逆向的人 |
| L3 自校验+读保护 | L2 基础上,运行时对代码区做 CRC 校验(种子含 UID),并开启 RDP 读保护禁止 SWD 读固件 | dump 固件者 + patch 校验分支者 | 芯片级攻击(F1 时代 RDP 有 glitch 风险) |
| L4 外挂安全芯片 | MCU 与 ATSHA204 类加密芯片做挑战-应答,密钥永不出芯片 | 几乎所有软件攻击 | 成本换来的 |
L2 的最小骨架(可直接加进 48_CHIPID 工程体会):
cu32 key[3]; // 用 UID 现场派生密钥,固件里不存明文
key[0] = ID[0] ^ 0xA5A53C3C; // 盐值自定
key[1] = ID[1] ^ 0x5A5AC3C3;
key[2] = ID[2] ^ 0xF0F00D0D;
// 授权块:量产前用上位机按同样算法加密魔数,烧到 flash 固定地址
// 固件上电解密,对不上就进残废模式
if (decrypt_magic(key) == 0x3C3CA5A5) {
run_normal();
} else {
while(1); // 残废:外设乱初始化 / 假数据 / 定期死机
}
量产授权流程三步:① 上位机读板子 UID → ② 按约定算法生成加密授权块 → ③ 烧到 flash 约定地址。固件全板通用,授权块每板唯一——这正是"授权烧录器"模式,产线一台工具全搞定。
诚实提醒:F103 是 2007 年的芯片,RDP 读保护有历史漏洞,高价值产品建议换 F4/F7 或外挂安全芯片;学习和中小商用,L2+RDP 已经能挡住 95% 的抄袭者。
#九、相关阅读
- 换机不翻车:嵌入式开发环境整机迁移复盘——本页验证所依赖的环境是怎么迁过来的
- 硬件 CRC 单元:一次实验看懂循环冗余校验
- 定时器与中断:1 秒心跳与五阶段状态机
#九、更新日志
| 日期 | 类型 | 更新内容 |
|---|---|---|
| 2026-09-15 | 新增 | 首次建档:CubeProgrammer 连接验证 → 中文路径/旧程序/GBK 编码三个坑 → 串口 5 秒打印 ChipID → 工具链闭环 |
| 2026-09-15 | 追加 | 补完反转:UID 换成自己板子后 chipID OK!,防伪最小闭环;附先开串口再复位的抓取技巧 |
| 2026-09-15 | 追加 | 新增第八节:UID 防抄袭四档方案(裸比对/密钥派生/自校验+读保护/安全芯片)、L2 最小代码与量产授权流程 |