从零手写一个Bootloader:IAP升级原理与实现
文章目录

每日一句正能量
“换位思考不只是理解别人,它的真正核心是放过自己。”
当你真正站到对方位置,你会发现他的行为自有逻辑,并非针对你。 于是你不再因别人的冷漠而自我攻击,不再因误解而耿耿于怀。理解别人,最终是解开了绑在自己心上的绳索。
一、引言:为什么每个嵌入式工程师都应该手写一次Bootloader
Bootloader是嵌入式系统中最"底层"的代码之一——它在操作系统之前运行,在C库初始化之前运行,甚至在main()函数之前运行。然而,大多数开发者对Bootloader的认知停留在"用ST-Link烧录即可"的层面,直到某天产品部署到现场,需要远程修复一个致命Bug时,才意识到IAP(In-Application Programming)能力的重要性。
手写Bootloader是一次对系统启动流程的彻底解剖:你将亲手处理向量表重定位、Flash擦写、中断关闭、栈指针切换,这些在应用层开发中永远接触不到的操作。这不仅是一项实用技能,更是理解ARM Cortex-M架构本质的最佳途径。
本文将从一个空白的main.c开始,逐步构建一个完整的Bootloader,涵盖Flash分区设计、YMODEM协议传输、向量表重定位和APP跳转等核心环节。
二、IAP的核心原理:Flash中的"双系统"
2.1 为什么需要Bootloader?
在传统的开发模式中,程序通过JTAG/SWD直接烧录到Flash起始地址(0x08000000),上电后直接运行。这种模式在量产和现场部署中面临三个致命问题:
- 无法远程更新:产品安装到客户现场后,没有JTAG接口可供烧录
- 无法回滚:新固件有Bug时,无法恢复到旧版本
- 无法差分升级:每次更新都传输完整固件,浪费带宽和时间
IAP通过在Flash中划分Bootloader区和APP区,实现"自我更新"的能力。
2.2 Flash分区设计

以STM32F103C8T6(64KB Flash,128KB Flash版本同理)为例,典型的分区方案如下:
┌─────────────────────────────────────────┐ 0x0800 0000
│ Bootloader (16KB) │
│ - 升级通信协议处理 │
│ - Flash擦写操作 │
│ - 版本校验与跳转逻辑 │
├─────────────────────────────────────────┤ 0x0800 4000
│ APP1 - 当前运行版本 (24KB) │
│ - 用户应用程序 │
│ - 独立的向量表和堆栈 │
├─────────────────────────────────────────┤ 0x0800 A000
│ APP2 - 升级缓存区 (24KB) │
│ - 接收新固件的临时存储 │
│ - 校验通过后覆盖APP1 │
├─────────────────────────────────────────┤ 0x0801 0000
│ 未使用 / 参数存储区 (0KB) │
└─────────────────────────────────────────┘ 0x0801 0000 (64KB边界)
分区设计的关键考量:
| 考量因素 | 设计决策 | 理由 |
|---|---|---|
| Bootloader大小 | 16KB | 足够容纳YMODEM协议栈+Flash驱动+跳转逻辑 |
| APP大小 | 24KB | 根据实际应用代码量调整,预留余量 |
| 双APP备份 | 是 | 确保升级失败时可回滚,避免"变砖" |
| 参数区 | 可选 | 存储设备序列号、校准参数等持久化数据 |
扇区对齐要求:STM32F103的Flash按**页(Page)**组织,每页1KB。擦除操作必须以页为单位,因此分区边界必须对齐到1KB整数倍。
三、Bootloader的启动流程

3.1 上电后的执行路径
上电/复位
│
▼
硬件从0x08000000读取MSP初值 → 加载到MSP寄存器
│
▼
硬件从0x08000004读取Reset_Handler → 跳转到Bootloader
│
▼
Bootloader的SystemInit()执行
├── 配置系统时钟(HSE/PLL)
├── 设置Flash等待周期
└── 设置VTOR = 0x08000000(Bootloader向量表)
│
▼
Bootloader的main()执行
├── 初始化通信接口(UART/USB)
├── 检查升级标志(GPIO/Flash/Timeout)
├── 判断是否需要升级
│ ├── 是 → 接收新固件 → 写入APP2区 → 校验 → 复制到APP1
│ └── 否 → 直接跳转
│
▼
跳转到APP1
├── 从APP1地址读取MSP初值
├── 设置MSP
├── 设置VTOR = APP1_BASE(关键!)
└── 跳转到APP1的Reset_Handler
│
▼
APP1运行
├── APP的SystemInit()执行
├── APP的__main()执行(.data搬运、.bss清零)
└── APP的main()执行
3.2 向量表重定位:VTOR寄存器的核心作用
这是Bootloader开发中最容易出错、也最关键的部分。
Cortex-M3内核的**VTOR(Vector Table Offset Register)**寄存器位于系统控制块(SCB)中,地址为0xE000ED08。它的作用是告诉CPU:当前使用的中断向量表在哪里。
默认情况:复位后VTOR = 0x00000000(通过硬件映射指向0x08000000),所有中断都从Bootloader的向量表取地址。
Bootloader阶段:VTOR保持默认值,Bootloader的中断正常响应。
跳转APP后:必须将VTOR设置为APP的向量表基地址,否则APP的中断会"跑飞"到Bootloader的ISR中。
// 关键代码:APP跳转前的VTOR设置
#define APP1_BASE_ADDR 0x08004000
void jump_to_application(void) {
// 1. 关闭所有中断,防止跳转过程中发生中断
__disable_irq();
// 2. 获取APP的MSP初值和Reset_Handler地址
uint32_t app_msp = *(volatile uint32_t *)APP1_BASE_ADDR;
uint32_t app_reset = *(volatile uint32_t *)(APP1_BASE_ADDR + 4);
// 3. 设置VTOR指向APP向量表(关键!)
SCB->VTOR = APP1_BASE_ADDR;
// 4. 设置主堆栈指针
__set_MSP(app_msp);
// 5. 创建函数指针并跳转
void (*app_reset_handler)(void) = (void (*)(void))app_reset;
// 6. 确保跳转前指令流水线已清空
__DSB();
__ISB();
app_reset_handler();
// 理论上不会执行到这里
while (1);
}
VTOR的位域结构:
| 位域 | 名称 | 说明 |
|---|---|---|
| [31:29] | TBLOFF | 向量表基地址的高位(保留) |
| [28:7] | TBLOFF | 向量表基地址的[28:7]位 |
| [6:0] | - | 保留,必须写0 |
对齐要求:VTOR的值必须是向量表大小的整数倍。对于68个向量(272字节),最小对齐为256字节(0x100)。实际中通常按1KB(0x400)或4KB(0x1000)对齐。
四、Bootloader代码实现
4.1 启动文件与链接脚本
Bootloader的启动文件(startup_bootloader.s)与标准启动文件几乎相同,但需要确保向量表位于0x08000000:
; 栈大小
Stack_Size EQU 0x00001000 ; 4KB栈
AREA STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem SPACE Stack_Size
__initial_sp
; 向量表
AREA RESET, DATA, READONLY
EXPORT __Vectors
EXPORT __Vectors_End
EXPORT __Vectors_Size
__Vectors DCD __initial_sp ; Top of Stack
DCD Reset_Handler ; Reset Handler
DCD NMI_Handler ; NMI Handler
DCD HardFault_Handler ; Hard Fault Handler
; ... 其他异常向量
__Vectors_End
__Vectors_Size EQU __Vectors_End - __Vectors
; 复位处理函数
AREA |.text|, CODE, READONLY
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT SystemInit
IMPORT __main
LDR R0, =SystemInit
BLX R0
LDR R0, =__main
BX R0
ENDP
Bootloader的链接脚本(bootloader.ld):
/* 内存区域定义 */
MEMORY
{
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 20K
/* Bootloader区域:从Flash起始地址开始,大小16KB */
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 16K
}
/* 栈顶地址 */
_estack = ORIGIN(RAM) + LENGTH(RAM);
SECTIONS
{
.text :
{
. = ALIGN(4);
KEEP(*(.isr_vector)) /* 向量表必须保留 */
*(.text*)
*(.rodata*)
. = ALIGN(4);
} >FLASH
.data :
{
. = ALIGN(4);
_sdata = .;
*(.data*)
. = ALIGN(4);
_edata = .;
} >RAM AT> FLASH
.bss :
{
. = ALIGN(4);
_sbss = .;
*(.bss*)
*(COMMON)
. = ALIGN(4);
_ebss = .;
} >RAM
}
4.2 Flash驱动:擦除与写入
STM32F103的Flash编程必须以**半字(16位)为单位,擦除以页(1KB)**为单位。
#include "stm32f10x.h"
#define FLASH_KEY1 0x45670123
#define FLASH_KEY2 0xCDEF89AB
#define FLASH_PAGE_SIZE 1024 // 1KB per page
/**
* @brief 解锁Flash,允许写入
*/
void FLASH_Unlock(void) {
FLASH->KEYR = FLASH_KEY1;
FLASH->KEYR = FLASH_KEY2;
}
/**
* @brief 锁定Flash,防止误写入
*/
void FLASH_Lock(void) {
FLASH->CR |= FLASH_CR_LOCK;
}
/**
* @brief 擦除指定页
* @param page_address 页起始地址(必须是1KB对齐)
* @return 0成功,非0失败
*/
uint8_t FLASH_ErasePage(uint32_t page_address) {
// 等待上次操作完成
while (FLASH->SR & FLASH_SR_BSY);
// 检查是否有错误
if (FLASH->SR & (FLASH_SR_PGERR | FLASH_SR_WRPRTERR)) {
FLASH->SR |= FLASH_SR_PGERR | FLASH_SR_WRPRTERR; // 清除错误标志
}
// 设置页擦除
FLASH->CR |= FLASH_CR_PER;
FLASH->AR = page_address;
FLASH->CR |= FLASH_CR_STRT;
// 等待完成
while (FLASH->SR & FLASH_SR_BSY);
// 清除页擦除位
FLASH->CR &= ~FLASH_CR_PER;
// 验证擦除结果
uint32_t *ptr = (uint32_t *)page_address;
for (int i = 0; i < FLASH_PAGE_SIZE / 4; i++) {
if (ptr[i] != 0xFFFFFFFF) {
return 1; // 擦除失败
}
}
return 0;
}
/**
* @brief 写入半字(16位)
* @param address 目标地址(必须是偶数地址)
* @param data 16位数据
* @return 0成功,非0失败
*/
uint8_t FLASH_WriteHalfWord(uint32_t address, uint16_t data) {
// 等待上次操作完成
while (FLASH->SR & FLASH_SR_BSY);
// 设置编程使能
FLASH->CR |= FLASH_CR_PG;
// 写入数据
*(__IO uint16_t *)address = data;
// 等待完成
while (FLASH->SR & FLASH_SR_BSY);
// 清除编程位
FLASH->CR &= ~FLASH_CR_PG;
// 验证
if (*(__IO uint16_t *)address != data) {
return 1;
}
return 0;
}
/**
* @brief 写入缓冲区(自动处理跨页擦除)
* @param address 目标起始地址
* @param buffer 数据源缓冲区
* @param length 字节数(必须是偶数)
* @return 0成功,非0失败
*/
uint8_t FLASH_WriteBuffer(uint32_t address, uint16_t *buffer, uint32_t length) {
uint32_t page_addr = address & ~(FLASH_PAGE_SIZE - 1);
uint32_t offset = address - page_addr;
uint32_t remaining = length;
uint32_t buf_idx = 0;
// 如果起始地址不在页边界,先擦除该页
if (offset != 0 || remaining < FLASH_PAGE_SIZE) {
if (FLASH_ErasePage(page_addr) != 0) return 1;
}
while (remaining > 0) {
// 如果跨越到新页,先擦除
if (offset == 0 && remaining >= FLASH_PAGE_SIZE) {
if (FLASH_ErasePage(page_addr) != 0) return 1;
}
// 写入半字
if (FLASH_WriteHalfWord(address + buf_idx, buffer[buf_idx / 2]) != 0) {
return 1;
}
buf_idx += 2;
remaining -= 2;
offset += 2;
// 页边界检查
if (offset >= FLASH_PAGE_SIZE) {
offset = 0;
page_addr += FLASH_PAGE_SIZE;
}
}
return 0;
}
Flash操作的黄金法则:
- 必须先解锁后操作:忘记解锁会导致硬件错误
- 必须先擦除后写入:Flash的"1"只能变"0",不能反向
- 擦除以页为单位:即使只改1字节,也要擦除整页(1KB)
- 写入以半字为单位:STM32F103不支持字节写入
- 操作完成后必须锁定:防止程序跑飞时误写Flash
4.3 YMODEM协议:可靠的固件传输

YMODEM是XMODEM的增强版,支持1024字节数据块和CRC校验,是嵌入式IAP中最常用的传输协议。
YMODEM帧结构:
┌────────┬────────┬────────┬────────────────┬────────┐
│ SOH │ 序号 │ 序号反码│ 数据(128B) │ CRC │
│(0x01) │ │ │ or (1024B) │(2B) │
│/STX │ │ │ │ │
│(0x02) │ │ │ │ │
└────────┴────────┴────────┴────────────────┴────────┘
控制字符:
| 字符 | 值 | 含义 |
|---|---|---|
| SOH | 0x01 | 128字节数据块起始 |
| STX | 0x02 | 1024字节数据块起始 |
| EOT | 0x04 | 传输结束 |
| ACK | 0x06 | 确认接收 |
| NAK | 0x15 | 否定确认(请求重传) |
| CAN | 0x18 | 取消传输 |
| C | 0x43 | 请求CRC校验模式 |
Bootloader中的YMODEM接收实现:
#include <string.h>
#include "stm32f10x.h"
#define YMODEM_SOH 0x01
#define YMODEM_STX 0x02
#define YMODEM_EOT 0x04
#define YMODEM_ACK 0x06
#define YMODEM_NAK 0x15
#define YMODEM_CAN 0x18
#define YMODEM_C 0x43
#define PACKET_SIZE_128 128
#define PACKET_SIZE_1024 1024
#define PACKET_HEADER_SIZE 3
#define PACKET_TRAILER_SIZE 2
#define PACKET_1K_SIZE (PACKET_HEADER_SIZE + PACKET_SIZE_1024 + PACKET_TRAILER_SIZE)
#define APP2_BASE_ADDR 0x0800A000
#define APP2_MAX_SIZE 24576 // 24KB
// 串口发送单字节
void UART_SendByte(uint8_t byte) {
while (!(USART1->SR & USART_SR_TXE));
USART1->DR = byte;
}
// 串口接收单字节(带超时)
uint8_t UART_ReceiveByte(uint32_t timeout_ms) {
uint32_t tick = HAL_GetTick();
while (!(USART1->SR & USART_SR_RXNE)) {
if (HAL_GetTick() - tick > timeout_ms) {
return 0xFF; // 超时
}
}
return USART1->DR;
}
// CRC16计算
uint16_t CRC16(uint8_t *data, uint16_t length) {
uint16_t crc = 0;
for (uint16_t i = 0; i < length; i++) {
crc ^= (uint16_t)data[i] << 8;
for (uint8_t j = 0; j < 8; j++) {
if (crc & 0x8000) {
crc = (crc << 1) ^ 0x1021;
} else {
crc <<= 1;
}
}
}
return crc;
}
/**
* @brief YMODEM接收固件
* @param dest_addr 目标Flash地址
* @param max_size 最大接收字节数
* @return 接收到的字节数,0表示失败
*/
uint32_t YMODEM_Receive(uint32_t dest_addr, uint32_t max_size) {
uint8_t packet[PACKET_1K_SIZE];
uint32_t received = 0;
uint8_t packet_number = 0;
uint8_t session_begin = 0;
uint32_t current_addr = dest_addr;
// 发送'C'请求CRC模式
UART_SendByte(YMODEM_C);
while (1) {
uint8_t header = UART_ReceiveByte(1000);
switch (header) {
case YMODEM_SOH: // 128字节包
case YMODEM_STX: // 1024字节包
{
uint16_t packet_size = (header == YMODEM_SOH) ? PACKET_SIZE_128 : PACKET_SIZE_1024;
uint16_t total_size = PACKET_HEADER_SIZE + packet_size + PACKET_TRAILER_SIZE;
// 接收包序号
packet[0] = header;
packet[1] = UART_ReceiveByte(100);
packet[2] = UART_ReceiveByte(100);
// 接收数据
for (uint16_t i = 0; i < packet_size + PACKET_TRAILER_SIZE; i++) {
packet[PACKET_HEADER_SIZE + i] = UART_ReceiveByte(100);
}
// 验证序号
if (packet[1] != (~packet[2] & 0xFF)) {
UART_SendByte(YMODEM_NAK);
break;
}
// 验证CRC
uint16_t crc = (packet[total_size - 2] << 8) | packet[total_size - 1];
uint16_t calc_crc = CRC16(&packet[PACKET_HEADER_SIZE], packet_size);
if (crc != calc_crc) {
UART_SendByte(YMODEM_NAK);
break;
}
// 处理文件名包(第0包)
if (packet[1] == 0 && !session_begin) {
session_begin = 1;
// 解析文件名和文件大小
char *filename = (char *)&packet[PACKET_HEADER_SIZE];
char *filesize = strchr(filename, '\0') + 1;
uint32_t size = atoi(filesize);
if (size > max_size) {
UART_SendByte(YMODEM_CAN);
UART_SendByte(YMODEM_CAN);
return 0; // 文件太大
}
UART_SendByte(YMODEM_ACK);
UART_SendByte(YMODEM_C); // 请求下一个包
break;
}
// 处理数据包
if (packet[1] == packet_number) {
// 写入Flash
uint16_t *data = (uint16_t *)&packet[PACKET_HEADER_SIZE];
uint16_t write_len = packet_size;
// 最后一包可能不足,按实际长度写入
if (received + packet_size > max_size) {
write_len = max_size - received;
}
FLASH_Unlock();
if (FLASH_WriteBuffer(current_addr, data, write_len) != 0) {
FLASH_Lock();
UART_SendByte(YMODEM_NAK);
return 0;
}
FLASH_Lock();
current_addr += write_len;
received += write_len;
packet_number++;
UART_SendByte(YMODEM_ACK);
} else if (packet[1] == (packet_number - 1)) {
n // 重复包,可能是ACK丢失
UART_SendByte(YMODEM_ACK);
} else {
UART_SendByte(YMODEM_NAK);
}
break;
}
case YMODEM_EOT: // 传输结束
UART_SendByte(YMODEM_NAK); // 第一次EOT,NAK
header = UART_ReceiveByte(1000);
if (header == YMODEM_EOT) {
UART_SendByte(YMODEM_ACK); // 第二次EOT,ACK
UART_SendByte(YMODEM_C); // 请求结束包
// 接收结束包(序号0)
header = UART_ReceiveByte(1000);
if (header == YMODEM_SOH) {
// 接收并丢弃结束包
for (uint16_t i = 0; i < 133; i++) {
UART_ReceiveByte(100);
}
UART_SendByte(YMODEM_ACK);
}
return received;
}
break;
case YMODEM_CAN: // 取消传输
UART_SendByte(YMODEM_ACK);
return 0;
default:
UART_SendByte(YMODEM_NAK);
break;
}
}
}
4.4 完整的Bootloader主程序
#include "stm32f10x.h"
#include <string.h>
#include <stdlib.h>
// 分区定义
#define BOOTLOADER_SIZE 0x4000 // 16KB
#define APP1_BASE_ADDR 0x08004000 // APP1起始地址
#define APP1_SIZE 0x6000 // 24KB
#define APP2_BASE_ADDR 0x0800A000 // APP2起始地址(升级缓存)
#define APP2_SIZE 0x6000 // 24KB
// 升级标志存储地址(使用APP2区最后一个字节)
#define UPDATE_FLAG_ADDR (APP2_BASE_ADDR + APP2_SIZE - 4)
// 升级标志值
#define UPDATE_FLAG_NONE 0xFFFFFFFF
#define UPDATE_FLAG_READY 0xAAAAAAAA
#define UPDATE_FLAG_DONE 0x55555555
// 超时时间(ms)
#define BOOT_TIMEOUT_MS 3000
// 函数声明
void SystemInit(void);
void GPIO_Init(void);
void UART_Init(void);
void Timer_Init(void);
void LED_Set(uint8_t on);
uint32_t YMODEM_Receive(uint32_t dest_addr, uint32_t max_size);
void JumpToApplication(uint32_t app_addr);
uint8_t VerifyFirmware(uint32_t addr, uint32_t size);
void CopyAPP2ToAPP1(void);
volatile uint32_t g_tick = 0;
int main(void) {
// 1. 系统初始化
SystemInit();
GPIO_Init();
UART_Init();
Timer_Init();
// 2. 显示Bootloader启动指示
LED_Set(1);
delay_ms(200);
LED_Set(0);
// 3. 检查升级标志
uint32_t update_flag = *(volatile uint32_t *)UPDATE_FLAG_ADDR;
if (update_flag == UPDATE_FLAG_READY) {
// 需要升级:APP2区有新固件
LED_Set(1); // 升级中常亮
// 擦除APP1区
FLASH_Unlock();
for (uint32_t addr = APP1_BASE_ADDR; addr < APP1_BASE_ADDR + APP1_SIZE; addr += FLASH_PAGE_SIZE) {
FLASH_ErasePage(addr);
}
// 将APP2复制到APP1
uint32_t *src = (uint32_t *)APP2_BASE_ADDR;
uint32_t *dst = (uint32_t *)APP1_BASE_ADDR;
for (uint32_t i = 0; i < APP1_SIZE / 4; i++) {
if (src[i] != 0xFFFFFFFF) {
FLASH_WriteHalfWord((uint32_t)&dst[i * 2], src[i] & 0xFFFF);
FLASH_WriteHalfWord((uint32_t)&dst[i * 2 + 2], (src[i] >> 16) & 0xFFFF);
}
}
// 清除升级标志
FLASH_ErasePage(UPDATE_FLAG_ADDR & ~(FLASH_PAGE_SIZE - 1));
FLASH_Lock();
LED_Set(0);
}
// 4. 检查是否需要进入升级模式
// 方式1:按键检测(BOOT按键按下)
// 方式2:串口收到特定命令
// 方式3:超时检测(3秒内无操作则跳转)
uint8_t enter_upgrade = 0;
uint32_t boot_start = g_tick;
// 发送启动提示
printf("Bootloader v1.0\r\n");
printf("Press 'u' to enter upgrade mode, or wait %d ms to jump to APP...\r\n", BOOT_TIMEOUT_MS);
while (g_tick - boot_start < BOOT_TIMEOUT_MS) {
if (USART1->SR & USART_SR_RXNE) {
uint8_t ch = USART1->DR;
if (ch == 'u' || ch == 'U') {
enter_upgrade = 1;
break;
}
}
// LED闪烁提示等待
if ((g_tick - boot_start) % 500 < 250) {
LED_Set(1);
} else {
LED_Set(0);
}
}
if (enter_upgrade) {
// 进入升级模式
printf("Entering upgrade mode...\r\n");
LED_Set(1);
// 擦除APP2区
FLASH_Unlock();
for (uint32_t addr = APP2_BASE_ADDR; addr < APP2_BASE_ADDR + APP2_SIZE; addr += FLASH_PAGE_SIZE) {
FLASH_ErasePage(addr);
}
FLASH_Lock();
// 接收新固件到APP2区
uint32_t received = YMODEM_Receive(APP2_BASE_ADDR, APP2_SIZE);
if (received > 0) {
// 校验固件
if (VerifyFirmware(APP2_BASE_ADDR, received)) {
// 设置升级标志
FLASH_Unlock();
FLASH_WriteHalfWord(UPDATE_FLAG_ADDR, UPDATE_FLAG_READY & 0xFFFF);
FLASH_WriteHalfWord(UPDATE_FLAG_ADDR + 2, (UPDATE_FLAG_READY >> 16) & 0xFFFF);
FLASH_Lock();
printf("Upgrade successful! Rebooting...\r\n");
delay_ms(500);
// 软件复位
NVIC_SystemReset();
} else {
printf("Firmware verification failed!\r\n");
}
} else {
printf("Upgrade failed!\r\n");
}
// 升级失败,等待手动复位
while (1) {
LED_Set(1);
delay_ms(100);
LED_Set(0);
delay_ms(100);
}
}
// 5. 检查APP1是否有效
uint32_t app_msp = *(volatile uint32_t *)APP1_BASE_ADDR;
if ((app_msp & 0x2FFE0000) != 0x20000000) {
// MSP值无效,APP不存在或损坏
printf("APP invalid! Entering upgrade mode...\r\n");
while (1) {
LED_Set(1);
delay_ms(500);
LED_Set(0);
delay_ms(500);
}
}
// 6. 跳转到APP1
printf("Jumping to APP...\r\n");
delay_ms(100);
JumpToApplication(APP1_BASE_ADDR);
// 理论上不会执行到这里
while (1);
}
/**
* @brief 跳转到应用程序
*/
void JumpToApplication(uint32_t app_addr) {
// 1. 关闭所有中断
__disable_irq();
// 2. 关闭外设(防止APP初始化时冲突)
RCC->APB2ENR = 0;
RCC->APB1ENR = 0;
RCC->AHBENR = 0;
// 3. 清除所有中断挂起位
for (uint8_t i = 0; i < 8; i++) {
NVIC->ICER[i] = 0xFFFFFFFF;
NVIC->ICPR[i] = 0xFFFFFFFF;
}
// 4. 设置VTOR指向APP向量表
SCB->VTOR = app_addr;
// 5. 获取APP的MSP和Reset_Handler
uint32_t app_msp = *(volatile uint32_t *)app_addr;
uint32_t app_reset = *(volatile uint32_t *)(app_addr + 4);
// 6. 设置MSP
__set_MSP(app_msp);
// 7. 创建函数指针并跳转
void (*app_reset_handler)(void) = (void (*)(void))app_reset;
// 8. 数据同步和指令屏障
__DSB();
__ISB();
app_reset_handler();
}
/**
* @brief 校验固件(简单的CRC32或校验和)
*/
uint8_t VerifyFirmware(uint32_t addr, uint32_t size) {
// 检查向量表有效性
uint32_t msp = *(volatile uint32_t *)addr;
if ((msp & 0x2FFE0000) != 0x20000000) {
return 0; // MSP无效
}
uint32_t reset_handler = *(volatile uint32_t *)(addr + 4);
if ((reset_handler & 0xFF000000) != 0x08000000) {
return 0; // Reset_Handler地址不在Flash范围内
}
// TODO: 添加CRC32校验
return 1;
}
// SysTick中断处理(用于延时)
void SysTick_Handler(void) {
g_tick++;
}
void delay_ms(uint32_t ms) {
uint32_t start = g_tick;
while (g_tick - start < ms);
}
五、APP工程的配置要点
5.1 链接脚本修改
APP的链接脚本必须将起始地址改为APP1_BASE_ADDR:
MEMORY
{
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 20K
FLASH (rx) : ORIGIN = 0x08004000, LENGTH = 24K /* 从APP1区开始 */
}
_estack = ORIGIN(RAM) + LENGTH(RAM);
SECTIONS
{
.text :
{
. = ALIGN(4);
KEEP(*(.isr_vector)) /* 向量表保留 */
*(.text*)
*(.rodata*)
. = ALIGN(4);
} >FLASH
.data :
{
. = ALIGN(4);
_sdata = .;
*(.data*)
. = ALIGN(4);
_edata = .;
} >RAM AT> FLASH
.bss :
{
. = ALIGN(4);
_sbss = .;
*(.bss*)
*(COMMON)
. = ALIGN(4);
_ebss = .;
} >RAM
}
5.2 启动文件修改
APP的启动文件中,SystemInit()必须设置VTOR:
// system_stm32f10x.c
#define VECT_TAB_OFFSET 0x4000 // APP1偏移16KB
void SystemInit(void) {
// ... 时钟配置 ...
// 设置VTOR指向APP向量表
SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET;
// ... 其他初始化 ...
}
5.3 生成.bin文件
Keil MDK中,在Options for Target → User → After Build/Rebuild中勾选"Run #1",并添加命令:
fromelf --bin --output .\Objects\app.bin .\Objects\app.axf
这将生成纯二进制固件文件,用于YMODEM传输。
六、升级流程与故障处理

6.1 完整的OTA升级流程
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 服务器 │────►│ 设备端 │────►│ Bootloader │
│ (新固件) │ │ (接收固件) │ │ (写入APP2) │
└─────────────┘ └─────────────┘ └──────┬──────┘
│
▼
┌─────────────┐
│ 设置升级标志 │
│ 软件复位 │
└──────┬──────┘
│
▼
┌─────────────┐
│ Bootloader │
│ (复制APP2 │
│ 到APP1) │
└──────┬──────┘
│
▼
┌─────────────┐
│ 跳转APP1 │
│ (运行新固件)│
└─────────────┘
6.2 故障安全机制
| 故障场景 | 检测方法 | 恢复策略 |
|---|---|---|
| 传输中断 | 超时检测 | 保留旧版本,下次启动重新升级 |
| 固件损坏 | CRC32校验 | 拒绝写入,保持旧版本 |
| 写入失败 | Flash验证读取 | 擦除失败区域,重试或回滚 |
| 跳转失败 | MSP有效性检查 | 停留在Bootloader,等待重新升级 |
| 新固件Bug | 看门狗超时 | Bootloader捕获复位,提供回滚选项 |
双备份策略:APP2作为"暂存区",只有校验通过后才复制到APP1。这意味着即使升级过程中断电,下次上电后Bootloader会发现APP2的升级标志不完整,自动丢弃并继续使用APP1。
七、进阶话题
7.1 加密与签名
对于安全敏感的应用,固件传输和存储需要加密:
// AES-128加密固件(简化示例)
#include "aes.h"
void DecryptFirmware(uint8_t *input, uint8_t *output, uint32_t size, uint8_t *key) {
AES_ctx ctx;
AES_init_ctx(&ctx, key);
for (uint32_t i = 0; i < size; i += 16) {
memcpy(output + i, input + i, 16);
AES_ECB_decrypt(&ctx, output + i);
}
}
// RSA签名验证
uint8_t VerifySignature(uint8_t *firmware, uint32_t size, uint8_t *signature, uint8_t *public_key) {
// 使用mbedtls或tinycrypt库验证RSA-PSS签名
// 确保固件来自可信来源
}
7.2 差分升级
对于大固件(>1MB),完整传输浪费带宽。可以使用bsdiff算法生成差分包,设备端使用bspatch还原:
# 主机端生成差分包
bsdiff old_firmware.bin new_firmware.bin patch.bin
# 设备端应用差分包
bspatch old_firmware.bin new_firmware.bin patch.bin
7.3 A/B分区无缝切换
高级方案中,APP1和APP2都是完整的可运行分区,通过标志位切换当前活动分区:
#define ACTIVE_PARTITION_ADDR 0x0800BFFC // 活动分区标志
void JumpToActivePartition(void) {
uint32_t active = *(volatile uint32_t *)ACTIVE_PARTITION_ADDR;
uint32_t target_addr = (active == 1) ? APP2_BASE_ADDR : APP1_BASE_ADDR;
JumpToApplication(target_addr);
}
八、调试技巧
8.1 使用调试器观察跳转过程
在JumpToApplication()函数处设置断点,观察:
- VTOR寄存器:确保写入值等于APP1_BASE_ADDR
- MSP寄存器:确保加载了APP的栈顶值(应在RAM范围内)
- PC寄存器:确保跳转到了APP的Reset_Handler
8.2 使用LED作为状态指示
// Bootloader状态机
typedef enum {
STATE_BOOT, // 启动中 - LED快闪
STATE_UPGRADE, // 升级中 - LED常亮
STATE_VERIFY, // 校验中 - LED慢闪
STATE_JUMP, // 跳转中 - LED灭
STATE_ERROR // 错误 - LED双闪
} BootloaderState_t;
void LED_SetState(BootloaderState_t state) {
// 根据状态设置LED闪烁模式
}
8.3 串口打印调试信息
在Bootloader中保留USART1用于调试输出,但在跳转前必须关闭:
// 跳转前关闭串口,防止APP重新初始化时冲突
USART1->CR1 = 0; // 关闭USART
GPIOA->CRH &= ~(GPIO_CRH_MODE9 | GPIO_CRH_MODE10); // 释放PA9/PA10
九、总结
手写Bootloader是一次从应用层到系统层的跨越。通过这个过程,你将深入理解:
- Flash的物理特性:擦除以页为单位、写入以半字为单位、必须先擦后写
- 向量表的工作机制:VTOR寄存器如何决定中断去向
- 栈指针的生命周期:从Bootloader的MSP切换到APP的MSP
- 中断的安全关闭:
__disable_irq()、清除NVIC寄存器、DSB/ISB屏障 - 通信协议的可靠性:YMODEM的ACK/NAK重传机制、CRC校验
这些知识不仅在Bootloader开发中有用,在RTOS移植、安全启动、故障诊断等高级场景中同样是基础。
最后,记住Bootloader开发的黄金法则:永远假设最坏情况会发生。断电、通信中断、Flash损坏、恶意固件——你的Bootloader必须能够优雅地处理所有这些情况,确保设备"变砖"的概率趋近于零。
转载自:https://blog.csdn.net/u014727709/article/details/162237499
欢迎 👍点赞✍评论⭐收藏,欢迎指正
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)