1. Bootloader基础框架搭建

Bootloader基础框架搭建 学习卡片

任务目标

本案例实现一个最小可用的Bootloader,核心功能是:上电后完成必要的硬件初始化,验证应用程序的有效性,然后跳转到应用程序入口点执行。Bootloader与应用程序位于不同的Flash分区,通过链接脚本和编译选项实现地址隔离。

环境与依赖

  • 硬件:STM32F407开发板(或其他Cortex-M4芯片,需调整链接脚本中的Flash地址)
  • 工具链:arm-none-eabi-gcc + OpenOCD + GDB,或Keil MDK(需自行调整启动文件)
  • 依赖:STM32标准外设库或HAL库(仅需时钟初始化和串口打印,用于调试信息输出)

实现原理

Bootloader与应用程序各自拥有独立的链接脚本。Bootloader被链接到Flash起始地址0x08000000,应用程序被链接到0x08010000(偏移64KB)。跳转的核心操作包括:

  1. 检查应用程序栈指针(位于0x08010000处)是否在RAM地址范围内,以判断应用程序是否有效;
  2. 关闭全局中断,确保跳转过程中不被中断干扰;
  3. 设置主栈指针(MSP)为应用程序的初始栈指针值;
  4. 取出应用程序复位向量(0x08010004处的值),将其作为函数指针调用,完成跳转。

关键完整代码

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

// 应用程序起始地址(需与app链接脚本一致)
#define APP_FLASH_BASE  0x08010000u
// RAM地址范围(用于栈指针有效性检查)
#define SRAM_BASE       0x20000000u
#define SRAM_SIZE       0x00020000u  // 128KB

// 函数指针类型:指向应用程序复位向量
typedef void (*pFunction)(void);

// 简单延时(用于串口打印稳定)
static void delay_volatile(volatile uint32_t count) {
    while (count--) {
        __NOP();
    }
}

// 串口发送字符串(USART2,PA2=TX,115200-8-N-1)
static void uart2_init(void) {
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
    RCC->APB1ENR |= RCC_APB1ENR_USART2EN;
    
    GPIOA->AFR[0] |= (7u << 8);   // PA2 - AF7 (USART2_TX)
    GPIOA->MODER  |= (2u << 4);   // PA2 - Alternate Function
    GPIOA->OSPEEDR|= (3u << 4);   // High speed
    
    USART2->BRR = 84000000u / 115200u;  // APB1=42MHz, 需×2? 此处按84MHz计算
    USART2->CR1 = USART_CR1_UE | USART_CR1_TE;
}

static void uart2_send_str(const char *str) {
    while (*str) {
        while (!(USART2->SR & USART_SR_TXE)) {}
        USART2->DR = *str++;
    }
}

// 检查应用程序是否有效:栈指针必须在RAM范围内
static int is_app_valid(void) {
    uint32_t app_sp = *(volatile uint32_t *)APP_FLASH_BASE;
    return (app_sp >= SRAM_BASE) && (app_sp < (SRAM_BASE + SRAM_SIZE));
}

// 跳转到应用程序
static void jump_to_app(void) {
    uint32_t app_sp = *(volatile uint32_t *)APP_FLASH_BASE;
    uint32_t app_pc = *(volatile uint32_t *)(APP_FLASH_BASE + 4u);
    pFunction app_entry = (pFunction)app_pc;
    
    // 关闭全局中断
    __disable_irq();
    
    // 设置主栈指针
    __set_MSP(app_sp);
    
    // 可选:设置VTOR指向应用程序向量表(Cortex-M3/M4)
    SCB->VTOR = APP_FLASH_BASE;
    
    // 跳转
    app_entry();
    
    // 不应返回
    while (1) {}
}

int main(void) {
    // 初始化时钟(使用默认HSI 16MHz,实际工程应配置PLL)
    uart2_init();
    delay_volatile(100000);
    
    uart2_send_str("Bootloader started\r\n");
    
    if (is_app_valid()) {
        uart2_send_str("App found, jumping...\r\n");
        delay_volatile(1000);
        jump_to_app();
    } else {
        uart2_send_str("No valid app, stay in bootloader\r\n");
    }
    
    while (1) {
        // Bootloader停留模式(可在此处添加固件下载逻辑)
    }
}

链接脚本(bootloader.ld)

/* 最小Bootloader链接脚本 - STM32F407 */
MEMORY
{
    FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 64K
    RAM   (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}

SECTIONS
{
    .isr_vector :
    {
        KEEP(*(.isr_vector))
    } > FLASH

    .text :
    {
        *(.text*)
        *(.rodata*)
    } > FLASH

    .data :
    {
        _sdata = .;
        *(.data*)
        _edata = .;
    } > RAM AT> FLASH

    .bss :
    {
        _sbss = .;
        *(.bss*)
        *(COMMON)
        _ebss = .;
    } > RAM
}

编译与烧录

# 编译Bootloader
arm-none-eabi-gcc -c -mcpu=cortex-m4 -mthumb -O2 -I. bootloader.c -o bootloader.o
arm-none-eabi-gcc -c -mcpu=cortex-m4 -mthumb startup_stm32f407.s -o startup.o
arm-none-eabi-ld -T bootloader.ld bootloader.o startup.o -o bootloader.elf
arm-none-eabi-objcopy -O binary bootloader.elf bootloader.bin

# 烧录到0x08000000
openocd -f interface/stlink.cfg -f target/stm32f4x.cfg -c "program bootloader.bin 0x08000000 verify reset exit"

验证方法

  1. 准备一个测试应用程序,将其链接到0x08010000,并在main函数中实现LED闪烁或串口输出"App running";
  2. 将应用程序烧录到0x08010000地址;
  3. 复位开发板,观察串口输出:应依次看到"Bootloader started"和"App found, jumping...",随后应用程序开始执行(LED闪烁或串口输出应用信息);
  4. 若Flash中无有效应用程序(全0xFF),串口应输出"No valid app, stay in bootloader",系统停留在Bootloader中。

通过条件:串口输出与上述预期一致,且应用程序代码能够正常运行,证明跳转成功。

常见问题

  • 跳转后死机:检查应用程序的链接脚本起始地址是否为0x08010000;确认Bootloader中已设置SCB->VTOR;检查应用程序启动文件中是否定义了正确的向量表。
  • 串口无输出:确认USART2的TX引脚(PA2)连接正确,波特率设置与终端一致;检查时钟配置是否正确。
  • 栈指针校验失败:确认应用程序确实烧录在0x08010000处,且其向量表第一个32位字为有效的RAM地址。