1. 构建最小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区),防止跳转到非法地址