3. 裸机启动流程:汇编启动代码编写、中断向量表配置、SDRAM初始化
说实话,很多做嵌入式开发的朋友,一上来就怼 RTOS,结果出了问题连启动流程都看不懂。我个人觉得,裸机启动是嵌入式开发的「基本功」,就像练武要先扎马步一样。这一章,我们就来聊聊 ARMv7 上电后到底干了些什么。
3.1 上电第一件事:复位向量
ARMv7 处理器上电复位后,CPU 会从 0x00000000 或者 0xFFFF0000 取指(具体看硬件设计)。这个地址放的不是代码,而是中断向量表。向量表的第一项就是 复位向量,它指向真正的启动代码入口。
我刚开始做项目时,犯过一个低级错误——向量表没对齐。结果芯片一上电就跑飞了,查了两天才发现是地址没对齐到 32 字节边界。嗯,这里要注意:ARMv7 要求向量表必须 32 字节对齐,否则异常处理会出问题。
3.2 汇编启动代码编写
启动代码通常用汇编写,因为 C 语言运行时环境还没准备好。说白了,你要先给 C 语言搭好「舞台」——初始化栈指针、清零 BSS 段、拷贝数据段。
下面是一个典型的 ARMv7 启动代码框架:
.section .vectors, "ax"
.global _start
.align 5
_start:
/* 中断向量表 */
ldr pc, reset_handler /* 复位 */
ldr pc, undef_handler /* 未定义指令 */
ldr pc, svc_handler /* 超级用户调用 */
ldr pc, pabt_handler /* 预取中止 */
ldr pc, dabt_handler /* 数据中止 */
nop /* 保留 */
ldr pc, irq_handler /* IRQ 中断 */
ldr pc, fiq_handler /* FIQ 中断 */
reset_handler:
/* 设置栈指针 */
ldr sp, =_stack_top
/* 清零 BSS 段 */
ldr r0, =_bss_start
ldr r1, =_bss_end
mov r2, #0
bss_loop:
cmp r0, r1
strlt r2, [r0], #4
blt bss_loop
/* 拷贝数据段 */
ldr r0, =_data_start
ldr r1, =_data_end
ldr r2, =_data_load
data_loop:
cmp r0, r1
ldrlt r3, [r2], #4
strlt r3, [r0], #4
blt data_loop
/* 跳转到 main */
bl main
b .
这段代码里,我习惯把向量表单独放在一个段里,方便链接脚本控制地址。你想想看,如果向量表和代码混在一起,链接器一优化,地址就乱了。
3.3 中断向量表配置
ARMv7 支持两种向量表位置:低端向量(0x00000000)和高端向量(0xFFFF0000)。具体用哪个,由系统控制协处理器的 SCTLR.V 位决定。
我在项目中遇到过一个问题:芯片从 BootROM 跳转到用户程序时,向量表地址没切换,导致中断全跑到了 BootROM 里。后来我加了一段代码,在启动时显式设置向量表基地址:
/* 设置向量表基地址到 SRAM */
ldr r0, =0x20000000 /* 假设 SRAM 基地址 */
mcr p15, 0, r0, c12, c0, 0 /* 写 VBAR 寄存器 */
这里用到了 VBAR(Vector Base Address Register),它是 ARMv7 用来重定位向量表的关键寄存器。写这个寄存器前,一定要确保目标地址已经初始化好了,否则写进去也是白搭。
| 异常类型 | 向量偏移 | 优先级 | 典型用途 |
|---|---|---|---|
| 复位 | 0x00 | 最高 | 系统启动 |
| 未定义指令 | 0x04 | 6 | 软件仿真 |
| SVC | 0x08 | 5 | 系统调用 |
| 预取中止 | 0x0C | 4 | 指令访问异常 |
| 数据中止 | 0x10 | 3 | 数据访问异常 |
| IRQ | 0x18 | 2 | 外设中断 |
| FIQ | 0x1C | 1 | 高速中断 |
ldr pc, [pc, #offset] 或者 ldr pc, =handler 这种间接跳转方式。
3.4 SDRAM 初始化
SDRAM 初始化是启动流程里最「玄学」的部分。为什么这么说?因为时序参数稍微不对,内存就罢工。我见过有人把 CAS 延迟写错了一位,结果系统跑起来偶尔死机,查了三天才找到原因。
SDRAM 初始化通常分这几步:
- 上电等待:供电稳定后,等待 100-200us,让内部电路稳定。
- 预充电:发送预充电命令,关闭所有行。
- 自动刷新:发送至少 8 个自动刷新周期。
- 设置模式寄存器:配置 CAS 延迟、突发长度、突发类型等。
- 正常操作:进入读写模式。
下面是一个简化的 SDRAM 初始化代码(以某款 ARM 芯片为例):
/* 使能 SDRAM 时钟 */
ldr r0, =0x40007000 /* 时钟控制寄存器地址 */
ldr r1, [r0]
orr r1, r1, #(1 << 5) /* 使能 SDRAM 时钟 */
str r1, [r0]
/* 配置 SDRAM 控制器时序 */
ldr r0, =0x40008000 /* SDRAM 控制器基地址 */
ldr r1, =0x00000002 /* CAS 延迟 = 2 */
str r1, [r0, #0x00] /* 写模式寄存器 */
/* 发送预充电命令 */
ldr r1, =0x00000001
str r1, [r0, #0x04]
/* 发送自动刷新命令(8 次) */
mov r2, #8
refresh_loop:
ldr r1, =0x00000003
str r1, [r0, #0x04]
subs r2, r2, #1
bne refresh_loop
/* 设置模式寄存器 */
ldr r1, =0x00000020 /* 突发长度 = 4, CAS = 2 */
str r1, [r0, #0x00]
/* 进入正常操作模式 */
ldr r1, =0x00000000
str r1, [r0, #0x04]
3.5 整体启动流程
把上面几块串起来,整个裸机启动流程就清晰了。我画了一张图,帮你理清脉络:
从这张图可以看出来,整个启动流程是线性的,每一步都依赖上一步的结果。我见过有人把 SDRAM 初始化放在向量表配置前面,结果向量表地址还没设好,SDRAM 就初始化了——这其实没问题,但如果你在 SDRAM 初始化过程中触发了异常,那就麻烦了。
3.6 常见问题与避坑
- 向量表地址不对齐:ARMv7 要求 32 字节对齐,否则异常处理会跳转到错误地址。
- SDRAM 时序参数错误:CAS 延迟、刷新周期这些参数必须和芯片手册一致,差一个周期都可能出问题。
- 栈指针未初始化:在设置栈指针前调用函数,会导致程序跑飞。我建议在复位后第一件事就是设栈指针。
- 中断使能过早:外设还没初始化就开中断,中断处理函数可能访问到未初始化的寄存器。
好了,这一章的内容就到这里。裸机启动看似简单,但每一步都有讲究。你把这些基础打牢了,后面学 RTOS 任务调度就会轻松很多。