第三章 启动流程第一阶段:SoC上电、ROM Code执行、启动设备选择与安全启动校验

好,咱们正式开始聊Bootloader的第一阶段。这一阶段,说白了就是芯片从「死」到「活」的那个瞬间。很多做上层应用的兄弟觉得这步很简单——上电就跑呗。但实际做过底层的人都知道,这恰恰是整个系统最容易翻车的地方。

我记得刚入行那会儿,带我的老工程师跟我说过一句话:「芯片上电的前100微秒,决定了你后面是加班还是准点下班。」当时我不理解,直到自己踩了坑才明白——嗯,这话一点不夸张。

3.1 SoC上电:从0到1的瞬间

当机顶盒插上电源,电源管理IC(PMIC)开始工作。它先给SoC提供稳定的电压,然后释放复位信号。这个过程不是瞬间完成的,而是有严格的时序要求。

我个人习惯把上电分成三个子阶段:

  • POR(Power-On Reset)阶段:内部复位电路检测到VDD达到阈值,释放内部复位
  • 时钟稳定阶段:PLL锁定,系统时钟开始稳定输出
  • Boot ROM映射阶段:复位向量指向ROM Code入口地址

这里有个关键点——复位向量地址。不同SoC厂商定义不同。海思的芯片通常把复位向量放在0xFFFF0000,而有些MIPS架构的芯片放在0xBFC00000。你想想看,如果Boot ROM映射错了,整个系统连第一条指令都跑不到。

⚠️ 我曾经踩过的坑:有一次调试新板子,上电后串口死活没输出。查了两天,最后发现是PMIC的Power Good信号时序不满足SoC要求,导致复位释放早了100微秒。SoC内部逻辑还没完全初始化就开始跑代码,直接挂死。从那以后,我每次画板子都会仔细看PMIC的datasheet,确认上电时序。

3.2 ROM Code执行:芯片出厂自带的「第一行代码」

ROM Code是固化在芯片内部ROM里的程序。用户改不了,也删不掉。它的任务很简单——初始化最基本的硬件环境,然后去找启动设备

ROM Code一般会做以下几件事:

  1. 初始化CPU核心(设置异常向量表、配置Cache)
  2. 初始化内部SRAM(作为临时栈和变量区)
  3. 配置基本时钟(让CPU能跑起来)
  4. 初始化启动设备控制器(比如SD/MMC控制器、NAND控制器)

代码量通常很小,几KB到几十KB不等。因为ROM空间太贵了,芯片厂商能省则省。

举个例子,某款主流机顶盒SoC的ROM Code伪代码大概长这样:

// 简化版ROM Code入口
void boot_rom_entry(void) {
    // 1. 关闭所有中断
    disable_interrupts();
    
    // 2. 设置栈指针
    sp = INTERNAL_SRAM_TOP;
    
    // 3. 初始化PLL,设置CPU频率
    pll_init(SYS_CLK_1000MHZ);
    
    // 4. 初始化启动设备控制器
    boot_dev_init();
    
    // 5. 读取启动设备选择寄存器
    boot_device = read_boot_sel_register();
    
    // 6. 根据启动设备加载Bootloader
    load_bootloader(boot_device);
}

你看,就这么几行逻辑。但每一行背后都是硬件细节。比如pll_init这个函数,它要等PLL锁定才能返回。如果晶振有问题,PLL永远锁不定,代码就卡死在这里了。

💡 小技巧:调试时如果发现芯片上电后完全没反应,先量晶振波形。我见过太多案例,最后发现是晶振负载电容配错了,导致起振失败。ROM Code连第一步都走不过去。

3.3 启动设备选择:芯片怎么知道从哪儿启动?

ROM Code执行完后,下一步就是决定从哪个设备加载下一级Bootloader。这个选择机制,不同厂商实现方式不同。

常见的启动设备选择方式有:

选择方式 原理 典型应用
硬件管脚采样 上电时采样GPIO电平,组合成启动序号 海思、瑞芯微
eFuse/OTP配置 芯片内部一次性编程寄存器,固化启动顺序 高安全要求的机顶盒
自动探测 ROM Code依次尝试各设备,直到找到有效Bootloader 部分低端芯片

我个人最喜欢硬件管脚采样的方式。为什么?因为调试方便。板子贴好了,想改启动顺序?换个电阻就行。不用重新烧录芯片。

但这里有个坑——采样时序。ROM Code在上电后极短的时间内(通常是几十微秒)完成管脚采样。如果外部电路的上拉/下拉电阻没配好,或者电容太大导致电平建立太慢,采样结果就是错的。

🔑 核心要点:启动设备选择是ROM Code和硬件设计之间的「握手协议」。硬件工程师必须确保采样时刻的电平是稳定的。软件工程师必须知道采样窗口有多长。

3.4 安全启动校验:不让恶意代码混进来

现在机顶盒基本都要求安全启动。说白了就是——ROM Code加载下一级Bootloader之前,先检查它是不是「正版」的。

安全启动的典型流程:

  1. ROM Code从启动设备读取Bootloader镜像
  2. 同时读取该镜像的数字签名
  3. 用芯片内部固化的公钥验证签名
  4. 验证通过 → 跳转执行;验证失败 → 停止启动

这里的关键是公钥存储在哪儿。通常放在eFuse或者OTP里,一次性写入,不可更改。芯片出厂时,厂商会把公钥烧进去。

我记得有个项目,客户要求支持安全启动。我们按照标准流程做了,结果发现每次启动都校验失败。查了很久,最后发现是eFuse烧录时电压没控制好,导致部分bit没烧进去,公钥读出来是错的。嗯,从那以后我每次烧eFuse都会用示波器盯着电压波形。

安全启动的校验算法一般是RSA或ECDSA。RSA2048比较常见,但计算量大。ECDSA签名更短,计算更快,现在越来越流行。

伪代码示例:

// 安全启动校验逻辑
int secure_boot_verify(uint8_t *image, uint32_t image_len) {
    // 1. 从eFuse读取公钥
    uint8_t pub_key[256];
    efuse_read(EFUSE_PUB_KEY, pub_key);
    
    // 2. 从镜像末尾读取签名
    uint8_t signature[256];
    memcpy(signature, image + image_len - 256, 256);
    
    // 3. 计算镜像哈希
    uint8_t hash[32];
    sha256(image, image_len - 256, hash);
    
    // 4. 验签
    if (rsa_verify(pub_key, hash, signature) == 0) {
        return OK;  // 校验通过
    } else {
        return FAIL; // 校验失败
    }
}

⚠️ 注意:安全启动不是万能的。如果ROM Code本身有漏洞,或者公钥被泄露,整个安全链就断了。我见过一些方案,为了调试方便,在ROM Code里留了「后门」——如果检测到某个GPIO拉低,就跳过校验。这种设计在生产板上必须禁用,否则安全启动形同虚设。

3.5 第一阶段总结

第一阶段的核心任务就三个字——找入口。找到下一级Bootloader的入口,验证它,然后跳过去。

这个阶段出问题,通常表现为:

  • 上电后完全没反应(时钟/复位问题)
  • 串口输出乱码(启动设备选错了)
  • 反复重启(安全校验失败)

调试手段也很有限——示波器量波形、看电流变化、听电容啸叫。嗯,听起来很原始,但这就是底层工程师的日常。

下一章我们会聊第二阶段——Bootloader自身初始化、DDR训练、以及如何把内核从Flash搬到内存里。那个阶段代码量更大,坑也更多。咱们到时候细说。