3、芯片启动流程:服务器芯片上电复位流程,Boot ROM与初始引导,多级引导加载程序(BL1/BL2/BL3)架构

各位同学,今天我们来聊聊服务器芯片上电后,到底是怎么「活过来」的。

很多人觉得芯片上电就是按一下电源键,然后操作系统就起来了。其实没那么简单。从按下电源到操作系统跑起来,中间有一段非常关键、也非常容易被忽视的过程——芯片的启动流程。我当年刚入行时,就吃过这个亏,以为Boot ROM就是个简单的加载器,结果调试了整整两周才发现是启动顺序出了问题。

好,我们正式开始。

3.1 上电复位:芯片的「第一口呼吸」

芯片上电后,第一件事是什么?不是执行代码,而是复位。

复位信号(Reset)会强制芯片内部所有寄存器回到一个已知的初始状态。这个状态包括:

  • 程序计数器(PC)指向一个固定的复位向量地址
  • 大部分外设模块被禁用
  • 缓存(Cache)和MMU默认关闭
  • 中断被屏蔽

为什么要这么做?说白了,就是确保芯片从一个「干净」的状态开始跑。你想想看,如果上电时寄存器里是随机值,那CPU第一条指令都不知道该去拿哪里的数据,整个系统就乱套了。

关键点:复位向量地址通常是芯片厂商硬编码的。比如ARM架构的芯片,复位后PC会指向0x00000000或0xFFFF0000。x86架构则指向BIOS所在的地址空间。这个地址,就是Boot ROM的入口。

我在项目中遇到过一种情况:某款芯片的复位信号时序不满足要求,导致部分寄存器没有正确复位。结果芯片偶尔能启动,偶尔不能。查了三天,最后用示波器抓到复位信号有毛刺。嗯,硬件上的小问题,软件上折腾半天也找不到原因。

3.2 Boot ROM:芯片的「出厂设置」

复位完成后,CPU开始执行第一条指令。这条指令就在Boot ROM里。

Boot ROM是芯片内部的一块只读存储器,出厂时就已经固化好了。它有几个特点:

  • 不可修改——芯片流片后就定死了
  • 容量极小——通常只有几十KB到几百KB
  • 执行优先级最高——复位后第一个被执行的代码

Boot ROM的主要任务是什么?说白了就两件事:

  1. 初始化最基本的硬件——比如时钟、电源管理、内存控制器(至少初始化一部分)
  2. 加载下一级引导程序——从Flash、eMMC、UART、USB等介质中读取BL1

个人建议:Boot ROM的代码虽然小,但它是整个启动链的基石。我建议你在设计芯片时,一定要给Boot ROM留足够的「容错机制」。比如,如果从主Flash加载失败,能不能自动切换到备用Flash?能不能通过串口手动下载?这些看似不起眼的功能,在量产调试时能救你一命。

我曾经遇到过一个案例:某款服务器芯片的Boot ROM只支持从SPI Flash加载,结果Flash焊接不良,整批板子都起不来。后来我们不得不在Boot ROM里加了一个回退机制——如果SPI加载失败,就尝试从UART接收数据。这个改动虽然小,但让产线的良率从70%直接提升到了99%。

3.3 多级引导加载程序:BL1/BL2/BL3架构

Boot ROM执行完后,接下来就是多级引导加载程序了。为什么需要多级?直接一步到位加载操作系统不行吗?

原因很简单:Boot ROM太小了,放不下完整的引导逻辑。而且,Boot ROM是只读的,如果发现bug也没法修。所以,业界普遍采用多级引导架构。

最常见的架构是三级:BL1、BL2、BL3。我们一个一个来看。

3.3.1 BL1:一级引导加载程序

BL1是Boot ROM加载的第一个程序。它通常存储在Flash的固定位置,比如偏移0x10000处。

BL1的任务包括:

  • 初始化DRAM控制器(让内存可用)
  • 设置基本的异常向量表
  • 验证BL2的签名(安全启动)
  • 从Flash中加载BL2到DRAM
  • 跳转到BL2的入口

注意:BL1通常由芯片厂商提供,用户一般不需要修改。但如果你在做定制芯片,BL1的代码就需要你自己写了。我记得第一次写BL1时,最头疼的就是DRAM初始化——时序参数稍微不对,内存就报错,而且没有任何调试输出,全靠逻辑分析仪一点一点抓。

3.3.2 BL2:二级引导加载程序

BL2比BL1大得多,功能也更丰富。它通常负责:

  • 初始化更多的硬件(比如PCIe、网络控制器、存储控制器)
  • 加载并验证BL3(或者直接加载操作系统内核)
  • 提供一些调试接口(比如串口命令行)
  • 支持从不同介质加载(比如从NVMe、SATA、网络启动)

BL2的典型代表就是U-Boot的SPL(Secondary Program Loader)部分。很多服务器芯片都用U-Boot作为BL2。

这里有个常见的坑:BL2的大小限制。因为BL2通常还是从Flash加载到SRAM或DRAM中,而SRAM的大小有限(比如256KB),所以BL2不能太大。我曾经见过一个团队,把U-Boot的所有功能都编译进了BL2,结果镜像超过了SRAM大小,启动时直接死机。嗯,后来他们不得不把一部分功能移到BL3里。

3.3.3 BL3:三级引导加载程序

BL3是完整的引导加载程序,比如完整的U-Boot、GRUB、或者UEFI固件。

BL3的任务包括:

  • 加载操作系统内核(Linux、Windows等)
  • 设置内核启动参数(设备树、命令行参数)
  • 提供用户交互界面(比如启动菜单)
  • 支持高级功能(比如安全启动、固件更新)

BL3通常运行在DRAM中,所以没有大小限制。它可以从各种介质加载内核:本地磁盘、网络(PXE)、USB等。

避坑指南:我曾经在调试BL3时,发现内核总是启动到一半就崩溃。查了很久,最后发现是BL3在加载内核时,没有正确设置设备树中的内存映射。说白了,就是告诉内核「内存在这里」,但实际内存地址不对。这个问题在单板调试时很难发现,因为单板的内存映射通常是固定的。但到了量产阶段,不同批次的内存颗粒可能映射地址不同,就出问题了。

3.4 启动流程总结

好,我们把整个启动流程串起来看一下:

阶段 存储介质 主要任务 典型大小
上电复位 硬件逻辑 寄存器复位,PC指向复位向量
Boot ROM 芯片内部ROM 初始化时钟、加载BL1 64-256KB
BL1 Flash 初始化DRAM、加载BL2 32-128KB
BL2 Flash → DRAM 初始化外设、加载BL3 128-512KB
BL3 DRAM 加载操作系统内核 1-8MB
操作系统 DRAM 接管系统

你可能会问:为什么不能把BL1、BL2、BL3合并成一个?

原因有三:

  1. 安全考虑——每一级都可以对下一级进行签名验证,防止恶意固件
  2. 灵活性——不同芯片、不同板卡可能只需要修改某一级,其他级可以复用
  3. 调试方便——如果启动失败,可以快速定位到是哪一级出了问题

警告:多级引导虽然灵活,但也带来了复杂度。每一级之间的接口(比如参数传递、内存布局)必须严格定义。我见过一个项目,BL2和BL3之间约定好使用某个内存地址传递设备树指针,结果BL3升级后改了内存布局,导致指针指向了无效地址。这种问题非常隐蔽,而且只有在特定条件下才会触发。

3.5 实际项目中的经验

最后,分享几个我在实际项目中积累的经验:

  • 调试输出要尽早——Boot ROM阶段可能连串口都没初始化,但至少可以用GPIO点灯来表示状态。我习惯在每一级引导的开始和结束都点一下灯,这样通过观察灯的闪烁次数,就能快速判断启动到了哪一步。
  • 预留回退机制——永远假设主启动路径会失败。Boot ROM应该支持从多个介质加载BL1,BL1也应该支持从多个位置加载BL2。这个「冗余设计」在量产时非常有用。
  • 版本管理要严格——每一级引导程序都有自己的版本号。我建议在启动时打印出所有级别的版本号,这样调试时一眼就能看出固件是否匹配。
  • 安全启动不是可选项——服务器芯片一定要做安全启动。从Boot ROM开始,每一级都要验证下一级的签名。否则,一旦固件被篡改,整个系统就暴露在风险中。

好了,关于芯片启动流程的内容就讲到这里。下一章我们会深入讨论Boot ROM的具体实现细节,包括如何设计一个既安全又灵活的Boot ROM。到时候我会拿一个实际项目的代码来讲解,敬请期待。