嵌入式 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 → APP | ROM → SPL → U-Boot → Kernel |
| 典型大小 | 4KB ~ 32KB | SPL 32KB + U-Boot 500KB |
| 升级方式 | 串口/SPI Flash OTA | SD/eMMC/TFTP/USB |
| 安全启动 | CRC 校验 / RSA 签名 | FIT Image 签名 + TPM |
| 双分区 | Factory + OTA | A/B 分区 (Android/Linux) |
| 链接脚本 | 手动偏移地址 | defconfig + menuconfig |
核心原则:
- Bootloader 越简单越可靠
- OTA 必做双备份 + 回退机制
- 跳转前清理所有外设和中断
- APP 必须重映射自己的向量表
- 生产环境使用签名校验防篡改
评论 (0)