← Back to Blog
FPGAZynqXilinxFSBLU-Boot嵌入式Linux启动流程BOOT.BIN

Zynq 实战 09|FSBL 与 U-Boot 启动流程详解:从上电到 Linux 登录提示符的每一步

This article was written in Chinese and auto-translated via Google Translate.
View Chinese Original →

Zynq 实战 09|FSBL 与 U-Boot 启动流程详解

这是《Zynq FPGA 嵌入式系统设计实战》系列的第 9 篇。 板子:Pynq-Z2(XC7Z020)。工具链:Vivado / Vitis 2023.2。 上一篇:《Zynq 实战 08|PetaLinux 构建与根文件系统定制》


0. 这一篇要解决什么问题

每次板子上电,你按下 RESET 键到 Linux shell 出现——中间发生了什么?这件事很多人知道”有 FSBL”、“有 U-Boot”,但真正要自己改 bif 文件、改 bootargs、改 U-Boot 环境变量的时候,就会卡住。

本文要把这个链路拆到**“每一步谁在跑、在内存的哪里、做了什么、在哪个文件里改”**的颗粒度。具体地:

  • BootROM 怎么读 boot mode pins(MIO[6:2]),读出来决定从哪里加载 FSBL
  • FSBL 执行期间到底初始化了什么,fsbl_hooks.c 里的 hook 函数在哪个时间点被调用
  • image.bif 怎么写——特别是 Zynq-7000 用 a9-0,不是 a53-0(这个坑我摔过)
  • U-Boot 配置入口在哪、怎么改 bootargs、怎么把环境变量持久化到 QSPI
  • DTB 什么时候、被谁、加载到内存的哪个地址

本文不涉及 PetaLinux 完整构建流程(那是第 08 篇的事),也不讲 secure boot / RSA 验证(那是另一个话题)。


1. 四阶段启动链路——先有整体画面

上电到 Linux 一共走四个阶段,每个阶段有明确的职责边界:

Zynq-7000 四阶段启动时间轴 Stage 0 BootROM 固化于芯片内部 读 MIO[6:2] 加载 FSBL→OCM FSBL.elf →OCM Stage 1 FSBL 跑在 OCM DDR/PLL/MIO 初始化 PCAP 加载 bitstream U-Boot →DDR Stage 2 U-Boot 跑在 DDR 加载 zImage + DTB 设置 bootargs → bootz 跳转 内核入口 Stage 3 Linux Kernel 解压 zImage 解析 DTB、初始化驱动 挂载 rootfs → /sbin/init 运行位置 ROM(只读) 无需外部介质 OCM 0x0000_0000 ≤ 192 KB DDR 0x0400_0000 由 FSBL 加载 DDR 0x0008_0000 (zImage 典型加载地址) 从哪启动 MIO[6:2] boot mode SD=00101 QSPI=00001 BOOT.BIN 由 BootROM 解析 header BOOT.BIN partition 或 SD / TFTP bootargs root= 参数 决定 rootfs 从哪挂载 上电 / RESET login: ← 典型总耗时 15~30 秒 →
图 1. Zynq-7000 四阶段启动时间轴(UG585 Chapter 6)

2. Stage 0:BootROM——你没法改,但必须理解它

BootROM 是固化在 Zynq 芯片内部的只读代码(UG585 第 6.1 节),不能更新也不能替换。上电复位后,Cortex-A9 core0 从 BootROM 地址开始执行(这段逻辑你在 JTAG 连上去 halt 也看不到源码)。

BootROM 干了什么(顺序执行):

  1. 读取 Boot Mode 寄存器SLCR.BOOT_MODE 地址 0xF800_0240)。这个寄存器在复位时采样 MIO[6:2] 的电平,决定从哪个介质加载 FSBL:
MIO[6:2]启动源Pynq-Z2 用法
00000JTAG调试、直接下载
00001Quad-SPI(单片,24 位地址)量产固件
00010Quad-SPI(双片叠加,32 位地址)大容量 QSPI
00101SD Card 0开发阶段常用
01011SD Card 1 / eMMCPynq-Z2 无此接口

Pynq-Z2 上有两个 Boot Mode 拨码开关(JP4/JP5),SD 模式就是 00101

  1. 初始化启动介质(PLL、外设时钟在这里做最基础设置——只够让 QSPI/SD 控制器工作,DDR 不在这里初始化)。

  2. 读取 BOOT.BIN 的 Boot Header,验证 magic word,找到 FSBL 的偏移和大小。

  3. 把 FSBL 拷贝到 OCM 低地址(0x0000_0000),然后跳转过去执行。

🚧 避坑:BootROM 把 FSBL 加载到 OCM,而 OCM 的低地址段只有 192 KB(256KB 总量中,最高 64KB 被 BootROM 本身占用)。如果你的 FSBL.elf 超过 192KB,BootROM 不会报错,但 FSBL 会执行到损坏区域直接挂死。Vitis 默认的 Zynq FSBL 模板大约 60-80KB,加上 printf 调试输出也很难超,但如果你往 FSBL 里加大量初始化代码要注意。


3. Stage 1:FSBL——在 OCM 里完成整个 PS 的”开机自检”

FSBL(First Stage Bootloader)在 OCM 里跑,此时 DDR 还不可用——这是一个很重要的约束,意味着 FSBL 代码里不能用大数组,所有数据必须放在栈/OCM 里。

FSBL 的执行顺序(对应 Vitis 2023.2 生成的 src/main.c):

1. XFsbl_Initialize()
   ├── 初始化 UART(115200 波特率,你看到的第一行 "Xilinx FSBL...")
   ├── 初始化 PLL 和时钟树(ARM PLL, IO PLL, DDR PLL)
   ├── 初始化 DDR 控制器(PS7_DDR block,参数来自 XSA/HDF)
   └── 初始化 MIO 引脚复用

2. XFsbl_BootDeviceInitAndValidate()
   ├── 读取 Boot Header(已在 OCM 里,BootROM 一起拷过来了)
   └── 验证分区表

3. [hook] FsblHookBeforeBitstreamDload()  ← 你的第一个插入点

4. XFsbl_LoadPartitions()
   ├── 如果有 bitstream 分区:通过 PCAP 加载到 PL
   └── 加载 U-Boot/裸机 ELF 到 DDR

5. [hook] FsblHookAfterBitstreamDload()  ← PL 已经配置好,可以用 PL 外设了

6. [hook] FsblHookBeforeHandoff()         ← 即将跳转前的最后机会

7. XFsbl_HandoffExit():跳转到 U-Boot 入口

3.1 PCAP 加载 bitstream

PCAP(Processor Configuration Access Port)是 PS 专门用来配置 PL 的接口,不是 JTAG。FSBL 调用 XFsbl_WriteToPcap() 把 bitstream 数据通过 DMA 搬到 PCAP,配置完成后 PCAP_STATUS 寄存器的 PCFG_INIT 位会置 1(地址 0xF8007014,UG585 Table B.65)。

有一点值得注意:PCAP 的配置时钟默认 100MHz,一块 7Z020 的 bitstream 大约 4MB,配置耗时约 40-50ms。如果你觉得启动慢,这段时间是一个固定开销,减不了多少。

3.2 在 Vitis 里创建和定制 FSBL

File → New → Application Project
  → Platform: 选择你的 XSA(来自 Vivado Export Hardware)
  → Language: C
  → Template: Zynq FSBL

生成后关键文件:

src/
├── main.c              # 主流程,一般不用动
├── fsbl_hooks.c        # ← 定制的唯一正确入口
├── fsbl_hooks.h
├── pcap.c              # PCAP 驱动,了解即可
└── qspi.c / sd.c       # 各启动源的读取驱动

fsbl_hooks.c 里加调试输出(这是我在做硬件早期 debug 时常加的):

// src/fsbl_hooks.c

u32 FsblHookBeforeBitstreamDload(void)
{
    /* PL 配置前:初始化你在 PL 配置完之前需要的外设 */
    fsbl_printf(DEBUG_GENERAL, "FSBL: Before bitstream download\r\n");
    return XST_SUCCESS;
}

u32 FsblHookAfterBitstreamDload(void)
{
    /* PL 已配置:此时 PL 侧的 AXI 外设可以访问了 */
    fsbl_printf(DEBUG_GENERAL, "FSBL: PL configured OK\r\n");

    /* 例:通过 GP0 总线检查 PL 内的版本寄存器 */
    u32 ver = Xil_In32(0x43C00000);
    fsbl_printf(DEBUG_GENERAL, "FSBL: PL IP version = 0x%08X\r\n", ver);

    return XST_SUCCESS;
}

u32 FsblHookBeforeHandoff(void)
{
    /* 即将跳转到 U-Boot,做收尾 */
    fsbl_printf(DEBUG_GENERAL, "FSBL: Handing off to U-Boot\r\n");
    return XST_SUCCESS;
}

要启用 fsbl_printf,编译时需要在 BSP 里开启 FSBL_DEBUG_INFO(Build Settings → C/C++ → Preprocessor → 加 FSBL_DEBUG_INFO)。

🚧 避坑fsbl_printf 不是 printf,它受 FSBL_DEBUG 宏控制。Release 模式下这些输出会被完全编译掉。如果你加了调试输出但看不到,检查编译器宏设置。另外,不要在 FSBL 里用 C 标准库的 malloc——heap 在 OCM 很有限,一旦溢出会静默损坏数据。


4. BOOT.BIN 格式与 bif 文件——别抄错 CPU 名称

BOOT.BIN 是 Xilinx 专有格式的启动镜像,由 bootgen 工具根据 .bif 文件打包生成。结构大致是:

BOOT.BIN
├── Boot Header(512 字节,BootROM 读取)
│   ├── Magic Word: 0xAA995566
│   ├── FSBL 偏移、大小、加载地址
│   └── 分区表指针
├── Partition 0: FSBL.elf(加载到 OCM)
├── Partition 1: system.bit(PL bitstream,可选)
└── Partition 2: u-boot.elf(加载到 DDR)

4.1 完整 image.bif 示例(Zynq-7000)

/* image.bif for Zynq-7000 (XC7Z020) */
/* 注意:destination_cpu=a9-0 是 Cortex-A9,不是 a53-0! */
/* a53-0 是 ZynqMP (Zynq UltraScale+) 的 Cortex-A53        */

the_ROM_image:
{
    /* FSBL:bootloader 属性告诉 BootROM 这是要加载到 OCM 的第一阶段引导 */
    [bootloader, destination_cpu=a9-0] FSBL.elf

    /* PL bitstream:可选,没有 PL 逻辑时可以删掉这行 */
    [destination_device=pl] system.bit

    /* U-Boot:普通 ELF 分区,FSBL 会把它加载到 ELF 头指定的地址 */
    /* u-boot.elf 的链接地址通常是 0x0400_0000 */
    [destination_cpu=a9-0] u-boot.elf
}

打包命令(在 Vitis Shell 或 Linux 终端里):

bootgen -image image.bif -arch zynq -o BOOT.BIN -w on

-w on 是覆盖写入,-arch zynq 指定 Zynq-7000(注意不是 zynqmp)。

🚧 避坑(重要!):网上有大量教程用的是 destination_cpu=a53-0。这个属性是 Zynq UltraScale+(ZynqMP,比如 ZU3EG) 的写法,那颗芯片是 Cortex-A53。Zynq-7000(7Z010/7Z020/7Z030/7Z045)是 Cortex-A9,对应属性是 a9-0。如果你在 7Z020 上用了 a53-0,bootgen 可能不报错(取决于版本),生成的 BOOT.BIN 格式会错乱,BootROM 解析 Boot Header 失败,板子会挂在上电后串口完全没输出的状态——很难排查。


5. Stage 2:U-Boot——配置入口与启动命令实战

U-Boot 跑在 DDR 里,有完整的 shell,可以交互。它的核心任务:

  1. 初始化剩余外设(以太网、USB 等)
  2. 从存储介质(SD / QSPI / TFTP)加载 zImage(Linux 内核)和 devicetree.dtb(DTB)
  3. 设置 bootargs 内核参数
  4. 调用 bootz(或 bootm)跳入内核

5.1 U-Boot 源码配置入口

Xilinx 维护了一个 U-Boot 分支(u-boot-xlnx),Zynq-7000 的配置文件:

configs/
├── xilinx_zynq_virt_defconfig   # ← 通用虚拟板配置,Pynq-Z2 的起点
├── zynq_zed_defconfig            # ZedBoard 专用
└── zynq_zybo_defconfig           # Zybo 专用

板级特定配置在:

include/configs/zynq-common.h    # Zynq 通用配置:内存地址、启动命令、环境变量

关键的内存地址定义(直接影响你手动输命令时用什么地址):

/* include/configs/zynq-common.h(截取关键部分) */

#define CONFIG_SYS_LOAD_ADDR    0x02000000   /* 默认 load 地址 */
#define KERNEL_LOAD_ADDRESS     0x02080000   /* zImage 典型加载地址 */
#define FDT_LOAD_ADDRESS        0x02000000   /* DTB 加载地址(覆盖 load addr 前注意) */

/* 一个更实用的分配方案(避免覆盖) */
/* zImage  → 0x02080000 */
/* initrd  → 0x04000000(如果有的话) */
/* DTB     → 0x02000000                */

🚧 避坑CONFIG_SYS_LOAD_ADDRfatload 等命令的默认目标地址。如果你先 fatload 了 DTB,再 fatload 了 zImage,两次都用默认地址,第二次会覆盖第一次的数据。明确指定两个不同的目标地址,不要依赖默认值。

5.2 DTB 在哪、什么时候被加载

DTB(Device Tree Blob)不在 BOOT.BIN 里(除非你用 PetaLinux 的 image.ub 格式,那是 FIT image 另一回事)。常规 SD 卡启动时:

  • SD 卡 FAT 分区(partition 1)里放:BOOT.BINuImage/zImagedevicetree.dtb
  • U-Boot 在运行时用 fatload 命令把 DTB 从 SD 读到 DDR 的某个地址
  • 调用 bootz 时把 DTB 地址作为第三个参数传给内核

内核靠 DTB 知道”板子上有什么外设、地址在哪”——所以 DTB 和内核版本要匹配。

5.3 U-Boot console 实战操作序列

以下是从 Pynq-Z2 SD 卡启动的真实交互序列(U-Boot 2023.01,Zynq):

U-Boot 2023.01-dirty (Jan 01 2023 - 12:00:00 +0900) Xilinx Zynq

DRAM:  512 MiB
Core:  18 devices, 15 uclasses, devicetree: separate
Flash: 0 Bytes
NAND:  0 MiB
MMC:   mmc@e0100000: 0
Loading Environment from SPI Flash... SF: Detected s25fl128s with page size 512 Bytes, erase size 256 KiB, total 16 MiB
OK
In:    serial@e0001000
Out:   serial@e0001000
Err:   serial@e0001000
Net:   ZYNQ GEM: e000b000, mdio bus e000b000, phyaddr 0, interface rgmii-id

Hit any key to stop autoboot:  3  2  1  0

中断 autoboot 后进入 shell(按任意键):

# ① 扫描 SD 卡
Zynq> mmc rescan

# ② 列出 FAT 分区内容(确认文件在)
Zynq> fatls mmc 0:1
   4296832   BOOT.BIN
   4702592   uImage
    125483   devicetree.dtb

# ③ 加载 zImage 到 DDR(也可以用 uImage,命令换成 bootm)
Zynq> fatload mmc 0:1 0x02080000 uImage
4702592 bytes read in 367 ms (12.2 MiB/s)

# ④ 加载 DTB 到 DDR(地址不能和 uImage 重叠)
Zynq> fatload mmc 0:1 0x02000000 devicetree.dtb
125483 bytes read in 12 ms (10 MiB/s)

# ⑤ 设置 bootargs(console 参数必须是 ttyPS0,不是 ttyS0!)
Zynq> setenv bootargs "console=ttyPS0,115200 root=/dev/mmcblk0p2 rw rootwait earlyprintk"

# ⑥ 持久化环境变量到 QSPI(这样下次启动 bootargs 还在)
Zynq> saveenv
Saving Environment to SPI Flash... Erasing SPI flash...Writing to SPI flash...done

# ⑦ 启动内核(uImage 用 bootm,zImage 用 bootz)
# 格式:bootm/bootz <kernel_addr> <ramdisk_addr(- 表示无)> <dtb_addr>
Zynq> bootm 0x02080000 - 0x02000000

## Booting kernel from Legacy Image at 02080000 ...
   Image Name:   Linux-5.15.36-xilinx-v2022.2
   Image Type:   ARM Linux Kernel Image (uncompressed)
   Data Size:    4702528 Bytes = 4.5 MiB
   Load Address: 00008000
   Entry Point:  00008000
   Verifying Checksum ... OK
## Flattened Device Tree blob at 02000000
   Booting using the fdt blob at 0x2000000
   Loading Kernel Image
   Loading Device Tree to 1f7e3000, end 1f7ff8cb ... OK

Starting kernel ...

[    0.000000] Booting Linux on physical CPU 0x0
[    0.000000] Linux version 5.15.36-xilinx ...

5.4 bootargs 关键参数解释

console=ttyPS0,115200   # PS UART0,波特率 115200。注意是 ttyPS0(Zynq PS UART 驱动),
                        # 不是 ttyS0(PC 标准 8250 串口)。写错的话 Linux 起来后
                        # 串口完全没输出,很难排查。

root=/dev/mmcblk0p2     # rootfs 在 SD 卡第二个分区
                        # partition 1 = FAT(放 BOOT.BIN / 内核 / DTB)
                        # partition 2 = ext4(放 rootfs)

rw                      # 以读写方式挂载根文件系统

rootwait                # 等待存储设备就绪后再挂载,SD 卡必须加这个,
                        # 否则内核在 SD 控制器初始化完成前就尝试 mount 会失败

earlyprintk             # 在串口驱动正式初始化之前也输出内核日志,
                        # 有助于排查启动早期崩溃

5.5 自动启动脚本——让板子上电自动跑

# 在 U-Boot 里设置 bootcmd(板子上电后自动执行)
Zynq> setenv bootcmd "run sdboot"
Zynq> setenv sdboot "fatload mmc 0:1 0x02080000 uImage; fatload mmc 0:1 0x02000000 devicetree.dtb; bootm 0x02080000 - 0x02000000"
Zynq> saveenv

# 验证设置
Zynq> env print bootcmd
bootcmd=run sdboot
Zynq> env print sdboot
sdboot=fatload mmc 0:1 ...

🚧 避坑:Zynq U-Boot 里 saveenv 默认保存到 QSPI Flash(如果探测到的话)。Pynq-Z2 上有 16MB QSPI(S25FL128S),环境变量存储在 QSPI 的 0x1E0000 偏移处(由 CONFIG_ENV_OFFSET 决定)。如果 QSPI 里的环境变量损坏,U-Boot 会提示 Bad CRC 并加载默认环境,这时重新 setenv + saveenv 即可。


6. 本篇你应该带走的几个判断

  • BootROM 只负责”读 boot mode → 找 FSBL → 搬到 OCM → 跳过去”,DDR 不在它这里初始化
  • FSBL 最大 192 KB(OCM 低地址段限制),超了会在奇怪的地方挂死
  • image.bif 里写 destination_cpu=a9-0,绝对不是 a53-0(a53 是 ZynqMP 的 Cortex-A53)
  • PL bitstream 由 FSBL 通过 PCAP 加载,不需要也不应该放到 U-Boot 阶段去处理
  • console=ttyPS0 不是 ttyS0,这个写错串口会哑掉
  • fatload 时手动指定两个不同的 DDR 地址给 zImage 和 DTB,不要两次都用默认地址
  • DTB 由 U-Boot 加载,通过 bootz/bootm 的第三个参数传给内核
  • 定制 FSBL 只动 fsbl_hooks.c,不要改 main.c 里的主流程

7. 下一篇预告

下一篇 《Zynq 实战 10|设备树(DTS)实战:从 XSA 生成到手写自定义节点》,我们会:

  • PetaLinux 生成的 DTB 里哪些节点是 PS 自动生成的、哪些是 PL 定制的
  • 手写一个 AXI 自定义 IP 的 DTS 节点,让内核的 UIO / 平台驱动能识别它
  • compatible 属性的命名规则(Xilinx 自己的 vendor prefix 是什么)
  • 如何用 fdtdump 反编译 DTB 验证自己的改动有没有进去

参考资料

文档号名称用途
UG585Zynq-7000 SoC Technical Reference Manual启动流程主要参考:Chapter 6 Boot and Configuration(第 157-218 页),PCAP 细节在 Chapter 6.4,OCM 映射在 Chapter 4.1
UG1283Bootgen User Guidebif 文件语法、[bootloader] / destination_cpu 完整属性列表
UG1144PetaLinux Tools DocumentationPetaLinux 构建 BOOT.BIN 的完整流程
UG821Zynq-7000 SoC Software Developers GuideFSBL 源码结构说明、PCAP 编程接口
Xilinx WikiFSBL — Xilinx WikiFSBL hooks 使用说明、debug 技巧
u-boot-xlnxGitHub: Xilinx/u-boot-xlnxconfigs/xilinx_zynq_virt_defconfiginclude/configs/zynq-common.h

本系列持续更新,覆盖 Zynq-7000 从启动链路、Linux 移植到 PL 硬件加速的完整实战路径。 如果你在 FSBL 或 U-Boot 阶段踩了坑,欢迎留言交流——很多问题第一眼都很难定位。