4. FSBL与BootROM:启动流程详解、FSBL源码分析、第一级引导加载程序配置

Zynq的启动过程,说白了就是芯片从「断电」到「跑起Linux」的完整链条。很多初学者一上来就对着Petalinux敲命令,结果板子起不来,卡在某个阶段半天找不到原因。我当年也踩过这个坑——后来老老实实把BootROM和FSBL啃了一遍,才明白问题出在哪。

这一章,我们就来拆解这个启动链条。我会带着你从BootROM开始,一路走到FSBL,把每一级引导程序到底干了什么、怎么配置、源码里哪些是关键路径,都捋清楚。

核心要点: Zynq启动分为三个阶段——BootROM(不可修改)、FSBL(用户可配置)、SSBL/ATF(第二级引导)。理解这三者的分工,是调试启动问题的基本功。

4.1 启动流程全景图

先看一张整体流程图。我习惯把Zynq的启动想象成「接力赛」:BootROM是第一棒,FSBL是第二棒,U-Boot或ATF是第三棒。每一棒都有固定的交接规则。

Zynq 启动流程全景图 上电复位 PS_POR_B 释放 BootROM 不可修改,片上ROM FSBL 第一级引导 硬件自动跳转 加载FSBL到OCM BootROM 内部 读取Boot Mode引脚 初始化NAND/QSPI/SD FSBL 内部 初始化PS基本外设 加载Bitstream/ATF SSBL / ATF U-Boot / ARM Trusted 加载Linux内核 关键说明 • BootROM:固化在芯片内部,用户无法修改。负责读取启动模式引脚,从外部存储加载FSBL到OCM。 • FSBL:用户可配置,由Xilinx SDK/Vitis生成。负责初始化DDR、加载PL Bitstream、加载ATF/U-Boot。 • SSBL/ATF:第二级引导,通常为U-Boot或ARM Trusted Firmware,负责加载Linux内核和设备树。 • 整个流程从复位到Linux启动,通常在2-5秒内完成(取决于存储介质和Bitstream大小)。

4.2 BootROM:芯片的第一口「奶」

BootROM是Zynq上电后第一个执行的代码。它固化在芯片内部的ROM里,你改不了它,也删不掉它。它的任务很简单:把FSBL从外部存储加载到OCM(On-Chip Memory)里,然后跳过去执行。

我记得第一次调试启动问题时,板子死活起不来,串口什么输出都没有。后来用示波器量了Boot Mode引脚的电平,才发现是拨码开关接触不良,BootROM读到了错误的启动模式——它以为要从NAND启动,但板子上根本没焊NAND。嗯,这种低级错误,犯过一次就记住了。

4.2.1 BootROM的工作流程

  1. 上电复位:PS_POR_B引脚释放后,BootROM开始执行。
  2. 读取Boot Mode引脚:根据MIO[6:2]的电平,决定从哪种介质加载FSBL。
  3. 初始化存储控制器:比如QSPI控制器、SD控制器、NAND控制器等。
  4. 搜索Boot Image:在存储介质的固定偏移位置查找合法的Boot Image头。
  5. 加载到OCM:将FSBL代码从存储介质拷贝到OCM(地址0xFFFF0000)。
  6. 跳转执行:设置CPU的PC指针到OCM中的FSBL入口地址。
小技巧: 如果你想知道当前板子是从哪种介质启动的,可以在FSBL里读取寄存器 slcr.BOOT_MODE。这个寄存器在BootROM阶段就已经被写入了。我在调试时经常用这个来确认硬件配置是否正确。

4.2.2 Boot Image的格式

BootROM能识别的Boot Image有固定的格式。说白了,就是一段头部信息加上实际的FSBL二进制。头部里包含了校验和、长度、目标地址等关键信息。

偏移 字段 说明
0x00 Image Header Table 分区表,描述每个分区的偏移和长度
0x20 Boot Header 包含宽度检测、加密标志、目标CPU等
0x40 Partition Header #0 FSBL分区的描述信息
... FSBL Binary 实际的FSBL可执行代码

这里有个坑:BootROM要求Boot Image必须存储在介质的固定偏移位置。比如QSPI Flash是偏移0x0,SD卡是偏移0x0(但需要FAT32文件系统)。如果你把镜像烧错了位置,BootROM找不到合法的头部,就会卡死在BootROM里——串口没有任何输出。

4.3 FSBL:第一级引导加载程序

FSBL(First Stage Boot Loader)是用户可配置的第一级引导程序。它由Xilinx SDK或Vitis生成,源码在Xilinx的GitHub上可以找到。我个人习惯把FSBL的源码下载下来,直接在里面加调试打印——这样能清楚地看到每一行代码的执行情况。

4.3.1 FSBL的主要职责

  • 初始化PS基本外设:包括DDR控制器、UART、时钟、PLL等。
  • 加载PL Bitstream:如果硬件设计中有FPGA逻辑,FSBL负责把Bitstream从存储介质加载到PL。
  • 加载第二级引导程序:比如U-Boot或ARM Trusted Firmware(ATF)。
  • 跳转到下一级:设置好CPU状态,跳转到SSBL的入口地址。
注意: FSBL运行在OCM中,OCM只有256KB(Zynq-7000系列)。所以FSBL本身不能太大,否则放不下。如果你的FSBL编译出来超过200KB,就要检查是不是加了太多不必要的驱动。

4.3.2 FSBL源码关键路径分析

FSBL的源码主要位于 fsbl/src/ 目录下。核心文件就几个,我带你过一遍最关键的。

1. main.c —— 入口函数

int main(void)
{
    /*
     * 第一步:初始化PCAP控制器
     * PCAP是PS访问PL的接口
     */
    PcapInit();

    /*
     * 第二步:加载并解析Boot Image
     * 从存储介质中读取分区表
     */
    FsblBootInit();

    /*
     * 第三步:处理每个分区
     * 根据分区类型执行不同操作
     */
    FsblHandoff();

    return 0;
}

这个流程看着简单,但实际执行时有很多细节。比如 FsblBootInit() 里会调用 FfsBootInit(),这个函数会遍历所有分区,找到类型为 IMAGE_TYPE_BOOT_IMAGE 的分区。

2. fsbl_handoff.S —— 跳转汇编

这是FSBL最后执行的代码,负责跳转到下一级引导程序。我当初看这段汇编时,最困惑的是为什么要清空一些寄存器——后来才明白,这是为了给下一级程序一个干净的运行环境。

_fsbl_handoff:
    /* 清空通用寄存器 */
    mov r0, #0
    mov r1, #0
    /* ... 省略中间部分 ... */

    /* 设置下一级入口地址 */
    ldr r0, =NextStageAddress
    ldr r1, [r0]

    /* 跳转 */
    bx r1
调试技巧: 如果你想在FSBL里加打印,可以直接调用 fsbl_printf() 函数。这个函数通过UART输出,前提是你已经在硬件设计里使能了UART。我曾经在 FsblBootInit() 前后各加一句打印,用来定位是加载阶段还是跳转阶段出了问题。

4.3.3 FSBL的配置选项

FSBL的行为可以通过Xilinx SDK/Vitis中的BSP设置来配置。常用的配置项如下:

配置项 可选值 说明
FSBL_LOAD_BITSTREAM TRUE / FALSE 是否加载PL Bitstream
FSBL_WDT_ENABLE TRUE / FALSE 是否使能看门狗定时器
FSBL_PERF_ENABLE TRUE / FALSE 是否使能性能测量(打印各阶段耗时)
FSBL_DEBUG_PRINT TRUE / FALSE 是否使能调试打印

我建议在开发阶段把 FSBL_DEBUG_PRINT 和 FSBL_PERF_ENABLE 都打开。这样你能看到FSBL每一步的耗时,比如加载Bitstream花了多少毫秒,初始化DDR花了多少。这些数据对优化启动速度很有帮助。

4.4 常见启动问题与排查方法

做Zynq项目,启动问题是最常见的。我整理了几个典型场景,你遇到了可以直接对照排查。

场景一:串口无任何输出
可能原因:BootROM阶段就卡住了。
排查步骤:
1. 检查Boot Mode引脚电平是否正确。
2. 检查存储介质中Boot Image的偏移位置是否正确。
3. 用示波器测量QSPI/SD的时钟信号,确认BootROM是否在读取。
场景二:FSBL打印到一半卡死
可能原因:DDR初始化失败,或者Bitstream加载出错。
排查步骤:
1. 检查DDR型号和硬件设计中的参数是否匹配。
2. 检查Bitstream文件是否损坏,重新生成并烧录。
3. 在FSBL源码的 DdrInit() 前后加打印,定位具体卡在哪一步。
场景三:FSBL正常执行,但跳转到U-Boot后无输出
可能原因:FSBL加载U-Boot的地址不对,或者U-Boot本身有问题。
排查步骤:
1. 确认U-Boot的加载地址是否与FSBL中配置的一致。
2. 检查U-Boot的编译选项,确保是针对Zynq平台编译的。
3. 用 bootgen 工具重新生成Boot Image,确保分区顺序正确。

4.5 实战:手动构建一个最小FSBL工程

说了这么多理论,我们来点实际的。下面是一个最小FSBL工程的构建步骤,你可以跟着操作一遍。

  1. 打开Vitis,创建一个新的应用工程。
  2. 选择模板:在模板选择界面,选择「Zynq FSBL」。
  3. 配置BSP:在BSP设置中,打开 FSBL_DEBUG_PRINT 和 FSBL_PERF_ENABLE。
  4. 编译:直接编译,生成 fsbl.elf。
  5. 生成Boot Image:使用 bootgen 工具,将 fsbl.elf、bitstream.bit、u-boot.elf 打包成一个 BOOT.BIN。
  6. 烧录:将 BOOT.BIN 烧录到SD卡或QSPI Flash中。
  7. 上电测试:连接串口,观察输出。

如果你看到类似下面的输出,说明FSBL跑起来了:

Xilinx Zynq FSBL
FSBL: Stage 1: Loading Bitstream
FSBL: Bitstream loaded successfully
FSBL: Stage 2: Loading U-Boot at 0x04000000
FSBL: Jumping to U-Boot at 0x04000000
我的习惯: 在FSBL的 FsblHandoff() 函数里,我会加一个延时循环——等待3秒,如果检测到某个GPIO引脚被拉低,就进入一个「调试模式」,打印更多信息。这样在生产环境下可以关闭调试打印,遇到问题时又能通过硬件触发进入调试模式,非常实用。

好了,这一章的内容就到这里。BootROM和FSBL是Zynq启动的基石,理解了它们,你就能看懂启动过程中每一步到底在干什么。下次板子起不来的时候,别急着怀疑硬件——先看看BootROM有没有读到正确的镜像,FSBL有没有正确初始化DDR。很多时候,问题就出在这些「理所当然」的环节里。