7.1 启动文件长什么样
打开 Start/startup_stm32f10x_md.s,最开头是:
Stack_Size EQU 0x00000400 ; 栈大小 = 1KB
AREA STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem SPACE Stack_Size
__initial_sp ; ★ 这个标号就是「初始 SP」
Heap_Size EQU 0x00000200 ; 堆大小 = 512B
AREA HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
Heap_Mem SPACE Heap_Size
__heap_limit
关键点:__initial_sp 这个标号的值,会被链接器填到向量表的第一个 4 字节(地址 0x0800 0000)。
所以 CPU 从地址 0 读到的就是它 —— 栈顶是链接时算出来的,不是运行时算的。
7.2 向量表:中断入口的清单
AREA RESET, DATA, READONLY
__Vectors DCD __initial_sp ; 0x00 ← 初始 SP
DCD Reset_Handler ; 0x04 ← 复位入口
DCD NMI_Handler ; 0x08
DCD HardFault_Handler ; 0x0C
DCD MemManage_Handler ; 0x10
DCD BusFault_Handler ; 0x14
DCD UsageFault_Handler ; 0x18
DCD 0 ; 0x1C 保留
DCD 0
DCD 0
DCD 0
DCD SVC_Handler ; 0x2C
DCD DebugMon_Handler ; 0x30
DCD 0
DCD PendSV_Handler ; 0x38
DCD SysTick_Handler ; 0x3C
; …… 之后是 60 个外设中断
DCD WWDG_IRQHandler ; 窗口看门狗
DCD PVD_IRQHandler ; 电源电压检测
DCD TAMPER_IRQHandler ; 侵入检测
DCD RTC_IRQHandler ; RTC
DCD FLASH_IRQHandler ; Flash 全局
DCD RCC_IRQHandler ; RCC
DCD EXTI0_IRQHandler ; 外部中断 0
...
为什么第一个是 SP 而不是指令地址:Cortex-M3 的硬件设计如此 —— 复位时先建立栈(因为后续可能进中断要压栈),再从第二个字开始执行。
这和 51 不同:51 复位后直接从 0x0000 执行指令。
7.3 Reset_Handler:真正的第一段代码
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT __main
IMPORT SystemInit
LDR R0, =SystemInit ; ① R0 = SystemInit 地址
BLX R0 ; ② 调用它(配置时钟)
LDR R0, =__main ; ③ R0 = __main 地址
BX R0 ; ④ 跳过去(不返回)
ENDP
| 指令 | 作用 |
LDR R0, =SystemInit | 取函数地址(伪指令,编译成从常量池加载) |
BLX R0 | 带返回的跳转(B=分支,L=链接返回地址,X=切换指令集) |
BX R0 | 不带返回的跳转 —— 因为 main 永远不返回 |
这就是课件第 17 页「工程架构」的原文:
startup_xx.s 复位中断 → 调用 SystemInit → 调用 main
你以前写的 main 为什么能被执行?答案就在这 4 条汇编。
7.4 为什么叫「弱定义」
EXPORT Reset_Handler [WEAK]
[WEAK] 是弱符号:如果别的地方(比如 stm32f10x_it.c)定义了一个同名的强符号,就用那个。
所以中断服务函数你可以在任何 .c 文件里写,只要名字对上,就会覆盖启动文件里的空实现:
// stm32f10x_it.c 里写
void TIM2_IRQHandler(void) // 名字必须和启动文件里的一致
{
// 你的中断处理
}
改中断函数名拼错 = 中断永远不进。这是新手最常见的问题之一:
TIM2_IRQHandler 写成 TIM2_IRQhandler / TIM2_Handler,编译不报错,但中断不响应。
7.5 本章练习
- 初始 SP 的值是链接时定的还是运行时算的?从哪个标号来?
- Reset_Handler 里为什么要先调 SystemInit 再调 __main?
- [WEAK] 是什么意思?为什么中断函数能随便写在哪个 .c 文件?
📖 参考答案(点击展开)
- 链接时定的。来自启动文件 STACK 段的
__initial_sp 标号,链接器把它的值填进向量表第一个 4 字节。 - 因为 __main 里要初始化内存(拷贝 .data、清 .bss),而 C 库的某些实现依赖正确的时钟;更重要的是 SystemInit 负责把时钟从 8MHz HSI 切到 72MHz PLL,必须在用户代码前完成。
- 弱符号。如果别处有同名强符号就用那个 —— 所以中断服务函数可以写在任意 .c 文件里,只要名字和启动文件里的一致就会覆盖空实现。