简介:针对Xilinx Zynq平台在Vivado SDK中不支持国产复旦微FM25F32 QSPI Flash、导致启动镜像无法烧写的问题,这套程序包给出了完整的实测验证方案。设计基于XC7Z020 FPGA与FM25F32 NOR Flash,通过自编烧写测试程序成功写入uboot与FSBL,并完成启动加载验证,可帮助从事国产化替代或Zynq二次开发的工程师快速绕开SDK工具链限制。压缩包共438个文件,约2.27MB,以C语言头文件与源文件(h/c)为主,配合Makefile、SDK工程文件、链接脚本及编译生成的o、a、elf、bin等中间产物,方便对照工程配置与编译流程。包内还包含项目说明文档与BOOT.bin等关键文件,覆盖从驱动适配、镜像生成到烧写验证的完整链路。目前已有2481人学习,对正在适配复旦微Flash或研究Zynq QSPI启动机制的开发者具有直接参考价值。
1. 开场:为什么在xc7z020上烧FM25F32会卡住
做了三块Zynq xc7z020的板子,QSPI Flash烧写这一步几乎没人一次过。常见动作是手里拿着ID被Vivado SDK默认收录的那几颗Flash去套国产FM25F32,结果SDK的Program Flash要么报Unknown Flash ID,要么烧完校验全Pass,拨到QSPI启动却整屏黑匣子。这问题之所以难缠,不是Flash型号冷门,而是QSPI启动链一共四段:硬件接线、FSBL里的ID识别、Boot Image分区、启动模式拨码,任何一处没对齐都会静默失败。这篇是我把FM25F32挂到xc7z020后从建工程、打BOOT.BIN到U-Boot烧写、串口验证的完整记录,适合手里有Zynq板子、正在被QSPI Flash烧写搞到头大的开发者抄作业。
2. 硬件接线与Vivado工程:把FM25F32挂到xc7z020的QSPI控制器上
2.1 确认链路:FM25F32的引脚与MIO分配
FM25F32是3.3V供电的QSPI NOR Flash,容量32Mbit,也就是4MB,支持标准SPI和四线QSPI两种模式。Zynq xc7z020的QSPI控制器接口固定在MIO[5:0]上,CPU侧不需要额外分配引脚,这省了事,也意味着没得选——硬件设计时只能按Zynq的固定映射来接,对应关系如下表:
| Zynq MIO | QSPI信号 | FM25F32引脚功能 |
|---|---|---|
| MIO0 | QSPI_SCK | SCK(时钟) |
| MIO1 | QSPI_CS | CS(片选) |
| MIO2 | QSPI_DQ0 | DI(数据输入) |
| MIO3 | QSPI_DQ1 | DO(数据输出) |
| MIO4 | QSPI_DQ2 | WP/HOLD复用 |
| MIO5 | QSPI_DQ3 | HOLD/WP复用 |
这里要特别盯住FM25F32的WP和HOLD两个脚。在标准SPI单线模式下,WP不接可能还能勉强跑起来,一旦把FM25F32切到四线QSPI模式,WP就变成了DQ2,HOLD变成了DQ3。这两个脚如果悬空或被拉低,四根数据线里有两根的电平状态不确定,读出来的数据按照字节移位错乱,表现就是Flash内容能识别、但启动时指令全乱。所以FM25F32的WP和HOLD必须通过10K电阻上拉到3.3V,这是烧写前第一道门槛,板上这一步错了后面软件怎么改都救不回来。
供电和电平也要顺手核一遍。FM25F32是3.3V器件,xc7z020的QSPI IO口也工作在3.3V,电平匹配没有问题。但如果你的板子把Flash放在了1.8V域,或者中间加了电平转换芯片,就要看转换芯片的OE方向和输出速率,QSPI时钟25MHz下转换芯片选型不当会导致信号沿变缓,烧写时偶发校验失败。我一般把电源和上拉电阻放在PCB检查清单最前面,比软件配置更早确认。
2.2 在Vivado Block Design里使能QSPI并锁定SDR模式
打开Vivado工程后进IP Integrator,在Block Design里双击Zynq7 Processing System,左侧导航走Peripheral I/O Pins → MIO Configurations,展开Quad SPI Flash列,勾上QSPI。勾选后会自动把MIO0到MIO5分配成QSPI的一组引脚,同时出现QSPI时钟、片选和数据脚的配置项。保存设计后Generate Output Products,再Generate Bitstream之前记得在Address Editor里看一眼,QSPI控制器的寄存器基地址默认落在0xE000D000,线性映射地址从0xFC000000开始,后面验证阶段会用到这个线性映射地址。
这里有两个配置项值得细看。一个是Flash Speed,生成FSBL时会按这个频率初始化QSPI控制器,我习惯设成25MHz。FM25F32的标称时钟余量通常够跑更高频率,Zynq QSPI控制器在SDR模式下跑到50MHz也很稳,但烧写阶段我不追速度,25MHz启动稳定性比省那几十毫秒重要。另一个是Flash型号模式,在QSPI IP配置里可以选Single和Dual。FM25F32单颗只占一个片选CS0,选Single;如果板子上为了扩大容量放了两颗Flash做双片选再选Dual。这个选项直接决定FSBL里QSPI驱动初始化几个片选,选错会导致片选地址计算偏移,验证时的现象是Flash内容能读出来,但首地址数据全空。
关于DDR模式要单独说一句。Zynq QSPI控制器支持SDR和DDR两种读时序,DDR模式下控制器用0xEB这类命令让Flash在时钟上下沿都输出数据,带宽翻倍。但很多通用SPI NOR Flash只保证SDR命令集的兼容性,对DDR命令的支持要看具体数据手册。FM25F32这类国产Flash,我的做法是强制走SDR,不在DDR上赌兼容性。把QSPI配置里的Read/Write模式保持SDR,U-Boot里也不要开DDR读,后面避坑章节5.2会再展开这个坑。
2.3 编译FSBL:让启动代码认FM25F32的ID
FSBL是Zynq启动的第一段代码,上电后由固化ROM从Flash加载执行,QSPI控制器初始化和Flash ID识别都由它完成。在Vivado SDK里右键平台工程,选择Source → Download Sources,把FSBL源码下载到本地,在src目录下能找到QSPI驱动的Flash参数表。不同版本SDK把这表放在不同文件,常见命名是ps7_qspi.c,也有可能放在flash_list.h里。表里每一项记录Flash容量、扇区大小和JEDEC ID。
JEDEC ID通过0x9F命令读出,一般两到三字节,包含厂商ID和型号ID。FM25F32如果不在SDK的这张表内,用Program Flash烧写时大概率报Unknown Flash ID,U-Boot的sf probe也可能读到一组不匹配的ID,然后拒绝挂载。处理办法有两个:一是从驱动源码里找到那张结构体数组,把FM25F32实际读出的ID填进去,重新编译FSBL,优点是SDK烧写流程原样可用,缺点是每换一颗不同型号Flash都要改一次驱动源码;二是干脆绕开SDK的Flash识别,后续用U-Boot的sf命令直接烧写。
我最终落在第二种方案上。原因很简单,U-Boot的QSPI驱动对厂商ID没那么敏感,只要擦除和编程命令集兼容就能直接操作,FM25F32的命令集和主流SPI NOR Flash基本一致,适配成本最低。如果你必须留着SDK烧写路径,那就按第一种方案改表,改完重新编译FSBL并重新打包整个BOOT.BIN,别只更新FSBL文件而忘了重新生成镜像。
3. 生成BOOT.BIN:FSBL、bitstream和应用一次打包
3.1 组装镜像需要的三份文件
想让FM25F32烧完就能引导,Flash里不能只有bitstream。Zynq上电后从QSPI Flash读到的第一段必须是FSBL;FSBL初始化DDR和外设后把bitstream加载进PL,也就是设计里的FPGA配置程序,再由FSBL引导应用或U-Boot。所以最终烧进Flash的是一个三合一镜像BOOT.BIN,三个分区缺一不可。准备文件时按下表对齐:
| 文件 | 来源 | 作用 |
|---|---|---|
| fsbl.elf | SDK新建Application工程,模板选Zynq FSBL | 启动第一段,初始化DDR、QSPI、PLL等 |
| 你的工程.bit | Vivado里Implementation → Generate Bitstream生成 | PL侧的配置程序 |
| u-boot.elf 或 app.elf | U-Boot构建产物,或你自己的C工程编译产物 | ARM侧的运行程序 |
fsbl.elf编译时建议选Release配置,Debug版本镜像体积大,挤占FM25F32本就紧张的4MB空间。bit文件在Vivado里生成后不要改名字,打包工具按照分区顺序读取,文件路径里不要带中文和空格。如果目标系统跑Linux,第三份文件就是u-boot.elf;只跑裸机的话就是自己的app.elf。先分别确认这三个文件都真实存在,再进打包步骤。
3.2 Create Boot Image的参数设置
Vivado SDK菜单里选Xilinx → Create Boot Image,打开Boot Image Editor。窗口左侧的Boot image partitions列表就是分区配置区,依次添加三个分区。第一步添加FSBL,Partition Type选bootloader,Offset写0x0,它必须位于Flash的0地址,因为Zynq固化ROM上电后固定从0地址读启动代码。第二步添加bitstream,Partition Type选datafile,Offset手动填0x100000,也就是1MB偏移处。第三步添加app或u-boot,Offset填0x200000,2MB偏移处。
这里有个参数取舍值得说。SDK提供一个Auto offset按钮,会自动给每个分区安排地址,我基本不用,手动固定地址能让我在调试时直接用U-Boot的sf read按已知偏移读数据做对比。Zynq的QSPI控制器把Flash映射到线性地址空间,分区偏移同时决定启动时加载地址,手动固定就少了一层黑匣子。偏移设置有个约束:QSPI NOR Flash的扇区一般是4KB,0x100000和0x200000都是4KB的整数倍,天然对齐,不需要额外做边界修正。如果改了偏移值,务必在BIF文件里确认分区顺序和Offset一一对应。
打包前最后一道检查是容量。FM25F32只有4MB,地址范围0x0到0x3FFFFF,三个分区文件大小加起来不能超过这个范围。fsbl.elf一般在200KB以内,一个典型bitstream在200KB到900KB之间,u-boot.elf大概1MB上下。如果发现总大小逼近3.5MB,就要考虑压缩bitstream或裁剪PL设计,而不是继续加大偏移。拿2MB处的app分区来说,它往后最多到0x3FFFFF,只剩约2MB可用,u-boot如果开了大量配置功能很容易超。
3.3 分区与加载地址对照
生成BOOT.BIN之前,把这张分区表打印出来贴在显示器边上。后面凡是遇到“烧写成功但起不来”,第一步就是核对Flash 0地址上是不是FSBL、0x100000是不是bitstream、0x200000是不是u-boot。三段错位是QSPI启动最常见的隐形故障,FM25F32本身没问题,背锅的是打包参数。
| 分区 | Flash偏移 | 典型大小 | 启动时加载到 |
|---|---|---|---|
| FSBL | 0x000000 | 128KB-256KB | OCM |
| bitstream | 0x100000 | ≤900KB | PL配置引擎 |
| u-boot/app | 0x200000 | ≤2MB | DDR |
Boot Image Editor里Output Format选BIN,生成之后用文本编辑器打开BOOT.BIN的BIF描述文件,重点看三个partition的offset字段是否和上表一致。不一致就回到分区列表重新排,然后重新生成。镜像到这一步就绪,可以进烧写环节。
4. 烧写FM25F32:SDK Program Flash与U-Boot两条路径
4.1 方式一:SDK Program Flash
最直观的方式是Vivado SDK里Xilinx → Program Flash。接线是JTAG或USB下载线连到板子,先在Hardware Manager里确认能看到xc7z020器件。这里有个细节容易被忽略:Hardware Manager里的JTAG时钟速度要手动降到10到30MHz。在线调试时高速JTAG没问题,烧Flash时数据吞吐大,JTAG链路不稳定会出现TDO stuck或者校验中途断开的玄学错误,降速后基本绝迹。
Program Flash对话框里的关键参数这样填:Flash Type选qspi-x4-single,这个名字要和FSBL的QSPI配置严格一致。FSBL里配的是Single加四线数据,这里就选x4-single,选错会在烧写阶段报控制模式不匹配。Image File选刚生成的BOOT.BIN,Offset填0x0,勾上Erase和Verify。点Program后SDK会先把FSBL通过JTAG加载进内存运行,由FSBL里的QSPI驱动执行实际擦写动作,日志窗口能看到擦除和编程的进度百分比。
这一步最怕报Unknown Flash ID。见到这个错误说明SDK的驱动没有收录FM25F32的ID,处理路径在2.3已经说了:要么改驱动源码加ID重新编译FSBL,要么切到U-Boot方式。如果你手里已经有能启动到U-Boot的环境,我强烈建议直接切U-Boot,少改一套代码。要是必须走SDK且不想动驱动,还有一个办法是把Image File换成MCS或BIT并使用Indirect Programming模式,但那个流程绕一圈还是绕不开ID识别,不如U-Boot干脆。
4.2 方式二:U-Boot用sf命令烧写
当板子已经能从SD卡或QSPI启动到U-Boot时,烧FM25F32完全不用依赖SDK的Flash列表。U-Boot命令行里操作即可:
# 探测QSPI Flash U-Boot# sf probe 0:0 0 0 # 擦除整片4MB U-Boot# sf erase 0x0 0x400000 # 从SD卡FAT分区加载BOOT.BIN到内存 U-Boot# fatload mmc 0 0x08000000 BOOT.BIN # 把内存中的镜像写入Flash起始地址 U-Boot# sf write 0x08000000 0x0 0x400000sf probe的三个参数是总线号、片选号和时钟频率。0:0表示控制器0的片选0上那颗Flash,也就是CS0;第二个0代表时钟频率填0,U-Boot会使用驱动默认值;第三个0是SPI模式。这样写能避开对不同Flash时序参数的硬编码,兼容性最好。probe成功后U-Boot可能把FM25F32识别成ID表中某颗容量一致的常见Flash型号,这不影响使用,擦除和编程命令都是标准的0x02、0x20、0xD8,命令集兼容就能直接操作。
sf erase擦除整片4MB,如果控制器不支持整片擦除命令,会自动退化成逐扇区循环擦除,只是耗时长一点。fatload把SD卡FAT分区根目录下的BOOT.BIN读到DDR地址0x08000000,mmc 0对应SD卡控制器,0x08000000是Zynq上常用的DDR加载地址,只要这个地址不在正在运行代码的区域内就安全。最后sf write从内存向Flash起始地址写入4MB,写完后U-Boot默认会做回读校验,看到校验通过信息就代表写入成功。如果不想一次写满4MB,可以把0x400000改成BOOT.BIN实际字节数,擦除时也改成对齐到扇区的同样大小。
4.3 校验:不能只信烧写日志
SDK的Program Flash勾了Verify会自动校验,U-Boot的sf write也自带校验,但这两者的校验都只是“写完回读比对”,不保证拔电后能启动。我每次烧完还会强制做一次回读看Flash首地址内容:
# 回读Flash前2MB到内存 U-Boot# sf read 0x08000000 0x0 0x200000 # 打印内存头部256字节十六进制 U-Boot# md.b 0x08000000 0x100md.b把内存里的内容打印成十六进制,前0x100字节应该是FSBL的头部。正常的启动代码开头是一段ARM异常向量表,能看到一堆跳转指令而不是整片FF或者全零。如果全是0xFF说明擦除后没写进去,常见原因是擦除时片选或地址没对齐;如果内容看起来像乱码但并非全FF,则要怀疑数据线连接或SDR模式配置。
除了sf read回读,Zynq还有一个更直接的内存映射校验法:QSPI控制器把Flash线性映射到0xFC000000地址,直接用md.b 0xFC000000 0x100读前256字节。这一步不经过sf命令,硬件上Flash像一块只读内存挂在总线上,读出来的内容和sf read一致,说明从Flash到控制器的整个硬件链路都是通的。两条校验路径我一般都会走一遍,U-Boot的sf read负责验证逻辑数据,0xFC000000线性读负责验证硬件映射。
5. 避坑:FM25F32在Zynq QSPI烧写中的5个典型问题
5.1 Unknown Flash ID:SDK不认这颗Flash
现象:SDK的Program Flash点击Program后,日志框几秒内弹出Unknown Flash ID或者Unsupported flash,烧写流程直接终止。
原因:SDK驱动里维护了一张Flash ID表,FM25F32的JEDEC ID不在表中。Zynq QSPI控制器本身能正常通信,是软件层的ID校验拒绝了它,并非硬件损坏。
解决:能进U-Boot就用U-Boot的sf命令烧写,绕开SDK的ID表;必须用SDK的话,在FSBL驱动源码里找到Flash信息结构体数组,把FM25F32实际读出的JEDEC ID填进去,注意结构体里一般有厂商ID和型号ID两个字段,改完重新编译FSBL并重新生成整个BOOT.BIN。改表方案我留作备用,日常烧写直接U-Boot更快。
5.2 烧写日志正常,拔电重启却整屏白
现象:烧写日志显示写入成功,Verify也通过,但拨到QSPI启动后串口没有任何输出,就像Flash是空的。
原因:两个原因占九成。一是启动模式拨码根本没切到QSPI档,Zynq复位时采样到的启动模式还是SD卡或JTAG,Flash内容根本没被执行;二是FSBL里QSPI被配成了DDR读模式,而FM25F32对DDR读命令的兼容性没有保证,FSBL执行时读回来的指令是乱的,自然起不来。
解决:第一步查开发板启动模式拨码,Zynq xc7z020的QSPI启动对应MIO[4:0]采样值11010,具体拨到哪个位置以板卡手册为准。第二步回Vivado把QSPI读写模式锁成SDR,重新生成FSBL并重新打包。这两个方向排查完,白屏问题基本都能落地。
5.3 擦除命令超时:状态寄存器轮询卡死
现象:U-Boot下sf erase执行到一半长时间不动,最后报write timeout或者erase error,有时连续擦多个扇区时第二个扇区就卡住。
原因:FM25F32擦除时状态寄存器bit0的翻转节奏和常见Flash有差异,U-Boot的轮询超时时间按标准Flash算,慢一点的Flash就触顶;另一个高频原因是WP引脚被拉低,芯片处于写保护状态,擦除命令直接被拒绝。
解决:先用万用表量FM25F32的WP引脚,必须为高电平,这是写操作的前提。电平正常还超时,就把整片擦除改成逐扇区擦,每次只擦4KB块,擦完读取状态寄存器确认空闲再擦下一块。U-Boot里可以写个短循环脚本执行sf erase 0x0 0x1000,地址循环累加直到擦完整个4MB,慢一些但稳定。
5.4 bitstream加载到一半失败:PLL或DNA报错
现象:FSBL阶段串口能看到打印,说明Flash和DDR已经工作,但bitstream加载到大约一半时设备宕掉,JTAG日志里出现PL初始化失败或DNA读取错误。
原因:QSPI时钟配得太高。FM25F32在较高时钟下的读时序参数不够稳定,FSBL通过DMA把bitstream从Flash读到PL配置引擎时,高速数据流下偶发位错,导致配置数据校验不过。
解决:回到Vivado的Zynq配置界面,把Flash Speed改成25MHz,重新生成FSBL,重新打包BOOT.BIN,再执行第4章的烧写流程。QSPI Flash时钟不是越快越好,25MHz是xc7z020 QSPI上最保守也最可靠的档位,牺牲少许加载时间换启动成功率,值得。
5.5 容量错觉:4MB地址算错
现象:BOOT.BIN超过4MB时也能烧“成功”,但启动时FSBL读到被截断的数据,串口打印乱码;或者U-Boot里sf erase直接报size out of range。
原因:FM25F32容量32Mbit,换算过来就是4MB,地址范围0x0到0x3FFFFF。BOOT.BIN整体大小超过4MB,或者某个分区偏移超过0x400000,都超出Flash可寻址空间,超出部分自然写不进去。
解决:打包前把三个分区文件大小相加,确认总和小于0x400000。分区偏移保持FSBL在0x0、bitstream在0x100000、app在0x200000的约定,别为了给某个分区让路把偏移改到0x400000以外。真要超容,先裁剪PL设计或压缩bitstream,改偏移只是自欺欺人。
6. 验证跑通:拨QSPI启动,用串口日志确认FSBL到应用
烧写完的镜像到底能不能用,唯一标准是冷启动。把开发板断电,启动模式拨到QSPI档,对应Zynq xc7z020的MIO[4:0]采样值11010。JTAG线如果没拔也先断开,因为JTAG适配器会抢占启动流程,干扰上电自检。串口按115200 8N1接好,打开终端,再给板子上电。上电瞬间的日志顺序比内容更重要,因为它能定位问题出在哪一段。
正常流程下第一屏是FSBL打印,典型内容是Boot Device: QSPI,跟着是DDR初始化信息。接着如果BOOT.BIN里带了bitstream,会出现PL配置完成的信息,然后进入U-Boot或裸机应用自己的输出。我习惯用一组日志节点做通过性检查:
FSBL: Boot from QUAD-SPI DDR initialization complete PL Configuration Done U-Boot banner / 应用自己的启动打印看到这四行,说明FM25F32的ID识别、SDR时序、分区偏移和启动模式全对上了。只看到FSBL打印但卡在DDR或PL,回看5.4的时钟问题;连FSBL打印都没有,基本是启动模式拨码或Flash首地址数据问题,先回查拨码和CS焊接。串口一个字不出时别急着动Flash,先拿万用表量MIO1引脚在拨到QSPI档后是不是正确拉成启动电平,这一步经常省掉一整个下午的无效烧写。
还有一个更狠的验证是二次上电。第一次上电成功不算数,断电等五秒再上电,串口必须再次完整打印同样的日志。QSPI Flash存在读时序的偶发问题,冷启动两次都成功,才说明FM25F32的时序裕量在xc7z020上真正够用。同一个BOOT.BIN连续三次上电都稳定,我才会把这颗板子从烧写台放走。
从那以后我每烧一颗FM25F32都强制走这道流程:打包时核对三段分区大小、烧完用sf read回读前256字节、然后断电冷启动两次看串口日志。这套流程帮我挡掉了至少三次只看校验日志而放过启动问题的翻车现场,也希望帮你少走这几步冤枉路。
本文还有配套的精品资源,点击获取