ZYNQ跑系统是很多玩嵌入式Linux的工程师早晚要迈过的一道坎。ZYNQ-7000这一系列芯片把双核ARM Cortex-A9处理器和FPGA可编程逻辑做进了同一个封装里跑移植Linux自然就成了项目里的头等活。这篇是“ZYNQ跑系统”系列的第一篇主角不是PetaLinux也不是Yocto而是最原始、最踏实的传统方式移植linux。什么叫传统就是FSBL自己建、U-Boot自己编译、设备树自己写、内核自己配、BusyBox自己裁每一个环节都看得见摸得着。这套流程走完你再回头看那些自动化工具的日志就能知道它在替你做哪几件事遇到启动异常时也能快速判断问题到底出在BootROM、FSBL还是内核驱动——这种排障能力纯靠敲命令是练不出来的。适合刚拿到ZYNQ开发板、想彻底搞懂Linux启动全过程的同学也适合已经用惯了PetaLinux但想补一补底层功底的工程师。1. 传统方式移植linux的整体设计思路先摸清启动链路1.1 什么叫“传统方式”跟自动化工具有什么区别现在聊起ZYNQ跑Linux很多人第一反应是打开PetaLinux填一个配置文件然后等它把整个镜像吐出来。这种方式当然高效但它像一个黑盒你告诉它“我有一块板子”它回你“这是你的BOOT.BIN、image.ub”中间发生了什么大部分人不清楚。传统方式走的是另一条路——不依赖PetaLinux不依赖Yocto所有组件手工获取、手工配置、手工编译最后手工打包。两者没有谁取代谁的关系。PetaLinux解决的问题是“版本兼容”和“自动化”但代价是它把所有细节都封装了起来。嵌入式Linux的启动链一旦出现问题最难受的不是“不会用工具”而是“不知道去哪看日志、看哪条日志、改哪个文件”。传统方式逼着你把启动链路上的每一环都亲自动一遍FSBL、U-Boot、内核、根文件系统每一段都有明确的输入输出和职责边界。跑完一遍之后你对整个系统的掌控感是完全不一样的。而且说实话传统方式的很多步骤并不比PetaLinux复杂多少只是把“点几个选项”换成了“敲几条命令”。对于只有一块裸板、没有现成BSP包的工程师来说传统方式反而是更可控的路径因为你改任何一个配置都清楚改在哪个文件里出了偏差也容易回溯。1.2 ZYNQ启动链路BootROM、FSBL、U-Boot、Kernel、RootfsZYNQ-7000的启动过程可以分成五段整个链路理解透了后面的操作就都是水到渠成。第一段是芯片固化的BootROM它上电后根据模式引脚的配置决定从SD卡、QSPI Flash、NAND、JTAG中的哪一个介质去读取镜像。BootROM本身的代码是芯片出厂写死的不需要你干预。它读到的第一个东西要求是BOOT.BIN格式的启动镜像。第二段是FSBLFirst Stage Boot Loader它的官方身份是第一阶段引导程序实际工作是初始化DDR内存、配置PS端的时钟和MIO复用、把PL侧的bitstream写入FPGA逻辑然后把第三段引导程序搬运到DDR里执行。FSBL的源代码Xilinx有现成模板基本不需要手写但它的生成过程依赖Vivado导出的硬件描述文件。如果你在Vivado里把DDR型号、时钟频率、引脚分配配置错了FSBL生成的初始化代码就会跟着错结果就是启动过程莫名其妙崩溃。第三段是U-Boot它是一个功能完整的引导程序能识别FAT32/ext4文件系统、能从SD卡或者网络加载内核镜像、能解析设备树然后跳转到Linux内核入口。U-Boot本身也是一个独立的操作系统启动过程中如果你按下任意键会进入它的命令行就可以手动敲命令来改变启动方式这一步排障时极其有用。第四段是Linux内核它被U-Boot加载到DDR的指定地址后经过自解压开始初始化然后挂载根文件系统启动第一个用户进程init。第五段是根文件系统也就是Rootfs。Linux内核本身只是把硬件资源抽象成设备模型真正让系统“能用”的是根文件系统里的BusyBox、库文件、配置脚本和应用程序。整个链路可以用一句话记牢BootROM找FSBLFSBL初始化PS并引导U-BootU-Boot加载内核和设备树内核挂载RootfsRootfs拉起init进程。1.3 这篇文章要完成的四件事把这个系列的目标拆开其实就是四个字让Linux转起来。说得再具体一点我要在文章里带着你完成四件核心事务第一准备好交叉编译环境。ZYNQ的ARM核和PC的x86架构不同编译器必须用arm-linux-gnueabihf这一类交叉编译工具链用PC自带的gcc编出来的程序根本无法在ARM上运行。第二生成并编译FSBL和U-Boot。这两个引导程序的职责一个是“让DDR能用、PL能加载”另一个是“把内核镜像和设备树从存储介质读进来”。这一环节的产出是一个BOOT.BIN文件它能被BootROM识别。第三配置设备树和内核。设备树告诉内核“板子长什么样”——内存多大、串口在哪、SD卡控制器有没有使能、GPIO怎么映射。内核配置决定哪些驱动被编入内核、哪些不需要。这一环节的产出是uImage和DTB文件。第四用BusyBox搭一个最小的根文件系统然后把所有文件按分区写入SD卡上电跑通。如果你的目标是让Linux在内核态跑起来、能登录shell、能操作目录和文件把这四件事做完就足够了。2. 环境准备交叉编译工具链与源码版本搭配2.1 硬件平台与板级差异动手之前最好对板子心里有数。我手上用的是常见的一块ZYNQ-7020开发板PS部分是双核ARM Cortex-A9DDR3容量和型号每个工程不一样。这里有个容易翻车的地方DDR3的型号、行列地址宽度、Bank数量会直接影响FSBL生成的ps7_init初始化代码所以硬件设计阶段在Vivado里把PS配置填错后面怎么救都救不回来。不同开发板之间的差异主要体现在几个关键部件上DDR芯片型号和布局、以太网PHY芯片、SD卡控制器连接、UART串口对应的是UART0还是UART1、以及PL侧的引脚分配。网上能找到的BSP包如果跟你的板卡不完全一致不用慌启动所必需的无非就是SD卡、UART、以太网这三样其余外设后续再在设备树和驱动里逐个补齐。我的建议是第一步先把开发板的原理图打开找到PS端的UART引脚和SD卡相关引脚记住它们对应的是哪一个I/O控制器。ZYNQ的PS端UART0和UART1在设备树里是两个不同的节点搞错一个字母启动时串口就是一片空白。2.2 交叉编译工具链的安装与验证交叉编译工具链的选择ZYNQ平台用得最多的是Linaro出品的arm-linux-gnueabihf工具链发行版自带或官网下载均可。个人建议直接使用Linaro的版本因为后面编译U-Boot和内核时它对ARM硬浮点的支持很成熟。安装完成之后建议先验证一下版本arm-linux-gnueabihf-gcc -v输出里能看到Target: arm-linux-gnueabihf这类信息说明架构和浮点ABI正确。还有一个容易被忽略的点是编译U-Boot和内核时如果工具链版本太新可能遇到一些老编译器没遇到过的问题这时可以设置export ARCHarm export CROSS_COMPILEarm-linux-gnueabihf-这两个环境变量在后续的每一次编译里都会用到。此外建议在PATH里加上工具链的bin目录因为后续不只编译一次。2.3 源码版本选择与目录规划源码版本搭配是整个流程里最容易被忽视的坑。Xilinx官方维护着自己的U-Boot分支和Linux内核分支两者的标签号和Vivado版本有对应关系。比如Vivado 2019.1对应u-boot-xlnx的2019.1分支和linux-xlnx的xlnx_release_v2019.1分支如果混着用有时候也能编过但设备树节点、驱动参数可能对不上运行起来会有各种灵异问题。建议在主机上单独建一个工作目录按功能划分子目录mkdir -p ~/zynq_linux/{uboot,kernel,dtb,rootfs,boot,tools}其中uboot放u-boot源码kernel放linux-xlnx源码dtb放编译生成和手工拷贝的设备树文件rootfs放BusyBox构建出来的根文件系统目录boot最终用来组装SD卡第一个分区的内容。源码获取用git clone就够了例如git clone https://github.com/Xilinx/u-boot-xlnx.git -b xlnx_release_v2019.1 uboot git clone https://github.com/Xilinx/linux-xlnx.git -b xlnx_release_v2019.1 kernelBusyBox则直接去官网或者使用git仓库拉取最新稳定版本注意BusyBox不像U-Boot和内核那样跟Xilinx版本强绑定但太老的版本可能在较新的glibc环境下编译有告警通常选1.30以上的稳定版就行。3. FSBL与U-Boot让BootROM交棒给Linux之前3.1 FSBL的生成HDF与FSBL工程FSBL是ZYNQ启动链路上的第二位主角。芯片上电后固化的BootROM先运行它从SD卡/QSPI/NAND里把第一阶段引导文件读进片上RAM读到的就是BOOT.BIN里的FSBL部分。FSBL接着初始化DDR、配置PS时钟、设置MIO的复用、把PL侧bitstream加载进去再把U-Boot搬到DDR里跑起来。生成FSBL的传统方式是先在Vivado里做硬件平台。打开Vivado工程完成PS端的配置后在File菜单里导出硬件描述文件旧版本是HDF文件新版本是XSA文件。然后在Xilinx SDK或者Vitis里基于这个硬件描述文件创建FSBL工程SDK会自动把ps7_init.c编进去生成fsbl.elf。如果你不想用IDE点来点去也可以走命令行。SDK提供了xsdk命令行接口可以指定workspace和硬件描述文件自动生成FSBL但日常使用IDE反而更快。生成fsbl.elf之后建议用记事本打开一下fsbl.elf所在目录下的ps7_init.c确认里面的DDR参数跟你板子上的DDR型号一致。特别是列地址、行地址、BANK数Vivado配置错了生成的FSBL自然也是错的。一个经验是FSBL工程模板里默认只包含PS配置和启动代码。如果PL侧有bitstream不要把在FSBL里手动加载bitstream的代码写进main.c函数Bootgen打包时会自动把bitstream追加到FSBL之后由FSBL统一加载你再手动触发一次反而可能造成二次配置冲突。3.2 U-Boot的编译与配置思路U-Boot的编译是整个流程里变量最多的一步。Xilinx维护的u-boot-xlnx仓库里带了很多板级defconfig例如zynq_zed_defconfig、zynq_zc706_defconfig、xilinx_zynq_virt_defconfig。如果你的板卡是ZedBoard、ZC706这种官方板直接用对应的defconfig基本就能跑。但很多第三方开发板跟官方板不完全一致这时我会采用一个保守策略先拿xilinx_zynq_virt_defconfig编译出一个能用镜像然后把板子特别的地方在设备树里修正。具体编译命令很简单cd ~/zynq_linux/uboot make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- xilinx_zynq_virt_defconfig make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- -j4编译产物是u-boot.elf。如果你的U-Boot版本带了SPLSecondary Program Loader你会发现还有u-boot-spl.bin之类的文件但ZYNQ平台传统流程里不需要它因为我们用FSBL代替了SPL的角色直接把u-boot.elf交给Bootgen打包。U-Boot跟板卡的结合点更多体现在设备树文件上。u-boot-xlnx仓库里的arch/arm/dts目录下有一堆zynq-*.dts文件默认defconfig编译时会带一个基础的设备树进去U-Boot启动时可以用它来初始化DRAM、识别启动介质但最终传给内核的设备树是单独加载的DTB两者互不干扰。所以U-Boot移植阶段只要能启动到“U-Boot 2019.01”这类提示符出现就说明板级差异还在可控范围内。3.3 BIF文件与BOOT.BIN的生成命令有了fsbl.elf、u-boot.elf和可选的bitstream.bit之后下一步就是打包BOOT.BIN。Bootgen是Xilinx官方的镜像打包工具它读一个BIFBoard Initialization File格式的文本文件来知道打包顺序。一个典型的BIF文件长这样the_ROM_image: { [bootloader]fsbl.elf bitstream.bit u-boot.elf }字段顺序很重要。bootloader标签指向fsbl.elf紧跟着是PL侧的bitstream文件最后是u-boot.elf。Bootgen会把FSBL放在最前bitstream和U-Boot按顺序追加到FSBL之后。然后执行bootgen -image boot.bif -o BOOT.BIN -w这条命令需要确保bootgen在你的环境变量里。Vivado安装目录下bootgen通常位于Xilinx/Vivado/xxx/tools/bin目录找不到就find一下。生成好的BOOT.BIN就是BootROM能识别的启动镜像后续往SD卡FAT分区一放就行。提示如果PL工程比较大bitstream文件会占不少体积。对于不需要PL逻辑的纯PS方案BIF里可以不放bitstream这样BOOT.BIN会小很多启动速度也快。4. 设备树与内核配置让Linux认得出你的板子4.1 设备树基础结构与ZYNQ专属写法设备树Device Tree本质上是一份描述板级硬件信息的文件。ZYNQ-7000平台的内核设备树核心是zynq-7000.dtsi这个芯片级文件它定义了PS内部几乎所有的外设控制器节点UART、I2C、SPI、SDHCI、GPIO、DMA等。但它们的status默认多数是disabled具体哪些外设能让它生效由板级设备树文件决定。板级文件通常放在arch/arm/boot/dts/zynq-zed.dts这类位置它用#include包含zynq-7000.dtsi然后覆盖或追加节点信息。如果你用的板子找不到完全对应的dts最快的方法是复制一个相近的官方dts再改成自己板子的实际情况。格式上节点名字和属性有严格语法写错一个逗号或分号编译阶段就直接报错。修改完成之后建议用内核自带的dtc工具编译验证make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- dtbs4.2 板级DTS的修改要点内存、外设与gpiochip编号改动最频繁的几个部分你可以像我一样对着原理图逐项过。第一是内存节点。确认你的DDR容量是512MB还是1GB这一点很多人都栽过跟头。如果你误配为1GB而实际上只有512MB内核启动时会访问不存在的物理地址轻则部分内存访问失败重则无缘无故死机。以512MB为例memory节点的配置写法是memory0 { device_type memory; reg 0x0 0x20000000; };0x20000000就是512MB如果是1GB写0x40000000。别把十六进制算错这是我见过最多的低级错误。第二是串口。ZYNQ的PS端有两个UART控制器开发板通常引出其中一个。设备树里使能对应节点的方法uart1 { status okay; };如果板子用的是uart0节点名就换成uart0。内核里的console参数要与这个节点匹配系统起来之后如果你输入cat /proc/device-tree/amba/seriale0001000/status这类命令能看到节点状态排障时很有用。第三是SD卡控制器。ZYNQ-7020的SDIO控制器一般对应sdhci节点。设备树里需要打开它sdhci0 { status okay; };如果SD卡无法识别重点检查两处一是模式引脚配置二是cadence的mmc驱动有没有编进内核里很多问题不是出在设备树而是出在内核config。第四是GPIO。ZYNQ的PS端GPIO在设备树里只有一个gpioe000a000节点但Linux把它注册成gpiochip时编号并不是从0开始。实际使用中你会发现/sys/class/gpio/gpiochipXXX有很多个其中对应PS端GPIO的chip base不一定为0这个现象让不少人困惑。排查方法很简单cat /sys/kernel/debug/gpio输出里能直接看到芯片的base、ngpio和当前使用情况。编写应用代码时直接使用硬件引脚号加上base值转换成总线编号就行。这个坑属于“知道一次终身受益”的类型。4.3 内核menuconfig与关键驱动开关内核配置是最需要耐心的一步。Xilinx官方内核已经带了一份与硬件平台配套的默认配置xilinx_zynq_defconfig。直接用它能覆盖绝大多数启动所需的配置项然后通过menuconfig做微调。cd ~/zynq_linux/kernel make ARCHarm xilinx_zynq_defconfig make ARCHarm menuconfig重点检查这几个配置开关是否打开CONFIG_SERIAL_XILINX_PS_UARTPS端UART驱动必须为y否则串口没有输出CONFIG_SERIAL_CORE_CONSOLE串口作为控制台必须为yCONFIG_MMC_SDHCI、CONFIG_MMC_SDHCI_OF_ARASANSD卡控制器驱动y和m均可但y更省事CONFIG_NET_CADENCEZYNQ的以太网MAC驱动y或mCONFIG_DEVTMPFS_MOUNT让内核自动挂载devtmpfs到/dev根文件系统没配好时这个可以救命如果你的工程用了initrd或者initramfs还要打开CONFIG_BLK_DEV_INITRD。如果不打算额外做initramfs直接挂载SD卡根文件系统就行。4.4 编译uImage与DTB内核编译生成U-Boot能直接识别的镜像格式常见的是uImage。ZYNQ平台的内核加载地址U-Boot通常约定在0x8000因此编译命令里要指定make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- UIMAGE_LOADADDR0x8000 uImage -j4注意这个UIMAGE_LOADADDR是内核镜像要加载到DDR里的地址不是开发板物理内存起始地址。DDR实际起始地址是0x00100000左右内核zImage加载地址0x8000会由U-Boot通过bootm命令重新映射这个约定不能随便改改乱了就会出现“跳转之后什么都没有”。设备树的编译通常顺着内核一起完成make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- dtbs生成的DTB文件名你可以在arch/arm/boot/dts目录里找到。如果是自己新建的dts文件记得先把它加进对应Makefile的dtb-y变量里否则make dtbs不会生成。5. BusyBox根文件系统没有它内核只是半成品5.1 BusyBox交叉编译与安装内核启动之后最后一步是挂载根文件系统。Linux内核本身只负责驱动硬件、管理进程和内存Shell、ls、cat、mount这些命令日常使用的一切工具都来自根文件系统。做一个最简的根文件系统BusyBox是首选它在一个二进制文件里集成了几百个常用命令。BusyBox的编译套路很固定先在源码根目录执行默认配置再交叉编译cd ~/zynq_linux/busybox make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- defconfig make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- -j4编译完成后不要急着找可执行文件busybox命令的安装方式有讲究。建议先用menuconfig把“Build static binary”选项打开也就是把busybox编译成静态链接版本make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- menuconfig在Settings → Build static binaryno shared libs里选择y。这样生成的busybox不依赖任何动态库拷到开发板上直接能跑省掉后面拷贝一堆.so文件的麻烦。代价是体积稍大但对嵌入式开发来说这点体积完全可接受。然后执行安装make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- install CONFIG_PREFIX$PWD/rootfsCONFIG_PREFIX指定安装目录busybox会把可执行文件和一个bin目录结构放到rootfs里。安装完成后rootfs/bin/busybox就是核心文件bin和sbin下面是它的符号链接。5.2 手动搭出根目录骨架与设备节点BusyBox安装生成的目录结构还比较简陋启动时需要手动补齐几个关键目录cd ~/zynq_linux/busybox/rootfs mkdir -p dev etc init.d lib proc sys tmp var mnt home rootpro、sys、tmp这三个目录建议真实存在因为内核启动时mount到这些挂载点如果目录不存在mount会报错。dev目录在启用了devtmpfs之后由内核自动填充但前期没有它系统会无法创建设备节点。如果你在menuconfig里没有开静态编译而是用的动态库方式那么lib目录里需要拷入工具链的共享库。库文件路径一般在arm-linux-gnueabihf/libc/lib目录下把libc.so.6、ld-linux-armhf.so.3等全部复制过去cp -P /path/to/arm-linux-gnueabihf/libc/lib/* rootfs/lib/注意用-P保留符号链接不然程序运行时找不到so文件。5.3 /etc配置与启动流程根文件系统能不能正常启动很大程度上看/etc目录下的配置是否正确。一个最小可用的/etc需要三个文件inittab、fstab、init.d/rcS。inittab告诉init进程该启动哪些程序内容大致如下::sysinit:/etc/init.d/rcS ::respawn:/sbin/getty 115200 ttyPS0 ::ctrlaltdel:/sbin/reboot第一行的rcS是系统初始化脚本第二行是为串口终端创建一个登录会话这样连接串口就能看到shell提示符。fstab定义挂载规则proc /proc proc defaults 0 0 sysfs /sys sysfs defaults 0 0 tmpfs /tmp tmpfs defaults 0 0rcS脚本是系统初始化的核心这里负责挂载虚拟文件系统、配置mdev热插拔#!/bin/sh mount -t proc none /proc mount -t sysfs none /sys mount -t tmpfs none /tmp echo /sbin/mdev /proc/sys/kernel/hotplug mdev -smdev是BusyBox里简化版的udev它能根据内核hotplug事件自动创建dev目录下的设备节点。如果你发现/dev目录为空设备无法访问大概率就是rcS脚本里没执行mdev -s或者内核的CONFIG_DEVTMPFS_MOUNT没开。最后给rcS加可执行权限chmod x rootfs/etc/init.d/rcS这样一个最小根文件系统就算成型了。6. 烧写、启动与常见问题速查6.1 SD卡分区与镜像烧写SD卡建议分两个分区第一个分区是FAT32放BOOT.BIN、uImage和devicetree.dtb第二个分区是ext4把rootfs的全部内容拷进去。这套结构跟U-Boot的启动命令是对应的也是ZYNQ开发板上最常用的布局。用fdisk分区时注意第一个分区起始位置建议从1MB偏移开始不要从0开始因为有些U-Boot和SD卡控制器对SD卡前几个扇区的访问有兼容性问题。sudo fdisk /dev/sdX分区完成后sudo mkfs.vfat -F 32 /dev/sdX1 sudo mkfs.ext4 /dev/sdX2然后挂载拷贝mount /dev/sdX1 /mnt/boot cp BOOT.BIN uImage devicetree.dtb /mnt/boot/ mount /dev/sdX2 /mnt/rootfs cp -a rootfs/* /mnt/rootfs/ sync如果你在U-Boot里看到SD卡但找不到文件先检查FAT格式和文件名是否够短BOOT.BIN和uImage这些名字都没有超过8.3命名规则问题不大。但如果你用了大小写混排记得U-Boot的FAT驱动是区分大小写的跟FAT32平时的行为不完全一致。6.2 bootargs与bootcmd参数设置U-Boot是动态加载内核的它需要知道三件事内核镜像在哪、设备树在哪、用哪些启动参数。这些都通过U-Boot环境变量设置setenv bootargs consolettyPS0,115200 root/dev/mmcblk0p2 rw rootwait setenv bootcmd fatload mmc 0 0x3000000 uImage; fatload mmc 0 0x2000000 devicetree.dtb; bootm 0x3000000 - 0x2000000 saveenvbootargs里的consolettyPS0必须与设备树里使能的uart节点一致。如果你使能的是uart0这里就是ttyPS0使能的uart1同样还是ttyPS0只是设备树里对应的是uart1节点——ZYNQ的PS端串口设备名都叫ttyPS0区别在于底层注册的amba外设地址。root/dev/mmcblk0p2表示根文件系统在SD卡第二个分区rootwait确保内核等到SD卡设备挂载完成后再尝试挂载根文件系统避免启动时序问题。bootcmd里的地址选择有讲究。0x3000000是uImage的加载地址0x2000000是设备树的加载地址两者都必须在DDR可利用内存范围内又不能跟U-Boot自身的运行区域重叠。以ZYNQ-7020的DDR从0x00100000开始为例0x3000000和0x2000000都在安全区不会覆盖U-Boot代码和堆栈。6.3 故障排查实录与经验技巧传统方式移植Linux最容易翻车的几个地方我按表现症状列成了一张速查表方便你对照症状大概率原因排查方向串口完全无输出UART节点配置错 / 波特率不对检查设备树uart节点检查fsbl里的MIO配置串口输出乱码U-Boot时钟配置与板卡不符核对PS输入时钟频率和U-Boot里的CONFIG_SYS_时钟配置打印Starting kernel...后无反应内核加载地址与U-Boot跳转地址不一致检查bootm参数和设备树地址卡在Uncompressing Linux...内核镜像损坏或加载地址重叠重新拷贝uImage检查UIMAGE_LOADADDR报No working FDT found设备树没加载成功检查DTB文件是否存在于FAT分区fatload路径是否正确挂载mmcblk0p2失败SD卡分区不是ext4 / 驱动没编译确认分区格式检查MMC驱动配置打印No init found根文件系统里的init进程丢失或不可执行检查rcS权限、busybox安装目录结构GPIO编号对不上多个gpiochip叠加导致base偏移用cat /sys/kernel/debug/gpio查看实际映射最典型的案例是“串口无输出”。有一次我在一块新板子上移植系统串口终端什么打印都没有用示波器量TX引脚波形发现根本没有任何数据。排查了很久最后一查原理图这根UART默认接到了PS端的MIO引脚但我在Vivado的硬件配置里把它留成了reset状态。FSBL没有使能MIO对应的引脚复用UART当然不会工作。这类问题在板级移植时非常常见排查思路是先看FSBL里的ps7_init再看设备树最后才轮到内核配置。另一个高频问题就是内核报“No working FDT found”。这个错误一般出现在U-Boot启动阶段通常是你指定了设备树地址但那个地址上根本没有合法的FDT blob。我遇到过编译完dtbs之后忘记拷贝到SD卡的情况U-Boot在fatload时默认失败但bootm还是继续执行最后内核起来之后发现没有设备树直接拒绝启动。把DTB文件放到FAT分区并确认fatload成功问题就解决了。还有一个值得单独提出来讲的是“启动过程中卡死在U-Boot命令行”。如果你在U-Boot启动时按了任意键它就会停在命令行等待输入这不是故障而是U-Boot的交互模式。如果你不想每次开机都停在命令行确认一下bootdelay变量不为0并且bootcmd里写的是启动命令而不是reset。6.4 一个扩展话题PL访问PS DDR与ILA观测这个系列虽然是Linux移植但既然ZYNQ是PSPL的SoC很多时候你的Linux跑起来不只是为了跑系统而是要跟PL侧的FPGA逻辑协同工作。两个常用的点我提前在这里提一下。第一个是PL访问PS外挂DDR。在Vivado工程里PL侧的AXI接口可以通过AXI_GP或AXI_HP端口访问PS端的DDR内存。其中AXI_GP端口的访问地址在0x40000000到0x7FFFFFFF区间对应的是PS端DDR的映射空间。你在Linux应用里如果直接操作这个地址通常需要先通过/dev/mem或UIOUserspace I/O驱动把物理地址映射到用户空间。设备树里则可以通过reserved-memory节点预留一块内存避免内核把这块空间分配出去否则跑着跑着数据被覆盖也没人知道。只要这一步配好PL和PS之间通过共享内存交换数据的通信方式就打通了。第二个是PS加载PL bitstream之后观测ILA信号。ILA是Vivado里非常常用的逻辑分析仪调试IP。当PL侧逻辑运行、bitstream从BOOT.BIN里被FSBL自动加载之后你可以在Vivado Hardware Manager里连接JTAG选中debug hub然后手动运行ILA的trigger。如果开发板同时连接了JTAG和调试串口这种“PS跑Linux、PL跑FPGA、ILA看波形”的组合调试方式是完整可用的。唯一要注意的是I_LA观测窗口的采集不需要Linux参与它在FPGA逻辑内部就已经完成所以即使Linux崩溃了只要PL逻辑还在工作ILA照样能看到波形。最后再分享一个小经验整个传统移植流程走完一遍之后我最大的体会是这个过程能让你对ZYNQ的启动架构产生肌肉记忆而不是停留在“按钮会点”的认知层面。以后再看到启动日志你会下意识地在一秒钟内定位当前处于哪一段看到Xilinx Zynq Boot First Stage这是FSBL在干活看到U-Boot 2019.01引导程序已经接管看到Starting kernel...马上要进内核看到Freeing unused kernel memory根文件系统马上就要接管了。这个定位能力在之后调试任何复杂的嵌入式系统时都极其值钱。关于那个设备树里memory节点的小坑我再啰嗦一句拿到一块板子第一步先查DDR芯片型号和容量第二步再动设备树。如果连DDR容量都对不齐后面的一切排查都会变成在错的地基上修房子。这个系列随着项目推进会继续更新下一篇准备讲一下现代化的自动化构建方式以及如何把传统流程里的那些手工步骤映射到工具链的配置项上。当你亲手用传统方式跑通过一次Linux之后再去看那些自动化方案你会有一种“原来它帮我干的活我也都干过”的踏实感。如果你在移植过程中遇到什么疑难问题欢迎把启动日志贴出来一起分析很多启动问题其实从打印信息上就能看出七八分端倪。