1. 项目概述:为什么需要“强制烧写”?
在Zynq-7000或UltraScale+ MPSoC的开发流程里,把程序固化到板载的Flash(如QSPI Flash、NAND Flash)中,是产品从原型走向量产的关键一步。标准的固化流程通常依赖于一个前提:开发板必须处于正确的启动模式。比如,对于Zynq-7000,你需要把板上的启动模式拨码开关(MIO[5:2])拨到“从QSPI Flash启动”的模式(通常是0010或0011),然后通过JTAG使用Vivado或Vitis的Flash Programmer工具进行烧写。这个流程清晰、稳定,是官方推荐的做法。
但实际工程中,我们总会遇到一些“标准流程”走不通的尴尬场景。想象一下,你正在调试一个复杂的系统,板卡已经集成到机箱里,启动模式开关被其他模块或外壳挡得严严实实,每次想更新固件都得拆机、拨开关、再装回去,效率极低且容易出错。或者,你手头有一批已经部署在现场的设备,需要通过远程或产线的方式更新程序,你不可能派人去现场手动拨动每一块板子的开关。更常见的是,在开发早期,你可能频繁地在“从JTAG启动调试”和“从Flash启动验证”两种模式间切换,每次都要去拨弄那个小小的拨码开关,不仅麻烦,长期下来还可能造成开关的物理损坏。
“不切换启动模式强制烧写”这个需求,就是在这种背景下被强烈催生出来的。它的核心目标是:无论板卡当前处于何种启动模式(通常是JTAG模式或SD卡模式),我们都能通过JTAG接口,绕过模式检查,直接对Flash存储器进行编程操作。这本质上是一种“特权”操作,它利用了JTAG接口对芯片的底层控制能力,直接与Flash控制器对话,从而摆脱了对启动引脚状态的依赖。
这个需求背后,是工程师对开发效率和部署灵活性的极致追求。它不是一个“花哨”的技巧,而是一个能切实提升工作效率、简化生产流程的实用技能。掌握了它,意味着你对Zynq的启动链和Flash编程机制有了更深一层的理解。
2. 核心原理:启动模式与Flash编程的底层逻辑
要理解如何“强制”烧写,首先得弄清楚在标准流程中,启动模式是如何影响Flash编程的,以及JTAG工具链在背后做了什么。
2.1 Zynq启动模式解析
Zynq的启动过程是一个多级引导的过程。上电或复位后,芯片内部的BootROM(只读存储器)会首先运行。BootROM的第一项工作就是采样MIO[5:2]这几个引脚的状态,根据其电平确定启动设备(Boot Device)。常见的模式有:
- JTAG模式 (MIO[5:2] = 0000):等待JTAG连接,用于调试。
- QSPI Flash模式 (MIO[5:2] = 0010/0011):从板载的QSPI Flash中加载第一级引导程序(FSBL)。
- SD卡模式 (MIO[5:2] = 0110):从SD卡中加载FSBL。
当BootROM检测到是QSPI Flash模式时,它会初始化QSPI控制器,然后从Flash的固定偏移地址(通常是0x0)开始读取数据到片上RAM(OCM)并执行。这个被读取的程序就是FSBL。
2.2 标准Flash烧写流程的“潜规则”
当我们使用Vivado Hardware Manager或Vitis的Flash Programmer进行标准烧写时,工具并非“傻乎乎”地直接往Flash里写数据。它执行了一个精密的“舞蹈”:
- 连接与初始化:通过JTAG连接到Zynq的PS(处理系统)端,此时芯片必须处于JTAG模式或一个允许JTAG接管控制权的状态。
- 下载编程算法:工具会首先将一个微小的、针对特定Flash型号的编程算法(Programming Algorithm)下载到Zynq的RAM中。这个算法是一个可执行程序,它知道如何与QSPI控制器通信,执行擦除、编程、校验等Flash操作。
- 执行算法:工具通过JTAG命令,让Zynq的ARM核跳转到RAM中的这个算法程序并运行。此时,ARM核变成了一个“Flash编程协处理器”。
- 数据传输与烧写:工具通过JTAG将待烧写的数据(如BOOT.BIN)传输到RAM的缓冲区,然后命令编程算法将这些数据写入Flash的指定地址。
关键在于第1步和第2步。标准的Flash Programmer工具在尝试下载编程算法前,会先检查系统的状态。如果它检测到启动模式不是JTAG模式(比如是QSPI模式),它可能会认为系统即将或正在从Flash启动,此时在RAM中下载并运行另一个程序(编程算法)可能会干扰正在运行的固件,导致不可预知的行为(如系统崩溃、通信中断)。因此,作为一种保护机制,工具可能会报错或拒绝执行,提示你需要将启动模式切换到JTAG。
2.3 “强制烧写”的突破口
“强制烧写”的思路,就是绕过工具的这种状态检查,或者以更底层、更直接的方式完成上述流程。主要有两个技术路径:
- 利用XSCT(Xilinx Software Command-Line Tool)进行底层控制:XSCT是Vitis命令行工具,它提供了Tel脚本接口,可以让我们以更精细的粒度控制JTAG会话和芯片。我们可以通过XSCT命令,手动完成连接、暂停处理器、下载编程算法、执行烧写等所有步骤,完全避开GUI工具的状态检查逻辑。
- 编写或使用定制的烧写脚本/程序:基于XSCT的命令,我们可以将整个强制烧写流程封装成一个脚本(.tcl文件)。这个脚本的核心逻辑是:无论当前模式如何,先强制让系统进入一个可控状态(例如,停止ARM核的运行),然后再进行Flash操作。
注意:强制烧写存在一定风险。如果板卡正在从你即将要擦写的Flash区域运行关键程序(虽然概率低,但在QSPI模式下理论存在),直接擦写会导致程序崩溃,JTAG连接也可能中断。因此,操作前务必确认当前运行的程序不重要,或者做好连接失败的心理准备。最稳妥的方式,还是在系统上电后、任何用户程序(如Linux)完全启动前,通过JTAG连接并快速执行烧写脚本。
3. 实操准备:工具与脚本解析
我们将使用XSCT命令行工具来完成强制烧写。这是最灵活、最可靠的方法。你需要准备好以下环境:
- 硬件:搭载了Zynq芯片的开发板(如ZC702, ZedBoard), Flash型号已知(例如Winbond W25Q256FV)。
- 软件:安装好Vivado和Vitis。确保
xsct命令可以在你的终端或命令提示符中运行。 - 待烧写文件:已经生成好的
BOOT.BIN文件。这个文件通常包含FSBL、硬件比特流(Bitstream)和应用程序(如裸机App或U-Boot)。
3.1 核心脚本详解
下面是一个功能强大的强制烧写TCL脚本,我们将其保存为program_flash_force.tcl。我将逐段解释其原理和关键命令。
# program_flash_force.tcl # 描述:用于强制烧写Zynq QSPI Flash,不依赖启动模式开关。 # 1. 连接目标 connect -url TCP:localhost:3121 # 如果使用本地hw_server,通常端口是3121。如果是远程或特定情况,需调整。 targets -set -filter {name =~ "ARM Cortex-A9 #0"} # 这里选择PS端的第一个ARM核。对于Zynq UltraScale+,可能是“Cortex-A53 #0”。 # 2. 停止处理器,确保系统状态可控 stop # 强制暂停ARM核的执行。这是“强制”的关键一步,让芯片进入调试状态,避免运行中的程序干扰。 puts "CPU stopped. Current state is under JTAG control." # 3. 加载Flash编程算法 # 这一步需要指定编程算法文件(.elf)的路径。这个文件由Vitis提供,位于安装目录下。 # 例如:C:/Xilinx/Vitis/2023.2/data/embeddedsw/lib/fixed_algorithm/qspi_versal_algo.elf # 注意:算法文件必须与你的芯片系列和Flash型号匹配。 set algo_file "C:/Xilinx/Vitis/2023.2/data/embeddedsw/lib/fixed_algorithm/qspi_zynq_algo.elf" # 对于Zynq-7000,常用的是 `qspi_zynq_algo.elf` 或 `qspi_polled_zynq_algo.elf`。 # 如果不确定,可以在Vitis GUI中正常配置一次Flash烧写,然后在生成的脚本或日志里找到算法路径。 if {[file exists $algo_file]} { puts "Loading flash programming algorithm: $algo_file" dow $algo_file # `dow` 命令将算法ELF文件下载到目标内存中。默认会下载到合适的地址(如0x100000)。 } else { puts "ERROR: Algorithm file not found at $algo_file" puts "Please check the Vitis installation path and chip family." exit 1 } # 4. 初始化Flash并擦除指定扇区 # 首先需要配置QSPI控制器的参数。这些参数通常与硬件设计中的Flash型号匹配。 # 以下参数适用于常见的 3字节地址、工作在SPI模式的Flash。 flash init -clear -flash_type qspi_single -base-addr 0xFC000000 -cabletype xilinx_tcf # -clear: 清除之前的Flash配置。 # -flash_type: 指定Flash类型和通信模式。`qspi_single`是单线SPI,`qspi_dual`是双线,`qspi_quad`是四线。必须与硬件设计一致! # -base-addr: QSPI Flash在Zynq内存映射中的起始地址。对于Zynq-7000 PS端,通常是0xFC000000。 # -cabletype: 连接类型,一般保持默认。 puts "Flash initialized. Erasing necessary sectors..." # 擦除从0x0地址开始,长度为待烧写文件大小的区域。 # 使用`flash erase_sector`命令。这里假设擦除整个前16MB(0x0 - 0xFFFFFF)以供BOOT.BIN使用。 # 更精确的做法是根据BOOT.BIN文件大小计算需要擦除的扇区数。 flash erase_sector 0xFC000000 0xFCFFFFFF # 地址参数是内存映射地址(0xFC000000 + 偏移量)。擦除0xFC000000到0xFCFFFFFF对应Flash物理地址0x0到0xFFFFFF。 puts "Flash erase completed." # 5. 编程Flash puts "Programming BOOT.BIN into flash..." set bootbin_file "./BOOT.BIN" # 指定你的BOOT.BIN文件路径 if {[file exists $bootbin_file]} { flash write $bootbin_file 0xFC000000 # 将文件写入从Flash内存映射起始地址开始的位置。 # 这会将BOOT.BIN烧录到Flash的物理地址0x0处。 } else { puts "ERROR: BOOT.BIN file not found at $bootbin_file" exit 1 } # 6. 验证烧写内容 puts "Verifying flash content..." flash verify $bootbin_file 0xFC000000 # 将Flash中的内容读回,与原始文件进行比对。 # 7. 清理与复位(可选) puts "Flash programming and verification SUCCESSFUL!" # 可以选择让处理器继续运行,或者复位。为了安全,我们通常断开连接,让用户手动复位。 # con # 如果取消上一行的注释,CPU会从当前停止的位置继续执行,这可能不稳定。 disconnect puts "Disconnected. Please power cycle the board or press the reset button to boot from Flash."3.2 关键参数与适配说明
这个脚本是模板,你需要根据实际情况修改几个关键点:
算法文件 (
algo_file):这是最大的坑点。Xilinx为不同的芯片家族和Flash模式提供了不同的算法。- 查找路径:在你的Vitis安装目录下,搜索
*qspi*algo.elf。常见路径是[Vitis_Install_Path]/data/embeddedsw/lib/fixed_algorithm/。 - 如何选择:
- Zynq-7000, 使用标准库:
qspi_zynq_algo.elf - Zynq-7000, 使用轮询库(更稳定):
qspi_polled_zynq_algo.elf - Zynq UltraScale+:
qspi_versal_algo.elf(注意,即使不是Versal,UltraScale+有时也用这个) - 终极方法:在Vitis GUI中,正常为目标板和Flash型号配置一个Flash烧写工程,然后点击“Generate Flash Image”。在Console或Log窗口中,会打印出它使用的算法文件完整路径,直接复制这个路径最保险。
- Zynq-7000, 使用标准库:
- 查找路径:在你的Vitis安装目录下,搜索
Flash类型 (
-flash_type):必须与你的硬件设计(Vivado中对Flash的配置)完全一致。- 在Vivado Block Design中,查看Zynq Processing System的配置,找到QSPI Flash的设置。看它是Single, Dual, 还是Quad模式。
- 如果设计是Quad模式,但这里用了
qspi_single,烧写必定失败。如果不确定,尝试从qspi_single开始,失败后再尝试qspi_dual或qspi_quad。
基地址 (
-base-addr):- Zynq-7000 PS端:
0xFC000000 - Zynq UltraScale+ PS端:通常是
0xC0000000,但请务必查阅对应芯片的《内存映射手册》。
- Zynq-7000 PS端:
连接方式:
connect -url TCP:localhost:3121假设你本地运行了hw_server。如果你使用直接连接(如USB JTAG),命令可能是connect -hw [device_name]。你可以先打开Vitis Hardware Manager,看看它识别到的服务器URL或设备名称。
4. 执行流程与现场操作记录
现在,我们一步步执行这个强制烧写操作。假设我们的环境是:Windows 10, Vitis 2023.2, 一块Zynq-7020开发板,启动模式开关目前在QSPI启动模式(0010),但我们想通过JTAG强制更新Flash。
4.1 第一步:启动硬件服务器
打开一个命令提示符或终端,首先启动Xilinx硬件服务器。这个服务器负责管理JTAG电缆和芯片的通信。
hw_server保持这个窗口运行。你会看到它监听在localhost:3121端口。
4.2 第二步:准备脚本和文件
在另一个终端窗口中,进入你的项目目录。确保以下文件存在:
program_flash_force.tcl(我们刚才编写的脚本)BOOT.BIN(待烧写的镜像文件)
使用文本编辑器打开脚本,根据上一节的说明,修改algo_file的路径、-flash_type等参数。例如,我确认我的设计是Quad SPI,算法文件在C:/Xilinx/Vitis/2023.2/data/embeddedsw/lib/fixed_algorithm/qspi_polled_zynq_algo.elf。
4.3 第三步:执行XSCT并运行脚本
在项目目录下,启动XSCT:
xsctXSCT交互环境启动后,提示符会变成xsct%。然后,使用source命令执行我们的TCL脚本:
xsct% source program_flash_force.tcl4.4 第四步:观察输出与问题判断
脚本开始运行,你会在终端看到实时输出。一个成功的流程输出大致如下:
connected to hw_server @ localhost:3121 Currently selected hardware target is not connected. Available targets: 1 xc7z020 (JTAG Device: 0, Index: 0) 2 ARM Cortex-A9 MPCore #0 (JTAG Device: 0, Index: 1) 3 ARM Cortex-A9 MPCore #1 (JTAG Device: 0, Index: 2) targets -set -filter {name =~ "ARM Cortex-A9 #0"} 2 ARM Cortex-A9 MPCore #0 (JTAG Device: 0, Index: 1) stopped. CPU stopped. Current state is under JTAG control. Loading flash programming algorithm: C:/Xilinx/Vitis/2023.2/data/embeddedsw/lib/fixed_algorithm/qspi_polled_zynq_algo.elf Downloading Program -- C:/Xilinx/Vitis/2023.2/data/embeddedsw/lib/fixed_algorithm/qspi_polled_zynq_algo.elf section, .text: 0x100000 - 0x1018f3 section, .init: 0x1018f4 - 0x10191b section, .fini: 0x10191c - 0x101943 section, .data: 0x101944 - 0x101a6f 100% (0x1a70) bytes downloaded at 0x00100000 Flash initialized. Erasing necessary sectors... Erasing flash at 0xfc000000... 100% done. Flash erase completed. Programming BOOT.BIN into flash... Programming flash at 0xfc000000... 100% done. Verifying flash content... Verifying flash at 0xfc000000... 100% done. Flash programming and verification SUCCESSFUL! Disconnected. Please power cycle the board or press the reset button to boot from Flash.看到最后的SUCCESSFUL!和验证通过,就表示强制烧写成功了。此时,即使你的启动模式开关还在QSPI位置,Flash里的内容也已经更新。你需要给板子断电再上电,或者按一下硬件复位按钮,让BootROM重新读取Flash,新的程序就会运行。
5. 常见问题与排查技巧实录
在实际操作中,你几乎一定会遇到各种错误。下面是我在多次实践中总结的“踩坑实录”和解决方案。
5.1 错误:ERROR: Flash Operation Failed或Cannot load flash programming algorithm!
这是最常见的一类错误,原因多样。
排查点1:算法文件路径与型号
- 症状:脚本在
dow $algo_file时报错,提示文件找不到或加载失败。 - 解决:百分之九十的问题出在这里。再次确认路径是否正确,文件名是否精确。强烈建议使用Vitis GUI生成一次Flash镜像,从日志中复制算法文件的绝对路径。另外,确保算法与芯片匹配(Zynq-7000不能用UltraScale+的算法)。
- 症状:脚本在
排查点2:Flash类型 (
-flash_type) 不匹配- 症状:能加载算法,但在
flash init或flash erase时失败。 - 解决:检查Vivado中QSPI控制器的配置。打开你的Block Design,双击Zynq IP,查看
MIO Configuration->Quad SPI Flash。看是Single, Dual, 还是Quad。脚本中的-flash_type参数必须与此一致。如果不确定,可以依次尝试qspi_single,qspi_dual,qspi_quad。
- 症状:能加载算法,但在
排查点3:硬件连接或电源问题
- 症状:连接不稳定,时好时坏,错误信息不固定。
- 解决:检查JTAG电缆是否插稳,板卡供电是否正常。尝试换一个USB口,或者重启
hw_server。
5.2 错误:Error while launching program: memory write error at 0x10000
这个错误通常发生在dow命令下载算法文件到内存时。
- 原因:脚本试图将算法下载到一个被当前运行程序占用的内存地址,或者该地址不可写。
- 解决:
- 确保CPU已停止:在
dow之前,必须有stop命令。如果stop失败,可能是JTAG连接权限不足,或者芯片处于某种锁死状态。尝试先执行targets -set -filter {name =~ “ARM Cortex-A9 #0”}再stop。 - 指定下载地址:
dow命令默认会自己找一个“安全”的地址,但有时不准。可以显式指定一个OCM(On-Chip Memory)地址,这部分内存通常总是可用的。修改dow命令:
将算法下载到0xFFFF0000(OCM高端地址)。然后,后续的dow $algo_file 0xFFFF0000flash init命令可能不需要改,因为算法自己知道在哪里运行。如果还不行,可能需要修改算法本身的链接地址,但这很复杂,优先尝试方法1和3。 - 更彻底的停止:在
stop之后,可以尝试执行rst命令进行软复位,让系统回到一个更干净的状态,然后再stop。但注意,rst后需要重新连接(connect)和选择目标(targets -set)。
- 确保CPU已停止:在
5.3 错误:flash download failed - target dll has been cancelled或flash timeout
这类错误通常指向通信超时或中断。
- 原因:Flash操作(尤其是擦除)耗时较长,JTAG连接在等待响应时超时,或者被其他进程(如突然弹出的杀毒软件)干扰。
- 解决:
- 增加超时时间:在
flash init之前,可以设置一个更长的超时。但XSCT命令本身可能没有直接参数。可以尝试在Vitis GUI的配置中调整,但对于脚本,更有效的方法是: - 优化擦除范围:不要动辄擦除整个Flash。计算你的
BOOT.BIN文件大小,只擦除必要的扇区。例如,如果BOOT.BIN是1MB,你可以只擦除0x0到0x100000的区域。
这样可以大大缩短擦除时间,降低超时风险。# 计算文件大小(字节) set file_size [file size $bootbin_file] # 转换为十六进制,并减去1(因为地址从0开始) set end_addr [format 0x%X [expr $file_size - 1]] # 擦除对应的内存映射区域 flash erase_sector 0xFC000000 0xFC$end_addr - 确保环境纯净:关闭不必要的软件,特别是可能占用USB端口的软件。以管理员身份运行命令提示符和XSCT。
- 增加超时时间:在
5.4 错误:烧写成功但板子不启动
- 排查点1:启动模式开关:虽然我们是强制烧写,但板子上电时BootROM依然会读取模式引脚。请再次确认你的启动模式开关确实拨到了QSPI启动模式(例如0010)。烧写只是改写了Flash内容,启动设备的选择权仍在硬件开关。
- 排查点2:BOOT.BIN文件格式:确保你的
BOOT.BIN文件是通过bootgen工具正确生成的,包含了FSBL。一个常见的错误是直接把应用程序的.elf文件改名为.bin就去烧写,这肯定无法启动。标准的BOOT.BIN结构是:FSBL + Bitstream + Application。 - 排查点3:Flash偏移地址:BootROM默认从Flash的物理地址0x0开始读取。我们的脚本烧写到
0xFC000000(内存映射地址),对应的就是物理地址0x0。请确保没有烧错偏移量。有些特殊的引导流程(如MultiBoot)可能会从其他地址启动,但标准启动就是0x0。 - 排查点4:硬件复位:烧写完成后,必须对板卡进行完整的断电再上电,或者按硬件复位按钮。软复位(通过JTAG发送
rst命令)有时不足以让BootROM重新初始化Flash控制器。
5.5 高级技巧与心得
将脚本参数化:创建一个更通用的脚本,通过命令行参数传入
BOOT.BIN路径和Flash类型,方便重复使用。# 用法: xsct program_flash_force.tcl my_boot.bin quad set bootbin_file [lindex $argv 0] set flash_mode [lindex $argv 1] flash init -clear -flash_type qspi_$flash_mode -base-addr 0xFC000000 -cabletype xilinx_tcf先读后写:在强制烧写前,特别是对正在运行的系统,可以先尝试读取Flash的头部内容,确认连接和访问是否正常。
# 读取Flash起始处4个字节 read_memory 0xFC000000 -size 4 -binfile flash_header.bin # 然后用hex编辑器查看flash_header.bin,应该能看到FSBL的魔数或代码。日志是救星:在执行脚本时,使用
xsct的日志重定向功能,将输出保存到文件,便于仔细分析错误。xsct -eval "source program_flash_force.tcl" > flash_log.txt 2>&1心理准备:强制烧写不是100%安全的。如果板子正在从待烧写的Flash区域运行关键代码(例如一个简单的裸机程序),擦写操作会导致程序立即崩溃,JTAG连接也可能丢失。因此,最佳操作时机是上电后,在FSBL运行之前(即BootROM阶段)或FSBL运行初期但还未跳转到应用程序时,通过JTAG快速连接并执行脚本。对于运行Linux等复杂系统的板子,风险更高,操作需格外谨慎。