全部例程

启动文件详解

W55MH32

更新于 2026年9月20日

本篇总结

解析 W55MH32 汇编启动文件:栈/堆分配、中断向量表、复位程序与中断服务程序。

🔑 KEIL MDK

启动文件由汇编编写,是系统上电复位后第一个执行的程序。主要完成 5 项工作:初始化堆栈指针 SP、初始化 PC 指针、初始化中断向量表、配置系统时钟、调用 C 库函数 __main 初始化用户堆栈并最终调用 main 函数。

ARM 汇编指令速查

在讲解启动代码时会涉及 ARM 的汇编指令和 Cortex 内核的指令,有关 Cortex 内核的指令可参考《CM3 权威指南CnR2》第四章:指令集。剩下的 ARM 汇编指令可在 MDK → Help → uVision Help 中搜索到,以 EQU 为例检索如下:

在 ARM Development Tools 帮助文档中搜索 EQU 指令,查看 Assembler User Guide

下面列出启动文件中使用到的 ARM 汇编指令,全部从 ARM Development Tools 帮助文档检索而来:

指令作用
EQU给数字常量取符号名,相当于 C 的 #define
AREA汇编一个新的代码段或数据段
SPACE分配内存空间
PRESERVE8当前文件堆栈需 8 字节对齐
EXPORT声明标号具有全局属性,可被外部文件使用
DCD以字为单位分配内存,4 字节对齐并初始化
PROC/ENDP定义子程序,成对使用表示子程序结束
WEAK弱定义,外部文件定义则优先使用(编译器功能)
IMPORT声明标号来自外部文件,类似 C 的 extern
B跳转到一个标号
ALIGN对齐指令/数据地址,缺省 4 字节对齐
IF/ELSE/ENDIF汇编条件分支语句

栈与堆的空间分配

栈用于局部变量、函数调用、函数形参等开销,由高向低生长。堆用于动态内存分配(malloc),由低向高生长。

; ==================== 栈 ====================
Stack_Size      EQU     0x00000400          ; 栈大小 1KB
                AREA    STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem       SPACE   Stack_Size          ; 分配 1KB 空间
__initial_sp                            ; 栈顶地址(栈由高向低生长)

; ==================== 堆 ====================
Heap_Size       EQU     0x00000200          ; 堆大小 512B
                AREA    HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base                          ; 堆起始地址(堆由低向高生长)
Heap_Mem        SPACE   Heap_Size
__heap_limit                         ; 堆结束地址

; ==================== 指令集声明 ====================
                PRESERVE8                   ; 堆栈 8 字节对齐
                THUMB                       ; 兼容 THUMB 指令集

EQU:宏定义的伪指令,相当于等于,类似 C 中的 define。AREA:告诉汇编器汇编一个新的代码段或数据段,STACK 表示段名,NOINIT 表示不初始化,READWRITE 表示可读可写,ALIGN=3 表示 8 字节对齐。SPACE:分配一定大小的内存空间,单位为字节。标号 __initial_sp 紧挨着 SPACE 语句放置,表示栈顶地址。

栈溢出排查

如果程序出现莫名错误并进入 HardFault,首先考虑栈是否溢出。增大 Stack_Size 试试。栈+堆不能超过内部 SRAM 大小(W55MH32 为 96KB)。

中断向量表

当内核响应了一个发生的异常后,对应的异常服务例程(ESR)就会执行。内核使用"向量表查表机制"决定 ESR 的入口地址。向量表是一个 WORD(32 位整数)数组,每个下标对应一种异常,该下标元素的值则是该 ESR 的入口地址。地址 0x00000000 存放 MSP 初值(不是入口地址),0x00000004 存放复位程序地址。

                AREA    RESET, DATA, READONLY
                EXPORT  __Vectors
                EXPORT  __Vectors_End
                EXPORT  __Vectors_Size

__Vectors       DCD     __initial_sp        ; 栈顶地址(地址 0x00)
                DCD     Reset_Handler       ; 复位程序(地址 0x04)
                DCD     NMI_Handler         ; NMI(地址 0x08)
                DCD     HardFault_Handler   ; HardFault(地址 0x0C)
                DCD     MemManage_Handler   ; MemManage(地址 0x10)
                DCD     BusFault_Handler    ; BusFault(地址 0x14)
                DCD     UsageFault_Handler  ; UsageFault(地址 0x18)
                DCD     0, 0, 0, 0          ; 保留
                DCD     SVC_Handler         ; SVCall
                DCD     DebugMon_Handler    ; Debug Monitor
                DCD     0                   ; 保留
                DCD     PendSV_Handler      ; PendSV
                DCD     SysTick_Handler     ; SysTick
                ; --- 外部中断开始 ---
                DCD     WWDG_IRQHandler     ; 窗口看门狗
                DCD     PVD_IRQHandler      ; PVD
                DCD     TAMPER_IRQHandler   ; 侵入检测
                ; ... 中间省略 ...
                DCD     DMA2_Channel4_5_IRQHandler
__Vectors_End
__Vectors_Size  EQU     __Vectors_End - __Vectors

DCD:分配一个或多个以字为单位的内存,4 字节对齐并初始化。向量表中存放的都是中断服务函数的函数名,而 C 语言中的函数名就是一个地址。

地址编号名称说明
0x0000 0000-MSP栈顶地址(非入口地址)
0x0000 0004-3Reset复位
0x0000 0008-2NMI不可屏蔽中断
0x0000 000C-1HardFault所有类型的错误
0x0000 003C6SysTick系统嘀嗒定时器
0x0000 0040+7+外部中断WWDG/PVD/TAMPER...DMA2

复位程序 Reset_Handler

复位子程序是上电后第一个执行的程序:调用 SystemInit 配置系统时钟,然后调用 C 库函数 __main 初始化堆栈并跳转到 main。

; ==================== 复位程序 ====================
Reset_Handler   PROC
                EXPORT  Reset_Handler    [WEAK]
                IMPORT  SystemInit        ; 外部函数:配置系统时钟
                IMPORT  __main            ; C 库函数:初始化堆栈→main

                LDR     R0, =SystemInit   ; 加载 SystemInit 地址到 R0
                BLX     R0                ; 跳转到 R0 执行,保存返回地址到 LR
                LDR     R0, =__main       ; 加载 __main 地址到 R0
                BX      R0                ; 跳转到 R0,不返回
                ENDP

; ==================== 中断服务程序(弱定义) ====================
NMI_Handler     PROC
                EXPORT  NMI_Handler       [WEAK]
                B       .                 ; 死循环(B . = 跳转到自身)
                ENDP

SysTick_Handler PROC
                EXPORT  SysTick_Handler  [WEAK]
                B       .
                ENDP

Default_Handler PROC                        ; 外部中断的默认处理
                EXPORT  WWDG_IRQHandler    [WEAK]
                EXPORT  PVD_IRQHandler     [WEAK]
                ; ... 中间省略 ...
                B       .
                ENDP

WEAK:表示弱定义,如果外部文件优先定义了该标号则首先引用,如果外部文件没有声明也不会出错。IMPORT:表示该标号来自外部文件,类似 C 语言的 extern 关键字。SystemInit() 是标准库函数,在 system_w55mh32.c 中定义,主要配置系统时钟。__main 是 C 库函数,初始化用户堆栈并最终调用 main 函数。

WEAK 弱定义的妙用

启动文件中所有中断服务程序都用 [WEAK] 声明,且函数体只有 B .(死循环)。如果用户在 C 文件中定义了同名函数,链接器优先使用用户定义;如果用户忘记定义,中断来临时进入死循环而非跑飞——这是安全网机制。

指令作用
LDR从存储器中加载字到一个寄存器中
BLX跳转到寄存器给出的地址,保存下条指令地址到 LR
BX跳转到寄存器给出的地址,不返回
B .跳转到自身(死循环)

用户堆栈初始化

根据是否定义 __MICROLIB 宏,有两种堆栈初始化方式。在 Keil 中通过 Options → Target → Use MicroLIB 勾选开启微库:

Keil Options → Target 中勾选 Use MicroLIB 开启微库模式
; 用户栈和堆初始化,由 C 库函数 __main 完成
        IF      :DEF:__MICROLIB      ; 若定义了 __MICROLIB(Keil 中开启微库)

                EXPORT  __initial_sp   ; 导出栈顶地址
                EXPORT  __heap_base    ; 导出堆起始地址
                EXPORT  __heap_limit   ; 导出堆结束地址
                                        ; 堆栈初始化由 __main 自动完成

        ELSE                          ; 未定义 __MICROLIB,使用双段存储器模式

                IMPORT  __use_two_region_memory
                EXPORT  __user_initial_stackheap

__user_initial_stackheap               ; 用户自行初始化堆栈
                LDR     R0, =Heap_Mem              ; R0 = 堆起始
                LDR     R1, =(Stack_Mem+Stack_Size); R1 = 栈底
                LDR     R2, =(Heap_Mem+Heap_Size)  ; R2 = 堆结束
                LDR     R3, =Stack_Mem             ; R3 = 栈起始
                BX      LR

                ALIGN
        ENDIF
        END

Microlib vs 标准库

Microlib(微库):代码体积小,无文件 I/O,__main 自动初始化堆栈。在 Keil → Options → Target → Use MicroLib 勾选开启。双段存储器模式:标准库模式,需用户实现 __user_initial_stackheap 函数指定堆栈区域。

常见问题

Q: 程序莫名进入 HardFault

首先检查栈是否溢出——增大 Stack_Size。常见原因:大型局部数组、深层递归调用、中断嵌套过深。栈+堆不能超过 SRAM 大小(96KB)。

Q: 中断服务程序写对了但不触发

检查函数名是否与启动文件中向量表定义完全一致。启动文件用 [WEAK] 弱定义了所有 ISR,函数名拼写错误会导致链接器使用启动文件的空函数(死循环),中断看似不触发。

Q: SystemInit 作用是什么

SystemInit() 是标准库函数,定义在 system_w55mh32.c 中,主要配置系统时钟(如 PLL 倍频)。在跳转到 main 之前执行,确保进入 C 世界时系统时钟已正确配置。

Q: __main 和 main 有什么区别

__main 是 C 库函数,负责初始化用户堆栈(RW/ZI 数据搬运),最后调用用户编写的 main 函数。__main 不需要用户实现,而 main 必须由用户实现。

Q: 何时需要修改启动文件

常见场景:① 栈不够用→增大 Stack_Size;② 使用 RTOS→可能需调整堆栈;③ IAP/Bootloader→需修改中断向量表偏移(SCB->VTOR);④ 外部中断数量变化→选择对应容量的启动文件。

总结

  • 启动五步:SP 初始化 → PC 指向 Reset_Handler → 中断向量表 → SystemInit 配置时钟 → __main 初始化堆栈 → main
  • 栈:EQU 0x400(1KB),高→低生长,用于局部变量/函数调用;堆:EQU 0x200(512B),低→高生长,用于 malloc
  • 向量表:DCD 数组,地址 0x00 存 MSP,0x04 存 Reset_Handler,后续依次排列系统异常和外部中断
  • WEAK 弱定义:所有 ISR 在启动文件中空实现(B . 死循环),用户定义则优先链接,未定义则安全卡死
  • Microlib:微库模式 __main 自动初始化堆栈;标准库模式需用户实现 __user_initial_stackheap

Andy

W5500 技术支持专家