Bootloader跳转APP代码流程

Tutorial: 卓越工程师 Category: 嵌入式 Published: 2026-10-23 11:05:29 Views: 0 Likes: 0 Comments: 0

199.2.7 Bootloader跳转APP代码流程

#define INTER_FLASH_APP_ADDR (0x8008000U)    /* inter_flashif.h:APP 向量表起始地址,等价于 0x08008000。 */

#define AC5_MODE 1                           /* boot_manager.c:课程工程使用 Arm Compiler 5 对应的声明分支。 */

typedef void (*APP_FUNC)(void);              /* 定义函数指针类型:无参数、无返回值,供调用 APP 复位入口。 */

#define CLOSE_ALL_INT() __set_PRIMASK(1)     /* 把 PRIMASK 置 1,屏蔽所有可配置优先级的异常和中断。 */

uint8_t boot_check_stack2jump_app(uint32_t addr) /* 检查 addr 处的初始 MSP,并尝试跳到该映像。 */
{                                            /* APP 检查与跳转函数开始。 */
#if (AC5_MODE == 1)                          /* 当前课程工程令 AC5_MODE 为 1。 */
    APP_FUNC jump2app;                       /* AC5 分支:定义保存 APP 复位入口的局部函数指针。 */
#else                                        /* 非 AC5 分支开始。 */
    static APP_FUNC jump2app;                /* 其他编译器分支:使用静态函数指针;保留课程源码。 */
#endif                                       /* 结束编译器相关的条件编译。 */

    if (((*(uint32_t *)addr) & 0x2FFE0000) == 0x20000000) /* 读取向量表首字,并粗略检查它是否像 SRAM 地址。 */
    {                                        /* 初始 MSP 通过课程掩码检查。 */
        jump2app = (APP_FUNC) * (volatile uint32_t *)(addr + 4); /* 读取向量表第 2 个字并转成函数指针。 */
        __set_MSP(*(volatile uint32_t *)addr); /* 把向量表首字写入主栈指针 MSP。 */

        CLOSE_ALL_INT();                     /* 跳转前令 PRIMASK=1,暂时屏蔽可配置优先级的中断。 */
        jump2app();                           /* 间接调用 APP 的 Reset_Handler,开始执行 APP 启动代码。 */

        printf("Bootloader end load app\r\n"); /* APP 复位入口若意外返回,才可能执行到这条日志。 */
    }                                        /* 合法初始 MSP 分支结束。 */
    else                                     /* 向量表首字未通过课程掩码检查。 */
    {                                        /* APP 无效提示分支开始。 */
        printf("APP Not Found!\r\n");       /* 提示指定地址处没有通过当前检查的 APP。 */
    }                                        /* APP 无效提示分支结束。 */

    return 0;                                /* 课程函数固定返回 0;成功跳转时通常不会返回这里。 */
}                                            /* APP 检查与跳转函数结束。 */

boot_check_stack2jump_app(INTER_FLASH_APP_ADDR); /* main.c:ota_flag=0 时跳转到已经存在的旧 APP。 */
boot_check_stack2jump_app(INTER_FLASH_APP_ADDR); /* main.c:YMODEM 接收成功后跳转到刚写入的新 APP。 */

__set_PRIMASK(0);                            /* APP 的 main.c:清除 PRIMASK,使可配置优先级中断可以响应。 */

本课集中解释 08Ymodem_Bootloader 中的 boot_check_stack2jump_app():Bootloader 如何从 APP 向量表取得初始主栈指针和复位入口,再把控制权交给位于 0x08008000的 APP。

这里要先建立一个准确概念:Bootloader 调用 APP 的 Reset_Handler是一次软件跳转,不是芯片发生了新的硬件复位。 APP 会重新经历自己的启动文件、SystemInit()、C 运行库初始化和 main(),但 Bootloader 已经配置的外设、时钟、NVIC 使能/挂起状态并不会因为这一跳自动恢复到上电默认值。

一、Bootloader跳转APP流程

Bootloader 初始化参数区后读取 Flash 中的升级标志:

读取并校验 0x08006000 参数区
              │
              ├─ ota_flag = 0
              │    └─ 检查 0x08008000 处现有 APP → 跳转旧 APP
              │
              ├─ ota_flag = 1
              │    └─ YMODEM 接收 APP.bin → 清 ota_flag
              │                         → 检查并跳转新 APP
              │
              └─ 参数读取失败
                   └─ 前面的初始化或后续错误路径处理

main.c第 114 行和第 143 行在课程工程中都是:

boot_check_stack2jump_app(INTER_FLASH_APP_ADDR); /* 两条路径都把 0x08008000 作为 APP 向量表起点。 */

两次调用的跳转机制完全相同,差别只在调用时机:

  • 第一次调用发生在 ota_flag == 0时,目标是芯片中原来已有的 APP;
  • 第二次调用发生在 Ymodem_Receive()返回 COM_OK、升级标志被清零之后,目标是刚写入的新 APP。

INTER_FLASH_APP_ADDR在配套源码中写成 0x8008000U,它与补齐前导零的 0x08008000U数值相同。原转录中的“八两个零八三个零”等内容都是地址口述识别错误。

跳转函数实际完成四步:

  1. 读取 addr + 0处的 32 位值,粗略判断初始 MSP 是否像 SRAM 地址;
  2. 读取 addr + 4处的 32 位值,把它解释为 APP 的 Reset_Handler函数指针;
  3. 用第一个值重设 MSP,并用 PRIMASK 暂时屏蔽可配置优先级中断;
  4. 调用函数指针,进入 APP 的启动流程。

1.1 代码讲解

1.1.1 addr与两次解引用

调用时 addr = INTER_FLASH_APP_ADDR = 0x08008000。因为 addr的类型是整数 uint32_t,表达式 addr + 4表示地址增加 4 字节。

表达式实际读取位置读取内容
*(uint32_t *)addr0x08008000APP 向量表第 1 项:初始 MSP
*(volatile uint32_t *)(addr + 4)0x08008004APP 向量表第 2 项:Reset_Handler入口

分解第一个表达式:

  • (uint32_t *)addr:把数值地址强制转换为“指向 32 位无符号整数的指针”;
  • 前面的 *:对指针解引用,真正读取该地址保存的 4 字节;
  • 读出的不是“APP 地址本身”,而是 APP 链接器写在向量表首项中的 初始主栈指针值。

第二个表达式多了 volatile,表示每次都要实际访问该地址,不能把读取随意替换成先前缓存的值。读出的 32 位整数再强转为 APP_FUNC,于是 jump2app()就能通过函数指针间接调用这个入口。

1.1.2 函数指针不是指针函数

课程声明为:

typedef void (*APP_FUNC)(void); /* APP_FUNC 表示“指向无参数、无返回值函数的指针”类型。 */

括号非常关键:

  • void (*p)(void):p是函数指针,保存函数入口地址;
  • void *f(void):f是一个函数,这个函数返回 void *指针,属于指针函数。

本课使用的是前者。jump2app = (APP_FUNC)...只是把向量表里的入口值保存为可调用的函数指针;执行 jump2app()时,编译器才生成间接分支指令,让处理器开始从 APP 的 Reset_Handler取指。

1.1.3 初始 MSP 掩码检查

课程源码的判断为:

if (((*(uint32_t *)addr) & 0x2FFE0000) == 0x20000000) /* 用掩码粗筛向量表首字的地址特征。 */

其意图是:正常 Cortex-M APP 的第一个向量应是 SRAM 中的初始栈顶,STM32F103 的 SRAM 区域以 0x20000000开头;如果 Flash 被擦除,首字通常为 0xFFFFFFFF,这个判断不会通过。

但它不是“APP 文件一定正确”的完整证明:

  • 该掩码没有精确检查本芯片 64 KiB SRAM 的上下界;
  • 没有检查 addr + 4中的复位入口是否位于 APP Flash 区;
  • 没有检查复位入口最低位是否为 1。Cortex-M3 异常向量最低位应为 1,用来表示 Thumb 状态;
  • 没有校验固件长度、签名、CRC 或其他向量。

因此,把函数命名理解为“检查栈顶特征后尝试跳转”最准确。输出 APP Not Found!也只代表首字没有通过这一个掩码,并不能区分“没有文件”和其他向量表损坏。

1.1.4 APP_FUNC的 AC5 条件分支

源码设置 AC5_MODE为 1,因此实际编译的是普通局部变量:

APP_FUNC jump2app; /* 当前 AC5 配置实际采用的函数指针声明。 */

#else中的 static APP_FUNC jump2app仍是课程源码的一部分,但本 target 不会编译它。这个条件只影响函数指针对象的存储期和编译器适配,不改变“读取复位入口 → 设置 MSP → 调用入口”的算法。视频没有进一步展开其编译器差异,不应据此凭空补写另一套跳转实现。

1.1.5 成功跳转后为何通常不会返回

jump2app()进入 APP 的 Reset_Handler后,启动代码最终进入 APP 的 main(),而嵌入式 main()通常停留在无限循环。因此正常情况下,下面这行不会执行:

printf("Bootloader end load app\r\n"); /* 只有 APP 启动入口异常返回到调用点时才可能执行。 */

同理,函数末尾的 return 0不能被当作“成功跳转返回 0”:成功跳转通常根本不返回;初始 MSP 检查失败时,它也返回 0。这个返回值在当前源码中没有区分成功与失败。

1.2 单片机内存结构

1.2.1 本项目的真实 SRAM 范围

视频的 Keil Target 页面、工程 .uvprojx和 Bootloader 分散加载文件一致显示:

IRAM1 Start = 0x20000000
IRAM1 Size  = 0x00010000 = 65536 B = 64 KiB

所以实际 SRAM 字节地址范围为:

0x20000000 ~ 0x2000FFFF

0x20010000是这段 SRAM 的末端后一地址;由于 Cortex-M 栈向低地址增长,初始 MSP 可以设置在这一个上边界,再通过压栈进入有效 SRAM。原笔记写成 0x2000E000不符合当前 STM32F103ZE 工程配置。

ST 的 STM32F103xC/D/E 数据手册也给出高密度器件最多 64 KiB SRAM,参见 STM32F103ZE 数据手册。

1.2.2 代码段、数据段、堆和栈

常见裸机映像中各区域的用途如下,但它们的准确位置由链接脚本决定,并不是所有工程都按一张固定示意图排列:

区域典型运行位置作用
向量表、代码、只读数据Flash保存入口地址、机器指令和常量
.dataSRAM;初值来自 Flash保存有初值的可写全局/静态变量
.bss / ZISRAM保存零初始化或未显式初始化的全局/静态变量
堆SRAM供动态内存分配使用,裸机工程可能完全不用
栈SRAM保存调用现场、部分局部数据和异常栈帧等,通常向低地址增长

“参数、返回值、局部变量全都放在栈里”是过度简化。按照 ARM 调用约定,前几个参数和很多返回值优先使用寄存器;寄存器不够、需要保存现场或局部对象无法只放寄存器时,才会使用栈。函数进入和退出时,编译器生成的序言/尾声通常通过改变 SP 分配和回收栈帧。

1.2.3 APP 与 Bootloader 的栈顶不一定数值不同

Bootloader 和 APP 是两个独立链接的固件,各自的向量表首字都声明自己的初始 MSP。跳转时必须读取 APP 的首字并重新设置 MSP,不能继续假定 Bootloader 的 MSP 适用于 APP。

不过,“两个栈顶地址必然不同”也不严谨:二者使用同一片 SRAM,若启动文件、链接布局和栈大小恰好相同,首字可能得到相同的数值。正确做法仍是把 APP 向量表中的值作为唯一依据,而不是比较它是否与 Bootloader 的数值不同。

1.3 单片机复位流程

1.3.1 向量表前两项

Cortex-M3 向量表的开头固定表示:

相对偏移内容在本课 APP 中的绝对位置
0x00初始 MSP 值0x08008000
0x04Reset_Handler入口0x08008004
0x08NMI 入口0x08008008
0x0CHardFault 入口0x0800800C

APP BIN 的第 0 字节就是这张向量表的开始。上节课核对过配套 APP.bin:首字为 0x20000900,第二字为 0x080081E1。后者最低位为 1,符合 Cortex-M 异常入口使用 Thumb 状态的要求。

ST 的 Cortex-M3 编程手册明确给出向量表偏移 0x0000为初始 SP、0x0004为 Reset,并说明异常向量最低位必须为 1,参见 PM0056。

1.3.2 真正硬件复位与软件跳转的区别

主 Flash 启动时,STM32 把 Flash 映射到 Cortex-M3 复位所访问的启动地址。硬件复位流程可概括为:

硬件从启动映射区的向量表首字装载 MSP
  → 从第二字装载复位入口并开始执行 Reset_Handler
  → Reset_Handler 调用 SystemInit
  → 进入 C 运行库 __main
  → 初始化 .data/.bss 等运行环境
  → 调用用户 main

而 Bootloader 跳 APP 时没有再触发复位电路,只是用 C 代码模拟最关键的两次取值:

从 APP_BASE + 0 读取 MSP,并调用 __set_MSP
  → 从 APP_BASE + 4 读取复位入口
  → 通过函数指针分支到 APP Reset_Handler

因此,视频所说“运行这个复位函数后单片机开始复位”应理解为“开始执行 APP 的复位处理程序”,不能理解为发生了 NVIC_SystemReset()那样的系统复位。

1.3.3 MSP、PSP 与 PC
  • MSP(Main Stack Pointer):复位后的默认栈指针,异常处理也使用它。本课 Bootloader 和无 RTOS APP 主要使用 MSP。
  • PSP(Process Stack Pointer):可在 Thread 模式下选用,常见于 RTOS 任务;本课跳转代码没有操作 PSP。
  • PC(Program Counter):控制处理器从哪里取指执行。调用 jump2app()后,间接分支把执行流转到 APP 的复位入口。

原笔记中的“PC 计算器”应为 PC 寄存器/程序计数器。入门时可把它理解为当前执行流的位置,但“始终保存下一条指令地址”仍是简化描述;Cortex-M 有流水线,软件读取 PC 时的可见值还受体系结构规则影响。本课只需掌握分支到复位入口会改变程序执行位置。

CMSIS 对 __set_MSP()和__set_PRIMASK()的定义说明:复位时默认使用 MSP,初始值来自向量表首个 32 位字;PRIMASK 是一位全局优先级屏蔽寄存器。

1.4 单片机启动流程

1.4.1 Stack_Size = 0x400到底表示什么

视频打开 startup_stm32f103xe.s,可以看到:

Stack_Size  EQU   0x400
Stack_Mem   SPACE Stack_Size
__initial_sp

0x400 = 1024,所以启动文件为该工程预留 1 KiB 主栈空间。但 MSP 并不是被写成数值 0x400:__initial_sp是栈空间顶部的链接地址,向量表首项保存的是这个地址。

还要区分两个概念:

  • Stack_Size是链接时预留的最大栈区域大小;
  • MSP 是运行时指向当前栈顶的地址,函数调用、返回和异常进出会改变它。
1.4.2 启动文件如何组织向量表

课程启动文件中的关键关系是:

__Vectors 第 1 项 → __initial_sp
__Vectors 第 2 项 → Reset_Handler

链接器把 RESET段放到各自映像的起始处:Bootloader 的向量表位于 0x08000000,APP 的向量表位于 0x08008000。所以相同的启动文件模板,经过不同链接地址后会形成两张不同的向量表。

1.4.3 Reset_Handler之后不是直接进入用户 main

视频展示的 Arm Compiler 5 启动代码先导入 SystemInit和__main,执行关系是:

Reset_Handler
  → SystemInit()
  → __main                 ← Arm C 运行库入口,不是用户 main.c 中的 main
       → 复制/初始化 RW 数据
       → 清零 ZI/.bss
       → 完成必要的 C 运行环境初始化
       → 调用用户 main()

因此:

  • SystemInit()是系统初始化函数,不是“系统复位函数”;
  • __main与用户写的 main不是同一个符号;
  • Bootloader 跳入 APP 的 Reset_Handler,正是为了让 APP 重新执行自己的系统和 C 运行时初始化,不能直接跳用户 main()。

上节课在 APP 的 SystemInit()中还设置了 SCB->VTOR = 0x08008000。这一步把后续异常和中断的向量表切到 APP;本课的 boot_check_stack2jump_app()本身没有设置 VTOR。

1.5 Bootloader跳转APP流程详解

1.5.1 实际控制权交接顺序

结合第 198 课的 APP 配置,完整顺序如下:

Bootloader 正在运行,VTOR/栈/外设状态属于 Bootloader
  → 检查 APP 向量表首字
  → 从 0x08008004 取得 APP Reset_Handler
  → 把 0x08008000 首字写入 MSP
  → PRIMASK 置 1
  → 调用 APP Reset_Handler
  → APP SystemInit 把 VTOR 改到 0x08008000
  → APP 的 __main 完成 C 运行时初始化
  → APP main 依次执行 HAL_Init、SystemClock_Config
  → APP 执行 __set_PRIMASK(0)
  → APP 初始化 GPIO、USART、CAN 等具体外设
  → 进入 APP 业务循环

这里的三个关键地址必须相互匹配:

项目地址
APP 在 Keil 中的链接起点0x08008000
Bootloader 的 INTER_FLASH_APP_ADDR0x08008000
APP 运行时写入 SCB->VTOR的值0x08008000
1.5.2 PRIMASK 并非真正“关闭所有中断源”

课程宏名叫 CLOSE_ALL_INT(),但 __set_PRIMASK(1)的准确作用是屏蔽所有 可配置优先级 的异常和中断:

  • 普通外设 IRQ 和 SysTick 等不会被处理;
  • NMI 和 HardFault 不受 PRIMASK 屏蔽;
  • NVIC 中各 IRQ 的使能位、挂起位没有被清除;
  • 外设本身的中断使能和状态标志也没有被清除。

APP 中的 __set_PRIMASK(0)只是取消这层全局屏蔽,并不等于重新配置了所有具体中断。若 Bootloader 留下了挂起中断,一旦 APP 清零 PRIMASK,它就可能立即响应。因此生产级 Bootloader 通常还会根据实际外设清理 SysTick、DMA、通信外设和 NVIC 状态;这些改进不在本课配套源码中,本笔记不把它们伪装成课程已经实现的代码。

1.5.3 为什么 APP 必须重新开中断

PRIMASK 属于处理器核心状态,软件跳转不会自动清零它。Bootloader 在跳转前执行:

__set_PRIMASK(1); /* 屏蔽可配置优先级异常和中断。 */

APP 的 main.c在系统时钟配置后执行:

__set_PRIMASK(0); /* 清除全局屏蔽,让已正确配置并使能的中断可以响应。 */

如果漏掉 APP 中这一行,轮询代码仍可能运行,但 SysTick、USART、CAN 等依赖可配置中断的功能可能表现为不工作。它与 APP 的 VTOR 重定位是两个独立条件:

  • VTOR 正确:中断发生时能从 APP 向量表找到正确入口;
  • PRIMASK 为 0:这些可配置中断有机会真正进入处理程序。

两者缺一不可。

1.5.4 本课源码的边界与注意事项
  1. __set_MSP()之后,代码仍在一个普通 C 函数中继续执行宏和函数指针调用。切换 MSP 后应避免再依赖 Bootloader 原栈上的局部数据;这类交接代码对编译器和优化设置较敏感,课程用 AC5_MODE保留了编译器分支,实际项目应对生成汇编进行验证。
  2. jump2app()是普通函数调用形式,APP 的 Reset_Handler理论上不应返回。若它返回,Bootloader 将在 MSP 已经切换、PRIMASK 仍为 1 的状态下继续执行,不能视为正常路径。
  3. 课程只粗略检查初始 MSP。更严格的产品代码还应检查复位入口范围、Thumb 位、映像长度与完整性,并在交接前有计划地反初始化 Bootloader 资源。
  4. 清除升级标志发生在 YMODEM 成功后、跳转前。如果新映像随后不能启动,当前课程没有双分区回滚或确认机制;这些属于更完整 IAP 方案需要解决的问题。

二、常见错误定位

现象优先检查
打印 APP Not Found!0x08008000首字是否为合理 MSP;BIN 是否烧到正确地址;APP 是否正确链接
跳转后立刻 HardFault0x08008004复位入口及 Thumb 位;APP 链接地址;栈地址;固件是否完整
APP 能进入 main(),一开中断就异常SCB->VTOR是否改为 0x08008000;Bootloader 是否留下挂起中断
APP 的中断完全不响应APP 是否执行 __set_PRIMASK(0);具体 NVIC 与外设中断是否初始化和使能
跳转函数后打印 Bootloader end load appAPP 的 Reset_Handler异常返回;这不是正常成功标志
升级后反复复位参数标志、APP 向量、Bootloader 错误路径以及新固件完整性

三、本课结论

Bootloader 跳转 APP 的核心不是简单地“把 PC 改到 0x08008000”,而是把一个独立固件的启动上下文交给处理器:

  1. 0x08008000保存 APP 声明的初始 MSP;
  2. 0x08008004保存 APP 的复位入口;
  3. Bootloader 装载 APP 的 MSP,并间接分支到复位入口;
  4. APP 自己执行 SystemInit()、运行库初始化和 main();
  5. APP 把 VTOR 切到自己的向量表,并解除 Bootloader 留下的 PRIMASK 屏蔽。

理解这五步后,就能把“同一片 Flash 上有两个独立固件”与“控制权如何从一个固件安全交给另一个固件”连成完整逻辑。