ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

ZYNQ无DDR运行:如何用OCM加载并运行裸机程序

ZYNQ无DDR运行:如何用OCM加载并运行裸机程序 做ZYNQ开发这几年大部分项目都被DDR“绑架”了。Vivado里加DDR、跑内存测试、要等DDR初始化完成、调试时还要看DDR的时序……这些流程大家都习以为常。但你真的每一次都需要DDR吗我之前接过一个低成本、小体积的项目只需要跑一个简单的状态机、控制几个外设、处理几百字节的协议数据整板空间紧张到连DDR颗粒的位置都挤不出来。调研后发现ZYNQ内部其实有一块被很多人忽略的OCMOn-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而是分成若干区域前192KB0x00000000至0x0002FFFF可以由CPU和DMA访问后64KB0x00030000至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上电后自动加载FSBLFSBL瘦身后加载应用应用在OCM里无缝跑起来。整个过程完全不需要人为干预符合工业现场的启动要求。缺点是FSBL和应用挤在同一个物理内存里地址管理要细心应用代码如果超过百来KB就会比较紧张。整个方案选型先列在这里如果你只是做开发验证、不想动FSBL可以直接跳到方案B。2.2 方案BJTAG直接把程序加载进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。
返回列表