1. 为什么程序固化这件事值得单独拿出来讲
搞Zynq 7020的兄弟大多有过这样的经历:在Vitis里点下载,程序跑得好好的,断电重启,板子跟没烧过一样,还是跑的老逻辑。这不是代码写错了,是程序固化这一步没做对。所谓固化,就是把编译生成的比特流和可执行文件打包成一个上电自启动的镜像,写进板载的QSPI Flash里,让FPGA在每次上电时自己从Flash里把配置和程序加载起来。
这件事听起来简单,但真正动手的时候,坑一个接一个:BOOT.bin生成时报错找不到fsbl.elf、烧录时提示找不到Flash器件、烧完不启动、启动了一半卡死……我见过太多人卡在这一步,明明逻辑没问题,就是固不进去。这篇内容就是把我自己在7020上反复折腾固化的完整链路梳理一遍,从FSBL的生成、BOOT.bin的打包,到Flash烧录和上电验证,每一步都讲清楚为什么这么做、容易在哪出问题。
适合的读者是已经能在Vitis里正常下载调试、但还没跑通固化流程的嵌入式开发者,也适合那些固化过一次但换板子又翻车、想搞明白底层逻辑的人。下面所有操作基于Vivado/Vitis 2020.x及以上版本,7020的QSPI Flash以常见的W25Q256为例,其他容量和型号思路一致,参数需要对应调整。
2. 固化前必须搞清楚的三个概念
2.1 FSBL到底在启动链路里扮演什么角色
Zynq的启动不是FPGA自己读比特流那么简单,它是一个多阶段引导的过程。上电后,芯片内部的BootROM先跑起来,这段代码是固化在芯片里的,改不了。BootROM会根据MIO引脚的电平判断启动模式,如果是QSPI启动,它就去Flash的固定偏移地址读一个头部,这个头部指向FSBL(First Stage Boot Loader)。
FSBL是用户自己生成的,它的职责很明确:初始化DDR控制器、配置时钟、把比特流写进PL、把应用程序搬到DDR里,最后跳转到应用程序入口。你可以把BootROM理解成电脑的BIOS,FSBL理解成GRUB之类的引导程序,应用程序才是真正的操作系统或业务代码。
这里有个关键点:FSBL必须包含比特流,否则PL不会被配置。很多人固化完发现PS端程序跑了但PL逻辑没生效,十有八九是FSBL里没带bitstream,或者BOOT.bin打包时漏了。
2.2 BOOT.bin里到底装了哪些东西
BOOT.bin本质上是一个按特定格式排列的容器,里面通常包含三个部分,顺序不能乱:
| 顺序 | 内容 | 作用 | 是否必须 |
|---|---|---|---|
| 1 | FSBL.elf | 第一阶段引导,初始化DDR和PL | 必须 |
| 2 | system.bit | PL的比特流配置 | 用到PL就必须 |
| 3 | app.elf | 用户应用程序 | 必须 |
BootROM只认第一个分区是FSBL,它会把FSBL加载到OCM里执行。FSBL再去解析后面的分区,把bitstream写给PL,把app.elf搬到DDR。所以顺序错了,或者FSBL不是第一个,启动必然失败。
注意:如果你的设计里PL完全不参与,只用PS端,那bitstream可以省掉,但FSBL和app.elf一个都不能少。
2.3 QSPI Flash的地址映射和分区规划
7020常见的QSPI Flash是256Mb(32MB)的W25Q256,也有128Mb的。BOOT.bin一般从Flash的0x00000000地址开始放。FSBL本身不大,通常几十KB到一百多KB,bitstream看逻辑规模,7020的话一般1.5MB到3MB之间,app.elf看代码量。
我一般建议在Flash里做个简单规划:BOOT.bin从0地址开始,后面留一块区域给数据存储或者双镜像备份。如果做双镜像(Golden Image + Update Image),那就要在Flash里放两份BOOT.bin,通过MultiBoot机制切换。这个后面会单独说。
3. 从Vivado到Vitis:生成可固化的镜像文件
3.1 导出硬件平台时容易漏掉的一步
在Vivado里综合实现完成、生成bitstream之后,不要急着直接打开Vitis。正确做法是Export Hardware,而且一定要勾选Include bitstream。这一步导出的.xsa文件里包含了硬件描述和比特流,Vitis创建平台工程时才能拿到bitstream。
我踩过的坑:有一次导出时没勾Include bitstream,后面在Vitis里生成BOOT.bin时怎么都找不到.bit文件,翻遍了工程目录才发现根本没导出来。所以导出后建议去.xsa所在目录确认一下,或者直接在Vitis里创建平台时看能不能识别到bitstream。
3.2 创建FSBL工程的正确姿势
在Vitis里,File → New → Application Project,选好平台后,模板里选Zynq FSBL。这一步Vitis会自动帮你生成FSBL的源码,包括main.c、fsbl.h等。
生成完之后,不要急着编译,先检查几个地方:
- DDR型号和容量:FSBL里的DDR初始化参数是根据你的硬件设计来的,如果Vivado里DDR配置和实际板子不一致,FSBL跑起来会在DDR初始化阶段卡死。7020常见的DDR3是MT41K256M16,容量512MB,确认ps7_ddr配置对得上。
- QSPI控制器:确认FSBL里使能了QSPI驱动,否则后面烧录和启动都会有问题。
- 串口打印:建议在FSBL里保留串口输出,方便调试。启动时能看到FSBL打印的版本信息和各阶段状态,出问题了好定位。
编译FSBL,生成fsbl.elf。如果编译报错,多半是BSP配置问题,检查一下standalone BSP里是否包含了qspi、sd、ddr等相关驱动。
3.3 应用程序工程的编译要点
应用程序就是你的业务代码,编译生成app.elf。这里要注意的是链接地址。7020的DDR从0x00100000开始可用(低地址留给OCM和寄存器),app.elf的链接脚本一般把代码段放在DDR里。如果你用的是Vitis自动生成的链接脚本,通常没问题,但如果你手动改过,要确认入口地址和FSBL跳转的地址一致。
另外,如果应用程序里用到了PL的IP,确保bitstream和app.elf是配套的,别拿旧版本的bit去配新版本的驱动。
3.4 用Create Boot Image打包BOOT.bin
这是核心步骤。在Vitis里,Xilinx → Create Boot Image。
- Architecture选Zynq。
- Output BIF file path和Output path自己指定。
- Boot image partitions里按顺序添加:
- fsbl.elf,分区类型选bootloader
- system.bit,分区类型选datafile
- app.elf,分区类型选datafile
添加时注意偏移地址,一般默认自动分配就行。但如果你的Flash有特殊分区要求,可以手动指定。比如bitstream的偏移要按Flash的扇区对齐,不过Vitis一般会处理好。
点Create Image,生成BOOT.bin。如果报错,常见原因有:
- fsbl.elf路径不对或文件损坏
- bit文件格式不对(必须是.bit,不能是.bin)
- app.elf的架构和FSBL不匹配
生成成功后,建议用bootgen命令行再验证一下,或者直接用Vitis的Program Flash功能试烧。
4. 烧录到QSPI Flash:JTAG方式全流程
4.1 烧录前的硬件连接检查
JTAG烧录Flash,硬件上要确认几件事:
- JTAG线连接正常:7020的JTAG接口是标准14针,确认下载器(比如Platform Cable USB或Digilent HS2)驱动装好,Vitis里能识别到器件。
- 启动模式跳线:烧录时启动模式要设成JTAG模式,否则BootROM会去读Flash,可能干扰烧录。烧完之后再改回QSPI模式。
- Flash供电正常:QSPI Flash的供电来自板子,确认板子上电正常,Flash的VCC在3.3V左右。
4.2 在Vitis里配置Flash烧录参数
Vitis里,Xilinx → Program Flash。
- Image File选刚才生成的BOOT.bin。
- Flash Type选对应的型号,比如qspi_single或qspi_dual。W25Q256一般用qspi_single,如果板子做了双线或四线连接,选对应的模式。
- FSBL File选fsbl.elf。这一步很多人问为什么烧Flash还要FSBL,因为烧录过程本身也是通过FSBL来执行的,FSBL里包含了QSPI的驱动。
- Offset一般填0。
点Program,Vitis会先下载FSBL到OCM,然后通过FSBL把BOOT.bin写进Flash。整个过程串口会打印进度,能看到擦除、写入、校验的阶段。
4.3 烧录失败的常见报错和排查
报错一:Flash ID读取失败,提示找不到Flash器件。
这个最常见。原因通常是Flash型号选错了,或者QSPI引脚在Vivado里没配置对。检查Vivado里QSPI的MIO配置,确认CS、CLK、D0-D3引脚和板子原理图一致。另外,有些板子的Flash是1.8V的,如果Vivado里配置成3.3V,也会读不到。
报错二:烧录到一半卡住,进度条不动。
多半是Flash擦除时间不够或者供电不稳。W25Q256的扇区擦除需要时间,如果FSBL里的超时设置太短,会误判失败。可以在FSBL源码里把QSPI的超时参数调大。另外,检查JTAG线是否太长或接触不良。
报错三:烧录成功但校验失败。
说明写入的数据和读回的不一致。可能是Flash有坏块,或者写入速度太快。可以降低JTAG时钟频率试试,在Program Flash的配置里把时钟从默认的调低一档。
提示:烧录前最好先Erase整个Flash,再Program。有些板子出厂时Flash里有旧数据,直接Program可能因为扇区没擦干净导致写入异常。
4.4 烧录完成后的启动验证
烧录完成后,断电,把启动模式跳线改回QSPI模式,重新上电。这时候串口应该能看到FSBL的打印信息,然后是应用程序的输出。
如果没反应,先别慌,按这个顺序排查:
- 串口有没有FSBL打印:如果没有,说明BootROM没找到FSBL,检查启动模式跳线、Flash里BOOT.bin的偏移地址、FSBL是否真的写进去了。
- 有FSBL打印但卡在DDR初始化:DDR参数不对,回Vivado检查DDR配置。
- FSBL跑完但app没起来:app.elf的链接地址不对,或者FSBL跳转地址和app入口不一致。
- PL逻辑没生效:bitstream没打包进BOOT.bin,或者FSBL里没使能PL配置。
5. 那些让人抓狂的固化问题与解决思路
5.1 烧录时提示“cannot load flash programming algorithm”
这个报错通常出现在Vitis的Program Flash阶段,意思是找不到对应Flash的编程算法。原因是Vitis的Flash算法库是预置的,如果你的Flash型号不在列表里,就会报这个。
解决办法有两个:一是换一个兼容的型号,比如W25Q256可以选qspi_single的通用算法;二是自己写Flash算法,但这个工作量大,一般不建议。更实际的做法是确认Vitis版本里有没有更新Flash算法库,或者去Xilinx官网下载对应的补丁。
5.2 JTAG固化Flash时到底需不需要DDR
这个问题在社区里被问过很多次。答案是:看情况。
FSBL在烧录阶段是跑在OCM里的,OCM只有256KB,如果BOOT.bin比较小(比如不带bitstream或者bitstream很小),FSBL可以完全在OCM里完成烧录,不需要DDR。但如果BOOT.bin很大,FSBL需要把数据先缓存到DDR里再写Flash,那就必须初始化DDR。
所以如果你在烧录时遇到DDR初始化失败导致烧录中断,可以尝试精简BOOT.bin,或者确认FSBL里的DDR配置是否正确。7020的DDR初始化参数在Vivado里配置好后,FSBL会自动生成对应的代码,一般不会错,除非硬件本身有问题。
5.3 双镜像与MultiBoot的实战配置
产品化的时候,双镜像几乎是标配。思路是在Flash里放两份BOOT.bin:一份Golden Image放在0地址,一份Update Image放在后面的偏移地址。Golden Image里跑一个简单的逻辑,负责检测Update Image是否有效,有效就跳过去,无效就留在Golden里。
具体做法:
- 在Vivado里使能MultiBoot,设置Golden Image的起始地址和Update Image的偏移。
- 生成两个BOOT.bin,分别烧到对应地址。
- 在Golden Image的FSBL里加入跳转逻辑,通过读取Flash里的标志位判断是否跳转。
这个机制的好处是,即使Update Image刷坏了,板子还能从Golden Image启动,不会变砖。代价是Flash空间要够,256Mb的Flash放两份BOOT.bin一般够用。
5.4 固化后PL逻辑时有时无的诡异现象
有兄弟遇到过:固化后第一次上电PL正常,断电再上电PL就不工作了。这种间歇性失效最折磨人。
排查下来,常见原因有两个:
一是bitstream的加载时序。FSBL配置PL需要时间,如果FSBL在PL还没配置完就去跳转app,app里又立刻访问PL的寄存器,就会读到错误数据。解决办法是在FSBL里加一个PL配置完成的等待,或者app启动后延时再访问PL。
二是Flash读取不稳定。QSPI时钟太高,或者Flash的保持时间不够,导致bitstream读出来有误码。可以降低QSPI时钟,或者在FSBL里加校验重试。
6. 几个提高固化成功率的实操习惯
6.1 每次固化前先做一次JTAG下载验证
在烧Flash之前,先用JTAG把bitstream和app.elf下载到板子上跑一遍,确认逻辑和程序都没问题。这一步能排除掉90%的“固化失败其实是代码本身有问题”的情况。很多人跳过这步直接烧Flash,结果烧完不启动,回头查半天发现是app本身就跑不起来。
6.2 保留一份可回退的Golden Image
不管你是不是做双镜像,建议在Flash的0地址永远保留一份确认能启动的Golden Image。后续更新都往后面的地址烧,通过MultiBoot切换。这样即使新版本有问题,也能通过跳线或者软件回退到Golden。这个习惯在量产阶段能救命。
6.3 串口打印是固化调试的生命线
FSBL和app里都要保留串口打印,而且打印要分阶段:FSBL打印DDR初始化、PL配置、跳转地址;app打印启动入口、关键外设初始化。固化失败时,串口输出能直接告诉你卡在哪一步,比盲猜快得多。
6.4 记录每次固化的参数配置
Flash型号、QSPI模式、BOOT.bin分区偏移、FSBL版本、Vitis版本,这些参数每次固化都记下来。换板子或者换版本时,对照记录能快速定位差异。我自己的习惯是建一个表格,每块板子一行,固化参数一列,出问题时直接查表。
| 板子编号 | Flash型号 | QSPI模式 | BOOT.bin偏移 | FSBL版本 | 备注 |
|---|---|---|---|---|---|
| Board-01 | W25Q256 | qspi_single | 0x00000000 | 2020.2 | 正常 |
| Board-02 | W25Q128 | qspi_single | 0x00000000 | 2020.2 | 需降时钟 |
| Board-03 | W25Q256 | qspi_dual | 0x00000000 | 2021.1 | 双镜像 |
这张表看起来简单,但真出问题的时候,能帮你省下大量对比时间。
6.5 关于Flash寿命的一点提醒
QSPI Flash的擦写次数是有限的,W25Q256一般标称10万次。固化调试阶段频繁烧录问题不大,但产品化后如果app需要频繁更新,建议用双镜像+远程更新的方式,减少对Flash的擦写。另外,Flash里的数据保存年限一般10到20年,工业场景下要考虑这个因素。
7. 从固化延伸出去:几个值得关注的方向
固化跑通之后,下一步通常会碰到远程更新的需求。思路是app在运行时通过网络或串口接收新的BOOT.bin,写到Flash的Update区域,然后触发MultiBoot切换。这里的关键是写Flash的驱动要可靠,以及更新过程中的断电保护。
另一个方向是启动时间优化。FSBL本身跑得很快,但bitstream的加载时间取决于逻辑规模。如果对启动时间敏感,可以考虑部分重配置,只加载变化的部分,或者把bitstream压缩后存储,FSBL解压后再配置PL。
还有就是安全启动。7020支持RSA认证和AES加密,可以对BOOT.bin做签名和加密,防止固件被篡改或逆向。配置起来需要在Vivado里生成密钥,用bootgen签名,FSBL里使能认证。这个在量产产品里越来越常见,尤其是对外发货的设备。
我自己在7020上固化过几十块板子,从最初的反复翻车到后来一次成功,最大的体会就是:固化不是玄学,每一步都有明确的原理,出问题一定是某个环节的参数或配置不对。把FSBL、BOOT.bin、Flash这三者的关系理清楚,再配合串口打印和参数记录,基本没有解决不了的固化问题。