ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Zynq7020开发板Linux实验全流程:从环境搭建到PL端协同开发

Zynq7020开发板Linux实验全流程:从环境搭建到PL端协同开发 1. 为什么这块Zynq7020开发板值得拿来跑Linux实验手里这块Zynq7020开发板在嵌入式圈子里算是老面孔了。它最特别的地方在于芯片内部同时集成了ARM Cortex-A9双核处理器PS端和FPGA可编程逻辑PL端这种硬核软核的异构架构让它在工业控制、图像处理、通信设备等领域一直有稳定的需求。很多人拿到板子第一反应是跑裸机程序或者纯FPGA逻辑但真正能把这颗芯片价值榨干的玩法是在PS端跑一个完整的Linux系统然后用Linux去调度PL端的硬件加速单元。我当初决定在这块板子上折腾Linux原因很直接裸机开发虽然响应快、资源占用少但一旦项目涉及网络通信、文件系统、多任务调度裸机代码就会变得极其臃肿且难以维护。而Linux自带完整的网络协议栈、文件系统和进程管理能把PS端的双核A9性能充分发挥出来。更重要的是跑通Linux之后你可以用设备树、字符设备驱动、UIO等机制去操作PL端的自定义IP核这才是Zynq异构计算的核心价值所在。这篇文章适合谁看如果你手里正好有一块Zynq7020系列的开发板已经跑过裸机例程或者点过LED想进一步把Linux系统跑起来并且最终目标是让Linux和FPGA逻辑协同工作那接下来的内容就是为你准备的。我会从环境搭建、启动流程、设备树配置、驱动开发到实际调试把整个链路拆开讲清楚包括我在这个过程中踩过的坑和总结出来的经验。需要提前说明的是Zynq7020的Linux实验涉及的知识面比较宽——嵌入式Linux、U-Boot、设备树、FPGA开发工具链都会碰到。我不会假设你全部精通但至少需要你能看懂基本的Linux命令知道什么是交叉编译对FPGA开发流程有个大致概念。如果这些你都还没接触过建议先补一下基础再来否则后面每一步都会卡住。2. 开发环境搭建工具链、SDK与目录规划2.1 交叉编译工具链的选择与安装在x86主机上为ARM架构编译程序交叉编译工具链是第一个绕不开的东西。Zynq7020的PS端是Cortex-A9属于ARMv7-A架构支持硬浮点VFPv3。所以工具链必须选带gnueabihf后缀的版本也就是硬浮点ABI。如果你用了软浮点的gnueabi版本编译出来的程序在板子上跑起来会莫名其妙地崩溃或者性能极差这个坑我见过太多人踩。具体来说我推荐使用Linaro或者ARM官方发布的GCC工具链。安装方式很简单下载压缩包解压到/opt目录然后把bin目录加入PATH环境变量tar -xvf gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz -C /opt export PATH/opt/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf/bin:$PATH验证是否安装成功arm-linux-gnueabihf-gcc -v如果能看到gcc版本信息说明工具链就绪。这里有个细节要注意不同版本的GCC对内核编译的兼容性不一样。我实测下来GCC 7.5这个版本编译Linux 4.14和5.4内核都比较稳太新的GCC比如10以上编译老内核容易出现各种warning甚至error。所以工具链版本和内核版本要搭配着选不要盲目追新。2.2 Vivado与PetaLinux的角色分工Zynq开发和其他ARM开发板最大的区别在于你需要两个大工具Vivado和PetaLinux或者用Yocto。Vivado负责硬件设计部分——配置PS端的时钟、DDR、外设引脚以及设计PL端的逻辑。PetaLinux则负责生成Linux系统相关的所有东西U-Boot、内核、设备树、根文件系统。很多人会问能不能不用PetaLinux手动编译U-Boot和内核当然可以而且我建议你至少手动编译一次这样才能真正理解整个启动链路。PetaLinux的好处是自动化程度高一条命令就能生成完整的启动镜像但它的黑盒特性也意味着出问题的时候你很难定位。我的做法是先用PetaLinux跑通整个流程确认硬件没问题然后再手动编译各个组件把每个环节都搞清楚。Vivado这边你需要创建一个Zynq Processing System的IP核配置好DDR型号和容量、时钟频率、外设使能UART、SD、以太网等。这些配置最终会导出一个.hdf或者新版叫.xsa文件这个文件就是硬件描述PetaLinux和U-Boot都要用它。2.3 目录结构规划与版本管理在开始之前我强烈建议你把目录结构规划好。我见过太多人把所有文件堆在一个目录里最后自己都分不清哪个是哪个版本。我的目录结构是这样的zynq_linux/ ├── hardware/ # Vivado工程和导出的xsa文件 ├── uboot/ # U-Boot源码 ├── kernel/ # Linux内核源码 ├── rootfs/ # 根文件系统 ├── tools/ # 工具链和辅助脚本 └── output/ # 最终生成的启动镜像每个目录独立管理用git做版本控制。特别是设备树文件和内核配置文件一定要纳入版本管理因为这两个东西改动频繁出问题的时候需要快速回退。我自己的习惯是每次修改设备树之前先commit一次改完测试通过再commit这样任何时候都能追溯到哪个改动导致了问题。提示Vivado工程文件很大不建议整个纳入git。只需要把导出的xsa文件和关键的约束文件、bd文件纳入版本管理即可。3. 启动链路拆解从上电到Linux命令行3.1 Zynq的启动阶段划分Zynq7020的启动过程和普通ARM处理器不太一样它有一个独特的Stage 0阶段。上电之后芯片内部固化的BootROM首先运行这段代码是出厂就烧死在芯片里的你改不了。BootROM会根据MIO引脚的电平状态判断启动模式QSPI、SD卡、JTAG等然后从对应介质加载FSBLFirst Stage Boot Loader。FSBL是Xilinx提供的主要做三件事初始化DDR控制器、配置PL端如果bitstream存在的话、加载SSBL通常是U-Boot到DDR中。FSBL的源码在Vivado的SDK里可以找到你也可以自己修改但大多数情况下用默认的就行。U-Boot作为第二阶段引导程序负责更复杂的初始化串口、网口、MMC等然后加载Linux内核和设备树到DDR最后跳转到内核入口。内核启动后会挂载根文件系统执行init进程最终给你一个命令行或者图形界面。整个链路可以概括为BootROM - FSBL - U-Boot - Linux Kernel - RootFS - Init理解这个链路非常重要因为后面任何一步出问题你都需要知道当前卡在哪个阶段。比如串口没有任何输出可能是FSBL都没跑起来如果FSBL有输出但U-Boot没输出那就是U-Boot加载失败如果U-Boot起来了但内核panic那就是内核或者设备树的问题。3.2 BOOT.BIN的组成与生成在SD卡启动模式下你需要一个BOOT.BIN文件它实际上是把FSBL、bitstream可选和U-Boot打包在一起。生成BOOT.BIN最简单的方法是用Xilinx SDK或者Vitis创建一个FSBL工程然后通过Create Boot Image工具把各个组件按顺序添加进去。顺序很重要FSBL必须在最前面然后是bitstream如果需要PL端在启动时就配置最后是U-Boot。如果顺序错了芯片根本找不到正确的入口点。我一般会生成两个版本的BOOT.BIN一个带bitstream一个不带。调试阶段用不带bitstream的版本因为加载bitstream会多花几百毫秒而且如果PL端逻辑有问题可能会影响启动。等系统稳定了再换成带bitstream的版本。3.3 启动模式选择与SD卡分区Zynq7020开发板上通常有一个拨码开关或者跳线帽来选择启动模式。你需要把它设置成SD卡启动。具体怎么设置看板子原理图一般是MIO[5:3]的组合。SD卡需要分成两个区第一个分区是FAT32格式放BOOT.BIN和内核镜像image.ub或者zImagedevicetree.dtb第二个分区是ext4格式放根文件系统。第一个分区必须从SD卡的第一个扇区开始而且必须是FAT32因为BootROM和U-Boot只认FAT文件系统。分区操作可以用fdisk或者gpartedsudo fdisk /dev/sdX # 创建第一个分区大小100MB左右类型设为FAT32 # 创建第二个分区占用剩余空间类型设为Linux格式化sudo mkfs.vfat -F 32 /dev/sdX1 sudo mkfs.ext4 /dev/sdX2注意SD卡设备名不要搞错/dev/sdX里的X根据你的主机实际情况替换。搞错了可能把主机硬盘格式化这个后果很严重。4. 设备树配置让内核认识你的硬件4.1 设备树的基本结构与Zynq特有节点设备树Device Tree是嵌入式Linux里描述硬件信息的数据结构。在Zynq平台上设备树尤其重要因为PS端的外设使能情况、DDR大小、时钟频率这些信息都在设备树里体现。内核启动时读取设备树根据里面的描述去初始化对应的硬件。一个典型的Zynq设备树文件.dts包含几个关键部分chosen节点指定启动参数bootargs和标准输出stdout-pathmemory节点描述DDR的起始地址和大小amba节点PS端所有外设的父节点包括UART、GPIO、SD、以太网等clocks节点时钟树描述PL端自定义IP的节点如果有的话chosen节点里的bootargs特别关键它决定了内核启动后去哪里找根文件系统。比如chosen { bootargs consolettyPS0,115200 root/dev/mmcblk0p2 rw rootwait earlyprintk; stdout-path serial0:115200n8; };这里的root/dev/mmcblk0p2表示根文件系统在SD卡的第二个分区。consolettyPS0,115200指定串口0作为控制台波特率115200。如果这两个参数写错了要么看不到任何输出要么内核找不到根文件系统直接panic。4.2 根据Vivado配置修改设备树Vivado里你对PS端的配置——比如使能了哪些外设、DDR容量多大、时钟频率多少——都需要在设备树里对应修改。最直接的方法是用PetaLinux根据xsa文件自动生成设备树然后在此基础上手动调整。自动生成的设备树通常包含了很多你用不到的外设节点这些节点如果使能了但硬件上没接可能会导致内核启动时卡住或者报错。我的做法是先把自动生成的设备树作为参考然后手动精简只保留实际用到的外设。比如UART节点如果你只用了UART0那UART1的节点就应该设置status disabled。SD卡节点要根据实际用的是SD0还是SD1来配置。以太网节点要注意PHY地址和接口类型RGMII还是MII这些参数错了网口就不通。4.3 PL端IP核的设备树描述这是Zynq设备树里最有意思的部分。当你在PL端设计了一个自定义IP核并且通过AXI总线连接到PS端你需要在内核设备树里添加对应的节点这样Linux才能识别并加载驱动。一个典型的PL端IP节点长这样my_ip: my_ip43c00000 { compatible my-vendor,my-ip-1.0; reg 0x43c00000 0x10000; interrupt-parent intc; interrupts 0 29 4; clocks clkc 15; };reg属性指定了IP核的寄存器基地址和地址范围这个地址必须和Vivado地址编辑器里分配的一致。interrupts指定了中断号和触发类型。compatible属性是驱动匹配的关键内核会根据这个字符串去找到对应的驱动。我踩过的一个坑是Vivado里修改了IP核的地址映射但忘记同步更新设备树结果内核访问的一直是旧地址读出来的数据全是0xFFFFFFFF。所以每次Vivado硬件有改动设备树一定要跟着改这两个东西必须保持一致。5. 内核编译与根文件系统制作5.1 内核配置的关键选项编译Linux内核之前defconfig的选择很重要。Xilinx维护了自己的内核分支里面包含了Zynq平台的默认配置。你可以从Xilinx的GitHub仓库拉取内核源码然后使用xilinx_zynq_defconfig作为基础配置。make ARCHarm xilinx_zynq_defconfig make ARCHarm menuconfig在menuconfig里有几个选项必须确认Device Drivers - Character devices - Serial drivers确保Xilinx UART驱动被编译进内核不是模块否则你连串口输出都看不到。Device Drivers - MMC/SD/SDIO card supportSD卡驱动必须编译进内核因为根文件系统在SD卡上。Device Drivers - Network device support如果要用网口以太网驱动也要编译进去。File systems - Ext4根文件系统是ext4格式的话这个必须选上。Device Drivers - FPGA Configuration Framework如果你需要在Linux运行时动态加载bitstream这个框架需要使能。编译命令make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- -j$(nproc) zImage make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- dtbs编译完成后arch/arm/boot/zImage就是内核镜像arch/arm/boot/dts/zynq-xxx.dtb就是设备树二进制文件。5.2 用BusyBox构建最小根文件系统根文件系统我推荐从BusyBox开始构建这样你能完全掌控里面有什么。BusyBox把常用的Linux命令ls、cd、ifconfig、vi等集成到一个可执行文件里体积很小非常适合嵌入式环境。构建步骤大致如下# 下载BusyBox源码配置并编译 make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- defconfig make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- menuconfig # 在menuconfig里设置安装路径为你的rootfs目录 make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- install编译安装完成后你需要在rootfs目录里创建必要的子目录和设备节点cd rootfs mkdir -p dev proc sys etc/init.d tmp var lib/modules sudo mknod dev/console c 5 1 sudo mknod dev/null c 1 3然后创建etc/init.d/rcS启动脚本内容大致是挂载proc、sysfs设置网络等#!/bin/sh mount -t proc none /proc mount -t sysfs none /sys mount -t tmpfs none /tmp /sbin/ifconfig lo 127.0.0.1 up最后给rcS加上可执行权限并在etc/inittab里配置启动流程。这个最小系统启动后你会看到一个命令行可以执行基本的Linux命令。5.3 根文件系统的打包与烧录根文件系统准备好之后可以直接把整个目录拷贝到SD卡的第二个分区ext4格式或者打包成rootfs.tar.gz再解压过去。我一般用rsync同步这样增量更新比较快sudo rsync -av rootfs/ /media/user/rootfs_partition/如果要用ramdisk方式启动还需要用mkimage工具把根文件系统打包成U-Boot能识别的格式。不过SD卡启动方式更简单调试也方便推荐优先用这种方式。6. 串口调试与常见启动故障排查6.1 串口终端的正确配置串口是Zynq Linux调试的主要通道。你需要一个USB转串口模块连接到板子上的UART0通常是Micro USB接口或者排针。在主机上用minicom、picocom或者screen打开串口sudo picocom -b 115200 /dev/ttyUSB0参数必须是115200-8-N-1。如果波特率不对你会看到乱码。如果完全没有输出检查TX/RX是否接反了这是新手最常犯的错误。6.2 启动失败的典型现象与定位方法启动失败的表现形式很多我按阶段来分类完全无输出检查供电是否正常、启动模式拨码是否正确、SD卡是否插好、串口线是否接对。如果这些都没问题可能是FSBL有问题尝试用JTAG模式启动看Vivado SDK能不能检测到芯片。FSBL有输出但U-Boot无输出通常是U-Boot镜像损坏或者加载地址不对。检查BOOT.BIN的生成顺序确认U-Boot被正确打包进去了。U-Boot有输出但内核无输出检查内核镜像和设备树是否在SD卡第一个分区文件名是否和U-Boot的加载命令匹配。U-Boot里可以用fatls mmc 0:1查看分区里的文件。内核panic找不到根文件系统检查bootargs里的root参数是否指向正确的分区SD卡第二个分区是否格式化为ext4并且有完整的根文件系统。内核启动到一半卡住通常是某个外设驱动初始化失败。可以在bootargs里加上earlyprintk和loglevel8看内核卡在哪个驱动。6.3 我踩过的三个典型坑第一个坑SD卡分区表类型。我用GPT分区表给SD卡分区结果U-Boot不认。后来改成MBR分区表就正常了。Zynq的BootROM和U-Boot对GPT的支持有限建议一律用MBR。第二个坑设备树里的DDR大小和实际不一致。Vivado里配置的是1GB DDR但设备树里写的是512MB结果内核只认一半内存跑大程序就OOM。这个一定要对齐。第三个坑U-Boot的环境变量保存在SD卡里但SD卡写保护了导致每次重启环境变量都恢复默认。后来把环境变量保存到QSPI Flash里才解决。7. Linux与PL端协同从GPIO到自定义IP7.1 通过sysfs操作PL端GPIO系统跑起来之后最直接的验证方式就是控制PL端的LED。在设备树里使能GPIO节点内核启动后会在/sys/class/gpio下生成对应的接口。你可以这样操作echo 960 /sys/class/gpio/export echo out /sys/class/gpio/gpio960/direction echo 1 /sys/class/gpio/gpio960/value这里的GPIO编号需要根据EMIO的映射关系计算。Zynq的MIO编号是0-53EMIO从54开始。如果PL端LED接在EMIO的某个引脚上编号就是54加上偏移量。具体计算方式参考Xilinx的文档不同版本的映射规则略有差异。7.2 编写字符设备驱动访问自定义IP对于更复杂的PL端IP核sysfs就不够用了需要写字符设备驱动。基本流程是在设备树里添加节点写一个platform driver匹配compatible属性在probe函数里做ioremap获取寄存器虚拟地址然后实现read/write/ioctl等文件操作接口。一个简化的驱动框架static int my_ip_probe(struct platform_device *pdev) { struct resource *res; res platform_get_resource(pdev, IORESOURCE_MEM, 0); base_addr devm_ioremap_resource(pdev-dev, res); // 注册字符设备创建设备节点 return 0; } static const struct of_device_id my_ip_of_match[] { { .compatible my-vendor,my-ip-1.0 }, { } };驱动编译成.ko模块后用insmod加载然后就可以在用户空间通过/dev/my_ip设备节点访问PL端寄存器了。7.3 动态加载bitstream的注意事项有时候你希望Linux启动后再加载PL端的bitstream而不是在FSBL阶段就配置好。这需要内核使能FPGA Configuration Framework然后通过/sys/class/fpga_manager接口加载.bit文件。需要注意的是动态加载bitstream时如果PL端逻辑会驱动某些引脚要确保这些引脚在加载前处于安全状态否则可能出现短暂的信号冲突。另外加载bitstream会重置PL端的所有逻辑如果之前有驱动已经映射了PL端寄存器加载后这些映射会失效需要重新初始化。8. 系统优化与长期运行稳定性8.1 启动时间优化默认配置下Zynq Linux从给电到命令行大概需要10-15秒。如果项目对启动时间有要求可以从几个方面优化裁剪内核去掉不需要的驱动和文件系统、使用静态编译的BusyBox、关闭不必要的内核调试输出、把根文件系统做成ramdisk减少SD卡读取时间。我实测下来优化到位的话可以压到5秒以内。8.2 看门狗与掉电保护工业场景下系统需要长期无人值守运行。Zynq的PS端有内置看门狗可以在内核里使能cadence_wdt驱动然后用户空间定期喂狗。如果系统卡死看门狗超时会触发复位。另外如果根文件系统是可写的突然掉电可能导致文件系统损坏建议把关键分区挂载为只读或者使用overlayfs把写操作重定向到内存。8.3 温度监控与散热Zynq7020在跑Linux并且PL端逻辑负载较高时芯片温度会明显上升。PS端内部有温度传感器可以通过/sys/class/thermal读取。如果温度超过85度需要考虑加散热片或者降低PL端时钟频率。我在夏天室温30度环境下实测带散热片的情况下芯片温度稳定在60度左右不加散热片会到75度以上。9. 关于这套实验流程的个人体会这套Zynq7020的Linux实验流程我从第一次跑通到现在反复折腾了不下几十次每次换板子或者换内核版本都会遇到新的问题。最大的感受是不要怕失败串口输出的每一行信息都是线索。很多时候启动卡住不是因为什么高深的原因就是某个配置项写错了、某个文件放错位置了、某个参数没对齐。另外设备树这个东西一定要亲手写一遍不要完全依赖工具自动生成。自动生成的设备树虽然能跑但里面有很多冗余节点而且一旦出问题你根本不知道从哪查起。自己从零写一个最小设备树只保留UART、SD、DDR这几个必要节点跑通之后再逐步添加其他外设这样每一步都是可控的。最后说一个实用技巧在U-Boot阶段用md和mw命令直接读写内存和寄存器可以快速验证硬件是否正常。比如读一下DDR的起始地址如果读出来全是0或者全是F那DDR初始化肯定有问题。这个方法比在内核里调试快得多因为U-Boot阶段没有复杂的驱动层直接操作硬件。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进