简介:针对Xilinx Zynq-7000系列FPGA的国产化替代需求,一套以XC7Z020配合复旦微FM25F32 Nor Flash的QSPI烧写配置程序,解决了Vivado SDK无法识别并烧写该国产Flash的难题,适合嵌入式与FPGA开发工程师参考。压缩包内共438个文件,涵盖165个C头文件、151个C源文件、makefile构建脚本、bin/elf镜像、ld链接脚本以及说明文档,整体约2.27MB,既包含BSP库级驱动源码,也有可独立编译的测试程序工程,便于对照梳理QSPI控制器驱动、缓存与启动流程。其中演示了在SDK自带工具失效时,通过自编测试程序成功烧写uboot与FSBL,并实现启动加载的完整验证过程,这一过程覆盖Flash ID识别、扇区擦除、页编程与状态查询等底层环节,能够帮助读者理解该Flash的擦写时序、FSBL/uboot镜像布局以及Zynq QSPI启动链配置。目前已有2481人学习下载,对从事Zynq国产化适配、Bootrom级启动开发或Flash驱动移植的工程师具有直接参考价值。
1. 为什么xc7z020会选复旦微FM25F32:QSPI启动的选型与资源定位
Zynq QSPI Flash启动,是每个用xc7z020的工程师都会碰到的关卡。板子在调试、量产时最容易翻车的不是PL逻辑,而是Boot Flash——型号一换,启动立刻变成黑匣子。复旦微FM25F32是一款国产QSPI NOR Flash,容量32Mbit,管脚和主流封装兼容,但指令集、状态寄存器定义和国外大厂产品有差异。这套烧写配置程序正是围绕这块Flash整理的,覆盖镜像制作、烧写路径、异常排查的完整流程,适合正在做国产Flash替换、或者第一次在Zynq上启用QSPI启动的工程师。下文所有命令和代码都基于xc7z020+FM25F32这套组合来展开。
2. 烧写前的硬件与镜像准备:启动模式、FM25F32管脚映射与BOOT.bin生成
2.1 启动模式引脚与QSPI Flash管脚映射
xc7z020的启动模式由MIO[5:2]引脚电平决定。QSPI启动对应一个固定的二进制组合,PCB上的上下拉电阻稍有不对,就会落到JTAG模式或者SD卡模式。最稳妥的做法是查对应型号的Boot Mode表格,再顺着原理图逐个确认MIO[2]到MIO[5]的电阻接法,不能直接照抄别的板卡。常见错误是JTAG模式可以连上,但掉电重启后没有引导,这类问题常常和启动模式引脚混淆在一起。
FM25F32与Zynq QSPI控制器的连接是标准的4位QSPI,数据线DQ0到DQ3分别接到PS侧的MIO13到MIO16。需要注意,DQ2和DQ3同时也是WP和HOLD功能引脚,如果板卡没有给它们加上拉电阻,进入Quad模式后容易异常。经验是硬件上把这四个引脚都加上拉,且保证映射顺序和Zynq QSPI控制器定义一致,否则后面sf probe会报ID乱码。
启动模式速查表如下:
| MIO[5:2] | 启动模式 | 用途 |
|---|---|---|
| 0000 | JTAG | 调试 |
| 0010 | QSPI | Boot Flash |
| 0101 | SD | SD卡启动 |
注意:具体对应关系以芯片手册为准,不同等级芯片版本可能有差异。我这块板卡上使用的是0010配置,烧写前用万用表复核过电阻分压,确认无误后才继续。
2.2 用Vivado生成FSBL与BOOT.bin
镜像制作流程:创建硬件平台、生成FSBL、用bootgen打包。很多人第一步就卡住,因为Vivado SDK的Flash型号列表里没有复旦微,但这不影响FSBL生成。FSBL的主要工作是初始化PS侧时钟、DDR和QSPI控制器,只要它按通用SPI时序配置,后续U-Boot或裸机程序能识别Flash ID就行。
在Vivado SDK中新建Application Project,操作系统选standalone,模板选Zynq FSBL。编译后得到fsbl.elf。如果PL需要加载bitstream,还需要system.bit。打包时写一个boot.bif文件,内容如下:
the_ROM_image: { [bootloader]fsbl.elf system.bit u-boot.elf }然后执行:
bootgen -image boot.bif -o BOOT.bin -w on参数说明:-image 指定bif输入文件,bootgen会按顺序打包分区;-o 指定输出BOOT.bin;-w on 表示允许覆盖已存在文件。这里有两个关键细节。如果不需要PL配置,可以把system.bit从bif里去掉,BOOT.bin会小一些;u-boot.elf不是必须的,但建议放进去,因为很多工程需要U-Boot来烧写后续镜像。
分区偏移也可以在这里控制。FM25F32只有4MB空间,习惯上把BOOT.bin放在偏移0x0处,然后把image.ub放在0x100000偏移处。boot.bif里可以用[offset]字段显式指定分区偏移,避免U-Boot的默认偏移和实际不符。这个偏移要和后续烧写命令里的偏移保持一致,否则启动阶段会找不到正确镜像。
生成BOOT.bin后,先不要急着烧Flash。建议在SDK里做一个最小application,只读Flash ID验证一下QSPI通路。办法是新建一个Hello World工程,通过XQspiPs读ID寄存器。如果ID能读出来,说明FSBL和硬件连接都没问题,再进入烧写阶段。
2.3 确认FM25F32的ID与兼容模式
拿到板子后,第一步是读Flash ID,不是烧写。FM25F32的厂商ID和型号ID不能想当然套用Winbond或Micron的,否则后面所有指令都在用错误的对象。最简单的方式是U-Boot下执行sf probe:
=> sf probe SPI NOR flash detected: FM25F32 with page size 256 Bytes, erase size 4 KiB, total 4 MiB如果输出unrecognized JEDEC id bytes,说明ID表没有覆盖复旦微。此时不要急着改驱动,先确认读到的ID字节和手册是否一致。如果ID值不停变化,先降QSPI时钟频率,很多玄学读ID问题都是时钟过快导致的。
确认ID后,还要确认FM25F32支持哪些指令。重点看三条:读状态寄存器指令0x05、写使能指令0x06、页编程指令0x02。这几条和主流QSPI Flash一致,通常没问题。但Quad相关的读指令和状态寄存器配置位要看手册,不同厂商差异很大。下面这段C代码是在裸机里读ID的常用写法,逻辑上按“发0x9F命令,再跟3个空字节,同时逐字节接收Flash返回的ID”执行:
#include "xqspips.h" u32 QspiReadID(XQspiPs *QspiInstance) { u8 cmd[4] = {0x9F, 0x00, 0x00, 0x00}; u8 buff[4] = {0}; u32 flashId = 0; XQspiPs_PolledTransfer(QspiInstance, cmd, buff, 4); flashId = (buff[1] << 16) | (buff[2] << 8) | buff[3]; return flashId; }参数说明:0x9F是JEDEC Read ID指令;PolledTransfer会分别发送命令字节和接收数据字节;buff[1]是厂商ID,buff[2]是设备ID,buff[3]是扩展ID。如果读回的flashId为0xFFFFFF,多半是控制器没在QSPI模式,或者CS没有正确选中。
3. 在硬件上烧写FM25F32:从SDK到U-Boot的三种可行路径
3.1 路径一:Vivado SDK自带的QSPI Flash Programmer
Vivado SDK自带GUI烧写工具,位于Xilinx菜单下的Program Flash。操作时先加载FPGA bit文件初始化QSPI控制器,然后选择BOOT.bin并点击Program。第一次使用的人大概率会在这儿碰壁:工具内置的Flash列表里没有FM25F32,它会在Programming完成的瞬间报错Flash ID Mismatch。
原因在于这个工具会校验目标Flash的ID,而复旦微的ID并不在它内置的型号数据库里。解决思路是给这个工具添加FM25F32条目。不同SDK版本的数据库文件路径不同,通常在安装目录下的data/embeddedsw/drivers/qspips目录,文件格式是XML或ini。需要添加厂商ID、设备ID、容量、指令集标志等信息。我实际改过一次,改完后工具能识别了,但在擦除大扇区时依旧会超时,因为它的超时时间阈值是按照常见Flash计算的。所以这条路能走通,但不顺畅,我更推荐把精力花在下面两条路上。
3.2 路径二:U-Boot的sf命令整体烧写
工程量产和调试最常用、也最可靠的是U-Boot的sf命令。前提是U-Boot已经支持FM25F32,至少能sf probe通过。如果还不支持,请先按第4.1节的方法把ID表补上。
确认支持后,先把BOOT.bin传输到内存。有网络环境用tftp,没有的话从SD卡读取:
fatload mmc 0:1 0x01000000 boot.bin然后执行:
sf probe sf erase 0x0 0x400000 sf write 0x01000000 0x0 0x400000参数说明:sf erase的第一个参数是起始偏移,第二个参数是擦除长度。FM25F32整片是0x400000(4MB),擦除时Flash控制器会按自身块大小逐块擦除。sf write的参数依次是内存源地址、Flash目标偏移、写入长度。这里最容易犯的错误是写入长度大于镜像实际大小,把内存里残留的数据也写进去。建议用实际文件长度。
如果BOOT.bin只占头部,后面还要放image.ub,就分两次写:
sf erase 0x0 0x400000 sf write 0x01000000 0x0 0x100000 sf write 0x02000000 0x100000 0x100000第一条写BOOT.bin,第二条写image.ub到偏移0x100000。注意两个地址空间不要重叠,同时确保第一个镜像在启动时知道第二个镜像的位置,否则U-Boot环境变量里也必须对应。
U-Boot下烧写的优势是既可交互也可脚本化。把命令写进文本文件,通过tftp或串口执行,就能实现量产烧写的半自动化。这里我自己的习惯是烧写完成后立刻执行一次整片回读,和内存里的源镜像做对比,避免“写完只提示成功但数据不对”的情况。
3.3 路径三:裸机代码驱动QSPI控制器按页编程
当项目最终要脱离U-Boot,直接在产线用裸机程序烧写时,就需要直接在PS侧QSPI控制器上写代码。裸机编程的大致流程是:初始化控制器、发送写使能、擦除扇区、按页写入、检查忙标志、回读校验。
初始化代码如下:
#include "xqspips.h" XQspiPs QspiInstance; XQspiPs_Config *QspiConfig; QspiConfig = XQspiPs_LookupConfig(XPAR_XQSPIPS_0_DEVICE_ID); XQspiPs_CfgInitialize(&QspiInstance, QspiConfig, QspiConfig->BaseAddress); /* 手动控制片选,适合裸机按命令方式操作 */ XQspiPs_SetOptions(&QspiInstance, XQ_SPI_MANUAL_CS | XQ_SPI_FORCE_SS_ASSERTED); XQspiPs_SetClkPrescaler(&QspiInstance, XQ_SPI_CLK_PRESCALE_16);参数说明:LookupConfig根据设备ID找到控制器配置;CfgInitialize完成基地址映射和初始化;SetOptions里手动CS和强制SS_ASSERTED是为了后续每次传输都保持CS有效;分频系数16把时钟降下来,保证读写稳定。
擦除扇区的逻辑:
u8 erase_cmd[4] = {0x21, 0x00, 0x00, 0x00}; /* Sector Erase 4K */ u32 sector_addr = 0x0; erase_cmd[1] = (sector_addr >> 16) & 0xFF; erase_cmd[2] = (sector_addr >> 8) & 0xFF; erase_cmd[3] = sector_addr & 0xFF; XQspiPs_PolledTransfer(&QspiInstance, erase_cmd, NULL, 4);说明:0x21是Sector Erase 4KB指令,配合24位地址。注意FM25F32若支持Block Erase 64KB(0xD8),擦除大块能节省时间,但前提是确认指令支持。我在这颗Flash上实测过0xD8指令,返回错误,后面一查手册,发现它必须用4KB的0x21,这个坑在第4章会详细展开。
页写入代码:
u8 page_prog[4] = {0x02, 0, 0, 0}; /* 先写使能 */ u8 wren = 0x06; XQspiPs_PolledTransfer(&QspiInstance, &wren, NULL, 1); page_prog[1] = (addr >> 16) & 0xFF; page_prog[2] = (addr >> 8) & 0xFF; page_prog[3] = addr & 0xFF; XQspiPs_PolledTransfer(&QspiInstance, page_prog, NULL, 4); XQspiPs_PolledTransfer(&QspiInstance, data_buf, NULL, page_size);写在写入后必须等待Flash内部完成。等待方式是通过读状态寄存器判断忙位:
u8 status; do { u8 rd_stat = 0x05; XQspiPs_PolledTransfer(&QspiInstance, &rd_stat, &status, 1); } while (status & 0x01);注意,判断忙位的bit位置因芯片而异,FM25F32常见是Bit0,但有些型号会定义其他位。如果发现等待永远无法结束,先去查数据手册的Status Register定义。我就是在这里折腾了一个下午,最后看到Bit1才是Busy,改完代码后一切正常。
4. 避坑与常见问题:FM25F32与主流QSPI Flash的差异排查
4.1 镜像识别与启动异常:ID、Dummy Cycle与上电时序
现象一:sf probe不认Flash,打印unrecognized JEDEC id bytes: xx, xx, xx。
原因:U-Boot内置的Flash ID表未包含复旦微FM25F32。
解决:在设备树中追加compatible = "jedec,spi-nor",开启后许多U-Boot版本会支持任意JEDEC ID;如果用的是较老的内核/U-Boot,需要手动在id_table里添加FM25F32的厂商ID、设备ID、页长、块长。这是最常碰到的第一道坎,也是最容易解决的问题。
现象二:烧写完成后上电,完全无启动迹象,也没有JTAG抓取。
原因:FSBL初始化QSPI时用的Dummy Cycle(空周期)或Mode Bits参数不匹配。FM25F32的Fast Read 0x0B/Quad Read 0x6B需要的dummy cycle数量和Spansion/Winbond不同。
解决:修改FSBL中的QSPI初始化结构体,正确配置读命令和Dummy数。或者先用U-Boot引导验证,确认U-Boot下sf read正常后,再回改FSBL中的参数。这个排查过程会有点烦,建议用示波器抓CLK和IO,看看读出数据在哪个时钟沿开始错位。
现象三:QSPI启动时经常第一次失败,复位后又能启动。
原因:上电时序问题。Flash供电稳定时间和PS启动的时序窗口不一致,或者启动时MISO被拉低。
解决:在硬件上调整Flash使能时间,确保上电后Flash准备好再释放复位;软件上可以在FSBL里加延时等待Flash就绪。建议用示波器看CS拉低前Flash供电是否已稳定。这种现象在国产Flash上时有发生,往往不是芯片坏,而是板卡电源斜坡时间太长。
4.2 擦写过程中的超时与数据错位:指令集、QE位与硬件干扰
现象四:擦除4KB扇区卡死,超时报错。
原因:FM25F32的擦除指令集和常见默认指令不一致。比如一些型号仅支持4KB Sector Erase,不支持64KB Block Erase;或者擦除前没有正确发送写使能(0x06)。
解决:先确认数据手册支持的擦除指令,在驱动里把命令号改对;发送擦除后循环读状态寄存器等到busy释放,不要一味增加延时。在裸机程序里用0xD8擦除时卡死,换成0x21后正常,说明这颗Flash不支持重载指令,只能按4KB小步走。
现象五:Quad模式写入的数据回读错位,SPI模式正常。
原因:FM25F32的Quad Enable(QE)位在状态寄存器中的位置可能不是默认的Bit6。Zynq QSPI控制器开启Quad读前会尝试设置QE位,如果位置不对,实际没进Quad,但后续又按Quad时序发送了指令,导致数据错乱。
解决:在初始化时向状态寄存器写入明确的QE bit位置值,或者干脆保持Single SPI模式。量产环境下,除非要追求下载速度,否则1-bit SPI足够稳定。开发阶段为了赶时间,可以先用SPI模式跑通流程,之后再慢慢研究Quad优化。
现象六:烧写后回读有偶发错误,重试后正常。
原因:DQ2/DQ3(WP/HOLD)引脚悬空,受到干扰导致内部状态跳变。
解决:在硬件上给这两个引脚加10k上拉电阻,再检查Flash去耦电容。大部分偶发问题其实是硬件问题,软件排错排到最后都是泪。我在这块板卡上遇到过一次,最后发现是HOLD引脚没有上拉,导致写入过程中偶发暂停信号,数据链路瞬间中断。
5. 让配置程序更可靠:校验、回读与量产烧写技巧
烧写跑通之后,量产阶段最怕的是坏块和误校验。我现在的习惯是烧写后强制做一次全片回读,逐字节比对源镜像,而不是只比较文件长度。U-Boot下可以用md5sum做一致性校验:
sf read 0x02000000 0x0 0x400000 md5sum 0x02000000 0x400000 md5sum 0x01000000 0x400000第一条把Flash内容读到DDR地址0x02000000,后面两个md5sum分别对回读数据和内存里的源镜像计算MD5,两个值一致才算通过。如果不一致,说明有坏块或者写入时序问题,需要重新擦写。
量产时可以把这些命令写成一个U-Boot脚本,通过串口或网口执行,实现自动化。脚本里加上错误判断,比如在sf write后立即sf read进行回读比对,不通过则打印ERROR并停止,避免带病进入下一道工序。
另外,保留一份经过验证的BOOT.bin原文件,放在固定位置,每次烧写时都从这个源文件同步,防止开发过程中改来改去导致镜像不一致。Flash用久了也需要关心寿命,擦写次数多的时候,出现CRC错误的概率会上升,回读校验这一步真的不能省。
三种烧写路径在实际项目中各有用途:
| 烧写路径 | 适用场景 | 稳定性 | 速度 |
|---|---|---|---|
| SDK Programmer | 开发期 | 一般 | 中 |
| U-Boot sf命令 | 调试/量产 | 高 | 快 |
| 裸机驱动 | 产线集成 | 高 | 可调 |
从那次因为漏掉回读校验、整整浪费一天排查“玄学启动失败”之后,我每次完成QSPI Flash烧写都强制走一遍全片回读和MD5比对,再上电验证。希望帮到你。
本文还有配套的精品资源,点击获取