Journal Article

嵌入式 Bootloader 深度解析:结构、配置与裁剪

全面解析嵌入式Bootloader的启动流程、内存布局、配置方法、代码裁剪、OTA升级和安全启动,涵盖MCU(STM32/MCU)和Linux(U-Boot)两大体系的实战要点。

13 min4 views

嵌入式 Bootloader 深度解析:结构、配置与裁剪

Bootloader 是嵌入式系统上电后执行的第一段代码,负责初始化硬件、加载应用程序并跳转执行。掌握其原理是嵌入式开发的必修课。


Bootloader 的核心职责

code
上电复位 → Bootloader → 硬件初始化 → 检查升级 → 加载APP → 跳转 → 应用程序
                ↑                        │
                └── 升级失败回退 ←────────┘
职责说明
硬件初始化时钟、PLL、SDRAM/DDR、Flash、串口
固件升级OTA/串口/USB/SD卡刷写新固件
启动选择选择从哪个分区启动(Factory / OTA)
安全校验签名验证、CRC校验防篡改
故障恢复升级失败回退到旧版本

MCU 体系 Bootloader(以 STM32 为例)

内存布局

code
┌─────────────────────────┐ 0x08000000
│     Bootloader          │  16KB (0x08000000 - 0x08003FFF)
│  (向量表 + 跳转逻辑)     │
├─────────────────────────┤ 0x08004000
│     App 分区 A           │  例如 224KB
│  (应用程序镜像)          │
├─────────────────────────┤
│     App 分区 B           │  224KB(用于 OTA 双备份)
├─────────────────────────┤
│     参数区               │  存储版本号、标志位
└─────────────────────────┘

链接脚本裁剪

ld
/* STM32F103CBT6: 128KB Flash, Bootloader 占 16KB */
MEMORY
{
    FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 16K
    RAM   (rwx) : ORIGIN = 0x20000000, LENGTH = 20K
}

/* APP 的链接脚本偏移 */
/* APP 从 0x08004000 开始 */
MEMORY
{
    FLASH (rx)  : ORIGIN = 0x08004000, LENGTH = 224K
    RAM   (rwx) : ORIGIN = 0x20000000, LENGTH = 20K
}

最简 Bootloader 实现

c
// bootloader.c
#define APP_ADDRESS    0x08004000

typedef void (*app_entry_t)(void);

int main(void) {
    // 1. 最小硬件初始化
    SystemClock_Config();   // 只配时钟,不配外设
    UART_Init();            // 串口用于调试输出
    
    // 2. 检查是否进入升级模式
    if (check_upgrade_mode()) {
        enter_upgrade_mode();
    }
    
    // 3. 校验 APP 镜像
    if (!verify_app_crc(APP_ADDRESS)) {
        // 校验失败,等待升级
        enter_upgrade_mode();
    }
    
    // 4. 跳转前清理环境
    __disable_irq();                         // 关全局中断
    for (int i = 0; i < 8; i++) {
        NVIC->ICER[i] = 0xFFFFFFFF;          // 清除所有中断使能
    }
    SysTick->CTRL = 0;                       // 停 SysTick
    SCB->VTOR = APP_ADDRESS;                 // 重映射向量表
    
    // 5. 设置 MSP 并跳转
    uint32_t *app_vector = (uint32_t *)APP_ADDRESS;
    __set_MSP(app_vector[0]);                // MSP = APP 栈顶
    app_entry_t app_entry = (app_entry_t)app_vector[1];
    app_entry();                             // 跳转,永不返回
}

APP 端配合修改

c
// app_main.c
int main(void) {
    // 重映射向量表(APP 分区有自己的向量表)
    SCB->VTOR = 0x08004000;
    
    // 正常初始化外设
    HAL_Init();
    SystemClock_Config();
    // ...
}
ld
/* APP 链接脚本必须设置正确的 FLASH 起始地址 */
MEMORY
{
    FLASH (rx)  : ORIGIN = 0x08004000, LENGTH = 224K
}

OTA 升级与双分区管理

分区策略

code
┌────────────────┐
│  Bootloader    │  ← 永不升级
├────────────────┤
│  APP_A (运行)  │  ← 当前版本
├────────────────┤
│  APP_B (备用)  │  ← 下载新固件到此
├────────────────┤
│  OTA Data      │  ← 存储升级状态
│  boot_count    │     (启动计数)
│  pending_ver   │     (待验证版本)
│  confirmed     │     (是否确认)
│  state         │     (状态机)
└────────────────┘

OTA 状态机

c
enum ota_state {
    OTA_IDLE,           // 无升级
    OTA_DOWNLOADING,    // 下载中
    OTA_PENDING,        // 已下载,待验证
    OTA_CONFIRMED,      // 验证通过
    OTA_FAILED,         // 升级失败
};

void check_ota_state(void) {
    ota_state_t state = read_ota_state();
    
    switch (state) {
        case OTA_PENDING:
            boot_count++;
            if (boot_count > MAX_BOOT_RETRY) {
                // 多次启动失败,回退
                revert_to_factory();
            } else {
                // 尝试启动新版本
                boot_new_app();
            }
            break;
        case OTA_CONFIRMED:
            // 正常运行,标记为永久
            mark_ota_complete();
            break;
        default:
            boot_current_app();
    }
}

升级流程

code
1. APP 下载新固件 → APP_B 分区
2. 写入 OTA Data (state = PENDING, boot_count = 0)
3. 软件复位
4. Bootloader 检测到 OTA_PENDING → 校验签名
5. 校验通过 → 交换 APP_A / APP_B 
6. 启动新版本
7. APP 启动正常 → 标记 CONFIRMED
8. APP 启动失败 → boot_count++,超阈值回退

U-Boot 体系 Bootloader(以嵌入式 Linux 为例)

启动流程

code
ROM Boot → SPL (二级加载) → U-Boot → Linux Kernel → RootFS

阶段职责

阶段职责典型大小
ROM Boot加载 SPL 到 SRAM芯片内置
SPL初始化 DDR、时钟,加载 U-Boot~32KB
U-Boot加载内核、设备树、文件系统~500KB

SPL 裁剪

SPL 运行在 SRAM 中,极其精简:

c
// SPL 最小实现(以 AM335x 为例)
void spl_main(void) {
    board_init_f();      // 最小初始化
    spl_ddr_init();      // DDR 初始化
    spl_mmc_load_image(); // 从 SD/eMMC 加载 U-Boot
    jump_to_image_no_args(&spl_image); // 跳转
}

U-Boot 环境变量配置

bash
# bootcmd - U-Boot 启动命令
setenv bootcmd 'mmc dev 0; fatload mmc 0:1 0x82000000 zImage; fatload mmc 0:1 0x88000000 am335x-boneblack.dtb; bootz 0x82000000 - 0x88000000'

# bootargs - 传给内核的参数
setenv bootargs 'console=ttyO0,115200n8 root=/dev/mmcblk0p2 rw rootwait'

# 保存
saveenv

U-Boot 裁剪

bash
# 配置时按需裁剪
make am335x_evm_defconfig

# 菜单式裁剪
make menuconfig

# 去掉不需要的功能
# ARM architecture  --->
#   [ ] Enable bootz          # 不需要 zImage
#   [*] Enable booti          # 只留 Image
# Device Drivers  --->
#   [ ] USB support           # 不需要 USB 启动
#   [ ] Networking support    # 不需要 TFTP/网络启动
#   [ ] MMC/SD card support   # 需要 SD 卡
#   [ ] SPI Flash support     # 需要 SPI Flash

裁剪效果:U-Boot 可以从 ~500KB 裁到 ~200KB。


Bootloader 防篡改与安全启动

code
Boot ROM (不可改写)
    │
    ▼  验证签名
SPL / Bootloader (公钥存储)
    │
    ▼  验证签名
APP / Kernel Image (签名文件)

实现方式

c
// 签名验证
bool verify_image(uint32_t addr, size_t len) {
    uint8_t *hash = compute_sha256((uint8_t*)addr, len);
    uint8_t *signature = read_signature(addr + len);  // 签名跟在固件后
    
    return rsa_verify(public_key, hash, signature);  // 公钥验签
}

密钥管理

  • 私钥由开发团队持有(离线签名)
  • 公钥烧录在 Bootloader 的 OTP/写保护区域
  • 启动时 Bootloader 用公钥验证 APP 签名

常用调试手段

c
// 进入 Bootloader 的条件判断
#define BOOT_MAGIC_ADDR  0x20000000  // SRAM 保留区

void check_boot_reason(void) {
    // 1. 按键检测
    if (HAL_GPIO_ReadPin(BOOT_BTN_PORT, BOOT_BTN_PIN) == 0) {
        enter_bootloader();
    }
    
    // 2. 魔法值触发(APP 写入 SRAM 后复位)
    if (*(uint32_t *)BOOT_MAGIC_ADDR == 0xDEADBEEF) {
        *(uint32_t *)BOOT_MAGIC_ADDR = 0;
        enter_bootloader();
    }
    
    // 3. OTA 状态触发
    if (read_ota_state() == OTA_PENDING) {
        attempt_ota_boot();
    }
}

Bootloader 裁剪 Checklist

裁掉原因节省
USB DFU只用串口 OTA~4KB
网络栈无需 TFTP/HTTP OTA~8KB
文件系统不需要 FAT/SD 卡启动~3KB
LCD 驱动Boot 阶段不需要显示~2KB
调试输出量产关闭串口打印~1KB
完整错误信息用错误码代替字符串~1KB

总结

要点MCU (STM32)Linux (U-Boot)
启动流程Bootloader → APPROM → SPL → U-Boot → Kernel
典型大小4KB ~ 32KBSPL 32KB + U-Boot 500KB
升级方式串口/SPI Flash OTASD/eMMC/TFTP/USB
安全启动CRC 校验 / RSA 签名FIT Image 签名 + TPM
双分区Factory + OTAA/B 分区 (Android/Linux)
链接脚本手动偏移地址defconfig + menuconfig

核心原则

  1. Bootloader 越简单越可靠
  2. OTA 必做双备份 + 回退机制
  3. 跳转前清理所有外设和中断
  4. APP 必须重映射自己的向量表
  5. 生产环境使用签名校验防篡改