1. 构建最小Bootloader并验证启动流程

构建最小Bootloader并验证启动流程 学习卡片

任务目标

编写一个最小Bootloader,运行于QEMU模拟的ARM Cortex-M3环境(STM32F103),实现从Flash起始地址加载应用程序并跳转执行,验证基本启动流程。Bootloader自身完成必要的硬件初始化和栈指针重定位,应用区为一个简单的LED闪烁程序(通过GPIO翻转模拟)。

环境与依赖

  • ARM交叉编译器:arm-none-eabi-gcc
  • QEMU系统模拟器:qemu-system-arm
  • 无需真实硬件,全程在QEMU中运行验证

内存布局设计

  • Bootloader区:0x08000000 ~ 0x08003FFF(16KB)
  • 应用程序区:0x08004000 ~ 0x08007FFF(16KB)
  • 向量表偏移:应用区向量表位于0x08004000

完整实现代码

// bootloader.c —— 最小Bootloader实现
#include <stdint.h>

#define APP_BASE_ADDR   0x08004000UL
#define FLASH_BASE      0x08000000UL

// 应用程序向量表结构
typedef void (*pFunction)(void);
typedef struct {
    uint32_t initial_sp;
    pFunction reset_handler;
} VectorTable;

// 简单延时函数(用于演示跳转后的LED翻转)
static void delay(volatile uint32_t count) {
    while (count--) {
        __asm volatile("nop");
    }
}

// 应用程序入口(实际应用中此函数位于APP区)
// 此处为演示,将APP代码链接到0x08004000
__attribute__((section(".app_code")))
void app_main(void) {
    volatile uint32_t *gpio_odr = (uint32_t *)0x4001080C; // GPIOC_ODR
    while (1) {
        *gpio_odr ^= 0x00002000;  // 翻转PC13
        delay(500000);
    }
}

// Bootloader入口
int main(void) {
    // 1. 使能GPIOC时钟(RCC_APB2ENR, bit4)
    volatile uint32_t *rcc_apb2enr = (uint32_t *)0x40021018;
    *rcc_apb2enr |= (1UL << 4);

    // 2. 配置PC13为推挽输出(GPIOC_CRH, bit20-23)
    volatile uint32_t *gpio_crh = (uint32_t *)0x40010804;
    *gpio_crh = (*gpio_crh & ~(0xFUL << 20)) | (0x2UL << 20);

    // 3. 跳转到应用程序
    VectorTable *app_vector = (VectorTable *)APP_BASE_ADDR;

    // 验证向量表有效性(栈指针应在RAM区,复位向量在Flash区)
    if ((app_vector->initial_sp & 0x2FFE0000) == 0x20000000 &&
        (app_vector->reset_handler & 0x1FF00000) == 0x08000000) {
        // 设置主栈指针
        __asm volatile("msr msp, %0" : : "r" (app_vector->initial_sp));
        // 跳转到应用复位向量
        app_vector->reset_handler();
    } else {
        // 向量表无效,进入错误处理
        while (1) {
            // 点亮PC13表示错误
            volatile uint32_t *gpio_odr = (uint32_t *)0x4001080C;
            *gpio_odr |= 0x00002000;
        }
    }

    return 0;
}

// 启动代码(最小启动文件)
__attribute__((naked, section(".isr_vector")))
void reset_handler(void) {
    __asm volatile(
        "ldr sp, =0x20005000\n"   // 设置Bootloader栈顶
        "bl main\n"
        "b .\n"
    );
}

// 链接脚本片段(完整脚本见下方说明)
// MEMORY { FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 16K }
//         RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 20K
// SECTIONS {
//   .isr_vector : { KEEP(*(.isr_vector)) } > FLASH
//   .text : { *(.text*) *(.rodata*) } > FLASH
//   .app_code : { *(.app_code) } > FLASH
//   .data : { *(.data*) } > RAM AT> FLASH
//   .bss : { *(.bss*) } > RAM
// }

编译与链接

# 编译Bootloader(含APP代码,链接到同一镜像)
arm-none-eabi-gcc -c -mcpu=cortex-m3 -mthumb -O2 -o bootloader.o bootloader.c
arm-none-eabi-ld -T linker.ld -nostdlib -o bootloader.elf bootloader.o
arm-none-eabi-objcopy -O binary bootloader.elf bootloader.bin

# 生成QEMU可加载的镜像(将bin填充到0x08000000)
dd if=/dev/zero of=flash.bin bs=1 count=131072
dd if=bootloader.bin of=flash.bin conv=notrunc

运行与验证

# 启动QEMU(STM32F103-SO8开发板模拟)
qemu-system-arm -M stm32f103-so8 -kernel flash.bin -nographic -monitor none -serial stdio

# 观察串口输出(应看到LED翻转的调试信息)
# 在QEMU monitor中(Ctrl+A C)执行:
# info registers  查看PC是否跳转到0x08004000+1(Thumb模式)
# x/4xw 0x08004000  查看应用向量表内容

通过判定标准

  • QEMU启动后无异常退出,程序持续运行
  • 通过info registers观察PC值位于0x08004000~0x08007FFF区间(应用代码区)
  • GPIOC_ODR寄存器(0x4001080C)的值周期性翻转(0x00002000 ↔ 0x00000000),证明应用代码正在执行
  • 若向量表校验失败,PC将停留在Bootloader错误循环中,GPIOC_ODR保持0x00002000不变

关键说明

  • 本案例将Bootloader和APP编译为单一镜像,通过链接脚本将app_main放置在0x08004000起始处,模拟真实场景中APP独立编译后烧录到该地址的效果
  • 实际工程中APP应单独编译链接到0x08004000,Bootloader通过UART或I2C接收固件并写入Flash;本案例聚焦启动流程验证,故简化了固件分发过程
  • 向量表校验条件:栈指针高16位为0x2000(RAM区),复位向量高8位为0x08(Flash区),防止跳转到非法地址