Zynq 实战 09|FSBL 与 U-Boot 启动流程详解:从上电到 Linux 登录提示符的每一步
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 一共走四个阶段,每个阶段有明确的职责边界:
2. Stage 0:BootROM——你没法改,但必须理解它
BootROM 是固化在 Zynq 芯片内部的只读代码(UG585 第 6.1 节),不能更新也不能替换。上电复位后,Cortex-A9 core0 从 BootROM 地址开始执行(这段逻辑你在 JTAG 连上去 halt 也看不到源码)。
BootROM 干了什么(顺序执行):
- 读取 Boot Mode 寄存器(
SLCR.BOOT_MODE地址0xF800_0240)。这个寄存器在复位时采样 MIO[6:2] 的电平,决定从哪个介质加载 FSBL:
| MIO[6:2] | 启动源 | Pynq-Z2 用法 |
|---|---|---|
00000 | JTAG | 调试、直接下载 |
00001 | Quad-SPI(单片,24 位地址) | 量产固件 |
00010 | Quad-SPI(双片叠加,32 位地址) | 大容量 QSPI |
00101 | SD Card 0 | 开发阶段常用 |
01011 | SD Card 1 / eMMC | Pynq-Z2 无此接口 |
Pynq-Z2 上有两个 Boot Mode 拨码开关(JP4/JP5),SD 模式就是 00101。
-
初始化启动介质(PLL、外设时钟在这里做最基础设置——只够让 QSPI/SD 控制器工作,DDR 不在这里初始化)。
-
读取 BOOT.BIN 的 Boot Header,验证 magic word,找到 FSBL 的偏移和大小。
-
把 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,可以交互。它的核心任务:
- 初始化剩余外设(以太网、USB 等)
- 从存储介质(SD / QSPI / TFTP)加载
zImage(Linux 内核)和devicetree.dtb(DTB) - 设置
bootargs内核参数 - 调用
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_ADDR是fatload等命令的默认目标地址。如果你先fatload了 DTB,再fatload了 zImage,两次都用默认地址,第二次会覆盖第一次的数据。明确指定两个不同的目标地址,不要依赖默认值。
5.2 DTB 在哪、什么时候被加载
DTB(Device Tree Blob)不在 BOOT.BIN 里(除非你用 PetaLinux 的 image.ub 格式,那是 FIT image 另一回事)。常规 SD 卡启动时:
- SD 卡 FAT 分区(partition 1)里放:
BOOT.BIN、uImage/zImage、devicetree.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 验证自己的改动有没有进去
参考资料
| 文档号 | 名称 | 用途 |
|---|---|---|
| UG585 | Zynq-7000 SoC Technical Reference Manual | 启动流程主要参考:Chapter 6 Boot and Configuration(第 157-218 页),PCAP 细节在 Chapter 6.4,OCM 映射在 Chapter 4.1 |
| UG1283 | Bootgen User Guide | bif 文件语法、[bootloader] / destination_cpu 完整属性列表 |
| UG1144 | PetaLinux Tools Documentation | PetaLinux 构建 BOOT.BIN 的完整流程 |
| UG821 | Zynq-7000 SoC Software Developers Guide | FSBL 源码结构说明、PCAP 编程接口 |
| Xilinx Wiki | FSBL — Xilinx Wiki | FSBL hooks 使用说明、debug 技巧 |
| u-boot-xlnx | GitHub: Xilinx/u-boot-xlnx | configs/xilinx_zynq_virt_defconfig,include/configs/zynq-common.h |
本系列持续更新,覆盖 Zynq-7000 从启动链路、Linux 移植到 PL 硬件加速的完整实战路径。 如果你在 FSBL 或 U-Boot 阶段踩了坑,欢迎留言交流——很多问题第一眼都很难定位。