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指令有两种形式——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控制器
核心原则:SDRAM初始化必须严格按照芯片数据手册的时序要求来,少一个刷新周期都不行。我曾经因为自动刷新次数只做了4次(手册要求8次),结果系统运行几分钟后就随机死机。

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
调试建议:初始化完成后,先往SDRAM写一个已知数据(比如0xAA55AA55),再读回来验证。如果读写不一致,八成是时序参数不对。我习惯用JTAG直接查看内存,比串口打印快得多。

3.4 本章知识体系总览

下面这张图把启动流程、汇编指令、中断向量表和SDRAM初始化的关系串起来了。你把它打印出来贴在工位上,写代码时瞄一眼就行。

ARMv7启动流程与核心模块关系图 ARMv7 启动流程与核心模块关系图 芯片上电 Reset 设置中断向量表 初始化 SDRAM 跳转到 main() ARM 汇编指令集 中断向量表配置 SDRAM 初始化 关键提醒 • 向量表必须4字节对齐 • SDRAM时序参数 必须查手册 • 栈指针在跳转C前 必须设置好 • 不同模式用不同栈 • 初始化后验证 SDRAM读写 • 用B指令跳转 不要用LDR PC

3.5 避坑指南——我替你试过的错

最后分享几个实战中容易踩的坑,都是我拿加班换来的经验:

  • 向量表对齐问题:ARMv7要求向量表32字节对齐,否则VBAR写入会异常。我建议在链接脚本里用.align 5强制对齐。
  • SDRAM刷新间隔:有些控制器默认刷新周期是0(不刷新),你忘了配的话,SDRAM数据会在几十毫秒后丢失。我曾经因为这个bug,查了整整两天。
  • 栈溢出:启动代码里的栈通常只有几百字节,如果你在main之前调用了复杂的初始化函数,栈可能不够用。我习惯在链接脚本里给启动栈单独分配4KB。
  • 指令流水线:修改CPSR后,紧接着的指令可能还在旧模式下执行。建议在模式切换后加一条NOP或者ISB指令。
血的教训:千万不要在SDRAM初始化完成之前往SDRAM地址写数据!我见过有人把全局变量放在SDRAM段,结果启动时还没初始化就访问,直接触发Data Abort。正确的做法是:启动代码和栈都放在内部SRAM,等SDRAM初始化完再搬数据。