资讯详情

资讯详情

ZYNQ无DDR运行方案:OCM片上存储器的启动流程与链接脚本实战

1. 为什么要折腾“不带DDR的ZYNQ”OCM在无外置内存方案中的真实位置1.1 先搞清楚ZYNQ里OCM到底是什么ZYNQ这类芯片和普通单片机最大的不同就是它内部集成了ARM Cortex-A9双核处理器但这并不意味着离开了外部DDR它就不会工作。很多人拿到Vivado SDK的第一反应是新建工程、用默认链接脚本、在DDR地址0x00100000处跑一个Hello World于是潜意识里形成了“ZYNQ必须有DDR”的印象。实际上PS内部还有一个常被忽略的存储区——OCMOn-Chip Memory片上存储器。OCM是集成在PS内部的SRAM不是PL端的Block RAM也不是挂在AXI总线上的外设。以Zynq-7000系列为例OCM由两部分组成低端256KB区域地址范围从0x00000000到0x0003FFFF高端64KB区域地址范围从0xFFFC0000到0xFFFCFFFF。低端OCM正是BootROM在启动阶段存放FSBL的地方。换句话说芯片上电后、DDR还完全没被初始化之前CPU执行的代码就放在OCM里。把OCM理解成“芯片自带的暂存仓库”比较直观外部DDR相当于一个需要通电、配置、等待训练完成的仓储中心而OCM是CPU身边的小型储物柜。存储柜只有256KB但胜在随取随用、不需要任何初始化流程CPU复位后就能直接读写。1.2 什么情况下值得砍掉DDR我接触无DDR的ZYNQ方案最初来自一个成本敏感的项目产品只做简单的串口协议转换和数据采集PL面只用了一些轻量逻辑整板物料成本被压得很紧。DDR颗粒、DDR电源、布线空间、PCB层数这些都是实打实的成本砍掉DDR可以节省好几块钱还能把板子面积缩小一圈。当时我反复问自己一个256KB的程序真的能覆盖需求回答是如果应用被约束为裸机单任务、代码尺寸被刻意精简很多设备根本用不上几十MB的内存。抛开成本因素还有两类场景非常依赖OCM。一类是硬件调试早期PCB刚贴片回来DDR芯片可能没焊好或者DDR电源有问题此时想验证PS能不能正常工作、PL能不能加载比特流OS、烧写工具、PetaLinux统统派不上用场写一个不超过几十KB的裸机测试程序直接放到OCM里跑串口输出是最快的验证路径。另一类是启动加载链路设计BootROM本来就把FSBL放进OCM执行如果第一级加载器足够小可以直接让程序在OCM里完成硬件初始化、外设配置再决定后续加载动作。这在安全启动、远程更新、工厂测试中都很常见——你先有一个不依赖DDR的“最小系统”再让这个最小系统去初始化更大的世界。需要强调在ZYNQ体系里试一下“无DDR跑程序”不是旁门左道而是理解芯片启动流程的关键一步。当你亲手把程序放到OCM并成功跑起来你对BootROM、FSBL、链接脚本、内存映射这些概念的理解会提升一个层级。1.3 为什么多数人的第一反应是“不可能”绕不开的原因是默认工具链的路径依赖。Vivado SDK创建一个应用工程时生成的链接脚本会自动把DDR区域设为最大的可用内存区域。FSBL更是直接把DDR初始化当成必经步骤。如果你不做任何修改让FSBL去初始化一片不存在的外设CPU访问的是空洞地址轻则读到随机数据重则触发数据异常表现就是程序卡死、串口无输出、JTAG连接异常。一旦你理解FSBL默认行为只是Xilinx工程师为你准备的“标准路径”而不是芯片的物理限制问题就清楚多了我们完全可以修改FSBL跳过DDR初始化同时把应用链接脚本指向OCM最后让CPU在OCM里执行完整程序。这既不是重写芯片驱动也不需要高深技巧关键只是动手前把内存映射和启动流程理顺。2. 不带DDR时BootROM与FSBL的真实行为2.1 BootROM阶段FSBL落在哪里很多人以为FSBL是从启动设备被“读取”出来然后“运行”的但对它运行时的存储位置却不清楚。Zynq上电后BootROM固化在芯片内部它负责根据MIO引脚的启动模式设置从QSPI Flash、SD卡、NAND等介质中读取FSBL镜像然后放入OCM。BootROM本身的代码很小不依赖DDR控制器也不需要任何外部存储初始化。因此无DDR设计里BootROM的流程和有DDR设计完全一样BootROM不会因为DDR不存在而罢工。它只关心两件事启动设备是否可读以及读出来的FSBL头部是否满足校验要求。只要FSBL二进制能被BootROM正确解析并搬运到OCMCPU就会跳转到OCM中FSBL的入口地址开始执行。整个过程是芯片内部机制外部有没有DDR完全不参与。这里有一个容易踩的误区有人以为BootROM会把FSBL放到DDR里所以没有DDR就无法完成任何启动。实际上BootROM第一个加载阶段永远使用OCM。FSBL之所以看起来“活在DDR里”那是因为FSBL运行起来之后默认的链接脚本和运行地址都在DDR中FSBL在初始化DDR后会把自身需要的数据和代码加载到DDR。但注意FSBL的“源码”在编译的时候已经决定了它的起始运行地址如果你的FSBL链接脚本把起始地址放在0x00100000而DDR不存在那么BootROM在把FSBL放到DDR时就会失败。这正好回答了为什么无DDR方案里不但要改应用链接脚本还要让FSBL在OCM里运行。2.2 FSBL默认依赖DDR的三个环节FSBL并不是只有一个“初始化DDR”的函数那么简单它对DDR的依赖体现在三个层面上的联动只看代码很难全暴露出来。第一个环节是HAL层的初始化。FSBL启动后会调用XFsbl_InitDdr之类的函数这个函数会配置DDR控制器寄存器、执行DDR Training训练时序参数、等待DDR PLL锁定。如果DDR缺失寄存器操作本身不会崩溃但状态寄存器永远等不到“ready”标志程序会陷入等待循环或报错分支。第二个环节是地址空间的假设。FSBL内部很多数据结构、分区表信息都被链接到DDR地址。Vivado SDK默认生成的FSBL工程里linker script中分配了DDR地址空间FSBL函数的栈、堆以及运行时代码都假设DDR已经可用。如果你只是简单地在FSBL代码里跳过了DDR初始化函数但仍然让FSBL链接到DDR地址运行第一次函数调用就会出错。第三个环节是镜像搬运的目标地址。FSBL从启动设备读取二级镜像时是根据应用ELF文件里的段地址来决定目标位置的。如果你的应用在无DDR设计里依然被编译链接到DDR地址FSBL会把代码搬到不存在的地址。我们后面要做的链接脚本修改本质上就是把这个目标地址从DDR改成OCM。2.3 把最终程序整个塞进OCM的真实容量约束OCM的256KB听起来不大但和传统嵌入式单片机相比已经很可观。一个精简的裸机串口程序关闭优化的情况下可能也就几十KB开启优化后可以压到十几KB。中断向量表、堆栈段、数据段全部加起来只要程序逻辑不涉及大型协议栈、不依赖文件系统、不跑重量级RTOS是完全塞得下的。实际操作时还要注意两点。第一OCM的低端256KB区域BootROM在启动初期会占用一部分不过一旦FSBL接管并跳到应用入口这部分地址就已经被释放如果你的FSBL在跳转前还需要从OCM里读取自己的表格就得避开重叠区。第二高端OCM0xFFFC000064KB虽然也可用但它和低端OCM不是连续的地址空间链接脚本里需要用两个MEMORY区域来分别描述不能简单地合并成一个连续范围。这些约束听起来麻烦实际上会逼着你写出更干净的嵌入式代码。我见过不少工程师在DDR充足时写出“堆到几MB都不心疼”的程序换到OCM环境后为了省几千字节不得不认真分析栈深度、优化全局变量、裁剪打印信息——训练出来的代码质量反而更扎实。3. 工程改动第一步把链接脚本收进OCM3.1 找到并读懂lscript.ld在Vivado SDK里创建一个裸机应用工程后工程目录下会有一个lscript.ld文件。它由SDK根据硬件平台BSP自动生成默认内容大致是MEMORY { ps7_ddr_0_S_AXI_BASEADDR : ORIGIN 0x00100000, LENGTH 0x1FF00000 ps7_ram_0_S_AXI_BASEADDR : ORIGIN 0x00000000, LENGTH 0x00004000 ps7_ram_1_S_AXI_BASEADDR : ORIGIN 0xFFFF0000, LENGTH 0x0000FE00 }这里可以看到Xilinx默认把容量最大的DDR区域排在最前面而且程序默认的入口点、堆栈、初始化代码段都被安排到DDR区域里。ps7_ram_0代表低端OCMps7_ram_1代表高端OCM但默认只分配了很小的空间。我们要做的第一件事就是把MEMORY区域的定义改掉让所有代码块都进入OCM。一个稳妥的做法是直接用一个地址连续的OCM区域MEMORY { ps7_ram_0_S_AXI_BASEADDR : ORIGIN 0x00000000, LENGTH 0x00040000 }0x00040000就是256KB正好覆盖低端OCM的完整范围。如果你的应用确实需要用到高端OCM可以再补一段ps7_ram_1_S_AXI_BASEADDR : ORIGIN 0xFFFC0000, LENGTH 0x00010000但我要提醒一句想真正跑通无DDR方案好习惯是先把程序全部放在低端OCM里因为拿到0x00000000这个起始地址和BootROM/FSBL的默认行为最兼容调试干扰最少。高端OCM等程序在低端OCM跑通之后再考虑。3.2 向量表、堆、栈在OCM里的排布方式链接脚本不只是定义一段内存范围那么简单它内部的SECTIONS指令决定了代码段、只读数据段、可读写数据段、BSS段、堆和栈分别被放哪里。SDK默认生成的lscript.ld里已经有下面这段结构SECTIONS { .text : { *(.text) *(.text.*) *(.gnu.linkonce.t.*) } ps7_ddr_0_S_AXI_BASEADDR .init : { KEEP (*(.init)) } ps7_ddr_0_S_AXI_BASEADDR .fini : { KEEP (*(.fini)) } ps7_ddr_0_S_AXI_BASEADDR .rodata : { __rodata_start .; *(.rodata) *(.rodata.*) } ps7_ddr_0_S_AXI_BASEADDR .data : { *(.data) *(.data.*) } ps7_ddr_0_S_AXI_BASEADDR .bss : { *(.bss) *(.bss.*) } ps7_ddr_0_S_AXI_BASEADDR .heap : { __heap_start .; . . HEAP_SIZE; __heap_end .; } ps7_ddr_0_S_AXI_BASEADDR .stack : { __stack_start .; . . STACK_SIZE; __stack_end .; } ps7_ddr_0_S_AXI_BASEADDR }所有 ps7_ddr_0_S_AXI_BASEADDR都指向DDR。我们不需要逐行理解每一段是什么意思只需要记住一个原则把这里所有的输出段指向区域改成ps7_ram_0_S_AXI_BASEADDR。你可以用文本编辑器打开lscript.ld把DDR区域的名称替换成OCM区域名称。注意文本中可能有一堆*(.text .text.*)之类的模式这些是链接器通配符不涉及修改只需要改段后面的内存区域归属。你可能已经注意到链接脚本顶部往往通过_STACK_SIZE 0x1000;这样的符号定义堆栈大小。对于OCM环境我建议把STACK_SIZE压到0x10004KB以内因为OCM总共就256KB栈开得越大留给业务逻辑的空间就越少。如果是纯裸机程序、没有递归调用4KB栈已经足够。3.3 容量上限与对齐检查不要等链接失败才后悔链接脚本改完后重新编译工程链接器会做两件事一是把所有输入节拼装成最终的ELF文件二是检查总占用是否超过MEMORY区域定义的LENGTH。如果超了会报类似“region ‘ps7_ram_0_S_AXI_BASEADDR’ overflowed by 15872 bytes”的错误。这不是坏事它逼着你提前看清程序尺寸。推荐在命令行执行arm-none-eabi-size 你的工程目录/Debug/xxx.elf输出会显示text、data、bss三部分的大小。text代表代码和只读数据data代表已初始化全局变量bss代表未初始化全局变量。三者之和再加上栈空间就是OCM的实际占用。如果text有300KB那就不要挣扎了要么裁剪代码要么换带DDR的方案。还要注意对齐问题。ARM Cortex-A9要求中断向量表按一定边界对齐链接脚本本身通常会为.isr_vector或_vector_table加KEEP和. ALIGN(4)之类的修饰不建议随意改动。如果程序一开始乱跳、中断不触发第一件事就去查链接脚本里向量表的对齐和起始地址是不是0x00000000。4. 定制最小FSBL跳过DDR初始化却不破坏启动链路4.1 修改FSBL代码或编译选项FSBL在无DDR场景里是个微妙的存在。BootROM把FSBL放进OCM执行但如果FSBL源码的链接地址在DDR那它一运行就会崩。所以第一步是回到FSBL工程把FSBL自己的链接脚本也改成OCM地址。Vivado SDK的FSBL工程默认同样生成lscript.ld修改方法和前面应用工程的修改别无二致只是把FSBL工程里所有内存区域指向OCM的256KB。这件事做完后FSBL才能在无DDR的物理环境下安全运行。接下来才是跳过DDR初始化。打开FSBL主函数一般位于fsbl_main.c或xfsbl_main.c。函数调用链里有一句核心调用Status XFsbl_InitDdr(); if (Status ! XFSBL_SUCCESS) { XFsbl_ErrorUpdate(XFSBL_ERR_DDR_INIT_FAIL); goto ErrorHandler; }这是FSBL默认初始化DDR的入口。对于无DDR设计我通常把这一整段判断注释掉或者用一个条件宏包起来。你要想保持工程整洁可以在fsbl_hooks.c里自己写一个空实现的DDR初始化回调函数然后在编译选项中定义对应的宏让FSBL编译进空函数而不是默认的DDR配置流程。具体宏名在不同Vivado版本里略有差异我自己更倾向直接修改源码因为FSBL代码是Xilinx提供的开放源码注释掉那一行并不影响其他外设初始化。注意FSBL除了显式初始化DDR还可能通过MIO和时钟配置间接引用DDR引脚。如果硬件设计上没有连接DDRFSBL里与DDR相关的MIO配置最好也从初始化列表里去掉否则可能出现引脚复用冲突。4.2 一段极简OCM加载器的等效实现如果你觉得修改FSBL不够痛快还有一个更彻底的方法直接写一个比FSBL短得多的自研加载器替代Xilinx FSBL。这个加载器的任务非常简单初始化UART、初始化QSPI或SD卡控制器、读取应用镜像、把镜像搬到OCM指定地址、跳转到入口。它不需要DDR不需要复杂的Handoff参数传递也不需要支持PCW寄存器设置。我曾经在一个量产项目里就采用了这种方案。板子上连FSBL都不跑BootROM直接加载我自研的bootloader到OCMbootloader再判断是需要进入烧写模式还是正常启动。这个bootloader代码量控制在40KB左右完全可以在256KB的OCM区域里从容运行后续应用则被加载到OCM的另一块可用地址。这种做法的好处是启动链路完全透明、可控。坏处是需要自己处理启动设备驱动工作量比单纯注释代码大。对于只是想验证无DDR方案是否可行的朋友我还是建议从修改FSBL开始先把流程跑通再决定要不要自己做精简加载器。4.3 生成BOOT.BIN并烧入QSPI/SD的关键步骤当FSBL和应用工程都编译好之后需要用Bootgen工具把它们打包成BOOT.BIN。BIF文件内容大致如下the_ROM_image: { [bootloader] fsbl.elf hello_world.elf }第一行指定FSBL为bootloader第二行是我们要加载的应用。Bootgen会根据每个ELF文件的段地址来决定把数据放到哪里。因为我们的应用ELF已经被链接到OCM地址所以Bootgen会把应用代码放到0x00000000开始的区域。然后执行bootgen -image bootimage.bif -o i BOOT.bin再用QSPI烧写工具或SD卡格式化工具把BOOT.BIN写入启动介质。设置好启动引脚后上电如果一切正常你会看到无DDR系统从QSPI启动后自动运行OCM里的应用。这个小实验能彻底改变你对ZYNQ启动流程的理解。4.4 跳过FSBL直接下载与设备启动的路径选择在实际项目里我还遇到过不需要固化启动介质、只想通过JTAG快速验证OCM程序的情况。这种场景可以绕过FSBL直接用SDK或XSCT把ELF下载到OCM执行。具体流程在下一章详述但这里要说明一点JTAG下载方式适合调试不适合产品化验证。因为真实上电时芯片一定会经历BootROM和FSBL链路如果这个链路没跑通JTAG下能跑不代表产品能正常启动。所以在无DDR方案验证中我的建议是两条腿走路开发阶段用JTAG快速迭代验证阶段务必把BOOT.BIN烧到启动介质上完整复位启动一次。很多“奇奇怪怪的问题”只在冷启动时暴露比如FSBL把某个外设初始化了一半、BootROM占用的OCM区域和应用重叠等。5. 下载与验证没有DDR时的调试技巧5.1 用XSCT把ELF直接灌进OCMVivado软件自带的XSCTXilinx Software Command-Line Tool是调试无DDR工程的利器。硬件连接好JTAG后启动XSCT执行connect targets -set -filter {name ~ ARM*#0} rst -system dow hello_world.elf conrst -system会把整个PS系统复位同时让CPU停止在复位状态。这步不能省尤其在你之前可能通过JTAG加载过带DDR的程序时不复位会导致某个外设处于脏状态程序行为变得不可预测。dow命令会把ELF中的各个段下载到它们链接的地址——因为我们链接脚本已指向OCM所以它会下载到0x00000000附近。最后con让CPU开始执行。如果在执行dow时遇到“存储器写入失败”或“目标地址错误”多半是链接脚本里还残留DDR地址。用info mem或memmap命令查看目标存储映射确认0x00000000区间在调试器的内存视图里是有效区域。5.2 确认程序真的在OCM里跑而不是幻觉有一种情况特别迷惑人程序加载成功、串口也打印了东西但你并不确定它是从OCM运行的可能FSBL或调试器偷偷把代码复制到了其他地方。要验证实际执行位置有一个很朴素的方法查看链接生成的map文件。map文件会列出每个段的加载地址Load Address和运行地址Virtual Address。如果OCM Link脚本生效你会看到.text段的运行地址从0x00000000开始。再配合XSCT的内存读取命令mrd 0x00000000 8这会把OCM开头的8个字打印出来。正常情况下应该看到ARM中断向量表的前几条指令比如跳转指令ea000016之类的十六进制数据。如果你读到的全是0xFF或0x00000000说明ELF没有正确加载到OCM起始地址。我还习惯在代码里故意写一个只读全局变量volatile uint32_t ocm_magic 0xDEADBEEF;程序运行时用XSCT的mrd去读该变量地址看是否真是0xDEADBEEF。如果读到正确值说明数据段确实被链接进OCM并正确初始化了。5.3 常见踩坑对照OCM溢出、向量表、BSS清零无DDR的OCM调试中最常见的故障可以整理成下面这张表症状可能原因对策链接时提示溢出程序总量超过256KB裁剪代码、开启-O2优化、检查是否有库函数引入大量代码启动后PC跑到0xFFFF...向量表不在0x00000000检查lscript.ld中.text段和第0个MEMORY区域的起始地址全局变量初值不对BSS段未清零或data段初始化拷贝未执行确认crt0.S提供的启动代码被链接进.text段中断完全不触发GIC或异常向量表配置地址错误确认_vector_table的链接位置和XScuGic的CPU接口设置程序偶尔跑飞栈溢出覆盖相邻代码段减小栈大小、检查递归、用call stack监控栈指针其中BSS清零问题最容易掉以轻心。OCM是SRAM上电初始值可能是随机值。标准启动代码会把BSS段清零并把data段从加载地址拷贝到运行地址。如果你自己写启动代码或精简了crt0.S必须保证这两个动作存在否则未初始化全局变量初始值随机程序行为就会像“薛定谔的猫”。另外一个经验是在无DDR调试阶段尽量用-O0关闭优化或-O1轻量优化。原因很简单-O2可能对程序做激进的指令重排和常量化导致源码级断点位置不准误导你判断执行路径。等程序跑稳定了再开启-O2验证最终行为。6. 一个能跑的最小示例与量产扩展思路6.1 UART输出Hello的小工程拆解我用一个最低限度的串口打印程序来收束前面的内容。这个demo只用到ZYNQ的UART外设不涉及DDR不涉及PL适合验证OCM启动链路。第一步在Vivado里创建一个基于Zynq的硬件工程只保留PS端UART1MIO引脚、QSPI或SD控制器按启动介质选择不添加DDR IP不勾选DDR Bank甚至可以把DDR控制器在Zynq配置界面里直接关掉。生成比特流后Export Hardware并启动SDK。第二步在SDK里创建FSBL工程按第四章的方法修改FSBL链接脚本并跳过DDR初始化重新编译。第三步创建Application工程修改lscript.ld把内存区域改为OCM。主函数可以这样写#include xparameters.h #include xuartps.h #include xil_printf.h #define UART_BASEADDR XPAR_XUARTPS_0_BASEADDR int main(void) { XUartPs uart; XUartPs_Config *cfg; cfg XUartPs_LookupConfig(UART_BASEADDR); XUartPs_CfgInitialize(uart, cfg, UART_BASEADDR); XUartPs_SetBaudRate(uart, 115200); xil_printf(\r\nOCM BOOT OK, run from 0x00000000\r\n); while (1) { /* 空循环保持程序运行 */ } return 0; }这个程序编译出来非常小text段大约十几KB。用JTAG按第五章的方式下载串口助手会看到打印信息。之后再按4.3节生成BOOT.BIN烧入启动介质复位后同样打印就代表无DDR启动链路完全打通。6.2 从OCM验证平滑过渡到带DDR的正式设计很多人会问无DDR验证完以后如果产品最终还是要加DDR前面的工作是不是白做了答案是不仅不白做还能让正式的带DDR设计更稳。因为你在OCM阶段已经把启动链路、外设初始化、应用程序骨架全部验过一遍等DDR焊上板子后只需要把FSBL里的DDR初始化恢复、把应用链接脚本改回DDR地址就能在成熟骨架上继续堆功能。我个人的习惯是在工程里保留两套链接脚本一套lscript_ocm.ld一套lscript_ddr.ld通过编译配置切换。这样即使量产版本带DDR我也能在现场诊断时快速编出一版OCM固件在不起DDR的情况下测试核心硬件这对定位“DDR颗粒接触不良”“DDR电源纹波大”这类硬件问题极有帮助。你可以把无DDR的OCM诊断固件当成一套独立的硬件自检系统它不依赖任何外部存储永远能在最恶劣的硬件状态下给你留一扇门。6.3 使用OCM做程序运行区时需要养成的几个习惯无DDR方案一旦要上产品有几个习惯最好提前养成。第一全局变量能省则省能用局部变量就不用全局变量能放到.rodata的常量绝不放.data因为.data段要占双份空间一份在Flash里存初始值一份在OCM里放运行时副本。第二谨慎使用printf族函数这些函数会把内部缓冲和格式化逻辑带进来代码尺寸暴涨必要时自己写一个极简的字符输出函数。第三定期检查map文件把明显异常的节找出来比如某个驱动意外引入了4KB对齐表格。OCM不是用来承载庞大业务的它是启动链路的接棒者、硬件自检的看门人、也是低成本产品里那道最后的防线。搞清楚它的脾气你才算真正驾驭了ZYNQ这套系统而不是被默认工程模板牵着走。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →