做过ZYNQ(Zynq-7000)在线升级的工程师,大概率都遇到过这两种情况:固件明明写进了QSPI Flash,复位一下,板子却还在跑旧程序;或者设备死机后看门狗一直在复位,可是系统要反复重启好几次才恢复。这些现象背后其实都指向同一个机制——ZYNQ的启动不是简简单单“从头跑一次”,BootROM、FSBL、Multiboot寄存器、启动介质上的镜像头共同决定了系统下一次从哪里起来。把这套东西弄明白,软件复位和程序跳转就完全可控了。
这篇文章就围绕“ZYNQ软件复位重启、程序跳转”展开,重点讲透Multiboot机制。内容包括ZYNQ启动流程回顾、软复位和程序跳转的几种实现、双镜像在线升级的完整实操流程,以及我实际项目中踩过的坑和排查方法。适合正在做ZYNQ裸机开发、需要在QSPI或SD卡上做A/B镜像升级、或者被看门狗复位问题反复折磨的工程师参考。内容基于Zynq-7000平台,Zynq UltraScale+的寄存器布局不同,方法可借鉴但不能直接套用。
1. 先搞清楚ZYNQ怎么启动的,才能理解什么叫跳转
1.1 启动链:BootROM → FSBL → 应用
ZYNQ-7000的上电启动分成三个阶段。第一阶段是片内BootROM,在上电复位后由硬件自动执行,代码固化在芯片里,改不了。BootROM会根据MIO[6:2]上的启动模式引脚去初始化对应的外部存储控制器,然后在启动介质上找“Image Header”。这个Header是64字节的,以魔数0xAA995566开头,里面记录了FSBL镜像在Flash中的偏移、长度、加载地址和执行地址。BootROM拿到这些信息后,把FSBL从Flash搬到OCM(片内256KB SRAM),然后跳进去执行。
第二阶段是FSBL,也就是First Stage Boot Loader。它的工作比BootROM复杂得多:初始化MIO、PLL、DDR控制器等PS关键外设;如果有PL bitstream,还要通过DevCfg接口给FPGA下载配置;最后把应用镜像(裸机ELF或者U-Boot)加载到DDR,跳到它的入口地址。FSBL这层还承担了Multiboot地址的解析和回退逻辑,后面会详细展开。
第三阶段就是你的应用了。裸机模式下,App在DDR里直接跑;Linux环境下,U-Boot作为第二阶段负载执行,接着引导内核和根文件系统。U-Boot会去读boot.scr脚本、加载image.ub,完成整个系统启动。
理解这个链条之后,你就会明白“程序跳转”到底是在跳什么:不是简单地在内存里用一个函数指针指过去,而是决定BootROM/FSBL下一次去哪个地址找镜像、找哪个镜像。这个决策信息就存在DevCfg模块的MULTIBOOT_ADDR寄存器里。
1.2 Multiboot是什么,解决什么问题
Multiboot的官方定义很直接:它允许FSBL/BootROM不固定从Flash偏移0x0处启动,而是从MULTIBOOT_ADDR寄存器指定的地址启动。这个寄存器在DevCfg模块中,地址是0xF8007010,默认值是0,表示“不启用Multiboot,按默认地址启动”。
为什么需要这个机制?最典型的场景是A/B镜像升级。Flash里同时放着旧镜像(Golden Image)和新镜像(Update Image)。升级时如果直接覆盖旧镜像,一旦新镜像有问题,系统就起不来了。有了Multiboot,可以把新镜像写到Flash的另一个偏移位置,然后设置MULTIBOOT_ADDR指向新位置,复位后系统先尝试新镜像。如果新镜像在加载阶段就校验失败,FSBL还能清掉Multiboot地址,自动退回旧镜像。这是一个硬件层面的容错升级方案,不依赖外部看门狗芯片也能实现基本回退。
有一个细节必须重点提醒:MULTIBOOT_ADDR寄存器里存的值和Flash真实字节偏移不是直接相等的。FSBL会根据启动介质做换算,QSPI模式下常见做法是把Flash偏移右移2位再写入寄存器,BootROM/FSBL读出来再左移回去。NAND、SD等介质的换算系数又不同。具体换算逻辑在Vitis生成的FSBL代码里维护,写工程时一定要去xfsbl_multi_boot.c里确认自己平台的系数,别想当然地直接把偏移值写进去。
2. 软件复位和程序跳转的几种实现方式
2.1 最简单的软复位:PSS_RST_CTRL
软件复位(Warm Reset)和上电复位(Power-On Reset)最大的区别有两个:一是复位源不同,软复位不触发电源时序,不彻底断电,所以DDR里的数据大概率还在;二是BootROM依然会被执行,启动流程会重新走一遍,只不过启动介质、Multiboot寄存器这些硬件状态是上一次遗留下来的。如果你没有修改过Multiboot地址,那么软复位后系统会从默认位置重新加载,跑的还是原来那套程序。
ZYNQ-7000上最常用的软件复位方法是直接操作PSS_RST_CTRL寄存器,地址0xF8000200,向bit0写1即可触发:
#include "xil_io.h" #define PSS_RST_CTRL 0xF8000200 #define SOFT_RST_MASK 0x1U void system_soft_reset(void) { Xil_Out32(PSS_RST_CTRL, SOFT_RST_MASK); while (1) { /* 等待复位生效 */ } }这个操作在裸机和Linux下都有效。Linux下用devmem也能直接敲:
devmem 0xF8000200 32 0x1写完之后CPU会立刻复位,后面的代码不再执行。注意不要在中断上下文里调用这种函数,因为复位是全局的,中断上下文里调用没有任何意义。复位前也要把重要数据先落盘,DDR内容虽然大概率保留,但软件复位不保证断电,万一后面有人改了电源方案,这个假设就不成立了。
2.2 Multiboot寄存器跳转(核心操作)
紧接着上面的思路:只复位不跳转,解决不了“切换到另一个镜像”的需求。要跳转到Flash里另一个镜像的位置,得先把Multiboot地址设置好。下面是我在裸机工程里常用的完整跳转函数:
#include "xil_io.h" #define DEVCFG_BASE 0xF8007000 #define DEVCFG_UNLOCK_OFFSET 0x08U #define DEVCFG_CTRL_OFFSET 0x00U #define DEVCFG_MULTIBOOT_OFFSET 0x10U #define DEVCFG_UNLOCK_MAGIC 0xDF0DU #define DEVCFG_CTRL_MULTIBOOT_EN 0x08U #define PSS_RST_CTRL 0xF8000200 #define SOFT_RST_MASK 0x1U void zynq_multiboot_to(u32 flash_offset) { /* 1. 解锁DevCfg模块 */ Xil_Out32(DEVCFG_BASE + DEVCFG_UNLOCK_OFFSET, DEVCFG_UNLOCK_MAGIC); /* 2. 写入目标镜像偏移(QSPI下右移2位,具体见FSBL换算逻辑) */ Xil_Out32(DEVCFG_BASE + DEVCFG_MULTIBOOT_OFFSET, flash_offset >> 2); /* 3. 使能Multiboot */ Xil_Out32(DEVCFG_BASE + DEVCFG_CTRL_OFFSET, Xil_In32(DEVCFG_BASE + DEVCFG_CTRL_OFFSET) | DEVCFG_CTRL_MULTIBOOT_EN); /* 4. 触发软复位 */ Xil_Out32(PSS_RST_CTRL, SOFT_RST_MASK); while (1) { } }这里解释每一步为什么必须这么写。
第一,DevCfg模块默认是锁定状态。向UNLOCK寄存器(0xF8007008)写入0xDF0D这个固定魔术值之后,才能修改CTRL和MULTIBOOT_ADDR寄存器。如果不解锁,后面的写入会被直接忽略,这是很多刚开始做Multiboot的人最容易漏掉的一步。
第二,MULTIBOOT_ADDR寄存器在QSPI启动下存的是“Flash偏移右移2位”的值,也就是字节地址除以4。为什么FSBL要这么设计?因为BootROM/FSBL读到这个寄存器之后还要做一次换算,在介质控制层面它关心的是块地址而不是字节地址。如果你直接写字节偏移,FSBL读出来的地址会相差4倍,跳转必然失败。这个换算和FSBL的启动介质强相关,SD、NAND、NOR都有可能不一样,务必去DSDK里查看你自己用的FSBL源码。
第三,CTRL寄存器的bit3是MULTIBOOT_EN,必须置1,告诉硬件“我要用Multiboot地址”。不置这一位,即使地址写对了,系统启动时也不会去那边找镜像。很多工程师写完了地址、也复位了,结果发现还是从0x0启动,查半天都是漏了这一步。
第四,最后才是软复位。复位是整个流程的“扣扳机”,让BootROM重新走启动链。复位之后,BootROM/FSBL会读取Multiboot地址,并尝试从目标位置加载镜像。加载失败的Fallback逻辑在FSBL里实现,后面细讲。
我在实际项目中验证过,QSPI Flash偏移0x300000处放了第二份BOOT.bin,调用zynq_multiboot_to(0x300000)后串口日志显示FSBL从0x300000开始解析镜像,启动正常。跳转后如果发现启动不对,第一件事就是回读0xF8007010,确认写进去的值符合你的预期。
2.3 不走BootROM的内存跳转
还有一种跳转方式在裸机调试阶段很实用:不走BootROM,直接在DDR里跳到一个已经准备好的程序入口。这个方法的优点是快,缺点是风险高,因为它绕过了FSBL的初始化流程。DDR时序、中断向量表、Cache状态、MMU配置这些都得自己负责。
简单的实现长这样:
typedef void (*app_entry_t)(void); void jump_to_entry(u32 entry) { /* 关闭中断、刷Cache、停MMU,保持现场干净 */ __disable_irq(); dsb(); isb(); app_entry_t app = (app_entry_t)entry; app(); while (1) { } }看着简单,实际坑很多。跳转前要确认新镜像的中断向量表已经重定位到新地址,否则中断一来CPU还是从老向量表取地址,立刻跑飞。还要确认新镜像的栈已经初始化好,如果新程序用的是自己定义的栈,跳转前得把SP切过去。MMU和D-Cache更是重灾区,两个镜像如果共享同一片DDR,Cache没刷干净,跳过去读到的可能是老数据。
所以我的建议是:生产环境能走Multiboot就走Multiboot,不要图省事直接内存跳转。内存跳转只适合在调试阶段、确认目标程序只是改了几个逻辑、不会碰DDR配置时临时用。
2.4 几种方式横向对比
| 实现方式 | 实现难度 | 是否经过BootROM | 是否依赖Flash | 适用场景 |
|---|---|---|---|---|
| PSS_RST_CTRL软复位 | 极低 | 是 | 否 | 重启当前镜像、升级后落回默认地址 |
| Multiboot寄存器跳转 | 中 | 是 | 是(Flash介质) | 在线升级、A/B镜像切换、安全回退 |
| 纯内存跳转 | 较高 | 否 | 否 | 调试快速切换、DDR内已有程序 |
实际项目里,Multiboot组合软复位是最稳妥的升级路径。它把“初始化硬件”这摊脏活累活重新交给FSBL,应用层只负责把镜像写进去、把地址指过去、然后复位,剩下的交给启动链。
3. 双镜像在线升级的完整实操流程
3.1 Flash分区设计是升级方案的地基
很多人一上来就写代码,结果Flash分区没规划好,后面怎么调都不对。我做双镜像升级时,Flash分区一般按下面的思路划分,以16MB的QSPI Flash为例:
| Flash偏移 | 大小 | 内容 |
|---|---|---|
| 0x000000 | 3MB | Golden镜像(FSBL + bitstream + App) |
| 0x300000 | 8MB | Update镜像(新FSBL + bitstream + App)或Linux全量镜像 |
| 0xB00000 | 5MB | 用户数据区、日志区、升级标志位区 |
这个布局有几个讲究。
第一,分区起始地址尽量按1MB对齐。QSPI Flash常见的扇区大小是64KB,1MB对齐意味着擦除块边界正好落在分区边界上,升级时按扇区擦写不会跨块,代码逻辑好写,也不会出现擦了一个块把另一个分区的头擦掉的情况。
第二,两个镜像的头部都不放在同一个4KB块里。BootROM读镜像头是按固定偏移读的,如果Golden镜像和Update镜像的头部靠得太近,擦写其中一个分区时可能殃及另一个。
第三,预留一个独立的“升级标志位区”,用来记录当前系统处于哪个状态。比如New Image写得了一半断电了,下次启动时判断标志位发现“升级未完成”,就直接清掉Multiboot地址回Golden。这个设计在工程上价值极大,可以有效规避升级中断电变砖的问题。
3.2 镜像制作:同一个BOOT.bin,烧到不同位置
裸机场景下,用Vitis生成FSBL和应用工程后,通过bootgen制作镜像:
# boot.bif the_ROM_image: { [bootloader] zynq_fsbl.elf system.bit app.elf }bootgen -image boot.bif -o BOOT.bin -w on生成的BOOT.bin包含了FSBL、bitstream和App。烧写到Flash时,Golden镜像烧到0x000000,Update镜像烧到0x300000。这里有个常见的误解:有人以为第二份镜像要用bootgen生成一份“偏移过的”特殊文件。实际上不需要,BootROM和FSBL在解析Image Header时,计算的是Header相对自身位置的偏移,同一个BOOT.bin放到Flash里任意对齐位置都能被识别。你只需要保证烧写时确实从0x300000这个偏移开始写,并且这个偏移和Multiboot寄存器换算后的地址一致。
Petalinux场景下制作启动镜像一般用:
petalinux-package --boot --fsbl --fpga --u-boot --force生成BOOT.BIN,而Linux的根文件系统和内核打包在image.ub中,由U-Boot通过boot.scr加载。升级时候选择把BOOT.BIN写到QSPI还是image.ub写到SD卡,取决于你的系统设计。但无论如何,多镜像的启动判断逻辑是一样的,都是通过Multiboot地址实现。
3.3 裸机侧升级主流程:写Flash、校验、跳转
裸机升级的核心流程分成四步:擦除目标分区、写入新镜像、回读校验、Multiboot跳转。写Flash的驱动通常用Xilinx的XQspiPs,示例流程如下:
static int upgrade_fw(const u8 *fw_buf, u32 fw_len, u32 dest_offset) { /* 1. 使能QSPI写,关闭写保护 */ qspi_write_enable(); /* 2. 按扇区擦除目标区域,擦除长度对齐到64KB */ u32 erase_len = (fw_len + 0xFFFF) & ~0xFFFF; qspi_erase(dest_offset, erase_len); /* 3. 写入固件数据 */ qspi_write(fw_buf, dest_offset, fw_len); /* 4. 回读校验,确保数据真正写进去了 */ if (qspi_verify(fw_buf, dest_offset, fw_len) != 0) { return -1; } return 0; } void ota_main(const u8 *new_fw, u32 new_fw_len) { const u32 UPDATE_OFFSET = 0x300000; if (upgrade_fw(new_fw, new_fw_len, UPDATE_OFFSET) == 0) { /* 升级写好了,跳转到新镜像 */ zynq_multiboot_to(UPDATE_OFFSET); } else { /* 写失败,不跳转,继续跑当前程序 */ xil_printf("upgrade failed, keep running\r\n"); } }这段逻辑看着简单,但实际工程里至少还要加三样东西:升级包版本号校验、升级包CRC校验、升级标志位落盘。版本号校验防止把旧固件当新固件写上去,CRC校验防止传输过程中数据损坏,升级标志位则用来处理“写到一半断电”的极端情况。我在一个量产项目里就是吃了没做升级标志位的亏,设备在升级过程中断电,虽然Multiboot地址没写,但Update分区写了一半,下次启动时BootROM撞上一个非法的Image Header,整个板子卡死,最后只能拆机用JTAG救回来。从那以后,每次升级前先写“升级中”标志位,启动时检查这个标志位,发现异常直接清Multiboot回Golden,再也没出过变砖问题。
3.4 Linux侧如何触发Multiboot跳转
Linux环境下触发Multiboot跳转有两种常见做法。一种是直接操作物理地址,用devmem写寄存器,然后reboot:
# 解锁DevCfg devmem 0xF8007008 32 0xDF0D # 写目标地址(以0x300000为例,右移2位) devmem 0xF8007010 32 0x000C0000 # 使能multiboot devmem 0xF8007000 32 0x00000008 # 强制重启 reboot -f这种方法适合调试,但不建议直接用在量产设备上。原因有两个:一是devmem绕过了驱动层的保护,写错了地址可能把系统搞挂;二是Linux自身的驱动和U-Boot之间的交互不算实时,直接写物理寄存器时,如果此时有别的驱动在操作DevCfg,存在竞争风险。
更稳的做法是把跳转逻辑做进升级脚本里,升级完成后设置U-Boot环境变量,或者直接通过一个小工具去写寄存器后再调用reboot。量产项目中,我倾向于做一个专门的升级守护进程,它负责下载固件、写Flash、设置Multiboot地址、最后触发重启。这样整个升级过程可控,日志可追溯,出现问题也能通过标志位回退。
3.5 Fallback回退与看门狗配合
Multiboot要真正可靠,必须配套“回退策略”。ZYNQ的FSBL里默认有一层回退逻辑:当它尝试从Multiboot地址加载镜像失败时——比如Image Header魔数不对、镜像长度非法——FSBL会清除Multiboot寄存器,然后回退到Flash偏移0x0去加载Golden镜像。这层机制解决的是“新镜像根本无法加载”的情况。
但有一个场景是这层机制管不到的:新镜像能加载、能跑,只是运行到一半崩了。这种情况下FSBL在加载阶段完全正常,它不会触发回退。看门狗超时后系统会复位,复位后BootROM/FSBL再次从Multiboot地址加载同一个有问题的镜像,进去又崩,又复位……你看到的现象就是“看门狗复位了好几次系统才恢复”,甚至一直恢复不了。
热词里提到的“单片机死机后软件看门狗需要多次复位”其实就是这个问题。解决思路有两种。
第一种是应用层自检回退:新镜像启动后,在规定时间内向“启动成功标志区”写入一个有效值;看门狗如果长时间没吃到喂狗,说明新镜像没正常工作。下电重启时,如果发现启动成功标志无效,就强制清Multiboot地址,回退到Golden镜像。
第二种是“N次复位强制回退”:用持久化区域记录复位次数,每次启动时递增。如果连续N次启动都没有完成任务(比如网络服务没起来、业务逻辑没跑通),就判定新镜像不健康,主动清Multiboot寄存器回退。
我在项目里用的是第二种思路的变体:每次FSBL阶段就检查启动计数器,如果连续3次都没能进入主业务循环,直接清Multiboot并复位。这个方法实现简单,不依赖外部看门狗,而且在QSPI Flash上写一个标志位很快,对启动时间影响很小。
4. 常见问题与排查技巧实录
4.1 跳转后串口没输出,板子像死了一样
排查这种问题,先别急着怀疑代码。把Flash读回来,看目标偏移处的64字节,确认Image Header的魔数是不是0xAA995566。很多时候是烧写工具没按你指定的偏移写,或者QSPI地址线和板卡布线有问题,导致读到的全是0xFF。如果Flash数据正确,再回读MULTIBOOT_ADDR寄存器确认跳转前设置的值和预期一致。最后确认CTRL寄存器的bit3有没有被置1,这一步被漏掉的概率极高。
还有一个隐蔽的坑:如果你的目标镜像里包含PL bitstream,要确认bitstream本身没有损坏。FSBL加载bitstream失败时会进入错误处理,看起来就是卡住不动。把新镜像单独放到0x0位置启动一次,能快速定位是不是镜像本身的问题。
4.2 看门狗反复复位,系统过很久才恢复正常
这个现象我在实际项目里遇到好几次,最后定位都是Multiboot指向了一个“能加载但运行不稳”的新镜像。FSBL加载期检查全部通过,但新镜像跑到业务逻辑阶段就崩,看门狗拉起来后又走同样的路径,形成一个死循环。
解决办法就是前面说的启动计数器加回退策略。在QSPI Flash里维护一个“启动计数”区域,每次启动加1,如果连续3次没有进入主业务循环,就把Multiboot清掉,强制从Golden启动。同时要把“启动成功”标志的写入放在业务逻辑真正跑起来之后,而不是放在App入口,否则判断没意义。
4.3 Linux下用devmem写了寄存器,重启后没反应
先确认你有没有写对地址。0xF8007010是MULTIBOOT_ADDR,0xF8007000的bit3是MULTIBOOT_EN。很多人只写了地址,没写使能位。另外,Linux reboot默认走的是优雅关机流程,可能不等设备完全复位就执行了其他逻辑,导致Multiboot寄存器被后面的代码清掉。调试时建议用reboot -f强制重启。
如果还是不行,大概率是U-Boot阶段覆盖了Multiboot设置。U-Boot启动时自己也会处理DevCfg寄存器,如果你的U-Boot配置里对Multiboot地址做了额外处理,那么Linux里写进去的值可能在重启后被U-Boot改掉。这种情况下改在U-Boot环境变量层面做跳转判断,或者干脆把跳转逻辑固化进U-Boot脚本,不要在Linux用户态硬写寄存器。
4.4 Flash Programmer提示“a valid fsbl file is required for flash operation”
这个提示经常出现在Vivado/Vitis的Flash Programmer工具里,意思是当前工具的烧写流程需要一个和启动介质匹配的FSBL文件。很多人用的是默认模板FSBL,但模板FSBL可能没有针对目标NAND或者目标QSPI型号做初始化,导致工具无法正确操作Flash。
解决办法是在Vitis的工程里重新生成一个和当前启动介质匹配的FSBL,编译成elf,然后在Flash Programmer里指定这个文件。如果用的是NAND,还要特别注意NAND型号的兼容性,Vitis默认支持的NAND型号有限,某些型号需要手动补驱动。这个领域容易踩坑,建议在选型阶段就查清楚你用的NAND型号是否在Xilinx官方支持列表里,避免后期在工具链上浪费时间。
4.5 QSPI镜像烧写正常,但Multiboot地址换算总对不上
很多工程师自己写BootROM级跳转逻辑时,会在QSPI字节偏移和MULTIBOOT_ADDR寄存器值之间来回换算,错一次就得查半天。我的经验是不要自己发明公式,直接看FSBL源码里怎么读怎么写。
打开xfsbl_multi_boot.c,搜索“MultiBootRegVal”,看它拿到寄存器值之后做了什么运算,再把你的Flash偏移按照同样的运算反推回去。只要你用的FSBL版本和生成工具是配套的,这个换算就一定是对的。不同版本的FSBL对在不同介质上的换算方式偶尔会有调整,所以不要拿网上三年前的代码直接抄。
5. 我个人的一点实操体会
Multiboot机制本身不复杂,复杂的是工程落地。做过的几个升级项目里,我最大的体会是:Multiboot只解决“跳到哪”的问题,真正决定升级成不成的,是镜像健康检查和回退策略。千万别把Multiboot当成“写完Flash就万事大吉”的跳板,升级路径上每一个环节都要有容错。
最后分享一个小技巧:每次升级前给新镜像埋一个版本号,启动后通过串口或者网络主动上报,这样确认系统到底跑没跑起来会方便很多。我习惯把版本号放在镜像头部预留的4个字节里,FSBL启动时读出来打印,应用层再读一次做校验。版本号都读不对,后面再怎么查也是白费力气。第一次做Multiboot升级,建议先在开发板上用两套不同打印的镜像反复试,确认跳转和回退都符合预期,再上真实设备,能省掉很多现场排查的时间。