第2章 启动流程与汇编基础:ARM复位流程、中断向量表、汇编指令基础(LDR/STR/MOV/BL)、C与汇编混合编程
好,咱们直接进入正题。上一章我们把开发环境搭好了,板子也点亮了。但很多同学会问:“上电之后,CPU到底干了什么?”
这个问题,说白了就是启动流程。不懂启动流程,你写的代码就是“玄学”——跑起来了不知道为什么,跑不起来更不知道为什么。我当年刚入行时,就吃过这个亏。
2.1 ARM复位流程:从Reset到main()
ARM Cortex-M系列(比如STM32)的复位流程,其实比你想的简单。它不像Linux那样要加载内核、挂载文件系统。裸机嘛,就三步:
- CPU从0x00000000取第一条指令(其实是取向量表中的Reset_Handler地址)
- 执行Reset_Handler:初始化堆栈、清零BSS段、拷贝数据段
- 跳转到main()
嗯,这里要注意:0x00000000存放的不是代码,而是栈顶指针。这是ARM设计的一个巧妙之处——上电后自动从0x00000000加载MSP(主栈指针),然后从0x00000004加载PC(程序计数器)。
核心记忆点:
- 0x00000000 → 初始栈顶地址(MSP)
- 0x00000004 → 复位中断向量(Reset_Handler)
- 0x00000008 → NMI中断向量
- ... 依次排列
我曾经在项目中遇到过一个问题:板子上电后死活不跑,调试器也连不上。查了半天,发现是Bootloader把向量表偏移设错了,CPU取到的Reset_Handler地址是0xFFFFFFFF。嗯,从那以后我每次写启动文件,都会先检查向量表首地址。
2.2 中断向量表:CPU的“紧急通讯录”
中断向量表是什么?我习惯把它比作“CPU的紧急通讯录”。每个中断源都有一个编号,CPU收到中断后,直接查表找到对应的处理函数地址,跳过去执行。
对于Cortex-M3/M4,向量表是这样的:
| 偏移地址 | 异常类型 | 优先级 | 描述 |
|---|---|---|---|
| 0x00 | 栈顶地址 | - | 初始MSP值 |
| 0x04 | Reset | -3(最高) | 复位处理函数 |
| 0x08 | NMI | -2 | 不可屏蔽中断 |
| 0x0C | HardFault | -1 | 硬件错误 |
| 0x10 | MemManage | 可编程 | 内存管理错误 |
| 0x14 | BusFault | 可编程 | 总线错误 |
| 0x18 | UsageFault | 可编程 | 用法错误 |
| ... | ... | ... | ... |
| 0x40+ | 外部中断 | 可编程 | 外设中断(GPIO、TIM、UART等) |
避坑指南:我曾经在移植FreeRTOS时,忘记把向量表重定位到SRAM,结果一开中断就HardFault。记住:如果代码在0x08000000运行,必须设置SCB->VTOR = 0x08000000,否则CPU还是去0x00000000查表。
2.3 汇编指令基础:LDR/STR/MOV/BL
裸机开发离不开汇编。你不需要成为汇编大师,但看懂启动文件、会写关键操作是基本功。我挑四个最常用的指令讲:
2.3.1 MOV:数据搬运
MOV R0, #0x12 ; R0 = 0x12
MOV R1, R0 ; R1 = R0
MOV R2, #0xFF ; R2 = 0xFF
限制:立即数只能使用8位循环移位后的值。比如0x12345678就不能直接用MOV,得用LDR伪指令。
2.3.2 LDR:从内存加载到寄存器
LDR R0, =0x40020000 ; 伪指令,加载32位立即数到R0
LDR R1, [R0] ; R1 = *(uint32_t*)R0
LDR R2, [R0, #4] ; R2 = *(uint32_t*)(R0+4)
LDR R3, [R0, R1] ; R3 = *(uint32_t*)(R0+R1)
嗯,这里有个坑:LDR R0, =0x40020000是伪指令,汇编器会把它展开成LDR R0, [PC, #offset],从常量池加载。而LDR R1, [R0]才是真正的内存访问。
2.3.3 STR:从寄存器存储到内存
STR R0, [R1] ; *(uint32_t*)R1 = R0
STR R2, [R3, #0x10] ; *(uint32_t*)(R3+0x10) = R2
STR R4, [R5, R6] ; *(uint32_t*)(R5+R6) = R4
STR和LDR是对称的。你想想看,操作外设寄存器时,本质上就是LDR读、STR写。
2.3.4 BL:带链接的跳转
BL Delay ; 跳转到Delay,同时保存返回地址到LR
BL MyFunction ; 跳转到MyFunction
BL是函数调用的核心。它会把当前PC+4保存到LR(R14),然后跳转到目标地址。被调函数返回时,执行BX LR即可。
个人习惯:我写启动文件时,会用LDR R0, =main; BLX R0来跳转到main()。为什么不用B?因为B指令跳转范围有限(±32MB),而LDR+BLX可以跳到任意地址。
2.4 C与汇编混合编程
实际项目中,我们不会全用汇编写应用代码。通常是C写逻辑,汇编写启动和关键操作。混合编程有三种方式:
2.4.1 在C中内联汇编
__asm void SystemReset(void)
{
MOV R0, #0x05
MSR FAULTMASK, R0
LDR R0, =0xE000ED0C
LDR R1, =0x05FA0004
STR R1, [R0]
BX LR
}
或者用__asm关键字嵌入单条指令:
void EnableIRQ(void)
{
__asm("CPSIE I"); // 开中断
}
2.4.2 汇编调用C函数
在启动文件里,我们经常这样跳转到main():
Reset_Handler PROC
EXPORT Reset_Handler
IMPORT main
LDR R0, =main
BLX R0
B .
ENDP
注意:调用C函数前,必须初始化堆栈指针SP,否则函数内的局部变量会写飞。
2.4.3 C调用汇编函数
在汇编文件中导出函数名,C文件声明extern即可:
; startup.s
EXPORT CPU_GetCoreID
CPU_GetCoreID PROC
MRS R0, CPUID
BX LR
ENDP
// main.c
extern uint32_t CPU_GetCoreID(void);
int main(void)
{
uint32_t id = CPU_GetCoreID();
// ...
}
我曾经踩过的坑:汇编函数返回时,一定要用BX LR而不是B LR。B LR不会切换指令集(ARM/Thumb),而Cortex-M只支持Thumb模式,用B LR会导致HardFault。嗯,这个bug我调了整整一个下午。
2.5 本章知识体系总览
下面这张图,把本章的核心知识点串起来了。建议你保存下来,写启动文件时对照着看:
学习建议:别急着背指令集。先打开STM32的启动文件(startup_stm32f4xx.s),对照本章内容逐行看一遍。看不懂的指令查ARM手册。我当年就是这么学的——读代码、改代码、烧代码,三遍下来就熟了。