3. 启动文件分析

启动文件分析 学习卡片

任务目标

分析STM32F103系列MCU的启动文件(startup_stm32f103xe.s),理解堆栈指针初始化、中断向量表定义以及复位处理流程,并修改启动文件以在调用main()之前完成必要的系统初始化。

启动文件核心结构

STM32启动文件(汇编)主要完成三件事:

  1. 堆栈指针初始化:将栈顶地址加载到SP寄存器
  2. 中断向量表:定义各中断服务函数的入口地址,向量表首字为栈顶地址,第二个字为复位处理函数地址
  3. 复位处理:调用SystemInit()配置时钟,然后跳转到main()

关键完整代码

以下为修改后的启动文件核心部分(基于STM32F103标准外设库的startup_stm32f103xe.s),增加了自定义的PreMainInit()调用:

; 栈配置
Stack_Size      EQU     0x00000400
                AREA    STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem       SPACE   Stack_Size
__initial_sp

; 堆配置
Heap_Size       EQU     0x00000200
                AREA    HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
Heap_Mem        SPACE   Heap_Size
__heap_limit

; 向量表
                PRESERVE8
                THUMB

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

__Vectors       DCD     __initial_sp               ; 栈顶地址
                DCD     Reset_Handler              ; 复位向量
                DCD     NMI_Handler
                DCD     HardFault_Handler
                ; ... 其他中断向量(此处省略中间部分,实际文件中完整保留)
__Vectors_End

__Vectors_Size  EQU     __Vectors_End - __Vectors

; 代码段
                AREA    |.text|, CODE, READONLY

; 复位处理
Reset_Handler   PROC
                EXPORT  Reset_Handler [WEAK]
                IMPORT  SystemInit
                IMPORT  PreMainInit
                IMPORT  __main
                LDR     R0, =SystemInit
                BLX     R0
                LDR     R0, =PreMainInit
                BLX     R0
                LDR     R0, =__main
                BX      R0
                ENDP

; 预主函数初始化(用户自定义)
PreMainInit     PROC
                EXPORT  PreMainInit [WEAK]
                ; 在此处添加用户自定义初始化代码
                ; 例如:启用FPU、配置外部存储器等
                BX      LR
                ENDP

; 默认中断处理
NMI_Handler     PROC
                EXPORT  NMI_Handler [WEAK]
                B       .
                ENDP
; ... 其他中断处理函数(省略)
                ALIGN
                END

修改说明

相比原始启动文件,本次修改的关键点:

  • 在Reset_Handler中,在SystemInit()之后、__main(最终调用main())之前,插入了对PreMainInit()的调用
  • 新增了PreMainInit函数定义,使用[WEAK]属性,允许用户在C代码中重新实现同名的强定义函数来覆盖默认的空实现
  • 向量表首字__initial_sp由链接器根据栈段自动计算,确保上电后SP指向栈顶

C语言侧实现

在C代码中提供强定义的PreMainInit函数,用于在进入main()前执行特定初始化:

/* user_premain.c */
#include <stdint.h>

/* 在main()之前调用的自定义初始化函数 */
void PreMainInit(void)
{
    /* 示例:在main之前开启某个外设时钟 */
    /* 实际使用时替换为具体寄存器操作 */
    volatile uint32_t *RCC_APB2ENR = (uint32_t *)0x40021018;
    *RCC_APB2ENR |= (1 << 4);  /* 使能GPIOC时钟 */
}

编译与链接

使用ARM Compiler或GCC工具链编译时,需确保启动文件与C文件一同参与链接:

# 使用arm-none-eabi-gcc示例
arm-none-eabi-gcc -c startup_stm32f103xe.s -o startup.o
arm-none-eabi-gcc -c user_premain.c -o premain.o
arm-none-eabi-gcc -c main.c -o main.o
arm-none-eabi-gcc startup.o premain.o main.o -T stm32f103_flash.ld -o firmware.elf

验证方法

完成以下检查即可判定修改生效:

  1. 反汇编验证:使用arm-none-eabi-objdump -d firmware.elf查看Reset_Handler的反汇编,确认调用顺序为SystemInit → PreMainInit → __main
  2. 调试器验证:在PreMainInit函数入口和main()入口各设断点,烧录后运行,观察程序先停在PreMainInit断点,继续运行后停在main()断点
  3. 向量表检查:在调试器中查看内存地址0x08000000处,确认前两个字分别为栈顶地址(0x20000400附近)和Reset_Handler地址

若程序能依次经过上述断点且SP初始值正确,说明启动文件修改成功。