Zynq强制烧写Flash:绕过启动模式限制的PL辅助方案详解
1. 项目概述:为什么需要“强制烧写”?
在Zynq-7000或UltraScale+ MPSoC的开发流程里,把程序固化到板载的QSPI Flash或NAND Flash里,是产品最终交付前的标准操作。常规的固化流程大家都很熟悉:在Vivado或Vitis里生成BOOT.bin,然后通过SDK/XSCT的program_flash命令,或者直接用JTAG配合flash命令,把镜像写进去。这个流程有个前提——你的Zynq芯片得处于能从Flash启动的模式(通常是MIO[5:0]=001010或001100对应的QSPI模式)。但实际调试中,我们经常会遇到一个尴尬的局面:板子可能因为硬件设计、模式引脚配置错误,或者干脆就是为了图省事,一直挂在JTAG模式上,没切到Flash启动模式。这时候你想烧写Flash,工具链就会报错,提示找不到Flash设备或者无法建立连接。
“不切换启动模式强制烧写”这个需求,就是针对这种场景的。它的核心价值在于提升调试效率,避免不必要的硬件操作。想象一下,你正在实验室调试一块核心板,启动模式跳线帽被其他板子借走了,或者板子已经集成到机箱里,抠跳线帽非常麻烦。又或者,你只是想在JTAG调试过程中,临时更新一下Flash里的内容,而不想重启、拔插、切换模式。这时候,如果能在JTAG模式下直接“强行”把程序写进Flash,无疑会省去大量时间,让开发流程更流畅。
这个操作的本质,是绕过Xilinx工具链对启动模式的常规检查,直接通过JTAG接口对Flash存储器进行底层编程。它依赖的是JTAG链对PS(处理系统)和PL(可编程逻辑)的完全控制能力。当你通过JTAG连接芯片时,你实际上拥有了最高的权限,可以配置PS端的MIO、初始化Flash控制器、然后像操作普通存储器一样,对挂在PS端的Flash芯片进行擦除和写入。这听起来有点“硬来”的感觉,但只要你清楚芯片的地址映射和Flash的操作时序,这在技术上是完全可行的,而且非常稳定。
接下来,我会拆解这个操作的完整思路、具体步骤,以及我踩过的那些坑。无论你是用的是Zynq-7000还是更高级的MPSoC,原理都是相通的。我会以最常见的Zynq-7020和Micron的QSPI Flash(型号如N25Q128A)为例,但方法可以举一反三,适配其他型号。
2. 核心思路与方案选型:绕过启动模式检查的几种路径
要实现强制烧写,核心矛盾在于:在JTAG模式下,PS的启动ROM不会去初始化Flash控制器,因此Flash设备对PS是不可见的。我们的目标就是手动完成这个初始化过程,让Flash变得可访问。主要有三种实现路径,各有优劣。
2.1 路径一:纯PS端JTAG脚本控制
这是最直接、依赖最少的方法。完全通过XSCT(Xilinx Software Command-Line Tool)或SDK的TCL脚本环境,利用JTAG连接,执行一系列命令来配置PS,然后烧写Flash。
工作原理:
- 通过JTAG连接芯片,停止ARM Cortex-A9核心的运行。
- 通过JTAG直接配置PS的MIO引脚,将其模拟设置为QSPI启动模式下的状态(特别是时钟、片选、数据线)。
- 直接对PS内部的QSPI Flash控制器寄存器进行编程,使其初始化并进入正常工作模式。
- 将Flash芯片映射到PS的地址空间(通常是0xQSPI_LINEAR_BASE,如0xFC00_0000)。
- 使用通用的Flash编程算法,或直接通过控制器向该地址空间写入数据,完成烧录。
优点:
- 无需额外硬件或PL工程:只需要标准的JTAG调试器和XSCT,环境最简洁。
- 通用性强:理论上适用于任何Zynq板卡,只要JTAG连接正常。
缺点:
- 步骤繁琐,寄存器操作复杂:需要非常熟悉PS的寄存器手册(UG585),手动配置几十个寄存器,极易出错。
- 稳定性依赖脚本:不同板卡、Flash型号的细微差异(如上拉电阻、时钟频率)都需要调整脚本,调试成本高。
- 速度可能较慢:通过JTAG间接操作寄存器,再读写Flash,速度不如PL方案。
实操心得:早期我尝试过这条路,为此写了几百行的TCL脚本。最大的坑在于Flash控制器的“DACR”(设备地址控制寄存器)和“LQSPI_CR”(线性QSPI控制寄存器)的配置顺序,一旦弄反,Flash就会进入一种“死”状态,必须完全断电重启才能恢复。对于偶尔操作来说,性价比不高。
2.2 路径二:利用PL设计辅助(推荐方案)
这是我最推荐,也是实践中最稳定、最高效的方案。其核心思想是:在PL部分设计一个简单的Flash控制器(或复用已有的AXI Quad SPI IP核),通过JTAG加载一个临时的PL比特流,然后通过这个PL侧的控制器去读写Flash。
工作原理:
- 在Vivado中为你的板卡创建一个最简工程,主要包含Zynq Processing System IP和AXI Quad SPI IP。
- 正确配置AXI Quad SPI IP,使其与板载Flash的型号、速度模式匹配,并连接到PS的AXI总线。
- 生成比特流文件(.bit)。
- 通过JTAG连接板卡,在XSCT中: a. 下载这个.bit文件到PL,配置PL。 b. 此时,AXI Quad SPI IP已经工作,Flash通过PL被挂载到了PS的地址空间。 c. 在XSCT中,你可以直接使用
dow命令将编程算法和镜像数据加载到PS的OCM或DDR中,然后运行一个小的烧写程序(通常由program_flash命令内部完成),或者直接通过内存读写命令操作AXI Quad SPI映射的地址来烧写Flash。
优点:
- 稳定可靠:利用了经过充分验证的AXI Quad SPI IP核,省去了手动配置PS寄存器的麻烦,兼容性最好。
- 速度快:通过AXI总线操作Flash,速度接近Flash的理论极限。
- 一劳永逸:为你的板卡做好一个通用的“强制烧写比特流”后,以后可以反复使用。
缺点:
- 需要提前准备比特流:必须为你的具体板卡和Flash型号生成一个匹配的.bit文件。
- 依赖PL资源:需要占用少量的PL逻辑和引脚资源,但对于绝大多数Zynq芯片来说这微不足道。
2.3 路径三:使用第三方开源工具(如OpenOCD)
这是一种更底层、更通用的方法。OpenOCD可以绕过Xilinx的工具链,直接通过JTAG接口发送命令,控制ARM核心和总线,从而操作Flash控制器。
优点:
- 跨平台,不依赖Vivado/Vitis:可以在Linux环境下独立运行。
- 灵活性极高:可以编写自定义的Flash驱动脚本。
缺点:
- 配置极其复杂:需要编写或修改OpenOCD的板级配置文件(.cfg),定义Flash芯片的详细参数和编程算法。
- 社区支持有限:针对特定Zynq板卡和Flash的成熟配置文件较少,需要自己摸索调试。
- 不适合快速解决问题:学习曲线陡峭。
综合来看,路径二(PL辅助方案)在易用性、稳定性和效率上取得了最佳平衡,是我们接下来重点详解的方案。它完美地解决了“启动模式不对”的问题,因为PL的配置完全由JTAG控制,与PS的启动模式无关。
3. 实操准备:创建通用的“强制烧写”比特流
这个步骤是整个方案的基础。你需要为你的目标硬件创建一个一次性的Vivado工程。
3.1 硬件平台确认与IP核配置
首先,明确你的板卡信息:
- 芯片型号:例如,XC7Z020-CLG400(Zynq-7020)。
- Flash型号与连接方式:这是最关键的一步。查看原理图,确认Flash芯片的具体型号(如Winbond W25Q256JV)、封装(如SOIC-8)、以及它是连接在PS的MIO上,还是通过PL转接。绝大多数开发板都是直接接在PS的MIO上。同时记录Flash的时钟频率(通常为50MHz或100MHz)。
打开Vivado,创建一个新的RTL工程,选择对应的芯片型号。
- 创建Block Design:添加一个Zynq Processing System IP核。
- 配置Zynq PS:
- 双击打开配置界面。
- 在“Peripheral I/O Pins”选项卡中,找到“Quad SPI Flash”。确保它被启用。即使你最终不用PS来驱动它,这个启用操作会确保相关的MIO引脚被正确分配和约束。
- 根据原理图,在“MIO Configuration”中查看QSPI相关的MIO引脚号(例如MIO[1:0]是数据线,MIO5是时钟,MIO6是片选)。这里主要是为了确认,不需要改动,因为我们将用PL的IP来驱动。
- 其他PS配置(如DDR型号、时钟)请根据你的板卡实际情况设置,但对此处功能非必需,可以先用默认值。
- 添加并配置AXI Quad SPI IP:
- 在Diagram中添加“AXI Quad SPI”IP核。
- 双击配置:
Mode: 选择“Standard Mode”。(如果你的Flash支持并使用了Dual或Quad模式以提高速度,也可以选择“Dual”或“Quad”,但Standard模式兼容性最好)。Frequency Ratio: 这个值决定了SPI时钟(s_axi_aclk)和SCK的关系。例如,如果AXI总线时钟100MHz,你想要SCK为50MHz,则比率设为2。初始调试建议设大一点(如8或16),降低时钟频率以保证稳定性。Transaction Width: 选择“8 Bits”(针对标准SPI指令)。FIFO Depth: 默认16即可。- 在“IP Configuration”下,取消勾选
C_SCK_RATIO的“Calculate”选项,然后手动输入一个值(如上面的频率比率值),这样配置更明确。
- 将这个IP的
ext_spi_clk和s_axi_aclk都连接到Zynq PS的FCLK_CLK0(通常为50M或100M)。 - 将IP的
SPI接口导出为外部端口(Make External),端口名例如qspi_flash。
- 连接与自动化:
- 使用
Run Connection Automation,将AXI Quad SPI的AXI4-Lite接口连接到Zynq PS的M_AXI_GP0接口。 - 将Zynq PS的
FCLK_RESET0_N连接到AXI Quad SPI和系统复位逻辑。
- 使用
- 创建顶层HDL与引脚约束:
- 右键Block Design,创建HDL Wrapper。
- 打开生成的顶层.v文件,找到导出的
qspi_flash端口。根据你的原理图,为这些端口分配正确的FPGA引脚。qspi_flash_io[3:0]-> 对应Flash的IO0, IO1, IO2, IO3(数据线)qspi_flash_sck-> 对应Flash的CLKqspi_flash_ss-> 对应Flash的CS#
- 在XDC约束文件中添加引脚位置和电平标准约束,例如:
关键点:这里的引脚约束,必须与你原理图上Flash芯片实际连接的FPGA引脚完全一致。这步错了,后续一切免谈。set_property PACKAGE_PIN T19 [get_ports {qspi_flash_io[0]}] set_property IOSTANDARD LVCMOS33 [get_ports {qspi_flash_io[0]}] # ... 为所有qspi_flash端口添加类似约束
3.2 生成与归档比特流
- 综合、实现、生成比特流:在Vivado中运行标准的流程。
- 导出硬件:生成.xsa文件(Vivado 2019.1及以后)或.hdf文件(旧版本)。这一步包含了硬件描述信息。
- 归档文件:将以下三个文件保存好,它们构成了你的“强制烧写工具包”:
强制烧写.bit:PL配置文件。强制烧写.xsa(或.hdf):硬件描述文件。强制烧写.xdc:引脚约束文件(用于追溯和修改)。
注意事项:强烈建议为这个“工具包”建立一个独立的版本管理。如果未来硬件改版(Flash型号或连接引脚变化),你需要同步更新这个比特流。
4. 强制烧写全流程解析与XSCT脚本详解
有了比特流,我们就可以在XSCT(Vitis或Vivado自带的命令行工具)中执行强制烧写了。下面是一个完整的、可复用的TCL脚本示例,并附上逐行详解。
假设我们要烧写的文件是BOOT.bin,它位于D:\project\bootimage目录下。
# 强制烧写Flash脚本 (force_program_flash.tcl) # 使用前请修改以下变量: set BIT_FILE "D:/path/to/your/强制烧写.bit" set XSA_FILE "D:/path/to/your/强制烧写.xsa" set FLASH_IMAGE "D:/project/bootimage/BOOT.bin" set TARGET_IP "localhost:3121" ;# 根据你的JTAG服务器端口修改 # 连接到硬件服务器 connect -url $TARGET_IP # 获取目标设备,通常第一个就是Zynq targets -set -filter {name =~ "*ARM*#0"} # 1. 下载并配置PL比特流(关键步骤!) puts "Step 1: Downloading PL configuration bitstream..." fpga -file $BIT_FILE puts "PL configured successfully via JTAG." # 2. 下载并运行Flash编程算法(从.xsa中提取) puts "Step 2: Loading Flash programming algorithm..." # 注意:`program_flash`命令需要硬件描述来定位Flash program_flash -f $FLASH_IMAGE -offset 0 -flash_type qspi_single -verify # -flash_type 根据你的Flash连接模式选择:qspi_single, qspi_dual, qspi_quad等 # -offset 0 表示从Flash的0地址开始烧写 puts "Flash programming completed!" disconnect脚本关键点解析与避坑指南:
fpga -file命令:这是强制烧写的灵魂。它通过JTAG直接将.bit文件配置到PL中,完全独立于PS的启动模式。执行后,你的AXI Quad SPI IP就开始工作了,Flash控制器已经就绪。program_flash命令的奥秘:这个命令看起来和正常烧写一样,但它此时能成功执行,依赖的是上一步配置好的PL。命令执行时,它会:- 从提供的.xsa文件中解析出Flash控制器的信息(虽然我们用的是PL的IP,但.xsa里包含了系统地址映射)。
- 将一个微小的“Flash编程算法”ELF文件下载到PS的OCM(On-Chip Memory)中并运行。
- 这个算法程序通过我们刚刚配置好的AXI Quad SPI IP,与Flash芯片通信,执行擦除、编程、校验等操作。
-flash_type参数:这是最容易出错的地方之一。你必须根据实际硬件连接和AXI Quad SPI IP的配置模式来选择。- 如果你的Flash只用了SI/SO两根线(标准SPI),IP配置为Standard Mode,则选
qspi_single。 - 如果用了两根数据线(IO0, IO1),IP配置为Dual Mode,则选
qspi_dual。 - 如果用了四根数据线(IO0, IO1, IO2, IO3),IP配置为Quad Mode,则选
qspi_quad。 - 选错会导致烧写失败或数据错误。如果不确定,先用
qspi_single尝试,这是最兼容的模式。
- 如果你的Flash只用了SI/SO两根线(标准SPI),IP配置为Standard Mode,则选
-verify参数:强烈建议加上。烧写完成后,它会回读Flash内容并与原始镜像比较,确保数据无误。Flash烧写偶尔会因电源波动等原因出现位错误,校验能给你最后的保障。
如何执行这个脚本?
打开Vitis或Vivado的XSCT命令行窗口(通常在Xilinx Design Tools -> XSCT Console),或者直接运行xsct.bat。然后执行:
source force_program_flash.tcl如果一切顺利,你将看到PL配置成功和Flash烧写进度的输出。
5. 常见问题排查与实战技巧实录
即使按照上述步骤操作,你也可能会遇到各种问题。下面是我在多次实践中总结的“排错清单”和技巧。
5.1 问题一:program_flash失败,提示“Cannot find Flash device”或“Flash programming algorithm failed to initialize”
这是最典型的错误,意味着工具无法与Flash建立通信。
排查步骤:
- 确认比特流正确加载:在执行
program_flash前,先用fpga -state命令检查PL是否已成功配置。状态应为PROGRAMMED。 - 检查
-flash_type参数:这是首要怀疑对象。用示波器或逻辑分析仪抓取Flash的CS#和CLK引脚。如果program_flash命令执行期间,完全没有片选或时钟信号产生,那一定是通信模式不匹配。尝试更换-flash_type(如从qspi_quad换成qspi_single)。 - 检查引脚约束:用硬件工具(示波器)测量Flash的引脚。确认在PL配置后,FPGA对应的引脚是否有信号输出?如果没有,说明你的.xdc约束文件可能错了,Flash引脚没有真正被PL的IP驱动。回头仔细核对原理图和约束文件。
- 检查Flash供电和硬件连接:确保Flash芯片的VCC、地线连接良好。用万用表测量电压是否正常(通常是3.3V或1.8V)。
- 降低时钟频率:回到Vivado,将AXI Quad SPI IP的
Frequency Ratio调大(即降低SCK频率),重新生成比特流。过高的时钟频率可能导致信号完整性问题,尤其是在飞线或长走线的情况下。
5.2 问题二:烧写过程缓慢,或在中途卡住、报超时错误
排查步骤:
- 检查Flash容量和镜像大小:使用
program_flash时,工具默认会擦除整个Flash扇区。如果你的Flash很大(如256Mb),而BOOT.bin很小,擦除整个芯片会非常耗时。可以使用-erase和-blankcheck参数进行更精细的控制,但一般不建议新手操作。 - 优化烧写脚本:
program_flash命令在后台会先擦除再编程。对于大容量Flash,这个过程可能超过JTAG服务器的默认超时时间。可以在连接时增加超时设置:connect -timeout 30000(单位毫秒)。 - 电源问题:Flash编程,特别是擦除操作,需要较大的瞬时电流。如果板卡电源功率不足或纹波过大,可能导致操作失败。确保使用稳定可靠的电源,并在Flash的VCC引脚附近有足够的去耦电容。
5.3 问题三:烧写成功,但板卡从Flash启动失败
烧写工具显示成功,但将启动模式切换到Flash后,板卡无法启动。
排查步骤:
- 验证BOOT.bin文件:首先确认你烧写的
BOOT.bin本身是正确的。可以在JTAG模式下,通过dow命令将这个BOOT.bin直接加载到DDR中运行,看功能是否正常。如果JTAG直接运行都失败,那问题在镜像本身。 - 检查烧写偏移地址:Zynq芯片从Flash启动时,默认从0x0地址开始读取数据。确保你的
program_flash命令使用了-offset 0参数。如果你烧写到了其他偏移地址,启动ROM是找不到引导头的。 - 检查Flash连接模式与启动模式设置:烧写时我们可能用了
qspi_single模式,但你的板卡硬件设计可能要求Zynq以Quad模式去读取Flash。这由Zynq的MIO[5:0]启动模式引脚决定。烧写模式可以和启动读取模式不同。例如,你可以用Standard模式烧写,但板卡配置为Quad模式启动。只要Flash里的数据格式正确,这是可以的。但更常见的是,两者不匹配导致启动失败。请核对原理图中启动模式电阻的设置,确保其与你期望的Flash读取模式一致。 - 使用Vivado的Flash读写功能进行验证:在Vivado Hardware Manager中,配置好PL后,可以尝试直接通过“Read/Write Memory”功能,读取Flash线性地址(如0xFC000000)开头的数据。你应该能看到BOOT.bin的头部信息(如FSBL的代码)。如果读出的全是0xFF或乱码,说明烧写的数据不对或没写进去。
5.4 独家避坑技巧
- 制作“黄金参考”比特流:为你手头每一款不同的板卡,都提前生成并保存好一个经过验证的“强制烧写比特流”。文件名可以包含芯片和Flash型号,如
zynq7020_w25q256_force_program.bit。这能节省大量重复调试时间。 - 在脚本中加入“Flash ID读取”验证:在执行正式烧写前,先运行一个读取Flash ID的小测试。你可以写一个简单的内存读写脚本,通过AXI Quad SPI的寄存器空间发送
0x9F(读ID)命令,并读取返回的制造商和设备ID。这能最直接地证明JTAG->PL->Flash这条通路是畅通的。 - 善用逻辑分析仪:如果遇到通信问题,逻辑分析仪是你的最佳伙伴。抓取SPI总线上的波形,对照Flash数据手册的时序图,可以清晰看到指令、地址、数据是否被正确发送和接收。很多棘手的软件问题,在波形面前一目了然。
- 注意Flash的写保护位:有些Flash芯片出厂时或上次操作后可能被设置了写保护(通过状态寄存器)。在烧写前,需要先发送解锁命令。
program_flash命令通常会处理这个,但如果它失败了,可以尝试手动通过XSCT发送SPI命令来清除写保护。
最后,我个人最深刻的体会是:“强制烧写”本质上是一种调试和应急手段,它体现了对硬件底层更深入的控制力。熟练掌握它,不仅能解决启动模式不对的尴尬,更能让你在遇到更复杂的Flash相关问题时(如Flash部分损坏需要修复、更新特定参数区等),拥有除标准流程外的另一把利器。把PL当作一个可编程的“桥梁”,通过JTAG这座“总控台”,你几乎可以操作板卡上的任何资源,这种自由度正是嵌入式开发的魅力所在。
