第1章:BootROM启动流程

各位同学,今天我们来聊聊BootROM的启动流程。说实话,这个主题我讲了十几年,每次都有新感悟。BootROM是芯片上电后执行的第一段代码,它就像系统的"接生婆"——从一片混沌中把CPU唤醒,然后一步步引导系统进入正轨。

我个人习惯把BootROM启动流程拆成五个关键阶段:上电复位 → CPU初始化 → BootROM代码执行 → 启动设备检测 → 加载并跳转到下一级Bootloader。这五个阶段环环相扣,任何一个环节出问题,系统都起不来。

核心要点:BootROM是固化在芯片内部ROM中的一段程序,上电后CPU自动从复位向量指向的地址开始执行。它负责最基本的硬件初始化和启动引导。

上电复位 Power-On Reset CPU初始化 CPU Init BootROM执行 ROM Code 设备检测 Device Detect 跳转加载 Jump & Load BootROM启动五阶段流程图 ① 上电复位:电源稳定后,复位信号释放,CPU从复位向量地址(如0x00000000)取指 ② CPU初始化:设置栈指针、异常向量表、关闭中断、配置基本时钟 ③ BootROM执行:固化在ROM中的启动代码开始运行,完成核心初始化 ④ 设备检测:扫描启动设备(NAND/eMMC/UART/SPI等),按优先级尝试加载 ⑤ 跳转加载:将下一级Bootloader(如SPL/ATF)加载到RAM,跳转执行

1.1 上电复位——一切从这里开始

上电复位,说白了就是给芯片"通电并归零"。当电源电压达到稳定阈值后,复位控制器会释放复位信号。CPU看到复位信号撤除,立刻从预设的复位向量地址开始取指令。

这里有个细节很多人容易忽略:复位向量地址。不同架构不一样,ARM Cortex-M系列通常是0x00000000,而Cortex-A系列可能是0x00000000或0xFFFF0000。x86则是0xFFFFFFF0。嗯,这个地址是芯片设计时硬编码的,改不了。

小技巧:我在调试一块新板子时,第一件事就是用逻辑分析仪抓复位引脚和时钟引脚的波形。如果复位时序不对,后面全是白搭。曾经有个项目,板子死活起不来,查了两天发现是复位芯片的延迟时间不够——电源还没稳定就释放复位了。

1.2 CPU初始化——给处理器"穿衣服"

CPU从复位向量取到第一条指令后,首先要做的是初始化自身。这个阶段通常由汇编代码完成,因为C语言运行时环境还没建立。

主要干这几件事:

  • 设置栈指针(SP)——没有栈,函数调用都做不了
  • 配置异常向量表——告诉CPU中断来了该跳到哪里
  • 关闭所有中断——初始化期间不希望被打扰
  • 初始化关键寄存器——比如系统控制寄存器、时钟配置
  • 清零BSS段——全局变量初始化为0

你想想看,如果栈指针没设对,调用第一个函数就直接跑飞了。我见过最离谱的bug,就是有人把栈指针设到了未初始化的DDR地址上——结果DDR还没初始化,写进去的数据全丢了。

; 典型的ARM Cortex-A系列启动汇编片段
reset_handler:
    ; 设置栈指针,指向SRAM顶部
    ldr sp, =_stack_top
    
    ; 关闭所有中断
    mrs r0, cpsr
    orr r0, r0, #0xC0    ; 设置I和F位
    msr cpsr, r0
    
    ; 清零BSS段
    ldr r0, =_bss_start
    ldr r1, =_bss_end
    mov r2, #0
clear_bss:
    str r2, [r0], #4
    cmp r0, r1
    blo clear_bss
    
    ; 跳转到C语言入口
    bl main

1.3 BootROM代码执行——固化的"灵魂"

CPU初始化完成后,控制权交给BootROM的主代码。这部分代码是芯片出厂时烧录在ROM里的,用户改不了。BootROM的主要任务是什么?说白了就是检测启动设备,加载下一级程序。

BootROM代码通常包含:

  • 基本的硬件抽象层(操作GPIO、UART、时钟等)
  • 启动设备驱动(NAND、eMMC、SPI Flash、UART等)
  • 启动设备检测逻辑(按优先级轮询)
  • 镜像加载和校验逻辑(检查签名、CRC等)
  • 跳转执行逻辑

注意:BootROM的代码大小通常被严格限制。比如很多SoC的BootROM只有32KB或64KB。为什么?因为ROM面积很贵,每多1KB,芯片成本就多一分。所以BootROM代码必须精简高效,能用汇编就不用C,能省一行是一行。

1.4 启动设备检测——"找启动盘"

这一步很有意思。BootROM需要按照预设的优先级顺序,去检测各个启动设备。比如先看eMMC有没有合法的启动镜像,没有的话再看SPI Flash,再不行就试试UART下载。

典型的启动设备优先级表:

优先级 启动设备 典型用途 检测方式
1(最高) eMMC/SD卡 主流存储设备 读取特定扇区,检查签名
2 SPI NOR/NAND Flash 低成本方案 读取固定偏移,检查Magic Number
3 UART 调试/工厂烧录 等待主机发送数据
4 USB 量产烧录 USB枚举,等待主机命令
5(最低) 网络(TFTP/PXE) 网络启动 DHCP请求,等待服务器响应

我曾经在一个项目中遇到一个诡异的问题:板子偶尔启动失败,但多复位几次就好了。查了很久发现是eMMC初始化时序问题——BootROM检测eMMC时,eMMC还没准备好,返回了错误。后来在eMMC检测前加了一个小延时,问题就解决了。嗯,硬件时序问题,永远是嵌入式调试的"老朋友"。

1.5 加载并跳转到下一级Bootloader

BootROM找到合法的启动设备后,会把下一级Bootloader(通常叫SPL、TFA、或U-Boot SPL)加载到SRAM或DDR中。然后跳转过去执行。

这个跳转过程需要注意几个关键点:

  • 镜像格式——BootROM通常要求镜像有特定头部,包含加载地址、大小、校验和等信息
  • 加载地址——必须和下一级Bootloader的链接地址一致,否则跳转后跑飞
  • 关闭Cache和MMU——跳转前通常要关闭,因为下一级Bootloader会重新配置
  • 传递参数——BootROM可以通过寄存器传递一些信息,比如启动设备类型、时钟频率等
// BootROM跳转前的典型操作(伪代码)
void bootrom_jump_to_next(void *image_addr, uint32_t entry_point)
{
    // 1. 关闭中断
    disable_interrupts();
    
    // 2. 关闭MMU和Cache
    mmu_disable();
    cache_disable();
    
    // 3. 设置参数寄存器
    // r0 = 启动设备类型
    // r1 = 设备实例号
    asm volatile("mov r0, %0" : : "r"(g_boot_device));
    asm volatile("mov r1, %0" : : "r"(g_boot_instance));
    
    // 4. 跳转到入口地址
    void (*entry)(void) = (void (*)(void))entry_point;
    entry();
    
    // 正常情况下不会执行到这里
    while(1);
}

避坑指南:我曾经在跳转前忘记关闭MMU,结果下一级Bootloader一上来就访问了虚拟地址,而页表已经被BootROM覆盖了——直接触发Page Fault。从那以后,我每次写跳转代码都会再三确认:Cache关了没?MMU关了没?中断关了没?

小结

BootROM的启动流程,说白了就是芯片从"死"到"活"的过程。上电复位是唤醒,CPU初始化是穿衣打扮,BootROM代码是大脑决策,设备检测是找启动盘,最后跳转加载是把接力棒交给下一棒。

理解了这个流程,你就能明白为什么有时候板子起不来——可能是复位时序不对,可能是BootROM没找到启动设备,也可能是跳转地址配错了。每个环节都是潜在的坑,但也是我们可以调试和优化的点。

嗯,这一章就到这里。下一章我们会深入BootROM的代码结构,看看那些固化在ROM里的汇编和C代码到底长什么样。

可可精品资料库