简介: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。
选厂商预编译工具链也行,风格是版本匹配度好,但更新节奏慢,社区资料少。真遇到工具链本身的bug,Linaro文档和issue列表更容易搜到解法。以我的经验,Linaro 7.5配合内核4.14~5.4、uboot 2018~2022这几个组合都验证过,没踩过工具链本身的坑。
2.2 源码版本组合的稳定性原则
uboot、内核、设备树、文件系统之间没有强制的版本绑定关系,所以版本组合是移植里第一个需要做决策的点。原则是:选择你手上板子出厂时对应的内核大版本,然后uboot选同一年代或稍新的release。比如板子资料里写的内核是4.14,那uboot用2018.01~2020.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这个布局的好处是每个镜像的生成路径固定,后面写脚本时不用到处找路径。还有两个必设的环境变量:ARCH=arm和CROSS_COMPILE=arm-linux-gnueabihf-。很多人uboot编译失败就是忘了这两个,make不知道给哪个架构编。下面是我的~/.bashrc里实际内容,注意CROSS_COMPILE结尾的短横线不能省:
export ARCH=arm export CROSS_COMPILE=arm-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.elf,Zynq启动还需要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_LOADADDR=0x8000 uImage make -j$(nproc) dtbs ls -la arch/arm/boot/uImage ls -la arch/arm/boot/dts/zynq-zc706.dtbUIMAGE_LOADADDR=0x8000这个参数是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 'console=ttyPS0,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看网卡IP,cat /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往回翻,白白耗了半天。这个习惯救过我很多次,希望帮到你。
本文还有配套的精品资源,点击获取