2. Bootloader启动流程实现

任务目标

实现一个最小可复现的Bootloader启动流程,包含三个核心阶段:硬件初始化、跳转条件判断(是否进入应用程序)、以及跳转到应用程序执行。本案例以STM32F103系列为目标平台,使用标准外设库编写,通过串口打印状态信息辅助验证。

环境与依赖

  • 硬件:STM32F103最小系统板(或兼容开发板),板载LED(PC13),USART1(PA9/PA10)
  • 软件:Keil MDK-ARM 5.x 或 IAR EWARM,STM32标准外设库 V3.5
  • 烧录工具:ST-Link / J-Link,配合STM32 ST-LINK Utility或Keil内置下载

实现方案

Bootloader与应用程序分别存放在不同的Flash区域。Bootloader位于0x08000000起始地址,应用程序起始地址设为0x08008000(偏移32KB)。启动流程如下:

  1. 硬件初始化:时钟、LED、串口、按键(PA0,用于强制进入Bootloader等待模式)
  2. 跳转条件判断:上电后检测应用程序区首个字是否为有效栈顶地址(范围在0x20000000~0x20010000内),且PA0按键未按下;若满足则跳转,否则停留在Bootloader并打印提示
  3. 应用程序启动:通过函数指针跳转到应用程序复位向量

关键完整代码

// main.c - Bootloader启动流程实现
#include "stm32f10x.h"
#include <stdio.h>

// 应用程序起始地址(偏移32KB)
#define APP_START_ADDR   0x08008000
// 应用程序栈顶地址有效范围(STM32F103内置SRAM 20KB~64KB)
#define SRAM_BASE        0x20000000
#define SRAM_END         0x20010000

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

// 串口发送一个字符(重定向printf)
int fputc(int ch, FILE *f) {
    while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
    USART_SendData(USART1, (uint8_t)ch);
    return ch;
}

// 硬件初始化:时钟、LED、串口、按键
static void Hardware_Init(void) {
    GPIO_InitTypeDef GPIO_InitStructure;
    USART_InitTypeDef USART_InitStructure;
    NVIC_InitTypeDef NVIC_InitStructure;

    // 使能时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOC |
                           RCC_APB2Periph_USART1 | RCC_APB2Periph_AFIO, ENABLE);

    // LED(PC13)推挽输出
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;
    GPIO_Init(GPIOC, &GPIO_InitStructure);
    GPIO_SetBits(GPIOC, GPIO_Pin_13);  // 默认熄灭

    // 按键PA0下拉输入(按下为高电平)
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    // USART1: PA9(TX) PA10(RX), 115200-8-N-1
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    USART_InitStructure.USART_BaudRate = 115200;
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;
    USART_InitStructure.USART_StopBits = USART_StopBits_1;
    USART_InitStructure.USART_Parity = USART_Parity_No;
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    USART_InitStructure.USART_Mode = USART_Mode_RX | USART_Mode_TX;
    USART_Init(USART1, &USART_InitStructure);
    USART_Cmd(USART1, ENABLE);
}

// 跳转条件判断:返回1则跳转,返回0则停留在Bootloader
static uint8_t Check_JumpCondition(void) {
    uint32_t app_stack = *(volatile uint32_t *)APP_START_ADDR;

    // 条件1:PA0未按下(低电平)
    if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) == SET) {
        return 0;  // 按键按下,强制停留在Bootloader
    }
    // 条件2:应用程序区栈顶地址有效
    if ((app_stack < SRAM_BASE) || (app_stack >= SRAM_END)) {
        return 0;  // 栈顶无效,无有效应用程序
    }
    return 1;
}

// 跳转到应用程序
static void Jump_To_Application(void) {
    uint32_t app_entry = *(volatile uint32_t *)(APP_START_ADDR + 4);
    pFunction jump_func = (pFunction)app_entry;

    printf("Jump to App @0x%08X\r\n", (unsigned int)app_entry);

    // 关闭全局中断
    __disable_irq();
    // 设置主栈指针
    __set_MSP(*(volatile uint32_t *)APP_START_ADDR);
    // 跳转
    jump_func();
}

int main(void) {
    Hardware_Init();
    printf("\r\n=== Bootloader v1.0 ===\r\n");

    // 闪烁LED 3次,提示进入Bootloader
    for (int i = 0; i < 3; i++) {
        GPIO_ResetBits(GPIOC, GPIO_Pin_13);
        for (volatile uint32_t d = 0; d < 200000; d++);
        GPIO_SetBits(GPIOC, GPIO_Pin_13);
        for (volatile uint32_t d = 0; d < 200000; d++);
    }

    if (Check_JumpCondition()) {
        printf("Valid App found, jumping...\r\n");
        Jump_To_Application();
    } else {
        printf("Stay in Bootloader (key pressed or no valid app)\r\n");
        // 停留在Bootloader:LED常亮指示
        GPIO_ResetBits(GPIOC, GPIO_Pin_13);
        while (1);
    }
}

工程配置要点

  • 在Keil中设置IROM1起始地址为0x08000000,大小为0x8000(32KB),用于Bootloader
  • 应用程序工程需设置IROM1起始地址为0x08008000,大小根据实际调整
  • 两个工程需分别编译烧录:先烧录Bootloader到0x08000000,再烧录应用程序到0x08008000
  • 若使用标准外设库,需在工程中添加stm32f10x_gpio.c、stm32f10x_usart.c、stm32f10x_rcc.c源文件

验证方法

  1. 无应用程序场景:仅烧录Bootloader,上电后串口输出Stay in Bootloader,LED常亮,程序不跳转
  2. 有效应用程序场景:在0x08008000处烧录一个简单的LED闪烁程序(其工程IROM1起始地址设为0x08008000),上电后串口输出Valid App found, jumping...,随后应用程序开始运行(LED按应用逻辑闪烁)
  3. 强制停留场景:上电时按住PA0按键,即使存在有效应用程序,串口输出Stay in Bootloader,不跳转

通过判定标准:上述三种场景的行为与预期完全一致,串口输出正确,LED状态符合描述,即判定Bootloader启动流程实现正确。

常见问题

  • 跳转后程序跑飞:确认应用程序工程的中断向量表已重映射(在应用程序的SystemInit或main开头调用NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x8000))
  • 串口无输出:检查USART1引脚是否与板载调试串口冲突,部分开发板需要断开ST-Link的TX/RX跳线
  • 栈顶地址判断失败:确认应用程序确实烧录在0x08008000处,且其首字为有效的SRAM地址(由链接脚本自动生成)