news 2026/9/30 1:16:05

ZYNQ传统方式移植Linux:从FSBL到根文件系统的完整启动链

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ZYNQ传统方式移植Linux:从FSBL到根文件系统的完整启动链

ZYNQ跑系统 系列(一) 传统方式移植linux

拿到一块ZYNQ开发板,很多人第一件事就是想把Linux跑起来。但真上手之后会发现,网上教程要么直接甩给你一个PetaLinux的现成镜像,要么就是把Vivado、SDK、设备树、U-Boot这些词堆在一起,看得一头雾水。这篇系列文章第一篇,我用传统手工方式把Linux搬到ZYNQ上——不依赖PetaLinux自动生成,从FSBL到U-Boot再到内核和根文件系统,每一步都手动编译、手动打包、手动烧写。这种方式虽然笨,但能让你真正看清ZYNQ启动的每个环节,后续不管是调驱动、改启动方式还是做双核应用,心里都有底。

先说明一下,ZYNQ的架构和普通ARM单片机最大的不同,在于它内部有PS(Processor System,即ARM Cortex-A9双核)和PL(Programmable Logic,即FPGA逻辑)两个世界。Linux跑在PS上,但启动过程要先经过BootROM、FSBL,再由U-Boot引导内核,任何一个环节出错都起不来。所以本文的“移植”,本质上是把启动链条上的每一段程序都手动构建一遍,最终得到一个能从SD卡启动的完整Linux系统。

1. 动手前先搞清楚:ZYNQ的启动架构与传统移植的本质

1.1 从BootROM到Linux的完整启动链条

ZYNQ上电后的启动过程,很多人第一次接触时容易懵,因为它不像STM32那样直接从Flash里取代码执行。ZYNQ内部有一个固化的BootROM程序,上电后它先根据模式引脚(MIO[5:4])的电平,决定从哪类介质读取启动镜像——QSPI Flash、SD卡、JTAG还是NAND。BootROM本身很小,它只负责把启动镜像的头几个字节读进来做校验,然后跳转执行。

所谓的启动镜像,就是BOOT.bin。这个文件里面至少包含两个东西:FSBL(First Stage Boot Loader)和U-Boot。FSBL是芯片厂商固定的第一阶段引导程序,它的核心职责有三个:初始化DDR控制器、初始化PS端的时钟(PLL)、把第二阶段引导程序(也就是U-Boot)从启动介质拷贝到DDR里,然后跳转过去。

U-Boot起来之后,它会读取设备树(dtb文件)和内核镜像(uImage或zImage),把内核加载到DDR指定地址,设置好启动参数,最终跳到内核入口。内核再挂载根文件系统,执行init进程,这才算“跑起了Linux”。

这条链路上的每一步,都对应一个需要你手工编译或者生成的产物。如果只做应用开发,PetaLinux一条命令全包了;但如果你想知道“系统是怎么长出来的”,就必须把每个产物单独拿出来研究。

1.2 传统手工方式 vs PetaLinux自动流程

很多初学者一开始就投入PetaLinux的怀抱,但PetaLinux本质上是个自动构建工具,它把硬件描述文件(hdf)、FSBL、U-Boot、内核、设备树甚至根文件系统一股脑打包成镜像。好处是快,坏处是你不知道里面发生了什么。

对比一下两种方式的差异,你就明白为什么要学传统方式了:

对比项PetaLinux自动方式传统手工方式
FSBL工具自动生成SDK/XSCT里手动生成
U-Boot工具自动拉取并配置手动下载源码、改配置、交叉编译
内核自动裁剪配置手动make menuconfig选驱动
设备树根据hdf自动生成手动编写并编译
根文件系统工具自带或自动构建手动用BusyBox搭建
可定制性受限于工具的配置项任意环节可深度定制
学习收益基本学不到原理每一步都清楚

我在实际项目中见过不少用PetaLinux的工程师,遇到启动失败时完全没有排查思路,因为镜像是一个黑盒。传统方式里每个文件都是你自己生成的,哪个环节出问题,日志一出来你基本能猜到是哪一步。

1.3 传统移植的交付物清单

在开始动手之前,先把目标产物列清楚。本文最终要得到三个独立的东西:

  • BOOT.bin:包含FSBL和U-Boot,放在SD卡FAT分区里;
  • uImage或zImage:Linux内核压缩镜像,放在FAT分区;
  • devicetree.dtb:设备树编译产物,放在FAT分区;
  • 根文件系统:可以做成ext4分区里的目录,也可以做成ramdisk镜像。

如果你的板子和常用的黑金、米联客、正点原子ZYNQ开发板布局相似,那么SD卡FAT分区启动是最省事的调试方式——不用反复烧写Flash,文件直接拷进卡里就能改。本文就以SD卡启动为例来操作。

2. 交叉编译环境:版本匹配是第一个大坑

2.1 工具链选择与安装

ZYNQ的PS端是ARM Cortex-A9,32位处理器,所以我们需要一个arm架构的交叉编译工具链。常见选择是arm-linux-gnueabihf-(支持硬件浮点)或者arm-none-eabi-(裸机用)。跑Linux必须用前者,因为内核和用户态程序都依赖glibc之类的C库。

这里有个容易踩的坑:工具链的版本和你要编译的内核版本、U-Boot版本之间存在匹配关系。太老的工具链(比如gcc 4.x)编译新版内核可能会报错,太新的工具链(比如gcc 12.x)也可能因为内核代码里的老旧语法产生warning甚至error。我个人的推荐组合是:U-Boot 2020.04、Linux 4.19或5.4、gcc 7.x或8.x。这套组合在ZYNQ上非常成熟,网上资料多,踩坑也少。

以Ubuntu 18.04/20.04为例,直接安装:

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

装完验证一下:

arm-linux-gnueabihf-gcc --version

如果系统自带的是gcc 9.x也没关系,编译内核时可以接受少量warning,只要没有致命error就行。但如果你是处女座性格,想完全复现我这边零告警的环境,可以直接到ARM官网下载gcc-arm-8.3-2019.03-x86_64-arm-linux-gnueabihf这个版本,解压后把bin目录加入PATH即可。

2.2 环境变量与目录规划

传统移植过程中会反复用到交叉编译命令,每次都写全路径很烦。这里建议把所有环境变量写进一个脚本,每次打开终端先source一下:

export CROSS_COMPILE=arm-linux-gnueabihf- export ARCH=arm export UBOOT_DIR=$HOME/zynq/uboot export KERNEL_DIR=$HOME/zynq/kernel export ROOTFS_DIR=$HOME/zynq/rootfs

另外,交叉编译时有个老生常谈但很多人真会犯的错:CROSS_COMPILE和ARCH这两个变量缺一不可。ARCH=arm告诉编译系统内核面向ARM架构,CROSS_COMPILE告诉它用哪个前缀的编译器。少了任何一个,要么编译出x86架构的镜像,要么调用本机gcc导致一堆无法链接的错误。

我把所有源码放在$HOME/zynq目录下,大家也可以这么规划:

zynq/ ├── uboot/ # U-Boot源码 ├── kernel/ # Linux内核源码 ├── rootfs/ # 根文件系统工作区 ├── outputs/ # 最终产物:BOOT.bin、uImage、dtb等 └── sdk/ # Xilinx SDK或Vitis安装目录

规划目录的意义在于,后续做多个版本迭代时,你永远知道哪个文件在哪里,不会出现“改了源码但没编译进去”“编译了但拷错镜像”这种低级事故。

2.3 硬件导出文件(hdf)的准备

传统方式虽然不用PetaLinux,但依然需要Vivado生成硬件描述文件。因为在ZYNQ上,PS的外设配置(比如DDR型号、MIO分配、UART接口)是由硬件工程决定的,U-Boot的配置、设备树里的节点,都要和这个硬件描述保持一致。

操作步骤是这样的:

  1. 在Vivado里搭建好最小系统:ZYNQ PS核、DDR、UART1(一般是MIO48/49对应开发板上的USB转串口);
  2. 如果需要PL逻辑,把PL的约束和比特流生成好;
  3. File -> Export -> Export Hardware,勾选Include bitstream(如果PL有逻辑),生成一个.hdf文件。

这个hdf文件在PetaLinux里是输入,但在传统方式里我们主要用它来获取两样东西:FSBL工程需要的硬件信息,以及设备树源文件的生成依据。如果把PL比作一块画布上已经画好的图案,那PS就是画布本身,hdf就是这张画布的详细规格书——尺寸、材质、哪些区域能画画,都写在里面了。

拿到hdf后,其实可以用一个叫xsct的命令行工具从中提取FSBL源码和设备树,这是Xilinx SDK底层做的事情。我们接下来看FSBL怎么生成。

3. 从FSBL到U-Boot:第一段代码如何跑起来

3.1 用XSCT生成FSBL

FSBL虽然是一个固定流程的引导程序,但它不是一成不变的二进制。不同板卡的DDR型号、时钟频率都不同,所以FSBL需要根据你的硬件配置重新编译一次。

打开Xilinx SDK(现在叫Vitis),新建一个Application Project,硬件平台选择刚才导出的hdf文件。模板里专门有一个Zynq FSBL的选项,直接选它,然后Build。编译出来的文件叫fsbl.elf。

如果用命令行操作更顺手,可以这么干:

xsct % hsi open_hw_design system.hdf % hsi generate_app -dir ./fsbl_src -app zynq_fsbl % hsi close_hw_design cd fsbl_src make

编译完成后会得到一个fsbl.elf。这个文件需要和U-Boot一起打包进BOOT.bin。

这里有个小知识点:FSBL的源码实际上是一套汇编加C的混合工程,核心逻辑在fsbl_handoff.S、fsbl_main.c这些文件里。它初始化DDR的时序参数,在有些官方BSP里甚至能看出来它如何搬运U-Boot到指定地址。如果你后续想深入研究启动流程,直接把FSBL源码打开通读一遍比看任何教程都有效。

3.2 U-Boot源码获取与板级配置

U-Boot的官方代码仓库在https://github.com/u-boot/u-boot,但针对ZYNQ,我建议直接用Xilinx维护的分支,它是基于上游U-Boot加了很多ZYNQ特有补丁。拉取命令:

git clone https://github.com/Xilinx/u-boot-xlnx.git -b xilinx-v2020.2

这里有个痛点——不同分支对应不同Vivado版本,选不好会出现编译错误。如果你用的Vivado 2019.1,那U-Boot选xilinx-v2019.1分支最稳;用2020.2版本就选xilinx-v2020.2。版本号对齐能省掉很多莫名的兼容性问题。

进入U-Boot目录后,先配置ZYNQ的板级config:

make zynq_zc706_defconfig

如果你的开发板不是ZC706而是其他厂商的(比如黑金ZYNQ开发板、正点原子启明星),大概率也有对应的defconfig;实在找不到,用make zynq_common_defconfig,然后通过menuconfig微调。

一个很多新手会犯的错:只跑make zynq_zc706_defconfig就急着make,结果编译出来一个默认配置的U-Boot,DDR容量不对、外设没开,启动到一半就卡死。所以编译前一定要进menuconfig检查:

make menuconfig

重点检查两个地方:

  • Device Tree Control,确保U-Boot自带设备树并且选中你的板型;
  • Serial配置,默认ZYNQ的UART地址是0xE0001000,对应UART1,如果开发板用的是UART0就要在设备树里改。

然后开始编译:

make -j4

编译产物是u-boot.elf,有的配置也会生成u-boot.bin。打包BOOT.bin时我们需要的是u-boot.elf。

3.3 制作并验证BOOT.bin

BOOT.bin的生成工具是Xilinx的bootgen,可以从SDK安装目录里找到,也可以直接用Xilinx SDK的图形界面。最常见的方式是在SDK里双击创建Zynq Boot Image,把fsbl.elf和u-boot.elf加进去,设置分区属性:FSBL对应的partition是bootloader,U-Boot对应partition的启动地址填0x8000000(ZYNQ约定U-Boot加载到DDR的这个位置)。

命令行的bootgen方式也分享一下,方便脚本化重复构建:

bootgen -image boot.bif -o BOOT.bin

其中boot.bif内容如下:

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

生成BOOT.bin后,把它拷到SD卡FAT分区,插上开发板,设置启动模式为SD启动,打开串口终端(波特率115200),上电后如果能看到U-Boot的启动Logo和命令行提示符,那说明第一个里程碑完成了。

串口完全无输出的排查方法我放到后面专门章节,这里先继续往下走。在做内核之前,建议先把U-Boot环境变量和启动参数弄清楚,因为U-Boot的bootcmd决定了它怎么去加载内核。

4. 内核与设备树:传统移植的核心环节

4.1 内核源码选择与裁剪思路

Linux内核的源码仓库极大,但ZYNQ相关的代码集中在一块。和U-Boot一样,我推荐用Xilinx维护的内核分支:

git clone https://github.com/Xilinx/linux-xlnx.git -b xlnx_rebase_v4.19

或者直接用LTS主线内核4.19或5.4,原因之前说过:ZYNQ的BSP支持它们最完善。

内核编译的第一步不是直接make,而是先选配置。ZYNQ的in-tree默认配置文件在arch/arm/configs/xilinx_zynq_defconfig:

make xilinx_zynq_defconfig

但这仅仅是起点。因为这个defconfig里开了很多你可能用不到的驱动,也关了一些你需要的驱动。此时跑一遍内核编译,产物可以启动,但你会发现镜像很大,编译时间很长,部分外设不受控。所以生产级项目一定会做进一步裁剪。

我的习惯是执行完defconfig之后,直接make menuconfig做以下几项操作:

  • 关掉不需要的网络协议栈特性(如果项目不跑复杂网络服务);
  • 确认开启UART驱动:Device Drivers -> Character devices -> Serial drivers -> Xilinx UART/UARTLITE controller support;
  • 确认开启SD卡驱动:Device Drivers -> MMC/SD/SDIO card support -> Xilinx Zynq MMC controller;
  • 文件系统支持:把ext4编译进内核(File systems -> Ext4),如果根文件系统是ramdisk,需要开启initramfs支持;
  • 确认开启CONFIG_ROOT_NFS(如果网络调试需要)。

裁剪的核心逻辑是:让内核只包含你硬件上确实存在的设备驱动,少一个不必要的模块,就少一份启动出错的可能。

内核配置是门细活,你可以先开个比较全的配置把系统跑通,再逐步裁剪。不要一上来就追求极简——你还不知道哪些是你开发板上的“标配”驱动缺失会直接导致启动失败。

4.2 编译uImage的完整命令

配置完成后,编译命令如下:

make -j4 UIMAGE_LOADADDR=0x8000 uImage LOADADDR=0x8000

这里必须指定UIMAGE_LOADADDR=0x8000,因为ZYNQ里U-Boot把内核加载到DDR地址0x2080000附近,而uImage头里有自解压地址信息,如果内核镜像实际加载地址和自解压地址对不上,内核会起不来或者跑到一半崩掉。这个0x8000实际是内核在DDR里的相对偏移,配合U-Boot的bootm 0x2080000使用。

编译正常的情况下,arch/arm/boot/uImage会生成,拷贝到SD卡前先看一下大小:

ls -lh arch/arm/boot/uImage

一般裁剪后的ZYNQ内核镜像在3MB到6MB之间。如果超过8MB,说明你没用的驱动开太多了,建议回头重新裁剪。

4.3 设备树:ZYNQ硬件和Linux内核之间的翻译官

设备树是整个传统移植里最容易让人一头雾水的东西。它的作用很简单:用一棵描述性树状结构,把硬件信息告诉内核。比如DDR大小、UART基地址、中断号、GPIO控制器等。

获取设备树源文件(dts)通常有两种途径:

  1. 在SDK里展开hdf时,有一个子目录包含生成的pl.dtsi和system.dts,这个最准确,因为它是Vivado根据你的硬件设计自动生成的;
  2. 手写。如果你不熟悉PS的寄存器地址,手写很容易写错,不建议刚开始就这么干。

拿到dts后,用设备树编译器编译:

dtc -I dts -O dtb -o devicetree.dtb system.dts

如果你的dts文件里有#include之类的C语言预处理指令,要先过一遍cpp:

cpp -nostdinc -I include -undef -x assembler-with-cpp system.dts system.dts.preprocessed dtc -I dts -O dtb -o devicetree.dtb system.dts.preprocessed

注意ZYNQ开发板的设备树中,最关键的一个节点是memory节点——它的reg属性要和你DDR实际容量匹配。如果你的板子是512MB DDR,但设备树里写的1GB,内核启动后肯定panic,因为U-Boot传递的r0-r3参数和DDR初始化不一致。这个我在后面的故障排查里会再提。

4.4 内核启动进程与总线初始化顺序

设备树编译好之后,值得花几分钟理解一下内核启动过程中设备树是怎么被消费的。内核启动早期,在setup_arch()阶段就会解析设备树,建立struct device_node的树形结构。总线驱动注册时,比如amba_pl_platform_driver(ZYNQ特有的AMBA总线),会遍历设备树里的节点,把节点匹配到的device和driver做绑定。

所以你在设备树里写了一个GPIO的子节点,Linux启动后/sys/class/gpio下面就会多出一个gpiochip。这也是为什么有些开发板教程里“改设备树就像改硬件一样”,因为你实际上是在告诉内核“这里有这么个硬件”。

有一个容易被忽略的点:ZYNQ的PL侧中断,设备树里如果配了interrupt-parent指向intc,但PS端对应的中断号是固定的(比如PL到PS的中断是ID 61),写错就收不到PL传来的中断。这些细节,后续系列文章我们单独讲PL驱动时再展开。

5. 最小根文件系统与BusyBox:让Linux真正“跑起来”

5.1 先理解根文件系统在Linux里的角色

内核起来了,但内核最后一步一定是找根文件系统。所谓根文件系统,就是一个包含/bin、/etc、/dev、/proc、/sys、/tmp、/usr、/var这些目录的完整目录树,里面放着用户态的程序、库、配置文件和设备节点。

内核启动时是根据bootargs里的root=参数来找根文件系统的。root=/dev/mmcblk0p2表示挂载SD卡第二个分区;root=/dev/ram0表示挂载一个内存中的ramdisk。

如果你把根文件系统做到SD卡的ext4分区,那么内核、U-Boot都放FAT分区,根文件系统放ext4分区,这是最常用的生产级布局。因为ext4支持日志、权限,可以在系统运行中修改文件。

5.2 交叉编译BusyBox

最小根文件系统不用像发行版那样自带整套GNU工具链,一个BusyBox就搞定了一百多个常用命令。BusyBox的策略是“一个二进制提供所有命令”,通过符号链接/bin/ls -> /bin/busybox来区分调用哪个功能。

下载BusyBox源码:

git clone https://git.busybox.net/busybox cd busybox make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- defconfig make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig

menuconfig里有一个必须开的选项:BusyBox Settings -> Build Options -> Build static binary (no shared libs)。静态链接意味着BusyBox不依赖任何动态库文件和glibc,拷贝到任何地方都能直接执行,这对于嵌入式的根文件系统非常关键——因为你还没配置库文件路径呢,动态链接很容易出现“找不到libc.so.6”的尴尬。

编译安装到一个临时目录:

make install CONFIG_PREFIX=$HOME/zynq/rootfs

这条命令会在rootfs目录下生成bin、sbin、usr目录,里面全是BusyBox的符号链接。

5.3 搭建init进程与inittab的关系

光有BusyBox还不够,Linux内核启动后第一个用户态进程是/sbin/init,这个init进程负责读取/etc/inittab,启动系统初始化脚本和服务。所以根文件系统里要创建etc/inittab文件。

最小化的inittab长这样:

::sysinit:/etc/init.d/rcS console::askfirst:-/bin/sh

第一行表示系统启动时先执行/etc/init.d/rcS脚本;第二行表示在console设备上启动一个交互Shell。askfirst的作用是每次启动时询问“Press Enter to activate this console”。

rcS脚本是初始化脚本,至少要做这么几件事:

#!/bin/sh mount -t proc proc /proc mount -t sysfs sysfs /sys mount -t tmpfs tmpfs /tmp mkdir -p /dev/pts mount -t devpts devpts /dev/pts mknod /dev/console c 5 1 2>/dev/null mknod /dev/null c 1 3 2>/dev/null

注意一定保留mount /proc和mount /sys这两行,否则ps命令和部分驱动接口根本用不了,很多程序会直接报错。

创建完rcS后记得加执行权限:

chmod +x $ROOTFS_DIR/etc/init.d/rcS

5.4 ramdisk方式与ext4方式的取舍

根文件系统到底用哪一种启动方式,我建议存储空间够就上ext4分区,原因很简单:修改起来方便,掉电不丢数据。但如果你想把系统做成只读启动,或者想把它和内核打包成一个镜像分发给别人,ramdisk方式更合适。

ramdisk的制作也很简单,把rootfs目录打成镜像:

dd if=/dev/zero of=ramdisk.image bs=1M count=16 mkfs.ext4 ramdisk.image mkdir /mnt/ramdisk mount -o loop ramdisk.image /mnt/ramdisk cp -a $ROOTFS_DIR/* /mnt/ramdisk/ umount /mnt/ramdisk gzip -9 ramdisk.image

制作ramdisk时有个坑:U-Boot的bootm ramdisk命令要求ramdisk镜像加载到DDR地址0x4000000(64MB处),并且在bootargs里要带rdinit=/sbin/init。如果加载地址和根文件系统预期不一致,内核会直接panic,日志会提示“RAMDISK: incomplete write”或者类似信息。

对我来说,调试Linux系统最顺手的还是ext4分区,因为任何文件都可以直接在开发板上改、重启生效。ramdisk在需要只读出厂配置或批量部署场景再考虑。

6. 打包、烧写与启动调试:串口日志里看门道

6.1 SD卡分区方案与文件摆放

拿到SD卡后,建议用GParted或fdisk分成两个分区:

  • 第一个分区:FAT32,大小256MB到512MB,存放BOOT.bin、uImage、devicetree.dtb;
  • 第二个分区:ext4,剩余全部空间,存放根文件系统。

分区命令示例:

sudo fdisk /dev/sdb # 创建第一个分区,类型c(FAT32 LBA) # 创建第二个分区,类型83(Linux) sudo mkfs.vfat /dev/sdb1 sudo mkfs.ext4 /dev/sdb2

SD卡做好后,把根文件系统内容拷贝进ext4分区:

sudo mount /dev/sdb2 /mnt/sd sudo cp -a $ROOTFS_DIR/* /mnt/sd/ sudo umount /mnt/sd

FAT分区里的BOOT.bin、uImage、devicetree.dtb,直接拷贝即可。在Windows下往卡里拷文件时,注意SD卡启动模式下不识别长文件名和特殊字符,最好统一用小写字母命名。

6.2 启动参数(bootargs)的完整配置与含义

U-Boot环境变量bootargs是决定内核启动行为的关键,传统方式里这个参数用setenv手动设置。我常用的完整配置:

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

逐段解释一下:

  • console=ttyPS0,115200:告诉内核用哪个串口作为控制台。ttyPS0对应PS端的UART0,如果你的U-Boot初始化的是UART1,这里要改成ttyPS1;
  • root=/dev/mmcblk0p2:根文件系统在SD卡的第二个分区。如果SD卡被识别成mmcblk1(有些ZYNQ板子SD控制器挂在SD0上),就要改成mmcblk1p2;
  • rw:根文件系统以读写方式挂载;
  • rootwait:等待SD卡设备节点出现。这个参数一定要有,因为SD卡驱动初始化和内核挂载根文件系统是异步的,不加rootwait有时会因为设备节点还没生成而挂载失败;
  • earlyprintk:在内核早期启动阶段就输出日志。如果内核死在早期,大概率要靠它才能看到最后一句打印。

U-Boot还要设置好加载命令bootcmd,手动输入也可以:

fatload mmc 0:1 0x2080000 uImage fatload mmc 0:1 0x2000000 devicetree.dtb bootm 0x2080000 - 0x2000000

这条命令的含义是:从mmc设备0的第一个分区,把uImage读到0x2080000,把设备树读到0x2000000,然后bootm使用这两个地址启动内核。注意bootm参数中间有个-,表示没有ramdisk,直接启动。

如果你把根文件系统做成了ramdisk,那就需要加载第三个文件:

fatload mmc 0:1 0x4000000 ramdisk.image.gz bootm 0x2080000 0x4000000 0x2000000

这些启动命令第一次配好之后,用saveenv保存到U-Boot的环境分区,后续重启会自动执行bootcmd,就不用每次敲了。

6.3 常见启动故障的完整排查链路

这部分是最有价值的实战经验汇总,我按故障现象从“完全没输出”到“内核panic”逐条梳理。

故障一:串口完全无输出

排查路径:

  1. 检查开发板启动模式跳线,是否真的拨到了SD启动;
  2. 检查串口波特率,ZYNQ的U-Boot默认115200,但有的开发板用了57600,先盲试几种常见波特率;
  3. 拔掉SD卡,如果U-Boot坏了,串口会有BootROM的“U-Boot SPL”之类信息或者完全没有;
  4. 确认BOOT.bin是否真的生成了,fsbl.elf是否编译成功。

如果BootROM阶段都没有输出,而JTAG连接正常,往往是因为FSBL没有正确生成,或者BOOT.bin内的分区顺序不对——FSBL必须是第一个分区且属性为bootloader。

故障二:U-Boot起来了,但bootcmd报找不到uImage

排查路径:

  1. ls mmc 0:1看看FAT分区里到底是什么文件,可能是文件名拼写错误;
  2. 确认是不是分区类型不是FAT而是其他格式;
  3. mmcinfo确认U-Boot识别到的mmc设备枚举号,有的是0有的是1。

故障三:内核加载后启动到一半panic,日志停在“Kernel panic - not syncing: No init found”

这是最常见的错误。原因几乎一定是根文件系统没挂上。继续看日志,如果你能看到“VFS: Cannot open root device mmcblk0p2”,那大概率是root参数里的分区号不对,或者这个分区类型格式不对。检查一下SD卡是不是一个FAT一个ext4,同时确认是否加了rootwait。

故障四:死机在“Starting kernel ...”之后,只是省略号就再没下文

这个问题比较隐蔽。常见原因有两个:设备树里没有配置正确的stdout-path(内核不知道控制台是哪个串口),或者uart时钟不对——U-Boot初始化uart的时钟频率和内核驱动推算出的频率不一致,导致串口输出的波特率漂移。排查方法:给bootargs增加earlyprintk和clk_ignore_unused试试,或者用JTAG连接看内核是否其实已经跑到了某个阶段。

故障五:启动时卡在“waiting for root device”

这种往往是rootwait等待了很长时间,说明内核的SD卡驱动根本没有把设备枚举出来。大概率是设备树里mmc@e0100000节点被禁用(status = "disabled"),或者没有正确写SD卡控制器的时钟和管脚信息。此时在内核配置里确认SD卡主机控制器驱动已经编进去。

故障六:文件系统挂载后,执行/bin/sh报Permission denied

多半是rootfs里的文件属性或软链接权限有问题。BusyBox的符号链接如果是指向/bin/busybox,那么busybox本身是可执行文件即可;但如果你把整个rootfs是从Windows拷贝到SD卡上的,可能权限全变成了rwxrwxrwx,反而没事,倒是ext4分区根目录的权限模式可能不对。给rootfs根目录设置chmod 755,关键的bin目录设chmod 755 /bin/busybox。

6.4 从串口日志中读取启动关键阶段

无论故障排查还是正常验证,学会看串口日志比什么都强。我把一次正常启动的日志关键行拆解成表,方便你对照:

日志内容对应阶段发生什么
U-Boot 2020.04 ...SPL/U-Boot加载完成FSBL已经跳转到U-Boot,DDR初始化成功
mmc: 0MMC控制器枚举SD卡控制器初始化完成
Reading uImage from mmc 0:1内核镜像加载fatload从SD卡读到DDR
Starting kernel ...跳转内核U-Boot把控制权交给内核入口
Booting Linux on physical CPU 0x0内核早期初始化内核vmlinux开始执行
Kernel command line: console=ttyPS0...bootargs解析内核收到启动参数
Freeing unused kernel memory内核初始化完成init进程即将启动
init started: BusyBox v1.36.0用户态开始根文件系统挂载成功,init进程运行

我在实际调试中养成了一个习惯:把正常启动的串口日志完整保存一份,命名为good_boot.txt;出问题时的日志另存为bad_boot.txt,两边做diff,通常几秒钟就能定位到从哪个阶段开始分叉。这个方法在以传统方式反复调试Linux启动时效率极高,比一条条看日志靠谱得多。

6.5 烧写QSPI与后续方向预告

如果你调试稳定了,要把它固化到开发板里,可以把BOOT.bin烧到QSPI Flash。用SD卡启动进入Linux后,在PS侧通过flashcp命令或者直接用U-Boot的sf命令即可烧写。QSPI启动的优缺点对比SD卡:

  • 启动速度更快,工业环境抗震性好;
  • 但烧写次数有限(QSPI Flash擦写寿命一般10万次),调试阶段频繁烧写不划算;
  • QSPI镜像可以包含完整的FSBL+U-Boot+内核+设备树+ramdisk,这样bootargs里可以不用SD卡。

建议先SD卡调通,再固化到QSPI。因为SD卡修改方便,调试成本低。等系统稳定了再烧Flash,能避免“Flash写坏只能换片”的悲剧。

到这里,ZYNQ传统方式移植Linux的完整链路已经走完。我最终得到的其实不是一堆文件,而是一条链条——从BootROM到FSBL、从U-Boot到内核、从设备树到根文件系统,每个环节都清楚它在干什么。这种“系统是自己搭出来”的感觉,是PetaLinux一键生成给不了的。后续系列里,我会继续展开在PS上移植BusyBox的细节优化、PL读写PS外挂DDR的FrameBuffer方案、双核AMP生成bin文件的流程,以及ZYNQ GPIO控制器编号在设备树里的映射关系。自己搭出来的系统,后面加什么都顺理成章。

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

CentOS 7 repo源管理:默认源排错、镜像选型与归档源配置

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

作者头像 李华
网站建设 2026/9/30 1:15:27

从 0 到 1000 Star:开源项目的里程碑与下一站

从 0 到 1000 Star:开源项目的里程碑与下一站昨晚深夜,终端 AI CLI 工具的代码仓库右上角数字跳过了 1000。 从最初在本地工作目录随手写下的一个几十行 Bash 包装脚本,到如今拥有多平台预编译二进制、活跃的 issue 讨论区和来自全球数十位贡…

作者头像 李华
网站建设 2026/9/30 1:15:09

支付功能测试七层穿透模型与实战Checklist

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

作者头像 李华
网站建设 2026/9/30 1:14:22

零基础学网站开发:从懂原理到动手搭建并部署上线

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

作者头像 李华
网站建设 2026/9/30 1:13:31

SAP MM高频术语全解析:从MIGO收货到MIRO发票校验

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

作者头像 李华
网站建设 2026/9/30 1:11:28

基于相对总变分的图像结构提取:去纹理保结构的利器

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

作者头像 李华