1. 项目概述:为什么FSBL的汇编代码是ZYNQ启动链上最不能跳过的“第一课”
Xilinx ZYNQ 7000系列不是一块普通FPGA,它是一颗把双核ARM Cortex-A9处理器(PS端)和可编程逻辑(PL端)深度集成在同一芯片上的SoC。这种架构决定了它的启动过程远比纯MCU或纯FPGA复杂——它不是“上电即跑main”,而是一场精密编排的多阶段接力赛。而FSBL(First Stage Boot Loader),就是这场接力中真正踩下起跑器的那个人。很多人在PetaLinux里敲下petalinux-package --boot --fsbl ./images/linux/zynq_fsbl.elf --fpga --u-boot --force时,只把它当成一个必须填进去的路径参数;但如果你没看过、没读懂、没亲手改过FSBL的汇编代码,你就永远只是在调用一个黑盒,而不是在驾驭ZYNQ。
我第一次在Vivado里生成FSBL工程,打开ps7_init_gpl.c和startup.S时,满屏的mrc,mcr,ldr,bne指令看得头皮发麻。但后来在调试一块死在“PS初始化完成,PL未配置”阶段的板子时,才真正体会到:FSBL汇编层就是ZYNQ启动的“神经末梢”。它不处理文件系统,不加载内核镜像,但它要亲手配置每一个关键寄存器——从PLL倍频系数、DDR控制器时序参数、到AXI总线地址映射、甚至PS-PL之间的中断路由。这些操作一旦出错,后续所有环节都会卡死,且错误信息往往只有JTAG上一闪而过的“BOOT_STATUS = 0x0A”(表示FSBL执行失败),没有任何日志可查。所以,“Xilinx ZYNQ 7000学习笔记二(FSBL代码分析启动-汇编代码)”这个标题,本质上是在说:别急着跑Linux,先学会看懂CPU刚上电时,那几百行汇编是如何用最原始的方式,把一颗冰冷的芯片唤醒成一个可编程平台的。它适合三类人:正在啃ZYNQ裸机开发的新手、被启动问题反复折磨的硬件工程师、以及想真正理解PetaLinux底层机制的嵌入式系统工程师。你不需要会写汇编,但你必须能读懂它在做什么、为什么这么做、以及改错时该盯住哪几行。
2. FSBL启动流程与汇编代码设计思路拆解
2.1 ZYNQ启动的四阶段接力:FSBL为何是不可替代的“第一棒”
ZYNQ 7000的启动流程是一个严格定义的四阶段(Stage)序列,每一阶段都由前一阶段加载并移交控制权。这个设计的核心目标是安全、可靠、可定制。FSBL之所以被称作“First Stage”,是因为它直接运行在BootROM之后、由BootROM加载的唯一用户可控代码。理解这个上下文,才能明白FSBL汇编代码的设计逻辑。
BootROM是固化在芯片内部的只读存储器,它在上电复位后立即执行。它的任务非常明确且有限:检测启动模式(QSPI、SD、JTAG等)、从指定介质读取前384KB数据(即FSBL的二进制镜像)、校验其CRC、然后将控制权跳转到FSBL的入口地址(通常是0x00000000)。BootROM不解析任何文件系统,不识别ELF格式,它只认原始的二进制流。这就决定了FSBL的第一个硬性要求:它必须是一个位置无关、自包含、无需外部依赖的纯二进制镜像。这也是为什么FSBL的链接脚本(lscript.ld)会强制将其所有段(.text,.data,.bss)都链接到片上RAM(OCM)的固定地址范围(0xFFFF0000 - 0xFFFFFFFF),因为OCM是BootROM加载完成后,CPU能立即访问的唯一高速内存。
第二阶段是FSBL自身。它的核心使命是“为下一阶段铺路”。这个“下一阶段”通常是U-Boot(Second Stage Boot Loader),但也可能是裸机应用或Xilinx提供的PMU Firmware。FSBL不负责业务逻辑,它只做三件大事:初始化PS硬件、配置PL、加载后续镜像。其中,PS初始化是它最重的任务,包括设置CPU频率、配置DDR控制器、初始化UART/GPIO等外设。而这一切,都必须在没有C运行时环境(如libc、堆栈管理)的情况下完成。这就是汇编代码存在的根本原因——在C代码能安全运行之前,必须有一段“裸金属”代码来搭建C运行所需的最小环境。FSBL的汇编部分(startup.S)正是干这个活的:它建立初始堆栈、关闭中断、配置向量表、跳转到C语言主函数main()。整个过程就像给一个刚出生的婴儿喂第一口奶——这口奶不能是复杂的辅食,必须是纯净、易吸收、能量密度最高的初乳。
第三阶段是U-Boot。它由FSBL从SD卡或QSPI Flash中加载到DDR内存,并跳转执行。U-Boot拥有完整的命令行、文件系统支持(FAT32/EXT4)、网络协议栈,可以加载Linux内核、设备树、根文件系统。它和FSBL的关系,类似于PC BIOS和GRUB的关系:BIOS负责最基本的硬件探测和初始化,GRUB则负责从硬盘读取操作系统内核并启动。
第四阶段是Linux内核。它由U-Boot加载并启动,最终接管整个系统。
提示:FSBL的“第一阶段”地位,决定了它无法使用任何高级抽象。你不会在FSBL里看到
malloc()或printf(),因为这些函数依赖于libc和已初始化的内存管理器,而这些恰恰是FSBL要帮它们建立的。所有操作都必须回归到最底层的寄存器读写。
2.2 汇编代码的结构化分层:从startup.S到ps7_init_gpl.c的协同逻辑
FSBL的源码并非全部是汇编,而是一个典型的“汇编+固件库+C”的混合体。这种分层设计,是Xilinx在“可读性”和“可靠性”之间做出的精妙平衡。startup.S是绝对的起点,它短小精悍,通常只有100行左右,却承担着最基础的硬件初始化任务。而ps7_init_gpl.c则是FSBL的“心脏”,它包含了所有PS端外设的详细配置,其代码量往往超过2000行。理解这两者的分工,是读懂FSBL的关键。
startup.S的核心任务有三个:建立运行环境、跳转到C世界、处理异常。它首先将SP(堆栈指针)设置为OCM的最高地址(例如0xFFFFFFFC),这是ARM Cortex-A9的惯例,确保堆栈向下增长时不会越界。接着,它会禁用IRQ和FIQ中断(cpsid i),因为在初始化过程中,任何意外的中断都可能导致灾难性后果。然后,它会配置向量表基址寄存器(VBAR),将异常向量表指向startup.S中预定义的vector_table区域。最后,它调用bl main,将控制权移交给C语言的main()函数。这段汇编代码的每一行,都是对ARM架构手册的直接翻译,没有一行是多余的。
ps7_init_gpl.c则完全不同。它是一个由Vivado工具自动生成的C文件,其内容完全取决于你在Block Design中对ZYNQ Processing System IP核所做的配置。当你在Vivado中设置了CPU频率为667MHz、DDR类型为DDR3L、UART0的波特率为115200时,Vivado就会在ps7_init_gpl.c中生成对应的寄存器写入序列。例如,设置CPU频率需要修改ARM_PLL_CTRL寄存器(地址0xF8000100),而配置DDR时序则需要向DDR_PHY_CTRL_0(0xF8006000)等一系列寄存器写入几十个参数。这些操作在C语言中是通过宏定义的寄存器地址和Xil_Out32()函数完成的。Xil_Out32()本身是一个内联汇编函数,它最终会编译成一条str指令,将数据写入指定地址。因此,ps7_init_gpl.c的本质,就是一套高度定制化的、针对你特定硬件设计的“寄存器配置脚本”。
这两部分的协同关系是:startup.S负责“搭台”,ps7_init_gpl.c负责“唱戏”。startup.S确保CPU有一个干净的、可控的执行环境;ps7_init_gpl.c则在这个环境下,精确地操控每一个硬件模块。这种分离让开发者既能掌控最底层的启动细节(通过修改startup.S),又能利用工具链的便利性(通过重新生成ps7_init_gpl.c)来适配不同的硬件变更。我曾经在一个项目中,因为客户临时更换了DDR颗粒型号,导致原FSBL无法初始化内存。我并没有重写整个FSBL,而是直接在Vivado中更新了PS IP核的DDR参数,重新生成了ps7_init_gpl.c,再配合startup.S中微调的延时参数,问题就迎刃而解。这充分体现了这种分层设计的工程价值。
2.3 为什么必须是汇编?C语言在这里为何“力不从心”
这个问题直击本质。在ZYNQ启动的最初毫秒,C语言的“舒适区”并不存在。我们可以从三个层面来剖析C语言在此刻的“失能”。
首先是运行时环境缺失。C语言程序默认期望一个完整的运行时(Runtime)环境:全局变量需要被初始化(.data段从Flash拷贝到RAM),未初始化的变量需要被清零(.bss段置零),函数调用需要堆栈空间,甚至简单的for循环都需要一个可靠的循环计数器。但在startup.S执行之前,这一切都是空白。OCM RAM是空的,堆栈指针是未定义的,中断控制器是关闭的。如果此时强行执行C代码,main()函数里的第一个int i = 0;就可能因为.data段未拷贝而将一个随机值赋给i,导致后续所有计算都错乱。startup.S的首要任务,就是手动完成这些“奠基工作”。它会显式地将.data段从Flash的加载地址复制到RAM的运行地址,并将.bss段的起始和结束地址之间的所有内存清零。这个过程在C语言里是透明的,但在汇编里,它就是几条ldr、str和cmp指令的组合。
其次是性能与确定性的严苛要求。FSBL的启动时间是系统级指标。从上电到U-Boot开始打印“U-Boot”字样,整个过程通常要求在几百毫秒内完成。而C语言的抽象层(如函数调用开销、参数压栈、返回地址保存)会引入不可预测的延迟。在汇编中,你可以精确控制每一条指令的执行周期。例如,在等待一个硬件模块(如DDR PHY)完成初始化时,你需要一个精确的“忙等”循环。在C中,while(1) { if (ready) break; }编译器可能会优化掉这个循环,或者插入不必要的指令。而在汇编中,你直接写1: ldr r0, [r1]tst r0, #1beq 1b,这是一个绝对精准、零开销的轮询。这种对时序的绝对掌控,是C语言无法提供的。
最后是调试与故障定位的终极需求。当系统卡在启动早期时,你没有任何调试器可以连接,没有串口输出,甚至JTAG都可能无法响应。唯一的线索,就是BootROM报告的BOOT_STATUS寄存器值。在这种“黑暗调试”场景下,汇编代码是你唯一的光源。因为它足够简单,足够直接。如果startup.S中的某一行mcr p15, 0, r0, c1, c0, 0(这条指令用于使能MMU)执行后系统挂死,那么问题一定出在r0寄存器的值上,或者你试图启用了一个尚未配置好的内存区域。这个推理链条是线性的、确定的。而如果这段代码是用C写的,你可能要花数小时去排查是编译器Bug、链接脚本错误,还是某个隐藏的库函数调用导致了栈溢出。
注意:不要试图用C语言重写
startup.S。这不是一个“谁更高级”的问题,而是一个“谁更贴近硬件”的问题。就像你不会用Python去写一个操作系统的中断处理程序一样,FSBL的汇编层是ZYNQ启动链上不可逾越的物理定律。
3. 核心汇编代码逐行解析与实操要点
3.1startup.S:从复位向量到main()的完整旅程
startup.S是FSBL的“门面”,也是我们深入ZYNQ启动机制的第一扇窗。下面,我将以Xilinx官方SDK 2020.2版本生成的startup.S为蓝本,进行逐行解析。请注意,不同SDK版本的代码细节会有差异,但核心逻辑万变不离其宗。
.section .vectors, "ax" .global _vectors _vectors: b reset /* Reset */ b undef /* Undefined instruction */ b swi /* Software interrupt */ b prefetch_abort /* Prefetch abort */ b data_abort /* Data abort */ b not_used /* Not used */ b irq /* IRQ */ b fiq /* FIQ */这段代码定义了ARM的异常向量表。.section .vectors, "ax"声明了一个名为.vectors的段,"ax"表示该段是可分配(a)和可执行(x)的。_vectors是该段的入口符号。接下来的8个b(branch)指令,分别对应ARM的8种异常类型。b reset意味着当发生复位(Reset)异常时,CPU会跳转到reset标签处执行。这是整个启动流程的绝对起点。向量表必须位于内存的最低地址(0x00000000),因为ARM CPU在复位后,会自动从这个地址开始取指。这也是为什么FSBL的链接脚本必须将.vectors段链接到0x00000000。
.section .text, "ax" .global _start _start: b reset.text段是代码段。_start是链接器的默认入口点。这里只有一条指令b reset,它将程序的执行流导向reset标签。这是一种标准做法,确保无论链接器如何安排段顺序,程序总能从正确的入口开始。
reset: /* Set stack pointer for each mode */ msr cpsr_c, #0xd3 /* Supervisor Mode, IRQ & FIQ disabled */ ldr sp, =0xFFFFFFFC /* OCM RAM top address */ /* Disable MMU, caches, branch prediction */ mrc p15, 0, r0, c1, c0, 0 bic r0, r0, #0x00000007 mcr p15, 0, r0, c1, c0, 0 /* Invalidate instruction cache */ mcr p15, 0, r0, c7, c5, 0 dsb isb这是reset标签下的核心初始化代码。第一行msr cpsr_c, #0xd3将CPU置于Supervisor模式(管理模式),并同时禁用IRQ和FIQ中断。#0xd3是ARM状态寄存器(CPSR)的一个位掩码,其中0xd0表示Supervisor模式,0x03表示禁用IRQ和FIQ。这是为了确保初始化过程不受任何中断干扰。第二行ldr sp, =0xFFFFFFFC将堆栈指针(SP)设置为OCM RAM的最高地址。OCM的大小是256KB,地址范围是0xFFFF0000 - 0xFFFFFFFF,所以0xFFFFFFFC是其顶部,为堆栈留出了足够的空间。
接下来的三行是禁用MMU(内存管理单元)、数据缓存(D-Cache)和指令缓存(I-Cache)。mrc p15, 0, r0, c1, c0, 0从协处理器p15(系统控制协处理器)的寄存器c1(控制寄存器)中读取当前值到r0。bic r0, r0, #0x00000007对r0执行位清除操作,清除低三位(bit[0]=M, bit[1]=A, bit[2]=C),分别对应MMU、对齐检查和D-Cache。mcr p15, 0, r0, c1, c0, 0再将修改后的值写回c1寄存器,从而生效。最后三行mcr p15, 0, r0, c7, c5, 0等是清空I-Cache的指令,dsb(Data Synchronization Barrier)和isb(Instruction Synchronization Barrier)是内存屏障指令,确保前面的写操作完成后再执行后续指令,这是ARM架构中保证指令执行顺序的关键。
/* Copy .data section from flash to ram */ ldr r0, =__data_start__ ldr r1, =__data_end__ ldr r2, =__data_load_start__ copy_data_loop: cmp r0, r1 beq copy_data_done ldr r3, [r2], #4 str r3, [r0], #4 b copy_data_loop copy_data_done: /* Zero out .bss section */ ldr r0, =__bss_start__ ldr r1, =__bss_end__ mov r2, #0 zero_bss_loop: cmp r0, r1 beq zero_bss_done str r2, [r0], #4 b zero_bss_loop zero_bss_done:这是startup.S中最关键的两段代码:.data段拷贝和.bss段清零。__data_start__、__data_end__、__data_load_start__等符号是由链接脚本(lscript.ld)定义的。.data段在Flash中是只读的,但其中的变量在运行时需要被修改,所以必须在启动时将其拷贝一份到RAM中。copy_data_loop就是一个标准的内存拷贝循环:r0是RAM中的目标地址,r1是目标结束地址,r2是Flash中的源地址。ldr r3, [r2], #4从r2地址读取一个32位字,并将r2加4;str r3, [r0], #4将r3写入r0地址,并将r0加4。.bss段则更简单,它只包含未初始化的全局变量,其内容在运行时必须为零。zero_bss_loop循环将r0到r1之间的所有内存地址都写入0。
/* Call C main function */ bl main /* Should never reach here */ hang: b hang最后,bl main调用C语言的main()函数。bl(Branch with Link)指令会将返回地址(即下一条指令hang的地址)存入lr(Link Register)寄存器,然后跳转到main。如果main()函数正常返回,CPU会执行mov pc, lr(由main的ret指令隐含),回到hang标签。hang是一个无限循环,它确保如果main()意外退出,CPU不会继续执行后面未知的内存,而是原地“挂起”,这是一种安全的失效模式。
实操心得:在调试FSBL时,我习惯在
bl main之前添加一条ldr r0, =0x12345678str r0, [r0]这样的“打点”指令。然后用JTAG调试器在bl main处设置断点。如果断点能命中,说明startup.S执行无误;如果断点无法命中,问题就出在前面的向量表、堆栈或寄存器配置上。这个技巧能帮你快速将问题域缩小到汇编层。
3.2ps7_init_gpl.c:寄存器配置的“黄金法则”与避坑指南
ps7_init_gpl.c是FSBL的“大脑”,它包含了所有PS端硬件的初始化序列。这份文件由Vivado自动生成,但它的内容绝非黑盒。理解其生成逻辑和关键配置点,是解决90%启动问题的钥匙。
该文件的主体是一个巨大的Ps7_Init()函数,它内部又调用了数十个以Ps7_*开头的子函数,如Ps7_MmuInit(),Ps7_DdrInit(),Ps7_UartInit()等。每个子函数都对应一个硬件模块的初始化。以Ps7_DdrInit()为例,它的核心就是一系列对DDR控制器寄存器的写入操作:
// 设置DDR PHY Control Register 0 Xil_Out32(0xF8006000, 0x00000001); // 设置DDR PHY Control Register 1 Xil_Out32(0xF8006004, 0x00000002); // ... 省略数十行类似的寄存器写入这些数值(0x00000001,0x00000002)并非随意填写,而是根据你选择的DDR颗粒型号、PCB走线长度、信号完整性仿真结果,由Xilinx的IP核配置向导(IP Catalog)计算得出的。这就是为什么在Vivado中修改PS IP核的DDR参数后,必须重新生成ps7_init_gpl.c——因为这些参数已经固化在了生成的C代码里。
然而,自动生成的代码并非完美。我在多个项目中遇到过因ps7_init_gpl.c导致的启动失败,最常见的两个坑是:
坑一:时序参数与实际硬件不匹配。Xilinx的IP核配置向导会基于一个“典型”模型进行计算,但你的PCB可能有更长的走线、更低质量的板材,或者你使用的DDR颗粒批次与参考设计不同。这会导致生成的时序参数过于激进,DDR初始化失败。解决方案是:在Ps7_DdrInit()函数中,找到Xil_Out32(0xF8006000, ...)这一系列写入PHY寄存器的代码。这些寄存器(0xF8006000 - 0xF800607C)控制着PHY的训练算法。你可以尝试将其中几个关键寄存器的值(如PHY_RDLVL_START_DELAY)人为增大1-2个单位,然后重新编译FSBL。这是一个“试错法”,但非常有效。
坑二:初始化顺序错误。ps7_init_gpl.c的执行顺序是固定的,但某些硬件设计可能要求特定的初始化顺序。例如,如果你的板子上,PS端的GPIO引脚被用来控制PL端的电源管理IC,那么在初始化GPIO之前,就必须先确保PL的电源已经稳定。而ps7_init_gpl.c默认的初始化顺序是先DDR、再UART、再GPIO。这时,你需要手动修改Ps7_Init()函数的调用顺序,将Ps7_GpioInit()提前到Ps7_DdrInit()之前,并在Ps7_GpioInit()中加入一个usleep(1000)的延时,确保电源稳定后再进行DDR初始化。
提示:
ps7_init_gpl.c中所有的Xil_Out32()调用,最终都会编译成一条str指令。你可以用反汇编工具(如arm-linux-gnueabihf-objdump -d zynq_fsbl.elf)查看其对应的汇编代码,确认它是否真的写入了你期望的地址。这比在C代码里加printk要可靠得多,因为printk本身就需要UART初始化成功。
3.3 启动模式与FSBL镜像的加载路径:zynq_fsbl.elf从哪里来?
petalinux-package --boot --fsbl ./images/linux/zynq_fsbl.elf --fpga --u-boot --force这条命令中的zynq_fsbl.elf,是整个启动链的基石。它的来源和生成过程,是很多新手的困惑点。它不是凭空出现的,而是经过一个严谨的“设计-综合-实现-封装”流程产生的。
第一步是硬件设计。你在Vivado中创建一个Block Design,添加ZYNQ Processing System IP核,并对其进行详细配置(CPU频率、DDR参数、外设使能等)。这个设计定义了FSBL需要初始化的所有硬件资源。
第二步是生成HDF。当你对Block Design满意后,点击“Generate Output Products”并勾选“Include bitstream”,Vivado会综合并实现这个设计,最终生成一个硬件描述文件(HDF)。这个HDF文件(通常命名为system.hdf)包含了所有IP核的配置信息、地址映射、中断号等元数据。
第三步是创建FSBL工程。在Vitis(Xilinx的软件开发套件)中,你选择“Create a new application project”,然后在模板中选择“Zynq FSBL”。Vitis会自动为你创建一个FSBL工程,并将你之前生成的HDF文件作为输入。Vitis会解析HDF,提取出PS IP核的配置,并据此生成ps7_init_gpl.c。
第四步是编译与链接。Vitis使用ARM GCC交叉编译器(arm-xilinx-eabi-gcc)编译startup.S和ps7_init_gpl.c等源文件,并使用一个专门的链接脚本(lscript.ld)将它们链接成一个ELF格式的可执行文件zynq_fsbl.elf。这个链接脚本强制将.vectors、.text、.data、.bss等段都放置在OCM RAM的指定地址范围内。
第五步是镜像打包。petalinux-package命令的作用,就是将zynq_fsbl.elf、FPGA比特流(.bit)、U-Boot镜像(u-boot.elf)等文件,按照ZYNQ BootROM所要求的格式(BOOT.BIN),打包成一个单一的二进制文件。这个过程会将zynq_fsbl.elf的.text和.data段提取出来,拼接成一个纯二进制流,并在头部加上一个特定的头信息(Header),告诉BootROM这个镜像是FSBL。
所以,zynq_fsbl.elf的“血统”非常清晰:它源于你的Vivado硬件设计(HDF),由Vitis工具链编译生成,最终被petalinux-package工具打包进BOOT.BIN。如果你在烧写后发现系统无法启动,首先要检查的就是这个zynq_fsbl.elf是否与你当前的硬件设计完全匹配。一个常见的错误是:在Vivado中修改了PS IP核的配置,但忘记重新生成HDF并更新Vitis中的FSBL工程,导致FSBL仍在用旧的寄存器参数初始化硬件,自然会失败。
4. FSBL启动全过程实操与关键环节实现
4.1 从零开始:手把手构建一个可调试的FSBL工程
理论终须落地。下面,我将带你从一个空白的Vivado工程开始,一步步构建一个可以真实运行、可以单步调试的FSBL工程。这个过程,比任何文档都更能加深你对启动流程的理解。
第一步:创建Vivado Block Design
- 打开Vivado 2020.2,创建一个新工程,选择你的目标开发板(如ZedBoard)。
- 在“IP Integrator”中,创建一个新的Block Design。
- 从IP Catalog中添加“ZYNQ7 Processing System” IP核。
- 双击该IP核,进入配置向导。在“Page 1”中,保持默认的“Zynq 7000”和“ZedBoard”。
- 在“Page 2”中,展开“PS-PL Configuration”,将“FCLK_CLK0”设置为100MHz(这是PL端常用的时钟)。
- 在“Page 3”中,展开“Peripheral I/O Pins”,勾选“UART 0”并将其配置为“EMIO”(这样UART信号会通过PL引出,方便调试)。
- 在“Page 4”中,展开“DDR Configuration”,选择你板子上实际使用的DDR型号(如“MT41K256M16 RE-125”),并确保“Memory Part”与之完全一致。
- 点击“OK”完成配置,然后点击“Run Block Automation”,让Vivado自动连接所有必需的接口。
- 最后,点击“Validate Design”,确保设计无误,然后右键Block Design,选择“Generate Output Products”,勾选“Synthesize design”和“Include bitstream”,点击“Generate”。
第二步:导出HDF并创建Vitis工程
- 在Vivado中,点击“File” -> “Export” -> “Export Hardware...”。
- 勾选“Include bitstream”,将HDF文件导出到一个你容易找到的路径,例如
./hw_export/system.hdf。 - 关闭Vivado,打开Vitis 2020.2。
- 创建一个新工作空间(Workspace),然后选择“File” -> “New” -> “Application Project”。
- 在向导中,Project name填
fsbl_debug,Hardware platform选择你刚刚导出的system.hdf。 - 在“Templates”页面,滚动到底部,选择“Zynq FSBL”,点击“Finish”。Vitis会自动创建一个FSBL工程,并将
system.hdf作为输入。
第三步:编译FSBL并准备调试
- 在Vitis的“Project Explorer”中,右键
fsbl_debug项目,选择“Build Project”。编译完成后,你会在./Debug/目录下看到zynq_fsbl.elf。 - 连接你的JTAG调试器(如Digilent HS2)到开发板。
- 在Vitis中,点击“Run” -> “Debug Configurations...”。
- 在左侧选择“Xilinx C/C++ Application (System Debugger)”,然后点击右上角的“New launch configuration”图标(一个加号)。
- 在“Main”选项卡中,“C/C++ Application”浏览到
./Debug/zynq_fsbl.elf。 - 在“Hardware Target”选项卡中,选择你的JTAG设备和目标处理器(
ps7_cortexa9_0)。 - 点击“Debug”按钮。Vitis会自动下载
zynq_fsbl.elf到OCM RAM,并在_start处暂停。
现在,你拥有了一个完全可控的FSBL调试环境。你可以按F5单步执行,按F6步入startup.S,按F7步入ps7_init_gpl.c,观察每一条指令对寄存器的影响。这是理解ZYNQ启动最高效的方式。
4.2 关键寄存器监控:用JTAG实时观测启动过程
仅仅单步执行还不够,你必须能看到“发生了什么”。JTAG调试器的强大之处,在于它能让你实时读取和修改CPU的任何寄存器。在FSBL调试中,有四个寄存器是你必须时刻关注的“生命体征”。
第一个是BOOT_STATUS寄存器(地址0xF8000008)。这是BootROM留下的“遗言”,它记录了FSBL执行失败的具体原因。在Vitis的“Xilinx Tools” -> “Hardware Window”中,点击“Add Watchpoint”,输入地址0xF8000008,你就能实时看到它的值。0x0A表示FSBL执行失败,0x0C表示FSBL加载U-Boot失败。这个寄存器是你的第一道诊断依据。
第二个是PS_SRST_CTRL寄存器(地址0xF8000200)。它控制着PS端的软复位。在调试过程中,如果你发现FSBL卡死了,可以手动向这个寄存器的bit[0]写入1,然后写入0,触发一次PS端的软复位,而无需断电重启。这能极大提升调试效率。
第三个是SCU_SAC寄存器(地址0xF8F00000)。它是Snoop Control Unit的共享属性寄存器,控制着多核CPU对内存的访问权限。在双核A9系统中,如果SCU_SAC配置错误,会导致一个核无法看到另一个核写入内存的数据,引发难以追踪的竞态错误。在ps7_init_gpl.c中,Ps7_ScuInit()函数会配置它,你可以在该函数执行前后,对比SCU_SAC的值,确认配置是否生效。
第四个是DDR_PHY_STAT寄存器(地址0xF8006080)。这是DDR PHY的状态寄存器,它的bit[0](PHY_READY)是DDR初始化成功的最终标志。在Ps7_DdrInit()函数的末尾,你应该看到一个轮询DDR_PHY_STAT的循环。在调试时,将断点设在这个循环内部,观察PHY_READY何时变为1。如果它一直为0,那就说明DDR硬件或时序参数有问题。
实操心得:我习惯在Vitis的“Hardware Window”中创建一个自定义的“Watch Group”,将这四个寄存器都添加进去,并设置自动刷新。这样,在单步执行时,我一眼就能看到所有关键状态的变化,就像在看一台精密仪器的仪表盘。
4.3 制作可烧写的BOOT.BIN:petalinux-package命令的深度解析
`petalinux-package --boot