作为 FPGA 圈子里最常用的 SoC 平台,ZYNQ 跑 Linux 几乎是每个做嵌入式开发的人都会遇到的需求。但很多人第一次接触"移植 Linux"这几个字时,很容易被各种概念绕晕:FSBL、bitstream、U-Boot、设备树、rootfs…… 每个词单独看都认识,连在一起就不知道从哪下手。我最早做这个的时候也一样,折腾了将近两周才把系统完整跑起来。这篇系列的第一篇,我先用最传统的"手工编译 + 手写配置"方式,把 ZYNQ 上 Linux 的完整启动链路讲清楚。这套流程适合所有 ZYNQ-7000 系列的芯片,无论你是想深度理解启动原理、还是手上板子是自定义的没法直接用官方 BSP,这篇文章都能给你一个能落地的完整路线。
1. 在开始移植之前:先搞懂 ZYNQ 的启动链路
很多教程上来就教你怎么敲命令,却不解释为什么是这三样东西在配合。我觉得这块必须花点篇幅先说清楚,否则你后面编译一百遍,也只是个"命令复读机"。
1.1 三样核心文件:FSBL、bitstream、U-Boot
ZYNQ 的整个启动过程,大致可以分成四个阶段:BootROM -> FSBL -> U-Boot -> Linux 内核。这四个阶段里,BootROM 是芯片出厂固化的,不需要你管;你需要亲手准备的,是下面这三样东西:
| 文件 | 作用 | 烧录位置 |
|---|---|---|
| FSBL(First Stage Boot Loader) | 初始化 DDR、时钟、MIO,加载 bitstream 到 PL | QSPI / SD / JTAG |
| bitstream(.bit) | PL 端的 FPGA 逻辑配置,俗称"下载进去的电路" | 由 FSBL 加载 |
| U-Boot | 二级引导程序,负责加载内核与设备树 | QSPI / SD |
这三样文件最终会被打包成一个统一镜像,叫 BOOT.BIN。ZYNQ 的 BootROM 只认 BOOT.BIN 这种格式,它会把 BOOT.BIN 里的 FSBL 挑出来启动,然后 FSBL 再去加载剩下的部分。所以你可以把 BOOT.BIN 理解成"一个装着三颗子弹的弹夹",BootROM 的第一枪一定是打在 FSBL 上。
1.2 上电之后,芯片内部到底发生了什么
我在纸上画过无数遍这条链路,后来发现用一句话其实就能说清楚:上电 -> BootROM 根据引脚电平选择启动介质 -> 加载 FSBL -> FSBL 初始化 PS 端并配置 PL -> 加载 U-Boot -> U-Boot 启动内核 -> 内核挂载 rootfs。
ZYNQ 的 BootROM 是芯片出厂前固化好的,它做的事很固定:根据 MIO[6:2] 这几个引脚的电平状态去判断该从 QSPI、SD、NAND 还是 JTAG 启动。判断完了,它会把对应介质里开头的 192KB 内容加载到片内 OCM(On-Chip Memory)里。这 192KB 装的就是 FSBL。
FSBL 启动后,要干的事情可不少:
- 初始化 PS 端的 DDR 控制器、时钟 PLL、MIO 复用;
- 把 bitstream 从启动介质里读出来,通过 PCAP(Processor Configuration Access Port)写到 PL 端,让 FPGA 逻辑先跑起来;
- 从启动介质里找到 U-Boot 的镜像,拷贝到 DDR 里,然后跳过去执行。
然后 U-Boot 启动,它在跑起来之后会按照环境变量bootcmd里的指令,去加载 Linux 内核(uImage)和设备树(dtb)到 DDR,最终把控制权交给内核。到这里,整个"硬件 + 引导 + 系统"的接力才算跑完。
这套链路里最容易绕晕的就是:FSBL 到底放在哪、U-Boot 又放在哪。实际上,在 QSPI 启动模式下,它们都在同一颗 Flash 里,只是地址不同。FSBL 在 0x0 偏移处,bitstream 通常在 0x100000 之后,U-Boot 再往后排。SD 启动模式下情况更简单,BOOT.BIN 直接放 FAT32 分区根目录,FSBL 和 U-Boot 都在这个文件里按顺序排着,BootROM 会按头部信息精确找到它们。
1.3 常见误区:BOOT.BIN 到底装了什么
说一个我见过很多次的误解:有人以为 BOOT.BIN 就是整个 Linux 系统,烧进去系统就能起来。
不是。BOOT.BIN 里只有 FSBL、bitstream 和 U-Boot。内核 uImage、设备树 dtb、rootfs 统统不在里面。如果你用的是 SD 启动,这些文件需要单独放到 SD 卡的一个 FAT32 分区里,U-Boot 启动后自己去读;如果你用 QSPI,就得在 U-Boot 环境变量里写好 Flash 地址,让它从固定偏移去加载内核和设备树。
我第一次做的时候就在这上面栽过跟头:BOOT.BIN 烧好了,板子上电只见 U-Boot 界面,却怎么都进不了 Linux。研究了半天才发现,内核根本没放到 SD 卡上。这个细节看着简单,但确实能让整个移植卡住好几天。
2. 准备工作:工具链、源码和 Petalinux 的取舍
搞移植的第一步不是编译,而是把环境搭好。这一步的选择会直接影响你后面调试时的效率,所以我把自己用过的几种组合以及它们之间的取舍都列出来。
2.1 典型开发环境的整体构成
一套完整的 ZYNQ Linux 手工移植环境,至少需要这些东西:
- 交叉编译工具链:在 x86 的 PC 上编译 ARM 目标代码,首选
arm-linux-gnueabihf-系列; - Linux 内核源码:推荐 Xilinx 维护的
linux-xlnx仓库,或者官方发布的 LTS 版本; - U-Boot 源码:同样推荐 Xilinx 维护的
u-boot-xlnx仓库; - 设备树编译器 dtc:把 dts 源文件编译成内核能读的 dtb;
- 根文件系统制作工具:buildroot 或 debootstrap 二选一。
我的建议是,内核和 U-Boot 都用 Xilinx 的维护分支,因为官方硬件平台对应的一些补丁和默认配置都已就位,能省掉大量"为什么这个驱动不生效"的排查时间。
2.2 为什么 U-Boot 之前要花那么大力气?
很多新手会问:为什么不跳过 FSBL,直接让 BootROM 加载 U-Boot?原因在于 BootROM 只认它自己的启动头格式,需要 FSBL 作为中间过渡。更进一步说,ZYNQ 的 DDR 初始化在 BootROM 阶段尚不确定,只有 FSBL 里才有完整的 DDR 控制器配置序列,U-Boot 即使在内存里运行,也无法替代这一步。
另外,bitstream 必须在 Linux 启动前加载完毕。Linux 内核里很多驱动直接映射并操作 PL 端的寄存器,若没有 bitstream,这些寄存器访问会挂死或读到随机值。FSBL 在 Linux 之前把 bit 流加载到 PL,等于提前把硬件"通电"。我之前在调试时试过把 bit 流放到 Linux 启动后再加载,虽然不是不行,但需要额外写一个 FPGA manager 驱动,复杂度会明显增加。传统方式里,FSBL 一上来就把 PL 配好,后面所有阶段都省心。
2.3 用 Petalinux 的快速路径 vs 全手工路径
如果你是官方开发板、项目周期又紧,直接用 Petalinux 是效率最高的选择。它会根据你导出的 XSA(Vivado 的硬件描述文件)自动生成 FSBL、U-Boot、内核配置和设备树。常用的命令大概是这样的:
# 1. 创建 Petalinux 工程(ZYNQ 对应模板是 zynq) petalinux-create -t project --template zynq --name my_project # 2. 导入硬件描述文件(XSA) petalinux-config --get-hw-description=./xsa_files # 3. 配置内核和设备树 petalinux-config -c kernel petalinux-config -c device-tree # 4. 编译 petalinux-build # 5. 生成 BOOT.BIN(包含 FSBL、bitstream、U-Boot) petalinux-package --boot --fsbl zynq_fsbl.elf --fpga system.bit --u-boot这套流程的好处在于,它几乎把所有低层细节都封装好了。但缺点也很明显:Petalinux 版本与 Vivado 版本强绑定、占用磁盘空间大、遇到非官方板时,它生成的设备树经常和你实际的硬件对不上。我之前做一个项目用的卡是自研的,Petalinux 生成的 BSP 不能直接用,最后还是转回全手工流程。所以这篇文章后面讲的,全部以全手工方式为准——这也是我自己真正学透 ZYNQ 启动原理的那条路。
3. 手工构建 Linux 系统的完整流程
如果你跟我一样,想把手里的板子完全吃透,那就静下心来走一遍手工流程。整个流程分四步:编译 U-Boot、编译内核、制作 BOOT.BIN、制作 rootfs。每一步都不难,但每一步都有几个"没想到"的坑。
3.1 交叉编译工具链的安装与环境变量
我用的是 32 位 ARM 内核,原因很简单:ZYNQ-7000 的 PS 端是双核 Cortex-A9,是 32 位架构。所以工具链选arm-linux-gnueabihf。如果你的板子是 ZYNQ UltraScale+ MPSoC(64 位),则要换成aarch64-linux-gnu。这里注意别选错,否则编出来的内核根本跑不起来。
在 Ubuntu 环境下,安装工具链和 dtc 非常方便:
sudo apt-get install gcc-arm-linux-gnueabihf device-tree-compiler安装完验证一下版本是否正常:
arm-linux-gnueabihf-gcc --version dtc --version然后就是固定搭配的源码。我建议内核和 U-Boot 都从 Xilinx 官方 GitHub 仓库拉取,使用与 Vivado 版本对应的 tag,尽量减少版本互相不兼容的问题。我当前用得比较顺手的组合是:内核 5.4 + U-Boot 2020.01,这套组合论坛资料多、各种坑基本都有人踩过。
3.2 编译 U-Boot:先选对 defconfig
U-Boot 源码拿到手之后,不要急着make,先看configs/目录下有哪些和你板子接近的 defconfig。官方 ZYNQ 开发板一般都有现成的配置,比如zynq_zc702_defconfig、zynq_zc706_defconfig等。如果你的板子接近这些官方板,直接用它,能省很多定制工作。
编译命令如下:
git clone https://github.com/Xilinx/u-boot-xlnx.git cd u-boot-xlnx export CROSS_COMPILE=arm-linux-gnueabihf- make zynq_zc702_defconfig make -j4编译完会在根目录生成u-boot.elf,这个是 ELF 格式,会被 bootgen 打包进 BOOT.BIN。
这里有个很多人踩的坑:U-Boot 的默认配置里,CONFIG_BOOTARGS和CONFIG_BOOTCOMMAND往往设定了默认启动介质。比如开发板默认是 QSPI 启动,你想从 SD 启动,就要去include/configs/zynq-common.h这类头文件里改默认启动参数。我当年就吃过这个亏:SD 卡里文件都放好了,U-Boot 却一直在 QSPI 里找内核,找半天找不到直接进了命令行。后来把CONFIG_BOOTCOMMAND里的启动设备改成 SD,重新编译 U-Boot,问题立刻解决。
3.3 编译 Linux 内核:两个容易搞错的细节
内核编译本身不算复杂,难在"配置"和"加载地址"这两个细节。
git clone https://github.com/Xilinx/linux-xlnx.git cd linux-xlnx export ARCH=arm export CROSS_COMPILE=arm-linux-gnueabihf- make xilinx_zynq_defconfig make -j4 UIMAGE_LOADADDR=0x8000 uImage make dtbs首行第一个要注意的是环境变量ARCH=arm,很多人在 x86 机器上编译时容易忘记设,结果编译出 x86 版本的内核,一启动就花屏或报"wrong image format"。第二个就是UIMAGE_LOADADDR=0x8000,这个地址对应 ZYNQ 在 DDR 中加载内核的位置,必须与 U-Bootbootm命令传入的地址一致。如果这里不对,内核虽然能被 U-Boot 识别,但跳转过去后 PC 指针跑到错误地址,系统直接没有任何输出。
编译完成后,你会得到两个关键文件:
- 内核镜像:
arch/arm/boot/uImage - 设备树:
arch/arm/boot/dts/zynq-zc702.dtb(具体文件名取决于 defconfig)
如果板子是自研的,还需要修改对应的 dts 文件。很多板卡参考官方设计,但改了 DDR 容量、某个外设在 PL 端、或换了 PHY 的型号,这些变化都要在 dts 里同步,否则轻则外设不工作,重则内核 panic。
3.4 制作 BOOT.BIN:bootgen 和 BIF 文件
BOOT.BIN 的打包工具叫 bootgen,一般随 Vivado 一起安装。如果你装了 Vivado,可以直接找到bootgen的可执行文件;如果你用的是 Petalinux,它内部也带了 bootgen。
打包需要一个 BIF 文件来描述镜像的组成。一个最简 BIF 长这样:
the_ROM_image: { [bootloader] zynq_fsbl.elf system.bit u-boot.elf }然后执行:
bootgen -image boot.bif -o BOOT.BIN -w其中zynq_fsbl.elf是从 SDK 或 Vivado 导出的 FSBL,system.bit是你的 FPGA 综合实现产出的 bit 流,u-boot.elf刚刚已经编出来了。三个文件凑齐,BOOT.BIN 就能生成。
有几点需要说明:
- bootgen 生成的 BOOT.BIN 可以把 bitstream 和 FSBL 合并,U-Boot 排在第三段,BootROM 会根据头部偏移自动找;
- 如果暂时不想加载 PL,可以不把 bit 放进去,但这样 PL 端无逻辑,跑 Linux 也只能用 PS 部分;
-w参数是指覆盖已有的同名输出文件,不加的话遇到已存在的 BOOT.BIN 会停止。
3.5 U-Boot 环境变量:启动参数的正确姿势
BOOT.BIN 搞定后,接下来要让 U-Boot 知道怎么找到内核和设备树。在 U-Boot 启动倒计时阶段按任意键进入命令行,然后设置环境变量。以 SD 启动为例:
setenv bootargs 'console=ttyPS0,115200 root=/dev/mmcblk0p2 rw rootfstype=ext4' setenv bootcmd 'fatload mmc 0:1 0x2080000 uImage; fatload mmc 0:1 0x2000000 zynq.dtb; bootm 0x2080000 - 0x2000000' saveenv逐条解释:
console=ttyPS0,115200:内核的串口终端,ZYNQ PS 端 UART 的设备名固定叫这个;root=/dev/mmcblk0p2:根文件系统在 SD 卡的第二个分区,设备节点是/dev/mmcblk0p2;rw:以可读写方式挂载,方便调试;fatload mmc 0:1 0x2080000 uImage:从第一个分区读取 uImage 文件到内存 0x2080000;fatload mmc 0:1 0x2000000 zynq.dtb:从第一个分区读取设备树到 0x2000000;bootm 0x2080000 - 0x2000000:启动内核,中间用-跳过 initrd,直接使用 dtb。
内存地址的选择要有讲究。不能随便写两个数字就完事,因为 U-Boot 自身也运行在内存中,如果加载地址和 U-Boot 镜像所在区域重叠,内核数据会被覆盖。通常 ZYNQ 下0x2000000到0x3000000这一段比较安全,可以避开 U-Boot、FSBL 留下的数据以及内核的初始加载区域。我习惯把 dtb 放到0x2000000,uImage 放到0x2080000,间隔一段距离防止覆盖。
如果你不想每次开机都进命令行设置,记着saveenv,环境变量会被保存在 boot 介质中的环境变量区域。对这个项目,如果从 SD 启动,U-Boot 的环境变量会默认保存在 SD 卡 FAT 分区里的一个特定位置(bootenv分区或者 uEnv.txt 文件)。
顺带说一个更现代化的做法:不少新版本 U-Boot 支持uEnv.txt文件。你可以在 FAT 分区写一个文本文件,内容就是bootcmd=...和bootargs=...。U-Boot 启动时自动读取并执行,效果等同于 saveenv,但好处是改启动参数不用动不动就重新烧写环境变量。
3.6 制作 rootfs:buildroot 与最小文件系统
启动参数里root=/dev/mmcblk0p2指的是 SD 卡的第二个分区,里面必须存放一个完整的根文件系统。这里有两个方案供你选择:
方案一:buildroot 定制最小系统
buildroot 适合快速生成一个精简、可裁剪的文件系统,产物体积小、启动快,非常适合做产品原型。我使用的方式:
git clone https://github.com/buildroot/buildroot.git cd buildroot make qemu_arm_vexpress_defconfig make menuconfig进入配置界面后重点调这几处:
Target options里选择 ARM 架构;Toolchain里使用外部工具链,指定arm-linux-gnueabihf-gcc,避免 buildroot 重新下载一波内部工具链浪费时间;Filesystem images里勾选 ext4 格式,这样生成的镜像可以直接dd到 SD 卡分区。
配置完执行:
make -j4编出来的文件系统位于output/images/rootfs.ext4。把它写入 SD 卡第二分区:
sudo dd if=rootfs.ext4 of=/dev/sdb2 bs=1M这命令里的/dev/sdb2取决于你 SD 卡的设备节点,不同机器可能不一样,操作前用lsblk确认一下,千万别写错盘。
方案二:debootstrap 制作 Ubuntu 根文件系统
如果你更习惯用 Ubuntu,可以搞一个 armhf 版本的 Ubuntu 根文件系统。相比之下包管理更成熟,装软件会方便很多,但体积也大不少。我们之前调试驱动时,甚至直接拿了个带桌面环境的 armhf Ubuntu 文件系统来跑 ZYNQ,只要能跑起来,开发机上能做的事,板子上都能做。唯一要注意的是,rootfs 的文件系统驱动要和你内核配置匹配,否则挂载不上。
4. QSPI 启动与 SD 启动的切换:从调试到量产的关键一步
手工移植成功后,很少会一直用同一种启动介质。我一般调试阶段用 SD 卡,量产阶段切 QSPI。这两种方式的区别不只是"从哪读文件",整个启动流程和文件布局都会变。
4.1 为什么调试阶段我推荐 SD 启动
SD 启动最大的优势是灵活:更新内核、设备树只需要往 FAT 分区里拷贝文件,不需要重新烧写 Flash,也不需要拔下来用专门的烧录器。调试驱动的时候,编译完内核直接拷贝到 SD 卡,插上板卡,上电就能验证,整个过程不超过 10 秒。QSPI 就完全不同了,每改一次内核都要重新烧写 Flash,烧写过程又慢又容易出错。
QSPI 的优势则是启动快、无需额外的外设依赖、更接近量产状态。对于需要快速上电、无外部存储、文件系统只读的产品,QSPI 是更稳的选择。但调试阶段真心不建议,过于浪费时间。
4.2 启动模式引脚 MIO[6:2] 怎么配置
ZYNQ 通过 MIO[6:2] 的电平状态来决定从哪个介质启动。不同板卡定义不一样,但常见的取值可以参考这张表:
| 启动模式 | MIO[6:2] 二进制 | 说明 |
|---|---|---|
| JTAG | 0 0100 | 调试模式 |
| QSPI | 0 0010 | 从 QSPI Flash 启动 |
| SD | 0 0110 | 从 SD 卡启动 |
| NAND | 0 0001 | 从 NAND Flash 启动 |
具体怎么拨,看板卡丝印和原理图。我最早调试时板卡默认拨在 QSPI,每次更新内核都要烧写整个 BOOT.BIN,后来把拨码开关切到 SD,更新内核变成"拷贝文件"级别的工作,调试效率直接翻倍。
4.3 SD 启动的分区布局建议
SD 卡如果按我说的用起来,建议分两个区:
- 第一个分区:FAT32,大小建议 512MB,放 BOOT.BIN、uImage、dtb;
- 第二个分区:ext4,占剩余空间,放 rootfs。
分区用fdisk、gparted都行。创建好之后千万别忘了把第一个分区设成 bootable 标志。虽然 ZYNQ 的 BootROM 不要求第一分区可引导,但 U-Boot 在读取文件和某些工具做校验时会参考这个标志,设置上更稳妥。
之后把 BOOT.BIN 复制到 FAT32 分区,把编译好的 uImage 和 dtb 也复制过去。这里再次强调:BOOT.BIN 只含 FSBL、bitstream、U-Boot,内核和设备树是独立文件,两者缺一不可。
5. 遇到问题怎么排:我踩过的坑和排查链路
移植 Linux 的过程中,系统能不能跑起来往往靠的不是运气,而是排查思路。我把最常见的现象和排查链路整理出来,你可以按图索骥。
5.1 U-Boot 起来了,Linux 起不来
现象:U-Boot 打印正常,执行bootm后卡住,或者输出 "Wrong image format"。
一般原因包括:
bootm的加载地址与UIMAGE_LOADADDR不一致;- 内核镜像不是 uImage 格式,U-Boot 无法识别;
- 设备树地址与内核地址重叠,内核启动时被设备树数据覆盖;
- 内核没有编译对应的 rootfs 驱动。
排查路径可以这样走:
- 先在 U-Boot 里执行
printenv,确认bootcmd和bootargs; - 执行
fatls mmc 0:1看看文件是否真实存在、名字是否和bootcmd里写的一致; - 执行
md 0x2080000查看内存地址,uImage 头部前四个字节应该是十六进制27051956,这是 uImage 的魔数,如果不是,说明文件没加载进去或者加载地址不对; - 执行
fdt addr 0x2000000、fdt print /看设备树能否正常解析。
5.2 内核起来但 rootfs 挂不上
现象:内核打印了一堆启动信息后,卡在类似VFS: Cannot open root device "mmcblk0p2"的地方。
原因和解决建议:
- 检查
root=参数是否指向了正确的分区。最经典的错误是把第二个分区写成了第一个; - 确认内核里选了
CONFIG_MMC、CONFIG_EXT4_FS,否则即使 rootfs 存在也认不出来; - 确认 rootfs 实际上已经写了进去,用
lsblk -f看一下/dev/sdb2的文件系统类型。
我在调试早期,曾把root=/dev/mmcblk0p2写错为p1,结果内核反复提示无法打开设备,那一刻我还以为是 rootfs 没烧好,浪费了整个下午。后来仔细看启动日志,一句 "Unable to open root device" 就点破了问题。
5.3 设备树导致的诡异问题
设备树出错最大的特点是"编译不报错,跑起来就是不对"。具体表现可能有串口没输出、某个外设注册失败、DDR 大小与实际不符导致频繁死机。
排查设备树问题我基本靠两个工具:
- U-Boot 里的
fdt addr、fdt print命令,可以直接查看设备树关键节点; - 内核启动日志中的设备树相关报错,比如
OF: fdt: Error之类。
常踩的坑还有把 DDR 总容量写成了 2GB,但硬件实际只有 1GB,内核启动时访问不存在的内存地址,莫名其妙就 page fault 或死机。这些错误只能靠认真对照硬件原理图和 dts 配置来排查。
5.4 强烈推荐的 TFTP + NFS 调试姿势
每次改内核都拔 SD 卡确实麻烦,所以我强烈建议你配置 TFTP + NFS 启动:内核和设备树通过 TFTP 从电脑加载,rootfs 通过 NFS 从电脑挂载。这样开发板端只负责运行,所有文件都在电脑上,改完驱动重启就能验证,不再需要动 SD 卡。
U-Boot 侧配置示例:
setenv ipaddr 192.168.1.100 setenv serverip 192.168.1.10 setenv bootargs 'console=ttyPS0,115200 root=/dev/nfs nfsroot=192.168.1.10:/srv/nfsroot rw ip=192.168.1.100:192.168.1.10::255.255.255.0::eth0' setenv bootcmd 'tftpboot 0x2080000 uImage; tftpboot 0x2000000 zynq.dtb; bootm 0x2080000 - 0x2000000' saveenvTFTP 服务器在 Ubuntu 下装个tftpd-hpa,NFS 服务器装个nfs-kernel-server,配置文件都不难。这套组合我到现在还在用,尤其是调试 Linux 驱动阶段,效率比 SD 卡拷贝高很多。NFS 根文件系统的另一个好处是:可以在电脑上直接修改文件系统里的脚本、库和程序,板子重启后自动生效,连打包都省了。
5.5 量产烧写 QSPI 的注意点
如果最终产品是 QSPI 启动,那么需要把 BOOT.BIN 烧到 QSPI Flash 的 0x0 地址。如果你想把内核和设备树也塞进 QSPI,那 U-Boot 的 bootcmd 就要改成从 Flash 读取。比如把内核放在偏移 0x100000 处、设备树放在 0x600000 处:
setenv bootcmd 'sf probe 0 0 0; sf read 0x2080000 0x100000 0x500000; sf read 0x2000000 0x600000 0x100000; bootm 0x2080000 - 0x2000000'其中sf read的第一个参数是内存目标地址,第二个是 Flash 中的源偏移,第三个是读取长度。使用 Vivado Hardware Manager 烧写时,烧的过程不要中断,防止 Flash 写入一半。烧完最好回读校验一下,否则只能返工。
6. 移植完成之后:启动时间优化与安全启动
系统跑起来只是第一步,真正做产品的都知道,后面还有两个绕不开的话题:启动时间、安全启动。
6.1 从 10 秒压到 3 秒的优化方向
ZYNQ 传统启动流程里,从上电到 Linux shell,通常需要 5~10 秒。这不完全是 Linux 内核慢,而是整个引导链路里存在多个串行步骤。可行的优化方向包括:
- 启用 U-Boot SPL,分流一部分 FSBL 的功能,减少一次引导跳转的开销;
- 内核镜像启用压缩,像 LZMA 或 LZO,减小镜像体积,从 Flash 或 SD 读取少一点;
- 设置 U-Boot 的
CONFIG_BOOTDELAY=0,省掉倒计时那几秒; - rootfs 使用只读压缩文件系统,比如 SquashFS,减少挂载时间;
- 把内核和设备树直接整合到 BOOT.BIN 里,避免 U-Boot 再去找文件。
这五个方向里,性价比最高的是 BOOTDELAY=0 和启用压缩镜像,改动小、收益明显。如果项目对启动时间有极致要求,那可能还得往"裁剪内核 + 定制 rootfs"方向走,这就属于另一篇文章的内容了。
6.2 安全启动:产品化的必经之路
ZYNQ 原生支持 RSA 签名认证。用 bootgen 给 FSBL 签名之后,BootROM 会验证签名是否合法,合法才继续执行。这个机制能防止非法镜像在你的设备上运行,也能防止固件被篡改。量产时要做的就是把密钥烧进 eFUSE 并锁定,让设备只能跑你签过名的镜像。
即使项目暂时不上安全启动,我建议设计阶段也把密钥管理和签名流程考虑进去。等产品上市了再补这层保护,成本会非常高。
6.3 什么时候该用 Petalinux,什么时候坚持手工
这里做个很务实的总结:
- 官方开发板 + 快速验证 + 项目周期紧:优先 Petalinux,它生成的 BSP 就是为官方板量身定制的;
- 自研板 + 需要深度定制内核/设备树 + 需要理解启动细节:手工流程更靠谱;
- 量产阶段:很可能两种方式混合用,Petalinux 管构建,手工脚本管定制和发布。
我在自研板项目上吃过 Petalinux 生成的 BSP 和实际硬件对不上的亏,后来干脆把所有 BSP 相关的逻辑从 Petalinux 里剥离出来,用手工脚本维护 U-Boot 配置、内核配置和设备树。这样做虽然前期麻烦,但一旦形成脚本,后面反而是最高效的。
7. 写在最后的实操感悟
这一篇把 ZYNQ 传统移植 Linux 的主流路径过了一遍:从启动链路、工具链、U-Boot、内核、BOOT.BIN、rootfs,再到启动介质切换和排错思路。看起来环节很多,但走通一次之后你会发现,ZYNQ 的启动链路本质上只是一个层层接力、把控制权从 BootROM 依次交接给 FSBL、U-Boot、内核的过程。每个环节的职责都相对独立,唯一需要你操心的就是让每个接力棒准确落在约定好的地址上。
我在实际项目中体会最深的一点是:手工跑一遍流程的价值不在于"不用 Petalinux",而在于你亲自编译 U-Boot、内核、设备树并逐一调试之后,你对整个系统的掌控感是完全不一样的。比如 bootgen BIF 文件里为什么[bootloader]要放在 FSBL 前面、UIMAGE_LOADADDR=0x8000为什么不能改、bootm中间那个-表示什么——这些问题,只有亲手试过、卡过、查过文档,才真正变成你自己的知识。
如果你也正在 ZYNQ 上移植 Linux,或者已经遇到了某个具体的问题,欢迎在评论区聊聊。这个系列之后我打算接着写 UltraScale+ MPSoC 的移植差异、启动时间优化以及把整个手工流程脚本化的实践,到时候我们可以继续深聊。