ZYNQ SoC FPGA手动移植Linux完整指南:从启动链到根文件系统

发布时间:2026/10/28 9:14:59
ZYNQ SoC FPGA手动移植Linux完整指南:从启动链到根文件系统

搞嵌入式这么久,要说哪件事最有门槛,我觉得是在ZYNQ这种SoC FPGA上把Linux跑起来。别看现在PetaLinux一键打包挺方便,很多新人装个工具就点Next,但一旦板子不正常启动、内核起不来、网口不通,就会发现自己对整条引导链路其实一无所知。所以我打算写一个系列,专门聊ZYNQ跑系统的路径,第一期先讲最传统的Linux移植方式,也就是不依赖PetaLinux,完全靠手动编译、手动配置,把U-Boot、内核、设备树、根文件系统一层层搭起来。这条路虽然前期费点力气,但走完一遍,整个SoC的启动机制、DDR初始化、外设配置这些底层逻辑基本就通了。

这个主题适合谁?我觉得两类人最该看:一类是从单片机转过来的工程师,习惯了裸机开发,第一次接触Linux系统移植不知道从哪里下手;另一类是已经被PetaLinux坑过、想搞清楚它背后到底干了什么的人。文章里我会把ZYNQ的启动流程、交叉编译环境搭建、U-Boot和内核的编译配置、设备树的作用、根文件系统的制作、以及SD卡镜像的烧写方法都过一遍,所有命令和参数都标注清楚,照着操作就能跑通。

1. 传统方式移植Linux的整体思路与启动链拆解

先说结论:传统方式移植Linux,核心工作就是把ZYNQ的启动链一截一截拼起来。这个启动链从芯片上电那一刻就开始了,顺序大概是这样的:BootROM -> FSBL -> U-Boot -> Linux内核 -> 根文件系统。每一环都有明确职责,前一个环节准备好环境,后一个环节才能运行。

BootROM是芯片出厂固化的,ZYNQ上电后第一件事就是执行它。它负责读取Boot Mode引脚配置的电平,判断从哪种介质启动(QSPI、SD卡、JTAG还是NAND),然后把第一阶段的启动镜像加载到片上OCM(On-Chip Memory)。OCM只有256KB,这个容量限制决定了BootROM不可能加载一个完整的U-Boot,它只能加载一个足够小的二级引导程序,也就是FSBL或者U-Boot SPL。

FSBL的全称是First Stage Boot Loader,它是Xilinx官方SDK生成的东西。很多人都不知道FSBL到底干了什么,这里多说一句:它主要做三件事。第一,初始化PS端的基础时钟和DDR控制器,这一步是关键中的关键——DDR要是配置不对,后面所有代码一跑就崩;第二,读取硬件描述文件(HDF)中的PL配置信息,把FPGA的bitstream下载进去,让PL端的逻辑先跑起来;第三,把下一级的引导程序(通常是U-Boot)从启动介质加载到DDR里,然后跳转过去。

U-Boot是第二级引导,它的任务要更丰富一些:初始化串口、网卡、SD控制器等外设,读取内核镜像、设备树、ramdisk到内存,设置内核启动参数(bootargs),最后跳转到内核入口。这里有个很有意思的细节——U-Boot本质上也是一个独立的操作系统,有自己的驱动框架和命令体系,你在串口终端敲的那些命令,其实是U-Boot的shell。因为U-Boot初始化了外设,Linux内核起来之后就不需要重复初始化了,很多驱动甚至可以直接从U-Boot那里接管硬件状态。

Linux内核本身则是整个系统的大管家,它接管CPU、内存、中断控制器、外设驱动,最终挂载根文件系统,执行init进程,系统就算正式跑起来了。整个链路中任何一个环节出问题,表现都不一样:卡在BootROM阶段多半是启动介质没选对或者镜像头有问题;卡在FSBL阶段大概率是DDR参数不对;卡在U-Boot阶段可能是镜像格式错误;内核挂在半路则要看串口最后一行输出,通常是设备树或驱动问题。

理解了这条链,你再看PetaLinux做的那些事就明白了:它本质上是把这几个环节自动化了,生成FSBL、编译U-Boot、编译内核、打包设备树、拼接根文件系统,脚本全帮你干了。但工具毕竟不知道你的板子长什么样,DDR型号、PHY芯片、串口配置这些最终还得人来指定。传统方式的优势就在于每一个环节都是自己手动控制的,出了问题你能定位到具体的层,而不是对着一个黑盒挠头。

2. 动手前的软硬件准备与方案选型

2.1 硬件平台和板级资料先摸清

搞ZYNQ Linux移植,第一步不是装软件,而是把自己的板子硬件搞清楚。我拿最常见的ZYNQ-7020开发板举例,但不管你是黑金、米联客还是正点原子的板子,原理图一定要先看明白几个关键点:DDR3/DDR4的具体型号和位宽、串口用的是哪个UART(一般是UART0或UART1,映射到MIO的哪几个引脚)、PHY芯片型号(RTL8211E还是KSZ9031,这决定了设备树里PHY节点的配置方式)、SD卡用的是SD0还是SD1接口。这些信息看着琐碎,但每一个都直接影响配置,漏一个后面就得多排查半天。

时钟频率也要确认。开发板一般用的是33.333MHz的PS_CLK,但也有50MHz的,这个频率是PS端的基准时钟,U-Boot和FSBL阶段都要按实际值配置,错了的话串口波特率会偏,打印出来的字符全是乱码。

还得确认启动模式拨码开关的位置。ZYNQ的Boot Mode引脚是MIO[6:2](具体看型号有细微差别),通过拨码开关组合成不同模式。QSPI启动、SD启动、JTAG启动的标志组合各不一样,板子说明书上一般都有表。第一次调试时建议两种模式都用一下:SD卡启动方便反复修改镜像,JTAG模式则可以在线加载、看早期调试输出。

2.2 交叉编译工具链的安装

Linux运行在ARM核上,而编译在PC上完成,所以必须有交叉编译工具链。我用的版本是arm-linux-gnueabihf-,不需要从零源码编译,直接安装现成的就行:

sudo apt-get install gcc-arm-linux-gnueabihf

装完确认一下版本:

arm-linux-gnueabihf-gcc -v

如果看到gcc version 7.5.0或者类似版本号,就说明工具链可用了。这里为什么强调版本?因为编译内核时,如果GCC版本过新或过旧,可能导致某些内联汇编或语法检查出问题,编译报错很难查。我用Ubuntu 18.04或20.04自带的工具链编译Linux 4.x到5.x内核都没遇到问题,但如果你用太新的发行版(比如Ubuntu 24.04自带GCC 13),编译老内核(如Linux 3.x)时可能会遇到隐式函数声明之类的错误,那就得换用Linaro或ARM官方提供的固定版本工具链了。

建议把工具链路径加到PATH里,并在每次编译前都手动导出两个环境变量:

export ARCH=arm export CROSS_COMPILE=arm-linux-gnueabihf-

这两个变量不设的话,内核和U-Boot编译系统默认会去调用本机的x86编译器,结果就是海量报错。这个小坑我觉得新手必踩,先提前打了预防针。

2.3 源码获取:U-Boot、内核、设备树各从哪里来

说到源码,这里必须强调一个认知:ZYNQ的官方支持代码都维护在Xilinx的Git仓库里,不是上游Linux主线社区那份,虽然主线也支持ZYNQ,但Xilinx官方仓库包含了对自家板卡更完整的板级配置和BSP补丁。我一般直接clone Xilinx的仓库,然后切换到合适的标签版本。

U-Boot源码:

git clone https://github.com/Xilinx/u-boot-xlnx.git cd u-boot-xlnx git checkout xlnx_release_v2020.2

Linux内核源码:

git clone https://github.com/Xilinx/linux-xlnx.git cd linux-xlnx git checkout xlnx_release_v2020.2

版本号怎么选?我的原则是U-Boot和内核版本尽量和Vivado版本对应。比如你用Vivado 2020.2,那BSP相关的HDF文件里硬件描述格式和工具链预期是匹配的,U-Boot和内核用2020.2的发布版本兼容性最好。如果你非要混搭,比如用Vivado 2019.2生成的FSBL去加载2022.1的U-Boot,也不是完全不行,但容易遇到一些莫名其妙的问题,没必要给自己添堵。

设备树不用单独拿源码,它就在内核源码的arch/arm/boot/dts/目录下。Xilinx官方仓库里已经放了针对自家开发板的dts文件,比如zynq-zed.dts、zynq-zc702.dts这些。如果你的板子是第三方做的,往往需要自己修改DTS,这个后面细说。

3. 核心部件的编译与配置实操

3.1 FSBL生成:用Vivado SDK创建工程

FSBL一般不用自己写代码,而是通过Vivado SDK从硬件平台文件(HDF)里自动生成。步骤大致如下:

先用Vivado创建硬件工程,添加ZYNQ7 Processing System的IP核,配置好DDR型号、MIO分配、UART、SD等外设,然后生成bitstream并导出硬件,导出时会得到一个hdf文件。接着打开Xilinx SDK,用这个hdf文件创建硬件平台工程,再新建一个Application Project,模板里选FSBL,编译后就能得到fsbl.elf。

这个过程看着不复杂,但有一个细节很多人会忽略:FSBL工程生成时,BSP(板级支持包)里有一个xilffs和xilsecure的选项。默认情况下FSBL的xilffs配置中,SD卡读取的偏移地址等参数是按分区布局来设定的,如果你的SD卡第一个分区不是FAT32格式,或者U-Boot镜像不是存放在第一个分区的某个偏移处,FSBL可能读不到U-Boot。一般默认配置就行,但如果你发现FSBL能跑但找不到U-Boot,先检查分区格式,再检查FSBL的BSP配置。

另外,FSBL生成的elf文件只是中间产物,最终要把它和U-Boot一起打包成BOOT.BIN才真正可用。打包用的是bootgen工具,这个我放到后面统一讲。

3.2 U-Boot编译:defconfig选型与关键配置

U-Boot编译前面几步很简单:

cd u-boot-xlnx export ARCH=arm export CROSS_COMPILE=arm-linux-gnueabihf- make zynq_zc702_defconfig make -j4

但这里有个问题:你能用的defconfig未必和你的板卡完全匹配。Xilinx官方仓库里提供了很多板子的defconfig,比如zynq_zybo_defconfig、zynq_zc702_defconfig、zynq_zc706_defconfig等。选一个和你的板子最接近的,然后必须去检查编译生成的配置文件里的几个关键项。

首先是DDR配置。U-Boot中的DDR参数其实分为两部分:一部分在FSBL阶段就已经初始化过了,U-Boot启动时不会重新训练DDR,而是直接使用;但U-Boot中仍有一个内存大小的宏定义,比如CONFIG_NR_DRAM_BANKS、CONFIG_SYS_SDRAM_SIZE这些参数,定义了U-Boot眼中内存的起始地址和大小。如果你的板子是512MB的DDR3,但U-Boot配置里写了1GB,U-Boot会把部分地址空间当作内存用,访问到不存在的物理地址时就会卡死或者数据异常。所以这里一定要核对板子的实际DDR容量。

其次是串口配置。在zynq_common.h里可以看到CONFIG_SYS_BAUDRATE_TABLE等定义,默认波特率一般是115200,这个通常没问题。但UART编号要确认,默认配置用的是UART0,如果你的板子串口是接在UART1上,那就要改设备树里serial节点的alias,也要检查U-Boot的设备树(U-Boot也有一份自己的dts,放在arch/arm/dts/目录下)中serial0是否对应正确的UART控制器。串口接错这个事,症状很典型:开机后终端完全没有输出。我认识的朋友里好几个排查了一下午才发现是UART1接的串口但代码里用的UART0。

然后是启动参数,也就是bootcmd。你希望U-Boot从SD卡启动时执行什么命令,都写在这里。我们实测下来比较顺的启动命令序列是:

setenv bootcmd 'fatload mmc 0:1 0x3000000 uImage; fatload mmc 0:1 0x2000000 zynq.dtb; bootm 0x3000000 - 0x2000000'

解释一下这条命令干了什么:fatload从mmc设备的第0个分区的地址0x3000000处加载内核镜像uImage,从0x2000000处加载设备树dtb,然后bootm启动内核镜像,中间的“-”表示没有ramdisk。加载地址是ZYNQ中约定俗成的几个位置,0x2000000到0x3000000这段是给设备树和内核镜像用的,不冲突就行。

U-Boot编译完成后,在源码目录下会生成u-boot.elf,这个要和fsbl.elf一起打进BOOT.BIN。

3.3 内核编译:defconfig选型与设备树的修改

内核编译用的defconfig选择也很讲究。Xilinx仓库里有一个xilinx_zynq_defconfig,这个是覆盖通用ZYNQ平台的最小配置,编译出来的内核基本能启动、能访问串口和SD。建议编译之前先装好一些依赖:

sudo apt-get install u-boot-tools device-tree-compiler

u-boot-tools里包含了mkimage工具,这个是打包内核镜像格式必需的。然后进入内核源码目录,执行:

make xilinx_zynq_defconfig make -j4 uImage make -j4 dtbs

编译完成后,arch/arm/boot/uImage和arch/arm/boot/dts/zynq-*.dtb就是你要的两个产物。uImage是U-Boot可以识别和引导的内核镜像格式,它其实是在普通内核镜像前面加了一个64字节的头,包含镜像大小、加载地址、入口地址等信息,U-Boot的bootm命令就是靠这个头来解析内核的。这也是为什么不能用zImage直接替换uImage的原因。

设备树这一步,是你板子和官方板卡差异最大的地方。如果你的板子是ZedBoard或者ZC702这类的官方开发板,直接拷贝对应的dtb即可。如果是第三方板子,就得在官方dts基础上改。需要修改的地方通常有几个:DDR大小要改成实际容量,型号比如micron的MT41K256M16HA-125之类;PHY芯片节点的compatible属性和reg地址要看原理图上PHY的MDIO地址;还有网卡节点中phy-mode要确认是rgmii-id还是rgmii-txid,这会直接影响MAC与PHY之间的时钟相位,配错了网口要么不通要么丢包严重;SD卡节点要检查wp-gpios和cd-gpios是否存在,如果板子上没接写保护或者卡检测引脚,这两个属性反而会导致SD卡初始化失败,适当删掉就行。

设备树修改完之后编译,得到dtb文件。这里有个很容易被忽视的点:U-Boot在启动内核时用的dtb地址,必须和U-Boot加载dtb的地址一致,而且dtb文件的物理内存起始地址要求4字节对齐。如果地址不对或对齐不对,内核启动时会报“FDT address is illegal”之类的错误,卡在启动早期。

3.4 根文件系统:ramdisk方案与SD卡rootfs方案

Linux内核启动的最后一步是挂载根文件系统,然后执行/sbin/init。根文件系统可以从ramdisk加载,也可以从SD卡分区挂载。两种方案各有使用场景,我的建议是:前期调试验证用ramdisk,最终产品化用SD卡ext4分区。

ramdisk方案的制作思路是这样的:先准备一个基本的根文件系统目录,包含/bin、/sbin、/etc、/dev、/proc、/sys这些基本目录,再用BusyBox编译一个包含常用命令的hello world级工具集,最后把这个目录制作成归档、压缩、打包成U-Boot能识别的uramdisk.image.gz格式。BusyBox编译很简单:

make menuconfig # 选择静态编译,避免动态链接库依赖 make -j4 make install

装在目录里之后,执行以下命令把根文件系统打包:

find . | cpio -H newc -o > ../ramdisk.cpio gzip ../ramdisk.cpio mkimage -A arm -T ramdisk -C gzip -d ../ramdisk.cpio.gz ../uramdisk.image.gz

mkimage生成的uramdisk.image.gz可以直接让U-Boot通过bootm命令加载,然后内核把它解压到内存中作为rootfs使用。ramdisk方案的优点是SD卡上只需要一个FAT分区就够了,BOOT.BIN、uImage、dtb、uramdisk.image.gz全都放第一个分区,简单粗暴,而且文件系统完全是内存形态,读写不会磨损SD卡,也比较抗断电。缺点也很明显:所有修改重启就丢了,不适合做实际应用。

SD卡rootfs方案要稍微多一步,需要把SD分成两个分区:第一个分区是FAT32,大小建议512MB到1GB,放BOOT.BIN、uImage、dtb;第二个分区用ext4,作为Linux的根文件系统。你可以在Ubuntu虚拟机上用debootstrap或者直接解压一个现成的Ubuntu rootfs到第二个分区。我自己更快的方法是用一个现成的armhf架构的rootfs tar包:

sudo tar -xzf ubuntu-base-armhf.tar.gz -C /media/sd/rootfs

然后设置挂载需要的/dev、/proc、/sys这些虚拟文件系统的挂载点,chroot进去装上一些基础软件、设置默认账号密码、配置网络接口文件,再把U-Boot的bootargs改成:

setenv bootargs 'console=ttyPS0,115200 root=/dev/mmcblk0p2 rw rootfstype=ext4 earlyprintk'

这里面的root=/dev/mmcblk0p2是关键,它告诉内核根文件系统在SD卡的第二个分区。如果你用的是SD0接口,设备名就是mmcblk0;如果是SD1,那就是mmcblk1。搞混了这个,内核会报“VFS: Unable to mount root fs”,然后死循环panic。

4. 镜像打包与烧写详细步骤

4.1 bootgen制作BOOT.BIN

BOOT.BIN实际上是一个容器格式,BootROM从启动介质里最开始读到的就是它。它里面包含了FSBL,也可以包含U-Boot的elf、bitstream。制作BOOT.BIN用Xilinx的bootgen工具,Vivado安装目录下就自带。先写一个bif文件,比如叫boot.bif:

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

如果PL端有FPGA逻辑,还需要在fsbl后面加上bitstream文件:

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

然后执行:

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

生成的BOOT.BIN就包含了完整的引导链。这里有个概念要理清:BOOT.BIN里放的U-Boot是elf格式的,U-Boot.elf内部包含了U-Boot的代码段和数据段,FSBL在启动时会解析这个elf,按段加载到对应内存地址。所以打包这一步一定要用elf,而不是用编译出来的u-boot.bin裸二进制。

4.2 SD卡分区与镜像拷贝

用fdisk或者GParted把SD卡分成两个分区。我实测下来用命令行最可控,假设SD卡在/dev/sdb,操作大概是:

sudo fdisk /dev/sdb

fdisk交互界面里依次执行:删除已有分区(d),新建第一个分区(n)选主分区、大小设1GB、类型设为0x0c(FAT32 LBA),新建第二个分区(n)大小用剩余空间、类型默认Linux(0x83),最后w保存退出。然后格式化:

sudo mkfs.vfat -F 32 /dev/sdb1 sudo mkfs.ext4 /dev/sdb2

把BOOT.BIN、uImage、zynq.dtb拷贝到第一个分区的根目录,rootfs解压到第二个分区。最后用sync命令确保写盘完成。

如果你用的是ramdisk方案,第一个分区里还要加上uramdisk.image.gz,并且U-Boot的bootcmd改一下,需要用bootm的三个参数的写法:

setenv bootcmd 'fatload mmc 0:1 0x3000000 uImage; fatload mmc 0:1 0x2000000 zynq.dtb; fatload mmc 0:1 0x2000000 uramdisk.image.gz; bootm 0x3000000 0x2000000 0x2000000'

这里三个参数分别对应内核镜像地址、ramdisk地址、dtb地址。注意我前面对调试启动时的命令写的bootm后只有两个参数,中间用“-”跳过了ramdisk,如果要用ramdisk启动就要把第2个参数的实际地址写出来,而不是用“-”占位。

4.3 启动模式设置与串口终端连接

镜像文件和启动介质都准备好后,检查一下板子的启动模式是否设成了SD启动。如果是SD启动,拨码开关对应的电平组合可以参考板卡手册。连接串口终端时,参数设置为115200、8N1(8个数据位、无校验、1个停止位)。Windows下我用MobaXterm或者SecureCRT,Linux下我用minicom或者screen:

sudo minicom -D /dev/ttyUSB0 -b 115200

通电上电,终端应该能看到从BootROM、FSBL到U-Boot的整个启动日志。如果你的板卡串口完全没输出,先怀疑串口连接和UART编号,别急着往软件方向排查。如果能看到FSBL的打印但卡在某一步,那就按下一节的排查清单逐项核对。

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

老实说我第一次移植的时候,串口输出和信息不匹配,光是排查环境就花了两个晚上。这里我把踩过的坑按启动阶段整理成一个速查表,方便你定位问题。

现象可能原因排查方法
上电后完全没有串口输出启动模式不对、串口接错UART、FSBL没有生成成功确认拨码开关是SD启动;确认板卡原理图中串口接到UART0还是UART1;重编FSBL并检查U-Boot的串口配置
卡在FSBL输出之后,U-Boot死活不启动BOOT.BIN里的U-Boot偏移地址不对、SD卡分区格式不对确认FAT32格式且在第一个分区;确认BOOT.BIN内的镜像顺序;用JTAG模式加载U-Boot测试
U-Boot能启动,但执行bootm后卡在“Starting kernel...”内核镜像地址或设备树地址错误、内核配置缺失确认加载地址与bootm参数一致;确认uImage格式正确;用bootm 0x3000000 - 0x2000000时设备树要能被识别
内核启动报错,提示无法挂载根文件系统bootargs中root参数错误、rootfs分区不存在或格式不对在U-Boot中执行printenv bootargs确认参数;确认SD分区编号是mmcblk0p2而不是p1
内核起来后终端无登录提示/dev目录下缺少console节点、init程序不存在检查rootfs中是否包含/dev/console节点;确认/sbin/init存在或busybox配成了默认init
网口ping不通PHY芯片配置不对、设备树里phy节点错误确认网线插的是PS端的GEM接口;在U-Boot中执行mdio read读PHY寄存器确认链路;在Linux中ifconfig确认MAC地址
DDR容量显示不对U-Boot或设备树中内存配置错误用U-Boot命令bdinfo查看内存大小;在设备树memory节点中修正reg属性

5.1 启动早期无输出的排查思路

无输出是最磨人的问题,因为你能获取的信息最少。我的排查路径是:先确认硬件连接没问题(串口线TXD/RXD有没有接反、电平转换芯片是否正常),再确认启动模式,然后尝试用JTAG加载FSBL。JTAG有一个好处是可以通过Vivado的XSCT工具在PC侧读取寄存器状态,比如可以查0xFF000004这个寄存器里的BOOT_MODE值,确认BootROM确实读到了SD启动的信号。如果JTAG能看到时钟和复位正常,再怀疑FSBL本身。

有时候问题出在DDR初始化上。FSBL里DDR参数是根据HDF生成的,如果HDF里DDR型号本来就选错了,或者DDR电源没上电,FSBL会卡在DDR init的循环里,表现为FSBL打印完“DDR Init Start”后就再也没动静。这种情况只能回头检查Vivado工程里的DDR配置,看看厂商、容量、位宽、速率是否和板子实际颗粒一致。

5.2 U-Boot阶段常见问题的调试方法

U-Boot阶段出了问题,其实比早期阶段好查多了,因为U-Boot自带了一个相当好用的调试接口——串口命令行。如果U-Boot能起来,哪怕bootcmd执行失败了,你也能在串口终端里敲命令手动操作。比如bootcmd里那串fatload命令如果因为文件名大小写不对(Linux的FAT驱动默认区分大小写)而失败,你可以手动敲一遍,看输出的具体错误信息。

还有一个我特别喜欢用的U-Boot命令是bdi(board info),它会打印当前U-Boot识别的内存布局、时钟频率、启动设备等信息。启动异常时先执行bdi,看看DDR大小是不是预期的,U-Boot版本是不是你要的那个,很多时候能直接定位到配置层面。

如果U-Boot能加载内核,但bootm执行后立即卡死,一个常见原因是内核镜像被拷到DDR里的地址和内核编译时的加载地址不一致。mkimage默认的U-Boot镜像头里记录的入口地址,是以编译内核时CONFIG_ZBOOT_ROM等配置为准的。我们手工加载地址用的是0x3000000,如果U-Boot的配置里默认加载地址不是这个,或者你在bootm命令里指定的地址和编译时约定的地址不同,内核解压时会出现异常。解决方法是统一地址,习惯上大家都用0x3000000,你就固定在打包、拷贝、bootm三个环节都用0x3000000,不要临时改。

5.3 Linux内核启动阶段的问题定位技巧

内核起来后,大部分问题都集中在设备树和外设驱动上。串口有内核日志输出的话,优先看崩溃前最后几行。比如如果看到“No working init found. Try passing init= option to kernel”,说明内核起来了但找不到init进程,多半是rootfs里/init不存在或不可执行。

内存节点不匹配的典型报错是“Kernel panic - not syncing: Out of memory and no killable processes”,这个经常是DDR大小配置写错了。设备树里memory节点要填实际的物理内存范围,比如:

memory@0 { device_type = "memory"; reg = <0x0 0x40000000>; };

这里0x40000000就是1GB。如果你板子只有512MB而这里写了1GB,内核会把不存在的内存当作可用内存,访问时触发data abort,直接panic。

如果根文件系统挂载成功,但网络起不来,很多情况下不是内核问题,而是PHY没有正确复位。你可以先看看PHY的复位引脚是怎么接的——有的板子上PHY的复位接到了PS端的MIO引脚,需要在设备树里加一个gpio控制复位时序;也有板子是靠上电复位,那就得确认PHY的供电时序没问题。内核失败的地方通常打印“PHY reset timed out”,看到这个词基本就可以去查设备树里phy节点和复位引脚配置了。

5.4 一个必须掌握的通用调试手段

最后再说一个我每次调系统必用的手段:给内核加earlyprintk参数。在U-Boot的bootargs里加上earlyprintk,内核非常早期的调试信息就能通过串口打印出来,尤其是那些发生在标准console初始化之前的输出。这个参数在排查内核根本起不来的情况时几乎是唯一线索。

比如你把bootargs设置成:

setenv bootargs 'console=ttyPS0,115200 root=/dev/mmcblk0p2 rw rootfstype=ext4 earlyprintk'

保存后重启(setenv之后再saveenv存到环境变量里,否则重启后又回到默认值),就能看到内核在被console占用之前打印的早期信息。如果这些信息都看不到,那问题就不在内核本身了,而是U-Boot到内核的跳转环节出了问题——地址不一致、镜像格式错误、设备树地址非法等,回头去查U-Boot阶段的事。

传统方式移植Linux,说难也难,说不难也不难。难在涉及的工具链、配置项、启动角色太多,链路上任何一点不匹配都会导致静默失败;不难在于每个环节其实都是固化套路,只要能静下心来把原理弄懂,跟着文档和这篇流程走,一步一步验证,很快就能跑通。我个人其实建议所有做ZYNQ的人,哪怕以后要用PetaLinux做量产,也务必手动移植一次。这个经历会给你带来真正意义上的掌控感,后续遇到再复杂的启动问题,你脑子里始终是有一个完整的链路地图的,排查起来就有的放矢。