Zynq7000混合启动方案:QSPI引导+eMMC存储,打造稳定可靠的嵌入式Linux系统
做Zynq7000的板卡启动方案几乎决定了后续软件工作的复杂度。这篇聊一个我实测过很多次的组合QSPI Flash只放引导镜像eMMC用ext4承载内核和根文件系统。这种混合启动方案既兼顾了启动可靠性和存储容量又不像纯SD卡方案那样容易被拔走或者受干扰。适合正在做Zynq-7010/7020/7030量产固件、想给产品做OTA双备份、或者被QSPI Flash容量挤到墙角的工程师。先说结论QSPI负责“把系统拉起来”eMMC负责“把系统装下来”。听起来简单但实际落地时涉及BootROM配置、U-Boot环境变量、eMMC引脚分配、ext4分区布局等一系列细节任何一个环节没对齐都容易卡住。这篇文章我把整个流程拆开讲包括硬件设计要注意什么、BOOT.bin怎么生成、eMMC怎么分区格式化、U-Boot怎么设置启动参数以及我踩过的几个典型坑希望帮你少走弯路。1. 为什么需要混合启动方案选型与整体思路1.1 从启动介质的选择说起Zynq7000支持的启动介质不少QSPI Flash、NAND、SD卡、eMMC、JTAG、Nor Flash等。很多人一开始会纠结到底用哪个。如果只追求简单SD卡最方便U-Boot和内核都能直接放开发时还能随时拔卡换系统。但有一个致命问题SD卡是插拔器件接触不良、物理损坏、被误拔的概率都很高产品出货后根本不敢用。纯QSPI方案也不是不行但Zynq的QSPI启动镜像通常要放FSBL、bitstream、U-Boot再挤一点空间放内核和设备树轻轻松松就到8MB以上。而且内核和根文件系统如果都要塞进QSPI必须用JFFS2或UBIFS这类Flash文件系统维护麻烦更新也慢。更关键的是大容量QSPI Flash成本不低尤其是工业级宽温型号。这时eMMC就显出来了。eMMC是焊接在板上的BGA颗粒可靠性和寿命远高于SD卡容量从4GB到64GB随便选价格还比同容量的QSPI便宜。所以混合启动方案的思路就是QSPI只存最核心的启动镜像即BOOT.binFSBLBitstreamU-Boot内核、设备树和根文件系统全部放eMMC根文件系统直接用成熟的ext4格式。1.2 混合启动方案的核心分工这套方案的实际运行路径是Zynq上电后BootROM从QSPI Flash读取BOOT.bin运行FSBL完成PS初始化并加载bitstream然后跳转到U-Boot。U-Boot再去初始化eMMC从eMMC读取内核和设备树最后挂载eMMC上的ext4分区作为根文件系统启动Linux。我习惯把eMMC分成两个分区第一个分区用来放内核和设备树格式可以是FAT32也可以是ext4第二个分区放根文件系统格式是ext4。很多人问为什么要单独分一个boot分区直接把内核放在根文件系统里不行吗行是行但没必要。分开之后U-Boot加载内核时扫一下第一个分区就行不需要解析复杂的根文件系统结构也不容易因为根分区损坏导致内核都读不出来。这算是我个人的习惯实际按你的需求调整也行。这种分工的好处很明显QSPI里的BOOT.bin一般只有1MB到2MB烧写时间非常短量产时可以用JTAG批量烧录或者留一个U-Boot命令在线更新。eMMC里的文件系统则完全按Linux的玩法来升级内核就覆盖boot分区里的uImage升级应用就替换根文件系统里的目录OTA也方便设计。2. QSPI Flash引导阶段从BootROM到U-Boot2.1 Zynq7000的BootROM流程复习Zynq7000内部有一个固定不变的BootROM上电后会先根据硬件管脚状态决定从哪个介质启动。这个管脚组合是MIO[6:2]其中QSPI启动对应的编码是0b00010SD/eMMC启动是0b00110JTAG是0b00100具体数值可以查UG1085。很多开发板都会做成拨码开关调试时切来切去量产板直接用电平固定。BootROM选中QSPI之后会在Flash的0x0地址查找BootROM Header也就是我们常说的“启动头”。这个头部结构至少要包含镜像大小、跳转地址、校验信息等。BootROM读到合法头部后把QSPI里的FSBL镜像拷贝到OCM或者DDR然后跳到FSBL执行。FSBL是全功能的初始化程序负责配置DDR、MIO、PLL等之后加载bitstream到PL最后把U-Boot从QSPI拷贝到DDR并跳进去。这里有个值得强调的点很多人以为QSPI里只有U-Boot就行其实BootROM自己不会直接加载U-Boot它认的是FSBL生成的BOOT.bin格式。所以不管你是从SDK里生成还是用bootgen命令行最终烧进QSPI的必须是BOOT.bin而不是裸的U-Boot镜像。2.2 硬件连接与启动模式引脚设置如果你在画板子QSPI Flash的接法相对固定Zynq的PS端QSPI引脚主要是MIO1到MIO6这一组FPGA配置时分单路和双路模式。单路模式用一片Flash双路模式用两片Flash组成Quad SPI双通道。大部分板卡只接单路布线时注意把SCK、CS#、DQ0到DQ3做成等长Flash电源引脚需要一个0.1uF和1uF到10uF的电容滤波。启动模式引脚要特别留意上拉下拉电阻。MIO[6:2]在复位期间被BootROM采样所以这些引脚不能悬空必须通过电阻拉到确定的电平。我见过一块板子因为MIO3的上拉电阻贴错了导致拨码拨到QSPI却从JTAG启动查了半天才发现是启动模式电平有问题。最稳妥的办法是原理图设计阶段就按ZXynq Boot Mode真值表把每个MIO的默认电平标出来再跟PCB实物对一遍。QSPI Flash的型号选择也影响启动稳定性。Zynq BootROM对Flash制造商和型号有兼容性列表比如常见的Winbond W25Q128、Micron N25Q128、Spansion S25FL128S等都能直接支持。选型时尽量选列表内的我踩过用国产Flash导致第一批板卡能启动、第二批死活不启动的坑后来查出来是BootROM对某些非标准SFDP解析有兼容性问题。所以量产项目别在这个地方省小钱。2.3 生成BOOT.bin并写入QSPI以Vivado 2023.1为例在Vivado里综合出bitstream之后打开SDK/Vitis写一个最小的FSBL工程然后创建Boot Image。Boot Image的partition顺序必须是FSBLbootloader属性、bitstreamdatafile属性、U-Bootbootloader属性。这样bootgen才会生成规范的BOOT.bin。如果顺序错了或者FSBL没标注bootloader烧进去大概率起不来。我常用的生成命令是bootgen -image boot.bif -o i BOOT.bin -w on其中boot.bif长这样the_ROM_image: { [bootloader]fsbl.elf system.bit u-boot.elf }注意bitstream不是必须的。如果你的产品PL侧要在Linux起来后才配置或者用器件镜像的方式单独管理PL配置那BOOT.bin里可以不放bitstream。省掉bitstream还有一个好处FSBL加载阶段少一个步骤启动时间能快不少。生成BOOT.bin之后写QSPI的方式很多。开发阶段我一般用Vivado Hardware Manager配合Digilent JTAG在Flow里选择Add Configuration Memory Device填好Flash型号和BOOT.bin路径直接烧。量产阶段建议在U-Boot里用命令写先通过以太网或USB把BOOT.bin传到DDRsetenv serverip 192.168.1.100 setenv ipaddr 192.168.1.10 tftpboot 0x2000000 BOOT.bin sf probe 0 0 0 sf erase 0x0 0x1000000 sf write 0x2000000 0x0 0x1000000这里sf erase和sf write的地址和长度一定要对齐到Flash的擦除粒度。我通常把长度设置成BOOT.bin文件大小的整数倍宁可多擦一点也不能漏否则残留的旧数据可能导致启动头被污染。2.4 一个容易踩的坑QSPI地址对齐与启动头我见过不少人在QSPI烧写时吃过亏。一个典型场景BOOT.bin实际只有800KB但QSPI的最小擦除块是4KB如果不做对齐就写入新镜像和旧镜像的碎片混在一起BootROM解析时会把中间某些偏移当成启动头的一部分结果就是上电后死循环或者跑到未知地址。解决办法很简单每次更新QSPI时先整片擦除至少把BOOT.bin所在区域完整擦掉然后再写入。如果你用了双镜像方案A/B分区升级那要严格保证每个镜像分区起始地址对齐到Flash的sector边界并且在U-Boot里维护一份分区表避免覆盖。还有一个跟地址相关的点大容量QSPI Flash256Mbit以上默认可能是3字节地址模式超过128Mbit之后BootROM需要进入4字节地址模式才能读到高地址的内容。Zynq BootROM对这部分支持不算好我建议QSPI只放启动镜像别把内核或文件系统也塞进去。如果非要放尽量放低地址或者确认FSBL里配置了对应的地址模式否则后面就是无穷无尽的兼容性问题。3. eMMC ext4文件系统部署分区、格式化与根文件系统迁移3.1 eMMC硬件接入与关键引脚eMMC本质上是NAND Flash加了一个MMC控制器对外暴露标准的MMC接口不需要你自己做坏块管理或者磨损均衡。Zynq7000的PS端有两个SDIO控制器SD0和SD1都可以接eMMC。实际项目中eMMC常接在SD1上因为SD1没有SD卡的插卡检测和写保护引脚更贴合eMMC的直连方式。布线时要注意的引脚我用153Ball封装的eMMC来举例。大家都说“153ball eMMC引脚定义”其实真正需要关心的信号线没那么多主要有CLK时钟信号由主机提供一般25MHz到200MHz根据eMMC版本和模式决定。CMD命令线双向主机发送命令、eMMC返回响应都走这根线。DATA0~DATA78根数据线Zynq的SDIO控制器只支持4位模式所以至少接DATA0~DATA3DATA4~DATA7可以悬空。VCC主电源通常3.3V给NAND和控制器供电。VCCQI/O电源1.8V或3.3V由设计决定Zynq的MIO bank电压必须匹配。RST_N硬件复位建议由GPIO控制启动时拉低再释放。DS数据选通信号HS400模式才用到Zynq用不上可以不接。很多新手以为eMMC一定要8根数据线全接其实Zynq只支持SD 4-bit模式所以接4根就够了。我甚至见过某些开发板只接DATA0-DATA3跑200MB/s的内核加载速度依然够用。真正的难点在于CLK、CMD、DATA要走等长尽量包地处理避免高速信号辐射干扰。设计时给VCC和VCCQ各加一个4.7uF和一个0.1uF的退耦电容位置贴近eMMC的电源引脚。3.2 在Linux下分区与格式化首次部署eMMC时我会先用一个能完整启动的SD卡或QSPI系统进入Linux然后在Linux里操作eMMC。先确认设备节点ls /dev/mmcblk*如果你的是Zynq板卡SD0插的SD卡通常是/dev/mmcblk0SD1接的eMMC是/dev/mmcblk1。注意这不是绝对的取决于设备树和驱动注册顺序稳妥的办法是看/sys/block/mmcblk*/device/type和/sys/block/mmcblk*/device/name能区分出MMC、SD、SDIO等类型。确认无误后全新盘先分区fdisk /dev/mmcblk1我用fdisk只做两件事创建GPT分区表然后划分两个分区。第一个分区我习惯分512MB到1GB用来放内核和设备树格式可以稍后格式化成FAT32方便U-Boot读取。第二个分区用剩余全部空间作为根文件系统格式化成ext4。分区完成后格式化mkfs.fat -F 32 -n boot /dev/mmcblk1p1 mkfs.ext4 -L rootfs /dev/mmcblk1p2在Ubuntu 22.04下如果提示缺mkfs.fat装一下dosfstools就行。格式化ext4的时候我一般会留一点额外空间不用把整个分区塞满因为ext4默认的保留块是5%对根文件系统来说能避免文件系统碎片化导致的启动失败问题。3.3 构建根文件系统并迁移到eMMC根文件系统的来源很多可以用Yocto或Buildroot生成也可以直接拿已有的SD卡系统拷贝过去。我的经验是先用Debian的debootstrap构建一个干净的rootfs再把应用层代码放进去这样依赖关系干净不会带入一堆旧配置。构建rootfs后挂载eMMC根分区并拷贝mount /dev/mmcblk1p2 /mnt/rootfs rsync -aH --exclude/proc --exclude/sys --exclude/dev --exclude/run /path/to/rootfs/ /mnt/rootfs/ mkdir -p /mnt/rootfs/{proc,sys,dev,run,tmp} umount /mnt/rootfsrsync比cp好在能保留符号链接、权限和设备文件属性。拷贝完之后还要检查/etc/fstab。因为我用eMMC当根文件系统fstab里要么写/dev/mmcblk1p2要么写PARTUUID后者更稳防止设备节点漂移。用blkid /dev/mmcblk1p2查到PARTUUID然后写进fstabPARTUUIDxxxxxx / ext4 defaults,noatime 0 1同时把FAT32 boot分区也挂载上方便后续直接把内核拷进去。3.4 U-Boot环境变量与内核启动参数QSPI里的BOOT.bin只负责到U-Boot剩下的活全在U-Boot环境变量里。我的做法是启动时执行一段固定的bootcmd逻辑很简单初始化eMMC从第一个分区读内核镜像和设备树到DDR设置bootargs后启动。常用U-Boot命令大概是这样的setenv bootargs consolettyPS0,115200 root/dev/mmcblk1p2 rw rootfstypeext4 rootwait setenv loadboot mmc dev 0; fatload mmc 0:1 0x2080000 uImage; fatload mmc 0:1 0x2000000 system.dtb setenv bootcmd run loadboot; bootm 0x2080000 - 0x2000000 saveenv这里最关键的是root/dev/mmcblk1p2如果根文件系统在另一个设备节点启动时会直接Kernel Panic。另外rootwait一定要加因为eMMC在Linux启动早期可能还在初始化不加rootwait会出现找不到根设备的情况。如果你的内核镜像不是FIT image而是uImage加dtb分开的那就用bootm 内核地址 - dtb地址。如果你用ext4格式放内核U-Boot也支持ext4load不过我还是更推荐FAT分区放内核U-Boot对FAT的操作最成熟即便eMMC根分区坏了也不影响你加载内核。4. 实战中的坑与排查技巧实录4.1 U-Boot无法识别eMMC这个坑我遇到太多次了。现象是U-Boot启动后在命令行输入mmc list只看到SD卡看不到eMMC。排查分几步走第一查硬件第二查U-Boot配置第三查设备树。硬件方面首先要确认eMMC的RST_N有没有被正确拉起来很多eMMC需要复位引脚从低到高的跳变才会进入正常状态。如果RST_N悬空有些型号也能工作但不稳定。其次是VCCQ的电压是否和Zynq MIO bank的电压一致Zynq的SDIO bank如果配了1.8VeMMC的VCCQ也要1.8V否则信号电平不匹配。U-Boot方面Zynq的U-Boot配置里需要打开CONFIG_MMC_SDHCI_ZYNQ、CONFIG_MMC_SDHCI、CONFIG_CMD_MMC等选项。如果你是用Xilinx官方U-Boot分支默认配置一般没问题。如果自己裁剪过U-Boot就看日志里有没有sdhci-zynq相关的probe信息。设备树方面SD1节点要正确地描述在sdhci1里并且status okay。还有一个容易忽略的点Zynq的SDIO控制器有一个xlnx,has-cd和xlnx,has-wp属性eMMC没有卡检测和写保护引脚所以这两个属性必须设为0。如果U-Boot的驱动在等待卡检测信号超时就会导致设备识别失败。4.2 内核挂载ext4根文件系统失败启动到内核后出现Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(179,2)这个报错说明内核找不到或者挂不上根文件系统。可能性很多我排查时一般按照下面的顺序来。第一检查root参数写的对不对。root/dev/mmcblk1p2是裸设备节点但如果你内核里用的是initramfs则root参数可能需要传给initramfs内的脚本而不是直接给内核。第二确认内核里有没有编译ext4驱动。很多人为了简化内核把CONFIG_EXT4_FS编成了模块结果根文件系统在ext4上模块本身又存在根文件系统里这就成了死循环。正确做法是把CONFIG_EXT4_FS和CONFIG_EXT4_USE_FOR_EXT2都编进内核y不要用m。第三如果设备节点没问题、ext4也编进了内核那就要考虑是不是eMMC的硬件信号不稳定导致内核在读取分区表时偶发失败。这种问题最难查我碰到过一块板子因为CLK线上串了一个0欧电阻阻值偏大信号质量差内核时好时坏。解决办法是在设备树里调低SDIO时钟频率比如加speed或max-frequency属性先降到25MHz验证稳定性。4.3 启动速度优化QSPI到eMMC的切换混合启动方案的启动时间主要在两部分BootROM从QSPI读BOOT.binU-Boot从eMMC读内核。QSPI读BOOT.bin一般只有几MB跑Quad SPI的话不到半秒。大头在eMMC的初始化因为eMMC上电后需要经历内部上电时序、硬件复位、模式协商等过程如果U-Boot把每个步骤的延时都做得比较长整体就会慢。优化手段我试过几个首先在设备树或U-Boot里关闭不必要的探测。比如Zynq板卡上有SD0和SD1两个节点U-Boot会去扫描两个口的设备如果SD0根本没接卡就把SD0的status设为disabled能省下几百毫秒。其次U-Boot的mmc命令在读取内核前先执行mmc dev 0和mmc info这个初始化过程无法避免但你可以把mmc rescan去掉减少一次重新探测。另外一个容易被忽略的点是eMMC的预设速度模式。Zynq的SDIO控制器最高支持SDR104模式如果你的eMMC和U-Boot驱动都支持速度能到100MB/s左右。但在实际调试阶段我不建议上来就追求高速先用默认的默认传输模式跑通再通过U-Boot的mmc dev 0和mmc info确认当前速度时序最后再尝试提升。4.4 问题排查速查表症状可能原因解决方向上电后U-Boot串口无输出启动模式脚错误、BOOT.bin损坏、QSPI没烧录成功检查MIO[6:2]电平重新烧BOOT.bin换JTAG启动确认系统能跑U-Boot能启动但mmc list没有eMMCSDIO硬件或设备树配置问题查供电、复位、VCCQ电压检查设备树sdhci节点U-Boot能识别eMMC但fatload读不到内核分区格式不对或分区号不对用mmc part查看分区表用ls确认目录内核启动后VFS unable to mountroot参数错误、ext4未编译进内核核对root设备节点确认CONFIG_EXT4_FSy启动时eMMC读数据偶发错误信号完整性、时钟过高、电源纹波降低SDIO时钟频率检查布线等长加电容滤波根文件系统挂上了但很快只读ext4文件系统异常、掉电损坏重新mkfs检查fstab是否用了ro选项5. 最后一点个人经验Zynq7000的混合启动方案说白了就是“各司其职”四个字。QSPI就是个跳板负责把系统拉起来别指望它存多少东西eMMC才是真正的数据家园分区、格式化、挂载都要按照Linux的标准玩法来。我最开始做这个方案时也差点把内核和文件系统都塞进QSPI后来算了一下8MB的QSPI根本不够用才彻底转向eMMC。要说最值得你记住的一个建议那就是量产之前一定先把启动链路完整跑通至少几百次包括反复断电、频繁复位、恶劣温度下启动。eMMC和QSPI都属于嵌入式存储它们的时序和电源敏感性比想象中高很多问题在实验室里一天复现不了一次一旦出货就频繁出现。我在自己的板子上就遇到过低温下eMMC初始化失败的问题后来在硬件上增加了复位延时和电源时序控制才解决。另外如果你打算后续做OTA升级这套方案本身就很好扩展。BOOT.bin作为基础固件可以用U-Boot里的QSPI写命令做A/B分区切换内核和设备树放在eMMC的boot分区升级时直接替换文件再标记一下下一个启动分区根文件系统放在ext4分区用update-inetd、dpkg或者简单的tar包覆盖都行。只要分区规划清晰整个系统的远程升级链路就能建立起来。最后建议各位在开始前先看一遍UG1085和UG1144虽然文档枯燥但很多配置细节和坑都在里面写着。等你把QSPI引导和eMMC ext4部署这条路走通一次后面再做ZynqMP或者其他FPGASoC平台思路其实完全一致。祝调试顺利有问题欢迎在评论区交流。