
做过 STM32 启动流程相关工作的朋友应该都有过这种经历一块 STM32H573 板子上电后行为很怪你怀疑它压根没跑进自己的 App而是停在了 ST System Bootloader 里。烧录器连上看不到程序指针BOOT0 引脚量着又是对的于是来回复位、插拔线、甚至换芯片折腾半天。其实 H573 上是有办法在固件里直接判断“当前到底运行在哪段代码”的而且不只一种。这篇文章我把能落地的检测方法整理了一遍包括向量表判断、启动源寄存器判断、协议握手判断还附了可以直接改着用的 C 代码以及我在 H573 实际项目里踩过的几个坑。1. 在 H573 上为什么需要搞清楚“当前跑的是谁”1.1 一个把问题逼出来的现场我之前调试一块 H573 的工程App 里做了 IAP 升级功能需要跳到 ST 出厂自带的 System Bootloader让产线用串口下载固件。现象是跳转之后串口完全无响应设备像死了一样。我最初以为是跳转代码写错了反复检查函数指针、栈对齐、中断失能都没问题。后来加了一句日志在 main 函数最开头打印复位原因和 PC 的大致位置。结果发现设备根本没有跳进我们写的 App而是直接从系统 Bootloader 启动的。因为产线在上一轮测试里把选项字节里的启动源改成了 System Memory而我们的 App 跳转代码又假设“默认从 Main Flash 启动”两个逻辑互相打架设备每次上电都卡在 Bootloader 等待握手。从那以后我就在所有 H573 固件里加了一个“当前运行环境检测”专门回答一个问题现在运行的是我的 App还是 ST System Bootloader。1.2 H573 的启动链与 ST System Bootloader 的触发条件STM32H573 启动源不是只有 BOOT0 引脚决定的实际上它的启动路径比老型号复杂不少。H573 属于 STM32H5 系列复位后进入哪个存储器区域由 BOOT0 引脚和选项字节 nBOOT0、nBOOT1、nBOOT_SEL 共同决定。简单说nBOOT_SEL 为 0 时完全看 BOOT0 引脚电平nBOOT_SEL 为 1 时用户选项字节里的启动配置生效启动目标可能是 Main Flash、System Memory、SRAM在启用安全功能时还可能是 Secure Flash 等区域。其中 System Memory 就是 ST 出厂固化的 System Bootloader 所在区域在 STM32H573 上起始地址是 0x0FFE0000大小 64KB 左右。它是一段只读 ROM 代码上电后通过 UART、SPI、I2C、FDCAN、USB 等接口等待接收数据目的就是让你在没有烧录器的情况下往 Flash 里下载程序。问题在于很多新开发者以为 bootloader 问题是“下载软件才需要考虑的事情”但实际上只要选项字节被改过、Boot 引脚悬空不稳、或者 App 跳转操作失败H573 都可能直接跑进 System Bootloader。这个时候如果你不想拆机、不想用烧录器最好的办法就是让固件自己先认清楚自己所处的环境。1.3 检测结果能拿来做什么这个检测不是锦上添花如果做得好能在好几个场景里省下大量时间上电日志直接打印“当前是 Bootloader 环境”或“当前是 App 环境”调试时一眼看到状态如果设备意外进入 BootloaderApp 可以通过日志或 LED 提示操作者避免误认为硬件坏了在 IAP 流程里App 可以判断“自己是被用户从 Bootloader 切换回来的”从而跳过某些初始化在做产线自动化测试时通过检测结果决定下一步是发送固件还是进入自检流程。所以下面是重点用什么信号来判断。2. 能暴露运行环境的几个关键信号2.1 系统存储器地址是天然指纹最直观的思路是看代码当前运行在哪一段地址空间。STM32H573 的 Main Flash 应用一般在 0x08000000 开始SRAM 在 0x20000000 附近而 ST System Bootloader 位于 0x0FFE0000 开始的系统存储器区域。如果 PC、向量表地址或芯片内部某个寄存器指向了 0x0FFE0000 ~ 0x0FFEFFFF 这个范围那基本可以断定当前运行的是系统 Bootloader。但这个“地址指纹”不能只靠看 PC 来实现。App 里随便一个函数执行时 PC 当然在 Flash 区域这没法证明运行环境真正有意义的是看复位向量表或复位后的执行路径。2.2 SCB-VTOR向量表偏移寄存器Cortex-M33 内核里有一个寄存器叫 VTORVector Table Offset Register存放当前中断向量表的基地址。大多数 STM32 工程在启动初始化时会把 VTOR 设成 Flash 应用起始地址例如 0x08000000。而 ST System Bootloader 是独立的固件它有自己的一套启动流程不会使用你 App 的向量表。所以当你读取 SCB-VTOR 时如果它等于 0x08000000 附近说明当前大概率跑在 Main Flash App如果它落在 0x0FFE0000 附近说明当前很可能是 System Bootloader。这里要加个“大概率”因为有些 App 跑着跑着会把向量表改到 SRAM比如为了做运行时中断向量重映射。另外Bootloader 本身也不一定设置 VTOR有些 ROM 代码直接依赖 CPU 复位后的默认值 0。因此 VTOR 适合做第一层快速判断不能单独作为最终结论。2.3 RCC-CFGR 里的启动源编码STM32H5 系列在复位后会记录实际启动源这个信息在 RCC 的 CFGR 寄存器里对应一个叫 BOOT_MEM 的位域。通过读取这个字段可以知道这次上电是从 Main Flash、System Memory 还是其他区域启动的。我习惯把它当作“当前复位周期启动源”的证明。比如读到 BOOT_MEM 对应 System Memory 的编码那就说明复位后 CPU 拿到了 System Bootloader 的入口地址。这个值和 VTOR 配合起来基本能锁定结论。需要强调具体位域位置和编码值不同头文件、不同参考手册可能差一点写代码时一定要去查你手上的 RM0481 和设备 SVD 文件不要拿网上老旧 STM32F1/F4 的寄存器定义硬套。2.4 bootloader 协议握手还有一个更“行为级”的检测方法主动向芯片外设发送 ST System Bootloader 的握手命令看它是否返回 ACK。ST 系统 Bootloader 常见的 USART 协议里0x7F 是同步命令如果设备正处于 Bootloader 并成功识别会回应 0x79。这个方法的好处是不用关心地址映射、不用访问可能被保护的 System Memory 区域只要 Bootloader 在用某一个串口等待命令你发一个字节就能测出来。缺点也很明显需要占用一个外设接口而且如果设备当前跑的是 App这个额外发送可能会被 App 误当成业务数据。2.5 方法对比我把三种主流方法列个表方便你在工程里选型方法判断依据侵入性可靠性适用场景VTOR 地址判断向量表基地址范围无只读寄存器中App 可能改 VTORApp 启动早期快速初判RCC 启动源判断BOOT_MEM 位域编码无只读寄存器高反映本次复位启动源复位后尽早执行协议握手外设总线收到 0x79 ACK需占用外设并主动发送高但可能干扰业务数据无法访问寄存器/安全隔离时实际项目里我建议至少同时用前两个而不是只依赖某一个。后面代码部分就是按这个组合思路写的。3. 可落地的检测代码与组合判断3.1 用 VTOR 判断的代码先给一个最简单的 VTOR 范围判断。这段代码依赖 CMSIS可以直接放到 H573 工程里。#include stm32h5xx.h #define ST_SYSMEM_BASE 0x0FFE0000u #define ST_SYSMEM_END 0x0FFEFFFFu static int is_in_st_system_bootloader_by_vtor(void) { uint32_t vtor SCB-VTOR; if ((vtor ST_SYSMEM_BASE) (vtor ST_SYSMEM_END)) { return 1; } return 0; }如果你用的是 STM32CubeMX 生成的代码通常在 SystemInit 里会给你执行一行SCB-VTOR FLASH_BASE所以在 App 启动后 VTOR 大概率等于 0x08000000。如果这个函数返回 1基本可以肯定当前不是你的 App。这里有个细节SCB-VTOR 在复位后可能为 0而 0 地址在多个启动源之间是重映射的。所以如果你想更严谨一些当 VTOR 等于 0 时还应该继续读 RCC 的启动源字段而不是直接下结论。3.2 用 RCC 启动源编码判断的代码下面这段代码是读取 RCC-CFGR 里 BOOT_MEM 的位域然后判断它是否等于 System Memory 启动编码。注意我用了宏占位实际值请根据 RM0481 和你的头文件替换。/* 请根据实际 SVD 或参考手册确认以下定义 */ #define RCC_CFGR_BOOT_MEM_Pos (26u) #define RCC_CFGR_BOOT_MEM_Msk (0x3u RCC_CFGR_BOOT_MEM_Pos) #define RCC_CFGR_BOOT_MEM_SYS (0x1u RCC_CFGR_BOOT_MEM_Pos) static int is_in_st_system_bootloader_by_bootmem(void) { uint32_t boot_mem (RCC-CFGR RCC_CFGR_BOOT_MEM_Msk); if (boot_mem RCC_CFGR_BOOT_MEM_SYS) { return 1; } return 0; }读取时机很关键这个字段是复位时锁存下来的虽然名字叫 CFGR但它不是给你随意拿来存业务状态的地方。我习惯在 main 函数第一行就采样最多放到启动汇编之后、任何外设初始化之前。如果拖到 HAL 初始化最后再读某些库代码可能已经改写了 CFGR 的相关位虽然这种情况不常见但没必要冒险。3.3 用 UART 协议探测的代码当寄存器访问可能被安全机制拦截或者你想通过“现场探测”确认 bootloader 是否真的在某个串口上等待命令可以用握手方式。下面用 HAL 库写一个最小例子。#include stm32h5xx_hal.h static int probe_st_bootloader_uart(UART_HandleTypeDef *huart) { uint8_t cmd 0x7F; uint8_t ack 0; /* 发送之前记得清空接收缓冲避免读到脏数据 */ __HAL_UART_CLEAR_OREFLAG(huart); if (HAL_UART_Transmit(huart, cmd, 1, 50) ! HAL_OK) { return 0; } if (HAL_UART_Receive(huart, ack, 1, 50) ! HAL_OK) { return 0; } return (ack 0x79); }看起来简单实际使用时要注意几点。第一Bootloader 的 USART 默认格式通常不是 8-N-1AN3155 里明确约定是 8 位数据、偶校验、1 个停止位也就是 8E1。如果你把串口配成了 8N1很多时候也能通因为握手字节 0x7F 恰好避开校验位敏感的临界情况但为了稳妥建议按照 AN3155 配。第二H573 的 bootloader 可能支持自动波特率检测但你要保证起始波特率在一个合理范围比如 9600 到 115200 之间过低或过高都容易失败。第三如果你在 App 里跑这个探测发送的 0x7F 会被 App 自己的串口接收逻辑当成普通数据所以这个函数只建议在启动早期或进入 bootloader 管理模式时使用。3.4 把三者组合成一个函数单独用一种方法都有盲区我最终的组合判断逻辑是这样先看 VTOR如果命中系统存储器范围直接返回 Bootloader如果没命中再看 RCC 的 BOOT_MEM如果前两个都不能确认最后用串口探测兜底。typedef enum { RUN_ENV_APP_FLASH 0, RUN_ENV_ST_BOOTLOADER, RUN_ENV_UNKNOWN } run_env_t; run_env_t detect_run_environment(UART_HandleTypeDef *optional_uart) { /* 第一层向量表地址 */ if (is_in_st_system_bootloader_by_vtor()) { return RUN_ENV_ST_BOOTLOADER; } /* 第二层复位启动源 */ if (is_in_st_system_bootloader_by_bootmem()) { return RUN_ENV_ST_BOOTLOADER; } /* 第三层行为探测。如果你不想占用串口可以跳过这一层 */ if (optional_uart ! NULL) { if (probe_st_bootloader_uart(optional_uart)) { return RUN_ENV_ST_BOOTLOADER; } } return RUN_ENV_UNKNOWN; }返回 RUN_ENV_UNKNOWN 不代表不是 Bootloader它只是说你没拿到足够证据。工程上我通常会把 UNKNOWN 当作 App 处理因为继续执行业务逻辑总比卡死强但不做任何会破坏 Bootloader 状态的操作比如不擦写整个 Flash。4. 边界情况与我在实际项目里踩过的坑4.1 VTOR 不一定给你预期的值我第一次在 H573 上跑 VTOR 判断就遇到一个恶心问题设备明明在系统 Bootloader但 SCB-VTOR 读回来是 0而不是期待的 0x0FFE0000。原因也很简单ROM 里的 Bootloader 根本不需要像 App 那样执行 SystemInit 去设置 VTOR它直接用默认的 0 地址映射。CPU 通过地址重映射把 System Memory 映射到 0 地址所以向量表实际基地址在逻辑上可以认为是 0。如果你只写了 3.1 的代码就会漏判。这也是我坚持要加 RCC BOOT_MEM 判断的原因。反过来有些 App 跑起来之后把 VTOR 改到 SRAM比如为了给 USB 或者 RTOS 做向量表重定向这个时候 VTOR 既不是 0x08000000也不是 0x0FFE0000单看它同样没法判断。所以把 VTOR 当作第一层过滤没问题绝不能当作唯一证据。4.2 BOOT_MEM 的读取时机比你想的更敏感BOOT_MEM 记录的是本次复位选择的启动源但它本质上是个状态寄存器不是保险箱。HAL 库在时钟初始化过程中会对 RCC 寄存器做很多配置操作某些底层代码可能会把 CFGR 重新写入。虽然不是每个工程都会中招但我在 H573 加 MCO 时钟输出时调了 RCC 的 MCO 选择位居然把 BOOT_MEM 字段覆盖掉了。所以正确姿势是在 main 函数最开头甚至是在编译器初始化全局变量之前先把 RCC-CFGR 和 SCB-VTOR 读出来存到两个静态变量里。后面所有检测逻辑都用这两个快照而不是运行时反复读寄存器。这一步能避免非常多诡异的“偶尔有效、偶尔无效”现象。static uint32_t g_boot_cfgr_snapshot; static uint32_t g_vtor_snapshot; void run_env_capture_early(void) { g_boot_cfgr_snapshot RCC-CFGR; g_vtor_snapshot SCB-VTOR; }然后所有判断函数都基于快照实现。这个习惯后来帮我排查过好几类诡异问题强烈推荐。4.3 TrustZone 和 Secure Manager 会改变访问权限STM32H573 是带 TrustZone 的型号如果你启用了 Secure Manager或者启用了 Secure Firewall情况会变得复杂。非安全世界的代码去读取 0x0FFE0000 区域的向量表或者访问某些与安全相关的 RCC 位可能会直接触发 HardFault。我遇到过一台启用 Secure Boot 的 H573App 跑在 Non-Secure 侧我按老办法在 App 里直接读 0x0FFE0000 的栈顶值做比对结果一读就进 HardFault。后来排查才发现是 Secure Firewall 把 System Memory 区域设为安全属性了。应对方案有两个把检测函数放到 Secure 侧由 Secure 代码读寄存器再通过安全 IPC 把结果告诉 Non-Secure App放弃直接读地址改用协议握手法因为 Bootloader 的串口通信本身不要求你直接读 System Memory 地址。如果你做的项目默认不开 TrustZone这个坑可能遇不到。但只要产品涉及安全启动、代码保护就必须把它纳入设计考虑。4.4 协议探测会遇到的误判协议握手看起来最无脑实际坑也不少。H573 的 Bootloader 支持的串口引脚不是随便哪个串口都行具体哪些 USART、映射到哪些引脚得看 AN2606。我最初随便选了一个 USART3 去做探测结果 Bootloader 根本不监听那个串口发多少次 0x7F 都不会有反应。后来换到 AN2606 里明确列出的 USART1才正常。注意H573 的 Bootloader 文档里给出的引脚映射可能和你在 CubeMX 里默认配置的不一样要仔细对照表格尤其是 AF 复用编号。另一个坑是校准波特率。有些 STM32 的旧 Bootloader 要求主机先按特定波特率发同步字节然后 Bootloader 自动适配。H573 的 Bootloader 芯片版本比较新支持自动波特率检测但如果你发送端的精度太差或者线缆太长导致信号畸变握手一样失败。我后来在产线上加了回环测试先自己发一个字节再接收确认线路没问题才把串口探测的误判概率压下去。另外探测返回的 0x1F 是 NACK它代表设备在串口上收到了数据但数据不是 Bootloader 约定的同步字节或者当前状态不接受这个命令。如果你收到 0x1F至少说明这个串口通了只是协议状态不对可以继续排查波特率或引脚配置。5. 把检测逻辑放进工程后怎么用最顺手5.1 启动日志与运行环境标注我在 H573 固件的启动早期会做这样一件事把检测结果拼进日志头这样无论设备在什么状态下启动调试串口第一行就会明确告诉你它现在在哪。[BOOT] run env ST SYSTEM BOOTLOADER [BOOT] run env APP FLASH (0x08000000)这行日志价值很大尤其在远程调试或者产线分析时不需要猜不用连烧录器一条串口线就能定位问题。5.2 与 IAP 流程配合备份寄存器记录跳转意图如果你自己实现了跳转到 ST System Bootloader比如把函数指针指向 0x0FFE0000 后复位那么建议在跳转前往备份寄存器里写一个 magic 值。等 Bootloader 完成工作、系统复位回到你的 App 后启动早期读取备份寄存器如果 magic 值匹配就说明“刚才那次启动是从 Bootloader 切换回来的”。这个技巧和运行环境检测是互补关系运行环境检测回答“当前在哪”备份寄存器回答“之前发生了什么”。两个信息合起来就能设计出非常稳健的 IAP 状态机。#define MAGIC_JUMP_TO_BL 0xB007B007u void jump_to_system_bootloader(void) { /* 备份寄存器需要使能 PWR 时钟和备份域访问 */ HAL_PWR_EnableBkUpAccess(); WRITE_REG(TAMP-BACKUP_REG[0], MAGIC_JUMP_TO_BL); /* 复位前关闭中断恢复默认栈指针跳转 */ __disable_irq(); HAL_NVIC_SystemReset(); }在 App 启动时读这个 magic如果匹配就执行升级结果确认、日志标记或回滚逻辑。如果检测到当前处于系统 Bootloader 环境同时备份寄存器又写过 magic说明跳转目标正确只是 Bootloader 还没干完活。5.3 生产测试和返修场景下的快速定位产线上最容易出现的一种情况是板子从上一道工序出来Boot 引脚或选项字节被改掉导致整批设备不跑 App。如果没有运行环境检测产测软件只能傻等超时然后报“通信失败”。有了检测逻辑后产测固件可以主动上报一个状态码比如 0xA5 表示“当前在 Bootloader请重新烧写选项字节”0x5A 表示“当前在 App可以进入自检”。一线工人的处理路径就完全不一样了。返修场景更实用。维修人员拿到板子不知道上一手做了什么接上串口看第一行日志就基本能确定问题范围。这种“减少猜测、让状态自己说话”的做法对交付和售后都有很大价值。5.4 建议的工程封装方式我最终把这段逻辑封装成了一个独立模块不依赖任何 HAL 外设初始化只在调用时传入一个可选的串口句柄。模块只向上层提供一个枚举结果所有寄存器读取全部基于启动早期的快照串口探测作为一个独立的可选函数由业务逻辑决定是否调用模块内不引入任何延时函数避免阻塞启动流程。这样封装的好处是无论你是用裸机、RTOS 还是带 Secure Manager 的复杂工程都能直接复用。即便将来从 H573 换到其他 Cortex-M33 的 STM32 型号只需要改几个基地址和寄存器位域逻辑完全不用重写。我在实际项目里跑这套检测最大的感受是不要指望一个信号就给出百分百确定的答案把 VTOR、BOOT_MEM、协议握手三个互相独立的信号组合起来比迷信任何一个都靠谱。H573 的启动链路已经很复杂了再用单点判断去赌只会白白消耗调试时间。