1. Bootloader基础框架搭建
任务目标
本案例实现一个最小可用的Bootloader,核心功能是:上电后完成必要的硬件初始化,验证应用程序的有效性,然后跳转到应用程序入口点执行。Bootloader与应用程序位于不同的Flash分区,通过链接脚本和编译选项实现地址隔离。
环境与依赖
- 硬件:STM32F407开发板(或其他Cortex-M4芯片,需调整链接脚本中的Flash地址)
- 工具链:arm-none-eabi-gcc + OpenOCD + GDB,或Keil MDK(需自行调整启动文件)
- 依赖:STM32标准外设库或HAL库(仅需时钟初始化和串口打印,用于调试信息输出)
实现原理
Bootloader与应用程序各自拥有独立的链接脚本。Bootloader被链接到Flash起始地址0x08000000,应用程序被链接到0x08010000(偏移64KB)。跳转的核心操作包括:
- 检查应用程序栈指针(位于0x08010000处)是否在RAM地址范围内,以判断应用程序是否有效;
- 关闭全局中断,确保跳转过程中不被中断干扰;
- 设置主栈指针(MSP)为应用程序的初始栈指针值;
- 取出应用程序复位向量(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"
验证方法
- 准备一个测试应用程序,将其链接到0x08010000,并在main函数中实现LED闪烁或串口输出"App running";
- 将应用程序烧录到0x08010000地址;
- 复位开发板,观察串口输出:应依次看到"Bootloader started"和"App found, jumping...",随后应用程序开始执行(LED闪烁或串口输出应用信息);
- 若Flash中无有效应用程序(全0xFF),串口应输出"No valid app, stay in bootloader",系统停留在Bootloader中。
通过条件:串口输出与上述预期一致,且应用程序代码能够正常运行,证明跳转成功。
常见问题
- 跳转后死机:检查应用程序的链接脚本起始地址是否为0x08010000;确认Bootloader中已设置SCB->VTOR;检查应用程序启动文件中是否定义了正确的向量表。
- 串口无输出:确认USART2的TX引脚(PA2)连接正确,波特率设置与终端一致;检查时钟配置是否正确。
- 栈指针校验失败:确认应用程序确实烧录在0x08010000处,且其向量表第一个32位字为有效的RAM地址。