ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

ZYNQ软件复位与Multiboot跳转:在线升级与回退机制详解

ZYNQ软件复位与Multiboot跳转:在线升级与回退机制详解 做过ZYNQZynq-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更是重灾区两个镜像如果共享同一片DDRCache没刷干净跳过去读到的可能是老数据。所以我的建议是生产环境能走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偏移大小内容0x0000003MBGolden镜像FSBL bitstream App0x3000008MBUpdate镜像新FSBL bitstream App或Linux全量镜像0xB000005MB用户数据区、日志区、升级标志位区这个布局有几个讲究。第一分区起始地址尽量按1MB对齐。QSPI Flash常见的扇区大小是64KB1MB对齐意味着擦除块边界正好落在分区边界上升级时按扇区擦写不会跨块代码逻辑好写也不会出现擦了一个块把另一个分区的头擦掉的情况。第二两个镜像的头部都不放在同一个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镜像烧到0x000000Update镜像烧到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_ADDR0xF8007000的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升级建议先在开发板上用两套不同打印的镜像反复试确认跳转和回退都符合预期再上真实设备能省掉很多现场排查的时间。
返回列表