3. 启动流程与汇编基础:ARM汇编指令集基础、中断向量表配置、SDRAM初始化
好,咱们直接进入正题。这一章是裸机开发的基石,说白了就是芯片上电后,第一条指令从哪里开始,怎么把C语言跑起来。很多初学者觉得汇编难,其实你只要掌握十几条指令,就能把启动流程走通。我当年第一次调SDRAM的时候,板子死活不跑,最后发现是初始化时序差了半个周期——这种坑,今天咱们一起填上。
3.1 ARM汇编指令集基础——够用就行
ARMv7的汇编指令看着多,但启动代码里常用的就那么几条。我个人习惯把指令分成三类:数据搬运、跳转控制、内存访问。你想想看,CPU无非就是搬数据、做运算、跳来跳去。
3.1.1 核心指令速查表
| 指令 | 功能 | 我常用的场景 |
|---|---|---|
LDR |
从内存加载数据到寄存器 | 读取外设寄存器地址 |
STR |
将寄存器数据写入内存 | 配置GPIO/SDRAM控制器 |
MOV |
寄存器间数据传递 | 初始化栈指针SP |
B |
无条件跳转 | 跳转到main函数 |
BL |
带返回的跳转 | 调用子函数 |
MRS/MSR |
读写状态寄存器CPSR | 切换CPU模式 |
LDR R0, =0x12345678是伪指令,编译器会帮你生成文字池;LDR R0, [R1]才是真正的内存加载。新手容易搞混,我建议统一用伪指令形式,可读性更好。
3.1.2 一个完整的启动汇编片段
咱们直接看代码。这是我从一个量产项目中摘出来的启动头,加了详细注释:
@ 文件: start.S
@ 功能: ARMv7裸机启动入口
.section .text.start
.global _start
_start:
@ 1. 设置中断向量表基址
LDR R0, =_vector_table
MCR p15, 0, R0, c12, c0, 0 @ 写VBAR寄存器
@ 2. 设置栈指针(不同模式不同栈)
MRS R0, CPSR
BIC R0, R0, #0x1F @ 清模式位
ORR R1, R0, #0x12 @ IRQ模式
MSR CPSR_c, R1
LDR SP, =_irq_stack_top
ORR R1, R0, #0x13 @ SVC模式
MSR CPSR_c, R1
LDR SP, =_svc_stack_top
@ 3. 初始化SDRAM(后面细讲)
BL sdram_init
@ 4. 跳转到C语言main
BL main
B . @ 死循环
这段代码我用了好几年,基本没大改过。你注意看第2步——不同模式要配不同的栈,否则中断一进来就崩。我曾经见过一个项目,工程师图省事只配了一个栈,结果IRQ和SVC共用,调试了三天才发现。
3.2 中断向量表配置——CPU的紧急联络簿
中断向量表是什么?说白了就是一张表,告诉CPU每种异常来了该去哪执行。ARMv7支持8个异常类型,每个占4字节,一共32字节。但实际项目中,我们通常把向量表放在0x00000000或者0xFFFF0000,不过Cortex-A系列更灵活——通过VBAR寄存器可以任意设置基址。
3.2.1 标准向量表布局
| 偏移地址 | 异常类型 | 优先级 | 我踩过的坑 |
|---|---|---|---|
| 0x00 | Reset | 最高 | — |
| 0x04 | Undefined Instruction | 6 | 指令编码错误时触发 |
| 0x08 | SWI (SVC) | 6 | 系统调用入口 |
| 0x0C | Prefetch Abort | 5 | 取指令地址无效 |
| 0x10 | Data Abort | 2 | 访问非法内存地址 |
| 0x14 | Reserved | — | 不要用 |
| 0x18 | IRQ | 4 | 外设中断入口 |
| 0x1C | FIQ | 3 | 快速中断,有独立寄存器组 |
LDR PC, =handler,结果因为流水线预取导致跳转地址错位。正确的写法是B handler或者LDR PC, [PC, #offset]。
3.2.2 向量表代码实现
.section .vectors, "ax"
_vector_table:
B _reset_handler @ 0x00 Reset
B _undef_handler @ 0x04 Undefined
B _svc_handler @ 0x08 SWI
B _prefetch_handler @ 0x0C Prefetch Abort
B _data_handler @ 0x10 Data Abort
B . @ 0x14 Reserved
B _irq_handler @ 0x18 IRQ
B _fiq_handler @ 0x1C FIQ
嗯,这里要注意:B指令的跳转范围是±32MB,如果你的代码段很大,建议用LDR PC, =handler配合文字池。我习惯在向量表后面放一个跳转表,这样修改中断入口时不用改向量表本身。
3.3 SDRAM初始化——让CPU有内存可用
SDRAM初始化是启动流程里最麻烦的一步。为什么?因为SDRAM控制器本身需要配置,而配置参数又依赖具体的SDRAM芯片型号。我刚开始做的时候,对着数据手册看了三天,才把时序参数算对。
3.3.1 初始化流程概览
咱们先看整体流程,再拆细节:
SDRAM初始化步骤:
1. 配置SDRAM控制器时钟和引脚
2. 发送NOP命令(等待200μs)
3. 发送预充电命令(PRECHARGE)
4. 发送自动刷新命令(AUTO REFRESH)x8次
5. 配置模式寄存器(MRS)
6. 设置控制器时序参数(CAS、tRCD、tRP等)
7. 使能SDRAM控制器
3.3.2 关键时序参数解析
| 参数 | 含义 | 典型值(133MHz) | 配置寄存器 |
|---|---|---|---|
| tRCD | 行地址到列地址延迟 | 2-3个时钟周期 | TRCD字段 |
| tRP | 行预充电时间 | 2-3个时钟周期 | TRP字段 |
| tRAS | 行激活时间 | 5-7个时钟周期 | TRAS字段 |
| CAS | 列地址选通延迟 | 2-3个时钟周期 | CAS字段 |
| tWR | 写恢复时间 | 2个时钟周期 | TWR字段 |
这些参数怎么算?我教你一个笨办法:先看SDRAM芯片数据手册的AC特性表,找到对应频率下的最小周期数,然后往控制器寄存器里填。比如手册说tRCD最小是20ns,你的时钟周期是7.5ns(133MHz),那就要设成3个周期(20/7.5≈2.67,向上取整)。
3.3.3 一个实际的初始化代码
@ SDRAM初始化函数(以S3C2440为例)
sdram_init:
@ 1. 使能SDRAM时钟
LDR R0, =CLK_CON
LDR R1, [R0]
ORR R1, R1, #(1 << 15) @ 打开SDRAM时钟门控
STR R1, [R0]
@ 2. 配置SDRAM控制器
LDR R0, =SDRAM_BASE
LDR R1, =0x00010001 @ NOP命令
STR R1, [R0, #0x00]
@ 等待200μs(用循环延时)
LDR R2, =0x10000
delay_loop:
SUBS R2, R2, #1
BNE delay_loop
@ 3. 预充电
LDR R1, =0x00010002
STR R1, [R0, #0x00]
@ 4. 自动刷新x8次
MOV R3, #8
refresh_loop:
LDR R1, =0x00010003
STR R1, [R0, #0x00]
SUBS R3, R3, #1
BNE refresh_loop
@ 5. 设置模式寄存器
LDR R1, =0x00010004
STR R1, [R0, #0x00]
@ 6. 配置时序参数
LDR R1, =0x00030002 @ CAS=3, tRCD=2, tRP=2
STR R1, [R0, #0x04]
@ 7. 使能SDRAM
LDR R1, =0x00010000
STR R1, [R0, #0x00]
BX LR
3.4 本章知识体系总览
下面这张图把启动流程、汇编指令、中断向量表和SDRAM初始化的关系串起来了。你把它打印出来贴在工位上,写代码时瞄一眼就行。
3.5 避坑指南——我替你试过的错
最后分享几个实战中容易踩的坑,都是我拿加班换来的经验:
- 向量表对齐问题:ARMv7要求向量表32字节对齐,否则VBAR写入会异常。我建议在链接脚本里用
.align 5强制对齐。 - SDRAM刷新间隔:有些控制器默认刷新周期是0(不刷新),你忘了配的话,SDRAM数据会在几十毫秒后丢失。我曾经因为这个bug,查了整整两天。
- 栈溢出:启动代码里的栈通常只有几百字节,如果你在main之前调用了复杂的初始化函数,栈可能不够用。我习惯在链接脚本里给启动栈单独分配4KB。
- 指令流水线:修改CPSR后,紧接着的指令可能还在旧模式下执行。建议在模式切换后加一条
NOP或者ISB指令。