3. 启动流程分析

启动流程分析 学习卡片

任务目标

本案例基于STM32F407平台,通过OpenOCD+GDB单步调试,观察Bootloader从复位到跳转前的完整启动路径。重点验证两个核心机制:栈指针(SP)初始化与中断向量表重定位。完成本案例后,你应能解释:复位后CPU如何获得初始SP和PC、向量表如何从Flash搬运到RAM、以及重定位后中断响应路径的变化。

环境与依赖

  • 硬件:STM32F407VET6开发板(或任意F4系列),ST-Link V2调试器
  • 软件:OpenOCD 0.11+、ARM GCC工具链(arm-none-eabi-gdb)、Minicom(串口可选)
  • 工程文件:boot_startup/目录,包含startup_stm32f407xx.s(启动文件)、main.c、linker.ld、Makefile

关键实现

以下代码是Bootloader启动流程的最小可调试实现。核心逻辑位于main.c:复位后先执行SystemInit()(设置时钟),随后调用vector_relocate()将向量表从Flash复制到RAM低地址,并设置SCB->VTOR指向RAM新表。最后通过led_blink()制造可观察的运行状态,便于在调试器中确认重定位生效。

// main.c - Bootloader启动流程最小实现
#include "stm32f4xx.h"

// 向量表原始位置(Flash起始)
extern uint32_t _estack;          // 栈顶,由链接脚本提供
extern uint32_t _svector;         // 向量表Flash源地址

#define VTOR_ADDR   (0x20000000U) // RAM起始,向量表重定位目标

static void vector_relocate(void) {
    uint32_t i;
    uint32_t *src = &_svector;
    uint32_t *dst = (uint32_t *)VTOR_ADDR;
    
    // 复制整个向量表(STM32F407共97个向量:1个SP + 96个异常/中断)
    for (i = 0; i < 97; i++) {
        dst[i] = src[i];
    }
    
    // 设置VTOR寄存器指向RAM中的新向量表
    SCB->VTOR = VTOR_ADDR;
    
    // 内存屏障,确保VTOR写入完成后再响应中断
    __DSB();
}

static void led_blink(void) {
    // 初始化PD12(板载LED)
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIODEN;
    GPIOD->MODER |= (1U << (12 * 2));  // PD12设为输出
    
    while (1) {
        GPIOD->ODR ^= (1U << 12);      // 翻转LED
        for (volatile uint32_t i = 0; i < 500000; i++); // 软件延时
    }
}

int main(void) {
    // 1. 关闭全局中断(调试观察更清晰)
    __disable_irq();
    
    // 2. 系统时钟初始化(默认使用HSI 16MHz)
    SystemInit();
    
    // 3. 向量表重定位到RAM
    vector_relocate();
    
    // 4. 重新使能中断(此时中断将从RAM向量表取址)
    __enable_irq();
    
    // 5. 主循环(LED闪烁证明程序正常运行)
    led_blink();
    
    // 不会到达这里
    return 0;
}

链接脚本关键段定义(linker.ld片段):

/* 向量表Flash源地址符号 */
.section .isr_vector : {
    _svector = .;
    KEEP(*(.isr_vector))
} >FLASH

/* 栈顶地址符号 */
_estack = ORIGIN(RAM) + LENGTH(RAM);

编译与烧录

# 编译(生成boot.elf和boot.hex)
make

# 启动OpenOCD(终端1)
openocd -f interface/stlink-v2.cfg -f target/stm32f4x.cfg

# 烧录并启动GDB(终端2)
arm-none-eabi-gdb boot.elf
(gdb) target remote localhost:3333
(gdb) load
(gdb) monitor reset halt

单步调试与验证

按以下步骤观察启动流程,每步记录寄存器值并对照预期:

  1. 复位后初始状态:执行monitor reset halt后,GDB显示PC=0x00000000。此时sp寄存器尚未初始化(值为0或随机)。
  2. 第一条指令:stepi执行一次。观察sp被加载为_estack的值(应为0x20020000,即RAM末尾)。这是CPU硬件自动从Flash地址0x00000000读取初始SP。
  3. 第二条指令:再次stepi,PC跳转到Reset_Handler(地址0x08000000+偏移)。这是从Flash地址0x00000004读取的复位向量。
  4. 向量表复制:在vector_relocate()的for循环处设断点,continue执行。观察dst指向0x20000000,循环结束后检查内存:x/4wx 0x20000000,应看到与Flash向量表(x/4wx 0x08000000)相同的值。
  5. VTOR设置:单步执行到SCB->VTOR = VTOR_ADDR之后,执行p/x SCB->VTOR,确认值为0x20000000。
  6. 中断响应验证:在led_blink()的while循环中运行,触发Systick中断(若已使能)。在GDB中执行info registers观察PC跳转地址——若VTOR生效,PC应跳转到0x20000000+向量偏移(RAM区),而非0x08000000+偏移(Flash区)。

通过条件:

  • 步骤2中SP值等于链接脚本定义的_estack(0x20020000)
  • 步骤4中RAM地址0x20000000处的前4字节与Flash地址0x08000000处完全一致(均为初始SP值)
  • 步骤5中VTOR寄存器值为0x20000000
  • 步骤6中中断响应后PC落在0x20000000~0x20000184范围内(RAM向量表区域),而非Flash地址

常见问题

  • VTOR设置后程序跑飞:检查向量表复制是否完整(97个字),以及__DSB()是否执行。若复制不完整,中断会从RAM中未初始化区域取址。
  • GDB无法单步:确认OpenOCD已连接且目标板供电正常。执行monitor reset halt后应看到target halted提示。
  • LED不闪烁:检查GPIOD时钟使能是否在led_blink()内完成(本实现已包含),以及延时循环是否被优化掉(使用-O0编译)。
Flash (0x08000000) 向量表(97项) [0] 初始SP [1] Reset_Handler [2] NMI [3] HardFault ... CPU 复位 硬件自动读取 ① SP = [0x08000000] ② PC = [0x08000004] RAM (0x20000000) 新向量表(复制后) [0] 初始SP(同Flash) [1] Reset_Handler [2] NMI [3] HardFault ... ③ 复制97个字 SCB->VTOR = 0x20000000 ④ 指向RAM 中断发生 PC跳转到RAM向量表 ⑤ 从RAM取中断向量 启动流程:① 硬件读Flash[0]→SP → ② 读Flash[4]→PC → ③ 软件复制向量表到RAM → ④ 设置VTOR → ⑤ 中断从RAM取址