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主流程后,第一件事就是初始化硬件。这个阶段的目标是让芯片具备最基本的运行能力。我把它归纳为以下几个步骤:
- 时钟系统初始化:配置PLL,给CPU、总线、外设提供稳定的时钟。
- 内存控制器初始化:如果芯片有片内SRAM,直接可用;如果需要外部DDR,得先配好DDR控制器。
- 关键外设初始化:比如UART(用于打印调试信息)、定时器(用于超时控制)。
- 启动设备检测:判断从哪个设备加载下一级代码(NAND Flash、eMMC、SD卡、SPI NOR等)。
下面这张图展示了BootROM第一阶段初始化的典型流程:
你看,这个流程是串行的,每一步都依赖上一步。时钟没配好,内存控制器就没办法工作;内存没初始化,代码就没地方放。所以顺序不能乱。
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能打印,然后找到启动设备。下一阶段就是加载并验证下一级启动代码了,那个我们后面再聊。