2. 硬件初始化

说实话,很多做系统开发的工程师,对CPU上电那一刻到底发生了什么,往往是一知半解。大家更关心的是U-Boot怎么跑起来,Linux内核怎么解压。但我要说,真正决定系统能否稳定启动的,恰恰是上电后那几百微秒的硬件初始化过程。

我自己刚入行那会儿,也踩过这个坑。有一次调试一块新板子,死活起不来,JTAG连不上,示波器量电源轨也没问题。折腾了两天,最后发现是SoC内部的POR(Power-On Reset)时序不满足要求,ROM Code根本没开始执行。嗯,从那以后,我对上电流程就格外上心了。

2.1 上电复位:一切从POR开始

你按下电源键,或者板子刚插上电,第一个动作是什么?

不是CPU开始跑代码,而是电源管理单元(PMU)开始工作。PMU会检测各个电源域是否达到稳定电压。ARM64 SoC内部通常有多个电源域:

  • VDD_CORE:CPU核心供电,一般0.8V~1.2V
  • VDD_IO:IO接口供电,1.8V或3.3V
  • VDD_MEM:内存控制器供电
  • VDD_PLL:锁相环模拟供电

这些电源域的上电顺序是有严格要求的。我见过一个项目,因为硬件工程师把VDD_CORE和VDD_IO的使能信号接反了,导致每次上电CPU都进入一种奇怪的锁死状态。所以,上电时序不是软件能解决的,必须在硬件设计阶段就确认好。

关键点:POR信号释放后,CPU内部的复位逻辑才开始工作。此时所有寄存器回到默认值,Cache和TLB被清空,MMU处于关闭状态。

2.2 复位向量:CPU的第一口“奶”

当POR释放,时钟稳定后,CPU核心会从复位向量(Reset Vector)开始取指。ARM64架构下,复位向量地址通常是固定的:

  • 对于大多数Cortex-A系列,复位向量在0x00000000或0xFFFF0000
  • 但实际SoC设计中,这个地址会被映射到片内的ROM区域

为什么是ROM?因为此时DDR内存还没初始化,NAND Flash或eMMC也读不了。唯一能保证“上电就能用”的存储介质,就是SoC内部集成的Boot ROM。

我个人习惯把Boot ROM比作CPU的“出厂固件”。它体积很小,通常只有几十KB到几百KB,但功能极其关键。它负责:

  1. 最基本的CPU异常向量表设置
  2. 堆栈指针(SP)的初始化
  3. 跳转到ROM Code的主入口函数

这里有个细节要注意:ARM64在复位后默认处于EL3异常级别。这是最高权限级别,相当于整个系统的“上帝模式”。为什么?因为后续的ATF(ARM Trusted Firmware)需要在这个级别下完成安全初始化。

小技巧:如果你用JTAG调试器连接一个刚上电的ARM64芯片,读PC寄存器,看到的地址就是复位向量指向的ROM地址。这是验证上电流程是否正常的第一步。

2.3 ROM Code:SoC内部的“迷你引导程序”

ROM Code是固化在芯片内部的程序,用户无法修改。它的主要任务是:找到并加载下一级启动代码。

你可能会问:“ROM Code怎么知道从哪里加载?”

答案是:启动设备选择。SoC内部有一套硬件逻辑,根据芯片外部引脚的电平状态(Boot Mode Pins),决定从哪个设备读取启动代码。常见的启动设备包括:

启动模式 典型设备 特点
eMMC/SD卡 内置存储 速度快,容量大
NAND Flash 原始闪存 需要ECC校验
SPI NOR Flash 串行Flash 容量小,但启动简单
UART/USB 串口/USB下载 用于调试和工厂烧录

ROM Code会按照预设的优先级,依次尝试从这些设备读取数据。读取到的数据通常是Boot Image,它包含一个头部和实际的代码段。头部里记录了:

  • 镜像大小
  • 加载地址
  • 校验和或签名
  • 入口函数地址
注意:ROM Code在加载Boot Image时,通常会做简单的完整性检查。如果校验失败,它会尝试下一个启动设备。如果所有设备都失败,芯片会进入死循环或错误状态。这就是为什么有时候板子“变砖”了,连串口都没输出——ROM Code根本没找到可用的启动源。

2.4 时钟与PLL初始化:让CPU跑起来

ROM Code执行过程中,有一项关键任务:配置系统时钟。

刚上电时,CPU运行在一个非常低的频率上,通常是外部晶振的频率(比如24MHz或32.768KHz)。这个频率连看门狗都喂不饱,更别说跑复杂的代码了。ROM Code需要:

  1. 使能内部振荡器(OSC)
  2. 配置锁相环(PLL),将低频时钟倍频到目标频率
  3. 选择系统主时钟源(从晶振切换到PLL输出)
  4. 分配时钟给各个总线(AXI、AHB、APB)

我曾在调试一个网络处理器时,发现ROM Code阶段时钟配置有误,导致DDR控制器无法锁定PLL。结果就是,系统能启动到ROM Code,但一加载U-Boot就崩溃。最后查了芯片的勘误表,才发现是某个PLL的锁定时间不够,需要在ROM Code里加一个延时循环。

说白了,时钟树配置是SoC启动中最容易出问题的环节之一。因为不同芯片的PLL参数、分频系数、锁定时间都不一样,而ROM Code又是固定的,所以芯片原厂必须在流片前就把这些参数验证好。

2.5 安全启动与信任根

现代ARM64 SoC,尤其是面向服务器或汽车电子的芯片,都支持安全启动(Secure Boot)。ROM Code在这里扮演了信任根(Root of Trust)的角色。

流程是这样的:

  • ROM Code内部固化了芯片原厂的公钥哈希
  • Boot Image的头部包含数字签名
  • ROM Code用内置公钥验证签名的合法性
  • 验证通过,才允许执行Boot Image中的代码

如果签名验证失败,ROM Code会拒绝加载。这意味着,任何未经签名的第三方固件都无法在芯片上运行。这对防止恶意固件注入非常有效,但也给开发者带来了麻烦——你必须在开发阶段关闭安全启动,或者使用芯片厂商提供的签名工具。

经验之谈:我在做一款车规级芯片的启动方案时,安全启动的密钥管理是最头疼的部分。一旦私钥泄露,整个产品线的安全性就崩塌了。建议团队使用HSM(硬件安全模块)来存储和签名,不要图省事把私钥放在开发人员的电脑里。

2.6 从ROM Code到下一级:跳转前的最后准备

ROM Code完成所有初始化后,会做几件“善后”工作:

  • 关闭自身占用的中断和异常
  • 设置好堆栈指针(SP_EL3)
  • 将Boot Image从加载设备拷贝到SRAM或DDR中(如果DDR已初始化)
  • 跳转到Boot Image的入口地址

跳转时,CPU会保持EL3异常级别。这意味着,下一级代码(通常是ATF或U-Boot SPL)仍然拥有最高权限,可以继续配置系统资源。

这里有一个容易忽略的点:ROM Code在跳转前,会把一些关键信息传递给下一级代码。比如:

  • 启动设备类型
  • 时钟频率配置
  • DDR大小和时序参数
  • 芯片版本号

这些信息通常通过寄存器或共享内存传递。下一级代码必须按照芯片手册约定的格式来解析。我见过有人写U-Boot SPL时,忽略了ROM Code传递的DDR参数,自己重新初始化了一遍内存控制器,结果导致系统不稳定。其实,ROM Code已经帮你做好了最基础的硬件配置,没必要重复造轮子。

2.7 一张图看懂SoC内部启动流程

说了这么多,不如一张流程图来得直观。下面是我画的SoC内部启动流程,涵盖了从CPU上电到ROM Code执行完毕的全过程:

SoC内部启动流程(CPU上电 → ROM Code执行) 1. 上电 & POR释放 2. CPU取复位向量 3. ROM Code开始执行 ROM Code 核心任务 时钟 & PLL初始化 启动设备检测 安全启动验证 加载Boot Image 传递启动参数 4. 跳转到下一级代码 输出:ATF / U-Boot SPL

这张图把整个流程串起来了。你可以看到,ROM Code阶段其实做了大量工作,远不止“读个Flash”那么简单。时钟、安全、设备检测,每一步都关系到后续能否正常启动。

好了,关于SoC内部启动流程,我就讲到这里。记住一句话:上电后的几百微秒,决定了系统未来几小时甚至几年的稳定性。下次调试启动问题,不妨先从ROM Code入手,看看它有没有正确执行。

个人建议:如果你在开发新平台,第一步不是写U-Boot,而是先确认ROM Code能正常跳转。用JTAG单步跟踪,看PC指针是否走到了你期望的地址。这一步走通了,后面的事情就顺了。