在这里插入图片描述

每日一句正能量

“换位思考不只是理解别人,它的真正核心是放过自己。”
当你真正站到对方位置,你会发现他的行为自有逻辑,并非针对你。 于是你不再因别人的冷漠而自我攻击,不再因误解而耿耿于怀。理解别人,最终是解开了绑在自己心上的绳索。

一、引言:为什么每个嵌入式工程师都应该手写一次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),上电后直接运行。这种模式在量产和现场部署中面临三个致命问题:

  1. 无法远程更新:产品安装到客户现场后,没有JTAG接口可供烧录
  2. 无法回滚:新固件有Bug时,无法恢复到旧版本
  3. 无法差分升级:每次更新都传输完整固件,浪费带宽和时间

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操作的黄金法则

  1. 必须先解锁后操作:忘记解锁会导致硬件错误
  2. 必须先擦除后写入:Flash的"1"只能变"0",不能反向
  3. 擦除以页为单位:即使只改1字节,也要擦除整页(1KB)
  4. 写入以半字为单位:STM32F103不支持字节写入
  5. 操作完成后必须锁定:防止程序跑飞时误写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()函数处设置断点,观察:

  1. VTOR寄存器:确保写入值等于APP1_BASE_ADDR
  2. MSP寄存器:确保加载了APP的栈顶值(应在RAM范围内)
  3. 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是一次从应用层到系统层的跨越。通过这个过程,你将深入理解:

  1. Flash的物理特性:擦除以页为单位、写入以半字为单位、必须先擦后写
  2. 向量表的工作机制:VTOR寄存器如何决定中断去向
  3. 栈指针的生命周期:从Bootloader的MSP切换到APP的MSP
  4. 中断的安全关闭__disable_irq()、清除NVIC寄存器、DSB/ISB屏障
  5. 通信协议的可靠性:YMODEM的ACK/NAK重传机制、CRC校验

这些知识不仅在Bootloader开发中有用,在RTOS移植、安全启动、故障诊断等高级场景中同样是基础。

最后,记住Bootloader开发的黄金法则永远假设最坏情况会发生。断电、通信中断、Flash损坏、恶意固件——你的Bootloader必须能够优雅地处理所有这些情况,确保设备"变砖"的概率趋近于零。


转载自:https://blog.csdn.net/u014727709/article/details/162237499
欢迎 👍点赞✍评论⭐收藏,欢迎指正

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐