Zynq嵌入式Linux开发:非PetaLinux方式手工编译Uboot内核与根文件系统
简介Zynq系列PS端嵌入式源码开发教程面向需要深入理解嵌入式Linux底层原理的开发者提供一套完整的非Petalinux源码级移植方案。基于Xilinx官方GitHub的U-boot与内核源码遵循标准Linux开发流程从Vivado生成HDF文件开始经SDK设备树生成、U-boot移植编译、内核移植到busybox制作根文件系统实现NFS挂载开发后迁移至EMMC或SD卡实例在Zynq045板卡验证可用。资源为单份PDF文档大小3.69MB内容以图文结合方式详述每一步操作与配置包括boot.bin合成、烧录镜像生成、启动环境设置及常见问题处理。已有2529人学习适合想要摆脱Petalinux黑盒、进行定制化系统开发的Zynq开发者参考。1. 非petalinux方式做Zynq PS端开发为什么源码手工编译反而更快Zynq系列PS端的嵌入式Linux开发很多人一上来就开petalinux结果被它的黑匣子折腾得够呛——版本对不上、配置项找不到、编译报错没法查最后连uboot环境变量该改哪里都说不清。我做过几个Zynq项目后来全部改成非petalinux方式直接拉uboot、内核源码手工配交叉编译环境一条命令一条命令把镜像编出来。这套方式初看麻烦但每个环节都可查可控出问题能定位到具体配置项反而比图形化工具体系更快。这篇笔记就把uboot、内核、根文件系统三个部分的移植步骤、参数设置和踩坑记录完整写出来适合已经跑通过petalinux但想摆脱黑匣子的人也适合新项目想从源码层建立掌控感的团队。2. 交叉编译环境与源码体系工具链、版本组合和目录规划2.1 工具链选型Linaro GCC 还是厂商预编译工具链PS端是ARM Cortex-A9双核必须用arm-linux-gnueabihf-前缀的交叉工具链。常见做法是用Linaro发布的gcc-arm-linux-gnueabihf版本选择上不要太激进我一般固定在gcc 7.x或8.x系列。太新的gcc 11编uboot会冒出一堆warning虽然多数不影响但排查问题时分心太老的4.x系列编新版内核源码会直接报错因为内核对编译器版本有下限要求。# 解压工具链到 /opt 目录 sudo tar xf gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz -C /opt # 把工具链bin目录加入PATH建议写进 ~/.bashrc export PATH/opt/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf/bin:$PATH # 验证 arm-linux-gnueabihf-gcc -v这里有个细节工具链解压后不要放在有中文或空格的路径下make脚本解析路径时容易出玄学问题。另外-v输出最后一行如果看到gcc version 7.5.0就说明环境正常如果提示找不到命令先确认PATH写没写对、有没有执行source ~/.bashrc。选厂商预编译工具链也行风格是版本匹配度好但更新节奏慢社区资料少。真遇到工具链本身的bugLinaro文档和issue列表更容易搜到解法。以我的经验Linaro 7.5配合内核4.145.4、uboot 20182022这几个组合都验证过没踩过工具链本身的坑。2.2 源码版本组合的稳定性原则uboot、内核、设备树、文件系统之间没有强制的版本绑定关系所以版本组合是移植里第一个需要做决策的点。原则是选择你手上板子出厂时对应的内核大版本然后uboot选同一年代或稍新的release。比如板子资料里写的内核是4.14那uboot用2018.012020.01都行不必强求uboot也更新到2023。# 用git拉取源码注意指定分支 git clone https://github.com/Xilinx/u-boot-xlnx.git -b xlnx_release_v2019.2 git clone https://github.com/Xilinx/linux-xlnx.git -b xlnx_release_v2019.2为什么强调用厂商维护的分支因为Zynq的PS端有很多板级配置散落在uboot的arch/arm/mach-zynq和内核的arch/arm/boot/dts目录下厂商分支把这些板级支持提前合入直接用git checkout一个release tag就能编不用自己从零改配置。Xilinx的分支命名是xlnx_release_v版本号这个版本号和Vivado版本对应2019.2就配Vivado 2019.2这样FSBL和uboot的DDR初始化参数才能对上。内核源码同理拉厂商分支而不是mainline。mainline内核虽然也支持Zynq但设备树和驱动更新节奏不同手工backport板级补丁的成本够你加好几个功能了。我踩过一次mainline的坑网卡驱动在4.19之后改成phy状态机驱动导致千兆网卡协商异常换回厂商分支才恢复。2.3 环境变量与顶层目录布局规划源码开发最怕目录乱。交叉编译产物、中间文件、镜像输出分散在不同目录出错时连日志都对不上。我一般用一个顶层目录下面按boot、kernel、rootfs、images四个子目录分装环境变量直接写死到~/.bashrc里。# 目录规划示例 mkdir -p ~/zynq_work/{uboot,kernel,rootfs,images} # 设置环境变量export 全部写进 ~/.bashrc export ZYNQ_UBOOT~/zynq_work/uboot/u-boot-xlnx export ZYNQ_KERNEL~/zynq_work/kernel/linux-xlnx export ZYNQ_ROOTFS~/zynq_work/rootfs export ZYNQ_IMAGES~/zynq_work/images这个布局的好处是每个镜像的生成路径固定后面写脚本时不用到处找路径。还有两个必设的环境变量ARCHarm和CROSS_COMPILEarm-linux-gnueabihf-。很多人uboot编译失败就是忘了这两个make不知道给哪个架构编。下面是我的~/.bashrc里实际内容注意CROSS_COMPILE结尾的短横线不能省export ARCHarm export CROSS_COMPILEarm-linux-gnueabihf-设完环境变量后重新登录shell执行echo $ARCH $CROSS_COMPILE输出arm arm-linux-gnueabihf-才算就绪。这一步看起来基础实际项目里我见过有人把CROSS_COMPILE设成了绝对路径前缀导致make找到不存在的/opt/bin/arm-linux-gnueabihf-gcc卡了半小时才反应过来。3. 手工编译uboot、内核与根文件系统一条命令一条命令打通3.1 uboot板级配置选择与BOOT.BIN拼接uboot移植的第一步不是写代码而是选对配置。查看configs/目录下带zynq前缀的defconfig常见的比如zynq_zc702_defconfig、zynq_zc706_defconfig如果你用的是自己画的核心板选一个芯片型号最接近的defconfig然后改设备树或环境变量而不是从零创建配置。cd $ZYNQ_UBOOT make zynq_zc706_defconfig make -j$(nproc) # 编译产物在 u-boot.elf ls -la u-boot.elfdefconfig这一步是把默认配置展开成.config后续make menuconfig可以微调。比如你的板子DDR容量是1GB而不是默认的512MB在Device Tree Control里改reg属性就行。-j$(nproc)是用所有CPU核并行编译第一次编译建议先不加-j能看到完整日志确认没有报错后再改并行。uboot编完只是生成了u-boot.elfZynq启动还需要FSBL。FSBL的常见做法是在Vivado里导出硬件描述文件后用SDK或XSCT工具链编译生成。这个环节不用手工写代码但要在Vivado SDK里创建一个fsbl工程编译后会得到fsbl.elf。BOOT.BIN是把FSBL、uboot、bitstream打包的结果用bootgen工具完成。# bootgen 使用的 .bif 文件示例 the_ROM_image: { [bootloader] fsbl.elf bitstream.bit u-boot.elf } # 执行 bootgen bootgen -image boot.bif -o i BOOT.BINbif文件里三个partition的顺序是固定的bootloader在最前bitstream可选的如果你的FPGA逻辑不需要加载就整行删掉最后是uboot。我犯过的错是把u-boot.elf放在了bitstream前面结果uboot没被加载启动直接卡死在FSBL阶段。BOOT.BIN生成后放到SD卡FAT分区这是Zynq标准启动流程的第一步。启动模式拨码开关也要对照板子手册设置成SD卡启动否则板子不从SD卡取镜像。3.2 内核配置裁剪、设备树与uImage生成内核移植的核心工作是设备树不是内核本身。Zynq的设备树源文件在arch/arm/boot/dts/下以zynq-开头。每个具体板子有自己的dts文件继承自zynq-7000.dtsi公共定义。cd $ZYNQ_KERNEL make xilinx_zynq_defconfig # 图形界面微调比如关闭不用的驱动 make menuconfig # 编译内核与设备树 make -j$(nproc) UIMAGE_LOADADDR0x8000 uImage make -j$(nproc) dtbs ls -la arch/arm/boot/uImage ls -la arch/arm/boot/dts/zynq-zc706.dtbUIMAGE_LOADADDR0x8000这个参数是Zynq的约定uboot会把内核加载到这个地址。地址写错会出现内核启动时“run away”的乱码或者“Error: unrecognized image type”的登录失败。make uImage要求系统里有mkimage工具如果没有就先执行sudo apt install u-boot-tools。设备树的修改主要集中在zynq-xxx.dts文件里memory节点改DDR容量ethernet节点改PHY地址qspi节点改flash型号。我每次改设备树都遵循一个顺序先对照原理图把外设用的引脚找出来再在pinctrl节点确认复用关系最后才看具体外设节点。顺序反过来容易漏掉引脚复用配置功能时好时坏。# 编译单个设备树文件的快速方法 make arch/arm/boot/dts/zynq-zc706.dtb内核编完会得到一个uImage和一个dtb文件这两个文件加BOOT.BIN一起放SD卡FAT分区。注意uImage和dtb放在FAT分区根目录uboot启动脚本会用fatload命令把它们读到内存。3.3 根文件系统busybox构建与目录手工填充根文件系统我不用buildroot不是它不好而是非petalinux方式的初衷就是每一层都自己掌控。busybox是最可靠的起点它把常用的shell命令都包进一个可执行文件。mkdir -p $ZYNQ_ROOTFS cd $ZYNQ_ROOTFS # 下载busybox源码后解压进入目录编译 make menuconfig # 关键配置项Settings - Build static binary (no shared libs) make -j$(nproc) make installmake install默认把文件装到_install目录这个目录就是根文件系统的雏形。但只有busybox还不够还需要手工创建Linux运行需要的目录和设备节点。下面是我每次必做的完整目录创建命令cd $ZYNQ_ROOTFS/_install mkdir -p proc sys dev tmp etc root lib mnt # 用busybox的示例配置生成etc目录的初始文件 cp -r $ZYNQ_ROOTFS/busybox-1.36.0/examples/bootfloppy/etc/* etc/etc/inittab是启动后init进程的配置文件默认内容要改一下加上串口登录# 编辑 etc/inittab确保包含下面这行 ::respawn:/sbin/getty -L ttyPS0 115200 vt100没有这行的话启动后串口只有内核log没有登录提示符。ttyPS0是Zynq的UART设备名如果你用的是ttyS0那是对应老驱动两者都会导致一个现象看起来系统起来了但shell就是不出来。动态库的处理是另一个必踩的坑。busybox如果按静态编译file bin/busybox输出显示statically linked那不需要拷动态库。如果之前选了动态编译就要从工具链的sysroot目录拷贝libc.so等文件到根文件系统的lib目录否则启动到一半就报/sbin/init: not found。我统一用静态编译牺牲一点空间换省心。4. 移植避坑清单启动卡死、内核panic与rootfs报错排查4.1 现象一uboot启动后停在内核加载前输出只有一行“Starting kernel”有一次我把新编的内核替换到SD卡启动后uboot正常但输出Starting kernel ...之后就再无反应。查了uboot环境变量bootcmd里写的是fatload mmc 0 0x2080000 uImage; fatload mmc 0 0x2000000 zynq-zc706.dtb; bootm 0x2080000 - 0x2000000看起来没问题。原因出在内核镜像格式bootm要求加载的是uImage格式我当时图方便直接把zImage改名放进SD卡uboot识别不了。解决方法是先用mkimage把zImage包装成uImage或者直接在uboot里改成bootz命令。建议用前者因为bootm地址参数里包含dtb地址改动最小。mkimage -A arm -O linux -T kernel -C none -a 0x8000 -e 0x8000 -n Linux kernel -d zImage uImage-a和-e分别是加载地址和入口地址Zynq统一用0x8000。这里有个细节如果内核在menuconfig里开了CONFIG_ARM_APPENDED_DTB那dtb不能单独传否则反而会启动失败新内核默认不开这个选项所以dtb单独传没问题。4.2 现象二内核panic并提示无法挂载根文件系统内核起来后报错VFS: Unable to mount root fs on unknown-block(179,2)。这个报错信息本身已经把关键信息说清楚了内核找不到/dev/mmcblk0p2这个设备。179是MMC设备的主设备号2是分区号unknown-block说明内核里SD卡驱动没编进去或者编成了模块。我当时第一次移植时把MMC驱动设成了M模块被放在文件系统里但文件系统都挂不上自然加载不了模块死循环。解决方法是把驱动改成*编进内核menuconfig里进入Device Drivers - MMC/SD/SDIO card support确认MMC block device driver和ARM PrimeCell PL180/181 MMC/SDIO support都选成内建。另外一个常见的同款坑没加rootwait启动参数。内核挂载rootfs时设备还没枚举完成就报找不到设备。# uboot环境变量里设置正确的bootargs setenv bootargs consolettyPS0,115200 root/dev/mmcblk0p2 rw rootwait saveenvrootwait的意思是等待设备就绪再挂载不加它SD卡设备初始化慢一点就会翻车。这个参数在eMMC和SD卡启动时建议都加上代价只是多等一两秒。4.3 现象三文件系统起来了但/dev目录是空的系统能启动到登录提示符但是ls /dev只有几个设备节点console、ttyPS0都不见。原因是根文件系统里没有自动挂载devtmpfs。Linux的/dev可以由内核自动管理前提是根文件系统的/dev目录存在且内核开了CONFIG_DEVTMPFS然后在启动过程中挂载它。解决方法是修改根文件系统里的etc/fstab加入下面这行devtmpfs /dev devtmpfs defaults 0 0或者在启动脚本etc/init.d/rcS里手动执行mount -t devtmpfs devtmpfs /dev。用fstab的方式更符合习惯rcS脚本位置不同发行版不一样手工做rootfs时自己创建的启动脚本放在哪个路径都可以只要inittab里指定了就行。还有个更隐蔽的点如果busybox是按静态编译的在CONFIG_DEVTMPFS没开的内核上执行mount -t devtmpfs会报unknown filesystem type这个坑是内核配置问题跟文件系统做法无关。4.4 现象四BOOT.BIN能启动但uboot打印乱码或直接无输出这个坑最玄学用自己编的uboot替换后串口输出全是3这种乱码或者干脆没有任何输出。先说结论绝大多数情况是FSBL里配置的DDR初始化参数和uboot链接地址不匹配。uboot的CONFIG_SYS_SDRAM_BASE、CONFIG_SYS_MEM_SIZE要和FSBL里DDR控制器配置一致。解决路径是回到Vivado硬件定义里确认DDR颗粒型号和容量在FSBL和uboot两处都改成同一个值。如果确认地址没问题第二个排查点是uart时钟Zynq的UART时钟来自CLK_CTRL_REGISTER配置FSBL里设置不对会导致波特率漂移现象就是乱码而非无输出。踩过一次之后我养成了一个习惯换uboot版本时先拿厂商默认的BOOT.BIN验证串口通再换成自己编的。5. 验证与进阶SD卡启动自制镜像的完整测试路径5.1 使用SD卡进行完整启动验证的步骤要点移植完成后的验证不是直接把板子点亮就算完事建议按四步走每一步的输出都要有记录第一步是uboot验证启动后停在uboot命令行输出printenv确认bootcmd和bootargs内容正确。第二步是内核验证bootm启动后看内核log里设备树有没有成功加载执行ls /proc/device-tree应该能看到dts里的节点。第三步是文件系统验证登录后执行mount查看根文件系统的挂载选项cat /proc/cmdline确认启动参数和uboot里设一致。第四步是外设验证ifconfig -a看网卡IPcat /proc/interrupts看中断有没有注册。# 在板子上执行的验证命令 cat /proc/cmdline cat /proc/mounts ifconfig eth0 up每个命令的输出都和预期比对不要只看启动没报错就收工。我遇到过一次内核log完全没有错误信息但网卡驱动枚举到了错误的PHY地址导致ifconfig eth0逻辑不通这种问题只有主动验证外设才能发现。5.2 调试期用initramfs、稳定后换ext4的进阶做法用SD卡挂载ext4根文件系统是最终形态但在调试阶段有一个更好用的方案先用initramfs把系统拉起来确认内核和设备树没问题之后再切到ext4。initramfs是把rootfs打包成cpio.gz由uboot加载进内存内核直接从这个归档里启动。# 制作initramfs镜像 cd $ZYNQ_ROOTFS/_install find . | cpio -H newc -o | gzip ../initramfs.gz启动参数改成root/dev/ram0或直接rdinit/sbin/init不依赖SD卡驱动。调试阶段的优势是内核不用等SD卡设备就绪启动速度快而且每次修改rootfs内容只要重新打包不需要重新格式化分区。等所有驱动和外设都验证完再制作ext4镜像写入SD卡第二个分区。最后说一个习惯换镜像前先把旧的BOOT.BIN和uImage备份到一个专门目录命名里带日期。原因很简单新版本编出来可能启动失败你要有一个能快速回退的基线。我吃过忘记备份的亏新镜像起不来又找不到当时能跑的那版编译配置只能靠git reflog往回翻白白耗了半天。这个习惯救过我很多次希望帮到你。本文还有配套的精品资源点击获取