7.1 启动流程与Bootloader


文档摘要

7.1 启动流程与 Bootloader 从按下电源到 main 函数第一行,芯片经历了一条精确的接力链:硬件取向量、启动代码布置内存、可选的 Bootloader 决定跑哪个应用。这条链在 3.3 节看过物理视角,在 2.2 节看过 .data 搬运的代码,本节把散落的环节接成完整的因果链,并以它为地基理解 Bootloader 的分区设计——这也是 7.3 节 OTA 的直接前置。 完整启动链条:六步走到 main 以 STM32F103(BOOT0=0,从 Flash 启动)为例,复位后依次发生六件事: 硬件装载 SP 与 PC。内核强制从地址 0x0000 0000 读第一个字装入栈指针 SP,读第二个字装入程序计数器 PC。

7.1 启动流程与 Bootloader

从按下电源到 main 函数第一行,芯片经历了一条精确的接力链:硬件取向量、启动代码布置内存、可选的 Bootloader 决定跑哪个应用。这条链在 3.3 节看过物理视角,在 2.2 节看过 .data 搬运的代码,本节把散落的环节接成完整的因果链,并以它为地基理解 Bootloader 的分区设计——这也是 7.3 节 OTA 的直接前置。

完整启动链条:六步走到 main

以 STM32F103(BOOT0=0,从 Flash 启动)为例,复位后依次发生六件事:

  1. 硬件装载 SP 与 PC。内核强制从地址 0x0000 0000 读第一个字装入栈指针 SP,读第二个字装入程序计数器 PC。由于 BOOT 引脚配置,0x0000 0000 被映射为 Flash 起点 0x0800 0000——所以这两个字正是 Flash 开头向量表的第 0、1 项(3.3 节的 vector_table)。
  2. 跳入 Reset_Handler。PC 指向的复位服务程序由启动文件提供,它是 main 之前的总调度。
  3. 搬 .data。把存在 Flash 的初值镜像逐字复制到 SRAM(2.2 节逐行拆过),全局变量的"开箱即用"由此兑现。
  4. 清 .bss。未初始化的全局变量区域整段清零——C 标准保证它们为 0,但硬件不会白送,靠的就是这一步。
  5. 调用 SystemInit。配置时钟(2.3 节的 72 MHz 流程)、使能浮点(有 FPU 的型号)、设置向量表偏移寄存器 VTOR。
  6. 进入 main。标准库的初始化(如用到堆的 libc)之后,控制权第一次交给你写的代码。

用一张内存图把这六步走过的地址串起来:

图 7-2:启动前后的 Flash 与 SRAM 布局变化

图 7-2:启动前后的 Flash 与 SRAM 布局变化

理解这条链的实用回报立竿见影。程序"下进去了不跑"时,按链条从上往下查:BOOT0 是不是悬空或被拉高(跳进了串口 bootloader)、向量表前两个字对不对(调试器看 0x0800 0000 处是不是像样的栈顶值)、Reset_Handler 有没有被链接进去(.map 文件搜符号)。每一类故障都在链条上有一个明确的断点,而不是一句笼统的"烧录失败"。

Bootloader:产品固件的第二段程序

量产设备几乎从不"裸跑一个应用",而是分两段:Bootloader(引导程序)驻留在 Flash 前段,只干三件事——检查应用是否有效、必要时执行更新、跳转应用;应用占 Flash 后段。这样设计的动机在第 5 步之后就清晰了:复位永远从 Bootloader 起跑,它就有机会在应用运行前做决定——比如发现新固件已下载完毕且校验通过,就搬过来再跳;发现应用连续启动失败,就回滚到上一个版本。7.3 的 OTA 全靠这套机制兜底。

分区约定上,Flash 被划成几块(以 ESP32 的典型布局为例,STM32 自定义 Bootloader 同理):

分区 典型大小 内容
Bootloader 32 KB 以内 校验、搬运、跳转,几乎永不更新
分区表 4 KB 各分区的位置与类型(ESP32 特有)
应用 A(ota_0) 应用大小 当前运行的应用
应用 B(ota_1) 应用大小 OTA 备用区:新固件落在这里
存储 / 参数区 按需 配置、校准数据、升级标志

STM32 上自己写 Bootloader 时,核心是"校验 + 跳转"两步。跳转看似只是一句函数指针调用,实际要先替新程序把"家当"归位——重映射向量表、复位栈指针,再跳过去:

#include "stm32f10x.h" #define APP_ADDR 0x08004000UL /* 应用区起点:跳过前 16 KB 给 Bootloader */ #define APP_OK_MAGIC 0xDEADBEEFUL /* 应用区末尾的合法性标记(由应用写入)*/ typedef void (*app_entry_t)(void); static uint32_t app_checksum_ok(void) { uint32_t magic = *(volatile uint32_t *)(APP_ADDR + 0x1FCUL); uint32_t stack = *(volatile uint32_t *)(APP_ADDR); uint32_t entry = *(volatile uint32_t *)(APP_ADDR + 4UL); return magic == APP_OK_MAGIC /* 合法性标记对 */ && (stack >> 20) == 0x200 /* 栈顶值落在 SRAM 范围,基本可信 */ && (entry & 1UL) == 1UL; /* Thumb 入口地址最低位必须是 1 */ } void jump_to_app(void) { if (!app_checksum_ok()) { enter_update_mode(); /* 应用无效:留在 Bootloader 等升级 */ return; } __disable_irq(); /* 搬家前先熄火:别让中断踩进旧向量 */ for (int i = 0; i < 8; i++) NVIC->ICER[i] = 0xFFFFFFFF; /* 全部关中断 */ for (int i = 0; i < 8; i++) NVIC->ICPR[i] = 0xFFFFFFFF; /* 清全部挂起 */ SCB->VTOR = APP_ADDR; /* 向量表指到应用区:之后中断找新家 */ __set_MSP(*(volatile uint32_t *)APP_ADDR); /* 栈指针 = 应用栈顶 */ app_entry_t app = (app_entry_t)(*(volatile uint32_t *)(APP_ADDR + 4UL)); app(); /* 跳!从此 Bootloader 使命完成 */ }

应用侧也要配合改三处,否则跳过去就跑飞:链接脚本把 ROM 起点改到 APP_ADDR;系统初始化里把 VTOR 再设一遍(防 Bootloader 之外的复位路径);中断优先级分组与外设时钟保持"进门前干净"。这三处漏任何一处,症状都是"Bootloader 跳转后死机"——排查时按顺序核对即可。

Bootloader 的升级通道:串口 Ymodem 一例

自研 Bootloader 最经典的取数通道是串口 + Ymodem 协议:上位机工具发固件文件,Bootloader 按 1 KB 块接收、应答、写入下载区,CRC16 逐块校验。整个协议不到三百行 C 就能实现,无需网络栈,调试期和产线烧录都好用。网络型产品再把通道换成 HTTP 或 MQTT 即可,分区与校验逻辑原样复用。给 Bootloader 留一条"永不依赖应用"的取数通道(哪怕是串口),是售后救砖的最后一道保险——所有量产产品都值得有。

向量表重定位与启动文件的关系

细心的读者会问:复位后硬件只认地址 0 处的向量表,应用怎么收到中断?答案就是 VTOR 寄存器(向量表偏移)。Bootloader 跳转前把 VTOR 指向应用区,应用运行期间所有中断按新向量表分发;若应用自己复位(看门狗、软复位),硬件又从 0 处的 Bootloader 重新起跑——整个环路闭合。这套"向量表搬家"机制是 Cortex-M 体系对多段固件的制度性支持,ESP32 的二级引导(first/second stage bootloader)把同样的思想做得更工程化:分区表、校验、回滚标志全部制度化,应用代码一行不写就自动享有。

到这里,"程序从哪来、怎么开始跑"已经完整。下一节解决它的另一半:怎么把它装进去、出了问题怎么看见——烧录与调试的工具链实战。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U