3. Boot ROM 启动流程:上电复位序列、代码执行入口与硬件初始化

芯片上电那一刻,到底发生了什么?

很多人觉得 Boot ROM 就是一段固件,上电就跑。其实没那么简单。我做过好几款 SoC 的信任根设计,可以负责任地告诉你:从电源稳定到 CPU 执行第一条指令,中间藏着大量硬件时序和状态机逻辑。搞不清楚这些,后面做安全启动就是空中楼阁。

3.1 上电复位序列:不是一上来就跑代码

芯片上电后,第一件事不是执行代码,而是等电源稳定。你想想看,电压都没爬升到位,CPU 怎么工作?

完整的复位序列通常分三步:

  1. POR(Power-On Reset):电源检测电路监测 VDD 是否达到阈值。没到?就一直复位。到了?释放 POR 信号。
  2. 系统级复位:POR 释放后,时钟发生器开始起振。PLL 锁定需要时间,通常几百微秒到几毫秒。这期间所有模块保持复位。
  3. Boot ROM 复位释放:时钟稳定后,复位控制器释放 CPU 的复位信号。CPU 从复位向量地址取第一条指令。

关键点:Boot ROM 的代码执行入口,就是 CPU 复位向量指向的地址。这个地址在芯片设计阶段就硬编码死了,不可更改。

我曾经遇到过一个坑:某款芯片的 POR 阈值设得太低,电源纹波大时反复触发复位,导致 Boot ROM 永远跑不完初始化。后来加了迟滞电路才解决。嗯,硬件设计就是这样,细节决定成败。

3.2 Boot ROM 代码执行入口:复位向量的秘密

CPU 复位后,PC 指针指向哪里?

这取决于架构。ARM Cortex-M 系列从 0x00000000 取栈指针,从 0x00000004 取复位向量。RISC-V 通常从 0x80000000 或 0x00000000 启动。但不管哪种架构,Boot ROM 必须映射到复位向量所在的地址空间。

我习惯把 Boot ROM 放在芯片地址空间的最高优先级区域——通常是片内 ROM,不可写,不可篡改。这样攻击者没法通过修改复位向量来劫持启动流程。

架构 复位向量地址 Boot ROM 映射方式
ARM Cortex-M 0x00000004 ROM 直接映射到 0x00000000 起始区域
ARM Cortex-A 0x00000000 或 0xFFFF0000 ROM 通过别名窗口映射
RISC-V 0x80000000(典型) ROM 直接映射到复位地址
Xtensa 0xFE000000 ROM 固定映射,不可重映射

个人建议:设计 Boot ROM 时,在复位向量处放一条跳转指令,而不是直接放代码入口。这样后续如果要微调入口地址,只需要改跳转目标,不用动整个 ROM 布局。

3.3 硬件初始化步骤:Boot ROM 的「三板斧」

CPU 开始执行 Boot ROM 代码后,第一件事是什么?

不是加载操作系统,也不是校验签名。Boot ROM 首先要让芯片「活过来」——初始化最基本的硬件环境。

我总结为三步,业内叫「Boot ROM 三板斧」:

  • 第一斧:时钟与电源——配置系统时钟源、PLL 倍频、分频比。关闭不必要的电源域以降低功耗。
  • 第二斧:内存控制器——初始化 SRAM 或片内 RAM。没有 RAM,连栈都建不起来,后面啥也干不了。
  • 第三斧:关键外设——初始化启动介质控制器(如 SPI、NAND、eMMC 控制器)和调试接口(如 UART、JTAG)。

这三步的顺序不能乱。你想想看,没时钟怎么配外设?没 RAM 怎么建栈?

下面这张图展示了 Boot ROM 启动流程的核心逻辑:

Boot ROM 启动流程核心逻辑 Boot ROM 启动流程核心逻辑 芯片上电 POR 释放 时钟 PLL 锁定 CPU 复位释放 取复位向量 → 跳转 Boot ROM Boot ROM 三板斧 时钟 → RAM → 启动介质控制器 说明:整个流程从上电到 Boot ROM 执行,全部由硬件状态机控制,无需软件干预

3.4 代码示例:Boot ROM 初始化伪代码

下面是一段简化的 Boot ROM 初始化伪代码。实际芯片的代码比这复杂得多,但核心逻辑是一样的:

// Boot ROM 入口(复位向量指向此处)
void boot_rom_entry(void) {
    // 第一步:设置栈指针
    // 此时 RAM 还没初始化,栈放在片内 SRAM 或寄存器中
    asm("ldr sp, =__stack_top");
    
    // 第二步:时钟初始化
    // 配置系统 PLL,使 CPU 跑在目标频率
    pll_config_t pll_cfg = {
        .ref_clk = 24,       // 24MHz 参考时钟
        .mult = 25,          // 倍频 25 倍
        .div = 1             // 不分频
    };
    pll_init(SYS_PLL, &pll_cfg);
    
    // 等待 PLL 锁定
    while (!(pll_get_status(SYS_PLL) & PLL_LOCKED));
    
    // 切换系统时钟到 PLL 输出
    clock_switch_source(SYS_CLK, SRC_PLL);
    
    // 第三步:初始化片内 SRAM
    // 使能 RAM 控制器,配置时序
    sram_init();
    
    // 第四步:初始化启动介质控制器
    // 根据启动模式选择 SPI / NAND / eMMC
    boot_device_t dev = get_boot_device();
    media_init(dev);
    
    // 第五步:加载并验证下一级 Bootloader
    // 这是信任根的核心——校验签名
    if (verify_bootloader(dev) == PASS) {
        jump_to_bootloader();
    } else {
        // 校验失败,进入安全错误处理
        secure_error_handler();
    }
}

注意:Boot ROM 代码必须放在片内 ROM 中,且在生产时一次性写入。任何可重编程的存储介质(如 Flash)都不能作为信任根载体。这是硬件信任根的第一条铁律。

3.5 避坑指南:我踩过的三个坑

做了这么多年嵌入式安全,Boot ROM 初始化这块我踩过不少坑。挑三个典型的说说:

  • 坑一:PLL 锁定超时——我曾经遇到过一款芯片,PLL 锁定时间比数据手册长了 3 倍。Boot ROM 里用了固定延时,结果部分芯片启动失败。后来改成轮询锁定状态 + 超时复位,问题解决。
  • 坑二:RAM 初始化顺序——某次我把 RAM 初始化放在时钟切换之前,结果 RAM 控制器因为时钟不稳定,初始化了一半就挂了。嗯,这个错误很低级,但压力大的时候真会犯。
  • 坑三:复位向量被篡改——有客户问能不能把 Boot ROM 放在 Flash 里,方便升级。我说不行。Flash 可以被改写,一旦复位向量被篡改,信任根就形同虚设。硬件信任根必须是一次性写入、不可更改的。

3.6 小结:Boot ROM 启动流程的核心要点

回顾一下本章的核心内容:

  • 上电复位序列分三步:POR → 时钟稳定 → CPU 复位释放。每一步都有硬件状态机保证时序。
  • Boot ROM 代码执行入口由复位向量决定,这个地址在芯片设计阶段硬编码,不可更改。
  • 硬件初始化遵循固定顺序:时钟 → RAM → 启动介质控制器。顺序错了,系统就起不来。
  • Boot ROM 必须放在片内 ROM 中,这是硬件信任根的基础。