
从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加载到片内OCMOn-Chip Memory并校验然后跳转到FSBLFirst Stage Boot Loader第一阶段引导程序执行。FSBL的任务很关键它需要完成DDR内存控制器初始化、PLL时钟配置如果硬件工程里带了PL端bitstreamFPGA配置文件FSBL还会把bitstream加载到PL中进行配置。这些初始化完成之后FSBL把第二阶段引导程序U-Boot从启动介质拷贝到DDR内存中并跳转过去。到这里引导接力棒就交给了U-Boot。U-Boot做的事类比我平时用的台式机里的GRUB或老式BIOS引导它初始化外设、设定内存布局从SD卡读入内核镜像和设备树设置内核启动参数最后跳转到内核的入口地址。最后一段是Linux内核启动内核解压自身、初始化驱动识别SD、串口、网络等硬件外设然后根据设备树里描述的信息挂载根文件系统执行/sbin/init最终在串口终端上打印出登录提示符。这一整条链路就是“ZYNQ跑Linux”的本质。移植的意义不是简简单单把一个Linux镜像烧进去而是确保这条链路上每个环节都衔接正确FSBL要能初始化DDRU-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或UART1SD控制器和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生成的FSBLDDR初始化序列可能不同混淆版本经常导致DDR访问异常。我踩过一次坑用Vitis 2019.2生成的FSBL去引导Vivado 2022.2的硬件工程结果启动时DDR读写每隔一段时间就不稳定浪费了一整天才定位到工具链版本不一致上。3.2 编译U-Boot并打包BOOT.BINU-Boot针对不同开发板有对应的默认配置。Xilinx在configs/目录下提供了大量ZYNQ相关配置文件比较常见的有zynq_zed_defconfig、zynq_zc706_defconfig、zynq_zcu102_defconfig如果是自己画的核心板通常用最接近厂商板卡的配置起步。cd u-boot-xlnx export ARCHarm export CROSS_COMPILEarm-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 ARCHarm export CROSS_COMPILEarm-linux-gnueabihf- make xilinx_zynq_defconfig make uImage LOADADDR0x80008000 -j$(nproc) make dtbs make zynq-zed.dtbLOADADDR0x80008000是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_ZYNQy平台架构支持CONFIG_MMCy、CONFIG_MMC_SDHCIy、CONFIG_MMC_SDHCI_PLTFMySD卡控制器驱动CONFIG_EXT4_FSy文件系统支持CONFIG_DEVTMPFSy自动挂载/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-A9Toolchain - 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_dev0 /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/rootfsBOOT.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 consolettyPS0,115200 root/dev/mmcblk0p2 rw rootwait/dev/mmcblk0p2表示SD卡第二个分区rootwait是必须的它告诉内核在轮询等待SD设备就绪后再挂载不加这个参数在速度较慢的SD卡上经常挂载失败。consolettyPS0是对应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这套异构平台玩得明明白白。