news 2026/9/21 3:02:19

手动移植ZynqMP U-Boot与Linux Kernel:摆脱PetaLinux的启动定制实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
手动移植ZynqMP U-Boot与Linux Kernel:摆脱PetaLinux的启动定制实践

简介:面向ZynqMP平台嵌入式开发者,这份教程提供一套不依赖Petalinux、基于Xilinx官方GitHub源码手动移植Uboot与Kernel的完整实践路径,适合在自定义板卡上完成底层系统启动开发的工程师。PDF内容从aarch64交叉编译器安装配置讲起,依次覆盖FSBL与PMU固件生成、ATF可信固件编译,以及设备树修改、U-boot移植与Kernel定制等核心环节,每一步都有具体命令、图例与编译指导,并标注了板级适配时容易出错的细节,帮助读者理解从Bootloader到内核启动的完整链路。资源为单个PDF文件,整体仅5.89MB,便于下载后集中学习。已有3335人学习,是希望绕开Petalinux、自主掌控嵌入式移植流程的开发者值得参考的资料。 说实话,ZynqMP(ZU+系列)这类的MPSoC芯片,很多朋友一上来就直接打开PetaLinux,点两下鼠标,BOOT.bin、image.ub都给你生成得明明白白,烧进去也能跑。但你要是想在真实板卡上做一些定制化启动、快速验证外设驱动、或者想彻底搞懂U-Boot和Kernel的启动链路,全依赖PetaLinux反而会被它的封装绑住手脚。这篇博文我完整记录一下不用PetaLinux,直接基于Xilinx官方源码手动移植U-Boot和Linux Kernel的过程。适合那些已经有点嵌入式基础、想彻底搞清楚ZynqMP整套启动流程的Linux开发工程师,尤其是手上有自家板卡、不想被PetaLinux黑盒限制的朋友。

我先说结论:非Petalinux开发方式一点都不神秘,本质上就是三件事——编译一份信任链镜像(ATF/BL31)、编译一份U-Boot、编译一份内核和设备树,然后按规定的启动地址打包烧录。难点不在编译,而在理解ZynqMP的分级启动机制、DDR初始化、设备树描述和启动介质布局。下面我会把这些点一个个拆开讲,每一步都给到可直接执行的命令和参数。

1. 整体思路:为什么绕开Petalinux,手动移植反而更高效

1.1 非Petalinux方式的真实价值

Petalinux本质上是一个集成构建系统,它帮你管理了交叉编译工具链、内核配置、U-Boot配置、rootfs生成、打包脚本这一大堆事情。但它的工程结构非常重,一个工程目录几个GB很正常,而且版本升级、补丁管理、自定义设备树修改时,经常要等它跑完整个构建流程。更麻烦的是,Petalinux默认会生成一个它自己风格的BSP结构,出了问题你很难定位是配置问题还是脚本问题。

手动移植的收益非常直接:

  • 编译速度快,增量编译几十秒到一两分钟,不用每次全量构建。
  • 对U-Boot、Kernel的配置完全透明,每个配置项都能找到来龙去脉。
  • 方便版本管理,一个Git仓库就能维护板级DTS、defconfig和打包脚本。
  • 调试启动异常时,你可以手动逐段验证BL31、U-Boot、Kernel,问题边界非常清晰。

当然,手动移植也有门槛,最核心的是你得理解ZynqMP的启动流程,而不是像用Petalinux那样只管点“Build”。但这个门槛跨过去之后,收益是长期的。

1.2 准备工作和工具链选型

我这次使用的环境是Ubuntu 20.04 LTS(64位),目标板是Xilinx ZCU104开发板(基于ZynqMP XCZU7EV)。请不要照抄板卡型号,重点看思路。

需要的组件如下:

组件版本/仓库作用
U-BootXilinx的u-boot-xlnx分支(git clone)引导Loader,负责初始化DDR、外设,加载Kernel
Linux KernelXilinx的linux-xlnx分支(git clone)目标操作系统内核
Arm Trusted Firmware(ATF)Xilinx的arm-trusted-firmware分支生成BL31,EL3固件,DDR初始化之后由它跳转到U-Boot
交叉编译器aarch64-linux-gnu- (gcc 9.3+)编译ARM64代码
dtc / fdtdump系统自带或独立编译设备树编译和反编译
打包工具bootgen(Xilinx Vitis安装自带的bootgen)生成BOOT.BIN

交叉编译器的安装很简单:

sudo apt update sudo apt install gcc-aarch64-linux-gnu device-tree-compiler

bootgen我采用的是Vitis自带版本:通常在/tools/Xilinx/Vitis/2022.2/bin/bootgen路径下。你也可以从Xilinx单独下载,但直接用Vitis的就行。注意一定要把它加到PATH里,否则后面打包BOOT.BIN会报找不到bootgen。

2. ZynqMP的启动流程与U-Boot移植核心

2.1 从BootROM到BL31再到U-Boot,一条链路必须跑通

在做U-Boot移植之前,你得先搞清楚ZynqMP的启动顺序,这是整个移植的地基。ZynqMP内部有 BootROM(片上ROM),它上电后根据启动模式引脚(Boot Mode)从QSPI/SD/eMMC等处把FSBL(First Stage Boot Loader)或BL31加载到片内OCM并执行。官方推荐的典型链是这样的:

  • BootROM加载FSBL(或直接加载BL31,取决于配置)。
  • FSBL初始化PS侧的DDR、MIO、时钟,然后跳转BL31。
  • BL31(ATF)是EL3固件,它负责运行时服务、PSCI等功能,然后再跳转BL33。这里的BL33就是U-Boot。
  • U-Boot负责加载kernel Image和DTB,最后跳转kernel。

非Petalinux方式也完全遵循这条链,只是FSBL不再使用Vitis里的FSBL工程,而是直接用U-Boot SPL来替代。SPL就是U-Boot的二级引导程序,它的体积小,可以放在BootROM能访问的介质里。这样就能做到:BootROM → SPL(初始化DDR)→ BL31 → U-Boot → Kernel。这个组合非常经典,Xilinx官方也保留了SPL的使用方式。

我实际移植时用的就是SPL + BL31 + U-Boot + Kernel的组合,boot.bin里包含SPL和BL31即可。SPL编译后会生成u-boot-spl.bin,BL31编译后生成bl31.elf,后面打包时这两个文件都要进BOOT.BIN。

2.2 U-Boot配置与编译:defconfig该怎么选

U-Boot源码我拉的是Xilinx维护的仓库,因为针对ZynqMP的板级支持最全:

git clone https://github.com/Xilinx/u-boot-xlnx.git -b xlnx_release_v2022.2 cd u-boot-xlnx export ARCH=arm64 export CROSS_COMPILE=aarch64-linux-gnu-

然后执行:

make xilinx_zynqmp_virt_defconfig make -j$(nproc)

编译完成后,当前目录下会生成u-boot.elfu-boot.binu-boot-spl.bin等文件。

这里有一个非常关键的选型:xilinx_zynqmp_virt_defconfig。这个是Xilinx官方提供的“通用虚拟平台配置”,适用于绝大多数ZynqMP板卡,因为它默认使能了多个关键驱动:DDR、SD、QSPI、GEM(以太网)、SPI、I2C、USB等。它不绑定具体板卡的DTS,而是启动时通过编译进固件里的默认DTS来匹配。如果是自家板卡,一般从这个defconfig出发,然后修改设备树覆盖板级差异就行,比从头写defconfig容易太多。

编译时还可以加一个可选参数:

make u-boot.elf

如果你的开发流程里有Vitis工具链依赖,这个ELF格式会很方便;如果不跑Vitis,直接使用u-boot.bin搭配SPL方式也行。

2.3 SPL配置:让U-Boot跑起来的“前置Loader”

这里有个大家容易忽略的细节:在非Petalinux方式下,U-Boot编译出来后,还差一步——让BL31和U-Boot能在DDR里被正确加载。SPL就承担了DDR初始化工作。U-Boot的SPL配置需要确保打开了SPL相关的选项,一般xilinx_zynqmp_virt_defconfig里已经默认开启了CONFIG_SPL和CONFIG_SPL_FS_FAT等选项。

为了验证SPL是否正常,可以让U-Boot串口打印日志。默认的CONFIG_SPL_TEXT_BASE是在OCM里的,BootROM能够直接访问。串口输出重定向到PS UART0,一般ZynqMP默认串口是uart0,映射到MIO 18/19。这个在DTS里要检查是否匹配你的板卡原理图。

常见错误是SPL编译后无法运行,日志卡在BootROM阶段。这个时候优先检查启动模式拨码开关是不是拨到了SD启动(对应ZCU104是SW6),以及BOOT.BIN的分区是否正确。

3. Linux Kernel移植实操:defconfig和设备树才是重头戏

3.1 内核源码拉取与基础编译

内核我同样拉取Xilinx的linux-xlnx仓库,版本和U-Boot保持一致:

git clone https://github.com/Xilinx/linux-xlnx.git -b xlnx_release_v2022.2 cd linux-xlnx export ARCH=arm64 export CROSS_COMPILE=aarch64-linux-gnu-

然后编译:

make xilinx_zynqmp_defconfig make -j$(nproc) Image make -j$(nproc) dtbs

xilinx_zynqmp_defconfig是Xilinx官方的通用ZynqMP配置,它比defconfig更全面,包含了ZynqMP的驱动支持和Xilinx常用IP驱动。编译完成后,内核Image在arch/arm64/boot/Image,设备树文件在arch/arm64/boot/dts/xilinx/目录下,ZCU104对应的设备树是zynqmp-zcu104-revC.dtb

这一步没什么玄学,只要交叉编译器版本对、依赖齐全,基本能一次通过。如果有编译报错,最可能是老内核配了新编译器出现头文件不兼容,统一使用gcc 9.3左右会比较稳。

3.2 设备树修改与验证:串口、DDR、外设全是它的活

内核编译过了不意味着板子能跑。ZynqMP这种芯片,板级差异全靠设备树体现。这里我以ZCU104的DTS为例,把关键节点拆开讲:

打开arch/arm64/boot/dts/xilinx/zynqmp-zcu104-revC.dts,你会看到类似这样的结构:

/ { chosen { bootargs = "console=ttyPS0,115200 root=/dev/mmcblk0p2 rw rootwait"; stdout-path = "serial0:115200n8"; }; aliases { serial0 = &uart0; ethernet0 = &gem3; }; }; &uart0 { status = "okay"; pinctrl-names = "default"; pinctrl-0 = <&pinctrl_uart0_default>; };

这里面最核心的几处:

  • chosen节点里的bootargs,是内核启动参数。console指定了串口直接用ttyPS0,波特率115200。root指定了根文件系统所在设备。rootwait让内核等待存储设备准备好再挂载,这个是SD/eMMC启动必须的。
  • aliases节点很重要,它把uart0、ethernet0等逻辑名称和实际节点关联起来。U-Boot也会读取这个节点来设置stdoutstdin
  • &uart0里的 status 必须为 “okay”,否则内核会跳过该外设的初始化。

你自己画板卡的时候,要重点查这几项:

  • 内存大小:DTS里默认可能写的是4GB(ZCU104),如果板卡DDR只有2GB,必须手动修改reg = <0x0 0x0 0x0 0x80000000>;这类内存节点(实际用的是memory节点)。
  • MIO/GEM引脚复用:ZynqMP的引脚复用关系要写进pinctrl节点里,否则网口不通。
  • PS侧外部时钟:检查psu_pl_clk0psu_pl_clk1是否符合你的晶振频率。

修改完DTS之后,直接编译dtbs,得到zynqmp-zcu104-revC.dtb。如果板卡是自己画的,建议在xilinx目录下新建一个zynqmp-custom-board.dts,这样不污染官方文件。

3.3 手动验证设备树:用fdtdump检查关键信息

内核设备树编译好了,不急着烧。我强烈建议先反编译看一遍实际内容:

fdtdump -c arch/arm64/boot/dts/xilinx/zynqmp-zcu104-revC.dtb | less

重点确认几个关键内容是否存在:

  • chosen节点里的bootargs和stdout-path。
  • memory节点里的reg地址和大小。
  • 串口、网口节点的status是否为okay。
  • CPU节点是否包含4个A53核。

如果这些都没问题,设备树这关基本就过了。要是某个节点没生效,优先查是不是DTS里被覆盖了,比如&uart0覆盖了通用文件。Xilinx的dts include层级比较多,经常出现“改了个寂寞”的情况,反编译dtb是最快的验证方式。

4. 打包BOOT.BIN与烧录启动的完整步骤

4.1 使用bootgen打包启动镜像

现在手上的材料有:

  • SPL:u-boot-spl.bin
  • BL31:bl31.elf
  • 内核:Image
  • 设备树:zynqmp-zcu104-revC.dtb

打包BOOT.BIN需要写一个bif文件,我个人习惯叫它boot.bif,内容如下:

the_ROM_image: { [bootloader] u-boot-spl.bin bl31.elf }

注意,这里没有把U-Boot本体和内核放进去。U-Boot本体不是BootROM直接加载的,它由SPL从启动介质里加载。所以最后SD卡里要有三个部分:

  • BOOT.BIN(含SPL + BL31)
  • U-Boot的镜像文件(这里叫u-boot.itb或直接u-boot.bin
  • 内核Image + DTB(可以由U-Boot从FAT分区加载,也可以打包进Image.ub以便统一管理)

如果使用U-Boot的标准加载方式,比较稳妥的方案是生成一个Image.ub,把内核和设备树打进去:

mkimage -f auto -A arm64 -O linux -T kernel -C none -a 0x8000 -e 0x8000 -d arch/arm64/boot/Image -d arch/arm64/boot/dts/xilinx/zynqmp-zcu104-revC.dtb Image.ub

mkimage是U-Boot工具包里的命令,一般编译完U-Boot后会在tools/mkimage路径下。没有的话sudo apt install u-boot-tools

然后执行bootgen生成BOOT.BIN:

bootgen -image boot.bif -o i BOOT.BIN -w on

这个步骤会生成一个带安全头等的BOOT.BIN。这里要注意:bootgen的版本和你用的U-Boot代码库分支匹配最好,否则可能出现FSBL或者BL31加载地址错位的问题。2022.2的U-Boot配2022.2的Vitis bootgen,我实测下来无问题。

4.2 SD卡分区与文件放置

ZCU104的SD启动,SD卡需要两个分区:

  • 第一个分区:FAT32,存放BOOT.BIN、Image.ub(或U-Boot + DTB)、u-boot.bin。
  • 第二个分区:EXT4,存放rootfs。

分区可以用gparted或命令fdisk完成。我推荐用gparted,图形化不易出错。FAT32分区大小给512MB足够,剩下的给EXT4。

拷贝文件时,注意FAT分区不建议放太多小文件,否则U-Boot读取变慢。

放入文件后,插上SD卡,上电串口应该能看到类似日志:

U-Boot SPL 2022.01-... Trying to boot from MMC1 NOTICE: BL31: v2.5(release):... U-Boot 2022.01-...

然后U-Boot会自动读取fat分区里的Image.ub,如果有多个节点,需要检查U-Boot环境变量bootcmd。手动输入命令也能启动:

fatload mmc 0:1 0x10000000 Image.ub bootm 0x10000000

这里地址0x10000000是DDR里的一个临时加载地址,确保它不和Kernel镜像地址冲突。ZynqMP的DDR起始地址是0x0,通常内核Image的加载地址是0x8000,所以把Image.ub加载到0x10000000是安全的。

4.3 U-Boot环境变量与启动参数自动化的经验

每次手动敲命令不现实。我发现最省事的做法是在U-Boot里设置默认环境变量,把启动命令固化下来。方法是修改U-Boot源码里的include/configs/xilinx_zynqmp.h或者使用U-Boot的default environment机制:

bootcmd=if fatload mmc 0:1 0x10000000 Image.ub; then bootm 0x10000000; fi bootargs=console=ttyPS0,115200 root=/dev/mmcblk0p2 rw rootwait

如果你不想改源码,也可以在U-Boot命令行用setenvsaveenv保存。但注意保存环境变量的地址空间,ZynqMP在QSPI/SD不同介质里环境变量的存储位置不一样,曾经我因为环境变量保存到了QSPI里,导致一上电读了旧的环境变量,启动参数一直不对,排查了半天。后来干脆固定使用bootcmd里的分支判断,优先读取SD,读不到再回退网络启动,这样可以避免环境变量污染带来的麻烦。

5. 常见问题与排查技巧实录

5.1 启动日志异常的分阶段定位

ZynqMP启动链路长,问题可能出现在任何一个环节。我的排查套路是:先看日志停在哪,再判断是哪个组件挂了。

现象可能原因排查思路
上电串口无任何输出BootROM没找到启动介质,或启动模式不对查拨码开关;确认SD卡是否识别;用SD格式化工具重做FAT32分区
SPL打印后卡住,无BL31日志BL31没被正确加载,ATF配置问题或加载地址不符检查bif文件里的BL31路径;确认SPL对BL31地址设置
BL31打印后没进U-BootU-Boot入口地址配置不对,或BL31内存布局冲突检查U-Boot TEXT_BASE,一般BL33加载地址在DDR的某个偏移位置
U-Boot可以起来但加载kernel失败FAT分区读取失败,或Image.ub路径不对检查文件名大小写;确认FAT分区存在
Kernel启动panic,挂载不了rootfsrootfs分区设备名不对,或root指定错误确认root=/dev/mmcblk0p2是否对应EXT4分区;用rootwait

5.2 内存和DTS不一致导致的疑难杂症

有一次我在自家板卡上发现Kernel启动到一半突然重启,后来定位是DTS里memory定义成了3GB,但板卡实际DDR是2GB,导致内核访问了不存在的物理地址。这种问题最容易出现在修改DTS时只改一处、漏改另一处的情况。手动移植的板卡,建议先让内核只使用安全内存范围,跑通之后再去扩展DTS。

还有个隐秘问题:DDR的EPHY(ECC)功能。如果板卡DDR硬件不支持ECC,但在FSBL/SPL配置里开启了ECC位,启动时会随机死机。排查方式是关闭所有ECC相关的配置,只保留基础DDR控制器参数来自动训练。Xilinx在SPL里一般会用DDR DRAM driver自动训练,不需要手动配置具体的延时参数,别被那堆寄存器数吓到,默认auto training就行。

5.3 设备树改了半天没生效?怕是没编进Image.ub

这是个新手容易踩的坑。修改了DTS,编译出新的DTB,但启动时还是老设备树。原因通常是Image.ub里还包着旧的DTB。如果你用mkimage -f auto打包,依赖里可能会带进旧的dtb,或者你只更新了dtb文件,但没有重新生成Image.ub。

我一般在打包时会把内核Image和DTB分开加载,先加载DTB,再加载Image,这样排查起来更直观:

fatload mmc 0:1 0x10000000 zynqmp-zcu104-revC.dtb fatload mmc 0:1 0x8000 Image bootm 0x8000 - 0x10000000

这样每次只替换dtb,不用反复重新打包Image.ub,调试效率高很多。等设备树稳定了,再合成Image.ub方便发布。

5.4 网口PHY不识别、外设枚举不出来的通用解法

ZynqMP的GEM网卡驱动兼容性很好,遇到网口出不来,绝大多数是设备树里PHY地址错了。PHY的地址是由硬件决定的,比如ZCU104上CPLD配置PHY地址为0,DTS里写0就通。如果你用的是第三方核心板和底板的奇怪组合,PHY地址不确定,一个技巧是在U-Boot里先执行mdio read扫描PHY地址:

mdio list

如果PHY没列出,确认GEM reset引脚有没有被复用配置好。

外设枚举不到的问题,同理也是优先查DTS里status、时钟频率和复位引脚。不要一上来就怀疑内核驱动,先把设备树用fdtdump确认节点存在、status是okay,再看驱动的devicetree匹配逻辑。

写在最后的小体会

手动移植ZynqMP的U-Boot和Kernel,整套流程跑下来,最大的收获不是那些命令,而是对启动链路的敏感度。以后再遇到任何Linux启动异常,你心里会有一个清晰的检查地图:先看BootROM有没有找到介质,再看SPL有没有初始化DDR,然后确认BL31跳转,最后才是Kernel和设备树的事。这套经验是Petalinux给不了的。最后再建议一点,如果是在自家板卡上量产调试,建议把U-Boot环境变量里的bootcmd设置为优先读取SDImage.ub,同时保留一个串口交互的救急入口,这样即使在现场烧坏了系统,也能通过串口手动引导恢复。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/21 3:02:13

ESP32-P4 USB Host鼠标实验:从枚举到HID报告解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/21 2:48:29

用USB HID虚拟电池实现Windows零驱动电源管理调试

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华