尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

从一行日志读懂STM32:RDP、CPUID与Bootloader版本全解析

从一行日志读懂STM32:RDP、CPUID与Bootloader版本全解析 从一段实测日志说起。前阵子整理量产板的测试脚本终端里刷出一行这样的输出STM32F030C8T6 - RDP: 0xA5 | CPUID: 0x410CC200 | Bootloader: V1.5乍一看这行字很简单做调试的人大概每天都在看但真让我把这几个字段逐个讲清楚、讲透很多人其实停在“见过但没深究”的状态。RDP是什么它和网上搜出来的“rdp wrapper”“远程桌面传文件”有没有关系CPUID为什么固定是0x410CC200Bootloader版本藏在哪里、怎么读最靠谱这篇文章就是围绕这一行日志展开的把三个信息拆开揉碎再讲怎么在应用代码和产测工具里把它们读出来。适合正在做产测固件、固件保护方案或者刚接触STM32读保护的朋友哪怕你之前没碰过寄存器操作跟着走一遍也能直接用起来。1. 先把标题拆开三个缩写背后是三类完全不同的信息1.1 RDP读保护等级芯片的“安全锁”RDP的全称是Readout Protection在STM32的语境里专指Flash读保护。它和远程桌面协议Remote Desktop Protocol缩写一样但完全是两码事。网上搜索“RDP”时跳出来的大多是rdp wrapper怎么装、怎么卸载、远程桌面怎么用xcopy传文件这一类内容几乎没有任何一条跟单片机有关。所以刚接触STM32的人特别容易被带偏搜半天搜不到正经资料。这事我放到后面专门细说这里先把RDP本身讲明白。STM32F0系列上的读保护一共有三个等级等级选项字节值行为与效果Level 00xA5无保护调试接口可正常访问Flash和SRAMLevel 1其他值最常用0xAC禁止调试接口读取Flash、SRAM和系统存储器但代码可以正常运行Level 20xCC永久保护调试接口彻底失效不可回退也不支持从RAM/系统存储器启动你可以把RDP理解成一把锁Level 0是没锁门谁都能进Level 1是锁了门但还留着钥匙想解除保护可以代价是全片擦除Level 2是直接把锁焊死物理上就不打算再开。产线上为保护固件不被抄板最常见的做法是把RDP设置到Level 1少数对安全性要求极高的场合直接上Level 2但Level 2之后连调试和升级都做不了必须提前想清楚。1.2 CPUIDCortex-M0内核的“身份证号”CPUID是ARM Cortex-M内核内置的一个只读寄存器固定地址在0xE000ED00不属于任何外设也不受RDP等级影响。它反映的是芯片里那个CPU内核的身份不是芯片型号。STM32F030C8T6用的是Cortex-M0内核所以读出来通常是0x410CC200。这个32位数值拆开来看每一位段都有固定含义[31:24] Implementer厂商编码0x41代表ARM[23:20] Variant内核变体通常为0[19:16] Architecture架构代号0xC对应ARMv6-M[15:4] PartNo内核编号0xC20是Cortex-M00xC60是Cortex-M0[3:0] Revision内核修订版本也就是说CPU内核的身份信息一次就读全了。这个寄存器对调试工具来说特别重要ST-Link连接目标板之后第一件事就是读CPUID确认内核架构再决定后续的调试命令集。你在STM32CubeProgrammer里看到的CPU ID就是这么来的。1.3 Bootloader version出厂固件的版本号每颗STM32出厂时芯片内部系统存储器System Memory里都固化了段一段Bootloader程序负责通过USART、USB DFU、CAN、SPI或I2C等外设接口执行下载和烧写。这段固件是出厂烧录的用户擦不掉也改不了。Bootloader的版本号就是这段出厂固件的版本标识通常用BCD编码表示V1.5对应0x15V3.1对应0x31。Bootloader版本看着不起眼但它能反映芯片的生产批次和引导程序的迭代情况。同一颗型号的芯片不同批次出厂的Bootloader版本可能不同部分老版本Bootloader对某些命令的支持有缺陷比如读保护开启后的命令响应异常。所以产线工具和升级工具里记录Bootloader版本是排查“芯片现象很奇怪但是代码又没问题”这类问题的重要依据。2. 用现成工具先读出这些值建立参照系2.1 STM32CubeProgrammer的读取操作动手写代码之前建议先用官方工具把这些信息读一遍建立直观参照。打开STM32CubeProgrammer选择ST-Link连接方式默认SWD点击Connect工具会自动执行复位和内核连接流程。连接成功后主界面的Target信息区域会显示一串信息关键是这几个Device ID0x440这是STM32F030x4/x6/x8/xC系列的标识CPU ID0x410CC200Cortex-M0内核RDP Level当前读保护等级会明确显示Level 0或Level 1右下角的Log窗口也会打印出连接摘要包含核心频率、Flash大小等信息。注意CubeProgrammer本身不一定直接显示Bootloader版本号想看完整的Bootloader信息要么走串口Bootloader协议主动查询要么在连接时结合芯片型号查对应手册。这一点很多人一开始会找半天找不到也不用奇怪不是操作问题是工具压根没把这项信息放到界面上。2.2 工具读数里的隐藏提示用CubeProgrammer还有一个容易被忽略的细节在RDP Level 1的情况下点击Connect之后工具不会立刻报错而是正常显示Device ID、CPU ID这些信息但一旦你尝试读取Flash就会弹出一个提示大意是当前读保护等级禁止读取Flash内容需要先设置Option Bytes解除保护。这个提示框非常关键点确认之后工具会执行全片擦除。如果你只是想确认芯片保护状态千万别手滑点错否则固件就没了。这一小段实操的意义在于先通过现成工具确认芯片状态后续在代码里读到对应值的时候你心里就有底了。比如工具显示RDP Level 0你再去读0x1FFFF800看到0xA5就知道读法是对的。2.3 不同芯片型号的信息差异需要提醒一句不同STM32系列的CPUID、Device ID和选项字节布局并不一样。比如F0系列的选项字节映射在0x1FFFF800但F1系列用户选项字节的映射地址是0x1FFFF800之外读保护等级的定义也不同。F0系列上Level 1的判定是“只要不是0xA5和0xCC就算Level 1”这个规则不能原样套到其他系列上。做跨平台工具的时候读RDP的逻辑要按芯片型号分分支不能一把梭。3. 应用代码里读RDP和CPUID寄存器级别的实现3.1 CPUID寄存器地址与位域解析读CPUID不需要初始化任何外设也没有先决条件在main函数最开始执行都行。直接定义一个指向0xE000ED00的指针取值即可。#include stdint.h #define CORTEX_M0_CPUID_ADDR 0xE000ED00UL typedef struct { uint8_t implementer; uint8_t variant; uint8_t architecture; uint16_t partno; uint8_t revision; } cpuid_info_t; static void cpuid_parse(uint32_t raw, cpuid_info_t *info) { info-implementer (raw 24) 0xFF; info-variant (raw 20) 0x0F; info-architecture (raw 16) 0x0F; info-partno (raw 4) 0xFFF; info-revision raw 0x0F; } void read_cpuid(void) { uint32_t raw *(volatile uint32_t *)CORTEX_M0_CPUID_ADDR; cpuid_info_t info; cpuid_parse(raw, info); // 判断是否为Cortex-M0 if (info.partno 0xC20 info.architecture 0xC) { // 符合预期内核 } }实测在STM32F030C8T6上读出来的原始值就是0x410CC200。如果某颗芯片读出来PartNo是0xC60说明它其实是Cortex-M0内核0xC24则是Cortex-M3。这类判断在产线防呆里很好用——防止把F030误识别成别的型号。因为我见过把STM32F103的板子接到F0专用测试治具上的情况接口和引脚定义都对得上就是内核不一样CPUID一比对当场就能拦住。3.2 RDP的两种读法哪种更实用读RDP有两个路径直接读选项字节或者读Flash接口的状态寄存器。直接读选项字节是最简单直接的路径。在STM32F0系列上用户选项字节映射在0x1FFFF800首字节就是RDP值。#define OPTION_BYTES_BASE 0x1FFFF800UL uint8_t read_rdp_level(void) { uint8_t rdp *(volatile uint8_t *)OPTION_BYTES_BASE; if (rdp 0xA5) { return 0; // Level 0 } if (rdp 0xCC) { return 2; // Level 2 } return 1; // Level 1 }这个读法最直接也是我产测代码里的首选能区分出三个等级。另一种读法是读FLASH_OBR寄存器地址是0x4002201C里面有一个RDPRT位能反映读保护是否开启但它只能区分“保护”和“未保护”区分不了Level 1和Level 2。#include stm32f0xx.h uint8_t read_rdp_from_obr(void) { uint32_t obr FLASH-OBR; return (obr FLASH_OBR_RDPRT) ? 1 : 0; }所以在只需要判断“有没有开保护”的场景里用FLASH_OBR省事在需要精确知道是Level 1还是Level 2的场景里直接读0x1FFFF800。注意Level 2下SWD调试器已经完全连不上了但芯片里的程序还能执行所以应用代码读0x1FFFF800拿到的依然是0xCC。这意味着如果你的产品设置成了Level 2后续想通过调试接口读取或升级都不可能只能通过预留的IAP通道或者串口Bootloader协议去处理。3.3 顺带把UID也读出来既然聊到了F030顺手把96位唯一ID一起读出来产测里经常和RDP、CPUID一起做校验。STM32F0系列的唯一ID寄存器基地址是0x1FFFF7AC一共12字节用任意方式读取都不会被读保护挡住。#define UID_BASE 0x1FFFF7ACUL void read_uid(uint8_t uid[12]) { for (int i 0; i 12; i) { uid[i] *(volatile uint8_t *)(UID_BASE i); } }UID的用途很实在批量生产时把芯片UID和固件绑定实现一机一码或者把UID记录在产测数据库里做追溯。RDP是锁UID是身份CPUID是内核证明这三个配合起来产测工具能做的校验就非常完整了。4. Bootloader version三条获取路径与适用场景4.1 路径一从系统存储器固定地址直接读Bootloader版本信息实际上也存在系统存储器里理论上有固定地址可以去读。对STM32F0系列来说Bootloader版本的具体存放位置在AN2606文档里有描述不同批次、不同子型号的地址可能不一样直接去读系统存储器的地址要格外小心。如果要从应用代码里直接读思路是这样的查找芯片对应型号的系统存储器基地址和Bootloader版本偏移然后解引用地址取值。uint8_t read_bootloader_version_from_sysmem(void) { // 注意不同F0子型号地址可能不同务必核对AN2606 // 这里的0x1FFFF7CC仅为示例不代表所有芯片都适用 return *(volatile uint8_t *)0x1FFFF7CC; }这个方案的麻烦点在于版本地址没有统一的规律必须在手册里逐个型号确认。而且Bootloader区域在用户程序崩溃或选项字节异常时可能读不到预期值甚至触发HardFault。所以我很少建议在应用代码里冒这个险除非你能确认目标芯片的版本地址并且做了异常处理。4.2 路径二走USART内置Bootloader协议最可靠的路径最靠谱的获取方式是通过芯片出厂自带的Bootloader去询问协议叫AN3155适用于STM32系统存储器Bootloader。思路很简单把BOOT0引脚拉高复位芯片让芯片进入System Bootloader模式然后通过USART发命令让它自己汇报版本。STM32F030C8T6的USART1映射在PA9/PA10PA9是TXPA10是RX。与Bootloader通信的典型流程拉高BOOT0复位让芯片进入系统存储器启动模式等待芯片在USART1上初始化发送同步字节0x7F芯片回ACK 0x79表示同步成功发送命令0x0AGet命令再发送反码0xF5芯片回ACK然后返回一包数据Bootloader版本号、支持的命令数量和命令列表发送和接收的示意代码void send_byte(uint8_t b) { // 发送一个字节到USART1阻塞等待发送完成 while (!(USART1-ISR USART_ISR_TXE)) { } USART1-TDR b; } uint8_t recv_byte(void) { while (!(USART1-ISR USART_ISR_RXNE)) { } return (uint8_t)USART1-RDR; } void get_bootloader_version(void) { // 同步 send_byte(0x7F); uint8_t ack recv_byte(); // 应为0x79 // Get命令 send_byte(0x0A); send_byte(0xF5); // 0x0A的反码 ack recv_byte(); // ACK uint8_t version recv_byte(); // 第一个字节是BCD版本号如0x15表示V1.5 // 后续还有命令数量、命令列表和校验和按需解析 }我用这个方式实际读过F030的版本信息返回的版本号格式和AN3155描述一致。这个方案最大的优点是不依赖调试接口在RDP Level 1甚至Level 2下都能正常工作只要芯片还能通过USART启动。所以产线测试里如果需求里明确要求读取Bootloader版本我都是直接走这条路。4.3 路径三通过调试工具的扩展命令像ST-Link、J-Link这一类调试器其实能通过底层的调试接口访问系统存储器。部分调试工具和上位机软件在特定模式下会尝试读取Bootloader版本字段但这不是ST官方标配功能支持程度因工具而异。我自己试过在OpenOCD脚本里手动读系统存储器区域能读但前期要配置好内存映射和地址范围而且读回来的数据还要自己拼版本号。对比一下三条路径的适用场景获取方式依赖条件RDP Level 1下可用实现成本可靠度直接读系统存储器应用代码可运行可以低但要查手册确认地址中地址因型号而异USART Bootloader协议BOOT0可拉高USART口可用可以中需要实现协议高官方协议调试工具扩展命令调试器支持不能高工具差异大中我的建议很明确做产测或量产工具时优先用USART Bootloader协议因为RDP等级再高都拦不住它而且协议是公开的标准写一遍能在F0全系列复用。5. 容易被带偏的两件事RDP歧义和等级1的读取边界5.1 搜“RDP”出来的全是远程桌面怎么搜才有效这是我见过最多人踩的初级坑。在搜索引擎里输入“RDP”排在前面的永远是rdp wrapper、rdp wrapper library、如何卸载rdp wrapper、远程桌面用xcopy传文件这类内容。这些东西跟单片机完全没关系但热度奇高直接淹没了STM32读保护的资料。想搜到正经的STM32 RDP内容关键要加限定词。我会用这样一组关键词组合STM32 RDP LevelSTM32 readout protectionSTM32 option bytes RDPSTM32F0 RDP 0xA5 0xCC“Readout Protection”这个完整拼写是最高效的检索词因为它在全网范围内的歧义最小。如果搜中文记得带上“读保护”三个字“STM32读保护等级”基本能命中。别偷懒只搜缩写否则结果页会让你怀疑人生。5.2 Level 1下哪些信息能读、哪些不能读读保护等级对调试访问的限制是很多人搞混的重灾区。做一个简洁的边界总结能读CPUID0xE000ED00、Device ID0x40015800、UID0x1FFFF7AC、选项字节0x1FFFF800、DBGMCU配置寄存器不能读Flash主存储区内容、SRAM内容、系统存储器内容能做复位目标板、连接调试器、擦除Flash擦除操作在解除读保护时是允许的所以Level 1下用CubeProgrammer连接界面照样显示CPU ID和RDP Level但一读Flash就报错这个现象不是芯片坏了是保护机制在工作。产线上如果发现某块板子“读不了程序”先看一眼RDP Level再下结论经常能省掉一整轮排查。另外还有个坑从Level 1回退到Level 0时芯片会强制执行全片Flash擦除这个擦除不区分用户代码区和选项字节区。所以千万不要在没备份固件的情况下随手把RDP从Level 1改成Level 0改完固件就没了。想要保留固件、还解除保护先通过调试器把Flash内容完整读出来备份。这个原则我写进了产线的作业指导书里就是因为实际发生过操作员随手改选项字节导致整批板子返工的事故。5.3 Level 2的特殊性Level 2的“永久保护”体现在两个层面一是调试接口SWD/JTAG彻底失效任何工具都无法连接二是芯片不再允许从RAM或系统存储器启动。这意味着Level 2之后连串口Bootloader都用不了芯片被锁定在只能执行用户Flash里代码的状态。这也是为什么Level 2一定要在产品规划的早期就决定好别指望后期再改。真到了需要返厂处理的那一步只能换芯片没有快捷通道。6. 实战把这三个信息用在一个产测小工具里6.1 产线接入的校验流程假设你是做产线治具的每天要测几百块F030C8T6的板子。每块板子刷完固件之后需要自动确认三件事芯片型号正确、读保护已按生产要求设置、固件烧录成功。这个场景下读取RDP、CPUID和Bootloader版本就不是可有可无了而是完整的校验步骤。一个可行的流程用ST-Link连接目标板读取CPUID解析PartNo和Architecture确认是Cortex-M0内核读取Device ID确认是0x440进一步确认是STM32F030系列读取UID记录到产测系统数据库读取RDP选项字节确认保护等级符合当前生产阶段的预期拉高BOOT0并复位走USART Bootloader协议读取Bootloader版本与数据库中的批次信息比对全部通过输出PASS任何一个不匹配输出具体错误码这个流程用上位机脚本或LabVIEW都能实现核心逻辑并不复杂但需要把每个字段的校验阈值配置好。比如CPUID直接比对0x410CC200Device ID比对0x440RDP等级根据工单要求动态配置。6.2 我在产测工具里踩过的几个坑第一个坑是连接阶段的RDP误弹窗。CubeProgrammer在检测到RDP Level 1时会弹出解除保护的确认框产线操作员不懂技术看到弹窗就点确定于是整片Flash被擦掉固件丢失。后来我们干脆不在产线上用CubeProgrammer的图形界面全部改成命令行调用或者写独立脚本把人为确认的环节从流程里去掉。第二个坑是读取Bootloader版本的端口占用。USART1的PA9/PA10如果被产品设计成外接功能口拉BOOT0进Bootloader时会和外部设备抢总线。解决方法是产测治具上给USART1单独留一路切换开关测Bootloader版本时把外设断开测完再切换回来。另外BOOT0的拉高时序要注意必须是先拉高BOOT0再复位顺序反了芯片会直接进正常用户程序。第三个坑是Level 2芯片在产线上的误判。如果某批产品已经烧死到Level 2SWD完全连不上ST-Link会报连接失败。产线人员第一反应是“芯片坏了”拆下来换新才发现是读保护导致的。后来我们给测试脚本加了一行错误码提示连接失败且目标电压正常时优先提示检查RDP状态这个提示至少减少了一半无效换料。6.3 把三个信息变成常态化的日志输出最后分享一个我一直在用的小习惯在产品固件的调试串口输出里开机阶段就把RDP等级、CPUID和Bootloader版本打印出来做成固定格式产测时PC端直接收日志解析。这样做的成本很低收益却很大——它把“芯片状态检查”从生产测试环节前置到了每台设备上电自检环节。一旦设备在客户现场出了问题远程拿到一份开机日志立刻就能判断是不是芯片保护状态、芯片型号或者Bootloader版本导致的异常不用再拆机接调试器。这个习惯帮我定位过好几次“同一批设备只有某个批次表现诡异”的问题追根溯源都是Bootloader版本批次差异属于典型的隐藏信息价值。
返回列表