跳转至

STM32启动过程深度分析

学习目标

完成本教程后,你将能够:

  • 理解STM32从上电到main函数的完整启动流程
  • 掌握启动文件(startup.s)的结构和作用
  • 了解堆栈的配置和初始化过程
  • 理解时钟系统的初始化步骤
  • 掌握中断向量表的设置方法
  • 能够使用调试器单步跟踪启动过程
  • 学会分析和修改启动代码

前置要求

在开始本教程之前,你需要:

知识要求: - 了解C语言和汇编语言基础 - 熟悉ARM Cortex-M架构基础 - 掌握内存架构知识(Flash、SRAM) - 了解中断和异常的基本概念

技能要求: - 能够使用STM32开发环境(Keil/IAR/STM32CubeIDE) - 会使用调试器进行单步调试 - 能够阅读汇编代码

推荐但非必需: - 有STM32项目开发经验 - 了解链接脚本基础 - 熟悉ARM指令集

准备工作

硬件准备

名称 数量 说明 参考链接
STM32开发板 1 STM32F103C8T6或类似 -
ST-Link调试器 1 用于下载和调试 -
USB数据线 1 连接开发板 -

软件准备

  • 开发环境:STM32CubeIDE v1.10+ 或 Keil MDK v5.30+
  • 调试工具:ST-Link驱动程序
  • 辅助工具:文本编辑器(查看启动文件)

环境配置

  1. 安装STM32CubeIDE或Keil MDK
  2. 安装ST-Link驱动
  3. 创建一个简单的STM32项目
  4. 确保能够正常下载和调试

概述

STM32微控制器的启动过程是一个精心设计的序列,从上电复位到执行main函数,涉及多个关键步骤。理解这个过程对于嵌入式开发至关重要,它不仅帮助我们排查启动问题,还能让我们优化系统初始化,甚至实现自定义的启动流程。

为什么要学习启动过程

  1. 问题排查
  2. 系统无法启动时能够定位问题
  3. 理解复位后的系统状态
  4. 调试启动阶段的错误

  5. 系统优化

  6. 优化启动时间
  7. 减少功耗
  8. 自定义初始化流程

  9. 深入理解

  10. 掌握ARM Cortex-M的工作机制
  11. 理解编译器和链接器的作用
  12. 了解硬件和软件的交互

  13. 高级应用

  14. 实现Bootloader
  15. 固件升级
  16. 多应用程序管理

STM32启动流程概览

上电/复位
读取栈顶地址 (0x00000000)
读取复位向量 (0x00000004)
执行启动文件 (startup.s)
    ├─ 设置栈指针
    ├─ 初始化中断向量表
    ├─ 复制Data段到SRAM
    ├─ 清零BSS段
    └─ 调用SystemInit()
        ├─ 配置时钟系统
        ├─ 配置Flash预取
        └─ 其他硬件初始化
        调用__libc_init_array()
        (C++全局对象构造)
        跳转到main()函数
        用户程序开始执行

第一部分:复位与向量表

复位后的第一步

当STM32上电或复位后,处理器会执行以下操作:

  1. 读取栈顶地址
  2. 从地址0x00000000读取初始栈指针值
  3. 设置主栈指针(MSP)

  4. 读取复位向量

  5. 从地址0x00000004读取复位处理函数地址
  6. 跳转到该地址开始执行

关键概念

  • 地址0x00000000:这个地址实际上被映射到Flash的起始地址(0x08000000)
  • Boot模式:通过BOOT引脚可以选择从不同位置启动(Flash、SRAM、系统存储器)

中断向量表结构

中断向量表是一个包含所有异常和中断处理函数地址的数组,位于Flash的起始位置。

向量表布局

地址偏移    异常/中断名称              说明
0x0000      初始栈指针值              MSP初始值
0x0004      Reset_Handler            复位处理函数
0x0008      NMI_Handler              不可屏蔽中断
0x000C      HardFault_Handler        硬件错误
0x0010      MemManage_Handler        内存管理错误
0x0014      BusFault_Handler         总线错误
0x0018      UsageFault_Handler       用法错误
0x001C      保留                      -
0x0020      保留                      -
0x0024      保留                      -
0x0028      保留                      -
0x002C      SVC_Handler              系统服务调用
0x0030      DebugMon_Handler         调试监视器
0x0034      保留                      -
0x0038      PendSV_Handler           可挂起的系统调用
0x003C      SysTick_Handler          系统滴答定时器
0x0040      WWDG_IRQHandler          窗口看门狗
0x0044      PVD_IRQHandler           电源电压检测
...         ...                      外设中断

启动文件中的向量表定义

以STM32F103为例,查看启动文件中的向量表定义:

; startup_stm32f103xb.s

; 向量表定义
                AREA    RESET, DATA, READONLY
                EXPORT  __Vectors
                EXPORT  __Vectors_End
                EXPORT  __Vectors_Size

__Vectors       DCD     __initial_sp              ; 栈顶地址
                DCD     Reset_Handler             ; 复位向量
                DCD     NMI_Handler               ; NMI处理函数
                DCD     HardFault_Handler         ; 硬件错误处理
                DCD     MemManage_Handler         ; 内存管理错误
                DCD     BusFault_Handler          ; 总线错误
                DCD     UsageFault_Handler        ; 用法错误
                DCD     0                         ; 保留
                DCD     0                         ; 保留
                DCD     0                         ; 保留
                DCD     0                         ; 保留
                DCD     SVC_Handler               ; SVC处理函数
                DCD     DebugMon_Handler          ; 调试监视器
                DCD     0                         ; 保留
                DCD     PendSV_Handler            ; PendSV处理函数
                DCD     SysTick_Handler           ; SysTick处理函数

                ; 外部中断
                DCD     WWDG_IRQHandler           ; 窗口看门狗
                DCD     PVD_IRQHandler            ; PVD通过EXTI检测
                DCD     TAMPER_IRQHandler         ; 侵入检测
                DCD     RTC_IRQHandler            ; RTC全局中断
                ; ... 更多中断向量

__Vectors_End

__Vectors_Size  EQU  __Vectors_End - __Vectors

代码说明

  • DCD:定义一个32位常量(Define Constant Data)
  • __initial_sp:栈顶地址,由链接脚本定义
  • Reset_Handler:复位处理函数的地址
  • EXPORT:导出符号,使其他文件可以引用

第二部分:启动文件详解

启动文件的作用

启动文件(startup.s)是用汇编语言编写的,它是系统启动的第一段代码,主要完成以下任务:

  1. 定义中断向量表
  2. 设置栈指针
  3. 初始化内存(Data段和BSS段)
  4. 调用系统初始化函数
  5. 跳转到main函数

Reset_Handler详解

Reset_Handler是复位后执行的第一个函数,让我们详细分析它的代码:

; Reset处理函数
Reset_Handler   PROC
                EXPORT  Reset_Handler             [WEAK]
                IMPORT  SystemInit
                IMPORT  __main

                ; 1. 调用SystemInit函数
                LDR     R0, =SystemInit
                BLX     R0

                ; 2. 跳转到__main(C库初始化和main函数)
                LDR     R0, =__main
                BX      R0
                ENDP

代码分析

  1. PROC/ENDP:定义一个函数(过程)
  2. EXPORT [WEAK]:导出符号,WEAK表示弱符号(可以被覆盖)
  3. IMPORT:导入外部符号
  4. LDR R0, =Symbol:加载符号地址到R0寄存器
  5. BLX R0:带链接的跳转,会保存返回地址
  6. BX R0:跳转到R0指向的地址

堆栈配置

启动文件中定义了堆和栈的大小:

; 栈配置
Stack_Size      EQU     0x00000400              ; 1KB栈空间

                AREA    STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem       SPACE   Stack_Size
__initial_sp                                     ; 栈顶标号


; 堆配置
Heap_Size       EQU     0x00000200              ; 512字节堆空间

                AREA    HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
Heap_Mem        SPACE   Heap_Size
__heap_limit

代码说明

  • EQU:定义常量
  • AREA:定义一个内存区域
  • NOINIT:不初始化(不清零)
  • READWRITE:可读写
  • ALIGN=3:8字节对齐(2^3)
  • SPACE:分配空间

栈的工作方式

高地址
    ┌─────────────┐
    │ __initial_sp│ ← 栈顶(初始SP值)
    ├─────────────┤
    │             │
    │   栈空间    │ ← 向下增长
    │   (1KB)     │
    │             │
    ├─────────────┤
    │ Stack_Mem   │ ← 栈底
    └─────────────┘
低地址

默认中断处理函数

启动文件还定义了默认的中断处理函数:

; 默认中断处理函数(无限循环)
Default_Handler PROC
                EXPORT  NMI_Handler               [WEAK]
                EXPORT  HardFault_Handler         [WEAK]
                EXPORT  MemManage_Handler         [WEAK]
                EXPORT  BusFault_Handler          [WEAK]
                EXPORT  UsageFault_Handler        [WEAK]
                EXPORT  SVC_Handler               [WEAK]
                EXPORT  DebugMon_Handler          [WEAK]
                EXPORT  PendSV_Handler            [WEAK]
                EXPORT  SysTick_Handler           [WEAK]
                EXPORT  WWDG_IRQHandler           [WEAK]
                EXPORT  PVD_IRQHandler            [WEAK]
                ; ... 更多中断处理函数

NMI_Handler
HardFault_Handler
MemManage_Handler
BusFault_Handler
UsageFault_Handler
SVC_Handler
DebugMon_Handler
PendSV_Handler
SysTick_Handler
WWDG_IRQHandler
PVD_IRQHandler
                ; ... 所有中断处理函数都指向这里
                B       .                         ; 无限循环
                ENDP

WEAK符号的作用

  • 如果用户定义了同名函数,会覆盖这个默认实现
  • 如果用户没有定义,就使用默认的无限循环
  • 这样可以避免链接错误

示例:自定义中断处理函数

// 在C代码中定义,会覆盖启动文件中的WEAK符号
void SysTick_Handler(void) {
    // 自定义的SysTick中断处理
    HAL_IncTick();
}

第三部分:SystemInit函数

SystemInit的作用

SystemInit函数在Reset_Handler中被调用,负责配置系统时钟和其他关键硬件。这个函数通常在system_stm32f1xx.c文件中定义。

SystemInit函数实现

/**
 * @brief  系统初始化函数
 * @note   在启动文件中被调用,在main函数之前执行
 * @param  None
 * @retval None
 */
void SystemInit(void)
{
    /* 1. 配置FPU(如果有) */
#if (__FPU_PRESENT == 1) && (__FPU_USED == 1)
    SCB->CPACR |= ((3UL << 10*2)|(3UL << 11*2));  /* 使能CP10和CP11 */
#endif

    /* 2. 复位RCC时钟配置为默认状态 */
    /* 使能HSI */
    RCC->CR |= (uint32_t)0x00000001;

    /* 复位SW, HPRE, PPRE1, PPRE2, ADCPRE和MCO位 */
    RCC->CFGR &= (uint32_t)0xF8FF0000;

    /* 复位HSEON, CSSON和PLLON位 */
    RCC->CR &= (uint32_t)0xFEF6FFFF;

    /* 复位HSEBYP位 */
    RCC->CR &= (uint32_t)0xFFFBFFFF;

    /* 复位PLLSRC, PLLXTPRE, PLLMUL和USBPRE/OTGFSPRE位 */
    RCC->CFGR &= (uint32_t)0xFF80FFFF;

    /* 3. 禁用所有中断 */
    RCC->CIR = 0x009F0000;

    /* 4. 配置向量表位置 */
#ifdef VECT_TAB_SRAM
    SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET; /* 向量表在SRAM */
#else
    SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; /* 向量表在Flash */
#endif
}

代码说明

  1. FPU配置
  2. 如果芯片有FPU(如Cortex-M4F),需要使能
  3. 配置协处理器访问控制寄存器

  4. 时钟复位

  5. 使能内部高速时钟(HSI)
  6. 复位所有时钟配置为默认值
  7. 确保系统处于已知状态

  8. 中断禁用

  9. 清除所有中断标志
  10. 禁用所有中断

  11. 向量表重定位

  12. 设置向量表偏移寄存器(VTOR)
  13. 支持向量表在SRAM中(用于调试或特殊应用)

时钟系统配置

在SystemInit之后,通常还会调用SystemClock_Config函数来配置系统时钟到所需频率:

/**
 * @brief  系统时钟配置
 * @note   配置系统时钟为72MHz(使用外部8MHz晶振)
 * @retval None
 */
void SystemClock_Config(void)
{
    RCC_OscInitTypeDef RCC_OscInitStruct = {0};
    RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

    /* 1. 配置振荡器 */
    RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
    RCC_OscInitStruct.HSEState = RCC_HSE_ON;
    RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
    RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
    RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
    RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;  // 8MHz * 9 = 72MHz

    if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) {
        Error_Handler();
    }

    /* 2. 配置系统时钟 */
    RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK
                                | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
    RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
    RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;   // 72MHz
    RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;    // 36MHz
    RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;    // 72MHz

    if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK) {
        Error_Handler();
    }
}

时钟配置流程

外部晶振(HSE)
    8MHz
   PLL倍频
    ×9
   SYSCLK
    72MHz
    ┌─────┴─────┐
    ↓           ↓
  AHB总线    APB总线
  72MHz    APB1: 36MHz
           APB2: 72MHz

第四部分:C运行时初始化

__main函数的作用

在Reset_Handler中,调用SystemInit后会跳转到__main函数。这个函数由C运行时库提供,负责:

  1. 复制Data段从Flash到SRAM
  2. 清零BSS段
  3. 调用C++全局对象的构造函数
  4. 最后跳转到用户的main函数

内存初始化过程

Data段复制

/**
 * @brief  复制Data段从Flash到SRAM
 * @note   这段代码通常由C运行时库自动完成
 */
void __scatterload_copy(void)
{
    extern uint32_t _sidata;  // Data段在Flash中的起始地址
    extern uint32_t _sdata;   // Data段在SRAM中的起始地址
    extern uint32_t _edata;   // Data段在SRAM中的结束地址

    uint32_t *src = &_sidata;
    uint32_t *dst = &_sdata;

    // 复制Data段
    while (dst < &_edata) {
        *dst++ = *src++;
    }
}

BSS段清零

/**
 * @brief  清零BSS段
 * @note   这段代码通常由C运行时库自动完成
 */
void __scatterload_zeroinit(void)
{
    extern uint32_t _sbss;  // BSS段起始地址
    extern uint32_t _ebss;  // BSS段结束地址

    uint32_t *dst = &_sbss;

    // 清零BSS段
    while (dst < &_ebss) {
        *dst++ = 0;
    }
}

内存布局示意

Flash (0x08000000)
├── 中断向量表
├── 代码段 (.text)
├── 只读数据 (.rodata)
└── Data段初始值 (.data)
        │ 复制
SRAM (0x20000000)
├── Data段 (.data)      ← 从Flash复制
├── BSS段 (.bss)        ← 清零
├── 堆 (Heap)
└── 栈 (Stack)

C++全局对象初始化

如果使用C++,还需要调用全局对象的构造函数:

/**
 * @brief  调用C++全局对象的构造函数
 * @note   由__libc_init_array函数完成
 */
void __libc_init_array(void)
{
    extern void (*__init_array_start[])(void);
    extern void (*__init_array_end[])(void);

    size_t count = __init_array_end - __init_array_start;

    // 调用所有构造函数
    for (size_t i = 0; i < count; i++) {
        __init_array_start[i]();
    }
}

完整的启动流程代码

将所有步骤整合,完整的启动流程如下:

/**
 * @brief  完整的启动流程(伪代码)
 */
void _start(void)
{
    // 1. 设置栈指针(由硬件自动完成)
    // SP = *0x00000000;

    // 2. 跳转到Reset_Handler(由硬件自动完成)
    // PC = *0x00000004;

    // 3. Reset_Handler开始执行
    Reset_Handler();
}

void Reset_Handler(void)
{
    // 4. 调用SystemInit
    SystemInit();

    // 5. 跳转到__main
    __main();
}

void __main(void)
{
    // 6. 复制Data段
    __scatterload_copy();

    // 7. 清零BSS段
    __scatterload_zeroinit();

    // 8. 调用C++构造函数
    __libc_init_array();

    // 9. 跳转到main函数
    main();

    // 10. main函数返回后(通常不应该返回)
    while(1);
}

第五部分:调试启动过程

使用调试器跟踪启动

通过调试器单步跟踪启动过程,可以深入理解每一步的执行。

步骤1:设置断点

  1. 在Reset_Handler设置断点

    打开启动文件 startup_stm32f1xx.s
    在 Reset_Handler 处设置断点
    

  2. 在SystemInit设置断点

    // system_stm32f1xx.c
    void SystemInit(void)
    {
        // 在函数入口设置断点
    }
    

  3. 在main函数设置断点

    // main.c
    int main(void)
    {
        // 在函数入口设置断点
    }
    

步骤2:启动调试会话

  1. 连接开发板和调试器
  2. 在IDE中点击"Debug"按钮
  3. 程序会停在Reset_Handler

步骤3:单步执行

在Reset_Handler中

Reset_Handler   PROC
                EXPORT  Reset_Handler             [WEAK]
                IMPORT  SystemInit
                IMPORT  __main

                LDR     R0, =SystemInit    ; ← 断点1:加载SystemInit地址
                BLX     R0                 ; ← 单步:跳转到SystemInit

                LDR     R0, =__main        ; ← 断点2:加载__main地址
                BX      R0                 ; ← 单步:跳转到__main
                ENDP

观察寄存器

在调试器中查看寄存器窗口:
- SP (栈指针): 应该指向栈顶地址
- PC (程序计数器): 当前执行的指令地址
- R0-R12: 通用寄存器
- LR (链接寄存器): 返回地址

步骤4:查看内存

查看向量表

内存地址: 0x08000000
内容:
0x08000000: 20005000  <- 栈顶地址 (0x20005000)
0x08000004: 080001C1  <- Reset_Handler地址
0x08000008: 080003B9  <- NMI_Handler地址
0x0800000C: 080003BB  <- HardFault_Handler地址
...

查看Data段复制

复制前:
Flash (0x08001000): 12 34 56 78
SRAM  (0x20000000): 00 00 00 00

复制后:
Flash (0x08001000): 12 34 56 78
SRAM  (0x20000000): 12 34 56 78  <- 已复制

步骤5:监控变量

全局变量初始化

// 在main.c中定义
uint32_t g_initialized = 0x12345678;  // Data段
uint32_t g_uninitialized;             // BSS段

// 在调试器中观察
// 进入main之前:
// g_initialized = 0x12345678  (已从Flash复制)
// g_uninitialized = 0x00000000 (已清零)

调试技巧

  1. 查看调用栈

    调用栈窗口显示:
    main() at main.c:100
    __main() at __main.c:50
    Reset_Handler() at startup.s:200
    

  2. 查看反汇编

    在反汇编窗口可以看到C代码对应的汇编指令
    有助于理解编译器的工作
    

  3. 使用断点条件

    // 只在特定条件下停止
    if (counter == 100) {
        __BKPT(0);  // 软件断点
    }
    

常见问题排查

问题1:程序不执行

检查项:
1. 向量表地址是否正确 (VTOR寄存器)
2. 栈指针是否有效
3. Reset_Handler地址是否正确
4. Flash是否正确烧录

问题2:HardFault错误

可能原因:
1. 栈溢出
2. 访问无效内存地址
3. 未对齐的内存访问
4. 除零错误

调试方法:
1. 查看HardFault状态寄存器
2. 检查LR寄存器(返回地址)
3. 查看调用栈

问题3:变量值不正确

检查项:
1. Data段是否正确复制
2. BSS段是否清零
3. 链接脚本配置是否正确
4. 内存地址是否冲突

第六部分:启动优化

优化启动时间

在某些应用中,快速启动非常重要。以下是一些优化技巧:

1. 简化SystemInit

/**
 * @brief  最小化的SystemInit
 * @note   只配置必要的硬件
 */
void SystemInit(void)
{
    /* 只配置必要的时钟 */
    RCC->CR |= RCC_CR_HSION;  // 使能HSI

    /* 跳过不必要的配置 */
    // 不配置外部时钟
    // 不配置PLL
    // 使用默认的HSI时钟(8MHz)

    /* 配置向量表 */
    SCB->VTOR = FLASH_BASE;
}

2. 延迟初始化

/**
 * @brief  延迟初始化外设
 * @note   在需要时才初始化
 */
int main(void)
{
    /* 快速启动,只初始化关键外设 */
    HAL_Init();

    /* 执行关键任务 */
    Critical_Task();

    /* 延迟初始化其他外设 */
    Peripheral_Init();

    while(1) {
        // 主循环
    }
}

3. 使用内部时钟

/**
 * @brief  使用HSI代替HSE
 * @note   省去等待外部晶振稳定的时间
 */
void Fast_Clock_Config(void)
{
    /* 使用HSI(内部8MHz) */
    RCC->CR |= RCC_CR_HSION;
    while(!(RCC->CR & RCC_CR_HSIRDY));

    /* 配置PLL(可选) */
    // HSI * 9 = 72MHz
}

4. 减少Data段大小

// 不推荐:大量初始化数据
uint8_t buffer[1024] = {1, 2, 3, ...};  // 需要从Flash复制

// 推荐:使用const或运行时初始化
const uint8_t buffer[1024] = {1, 2, 3, ...};  // 直接在Flash中
// 或
uint8_t buffer[1024];  // BSS段,只需清零

启动时间测量

/**
 * @brief  测量启动时间
 */
void Measure_Boot_Time(void)
{
    /* 在Reset_Handler开始时启动计时器 */
    DWT->CYCCNT = 0;
    DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;

    /* 在main函数中读取计数 */
    uint32_t cycles = DWT->CYCCNT;
    uint32_t time_us = cycles / (SystemCoreClock / 1000000);

    printf("启动时间: %d us\n", time_us);
}

低功耗启动

/**
 * @brief  低功耗启动配置
 */
void Low_Power_Boot(void)
{
    /* 1. 使用低速时钟 */
    RCC->CR |= RCC_CR_HSION;

    /* 2. 禁用不需要的外设时钟 */
    RCC->APB1ENR = 0;
    RCC->APB2ENR = 0;
    RCC->AHBENR = RCC_AHBENR_SRAMEN;  // 只保留SRAM时钟

    /* 3. 配置GPIO为模拟输入(最低功耗) */
    GPIOA->CRL = 0x00000000;
    GPIOA->CRH = 0x00000000;

    /* 4. 进入低功耗模式 */
    // 根据需要配置
}

第七部分:高级应用

自定义启动流程

在某些应用中,需要自定义启动流程,例如:

1. 实现简单的Bootloader

/**
 * @brief  Bootloader启动流程
 */
void Bootloader_Start(void)
{
    /* 1. 检查是否需要升级 */
    if (Check_Update_Flag()) {
        /* 执行固件升级 */
        Firmware_Update();
    }

    /* 2. 验证应用程序 */
    if (Verify_Application()) {
        /* 跳转到应用程序 */
        Jump_To_Application(APP_ADDRESS);
    } else {
        /* 应用程序无效,停留在Bootloader */
        Bootloader_Main();
    }
}

/**
 * @brief  跳转到应用程序
 * @param  app_address: 应用程序起始地址
 */
void Jump_To_Application(uint32_t app_address)
{
    typedef void (*pFunction)(void);
    pFunction Jump_To_App;
    uint32_t JumpAddress;

    /* 1. 检查栈指针是否有效 */
    if (((*(__IO uint32_t*)app_address) & 0x2FFE0000) == 0x20000000) {
        /* 2. 获取应用程序的Reset_Handler地址 */
        JumpAddress = *(__IO uint32_t*)(app_address + 4);
        Jump_To_App = (pFunction)JumpAddress;

        /* 3. 设置主栈指针 */
        __set_MSP(*(__IO uint32_t*)app_address);

        /* 4. 跳转到应用程序 */
        Jump_To_App();
    }
}

2. 多应用程序管理

/**
 * @brief  多应用程序启动选择
 */
void Multi_App_Boot(void)
{
    uint8_t app_select;

    /* 1. 读取应用程序选择标志 */
    app_select = Read_App_Select();

    /* 2. 根据选择跳转到不同应用 */
    switch(app_select) {
        case 1:
            Jump_To_Application(APP1_ADDRESS);
            break;
        case 2:
            Jump_To_Application(APP2_ADDRESS);
            break;
        default:
            Jump_To_Application(DEFAULT_APP_ADDRESS);
            break;
    }
}

3. 向量表重定位

/**
 * @brief  重定位向量表到SRAM
 * @note   用于在SRAM中运行代码
 */
void Relocate_Vector_Table(void)
{
    extern uint32_t _vector_table_start;
    extern uint32_t _vector_table_end;

    uint32_t *src = &_vector_table_start;
    uint32_t *dst = (uint32_t*)0x20000000;  // SRAM起始地址
    uint32_t size = &_vector_table_end - &_vector_table_start;

    /* 1. 复制向量表到SRAM */
    for (uint32_t i = 0; i < size; i++) {
        dst[i] = src[i];
    }

    /* 2. 重定位向量表 */
    SCB->VTOR = 0x20000000;
}

4. 启动时的自检

/**
 * @brief  启动时自检
 */
void Power_On_Self_Test(void)
{
    /* 1. RAM测试 */
    if (!RAM_Test()) {
        Error_Handler();
    }

    /* 2. Flash校验 */
    if (!Flash_CRC_Check()) {
        Error_Handler();
    }

    /* 3. 外设测试 */
    if (!Peripheral_Test()) {
        Error_Handler();
    }

    /* 4. 时钟测试 */
    if (!Clock_Test()) {
        Error_Handler();
    }
}

/**
 * @brief  RAM测试
 * @retval 1: 通过, 0: 失败
 */
int RAM_Test(void)
{
    volatile uint32_t *ram = (uint32_t*)0x20000000;
    uint32_t backup;

    /* 简单的读写测试 */
    backup = *ram;
    *ram = 0x55AA55AA;
    if (*ram != 0x55AA55AA) return 0;
    *ram = 0xAA55AA55;
    if (*ram != 0xAA55AA55) return 0;
    *ram = backup;

    return 1;
}

5. 安全启动

/**
 * @brief  安全启动流程
 */
void Secure_Boot(void)
{
    /* 1. 验证固件签名 */
    if (!Verify_Firmware_Signature()) {
        /* 签名验证失败,停止启动 */
        while(1);
    }

    /* 2. 检查调试接口 */
    if (Is_Debugger_Connected()) {
        /* 调试器连接,可能存在安全风险 */
        Handle_Debug_Mode();
    }

    /* 3. 配置内存保护 */
    Configure_MPU();

    /* 4. 启动应用程序 */
    Jump_To_Application(APP_ADDRESS);
}

/**
 * @brief  配置MPU保护关键区域
 */
void Configure_MPU(void)
{
    /* 禁用MPU */
    MPU->CTRL = 0;

    /* 配置区域0:保护Bootloader */
    MPU->RBAR = BOOTLOADER_BASE | MPU_REGION_NUMBER0;
    MPU->RASR = MPU_RASR_ENABLE_Msk | 
                MPU_RASR_SIZE_32KB |
                MPU_RASR_AP_RO;  // 只读

    /* 配置区域1:保护配置数据 */
    MPU->RBAR = CONFIG_BASE | MPU_REGION_NUMBER1;
    MPU->RASR = MPU_RASR_ENABLE_Msk | 
                MPU_RASR_SIZE_4KB |
                MPU_RASR_AP_RO;  // 只读

    /* 使能MPU */
    MPU->CTRL = MPU_CTRL_ENABLE_Msk | MPU_CTRL_PRIVDEFENA_Msk;
}

实践练习

练习1:修改栈大小

任务:将栈大小从1KB修改为2KB,并验证修改是否生效。

步骤

  1. 打开启动文件 startup_stm32f1xx.s
  2. 找到栈大小定义:
    Stack_Size      EQU     0x00000400  ; 1KB
    
  3. 修改为:
    Stack_Size      EQU     0x00000800  ; 2KB
    
  4. 重新编译并下载
  5. 在调试器中查看栈顶地址是否改变

验证代码

void Check_Stack_Size(void)
{
    extern uint32_t _estack;
    extern uint32_t _sstack;

    uint32_t stack_size = (uint32_t)&_estack - (uint32_t)&_sstack;
    printf("栈大小: %d 字节\n", stack_size);
}

练习2:添加自定义初始化

任务:在SystemInit中添加自定义的初始化代码。

步骤

  1. 打开 system_stm32f1xx.c
  2. 在SystemInit函数末尾添加:
    void SystemInit(void)
    {
        // ... 原有代码
    
        /* 自定义初始化 */
        Custom_Init();
    }
    
    void Custom_Init(void)
    {
        /* 初始化一个GPIO作为指示灯 */
        RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;  // 使能GPIOC时钟
        GPIOC->CRH &= ~(0xF << 20);          // 清除PC13配置
        GPIOC->CRH |= (0x3 << 20);           // PC13配置为推挽输出
        GPIOC->BSRR = (1 << 13);             // 点亮LED
    }
    
  3. 编译下载,观察LED是否在启动时点亮

练习3:测量启动时间

任务:使用DWT计数器测量从复位到main函数的时间。

实现代码

/* 在启动文件的Reset_Handler开始处添加 */
void Reset_Handler(void)
{
    /* 使能DWT */
    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
    DWT->CYCCNT = 0;
    DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;

    /* 原有的启动代码 */
    SystemInit();
    __main();
}

/* 在main函数开始处添加 */
int main(void)
{
    uint32_t cycles = DWT->CYCCNT;
    uint32_t time_us = cycles / (SystemCoreClock / 1000000);

    HAL_Init();

    printf("启动时间: %d us (%d cycles)\n", time_us, cycles);

    // ... 其他代码
}

练习4:实现简单的Bootloader跳转

任务:实现从Bootloader跳转到应用程序的功能。

Bootloader代码

#define APP_ADDRESS 0x08004000  // 应用程序起始地址

void Jump_To_App(void)
{
    typedef void (*pFunction)(void);
    pFunction Jump;
    uint32_t JumpAddress;

    /* 检查栈指针 */
    if (((*(__IO uint32_t*)APP_ADDRESS) & 0x2FFE0000) == 0x20000000) {
        /* 禁用所有中断 */
        __disable_irq();

        /* 复位所有外设 */
        HAL_DeInit();

        /* 获取应用程序的Reset_Handler */
        JumpAddress = *(__IO uint32_t*)(APP_ADDRESS + 4);
        Jump = (pFunction)JumpAddress;

        /* 设置栈指针 */
        __set_MSP(*(__IO uint32_t*)APP_ADDRESS);

        /* 跳转 */
        Jump();
    }
}

应用程序链接脚本修改

/* 修改Flash起始地址 */
MEMORY
{
    FLASH (rx)  : ORIGIN = 0x08004000, LENGTH = 112K
    RAM (rwx)   : ORIGIN = 0x20000000, LENGTH = 20K
}

练习5:添加启动日志

任务:在启动过程的关键点添加日志输出。

实现代码

/* 简单的日志缓冲区 */
#define LOG_SIZE 256
char boot_log[LOG_SIZE];
int log_index = 0;

void Boot_Log(const char *msg)
{
    int len = strlen(msg);
    if (log_index + len < LOG_SIZE) {
        strcpy(&boot_log[log_index], msg);
        log_index += len;
    }
}

/* 在启动过程中添加日志 */
void SystemInit(void)
{
    Boot_Log("SystemInit start\n");

    /* 时钟配置 */
    Boot_Log("Clock config\n");
    // ...

    Boot_Log("SystemInit done\n");
}

int main(void)
{
    Boot_Log("Main start\n");

    /* 初始化UART */
    UART_Init();

    /* 输出启动日志 */
    printf("=== Boot Log ===\n%s", boot_log);

    // ... 其他代码
}

故障排除

常见启动问题

问题1:程序无法启动

症状: - 下载成功但程序不运行 - 调试器无法连接 - LED不闪烁

可能原因

  1. 向量表地址错误

    // 检查VTOR寄存器
    uint32_t vtor = SCB->VTOR;
    printf("VTOR = 0x%08X\n", vtor);
    // 应该是 0x08000000 (Flash) 或 0x20000000 (SRAM)
    

  2. 栈指针无效

    // 检查栈顶地址
    extern uint32_t _estack;
    printf("Stack top = 0x%08X\n", (uint32_t)&_estack);
    // 应该在SRAM范围内 (0x20000000 - 0x20005000)
    

  3. 时钟配置错误

    // 检查系统时钟
    uint32_t sysclk = HAL_RCC_GetSysClockFreq();
    printf("SYSCLK = %d Hz\n", sysclk);
    

解决方法

  1. 检查链接脚本配置
  2. 验证启动文件是否正确
  3. 使用ST-Link Utility重新烧录
  4. 尝试擦除整个Flash

问题2:HardFault异常

症状: - 程序运行一段时间后停止 - 进入HardFault_Handler - 调试器显示异常

调试方法

/**
 * @brief  HardFault处理函数(调试版本)
 */
void HardFault_Handler(void)
{
    /* 保存寄存器 */
    __asm volatile (
        "TST LR, #4\n"
        "ITE EQ\n"
        "MRSEQ R0, MSP\n"
        "MRSNE R0, PSP\n"
        "B HardFault_Handler_C\n"
    );
}

void HardFault_Handler_C(uint32_t *hardfault_args)
{
    volatile uint32_t stacked_r0;
    volatile uint32_t stacked_r1;
    volatile uint32_t stacked_r2;
    volatile uint32_t stacked_r3;
    volatile uint32_t stacked_r12;
    volatile uint32_t stacked_lr;
    volatile uint32_t stacked_pc;
    volatile uint32_t stacked_psr;

    stacked_r0 = ((uint32_t)hardfault_args[0]);
    stacked_r1 = ((uint32_t)hardfault_args[1]);
    stacked_r2 = ((uint32_t)hardfault_args[2]);
    stacked_r3 = ((uint32_t)hardfault_args[3]);
    stacked_r12 = ((uint32_t)hardfault_args[4]);
    stacked_lr = ((uint32_t)hardfault_args[5]);
    stacked_pc = ((uint32_t)hardfault_args[6]);
    stacked_psr = ((uint32_t)hardfault_args[7]);

    printf("[HardFault]\n");
    printf("R0  = 0x%08X\n", stacked_r0);
    printf("R1  = 0x%08X\n", stacked_r1);
    printf("R2  = 0x%08X\n", stacked_r2);
    printf("R3  = 0x%08X\n", stacked_r3);
    printf("R12 = 0x%08X\n", stacked_r12);
    printf("LR  = 0x%08X\n", stacked_lr);
    printf("PC  = 0x%08X\n", stacked_pc);
    printf("PSR = 0x%08X\n", stacked_psr);

    /* 检查错误原因 */
    if (SCB->CFSR & SCB_CFSR_IACCVIOL_Msk) {
        printf("指令访问违规\n");
    }
    if (SCB->CFSR & SCB_CFSR_DACCVIOL_Msk) {
        printf("数据访问违规\n");
    }
    if (SCB->CFSR & SCB_CFSR_MUNSTKERR_Msk) {
        printf("出栈错误\n");
    }
    if (SCB->CFSR & SCB_CFSR_MSTKERR_Msk) {
        printf("入栈错误\n");
    }

    while(1);
}

常见原因

  1. 栈溢出
  2. 访问无效内存地址
  3. 未初始化的指针
  4. 数组越界

问题3:变量值异常

症状: - 全局变量值不正确 - 初始化的变量变成0 - 未初始化的变量有随机值

检查方法

/* 检查Data段复制 */
void Check_Data_Section(void)
{
    extern uint32_t _sdata, _edata, _sidata;

    printf("Data段信息:\n");
    printf("Flash源地址: 0x%08X\n", (uint32_t)&_sidata);
    printf("SRAM目标地址: 0x%08X\n", (uint32_t)&_sdata);
    printf("SRAM结束地址: 0x%08X\n", (uint32_t)&_edata);
    printf("Data段大小: %d 字节\n", 
           (uint32_t)&_edata - (uint32_t)&_sdata);
}

/* 检查BSS段清零 */
void Check_BSS_Section(void)
{
    extern uint32_t _sbss, _ebss;

    printf("BSS段信息:\n");
    printf("起始地址: 0x%08X\n", (uint32_t)&_sbss);
    printf("结束地址: 0x%08X\n", (uint32_t)&_ebss);
    printf("BSS段大小: %d 字节\n", 
           (uint32_t)&_ebss - (uint32_t)&_sbss);

    /* 验证是否全部为0 */
    uint32_t *ptr = &_sbss;
    int all_zero = 1;
    while (ptr < &_ebss) {
        if (*ptr != 0) {
            all_zero = 0;
            printf("非零地址: 0x%08X = 0x%08X\n", 
                   (uint32_t)ptr, *ptr);
            break;
        }
        ptr++;
    }
    if (all_zero) {
        printf("BSS段已正确清零\n");
    }
}

问题4:时钟配置失败

症状: - 系统运行速度异常 - 外设工作不正常 - 定时器不准确

检查代码

/**
 * @brief  检查时钟配置
 */
void Check_Clock_Config(void)
{
    RCC_ClkInitTypeDef clk_init;
    uint32_t flash_latency;

    /* 获取时钟配置 */
    HAL_RCC_GetClockConfig(&clk_init, &flash_latency);

    printf("=== 时钟配置 ===\n");
    printf("SYSCLK: %d Hz\n", HAL_RCC_GetSysClockFreq());
    printf("HCLK:   %d Hz\n", HAL_RCC_GetHCLKFreq());
    printf("PCLK1:  %d Hz\n", HAL_RCC_GetPCLK1Freq());
    printf("PCLK2:  %d Hz\n", HAL_RCC_GetPCLK2Freq());
    printf("Flash延迟: %d\n", flash_latency);

    /* 检查时钟源 */
    uint32_t clk_source = __HAL_RCC_GET_SYSCLK_SOURCE();
    switch(clk_source) {
        case RCC_SYSCLKSOURCE_STATUS_HSI:
            printf("时钟源: HSI\n");
            break;
        case RCC_SYSCLKSOURCE_STATUS_HSE:
            printf("时钟源: HSE\n");
            break;
        case RCC_SYSCLKSOURCE_STATUS_PLLCLK:
            printf("时钟源: PLL\n");
            break;
    }
}

调试技巧总结

  1. 使用串口输出
  2. 在关键点输出调试信息
  3. 帮助定位问题位置

  4. 使用LED指示

  5. 在不同阶段点亮不同LED
  6. 快速判断启动进度

  7. 使用断点

  8. 在启动文件设置断点
  9. 单步跟踪执行流程

  10. 查看寄存器

  11. 检查关键寄存器的值
  12. 验证配置是否正确

  13. 使用逻辑分析仪

  14. 捕获启动时的信号
  15. 分析时序问题

总结

通过本教程,你学习了STM32微控制器完整的启动过程,从上电复位到main函数执行的每一个步骤。

核心要点

  1. 启动流程
  2. 复位后读取栈顶地址和复位向量
  3. 执行Reset_Handler
  4. 调用SystemInit配置系统
  5. 初始化C运行时环境
  6. 跳转到main函数

  7. 启动文件

  8. 定义中断向量表
  9. 配置堆栈大小
  10. 实现Reset_Handler
  11. 提供默认中断处理函数

  12. SystemInit

  13. 配置系统时钟
  14. 设置Flash预取
  15. 配置向量表位置
  16. 初始化关键硬件

  17. 内存初始化

  18. 复制Data段从Flash到SRAM
  19. 清零BSS段
  20. 调用C++构造函数

  21. 调试技巧

  22. 使用断点跟踪启动过程
  23. 查看寄存器和内存
  24. 分析HardFault异常
  25. 测量启动时间

最佳实践

  1. 不要修改启动文件
  2. 除非必要,不要修改启动文件
  3. 使用SystemInit进行自定义初始化

  4. 合理配置栈大小

  5. 根据实际需求设置栈大小
  6. 预留足够的安全余量
  7. 监控栈使用情况

  8. 优化启动时间

  9. 简化SystemInit
  10. 延迟初始化非关键外设
  11. 减少Data段大小

  12. 添加错误处理

  13. 实现详细的HardFault处理
  14. 添加启动自检
  15. 记录启动日志

  16. 保持代码可维护性

  17. 添加清晰的注释
  18. 使用有意义的符号名
  19. 遵循编码规范

进阶方向

掌握了基础的启动过程后,可以继续学习:

  1. Bootloader开发
  2. 实现固件升级功能
  3. 支持多应用程序
  4. 添加安全验证

  5. 低功耗启动

  6. 优化启动功耗
  7. 实现快速唤醒
  8. 保存和恢复状态

  9. 安全启动

  10. 固件签名验证
  11. 安全存储
  12. 防调试保护

  13. 实时操作系统

  14. RTOS的启动过程
  15. 任务调度初始化
  16. 中断管理

延伸阅读

推荐进一步学习的内容:

  1. ARM架构
  2. 处理器工作模式与特权级别
  3. 中断向量表与异常处理机制
  4. MPU内存保护单元配置

  5. 系统优化

  6. 处理器缓存机制与性能优化
  7. DMA工作原理与应用场景
  8. 总线架构与外设访问优化

  9. 高级应用

  10. 基于性能计数器的系统性能分析工具
  11. 底层调试技术:JTAG与SWD深入解析

参考资料

  1. 官方文档
  2. STM32F1xx参考手册 - ST Microelectronics
  3. ARM Cortex-M3技术参考手册 - ARM
  4. STM32启动文件说明 - ST Application Note

  5. 书籍推荐

  6. 《ARM Cortex-M3权威指南》 - Joseph Yiu
  7. 《STM32库开发实战指南》 - 野火团队
  8. 《嵌入式系统设计与实践》 - Elecia White

  9. 在线资源

  10. ST官方社区论坛
  11. ARM开发者网站
  12. GitHub上的开源项目

练习题答案

  1. 启动流程顺序: 复位 → 读取栈顶 → 读取复位向量 → Reset_Handler → SystemInit → __main → Data段复制 → BSS段清零 → main函数

  2. 向量表第一个元素: 栈顶地址(初始SP值)

  3. SystemInit的主要作用: 配置系统时钟、设置Flash预取、配置向量表位置

  4. Data段和BSS段的区别

  5. Data段:已初始化的全局变量,需要从Flash复制到SRAM
  6. BSS段:未初始化的全局变量,只需要清零

  7. WEAK符号的作用: 允许用户定义同名函数覆盖默认实现,避免链接错误

下一步:建议学习 处理器工作模式与特权级别,深入理解ARM Cortex-M的工作机制。