3. 裸机启动流程:汇编启动代码编写、中断向量表配置、SDRAM初始化

说实话,很多做嵌入式开发的朋友,一上来就怼 RTOS,结果出了问题连启动流程都看不懂。我个人觉得,裸机启动是嵌入式开发的「基本功」,就像练武要先扎马步一样。这一章,我们就来聊聊 ARMv7 上电后到底干了些什么。

3.1 上电第一件事:复位向量

ARMv7 处理器上电复位后,CPU 会从 0x00000000 或者 0xFFFF0000 取指(具体看硬件设计)。这个地址放的不是代码,而是中断向量表。向量表的第一项就是 复位向量,它指向真正的启动代码入口。

我刚开始做项目时,犯过一个低级错误——向量表没对齐。结果芯片一上电就跑飞了,查了两天才发现是地址没对齐到 32 字节边界。嗯,这里要注意:ARMv7 要求向量表必须 32 字节对齐,否则异常处理会出问题。

核心要点: 向量表基地址必须 32 字节对齐,且复位向量位于偏移 0x00 处。

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 高速中断
避坑指南: 我曾经在调试时发现,中断响应慢得离谱。后来一查,是向量表里每个入口只放了 4 字节的跳转指令,导致中断处理函数地址放不下。ARMv7 的向量表每个入口只有 4 字节,所以必须用 ldr pc, [pc, #offset] 或者 ldr pc, =handler 这种间接跳转方式。

3.4 SDRAM 初始化

SDRAM 初始化是启动流程里最「玄学」的部分。为什么这么说?因为时序参数稍微不对,内存就罢工。我见过有人把 CAS 延迟写错了一位,结果系统跑起来偶尔死机,查了三天才找到原因。

SDRAM 初始化通常分这几步:

  1. 上电等待:供电稳定后,等待 100-200us,让内部电路稳定。
  2. 预充电:发送预充电命令,关闭所有行。
  3. 自动刷新:发送至少 8 个自动刷新周期。
  4. 设置模式寄存器:配置 CAS 延迟、突发长度、突发类型等。
  5. 正常操作:进入读写模式。

下面是一个简化的 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]
调试技巧: 我建议在 SDRAM 初始化完成后,先写一个简单的内存测试函数,比如写 0xAA、0x55 模式,再读回来验证。如果这一步过不了,后面的代码跑起来也是「定时炸弹」。

3.5 整体启动流程

把上面几块串起来,整个裸机启动流程就清晰了。我画了一张图,帮你理清脉络:

ARMv7 裸机启动流程图 ARMv7 裸机启动流程图 上电复位 读取复位向量 汇编启动代码 设置栈指针 / 清零 BSS / 拷贝数据 SDRAM 初始化 预充电 → 自动刷新 → 设置模式寄存器 跳转 main() 注意:中断向量表必须在 SDRAM 初始化之前配置好,否则中断来了无处可去

从这张图可以看出来,整个启动流程是线性的,每一步都依赖上一步的结果。我见过有人把 SDRAM 初始化放在向量表配置前面,结果向量表地址还没设好,SDRAM 就初始化了——这其实没问题,但如果你在 SDRAM 初始化过程中触发了异常,那就麻烦了。

3.6 常见问题与避坑

  • 向量表地址不对齐:ARMv7 要求 32 字节对齐,否则异常处理会跳转到错误地址。
  • SDRAM 时序参数错误:CAS 延迟、刷新周期这些参数必须和芯片手册一致,差一个周期都可能出问题。
  • 栈指针未初始化:在设置栈指针前调用函数,会导致程序跑飞。我建议在复位后第一件事就是设栈指针。
  • 中断使能过早:外设还没初始化就开中断,中断处理函数可能访问到未初始化的寄存器。
曾经踩过的坑: 有一次我在调试时发现,系统偶尔会在启动时死机。后来用示波器抓 SDRAM 的时钟,发现时钟频率比预期高了 10MHz。原来是 PLL 配置错了,导致 SDRAM 跑在超频状态。所以,我建议在初始化 SDRAM 之前,先用示波器确认时钟频率。

好了,这一章的内容就到这里。裸机启动看似简单,但每一步都有讲究。你把这些基础打牢了,后面学 RTOS 任务调度就会轻松很多。