
1. 为什么要在ZYNQ上折腾无DDR启动手里捏着一块ZYNQ板子焊上DDR颗粒跑个Linux或者裸机程序这是绝大多数人接触ZYNQ的常规路径。但有些场景下你手上可能只有一块光秃秃的ZYNQ芯片DDR颗粒还没焊或者DDR硬件设计有问题导致初始化失败又或者你只是想验证一下PS端最小系统能不能跑起来。这时候让FSBLFirst Stage Boot Loader不依赖DDR直接在OCMOn-Chip Memory上运行就成了一条必须走通的路。OCM是ZYNQ芯片内部的一块SRAM总容量256KB分成四个64KB的块PS和PL都能访问。它最大的特点就是上电即可用不需要任何外部器件初始化。DDR就不一样了上电后是一片混沌必须经过完整的DDR控制器初始化、训练、校准流程才能正常读写。所以当DDR还没就绪或者压根不存在的时候把FSBL搬到OCM上跑是最直接也最可靠的方案。这个需求在实际工程中出现的频率比想象中高。比如硬件打样回来DDR焊接良率有问题但你想先确认PS端基本功能是否正常比如某些低成本应用场景直接用OCM当运行内存就够了不需要外挂DDR再比如教学场景想让学生先理解ZYNQ的启动流程从最简单的OCM启动入手避免DDR初始化失败带来的挫败感。这些情况下掌握FSBL的OCM运行改造方法能帮你省下大量排查硬件问题的时间。我前后在ZYNQ-7000系列上做过多次无DDR启动的验证从ZYNQ-7010到ZYNQ-7020都试过。踩过的坑包括链接脚本没改对导致代码跑飞、OCM空间不够用导致栈溢出、FSBL里DDR初始化代码没屏蔽干净导致卡死等等。下面把这些经验完整梳理一遍从原理到实操再到避坑尽量让看到这篇内容的人少走弯路。2. FSBL启动流程与OCM运行的核心原理2.1 ZYNQ启动的完整链路ZYNQ的启动过程分几个阶段。上电或者复位后芯片内部固化的BootROM首先运行它负责从启动设备QSPI Flash、SD卡、NAND等读取Boot Header然后根据Header里的信息加载FSBL到指定的目标地址。这个目标地址默认是DDR的起始地址通常是0x00100000或者0x00000000附近。BootROM把FSBL搬过去之后跳转到FSBL的入口点开始执行。FSBL做的事情就多了初始化DDR控制器、配置时钟、加载PL比特流如果有的话、加载SSBL通常是U-Boot或者裸机应用、最后跳转到SSBL。整个链路里DDR初始化是FSBL的一个关键步骤因为后续的SSBL和应用程序通常都链接在DDR地址空间上。现在问题来了如果DDR不存在或者初始化失败FSBL自己能不能先跑起来答案是能但前提是FSBL本身必须被加载到OCM里而不是DDR里。BootROM支持把FSBL加载到OCM只要Boot Header里指定的目标地址落在OCM范围内就行。2.2 OCM的地址空间与容量限制OCM在ZYNQ-7000里的地址映射是0x00000000到0x0003FFFF总共256KB。但实际可用的连续空间要看你怎么分配。OCM分成四个64KB的块通过中央互联开关可以灵活配置。默认情况下低128KB0x00000000-0x0001FFFF映射到OCM高128KB可能被其他模块占用或者保留。对于FSBL来说256KB的空间其实挺紧张的。FSBL本身编译出来大概几十KB到一百多KB不等取决于你开了多少调试信息和功能。栈空间、堆空间、全局变量都要从这256KB里抠出来。所以改造的核心思路就是把FSBL的链接地址改到OCM范围同时精简代码确保所有段都塞得进去。这里有个细节需要注意BootROM在加载FSBL时会读取Boot Header里的目标地址。这个地址必须和FSBL链接脚本里的起始地址一致否则代码搬过去之后跳转就会跑飞。很多人改的时候只改了链接脚本忘了同步更新Boot Header结果就是FSBL加载后直接挂掉。2.3 为什么不能直接在DDR地址上跑FSBL有人可能会想我能不能让FSBL还是链接在DDR地址但实际运行时把它搬到OCM理论上可以但操作起来很麻烦。BootROM加载FSBL时就会把它放到DDR地址如果DDR没初始化这个写入操作本身就是失败的。所以最干净的做法就是让FSBL从一开始就链接在OCM地址上BootROM直接加载到OCM跳转执行全程不碰DDR。另一个常见误区是有人觉得只要在FSBL里把DDR初始化代码注释掉就行。但实际上如果FSBL链接在DDR地址即使你不初始化DDR代码本身也在DDR地址空间上CPU取指就会失败。所以链接地址的修改是必须的不是可选项。3. 改造FSBL运行在OCM上的完整实操3.1 环境准备与工程创建我用的工具链是Xilinx SDK现在叫Vitis也可以但SDK对ZYNQ-7000的支持更成熟。首先在Vivado里创建一个ZYNQ-7000的工程配置PS端时DDR控制器可以保留默认配置因为我们后面要屏蔽掉它的初始化。其他外设根据实际需要勾选比如UART用于打印调试信息QSPI或者SD用于启动。导出硬件到SDK后新建一个FSBL工程。SDK会自动生成FSBL的模板代码包括main.c、fsbl.h、fsbl_debug.h等文件。这个模板就是我们要改造的基础。在开始改之前先确认一下FSBL的默认链接脚本。在SDK工程目录下找到lscript.ld文件打开看看里面的起始地址。默认应该是0x00100000或者类似的DDR地址。记住这个值后面要改。3.2 修改链接脚本将代码定位到OCM链接脚本的修改是整个改造的核心。打开lscript.ld找到MEMORY区域的定义。默认可能是这样的MEMORY { ps7_ddr_0_S_AXI_BASEADDR : ORIGIN 0x00100000, LENGTH 0x1FF00000 ps7_ram_0_S_AXI_BASEADDR : ORIGIN 0x00000000, LENGTH 0x00030000 ps7_ram_1_S_AXI_BASEADDR : ORIGIN 0xFFFF0000, LENGTH 0x0000FE00 }我们要把代码段、数据段、栈都放到ps7_ram_0里。修改后的MEMORY区域MEMORY { ps7_ram_0_S_AXI_BASEADDR : ORIGIN 0x00000000, LENGTH 0x00030000 ps7_ddr_0_S_AXI_BASEADDR : ORIGIN 0x00100000, LENGTH 0x1FF00000 }然后在SECTIONS区域里把所有段都指向ps7_ram_0。关键段包括.text、.rodata、.data、.bss、.heap、.stack。栈顶地址要设置在OCM范围内比如0x0002FFF0。这里有个坑OCM的低地址区域可能被BootROM或者安全模块占用了一部分。实际测试下来从0x00000000开始放代码是可行的但保险起见可以把起始地址稍微往后挪一点比如0x00010000留出前面的空间给BootROM使用。不过这样可用空间就少了64KB需要根据FSBL的实际大小来权衡。修改完链接脚本后重新编译工程。编译成功后查看生成的.elf文件大小确认没有超出OCM容量。可以用arm-xilinx-eabi-size命令查看各段大小。3.3 屏蔽DDR初始化代码链接脚本改好后FSBL已经可以链接到OCM地址了。但FSBL的main函数里还有DDR初始化的调用如果不屏蔽掉程序跑到那里就会因为访问不存在的DDR而卡死。在main.c里找到fsbl_main函数里面会调用ps7_init()。这个函数是Vivado根据你的PS配置自动生成的里面包含了DDR控制器的初始化代码。我们需要做的是要么直接修改ps7_init.c把DDR相关的寄存器配置注释掉要么在调用ps7_init()之前就跳过DDR部分。更干净的做法是修改ps7_init.c找到DDR初始化相关的函数调用比如ps7_ddr_init_data相关的配置把它们注释掉。但要注意ps7_init()里除了DDR还有时钟、MIO、PLL等配置这些是PS正常运行必需的不能全部屏蔽。具体操作打开ps7_init.c搜索DDR相关的寄存器写入操作。通常会有大量的Xil_Out32调用地址在0xF8006000附近DDR控制器寄存器。把这些调用注释掉或者用一个宏开关控制。同时ps7_init()里可能还有等待DDR校准完成的循环也要一并去掉。另一个需要注意的地方是FSBL里可能会调用fsbl_handoff()或者类似的函数这些函数可能会访问DDR地址。需要检查FSBL的代码流程确保在跳转到SSBL之前没有任何代码访问DDR地址空间。3.4 调整Boot Header中的目标地址FSBL编译好之后需要生成BOOT.bin文件。在SDK里创建FSBL的启动镜像时Boot Header里会有一个目标地址字段。这个地址必须和FSBL链接脚本里的起始地址一致。在SDK的Create Boot Image界面里添加FSBL.elf时会显示一个Destination Device和Destination Address。默认可能是DDR地址需要手动改成OCM地址比如0x00000000或者你链接脚本里设置的起始地址。如果用的是命令行工具bootgen需要在.bif文件里指定目标地址。比如the_ROM_image: { [bootloader]fsbl.elf system.bit application.elf }这里的[bootloader]属性会让bootgen自动处理FSBL的加载地址但有时候需要显式指定。可以在bif文件里加上offset参数确保FSBL被加载到OCM。生成BOOT.bin后把它放到SD卡或者QSPI Flash里设置启动模式上电测试。如果一切正常UART会打印出FSBL的调试信息然后跳转到应用程序。4. 实操中容易踩的坑与排查方法4.1 链接脚本改完编译报错空间不足这是最常见的问题。OCM总共就256KBFSBL加上栈和堆很容易超。编译时会报region overflow的错误提示某个段放不下。解决办法有几个方向。第一精简FSBL的功能。FSBL模板里有很多调试打印和错误处理代码如果不需要可以通过宏定义关掉。比如fsbl_debug.h里的debug宏把DEBUG级别调低能省不少空间。第二减小栈和堆的大小。默认栈可能是几KB如果FSBL里没有递归调用和大的局部变量可以把栈调到1KB甚至更小。第三检查是否有不必要的库被链接进来。比如printf相关的库很占空间如果不需要打印可以把UART输出全部去掉。我遇到过一种情况FSBL编译出来只有80KB左右但链接脚本里栈和堆各给了64KB加起来就超了。把栈改成2KB堆改成4KB问题就解决了。所以关键是看实际用了多少不要盲目给大空间。4.2 程序跑飞或者卡在某个地址不动链接脚本改对了DDR也屏蔽了但程序还是跑飞。这种情况通常是Boot Header里的目标地址和链接地址不一致导致的。BootROM把FSBL加载到了一个地址但FSBL的代码是按照另一个地址链接的跳转过去后取指就错了。排查方法用SDK的调试器连接查看PC指针停在哪里。如果停在一个莫名其妙的地址大概率是地址不匹配。检查bif文件或者Create Boot Image界面里的目标地址确保和lscript.ld里的ORIGIN一致。另一个可能的原因是OCM的地址空间被其他模块占用了。比如PL端如果配置了访问OCM可能会和PS端的访问冲突。这种情况下需要检查Vivado里的OCM配置确保PS端有完整的访问权限。4.3 DDR初始化代码屏蔽不干净导致卡死有时候明明注释了DDR初始化但程序还是卡在某个循环里。这通常是因为ps7_init()里还有其他地方间接访问了DDR。比如某些时钟配置或者PLL锁定检测可能会读取DDR相关的状态寄存器。排查方法在ps7_init()的每个步骤后面加打印看卡在哪一步。或者用调试器单步执行观察PC指针的变化。找到卡住的位置后把相关的寄存器操作也屏蔽掉。还有一种情况是FSBL里调用了fsbl_misc_init()或者其他函数这些函数可能会访问DDR地址。需要检查FSBL的整个调用链确保没有任何地方访问DDR。4.4 OCM空间不够导致栈溢出栈溢出是个很隐蔽的问题。程序可能正常运行一段时间然后突然跑飞。这是因为栈指针超出了OCM范围写到了未映射的地址空间。排查方法在链接脚本里把栈顶地址设置得保守一点比如0x0002F000留出足够的余量。同时在FSBL里尽量减少局部变量和递归调用。如果实在需要大的缓冲区可以考虑用全局变量或者静态分配避免放在栈上。我个人的经验是OCM启动的FSBL栈大小给2KB到4KB就够了。如果超过这个范围说明代码需要优化而不是加大栈。5. 无DDR启动的适用场景与扩展思路5.1 硬件调试阶段的快速验证硬件打样回来DDR焊接有问题或者还没焊这时候用OCM启动可以快速验证PS端的基本功能。比如UART能不能打印、GPIO能不能控制、QSPI能不能读写。这些验证通过后再排查DDR问题思路会清晰很多。我一般会准备一个最小的OCM启动镜像只包含FSBL和一个简单的裸机程序功能就是打印一条信息然后点亮一个LED。这个镜像不依赖任何外部存储器上电就能跑。用它来确认芯片本身是活的然后再逐步加外设。5.2 低成本应用的OCM-only方案有些应用场景对成本敏感不需要大内存OCM的256KB就够了。比如简单的传感器数据采集、LED控制、串口通信等。这种情况下整个系统可以完全跑在OCM上不需要外挂DDRBOM成本能省不少。这种方案的思路是FSBL加载到OCM应用程序也链接到OCMFSBL直接跳转到应用程序不需要SSBL。整个启动链路非常短可靠性也高。需要注意的是应用程序的大小要控制在OCM容量以内同时要留出足够的栈和堆空间。5.3 从OCM启动到DDR启动的平滑过渡有时候项目初期用OCM启动验证后期需要切换到DDR启动。这时候可以把FSBL的链接脚本改回DDR地址同时恢复DDR初始化代码。但要注意Boot Header里的目标地址也要同步改回去。一个更优雅的做法是在FSBL里加一个判断根据某个GPIO或者寄存器的状态决定是初始化DDR还是跳过。这样同一个FSBL镜像可以同时支持OCM和DDR两种启动方式。不过这样会增加FSBL的复杂度需要权衡。5.4 常见问题速查表问题现象可能原因排查方法编译报region overflowOCM空间不足精简代码减小栈堆程序跑飞链接地址与Boot Header不一致检查lscript.ld和bif文件卡在某个循环DDR初始化未完全屏蔽单步调试定位卡死位置运行一段时间后跑飞栈溢出减小栈大小优化局部变量UART无输出时钟或MIO配置被误屏蔽检查ps7_init()中的时钟配置这张表是我在实际调试中总结出来的基本上覆盖了大部分常见问题。遇到问题时先对照这张表排查能省不少时间。6. 个人实操心得与几个关键细节6.1 关于OCM起始地址的选择虽然OCM的地址范围是0x00000000到0x0003FFFF但我一般不会从0x00000000开始放代码。原因是BootROM在启动过程中可能会使用低地址区域存放一些临时数据。虽然理论上BootROM执行完后会释放这些空间但保险起见我会把FSBL的起始地址设在0x00010000留出64KB的余量。这样做的代价是可用空间少了64KB但对于大多数FSBL来说192KB已经足够了。如果FSBL实在太大再考虑从0x00000000开始但要做好充分的测试。6.2 调试信息的输出策略FSBL里的调试打印是排查问题的利器但也会占用空间和UART带宽。我的做法是在开发阶段打开全部调试信息方便定位问题在最终版本里只保留关键的错误信息把DEBUG级别的打印全部关掉。具体操作是在fsbl_debug.h里修改DEBUG_PRINT_LEVEL宏。把它从FSBL_DEBUG_INFO改成FSBL_DEBUG_ERROR或者FSBL_DEBUG_NONE。这样能省下不少代码空间同时减少启动时间。6.3 关于QSPI启动时的DDR依赖有人问过ZYNQ 7020使用JTAG固化Flash时必须使用DDR吗答案是固化过程本身不需要DDR但如果你用的Flash烧写工具或者镜像里包含了DDR初始化的FSBL那就会依赖DDR。用OCM启动的FSBL来固化Flash可以完全绕过DDR这在DDR有问题的板子上特别有用。具体做法是先用JTAG把OCM版本的FSBL加载到芯片里运行然后通过FSBL里的Flash烧写功能把正式的BOOT.bin写到QSPI Flash里。这样即使DDR有问题也能完成固化。6.4 一个容易被忽略的细节缓存配置OCM的访问速度和DDR不同缓存配置也需要调整。默认情况下FSBL可能会使能DDR区域的缓存。如果代码跑在OCM上缓存配置需要相应修改否则可能出现缓存一致性问题。在FSBL的初始化代码里找到MMU配置相关的部分确保OCM区域被正确映射为可缓存或者不可缓存。一般来说OCM作为代码运行区域可以配置为可缓存以提高取指效率。但如果有DMA访问OCM就需要考虑一致性。6.5 测试验证的完整流程改造完成后我一般会按照以下流程验证用SDK的调试器加载FSBL.elf确认PC指针停在OCM地址范围内。单步执行观察UART是否有输出。全速运行确认FSBL能正常跳转到应用程序。把BOOT.bin写到SD卡设置启动模式为SD启动上电测试。如果SD启动成功再尝试QSPI启动。每一步都要确认无误后再进行下一步避免问题叠加导致排查困难。这套OCM启动的方案我在多个项目里验证过从ZYNQ-7010到ZYNQ-7020都能稳定运行。关键就是链接脚本、Boot Header地址、DDR初始化屏蔽这三件事把这三件事做对了基本就不会有大问题。剩下的就是根据具体项目调整空间分配和功能裁剪。