第1章:启动ROM与PBL——芯片内部固化代码如何初始化硬件并寻找Bootloader

大家好,我是你们的讲师。今天咱们聊聊芯片上电后,第一行代码到底从哪来。

很多人写了好几年嵌入式,一提到启动流程,脑子里就是“上电→跑main()”。但实际呢?芯片内部藏着一套固化逻辑,它才是真正的“开天辟地”。这套逻辑,就是启动ROM(Boot ROM)和PBL(Primary Boot Loader)。

1.1 启动ROM:芯片出厂就焊死的“第一行代码”

启动ROM,说白了就是芯片制造商在生产时,用掩膜工艺固化在硅片上的只读代码。你改不了,也删不掉。它位于CPU的地址空间最底部,通常是0x00000000或者0xFFFF0000(取决于架构)。

它的核心任务只有三个:

  • 初始化最基础的硬件:比如CPU模式、堆栈指针、时钟源、内存控制器(如果片内有SRAM的话)。
  • 检测启动介质:扫描SPI Flash、eMMC、NAND、UART等接口,判断哪个设备里有可执行的Bootloader。
  • 加载并跳转:把找到的Bootloader(通常是SPL或U-Boot SPL)拷贝到片内SRAM,然后跳过去执行。

关键点:启动ROM本身不加载操作系统,它只负责把“下一级引导程序”请进内存。这个“下一级”就是我们常说的PBL。

我记得早年做一款ARM9的工控板,芯片手册里写着“Boot ROM supports NAND boot”。结果焊上去死活起不来。后来拿逻辑分析仪抓时序才发现,ROM里默认的NAND时序参数跟我的Flash不匹配。嗯,这就是固化代码的坑——你没法打补丁。

1.2 PBL:从ROM手里接过接力棒

PBL(Primary Boot Loader)是启动ROM加载进来的第一段用户代码。它通常非常小,比如TI的SPL只有8KB~32KB,放在片内SRAM里运行。

PBL要干的事,比ROM更具体:

  • 初始化DDR内存:片内SRAM太小,装不下完整的U-Boot或Linux内核。PBL得把DDR控制器配置好,让大内存可用。
  • 加载完整的Bootloader:从Flash或存储设备里,把U-Boot、LK(Little Kernel)或ABL(Android Bootloader)拷贝到DDR中。
  • 建立基本的设备树或参数传递:告诉下一级程序,当前硬件是什么配置。

我的经验:PBL阶段最容易翻车的是DDR初始化。DDR的时序参数、电压、ODT配置,错一个就黑屏。我曾经在量产前一周发现,某批次DDR颗粒的刷新周期比标准值慢了5%,导致PBL加载U-Boot时随机崩溃。排查了三天,最后用示波器量波形才找到原因。

1.3 启动介质检测:ROM怎么知道从哪启动?

芯片上电后,ROM会按照预设的优先级顺序,去尝试读取启动设备。这个顺序通常由芯片的启动引脚(Boot Mode Pins)决定。比如高通平台,会检测BOOT_CONFIG寄存器;全志芯片则看GPIO的上拉/下拉状态。

典型的检测顺序:

  1. SPI NOR Flash:最快,但容量小。常用于存放SPL。
  2. eMMC/SD卡:容量大,速度中等。Android设备的主流选择。
  3. NAND Flash:有坏块管理问题,ROM通常只读前几个块。
  4. UART/USB:用于工厂烧录或调试,ROM里会内置一个小的下载协议。

为什么会设计成顺序检测?说白了,是为了兼容性。你想想看,同一颗芯片可能用在路由器、手机、工控机上,启动介质千差万别。ROM只能一个个试,试到能读出有效数据为止。

避坑指南:我曾经遇到一个案例,客户把eMMC的BOOT分区配置成了“写保护”,结果ROM读不到有效的启动签名,一直回退到USB下载模式。折腾了两天,最后发现是eMMC的EXT_CSD寄存器里BOOT_WP位被置1了。所以,如果你遇到芯片反复进入下载模式,先查启动介质的状态。

1.4 代码示例:一个简化的PBL加载流程

下面这段伪代码,展示了PBL从SPI Flash读取U-Boot并跳转的核心逻辑。实际芯片的ROM代码比这复杂得多,但骨架是一样的。

// 伪代码:PBL加载U-Boot到DDR
void pbl_main(void) {
    // 1. 初始化DDR控制器
    ddr_init(CONFIG_DDR_TYPE, CONFIG_DDR_SPEED);
    
    // 2. 从SPI Flash读取U-Boot镜像
    //    偏移0x20000,大小0x80000字节
    uint32_t *uboot_dest = (uint32_t *)DDR_BASE_ADDR;
    spi_flash_read(0x20000, uboot_dest, 0x80000);
    
    // 3. 验证镜像签名(可选)
    if (verify_signature(uboot_dest) != 0) {
        // 签名失败,进入恢复模式
        enter_recovery_mode();
        return;
    }
    
    // 4. 设置CPU模式,关闭中断
    cpu_set_mode(CPU_MODE_SVC);
    disable_interrupts();
    
    // 5. 跳转到U-Boot入口
    //    注意:跳转前要清理缓存,避免指令不一致
    flush_dcache_all();
    jump_to((void (*)(void))DDR_BASE_ADDR);
    
    // 永远不会执行到这里
    while(1);
}

这段代码里,spi_flash_read是ROM提供的API,PBL可以直接调用。但要注意,ROM里的API通常是用汇编写的,调用约定跟C不一样。我建议你在移植PBL时,先读芯片手册的“ROM API”章节,别想当然地直接声明函数指针。

1.5 启动ROM与PBL的协作时序

为了让你更直观地理解,我画了个时序表。这是基于某款Cortex-A7芯片的真实数据。

阶段 执行者 耗时(典型值) 主要动作
上电复位 硬件 ~1ms 电源稳定、时钟起振、CPU复位释放
ROM启动 Boot ROM ~5ms 初始化CPU、堆栈、检测启动引脚
介质检测 Boot ROM ~10ms 轮询SPI/eMMC/UART,读取有效头部
PBL加载 Boot ROM ~2ms 将PBL从Flash拷贝到SRAM
PBL执行 PBL ~20ms 初始化DDR,加载U-Boot到DDR
U-Boot启动 U-Boot ~100ms 初始化外设,加载内核

你看,从复位到U-Boot跑起来,总共也就130ms左右。这里面ROM占了将近一半的时间。所以,如果你觉得系统启动慢,别光盯着内核,先看看ROM阶段的介质检测是不是超时了。

1.6 小结与思考

这一章我们聊了启动ROM和PBL的分工。说白了,ROM是“看门大爷”,负责开门;PBL是“搬运工”,负责把大件行李(U-Boot)搬进屋里。

最后留个思考题:如果ROM检测启动介质时,所有设备都返回错误,芯片会怎么办?

答案是——死循环。大多数芯片会反复尝试,或者进入一个内置的“下载模式”,等待外部工具通过UART/USB来救砖。这也是为什么很多开发板按住某个按键上电,就能进入烧录模式的原因。

个人建议:做产品时,一定要在PBL里加个“看门狗喂狗”操作。否则一旦PBL卡死在DDR初始化里,整个系统就彻底死给你看。我见过太多工程师在调试阶段,因为忘了喂狗,导致每次复位都要等看门狗超时,白白浪费几秒钟。

下一章,我们会深入PBL的代码细节,看看它到底怎么配置DDR控制器,以及如何安全地跳转到U-Boot。咱们下次见。