☰
ZYNQ无DDR启动实战:FSBL在OCM上运行的完整改造指南
2026/9/28 21:03:30 网站建设 项目流程

1. 为什么要在ZYNQ上折腾无DDR启动

手里捏着一块ZYNQ板子,焊上DDR颗粒,跑个Linux或者裸机程序,这是绝大多数人接触ZYNQ的常规路径。但有些场景下,你手上可能只有一块光秃秃的ZYNQ芯片,DDR颗粒还没焊,或者DDR硬件设计有问题导致初始化失败,又或者你只是想验证一下PS端最小系统能不能跑起来。这时候,让FSBL(First Stage Boot Loader)不依赖DDR,直接在OCM(On-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的块,通过中央互联开关可以灵活配置。默认情况下,低128KB(0x00000000-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总共就256KB,FSBL加上栈和堆,很容易超。编译时会报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上,不需要外挂DDR,BOM成本能省不少。

这种方案的思路是:FSBL加载到OCM,应用程序也链接到OCM,FSBL直接跳转到应用程序,不需要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 测试验证的完整流程

改造完成后,我一般会按照以下流程验证:

  1. 用SDK的调试器加载FSBL.elf,确认PC指针停在OCM地址范围内。
  2. 单步执行,观察UART是否有输出。
  3. 全速运行,确认FSBL能正常跳转到应用程序。
  4. 把BOOT.bin写到SD卡,设置启动模式为SD启动,上电测试。
  5. 如果SD启动成功,再尝试QSPI启动。

每一步都要确认无误后再进行下一步,避免问题叠加导致排查困难。

这套OCM启动的方案,我在多个项目里验证过,从ZYNQ-7010到ZYNQ-7020都能稳定运行。关键就是链接脚本、Boot Header地址、DDR初始化屏蔽这三件事,把这三件事做对了,基本就不会有大问题。剩下的就是根据具体项目调整空间分配和功能裁剪。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询