☰
传统方式手工移植Linux到ZYNQ:FSBL、U-Boot、设备树与根文件系统详解
2026/10/28 18:21:23 网站建设 项目流程

从FPGA逻辑工程师转来做嵌入式软件,很多人第一次接触ZYNQ都被它这种“一颗芯片里既有ARM处理器又有FPGA”的异构架构搞得很困惑。ZYNQ的全称是Zynq-7000 SoC,它把双核Cortex-A9的PS处理系统(Processing System)和7系列FPGA的PL可编程逻辑(Programmable Logic)封装在同一片硅片上。想在PS端跑Linux系统,如果不借助PetaLinux这类自动化工具,而是“传统方式”手工移植,第一眼会觉得步骤又多又零碎:FSBL、U-Boot、设备树、根文件系统,每个名词都能查半天资料。但这恰恰是理解ZYNQ启动链路最值得走的路,也是今天这篇文章要完整记录的内容。

这个系列的第一篇,我先把“传统方式移植Linux”这件事讲透。传统方式指的是不用PetaLinux集成工具,而是自己手工编译和打包Linux运行所需的每一块:用Vitis(旧版SDK)生成FSBL,手工编译U-Boot,手工编译内核并生成设备树,再用Buildroot或BusyBox构建根文件系统,最后在SD卡上摆放好所有启动文件,让ZYNQ脱离JTAG也能独立启动进入Linux命令行。这套流程适合嵌入式Linux开发者、FPGA工程师、刚入手ZYNQ开发板的学生,也适合那些必须深度定制启动流程的实战项目。跑通它,你对ZYNQ“上电之后到底发生了什么事”会有非常清晰的认知,以后再碰PetaLinux或Yocto,也不会觉得是黑盒。

1. 项目概述与核心思路

1.1 ZYNQ跑系统,本质上是跑一套经典嵌入式启动链路

很多人第一次上手ZYNQ时,会习惯性地把它类比成STM32或普通的ARM开发板,认为只要下载一个固件就能跑。真实情况远不止这么简单。ZYNQ上电后的启动链路是一条明确的流水线:芯片内部固化在ROM里的BootROM程序先运行,它根据MIO引脚配置的启动模式(JTAG、SD、QSPI、NAND等)决定从哪里读取第一段代码;在SD启动模式下,BootROM从SD卡第一个分区读取BOOT.BIN,加载到片内OCM(On-Chip Memory)并校验,然后跳转到FSBL(First Stage Boot Loader,第一阶段引导程序)执行。

FSBL的任务很关键,它需要完成DDR内存控制器初始化、PLL时钟配置,如果硬件工程里带了PL端bitstream(FPGA配置文件),FSBL还会把bitstream加载到PL中进行配置。这些初始化完成之后,FSBL把第二阶段引导程序U-Boot从启动介质拷贝到DDR内存中,并跳转过去。到这里,引导接力棒就交给了U-Boot。U-Boot做的事类比我平时用的台式机里的GRUB或老式BIOS引导,它初始化外设、设定内存布局,从SD卡读入内核镜像和设备树,设置内核启动参数,最后跳转到内核的入口地址。

最后一段是Linux内核启动,内核解压自身、初始化驱动,识别SD、串口、网络等硬件外设,然后根据设备树里描述的信息挂载根文件系统,执行/sbin/init,最终在串口终端上打印出登录提示符。这一整条链路,就是“ZYNQ跑Linux”的本质。移植的意义,不是简简单单把一个Linux镜像烧进去,而是确保这条链路上每个环节都衔接正确:FSBL要能初始化DDR,U-Boot要能找到内核,内核要能识别设备树并挂载文件系统。只要中间一环参数不对,启动就会卡死在某个阶段,串口终端往往只留下几行日志或一片空白。

1.2 为什么非要走传统手动移植,PetaLinux不香吗

说到ZYNQ上跑Linux,很多人第一反应是“直接用PetaLinux就完了”。PetaLinux确实是Xilinx官方主推的嵌入式Linux开发工具,它能根据硬件描述文件自动生成FSBL、设备树、内核配置和根文件系统,甚至一键打包BOOT.BIN,看起来确实省事。但我在实际项目里对PetaLinux的态度是“能用但不敢全信”,尤其是平台刚导入、芯片或板卡非常规、或者需要把Linux的某个子系统改得很深的时候,PetaLinux的封装反而成了障碍。

首先,PetaLinux作为一套完整的BSP工具链,它会带来大量与具体项目无关的软件包和构建系统代码,版本升级频繁,企业内网环境下部署还很麻烦。其次,PetaLinux生成的启动链参数很多是基于默认模板推导的,一旦DDR型号奇特、PL端设计复杂、或需要做安全启动等定制功能,自动生成的东西经常要手改底层文件,这时候如果不懂传统方式里FSBL和U-Boot的工作机制,改起来就很被动。

手工移植看似慢,实际上是把整套知识体系沉淀下来的过程。我第一次手工跑通ZYNQ Linux大约花了一个晚上,之后再用PetaLinux做项目,遇到启动异常反而能快速定位是U-Boot阶段挂掉还是内核挂掉。这篇文章所讲的“传统方式”,是打地基;PetaLinux是盖楼时的自动化设备,地基不稳时设备越先进越容易出错。所以我劝想把ZYNQ作为长期平台的开发者,至少完整走一遍手工流程,后面遇到问题会省下大把排查时间。

2. 动手前的概念拆解与工具准备

2.1 启动链路上的四样东西,分别是什么

动手之前,先把启动链路上需要的几样产物弄清楚。按照上电后各阶段加载的顺序,分别是BOOT.BIN(实际就是FSBL、bitstream、U-Boot打包在一起的单一文件)、U-Boot(第二阶段引导程序)、设备树文件(dtb)、Linux内核镜像(uImage或zImage),最后的rootfs则属于系统运行阶段。

FSBL是用Vitis基于硬件描述文件(HDF/ XSA)生成的C工程,它本质上是一个针对于你当前硬件系统的初始化小固件。之所以叫“一阶段引导”,是因为它运行在片内OCM里,此时DDR还没完成初始化,无法运行复杂的大段代码。FSBL代码量不大,但特点是很依附硬件,内核时钟频率、DDR型号和时序、MIO引脚复用配置都来自硬件工程,因此每次硬件设计变了,FSBL都需要重新生成。

U-Boot则是标准的开源引导程序,在嵌入式ARM领域几乎是事实标准。它支持从SD、QSPI、网络等多种介质加载镜像,提供交互式命令行,对调试非常友好。编译U-Boot时,开发板配置文件选择正确,就能生成适用于ZYNQ的u-boot.elf。设备树则是Linux用来描述硬件资源的配置文件,把处理器型号、内存大小、串口地址、SD控制器、中断信息等以树状数据结构描述出来。内核本身是平台无关的,具体板卡有什么硬件、寄存器和中断如何连接,都通过设备树在启动时告诉内核。传统移植里,设备树要么直接用厂商提供的dts源码编译,要么自己为定制板卡手工编写。

最后是内核镜像和rootfs。ZYNQ通常使用uImage格式(U-Boot可识别的带头镜像),rootfs则是一整套Linux用户空间文件和目录,包含/bin、/sbin、/etc、/lib等,最简单的可用BusyBox构建,也可以选择Buildroot生成一个完整可用的文件系统。理解了这堆东西分别负责什么,就不会在启动失败时一头雾水。

2.2 工具链与源码版本,选对组合比速度更重要

传统方式移植Linux,最怕版本不匹配。Xilinx的BSP对工具链版本、内核版本、U-Boot版本都有隐含绑定关系。我习惯使用Ubuntu 20.04 LTS作为主机环境,安装以下基础工具:

sudo apt update sudo apt install -y build-essential git curl libssl-dev device-tree-compiler \ u-boot-tools bc bison flex python3 cpio rsync file

交叉编译工具链是必须提前装好的。ZYNQ的ARM Cortex-A9是32位处理器,需要使用ARM 32位的交叉编译器。最稳妥的两种来源是:Ubuntu软件源里的gcc-arm-linux-gnueabihf或Linaro官方工具链。我用的是Linaro 7.5版本,实际项目里7.x和8.x系列都能稳定工作,过于老旧的4.x版本编新版内核会报各种语法错误。

sudo apt install -y gcc-arm-linux-gnueabihf arm-linux-gnueabihf-gcc --version

源码方面,优先使用Xilinx维护的发行分支,一是版本匹配度高,二是针对ZYNQ的补丁都已经合入。常见做法是直接克隆xlnx仓库:

git clone https://github.com/Xilinx/u-boot-xlnx.git -b xlnx-rebase-v2023.1 --depth 1 git clone https://github.com/Xilinx/linux-xlnx.git -b xlnx-rebase-5.15.0-LTS --depth 1

注意这里的分支名会随版本持续变化,到GitHub仓库页面看一下最新的release分支即可。不要随便拿主线Linux和主线U-Boot直接编,倒不是绝对编不出来,而是Xilinx的不少设备树改动和驱动补丁只在xlnx分支里,用主线版本会平白无故增加很多调试成本。

3. 手工移植全流程实操

3.1 硬件工程与FSBL,一切从这里出发

传统方式有个前提:你需要一份包含完整硬件配置的Vivado工程。没有它,后续的FSBL和设备树都无从谈起。在Vivado中配置ZYNQ Processing System时,至少要把以下三项确认清楚:DDR型号和位宽要与板卡实际颗粒一致;UART选择合适的一个(开发板通常用UART0或UART1);SD控制器和MIO引脚使能,因为我们要从SD卡启动系统。

配置完成后,在Vivado中综合、实现、生成bitstream,然后通过File -> Export Hardware -> Include bitstream导出一个XSA文件。现代版本Vivado导出的是XSA,老版本是HDF文件,本质都是描述硬件系统的打包文件。

有了XSA,打开Vitis工作空间,先创建一个新的平台工程,指向刚才导出的XSA文件。之后在这个平台工程基础上新建一个Application工程,从模板里选择“Zynq FSBL”。FSBL工程实际上是个半成品的引导代码,编译之后生成fsbl.elf。同期还可以把bitstream也备注好,预备后续打包进BOOT.BIN。

这一步值得啰嗦几句:FSBL不是越新越好,而是要跟着Vivado版本走。同一个XSA用不同版本Vitis生成的FSBL,DDR初始化序列可能不同,混淆版本经常导致DDR访问异常。我踩过一次坑,用Vitis 2019.2生成的FSBL去引导Vivado 2022.2的硬件工程,结果启动时DDR读写每隔一段时间就不稳定,浪费了一整天才定位到工具链版本不一致上。

3.2 编译U-Boot并打包BOOT.BIN

U-Boot针对不同开发板有对应的默认配置。Xilinx在configs/目录下提供了大量ZYNQ相关配置文件,比较常见的有zynq_zed_defconfig、zynq_zc706_defconfig、zynq_zcu102_defconfig,如果是自己画的核心板,通常用最接近厂商板卡的配置起步。

cd u-boot-xlnx export ARCH=arm export CROSS_COMPILE=arm-linux-gnueabihf- make zynq_zed_defconfig make -j$(nproc)

编译结束会在根目录生成u-boot二进制文件。但打包BOOT.BIN时,U-Boot需要的是ELF文件格式。因此需要额外把U-Boot编译成ELF格式,常用做法是运行make u-boot.elf,不过部分版本需要先单独编译对应的SPL或通过Vitis创建U-Boot工程来生成ELF文件。直接执行这一步时,如果报错找不到libfdt等头文件,通常是主机依赖没装齐,回到上一节把依赖装完再试即可。

拿到fsbl.elf、bitstream.bit、u-boot.elf之后,用Vitis自带工具Bootgen生成BOOT.BIN。Bootgen在Vitis的安装路径下,命令如下:

bootgen -image boot.bif -o BOOT.BIN

对应的boot.bif内容类似这样:

the_ROM_image: { [bootloader] fsbl.elf bitstream.bit u-boot.elf }

[bootloader]标记说明fsbl.elf是第一阶段引导代码,bitstream是可选的,如果PL端没有逻辑设计这行可以直接不写,也不会影响启动。Bootgen会把三部分按ZYNQ要求的启动镜像格式打包在一起,BootROM和FSBL就能按顺序执行它们。

3.3 内核编译与设备树生成,最容易出错的环节

内核编译本身不复杂,但有几个关键点需要注意。首先检查内核源码分支中是否包含arch/arm/configs/xilinx_zynq_defconfig这样的默认配置,没有就先检查是不是拉成主线分支了。建议的编译流程:

cd linux-xlnx export ARCH=arm export CROSS_COMPILE=arm-linux-gnueabihf- make xilinx_zynq_defconfig make uImage LOADADDR=0x80008000 -j$(nproc) make dtbs make zynq-zed.dtb

LOADADDR=0x80008000是ZYNQ平台常用的内核加载地址。生成uImage时会自动加一个64字节的U-Boot头部,U-Boot可以识别并跳转到这个地址执行。如果编译时提示缺少mkimage命令,说明没有安装u-boot-tools。

设备树文件在arch/arm/boot/dts/目录下。Xilinx为开发板准备了已经写好的.dts文件,比如zynq-zed.dts,编译后得到对应的.dtb。如果用的是自己设计的板卡,就需要基于现有dts修改,改动点通常包括内存大小、串口节点、SD节点的引脚配置,以及网络PHY的复位GPIO等。设备树的语法初看复杂,但改几个节点就能满足大多数定制需求,不必一步到位精通。

我这里强调一个新手很容易忽略的点:设备树里不要硬编码启动参数。ZYNQ的启动参数(也就是内核夺权前要用的console、root等信息)应该在U-Boot环境变量里设置,设备树可以选择性保留bootargs属性,但U-Boot启动时会以自己环境变量里的bootargs覆盖设备树中的值。既然U-Boot会接管,设备树里这部分保持默认即可,减少维护两处配置的麻烦。

还有一个容易踩的坑是内核配置里没有开启EXT4和SD卡驱动支持。默认的xilinx_zynq_defconfig通常已经包含,但如果自己从头裁剪配置,必须确认以下选项至少处于开启状态:

  • CONFIG_ARCH_ZYNQ=y,平台架构支持
  • CONFIG_MMC=y、CONFIG_MMC_SDHCI=y、CONFIG_MMC_SDHCI_PLTFM=y,SD卡控制器驱动
  • CONFIG_EXT4_FS=y,文件系统支持
  • CONFIG_DEVTMPFS=y,自动挂载/dev目录
  • CONFIG_SERIAL_8250或对应Cadence UART驱动,串口控制台输出

少了任何一个,系统可能启动到一半静默失败,或者卡在“Kernel panic - not syncing: VFS: Unable to mount root fs”。这是内核启动无法挂载文件系统的经典报错。

3.4 根文件系统构建,Buildroot是最舒服的平衡点

rootfs的构建方式有多种,最“原始”的是用BusyBox手搓,这个方式适合教学,能让你理解文件系统的最小结构,但在生产项目中效率偏低。我更推荐Buildroot,它介于“纯手工”和“Yocto全家桶”之间,既能生成包含工具链、库和应用的最小根文件系统,又不会引入过重的构建体系。

Buildroot的配置过程很简单:

git clone https://git.buildroot.net/buildroot cd buildroot make menuconfig

在配置界面里需要确认以下几项:

  • Target options -> Target Architecture = ARM (little endian)
  • Target options -> Target Architecture Variant = Cortex-A9
  • Toolchain -> C library 选择 glibc 或 musl 都可以,教程演示用默认glibc即可
  • System configuration -> 配置主机名、root密码、自动登录串口终端等
  • Filesystem images -> ext2/3/4 勾选,或者选择tar格式方便后续手动展开

保存退出后用make -j$(nproc)编译。整个Buildroot一次完整编译会从源码拉取并构建数百个软件包,耗时取决于网速和CPU性能,第一次大概需要30分钟到1小时。编译生成的output/images/rootfs.tar就是可供SD卡使用的根文件系统压缩包。

在调试阶段,一个轻量的BusyBox rootfs其实更灵活。也可以先用Buildroot把环境跑通,后面需要加什么库或应用时,再回Buildroot里勾选对应软件包重新编译,这样整个系统看起来依然“干净、可控”,不像Yocto那样生成几个GB的构建目录。

3.5 SD卡分区、烧写与首启动调试

SD卡分为两个分区是ZYNQ的传统做法:第一个分区用FAT32格式,放BOOT.BIN、uImage、设备树dtb这些启动相关文件;第二个分区用ext4格式,放根文件系统。用fdisk或gparted都能操作,下面是用fdisk处理8GB SD卡的分区流程(设备节点以实际为准,通常是/dev/sdb或/dev/mmcblk0)。

sudo fdisk /dev/sdb

先用p查看现有分区,用d删掉旧分区,然后依次创建新的分区。第一个分区选主分区,起始扇区保留默认,大小设为1GB左右,格式设置为W95 FAT32(分区类型代码为b);第二个分区占满剩余空间,格式设为Linux(代码83)。分区完成后分别格式化:

sudo mkfs.vfat -F 32 /dev/sdb1 sudo mkfs.ext4 -J journal_dev=0 /dev/sdb2

格式化完成后,把启动文件拷入FAT分区:

mkdir /tmp/boot /tmp/rootfs sudo mount /dev/sdb1 /tmp/boot sudo mount /dev/sdb2 /tmp/rootfs sudo cp BOOT.BIN uImage zynq-zed.dtb /tmp/boot/ sudo tar -xf rootfs.tar -C /tmp/rootfs/ sync sudo umount /tmp/boot /tmp/rootfs

BOOT.BIN、uImage、设备树这三个文件的名字不是固定的,U-Boot环境变量里指定的文件名要和实际一致。开发板厂商给的U-Boot默认环境变量通常定义好了默认的boot命令,比如读取BOOT.BIN、image.ub或uImage。如果你修改了文件名,就必须进U-Boot命令行修改环境变量或直接输入引导命令。

插上SD卡、连接串口线(ZYNQ PS端的UART通常通过USB转串口模块连到电脑),板载跳线设置成SD启动模式,上电后串口终端(波特率通常为115200)应该能看到U-Boot的启动日志。如果一切正常,几秒后会出现Linux启动日志,最后停在登录提示符,默认root账号、密码为空或为你在Buildroot里设置的密码,输入后就能进入Linux命令行了。第一次跑通系统,串口上出现欢迎信息的那一刻,说明整套传统方式移植链路已经正式打通。

4. 启动过程常见问题与排查技巧实录

4.1 串口完全无输出,问题大概率在BOOT.BIN之前

启动过程中最让人头痛的,莫过于上电后串口终端什么都没有。这时先不要怀疑内核或rootfs,很可能是BOOT.BIN根本没有被正确执行。排查顺序我建议这样走:先确认串口线没接错,ZYNQ的PS端串口引脚一般不在FT232等USB转串口模块的默认引出位置,很多开发板还有跳线开关控制;再用示波器或万用表确认PS端的MIO启动模式引脚电平是否正确,如果是SD启动模式,还要检查SD卡的FAT分区是否放在第一个分区,且BOOT.BIN位于分区根目录。

第二个高频原因是BOOT.BIN内部各文件不匹配。比如fsbl.elf是Vivado 2019.2生成、u-boot.elf是Vitis 2023.1生成,两者对DDR配置的理解不一致,FSBL跳转到U-Boot后U-Boot因为DDR配置异常而直接死掉,表现出来就是串口刷了几行日志后突然无声。建议BSP相关文件全部使用同一版本工具链生成,从源头避免这类问题。

第三个常见原因是BOOT.BIN里根本没有包含U-Boot。有时Bootgen的bif文件里漏写了u-boot.elf这一行,FSBL执行完自然没有后续内容,表现为串口只打印出“Xilinx First Stage Boot Loader”后就没有下文了。检查bif文件内容后重新用Bootgen生成即可。

4.2 内核启动卡死与rootfs挂载失败的原因对照

当串口日志已经能看到U-Boot输出,但Linux启动到某个阶段突然卡死,问题就比较集中了。我整理了一个速查表,实际排错时能节省很多时间:

现象大概率原因排查方法
U-Boot打印Starting kernel ...后无任何输出内核设备树与硬件不匹配,或内核地址错误检查U-Boot的加载地址,核对设备树中DDR大小和串口节点
内核打印No working init foundrootfs不完整或Init程序缺失检查SD卡ext4分区是否存在/sbin/init,检查rootfs压缩路径是否正确
内核打印Kernel panic - VFS: Unable to mount root fs内核没开启对应文件系统驱动,或root参数指定错误启用EXT4支持,检查U-Boot的bootargs中root=/dev/mmcblk0p2是否指向正确分区
启动到一半提示Waiting for root deviceSD卡驱动未找到设备确认设备树中SD控制器节点是否正确,检查CONFIG_MMC相关配置
系统起来后网络不通设备树未配置MAC或PHY复位配置不对核对网卡节点,检查PHY地址与复位GPIO
串口有输出但花屏或乱码波特率设置不对或FSBL串口初始化与终端不匹配建议统一切到115200 8N1,检查Vivado里UART配置

从实际经验看,rootfs挂载失败占了我遇到问题的六成左右,且多表现为“U-Boot正常启动、内核解压完成、打印出一堆硬件初始化日志、然后突然panic”。这时候优先检查U-Boot环境变量里的bootargs。ZYNQ常用的bootargs是:

setenv bootargs 'console=ttyPS0,115200 root=/dev/mmcblk0p2 rw rootwait'

/dev/mmcblk0p2表示SD卡第二个分区,rootwait是必须的,它告诉内核在轮询等待SD设备就绪后再挂载,不加这个参数在速度较慢的SD卡上经常挂载失败。console=ttyPS0是对应ZYNQ CADENCE UART的串口设备名,不同设备树文件名可能用ttyS0,需要和设备树节点里的compatible和device_type对应上,否则内核日志会输出到不存在或错误的串口上。

U-Boot引导内核时,有几种方式把你的环境变量传给内核。最简单的是在U-Boot命令行里执行启动命令前,手动setenv bootargs再saveenv。如果启动命令比较复杂,建议在U-Boot源码中的环境变量默认配置里预设好bootcmd,这样每次启动加载SD卡镜像时不会忘掉关键参数。我通常把启动命令写成一条完整命令:

fatload mmc 0 0x2080000 uImage fatload mmc 0 0x2000000 zynq-zed.dtb bootm 0x2080000 - 0x2000000

第一条加载内核镜像到0x2080000,第二条加载设备树到0x2000000,第三条U-Boot的bootm命令把内核和设备树一起启动。地址0x2080000不是随意选的,它避开了DDR起始地址附近U-Boot自身占用的空间,同时留出足够余量保证内核解压不会覆盖设备树。ZYNQ的DDR起始地址是0x00100000,但U-Boot代码、堆栈和内核加载区必须隔开,经验上内核加载用0x2080000左右是比较稳的,别用0x8000这种太靠近起始地址的数值。

5. 我在实际项目中积累的几点体会

传统方式移植Linux这个流程,我前后在三种不同型号的ZYNQ板卡上跑过,包括ZedBoard、ZC706以及一块自研核心板。每次遇到的坑都不一样,但总结下来有一个共同经验:任何一步报错,第一个反应都应该是“看启动阶段进展到哪里”,而不是盲目改配置。启动日志停留在BOOT.BIN阶段就查FSBL、U-Boot找不到镜像就查SD分区与文件名、内核打印到硬件初始化阶段卡住就查设备树和驱动、rootfs报错就查bootargs和文件系统格式。按这个思路排查,基本能在一个小时内定位问题。

另一个体会是版本锁定特别重要。ZYNQ的BSP所有组件之间存在隐式兼容性,很多看起来完全无关的小毛病,最后都被溯源到某个组件版本新旧不一致。我现在做项目时,会刻意记录一份文本清单,包括Vivado版本、Vitis版本、U-Boot分支、内核分支、Buildroot版本、交叉工具链版本,全部固定下来作为团队基线。这个习惯在遇到“偶发重启失败”“DDR读写出错”这类奇葩故障时,能直接排除掉一大批变量,省下大把时间。

最后再分享一个小技巧:在传统方式调试阶段,不要一上来就折腾完整的图形界面或复杂应用程序,先把最小系统跑稳定——串口登录、SD卡读写、网络连通,这三样搞定,后续在上面叠加其他功能就顺畅多了。这个系列后续内容,我会继续拆解设备树定制、PL端驱动与Linux内核交互、以及如何把传统流程固化成可复用的脚本,把ZYNQ这套异构平台玩得明明白白。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询