☰
ZYNQ无DDR运行:如何用OCM加载并运行裸机程序
2026/10/4 16:25:57 网站建设 项目流程

做ZYNQ开发这几年,大部分项目都被DDR“绑架”了。Vivado里加DDR、跑内存测试、要等DDR初始化完成、调试时还要看DDR的时序……这些流程大家都习以为常。但你真的每一次都需要DDR吗?我之前接过一个低成本、小体积的项目:只需要跑一个简单的状态机、控制几个外设、处理几百字节的协议数据,整板空间紧张到连DDR颗粒的位置都挤不出来。调研后发现,ZYNQ内部其实有一块被很多人忽略的OCM(On-Chip Memory),也就是280KB左右的片上SRAM。正常情况下它只给FSBL和BootROM用,但只要思路对,它完全可以承载你的裸机程序,让芯片在完全没有DDR的情况下正常启动和运行。

这篇东西就是想把“不带DDR的ZYNQ怎么用OCM加载程序并运行”这件事讲透。我不会只给你贴一个链接脚本片段,而是会把启动流程、FSBL改造、地址分配、Bootgen打包、JTAG调试、在线升级等整套玩法都拆开讲,顺便把我在这个过程中踩过的坑和优化思路一并写出来。适合谁看?如果你手头是个功能不复杂的裸机项目、想在低成本板卡上省掉一串DDR走线、或者正在设计一个不从DDR启动的Bootloader,这篇应该能帮你省下不少折腾时间。

1. 不带DDR的ZYNQ能做什么?先看懂OCM和启动链路

1.1 为什么要砍掉DDR:场景、收益与代价

很多人一听说ZYNQ不带DDR,第一反应是“这不就是残废芯片吗”。确实,带Linux的话DDR避不开,PetaLinux的U-Boot内核和根文件系统都指望那块大内存。但在裸机或者轻量RTOS场景里,DDR并不是必需品。省掉DDR之后,物料成本能直接下降一块,PCB上少一组上百根走线的DDR总线,板面积更紧凑,布线难度也低一大截。可靠性层面,少一个高频存储器件,也少了一类EMI和信号完整性问题。

当然,代价也很直接:可用的RAM总量从几百MB缩水到两百多KB,程序镜像必须精简,跑不了大堆栈的中间件。所以你只有在程序逻辑清楚、数据量可控、外设驱动数量有限的场景下才适合这么做。我的经验是:如果你需要的外设不超过UART、GPIO、SPI、I2C、CAN这类轻量接口,状态机和协议栈又能控制在几十KB以内,那OCM完全接得住。

1.2 关键硬件资源:这块片上存储到底有多大本事

ZYNQ-7000系列自带256KB的OCM,地址从0x00000000开始到0x0003FFFF结束。注意,它并不是一整块均质SRAM,而是分成若干区域:前192KB(0x00000000至0x0002FFFF)可以由CPU和DMA访问,后64KB(0x00030000至0x0003FFFF)通常被保留给安全启动和高系统控制使用,普通裸机程序尽量不要占用。实际开发中能自由使用的是前192KB,不过FSBL自己也要占一部分,所以真正留给应用代码和数据的地方通常只有100多KB。

OCM的访问速度和DDR不是一个量级的概念。DDR走的是内存控制器,有刷新、预充电、行列切换这些繁文缛节,而OCM直接挂在CPU的高性能端口上,访问延迟低得多。我实测过同一段纯计算代码,在OCM里跑比在DDR里跑明显快,因为它完全没有缓存未命中和总线仲裁的问题。它还允许PL侧的AXI主机访问,这意味着FPGA逻辑可以通过AXI接口读写OCM,在某些设计里可以当共享内存用。虽然容量小,但它并不是个“只能装FSBL的配角”,而是一块真正可用的高速SRAM。

提示:OCM的各个子区域可能存在访问权限限制,后64KB尤其敏感。非必要时不要碰0x00030000以上区域,否则可能触发异常。

1.3 启动流程回顾:BootROM、FSBL和OCM之间的关系

ZYNQ的上电启动流程是:芯片内部固化了一段BootROM,它先从配置引脚决定的启动源(QSPI Flash、SD卡、NAND、JTAG等)读取Boot Header,把FSBL镜像加载到OCM的开头地址,然后跳到OCM执行FSBL。FSBL再根据配置文件把用户程序加载到DDR(默认路径),或者加载到其他内存区域,最后跳过去运行。

在带DDR的常规设计里,FSBL的第一个大动作就是初始化DDR控制器,否则后面没法把应用程序从Flash搬到DDR里去。在无DDR设计里,这步就成了问题根源:FSBL可能会因为DDR控制器没接器件而卡死或者跑飞。所以要实现“无DDR运行”,核心工作就是两件事:第一,让FSBL不去初始化DDR模块;第二,让用户程序的链接脚本把代码段、数据段、堆栈都放进OCM的地图范围内。

2. 总体思路与方案选型:三种“不带DDR”跑法

2.1 方案A:定制FSBL + 应用链接在OCM(量产推荐)

这是我最推荐的方式,也是本文实操部分要展开的完整流程。基本思路是:在Vivado硬件工程里把DDR相关的接口和配置彻底去掉,然后把FSBL里DDR初始化相关的调用剪掉,再把裸机应用链接到OCM地址段,最后用Bootgen把FSBL和应用打包成BOOT.BIN,烧进QSPI Flash或者放到SD卡里。

这么做的好处是流程干净,贴近真实产品形态。BootROM上电后自动加载FSBL,FSBL瘦身后加载应用,应用在OCM里无缝跑起来。整个过程完全不需要人为干预,符合工业现场的启动要求。缺点是FSBL和应用挤在同一个物理内存里,地址管理要细心,应用代码如果超过百来KB就会比较紧张。

整个方案选型先列在这里,如果你只是做开发验证、不想动FSBL,可以直接跳到方案B。

2.2 方案B:JTAG直接把程序加载进OCM(开发调试推荐)

如果产品形态还没定型,你只是想知道“这个程序在OCM里能不能跑”,那完全没必要先折腾启动镜像。用Vitis或者XSDK的调试功能,把链接到OCM地址段的应用ELF直接下载到芯片里运行就行。开发工具会通过JTAG把程序写进OCM,然后控制CPU跑起来。

这个方案的优点是非常快,完全绕开BootROM、FSBL这些繁琐环节,改代码、编译、下载、跑起来,一分钟内就能验证一轮。缺点是你需要在电脑上插着JTAG调试器,没法脱离电脑独立运行,所以它只适合验证用,不适合最终产品。我一般先在方案B下把代码调通了,再去折腾方案A的启动镜像,这样能把两个问题的调试难度分开。

2.3 方案C:极简Bootloader常驻OCM,实现在线升级

再往下延伸一种场景:不带DDR的板子需要支持在线升级。此时的做法通常是设计一个极简Bootloader,把它放在OCM里,接收上位机发来的新固件,写入外部Flash,然后跳转到Flash里的应用代码执行。Bootloader本身不出现在最终应用里,只负责“搬运”和“校验”。

这种方案比前两种都复杂,因为它既要处理Flash擦写,又要管理协议和地址映射。我建议如果你只是做简单烧写,老老实实用方案A就行;只有当你有明确的远程升级需求时,才考虑在OCM里塞一个Bootloader。后面我会单独讲一下设计时容易踩的坑。

2.4 为什么不用U-Boot和Linux

有人会问:我能不能不初始化DDR,但照样跑个裁剪过的Linux?实话实说,几乎不可能。Linux内核本身就远超OCM容量,而且它的内存管理、页表、DMA子系统都建立在“有一片大内存”这个前提上。就算你想用PetaLinux去生成不含DDR的镜像,它在启动阶段也会因为找不到可用内存而崩溃。所以不要抱这个念想,无DDR场景下的操作系统选择就是裸机或极小RTOS,比如FreeRTOS,而且要非常克制地配置任务栈。

3. 实操:从Vivado到BOOT.BIN,一步步跑起来

3.1 第一步:创建一个不带DDR的硬件工程

先用Vivado搭建最小系统。新建工程,选择具体ZYNQ型号,然后添加ZYNQ7 Processing System IP。关键点来了:在ZYNQ配置界面里,找到DDR Configuration,把DDR控制器相关选项去掉或者选择“无”,具体版本界面略有差异,但你要确认生成的PS配置里不再包含DDR端口和引脚。如果你的板子物理上根本没有DDR颗粒,这一步其实和你平时“没选DDR”是一样的。

然后使能你需要的串口、GPIO、SPI等等外设。这里我的建议是最小化原则:能不用就不开,因为每个外设的驱动和缓冲区都会吃掉OCM的空间。配好之后,把PS和外部端口连接好,约束文件里管脚分配好,综合、实现、生成Bitstream。最后导出硬件到Vitis的XSA文件。

导出时要注意勾选“包括Bitstream”,因为后面FSBL工程可能需要PL配置。如果你不打算配置PL,只想跑纯PS程序,那Bitstream可要可不要,但导出XSA时尽量保持默认完整导出,省得后面缺东西。

3.2 第二步:定制FSBL,把它变成“无DDR感知”的引导程序

打开Vitis,用XSA创建一个FSBL工程。大多数版本里,你新建应用工程时可以直接搜索FSBL模板。生成出来的标准FSBL会调用ps7_init(),而ps7_init.c里会自动包含DDR初始化函数ps7_ddr_init()。既然我们硬件工程里已经去掉了DDR配置,这个函数在一些版本里会变成一个空壳或者压根不生成。但保险起见,我强烈建议你打开ps7_init.c检查一下,看看里面有没有DDR寄存器配置。

如果发现还有DDR初始化代码,有两条路可以走:一是直接编辑ps7_init.c,把ps7_ddr_init函数体内的寄存器写入操作注释掉;二是修改fsbl_main.c,在初始化流程里不调用包含DDR初始化的那个函数。我更推荐第一种,因为它最直观,而且万一以后你恢复DDR设计,这段代码也还在。

另外还要检查FSBL的链接脚本。FSBL本身也是跑在OCM里的,它的链接脚本默认就指向OCM低地址,这部分一般不用动。但你要留意FSBL的堆栈大小,因为在无DDR情况下,FSBL没法把栈临时切换到DDR,所有变量都在OCM里,默认配置一般够用,不用特意改。

编译FSBL,编译完成后查看一下生成的ELF映射表,确认.text和.data段都在0x00000000往上的OCM范围内。如果你在map文件里看到任何DDR地址段,说明你漏了什么,回头检查。

3.3 第三步:创建裸机应用并改写链接脚本到OCM

现在基于同一个XSA创建裸机应用工程。驱动、BSP配置都选最小化。重点是打开链接脚本(lscript.ld)。Vitis里可以用GUI的Linker Script编辑器,也可以直接改ld文件。你需要把可用的内存区域从DDR换成OCM。

我的做法是:把MEMORY描述里的PS7_DDR_0区域直接删掉,新增一个区域叫PS7_OCM_0,起始地址0x00000000,长度0x00040000,然后代码段、只读数据段、数据段、堆栈段全部映射到PS7_OCM_0上。如果你后64KB不想碰,那就把起始地址改成0x00000000,长度设置成0x00030000,只使用前192KB。

注意,Vitis默认生成时,可能还会引用DDR区域符号。如果你在GUI里改了内存区域后,还需要检查各个section的布局。栈和堆的大小尤其要控制,裸机程序默认的栈大小有时是1MB,这在无DDR下直接超了。把栈设成比如16KB或32KB就足够跑大多数裸机逻辑了。堆如果你用不到malloc,直接设成1KB都行。

再强调一下:链接脚本的堆栈段不能和代码段、数据段重叠。Vitis生成的段地址一般会自己按顺序排列,但你要确保_start地址在最前面,中断向量表能落到0x00000000附近。

3.4 第四步:用Bootgen生成BOOT.BIN并烧写

FSBL工程和应用工程都编译通过之后,打开Vitis的“Create Boot Image”工具。这一步需要添加一个引导镜像分区:第一部分选FSBL的ELF,第二部分选你的应用ELF。如果你的PL需要配置,还要把bitstream加进去,但无DDR项目通常不需要PL,可以不加。

在Bootgen的配置里,需要确认“Boot Mode”为QSPI、SD等对应你的启动介质。BOOT.BIN的生成原理是:FSBL的ELF和应用的ELF都会被转换成带有地址信息的镜像段,Bootgen按顺序排好,加上Boot Header打包成一个文件。因为我们的应用ELF已经链接到OCM地址,所以Bootgen打包时会把对应段的加载地址标记成0x00000000往后的OCM区域。FSBL运行时,会把应用从Flash读到OCM对应地址,然后跳过去。

之后就是烧写了。如果启动介质是QSPI Flash,可以用Vivado的Hardware Manager烧BOOT.BIN;如果是SD卡,直接把BOOT.BIN放到FAT32分区的根目录即可。上电启动后,看串口输出和应用行为,确认程序跑起来。我习惯先让应用点个LED或者周期性打印一段字符,这样验证最直观。

3.5 验证执行流:串口打印、GPIO输出和调试器观测

无DDR启动的验证重点不是“程序有没有跑”,而是“它是不是从OCM里跑的”。最简单的方法是在应用代码开头打印一段带地址信息的日志,比如读取当前PC寄存器的值,然后通过串口发出来。PC值落在0x00000000到0x0003FFFF之间,就说明程序确实在OCM执行。

第二个验证方法是点灯。把GPIO配置成某个LED,程序里循环翻转电平,用示波器或者肉眼观察闪烁频率。这个测试主要确认FSBL成功完成了跳转,而且应用的主循环没有被异常打断。

第三个方法是接JTAG调试器,直接在Vitis里连接运行中的目标,查看寄存器。调试器能挂上,说明CPU没有死循环在异常向量里,运行状态健康。我在实际项目中会把这三种方法都用上,因为它们分别验证了链接地址、跳转行为和运行稳定性。

4. 常见问题与排查技巧实录

4.1 FSBL卡死或反复重启,连串口都没有输出

这个问题十有八九是FSBL仍然尝试初始化DDR控制器,而系统里根本没有DDR颗粒,导致寄存器操作失败或者总线事务挂起。排查思路:先用JTAG连上芯片,看看PC停在哪条指令上。如果停在任何涉及DDR地址的代码段,基本就是这个问题。解决方案就是回到3.2节,把ps7_init里DDR初始化部分彻底关闭,必要时直接用纯文本编辑器打开ps7_init.c,把包含DDR寄存器配置的数组或初始化函数跳过去。

另一种可能是Boot Header配置的启动设备和你实际烧写的介质不一致。比如你烧到QSPI,却把启动模式引脚跳线设成了SD卡,那BootROM根本找不到FSBL,自然没有任何输出。检查一下MIO启动模式引脚的电平,对照ZYNQ手册确认和设备对应。

4.2 程序能运行,但一访问外设就死机或数据错乱

这可能涉及OCM地址安全属性和Cache配置的问题。默认情况下,OCM是支持CPU的Cache操作的,但如果你在FSBL里配置了MMU,允许了某些地址段为Device类型,那么对OCM的访问就可能产生异常。裸机BSP通常有自己的MMU配置,建议查阅BSP生成的内存属性表,确认OCM区域被标记为可缓存或至少为普通内存类型。

另外,如果你在应用里用了DMA引擎,且DMA缓冲区放在OCM里,要特别注意一致性。OCM虽然是SRAM,但DMA和CPU并发访问同一缓冲区时,Cache会造成数据不同步。解决方法是:要么关闭D-Cache,只开I-Cache;要么在DMA传输前后执行Cache清理和无效化操作。我建议无DDR环境下干脆只用I-Cache,省心很多。

4.3 链接报错,bin文件超过OCM容量

这是最直白的空间不足问题。当你的程序代码量、只读数据、全局变量、堆栈总和超过你设定的OCM保留区域时,链接器会报错或者生成超限的bin文件。我的经验是把代码精简作为第一优先级,不要试图通过调整地址硬塞进去。

一个比较实用的办法是观察map文件里各段的大小。通常占大头的是库函数和标准库初始化代码,比如printf的浮点格式化功能非常占空间。如果你只需要整数打印,可以用自己实现的简易输出函数,能省下几十KB。另外,用-Os编译优化选项也能有效控制代码体积。Vitis里可以在应用工程的编译选项中开启优化大小。

4.4 在线升级设计中,Bootloader需要注意什么

如果你的Bootloader和App都试图塞进OCM,就会遇到一个问题:Bootloader本身的空间会挤压App的空间。实际操作中我做的是把Bootloader放在OCM的前64KB,然后把App放在QSPI Flash里,App运行时也直接从Flash里执行,或者只在启动时被Bootloader搬运到OCM剩余区域。

运行在Flash里虽然慢一点,但胜在省内存。不过要注意Flash的随机访问延迟和缓存命中率问题,建议开启I-Cache。升级过程中,Bootloader接收新固件时,要先把新固件写入Flash的临时分区,全部写完并校验CRC通过后,再覆盖运行分区,否则中途断电会直接变砖。我校验用的是CRC32,内存占用小,对OCM环境非常友好。

4.5 OCM运行的程序掉电后不保存,别把运行数据放错地方

刚接触OCM开发的人容易把“运行程序在OCM”和“数据存在OCM”混为一谈。OCM是SRAM,掉电全丢,所以它只能用来放运行时代码和数据,不能当持久化存储使用。你要保存的参数、日志、校准值,应该放在外部Nor Flash、EEPROM或者SD卡里。

如果你希望上电后能快速读回上次运行的状态,可以在应用启动后立即从Flash读取配置到OCM内存,然后在运行中频繁访问OCM副本。这样做的好处是读写速度快,坏处是每次修改配置后要及时回写Flash,别等掉电了再后悔。

5. 一些扩展与我的个人实际体验

5.1 在量产项目里用OCM省掉DDR的心得

这个项目最后交付的时候,我的同事还半信半疑,觉得“ZYNQ不装DDR还跑得动吗?”实践证明,跑得很稳。整个系统的逻辑很单一:传感器数据通过SPI进来,经过简单算法处理,结果通过UART发出去,外加控制两个继电器。代码量压缩到大概80KB,数据缓冲区控制在30KB以内,剩下空间用于堆栈,余量充足。

量产之后几乎没有出现内存相关的问题,因为OCM是片上SRAM,不会像DDR那样有信号完整性和刷新故障。低温环境下,DDR偶尔会出初始化失败,而OCM完全没这个顾虑。当然我们也在选型时反复斟酌过容量问题,宁可多花时间精简代码,也不冒超额的风险。

5.2 什么情况下坚决不建议省略DDR

如果你的应用涉及Linux、视频缓冲、大数据采集、复杂TCP/IP协议栈、机器学习推理这些,那不要考虑OCM了,老老实实上DDR。OCM不是万能药,它只适配“小、快、稳”的场景。拿我另一个项目例子来说,想用ZYNQ做网口数据采集,数据包动不动几MB,即便程序能塞进OCM,缓冲区也无处安放。

判断标准很简单:把需求列出来,估算一下所有全局变量和动态内存之和。如果超过OCM可用空间的一半,我建议趁早放弃,别在嵌入式开发里赌运气。内存这种东西,余量一定要留足,否则后期每加一个功能都是痛苦的挪地址过程。

5.3 最后分享一个小技巧:把OCM当“黑匣子”用

在不带DDR的项目里,OCM空间虽然主要给程序用,但你可以刻意留出最后几KB作为运行日志缓冲区。程序里把关键状态、错误码、变量快照写进去,掉电前把缓冲区整体写入外部Flash。下次启动时Bootloader或者应用先检查这个区域,就能知道上一次异常发生在哪里、当时的现场是什么样。

这个方法帮我解决过一次很棘手的偶发死机问题。因为OCM访问快、不影响主循环,几乎可以把日志当成实时记录。等问题定位完,再把缓冲区缩小,腾出空间给其他功能。如果你做的也是无DDR的小产品,强烈建议在链路设计时就把这个黑匣子区域预留出来,省得后面满世界找bug。

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

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

立即咨询