BLOG8
跳转至

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的工作机制。