2. BootROM的启动流程:上电复位与CPU初始化、BootROM代码入口点、第一阶段硬件初始化

好,我们正式开始聊BootROM。上一章我们说了,芯片上电后第一个执行的代码就是它。那它到底干了些什么?说白了,BootROM就是一段固化在芯片内部ROM里的“最小启动引导程序”。它负责把CPU从复位状态拉起来,把最基础的硬件初始化好,然后找到下一级启动代码(比如SPL或U-Boot),把控制权交出去。

我个人习惯把BootROM的启动流程拆成三个阶段:上电复位与CPU初始化、BootROM代码入口点、第一阶段硬件初始化。咱们一个一个来过。

2.1 上电复位与CPU初始化

芯片上电那一刻,所有逻辑电路都处于不确定状态。这时候硬件会做两件事:

  • 释放复位信号:电源稳定后,POR(Power-On Reset)电路会拉高复位引脚,让CPU从复位向量指向的地址开始取指。
  • CPU进入已知状态:大部分ARM Cortex-A系列处理器在上电后会进入SVC模式(超级用户模式),关闭中断,MMU和Cache默认都是关闭的。

嗯,这里要注意:不同架构的复位向量地址不一样。ARM Cortex-A系列通常从0x00000000或0xFFFF0000开始,而有些RISC-V芯片则从0x80000000开始。我遇到过一块国产MCU,它的BootROM入口居然在0x1FFF0000,当时查手册查了半天才找到。

核心要点:复位向量地址是芯片设计时定死的,你改不了。BootROM代码必须放在这个地址上,否则CPU一上电就跑飞了。

2.2 BootROM代码入口点

CPU从复位向量取到第一条指令,这条指令通常是一个跳转指令,直接跳到BootROM的真正入口函数。为什么这么设计?因为复位向量只有4字节(ARM模式下),放不下太多代码。所以一般放一条B Reset_Handler或者LDR PC, =Reset_Handler。

我见过一个典型的BootROM入口代码,大概长这样:

; ARM Cortex-A7 BootROM入口示例
.section .vectors, "ax"
.global _start

_start:
    B       Reset_Handler        ; 复位向量
    B       Undefined_Handler    ; 未定义指令
    B       SVC_Handler          ; 软件中断
    B       Prefetch_Handler     ; 预取中止
    B       Data_Handler         ; 数据中止
    NOP                          ; 保留
    B       IRQ_Handler          ; 普通中断
    B       FIQ_Handler          ; 快速中断

Reset_Handler:
    ; 设置栈指针
    LDR     SP, =__stack_top
    ; 关闭看门狗
    LDR     R0, =WDT_BASE
    MOV     R1, #0
    STR     R1, [R0]
    ; 初始化关键寄存器
    BL      SystemInit
    ; 跳转到主流程
    BL      BootROM_Main

你看,入口点干的事很明确:设置栈指针、关看门狗、初始化关键寄存器,然后跳转到主流程。为什么先关看门狗?因为BootROM执行需要时间,如果看门狗没关,还没跑完就被复位了,那就死循环了。我曾经在一个项目里忘了关看门狗,结果BootROM每次跑到一半就被复位,折腾了两天才发现。

小技巧:调试BootROM时,可以在入口点加一个GPIO翻转操作。用示波器抓这个GPIO,就能确认CPU是否真的跑到了你的代码。这比用JTAG快多了。

2.3 第一阶段硬件初始化

进入BootROM主流程后,第一件事就是初始化硬件。这个阶段的目标是让芯片具备最基本的运行能力。我把它归纳为以下几个步骤:

  1. 时钟系统初始化:配置PLL,给CPU、总线、外设提供稳定的时钟。
  2. 内存控制器初始化:如果芯片有片内SRAM,直接可用;如果需要外部DDR,得先配好DDR控制器。
  3. 关键外设初始化:比如UART(用于打印调试信息)、定时器(用于超时控制)。
  4. 启动设备检测:判断从哪个设备加载下一级代码(NAND Flash、eMMC、SD卡、SPI NOR等)。

下面这张图展示了BootROM第一阶段初始化的典型流程:

BootROM第一阶段硬件初始化流程 上电复位 CPU初始化 时钟系统初始化 内存控制器初始化 关键外设初始化 启动设备检测

你看,这个流程是串行的,每一步都依赖上一步。时钟没配好,内存控制器就没办法工作;内存没初始化,代码就没地方放。所以顺序不能乱。

2.3.1 时钟系统初始化

时钟初始化是第一步,也是最容易出问题的一步。芯片上电后默认使用内部低速振荡器(比如32kHz或几MHz),BootROM需要把它切换到PLL锁相环产生的高频时钟。

我一般会按这个顺序配时钟:

  • 先使能外部晶振(如果有的话),等待它稳定。
  • 配置PLL的倍频和分频系数,得到目标频率。
  • 把系统主时钟切换到PLL输出。
  • 配置各总线时钟分频(AHB、APB等)。

注意:切换时钟源时,一定要先确认新时钟已经稳定。我见过有人直接切PLL,结果PLL还没锁定,CPU直接跑飞了。正确的做法是:先使能PLL,轮询锁定标志位,再切换时钟源。

2.3.2 内存控制器初始化

如果芯片有片内SRAM,这一步就简单了——SRAM上电即可用,不需要额外配置。但如果要用外部DDR,那就复杂了。

DDR初始化通常包括:

  • 配置DDR控制器寄存器(时序参数、地址映射等)。
  • 发送DDR初始化序列(NOP、预充电、刷新、模式寄存器设置等)。
  • 做一次写读校验,确认DDR工作正常。

我做过一个项目,DDR布线有点长,导致信号质量不好。BootROM里做写读校验时,发现读回来的数据和写进去的不一样。后来在DDR控制器里加了ODT(片上端接)和驱动强度调整才搞定。所以,DDR初始化一定要做校验,不然后面代码跑起来莫名其妙地崩溃,你都不知道问题出在哪。

2.3.3 关键外设初始化

这一步主要是初始化UART和定时器。UART用来打印调试信息,定时器用来做超时控制。

UART初始化很简单:配波特率、数据位、停止位、校验位。我习惯把UART的波特率设成115200,因为大部分串口工具都支持这个速率。

定时器呢?BootROM里一般用系统定时器(System Timer)或者看门狗定时器(WDT)来做超时。比如等待外部晶振稳定时,如果超时了还没稳定,就说明晶振有问题,BootROM可以切换到内部振荡器继续执行。

2.3.4 启动设备检测

这一步决定了BootROM从哪个设备加载下一级代码。芯片通常支持多种启动方式,比如:

启动模式 典型设备 特点
NAND Flash 并行NAND 容量大,需要ECC校验
eMMC 嵌入式MMC 速度快,协议复杂
SD卡 SD/MMC卡 可插拔,常用于开发调试
SPI NOR 串行NOR Flash 容量小,可直接执行(XIP)
UART 串口下载 速度慢,用于工厂烧录

启动模式通常由芯片的BOOT引脚电平决定。BootROM会读取这些引脚的状态,然后按优先级尝试从对应的设备加载代码。

举个例子,如果BOOT引脚配置成从eMMC启动,BootROM就会初始化eMMC控制器,读取eMMC的特定扇区(比如扇区0或扇区1),把数据加载到SRAM或DDR中,然后跳转过去执行。

经验之谈:调试阶段,我建议把UART下载模式也加上。万一Flash里的代码写坏了,还能通过串口重新烧录。不然就得拆芯片用编程器,那叫一个麻烦。

好了,BootROM的第一阶段初始化就这些内容。说白了,它就是给芯片搭一个最小的运行环境:让CPU跑起来,让内存能用,让UART能打印,然后找到启动设备。下一阶段就是加载并验证下一级启动代码了,那个我们后面再聊。