1. 先想清楚:这套板子到底该用裸机还是跑系统
第一次把 XC7Z010 焊到板子上通电的时候,串口一个字都不吐,我盯着那颗电源指示灯看了半天,心里想的是“这几百块钱的芯片总不能是坏的”。后来的事实是芯片一点问题没有,问题出在我压根没搞明白这颗芯片上电之后的头几百毫秒到底发生了什么。Zynq-7000 这个系列比较特殊,它把双核 Cortex-A9 的处理器系统和可编程逻辑放在同一颗 die 上,PS 侧带着 DDR 控制器、UART、SD、QSPI、以太网这些外设,PL 侧是你熟悉的 FPGA 逻辑资源。XC7Z010 和 XC7Z020 是这条产品线里被用得最多的两颗,前者 28K 逻辑单元、80 个 DSP、2.1Mb BRAM,后者 85K 逻辑单元、220 个 DSP、4.9Mb BRAM,PS 部分基本一致,都是双核 A9 加 256KB 片上 OCM、512KB L2。做图像采集、电机控制、协议转换这类活,020 的余量更足;只做接口桥接或者轻量控制,010 完全够用,还省成本。
这篇东西想聊的是从一个空工程到板子能自己跑起来这条完整链路:裸机工程怎么编译、FreeRTOS 怎么塞进去、生成的镜像怎么烧到 QSPI 或者丢到 SD 卡里让板子自己启动。目标读者是刚接触这颗芯片、手上有块开发板但卡在“编译能过、上板就死”的兄弟,也包括那些裸机跑得挺顺、想上 RTOS 又怕踩坑的人。我不会只贴步骤,每个环节为什么这么配置、参数是怎么算出来的,我都会说清楚,因为这些东西你在手册里翻得到,但没人告诉你哪一步最容易翻车。
1.1 XC7Z010 和 XC7Z020 的差异比你想象的更影响工程决策
很多人选型的时候只看逻辑单元数,觉得 010 和 020 就是 PL 资源多少的区别,PS 侧代码完全一样,随便选。这话对了一半。PS 侧确实共用同一套处理器子系统,寄存器映射、启动流程、外设地址空间几乎一模一样,同一份裸机代码在两颗芯片上都能跑。但 BRAM 容量的差距会直接改变你的数据通路设计。010 只有 2.1Mb BRAM,做一路 1080p 的图像行缓存就差不多吃满了,你不得不用 DDR 做帧缓存,那问题就来了——DDR 带宽要和 CPU 抢,cache 一致性也得自己管。020 有 4.9Mb,能在 PL 侧做乒乓缓冲,把一部分数据流锁在片上,CPU 那边就轻松很多。
还有一个容易被忽略的点:DDR 颗粒的适配。XC7Z010 的开发板常见 512MB 或者 1GB DDR3,XC7Z020 的板子大多是 1GB。DDR 的初始化参数写在 FSBL 里,由 PS 的 DDR 控制器完成训练和校准。你在 Vivado 里配置 PS 的时候,DDR 型号、位宽、时序参数如果和实际板子对不上,最典型的症状就是 FSBL 跑到 DDR 初始化那一步直接卡死,串口没有任何输出。这种问题查起来很折磨人,因为它不报错,就是停在那儿。我后面会专门讲怎么用 FSBL 的调试输出定位到具体是哪一步。
1.2 裸机和 FreeRTOS 的边界在哪里
裸机不是“低级”,RTOS 也不是“高级”,它们解决的是不同的问题。裸机适合那种逻辑单一、实时性要求靠中断硬扛的场景,比如一个协议转换器,主循环里轮询几个状态机,中断里收发数据,代码量小,调试直观。但一旦你的系统里出现“几个事情要同时做,而且彼此之间有时序要求”的情况,裸机的主循环就会迅速退化成一大坨状态机嵌套,改一个功能牵连一片。
FreeRTOS 在这颗芯片上的价值,主要是把并发逻辑从状态机里解放出来。比如一个数据采集系统,你要同时做 AD 采样、串口上报、SD 卡存盘、按键响应,用 RTOS 就是四个任务加优先级,每个任务内部是顺序代码,可读性完全不是一个量级。代价是你要理解任务栈、优先级抢占、临界区这些东西,还要处理 A9 的 cache 和 DMA 打架的问题。我的建议是:如果你的裸机主循环里出现了三层以上的状态机嵌套,或者你开始需要“延时但不阻塞其他逻辑”,那就是该上 RTOS 的信号了。
1.3 整条链路先在心里过一遍
从代码到板子自己跑,中间有这么几层:你写应用代码,编译器把它编成 ELF;原厂工具把 FSBL(第一级引导加载程序)、PL 的比特流、你的应用 ELF 打包成一个 BOOT.BIN;板子上电后,片内固化的 BootROM 根据启动模式引脚决定从哪里读这个 BOOT.BIN;读进来之后先跑 FSBL,FSBL 负责初始化 DDR、配置 PL、然后把应用搬到 DDR 里跳过去执行。这条链路里任何一环出问题,现象都是“板子没反应”,所以定位问题必须靠分段验证,不能靠猜。
下面我会按“环境搭建、裸机编译、FreeRTOS 移植、镜像打包、程序加载、问题排查”这个顺序往下走,每一段都配上我实际踩过的坑。
2. 开发环境与工程骨架:别在第一步埋雷
工具链这块,早期大家用的是 ISE 那一套,后来统一到 Vivado 加 SDK,再后来 SDK 又被 Vitis 取代。现在手上如果是新装的机器,基本就是 Vivado 加 Vitis 的组合。版本选择上有个原则:不要追最新,跟着你手上开发板的参考工程走。板厂给的例程是用哪个版本验证的,你就装哪个版本,跨大版本迁移工程经常会遇到 IP 核升级、BSP 重新生成之类的麻烦事,对新手来说纯属给自己加难度。
2.1 器件库和安装选项
安装 Vivado 的时候,器件选型那一步一定要勾上 Zynq-7000 系列,别只勾自己以为需要的那一颗。XC7Z010 和 XC7Z020 的封装有 CLG400、CLG484 这些区别,同一个型号不同封装的引脚定义不一样,如果你装的器件库不全,建工程的时候选不到对应的封装,后面约束文件对不上,编译能过但上板引脚全错。
安装空间上要有心理准备,完整装下来几十个 G。我一般建议把 Vitis 一起装上,因为 BSP 生成、FSBL 编译、BOOT.BIN 打包都在这个环境里完成,分开装反而麻烦。
2.2 建一个最小可跑工程
不要一上来就把你那个复杂项目搬进来。先建一个最小的:PS 只开 UART 和 DDR,PL 什么都不放,应用就是往串口打印一行字符串然后死循环。这个最小工程能跑通,说明工具链、下载器、串口、电源全都没问题,后面出问题就一定是新加的东西引起的,排查范围立刻缩小。
建工程的流程大致是这样:Vivado 里新建工程,选对型号和封装,创建一个 Block Design,把 Zynq Processing System 这个 IP 拖进去,双击配置。配置界面里重点看三块:一是 PS 的时钟,输入晶振频率要和你板子上的实际晶振对上,常见 33.333MHz 或者 50MHz,填错了 UART 波特率全是乱的;二是 DDR 型号,按板子的实际颗粒填;三是外设,UART0 或者 UART1 使能,MIO 引脚分配按板子的原理图来。配置完生成 HDL Wrapper,跑综合、实现、生成比特流。
提示:MIO 引脚分配这一步千万不要凭感觉。板子的原理图上 UART 接的是 MIO14/15 还是 MIO48/49,一定要看清楚,配错了串口就是没输出,你会以为是代码问题查半天。
2.3 导出硬件并生成 BSP
比特流生成之后,导出硬件平台(包含 .xsa 文件),在 Vitis 里新建平台工程和应用工程。平台工程会根据 .xsa 生成 BSP,BSP 里包含驱动库、启动代码、链接脚本模板。这里有个细节:BSP 的版本和你应用工程的版本必须一致,混用不同版本的 BSP 会出现一些诡异的链接错误或者运行时挂死。
工程目录我习惯这样组织:顶层一个目录放硬件导出文件,下面分 platform 和 app 两个子目录,app 里再分 src 和 include。不要把所有东西平铺在一起,等你项目里有五六个应用工程的时候,你会感谢当初分类的自己。
3. 裸机工程的编译:链接脚本才是真正的关键
裸机工程的编译本身不难,难的是让编译出来的东西能在一个正确的内存位置上运行。很多人写的代码逻辑没问题,编译零警告,下载进去就是不跑,八成是链接脚本或者启动地址配错了。
3.1 启动链路的三层结构
再强调一次这个结构,因为它决定了你所有的地址规划:BootROM 是芯片出厂固化的,你改不了,它只干一件事——按启动模式把 FSBL 搬到一个很小的片上 RAM 里执行;FSBL 是你能改的第一段代码,它初始化 DDR、时钟、PL,然后把应用搬到 DDR;应用才是你写业务逻辑的地方。
FSBL 是跑在 OCM 里的,OCM 总共 256KB,实际能给 FSBL 用的更少,所以 FSBL 的体积有硬限制,一般要求控制在 192KB 以内。你在 FSBL 里加太多调试打印或者大数组,很容易就超了,超了之后的表现是打包能过,上板启动失败,因为 BootROM 搬不下这么多东西。
应用的运行位置通常在 DDR,起始地址我一般用 0x00100000。为什么不从 0x00000000 开始?因为 DDR 的低地址段有时候会被 FSBL 或者一些保留区域占用,从 1MB 偏移开始最稳妥,也方便你在前面留出空间做别的用途。这个地址要在链接脚本里写死,同时 FSBL 的加载地址也要和它一致,两边对不上就是跳过去执行了一堆垃圾指令。
3.2 链接脚本里到底写了什么
链接脚本(.ld 文件)的核心就几段:MEMORY 定义有哪些内存区域、各自的起始地址和长度;SECTIONS 定义代码段、数据段、bss 段、堆栈分别放在哪。Zynq 裸机工程里常见的写法是把 DDR 定义为ps7_ddr_0,起始 0x00100000,长度按你的 DDR 容量减去偏移算;OCM 定义为ps7_ram_0,起始 0x00000000,长度 0x00030000 左右。
堆和栈的大小是要自己算的。栈是每个任务或者主程序用的,裸机里就一个主栈,默认给个 0x2000 到 0x4000 通常够用,但如果你在函数里开了大数组,栈就会悄悄溢出,症状是跑到某个函数就飞了。堆是给 malloc 用的,裸机里如果不用动态分配,可以把堆设得很小甚至为零,省下来的空间给数据段。
编译报错里最常见的一条是region 'ps7_ddr_0' overflowed by XXXX bytes,意思就是你的代码加数据超过了你给这个区域划的长度。这个时候不要急着改链接脚本把长度调大,先看看是不是哪个大数组被定义成了全局变量,或者是不是优化等级太低导致代码膨胀。
3.3 优化等级和 printf 的取舍
调试阶段我一般用 -O0 或者 -Og,代码好单步,变量不会被优化掉。但 -O0 编出来的代码体积可能是 -O2 的两三倍,如果你的应用本来就贴着内存上限,-O0 直接编不过。发布版本切到 -O2,体积和速度都合适。-Os 更激进,但有些对时序敏感的循环会被优化出意想不到的行为,慎用。
printf 这块有个坑:标准库的 printf 带浮点支持,会把代码体积撑大几十 KB,而且在裸机上需要你实现 _write 之类的系统调用桩函数,否则链接会报一堆 undefined reference。裸机阶段我更推荐用xil_printf,它是轻量实现,不支持浮点,但打印个整数、字符串足够了,而且不需要额外的桩函数。真要看浮点,自己乘个 1000 打整数再脑补小数点,比折腾库省事得多。
3.4 FSBL 的编译和调试输出
FSBL 的源码在平台工程里是可见的,你可以改。调试启动问题时,把fsbl_debug.h里的FSBL_DEBUG_INFO打开,FSBL 会把每一步执行到哪、DDR 校准结果、读到的镜像信息都从串口打出来。这个输出是定位“上电没反应”最有效的手段:如果连 FSBL 的第一行打印都看不到,问题在 BootROM 到 FSBL 这一段,可能是启动模式引脚不对、QSPI 里没东西、或者 FSBL 本身超了 OCM 容量;如果打印到 DDR 初始化就停了,那基本就是 DDR 参数不对。
4. FreeRTOS 移植:BSP 点几下就行,坑全在配置里
把 FreeRTOS 跑起来这件事本身不复杂,BSP 里勾一下就有了。真正花时间的是配置参数怎么定、任务怎么划分、cache 和 DMA 怎么共存。
4.1 BSP 里的 FreeRTOS 开关
在平台工程的 BSP 设置里,找到freertos这个选项,把它从 standalone 切成 freertos,然后重新生成 BSP。这一步会自动把 FreeRTOS 的内核源码、移植层(针对 Cortex-A9 的端口文件)、以及FreeRTOSConfig.h加进工程。同时 BSP 会提供xSemaphoreCreateMutex这类原语和中断控制器的对接,中断向量表的注册方式也从裸机的XScuGic_Connect变成需要和 FreeRTOS 的configMAX_SYSCALL_INTERRUPT_PRIORITY配合。
这里有一个非常关键的概念:优先级分组。Cortex-A9 的 GIC 中断优先级数值越小优先级越高,而 FreeRTOS 里有一个阈值,低于这个阈值(数值上更小)的中断不受 RTOS 临界区影响,可以随时打断系统;高于阈值的必须调用FromISR版本 API。这个阈值配错了,症状是偶发的死机,而且极难复现。
4.2 堆方案和内存配置
FreeRTOS 有五种堆管理方案,Zynq 的 BSP 默认用 heap_4,也就是带碎片合并的动态分配。configTOTAL_HEAP_SIZE决定了 FreeRTOS 自己管理的这块内存有多大,它和应用本身的堆是分开的两块区域,别搞混了。我的经验值是:每个任务的栈按 1KB 到 4KB 估,加上队列、信号量、内核对象,再留 50% 余量。比如你有六个任务,平均 2KB 栈,那就是 12KB,配置成 24KB 比较稳。
任务栈溢出是新手最常遇到的问题,现象是任务跑一阵子突然不进调度了,或者进了 HardFault。FreeRTOS 提供了栈溢出检测钩子,把configCHECK_FOR_STACK_OVERFLOW设成 2,然后实现vApplicationStackOverflowHook,在里面打个打印或者点个灯,能帮你快速定位是哪个任务爆了栈。
4.3 中断、tick 和优先级设计
configTICK_RATE_HZ我一般设 1000,也就是 1ms 一个 tick,对于大多数控制类应用够用。设太高会增加调度开销,设太低则vTaskDelay的精度不够。系统节拍用的是 PS 的私有定时器,这个在 BSP 里已经接好了,不用你手动配。
任务优先级的设计有个简单原则:越靠近硬件、越有实时性要求的任务优先级越高。比如数据接收任务高于数据处理任务,数据处理高于存盘任务。但不要滥用优先级,所有任务都设成高优先级等于没有优先级。另外,同优先级的任务在 FreeRTOS 里默认是时间片轮转的,如果你的任务里有死循环且不让出 CPU,同优先级的其他任务就永远得不到执行。
4.4 cache 和 DMA:这块不处理好,数据会随机出错
A9 有 L1 数据 cache,PL 侧通过 AXI 总线访问 DDR 的时候是绕过 cache 的。这就意味着:CPU 写了一段数据到内存,数据可能还在 cache 里没落到 DDR,这时候 PL 去读,读到的是旧数据;反过来 PL 往 DDR 写了数据,CPU 去读,读到的可能是 cache 里的旧副本。
解决办法是在启动 DMA 之前做 cache 刷新(flush),在 DMA 完成之后做 cache 失效(invalidate)。相关函数是Xil_DCacheFlushRange和Xil_DCacheInvalidateRange,传入你要操作的缓冲区地址和长度。这里有个坑:缓冲区地址必须按 cache line 对齐,cache line 是 32 字节,如果你传的地址没对齐,刷新操作会把相邻的数据也一起刷掉或者失效掉,导致其他变量莫名其妙被改。
注意:如果 DMA 和目标缓冲区都在 DDR 且缓冲区很大,每次收发都做全量 cache 操作会明显拖慢性能。更彻底的做法是把这块区域配置成非 cache 属性,在 MMU 页表里设置,这样就不需要手动维护了,代价是 CPU 访问这块内存变慢。选哪种取决于你的数据量和实时性要求。
5. 镜像打包与程序加载:三种方式各有各的适用场景
代码编译出来只是个 ELF,板子不认识。要让它自己跑起来,得打成 BOOT.BIN,然后放到板子能读到的地方。
5.1 BOOT.BIN 里到底装了什么
BOOT.BIN 是 BootROM 能识别的格式,里面按顺序装三样东西:FSBL 的 ELF、PL 的比特流(可选)、应用的 ELF。打包用一个叫 bootgen 的工具,输入是一个 .bif 文件,内容大致长这样:
the_ROM_image: { [bootloader]fsbl.elf system.bit app.elf }[bootloader]这个标记告诉工具哪个是第一阶段引导程序,必须放在第一个。比特流放在 FSBL 之后、应用之前,FSBL 会负责把它配置进 PL。应用放最后,FSBL 把它搬到 DDR 然后跳转。
打包命令大概是bootgen -image xxx.bif -arch zynq -o BOOT.BIN -w on,-w on是覆盖已有文件。打包成功后会生成 BOOT.BIN,大小通常在几百 KB 到几 MB 之间,取决于你的比特流和应用大小。
5.2 JTAG 在线加载:调试阶段的主力
JTAG 方式最简单,不用烧任何东西,下载器连着,工具里点下载就行。流程是先初始化 PS(这一步会把 DDR、时钟配好),然后下载比特流到 PL,最后下载应用 ELF 到 DDR 并运行。这种方式适合反复改代码反复验证的阶段,改一行编译一次下载一次,几十秒一个循环。
JTAG 有个细节:如果你只下载应用不重新初始化 PS,而之前 PS 已经被初始化过了,那运行是没问题的;但如果板子刚上电,你直接下载应用 ELF,DDR 还没初始化,代码根本没地方放,下载会报错或者下进去跑飞。所以顺序一定是先初始化 PS,再下比特流,最后下应用。
5.3 QSPI Flash 固化:产品形态的常规选择
板子要脱离调试器独立运行,最常用的是把 BOOT.BIN 烧到板载的 QSPI Flash 里。烧写可以通过工具里的 Flash 编程功能,也可以通过 JTAG 跑一段小程序去擦写。烧写之前要确认几件事:Flash 的型号和容量、工作在哪种模式(单线、双线、四线)、分区怎么划。
QSPI 启动时,BootROM 从 Flash 的 0 地址开始读,读多少?它读固定长度的头部,解析出 FSBL 的长度,然后把 FSBL 搬进 OCM。所以 FSBL 必须放在 Flash 的最开头,这一点 bootgen 已经帮你保证了。
这里有个实际生产中会遇到的问题:如果你的应用需要在线升级,那就不能只放一个 BOOT.BIN,得做双镜像或者多镜像,靠一个标志位决定从哪个分区启动。BootROM 本身支持多镜像查找,它会在 Flash 里按顺序找有效的镜像头,第一个校验失败就找下一个。利用这个机制可以做简单的回滚:新固件写在第二个分区,启动成功后在应用里把标志改成指向新分区,启动失败则保持指向老分区。
5.4 SD 卡启动:研发阶段最省事的方式
SD 卡启动的好处是改固件不用烧 Flash,把卡拔下来插电脑上换个文件就行。做法是把 BOOT.BIN 放到 FAT32 分区根目录,文件名必须是BOOT.BIN(全大写),板子的启动模式拨到 SD 卡,上电就能跑。
SD 卡启动有个坑:卡的兼容性。不是所有卡都能被 BootROM 正确识别,尤其是大容量高速卡,有些初始化时序对不上。我遇到过一张 64G 的卡怎么都不启动,换一张 8G 的老卡立刻就跑了。所以研发阶段建议备几张不同品牌容量的卡,出问题先换卡排除。
启动模式是由板子上的 MIO 引脚电平决定的,通常是几个拨码开关或者电阻跳线。常见配置是 MIO[5:3] 决定主启动设备,100 是 QSPI,101 是 SD,000 是 JTAG。具体到你手上的板子,一定看原理图确认,别照着别的板子的拨法抄。
6. 常见问题排查:这些现象背后到底是什么
我把这几年遇到过的问题整理成了一张速查表,按现象分类,方便你对号入座。
| 现象 | 最可能的原因 | 排查动作 |
|---|---|---|
| 上电串口无任何输出 | 启动模式引脚错、Flash 里没镜像、FSBL 超 OCM 容量 | 拨到 JTAG 模式验证硬件,检查 FSBL 体积 |
| 串口输出乱码 | 晶振频率配置错、波特率不匹配 | 核对原理图晶振值,检查 PS 时钟配置 |
| FSBL 打印到 DDR 初始化停止 | DDR 型号或时序参数与实际不符 | 按颗粒手册重新配置 DDR 参数 |
| 应用下载后立即跑飞 | 链接地址与加载地址不一致 | 检查 .ld 文件和 FSBL 加载偏移 |
| 编译报 region overflowed | 代码或全局数组超出内存区划 | 查 map 文件找大对象,调高优化等级 |
| 链接报 undefined reference to _sbrk | 用了标准库函数但没提供桩函数 | 换 xil_printf 或补齐系统调用实现 |
| RTOS 跑一阵子死机 | 任务栈溢出或中断优先级配置错 | 开栈溢出检测,核对优先级阈值 |
| DMA 数据偶发错误 | cache 一致性未处理 | 收发前后做 flush/invalidate |
6.1 上电不启动的分层定位法
遇到“板子没反应”,不要从头到尾瞎试,按层切。第一层,把启动模式拨到 JTAG,用工具连上看能不能识别到芯片,识别不到说明供电或者 JTAG 链路有问题,跟软件无关。第二层,能识别但初始化 PS 报错,说明时钟或者 DDR 配置和硬件不匹配。第三层,JTAG 能跑但 Flash 启动不行,问题就在镜像打包或者 Flash 烧写上。
这个分层法能帮你在十分钟内把范围从“整个系统”缩到“某一个环节”,比反复重新编译有效得多。
6.2 那些不报错但就是不对的问题
最难受的不是编译报错,是编译全过、下载也成功、就是不按预期跑。举两个我印象最深的例子。
一个是中断不进。代码里注册了中断,使能了,就是不触发。查了半天发现是 GIC 的优先级阈值配得比中断本身的优先级还高,导致这个中断被 FreeRTOS 的临界区屏蔽掉了。改了阈值立刻就好。
另一个是变量值莫名其妙被改。一个全局的标志位,主循环里明明没动它,跑着跑着就变了。最后定位到是相邻的一个数组越界写入,把标志位覆盖了。这种问题用调试器看变量变化很难抓,我的做法是在可疑变量前后各放一个固定值的“哨兵”变量,定期检查哨兵有没有被改,被改了就说明发生了越界。
6.3 关于 printf 调试的一点经验
裸机阶段串口打印是最有效的调试手段,但打印本身会改变时序。有些问题加了打印就消失,去掉打印就复现,这就是典型的时序敏感问题,通常和中断竞争或者硬件握手有关。遇到这种情况,把打印换成一个 GPIO 翻转,用示波器或者逻辑分析仪看波形,时序影响小得多。
另外,在中断服务函数里打印是大忌。串口输出是阻塞的,在中断里打印会严重拉长中断响应时间,可能导致其他中断丢失甚至系统卡死。中断里最多置个标志或者发个信号量,真正的打印放到任务里做。
7. 一些踩过坑之后才明白的事
工程能跑起来和工程能稳定跑起来,中间隔着的就是这些细节。我最后悔的一件事是早期做项目时没有把 FSBL 的调试输出保留在发布版本里,结果现场设备出问题,串口一点信息都没有,只能靠猜。后来学乖了,发布版本里保留一个极简的启动日志,就打印几行关键信息——镜像版本、DDR 容量、启动耗时,出问题时这几行字能省掉几个小时的扯皮。
还有一点关于版本管理:硬件导出文件、BSP 配置、链接脚本这三样东西一定要进版本库,而且要和代码同一次提交。我见过太多“代码是对的但编出来的东西不对”的情况,最后发现是某个人本地改了一下 BSP 配置没提交,别人拉下来编译就是另一个结果。BSP 这种自动生成的东西尤其容易被忽略,但它里面藏着中断配置、时钟配置、内存布局,任何一项变了行为都会变。
至于性能优化,别一上来就盯着 -O3 和各种编译选项。Zynq 这类带 PL 的芯片,真正的性能瓶颈往往在数据搬运上,也就是 PS 和 PL 之间的 AXI 带宽、DDR 访问效率、cache 命中率。把数据通路理清楚,让频繁访问的数据尽量留在片上或者 cache 里,比调编译选项带来的提升大一个数量级。这一点在我做过的几个采集类项目里反复被验证,代码逻辑一行没改,只是把缓冲区从 DDR 挪到 OCM,处理延迟直接降了一半。
如果你现在正卡在某个环节,我的建议是回到最小系统:最小工程能跑,就一点点往上加你的功能,每加一块验证一次。嵌入式开发里最忌讳的就是一次性把所有模块都堆上去然后祈祷它能跑,因为一旦出问题,你要面对的是组合爆炸式的可能性,而不是一个可以定位的故障点。