ARTICLE DETAIL

资讯详情

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

Zynq UltraScale+ MPSoC双核R5裸机OpenAMP通信工程搭建详解

Zynq UltraScale+ MPSoC双核R5裸机OpenAMP通信工程搭建详解 一直想在Zynq UltraScale MPSoC上把双核R5用起来的人多半都卡在了同一步不知道怎么在Vitis IDE里把一个能跑的OpenAMP裸机工程从零弄出来。如果你手头正好是Zynq UltraScale MPSoC的板子又想在RPUCortex-R5F上跑裸机程序还需要和APU侧Linux或另一个R5核通信那这篇就是给你写的。我尽量把从新建工程到跑通demo的每一步都掰开揉碎包括平台工程怎么建、哪些参数不能乱选、代码里哪些API是OpenAMP的关键、以及实际调试时最容易踩的坑。OpenAMP这套东西说简单也简单说麻烦也麻烦。简单在于它本质上就是一套共享内存加中断的通信机制麻烦在于Xilinx现在叫AMD在Vitis里把这个流程封装得比较深文档分散在好几本手册里新手很容易在第一步选platform的时候就走错门。所以这次我直接用Xilinx官方推荐的方式在Vitis IDE里从Platform工程开始一步步带你把一个dual-core R5的OpenAMP裸机应用跑起来。1. OpenAMP的原理与选型逻辑1.1 为什么在RPU上跑裸机还要用OpenAMP很多人第一次接触OpenAMP时都有一个困惑我就在R5上写个裸机程序晶振初始化好、点个灯、跑个循环不就完了为什么还要引入OpenAMP这么重的一个通信框架这个问题的答案取决于你到底要做什么。如果你的R5程序完全自给自足不和外界交互那确实不需要OpenAMP。但实际项目里很少有这样的场景——通常R5上跑的实时控制逻辑需要把状态上报给Linux在APU上或者需要接收来自Linux的配置参数再或者更复杂一点两个R5核R5_0和R5_1各自承担一部分实时任务核与核之间需要同步和传数据。这时候你就面临三个方案第一个方案是自己写一套共享内存协议约定好DDR里的某个地址作为消息区再配合中断做通知。这个方案最灵活但坑也最多你要自己处理缓存一致性尤其R5默认是写回缓存、要设计消息格式、要考虑双核同时访问共享内存的竞争条件。第二方案是用Xilinx提供的专属库比如Mailbox等但这也是一个半定制的轮子离开了具体BSP就很难复用。第三个方案就是OpenAMP它把共享内存、中断、消息格式、生命周期管理这些都做成了标准框架你只需要调用几个API就能完成双核通信。从我的经验看如果你只是点个灯自己写裸机main函数就够了但只要你沾到双核通信这件事越早引入OpenAMP越划算。因为通信这块的调试成本非常高用现成框架虽然会牺牲一些底层自由度但换来的是可维护性和可移植性。而且OpenAMP是Linux基金会下面的开源项目在异构多核领域几乎是事实标准你学会了这套接口后面换到其他支持OpenAMP的芯片平台也能平滑迁移。1.2 OpenAMP在MPSoC上是怎么工作的OpenAMP的全称是Open Asymmetric Multi-Processing Framework翻译过来就是“开放式非对称多处理框架”。所谓非对称指的是参与通信的多个核跑的系统不一样最常见就是Linux加裸机甚至裸机加裸机。在Zynq UltraScale MPSoC上OpenAMP的典型布局是这样的APU侧的四核A53跑LinuxLinux侧有OpenAMP的驱动和rprocremote processor框架RPU侧的两个R5核跑裸机程序裸机侧通过OpenAMP的库libmetal加open-amp来注册通信端点。Linux和R5之间通过一个叫作“shared memory mailbox interrupt”的通道通信这个mailbox中断在MIO里是私有中断。画了一张简化的数据流APU Linux通过remoteproc驱动加载R5的elf固件启动R5执行R5跑起来后OpenAMP库会初始化虚拟IOvirtio的传输层Linux侧对应也有一个OpenAMP的tty或者rpmsg节点应用数据通过virtio的buffer发到共享内存区域收方收到后产生mailbox中断通知对方。这套机制里最关键的是“到底谁先启动、共享内存到底在哪里”这两个问题。通常在Zynq UltraScale MPSoC上我们要在Vitis的平台工程里预先把DDR里的一块区域保留给OpenAMP的共享内存和R5固件Linux起来之后知道自己不能踩这块区域。而Vitis里创建平台工程时也会在linker script里自动把R5代码、ELF固件放在保留区域内。这部分概念理解了后面配参数就不会瞎填。1.3 什么时候用OpenAMP什么时候别硬用虽然OpenAMP是好东西但我要先泼一盆冷水。如果你的R5核只是独立控制一个电机、跑一段固定逻辑不需要和任何其他核通信那真的不要硬上OpenAMP。框架的初始化会引入额外的启动时间占用的内存也比你裸写一个循环大得多而且还会引入一个在中断上下文里的调度实体。这种情况下传统的无系统裸机设计就够用了。反过来如果你的场景是需要和APU侧Linux交换大量数据R5_0和R5_1各自跑实时任务但需要协同将来可能要动态加载R5固件Linux侧做远程管理团队里有多个人协作通信协议需要标准化。那就不用犹豫OpenAMP就是这几类需求的默认解。还有一种折中的情况你在隔离模式下用RPU也就是R5完全独立于A53运行不使用Linux启动但两个R5核之间要互相通信这时OpenAMP裸机到裸机的模式就派上用场。Vitis里提供“OpenAMP echo-test”模板其实就是裸机到裸机的参考实现你完全可以在它的基础上改成自己的业务。这篇教程的主体部分我也是以这种模式为主线来讲的。2. 环境准备与工程创建要注意的细节2.1 硬件、工具链和基础知识的准备清单先把这一步说清楚因为很多人卡在“Vitis里选不到想要的platform”就是环境准备没做对。我们需要的东西如下表类别具体选项说明开发板Zynq UltraScale MPSoC系列ZCU102、ZCU106、或者自己画的板子只要芯片是ZU就行流程一致调试器板载JTAG或外部USB-JTAG用于下载程序和调试工具Vitis统一IDE2022.1及之后版本都适用本文以2023.1为例集成了平台工程、应用工程、调试、终端依赖如果要用Linux侧需要PetaLinux构建的映像如果不跑Linux纯裸机则不用PetaLinux本文以裸机对裸机为例不需要PetaLinux文档ZU TRMUG1085、Vitis平台文档UG1400查寄存器、查启动流程、查平台配置在开始之前你最好先确认手头的板子对应一个“xsa”文件硬件描述文件。这个文件由Vivado工程导出里面包含了PS配置、DDR信息、MIO/EMIO分配等。如果之前做过Vivado的硬件工程导出xsa后需要在Vitis里用它作为硬件平台的基础。你不需要在这个阶段启动Linux或加载任何系统Vitis 2023.1的流程是命令行打开vitis然后基于xsa创建平台工程就可以生成我们需要的standalone BSP和linker script。关于版本强烈建议直接用Vitis 2023.1或更新版本2020.1到2022.2流程都差不多但2023.x对RPU多核调度的支持更顺滑坑少一些。如果你还在用老版本SDK2019.2之前这套流程的菜单名称会略有出入但整体架构一样对照着改就行。2.2 在Vitis中创建Platform工程的完整步骤打开Vitis IDE后我们第一步不是急着写代码而是先创建一个Platform工程。这个工程相当于给整个R5裸机运行环境打了一个“地基”。具体路径是File - New - Platform Project。填一个名字比如叫“mpsoc_platform”。之后在Platform Project界面选择硬件规格也就是导入之前准备好的xsa文件。这一步有一个地方很容易被忽略在“Operating System”那一栏一定要选择“standalone”并且在“Processor”列表里勾上“psu_cortexr5_0”和“psu_cortexr5_1”。如果你只勾了一个核后面OpenAMP多核应用就没法做了因为OpenAMP要求至少两个R5核里的固件都能被framework管理。创建完成后Vitis会生成一个平台的硬件对应关系。在平台工程里需要把R5核跑在“LockStep模式”还是“Split模式”选好。LockStep模式是两个R5核作为冗余同步运行逻辑上像一个核不能跑双核通信Split模式才是我们需要的两个核各自独立可以跑OpenAMP。这个选项在哪里选中平台工程下的“mpsoc_platform” - 右键 - Board Support Package Settings在“psu_cortexr5_0”这个域的Overview里能找到“Configuration”或类似的选项。不同版本名字略有不同但核心语义一致确认是Split模式。必须是Split模式选成LockStep后面OpenAMP的dual-core demo就编译能过运行直接卡住。接着平台工程需要生成。右键平台工程 - Build。等编译结束后你会看到生成的BSP里有standalone的库这些库是给R5用的驱动基础。2.3 平台工程的域配置与DDR保留区平台工程里还有一个容易被忽略的点DDR保留区。OpenAMP的裸机到裸机模式下R5代码需要放在一片DDR空间里这片空间同时要包含两个核的vector table、代码段、栈和堆以及OpenAMP通信用的共享内存。Vitis平台工程在生成时会根据xsa里的DDR配置自动生成一个linker script模板默认的frame地址和栈大小都不一定适合你。我的习惯是直接在平台工程里先查BSP生成的内存布局确认以下几点代码段基地址通常选在0x3E400000附近不同板卡略有差异以DDR映射为准堆大小给OpenAMP至少留32KB以上栈大小每个R5核至少16KB调试时建议32KB共享内存地址建议固定一个区域比如0x3ED00000长度2MB供OpenAMP的virtio buffer使用。这个共享内存区域很重要它不能和代码段、栈堆区域重叠。而且因为R5默认对DDR的cache策略是写回的OpenAMP底层库已经在libmetal中处理了缓存flush和invalidate你不需要自己加内存屏障但前提是“你用的OpenAMP库是Xilinx适配过的版本”。Vitis自带的BSP库函数就是这样够用。如果你在写自己的共享数据收发逻辑时额外自己开了一片共享内存那就千万要自己处理flush操作这是最常见的数据不同步问题来源后面我还会专门讲到。2.4 创建OpenAMP应用工程的两种姿势Platform建好以后创建应用工程就有两条路可以走一条是用Vitis官方模板另一条是手动创建一个空应用再手动添加OpenAMP库。新手我强烈建议先用模板跑通一次再去手工玩。模板创建方式File - New - Application Project。工程名填个“r5_openamp_demo”之类在平台选择时选刚才建的“mpsoc_platform”。之后会弹出一个“Available Templates”列表。往下翻找到“OpenAMP echo-test”模板选它。这个模板默认会创建两个domainpsu_cortexr5_0和psu_cortexr5_1每个domain一个应用工程一个作为“master”一个作为“remote”。模板里已经写好了共享内存初始化和收发回显的逻辑我们只需要构建、下载、看串口输出就能验证整个通信链路。手动创建的方式适合你已经完全掌握了模板逻辑想写自己的业务代码。流程是先建一个空应用Empty Application然后在BSP设置里勾选open_amp库和libmetal库。再手动配置两个核的linker script最后自己写main函数、注册端点和回调。这条路麻烦一些但可控性高。综合来看“模板跑通”和“手动自建”这两步都要走模板的作用是验证环境手动的作用是理解原理。你在项目里最终一定是手工搭的工程。3. 核心实操多核R5裸机OpenAMP的完整搭建有了基础概念和环境接下来就到了动手环节。我会带你过一遍从工程配置到代码烧录的完整过程。这一节是全文的重心建议你照着做的时候把每个选项都看清楚再点下一步尽量别跳。3.1 创建“空应用OpenAMP库”的工程骨架我们先不用模板直接按手工搭建的路径走一遍这样你对整个链路更有把握。第一件事创建空应用工程。File - New - Application Project在其他选项里选择我们之前建好的“mpsoc_platform”Processor选“psu_cortexr5_0”模板选“Empty Application”。工程名可以叫“r5_0_app”。同一个操作再做一次只不过Processor选“psu_cortexr5_1”工程叫“r5_1_app”。这样我们得到两个独立的应用工程分别对应两个R5核。接着要让每个应用都包含OpenAMP库。选中r5_0_app下的BSP包也就是“psu_cortexr5_0_bsp”右键-Board Support Package Settings。打开之后在“Libraries”这一栏里勾选open_amp和libmetal两个库。这个地方很多文档没写细如果只勾了open_amp而没勾libmetal编译的时候会报一堆“找不到metal/xxx.h”的错因为OpenAMP依赖libmetal来访问物理内存、注册中断、映射寄存器。同理为r5_1_app也把这两个库勾上。这个环节我建议你先编译一版空工程确保BSP本身没有问题。如果你在这里就报错了多半是前面的platform生成有问题或者xsa的硬件配置和BSP不匹配。先不要继续往下写业务代码把基础环境整干净了后面才不会抓狂。3.2 linker script的正确配置代码段、堆栈和共享内存构建空工程通过后接下来要动内存映射了。每个R5核的linker script决定它的代码放哪里、栈堆放哪里。这里只讲最关键的部分。双击“r5_0_app/src/lscript.ld”打开。你会在文件里看到这样几个regionMEMORY { psu_ddr_0_MEM_0 : ORIGIN 0x3E400000, LENGTH 0x10000000 psu_r5_0_atcm_MEM_0 : ORIGIN 0x0, LENGTH 0x10000 psu_r5_0_btcm_MEM_0 : ORIGIN 0x20000, LENGTH 0x10000 psu_r5_1_atcm_MEM_0 : ORIGIN 0x0, LENGTH 0x10000 psu_r5_1_btcm_MEM_0 : ORIGIN 0x20000, LENGTH 0x10000 }这个文件是Vitis根据平台配置自动生成的。R5核有个特性它有TCMTightly Coupled Memory访问延迟低适合放中断向量表和对时间敏感的代码。但TCM空间很小每个R5只有64KB左右的ATCM和64KB的BTCM放不下整个应用。所以实际项目里我们通常把代码丢到DDR里跑把TCM留给中断向量表或者实时性要求最高的那部分代码。DDR地址0x3E400000不是随便来的。如果你用的是ZCU102DDR通常映射在0x00000000到0x7FFFFFFF实际上要看xsa但R5的独立地址空间里我们特意把图像基址设到0x3E400000区域是为了避开低地址处的ATCM保留区以及可能留给安全启动代码的区域。不同板卡这个值略有不同你可以打开Vitis生成默认linker script看它默认的ORIGIN通常就是可用区域。接下来要给r5_0_app和r5_1_app分别设置不同的内存起点千万不能重叠。比如r5_0_app代码放在0x3E400000栈堆放在0x3E400000之后的区域r5_1_app代码放在0x3EC00000栈堆再往后排共享内存保留区0x3ED00000到0x3EF00000作为OpenAMP的接收和发送缓冲区。两个核的运行地址重叠后烧进去会发现直接跑飞或者经常出现奇怪的内存SDRAM段错误。分配内存的原则是宁多勿少但别重叠。宁可把共享内存区划大一点也比跑起来数据错乱强。3.3 深入linker script调整示例打开r5_0_app的lscript.ld把MEMORY区域改成类似这样MEMORY { psu_ddr_0_MEM_0 : ORIGIN 0x3E400000, LENGTH 0x800000 psu_r5_0_atcm_MEM_0 : ORIGIN 0x0, LENGTH 0x10000 psu_r5_0_btcm_MEM_0 : ORIGIN 0x20000, LENGTH 0x10000 psu_r5_1_atcm_MEM_0 : ORIGIN 0x0, LENGTH 0x10000 psu_r5_1_btcm_MEM_0 : ORIGIN 0x20000, LENGTH 0x10000 }注意r5_0_app的linker script里其实不必包含r5_1的TCM但Vitis模板会自动带出来我们只需关注DDR区域即可。把r5_1_app的DDR起点改成0x3EC00000长度0x1000001MB。栈和堆的大小建议至少都设成0x800032KBOpenAMP在初始化时可能会跑一些递归和回调太小的栈会触发栈溢出症状非常难查。共享内存是在application代码里通过宏定义指定的不是linker script直接指定的。但你要在linker script里预先保留这段地址空间防止编译器把变量分配到那里。最稳妥的做法在linker script里补一段保留区域或者在代码里用__attribute__((section(.sh_mem)))把共享内存变量放到指定section。这里推荐后者更直观。3.4 双核R5的代码设计master和remote的分工OpenAMP裸机对裸机的典型模型是这样r5_0作为master负责初始化OpenAMP框架、注册通知中断然后启动r5_1r5_1作为remote启动后也注册OpenAMP然后两个核就处于对等状态通过rpmsg互相发消息。启动顺序有讲究必须先启动master等master端把共享内存和virtqueue初始化好再让remote接入。如果remote先跑会发现自己要连接的共享内存区域还是空的初始化直接失败。在实际工程里我们会把整体代码分成两个部分r5_0_app的main初始化UART打印调试信息初始化OpenAMP调用open_amp库的初始化API轮询等待消息收到消息后原样回传echo。r5_1_app的main初始化自己的UART初始化OpenAMP库此时不需要再创建新的共享内存直接连接已有的共享内存然后进入消息等待循环。核心的API调用大致是这几个metal_initlibmetal的运行环境初始化配置物理地址映射、缓存策略等open_amp库的初始化建立endpoint和传入回调函数传入一个共享内存描述结构体包括内存地址、长度、vuq数量等启动后在一个循环里处理收到的消息回调。每个人写的代码细节不完全一样但骨架一定是上面这几步。如果你是用模板建的工程模板代码里已经把这些都封装好了你只需要修改业务回调函数即可。3.5 完整代码示例一个能在R5上跑通的echo程序我不会贴出几千行代码但我会给出一个精简到不能再精简的可运行骨架并说明每部分作用。在实际板子上你可以在模板代码基础上替换这个main函数。下面这个是r5_0_app的main骨架示意#include metal/sys.h #include metal/device.h #include metal/io.h #include openamp/open_amp.h #include platform_info.h #include rsc_table.h static struct metal_io_region *shm_io; static struct metal_io_region *rsc_io; static struct hil_proc *proc; static struct rpmsg_endpoint lept; static int shutdown_req; static int err; static void rpmsg_read_cb(struct rpmsg_endpoint *ept, void *data, size_t len, uint32_t src, void *priv) { (void)src; (void)priv; /* 将收到的数据原样回传 */ rpmsg_send(ept, data, len); } int main(void) { struct rpmsg_device *rpdev; struct rpmsg_channel_info ch_info {.name rpmsg-openamp-demo-channel, .src 0, .dst 0}; unsigned int i; /* step 1: 初始化串口和基础驱动 */ uart_init(); /* step 2: 初始化libmetal */ metal_init(); /* step 3: 构建proc资源表 */ rsc_io metal_io_get_region(); shm_io metal_io_get_region(); proc hil_proc_create(shm_io, rsc_io, rsc_table, NULL); /* step 4: 启动远程处理器 */ hil_proc_start(proc); /* step 5: 创建endpoint */ rpdev hil_proc_get_rpmsg_device(proc); rpmsg_create_ept(lept, rpdev, ch_info, rpmsg_read_cb, NULL, NULL); while (!shutdown_req) { /* 循环等待由中断驱动的回调来处理消息 */ } return 0; }这一段代码里hil_proc_create和hil_proc_start是Xilinx封装好的底层函数它们内部处理了virtio设备的创建和远程核的启动。你在自己工程里直接用即可不需要深挖每个细节。重要的是要明白端点回调函数rpmsg_read_cb里做了一件事把收到的数据原样发回去这就是一个最基础的echo服务。我们后面验证OpenAMP链路是否通就是靠这个echo。r5_1_app的main就要简单很多核心就是连上共享内存、注册回调、等待消息static int rpmsg_endpoint_cb(struct rpmsg_endpoint *ept, void *data, size_t len, uint32_t src, void *priv) { (void)src; (void)priv; rpmsg_send(ept, data, len); return 0; } int main(void) { metal_init(); /* 直接初始化openamp连接共享内存 */ struct rpmsg_device *rpdev platform_open(); struct rpmsg_channel_info ch_info {.name rpmsg-openamp-demo-channel, .src 0, .dst 0}; struct rpmsg_endpoint ept; rpmsg_create_ept(ept, rpdev, ch_info, rpmsg_endpoint_cb, NULL, NULL); while (1) { metal_poll(); } }需要注意一点这里如果你是Cubexsa里配置两个R5核都使能了r5_1的固件是单独编译、单独放在某个DDR地址上然后由r5_0的代码或者Linux侧的rproc来load并启动的。在我们的纯裸机流程里最常见的方式是通过调试器把两个elf分别load到地址然后同时运行或者由master核调用r5_1的运行地址跳转指令来启动remote。Vitis调试器里提供了多核运行按钮我们后面会讲。3.6 编译、连接与ELF固件的生成代码写好之后直接在Vitis里选中两个应用工程分别Build。这里要强调一点r5_0_app和r5_1_app必须分开编译各自生成独立elf文件名字一般默认是r5_0_app.elf和r5_1_app.elf。编译中遇到最常见的报错有以下几类找不到openamp/header文件说明BSP里没有勾选open_amp库或者勾选后没有重新生成BSP。在Vitis里勾选库以后最好右键BSP包-Re-generate BSP Sources再重新编译。重复定义两个核共享了同一个源文件里的变量这通常是因为创建的“Application Project”里不小心把两个核的工程建立在了同一个cproject下导致编译系统重复包含源文件。解决办法是干净地建两个独立的工程目录。链接时内存溢出linker script里给DDR的长度不够尤其加了OpenAMP后代码段和BSS段会明显变大。解决办法是把LENGTH调大一点或者把优化等级从-O0调成-O2。构建成功后你会得到两个.elf文件。这是需要烧录到板子上的固件。但这里有一个关键点如果由master核来启动remote核你需要将r5_1_app.elf转成数组或者放到一个r5_1能访问到的地址然后由master核的代码在运行时加载到指定位置。Vitis模板里其实已经做了这一步模板的r5_0工程会自动加载r5_1的elf到对应的DDR地址再启动它。但如果你是自己手工建的工程就要注意了千万别让两个els单独下载到各自地址然后手动同时运行这种方式虽然能跑通但没法模拟上电后自动运行的场景。生产环境中最终要把两个elf打包到BOOT.bin。在Vitis里可以通过“Create Boot Image”来做添加工件类型时左边的分区类型选择“Bootloader”右边加入r5_0_app.elf作为“partition”把它配置成“r5_0_image”再添加r5_1_app.elf作为另一个partition。分区顺序和地址要严格按照linker里的地址来设置。这个环节做错启动时就会卡在Loading阶段。3.7 加载运行与串口验证程序烧录好后上电或者按一下复位键串口上应该能看到打印信息。以ZCU102为例我们用USB-UART接到PS的UART0波特率通常是115200。如果一切正常r5_0核的串口会打印一些OpenAMP初始化日志然后创建一个rpmsg通道r5_1核也会打印类似的连接成功信息。这时如果r5_0侧的程序里写了echo测试逻辑它会在初始化完成后自动测试发一条消息给r5_1r5_1收到后再回传r5_0收到回传后打印“echo test passed”类似信息。如果串口上没有输出或者卡在某一步先别急着怀疑代码先从这几个地方排查是不是两个核的启动顺序反过来了OpenAMP协议要求master先启动并初始化共享内存remote后启动去连接。是不是linker script里共享内存地址在r5_1侧没保留如果r5_1的代码段覆盖了共享内存的地址代码一运行就把内存踩了。是不是没有调用metal_init这个函数缺失时地址映射没建立回调函数里的数据访问全部走不通。串口信息是验证链路最直接的手段。先让串口输出完整、规范后面写日志都会方便得多。4. 调试技巧与疑难问题排查实录说实话OpenAMP在ZU上面的移植已经比较成熟了真正跑不起来的场景大多不是框架本身的问题而是工程配置、内存布局、缓存策略这些外围细节。我这里把我在实际项目中踩过、也帮别人排过的问题集中整理一下每个问题的排查思路都值得记下来。4.1 R5核没跑起来先查启动模式和JTAG连接如果你发现调试器根本连不上R5核或者连上了但PC指针一直在0先把启动模式跳线确认一下ZCU102上拨码开关状态决定了BOOT_MODE是JTAG还是SD卡或QSPI。在纯Vitis调试环境下我们默认板子应该保持在JTAG模式。如果板卡从QSPI启动了R5可能已经被固化程序占住你再去下载就会冲突。第二个可能是JTAG链上挂了多个设备调试器默认枚举到了A53而不是R5。在Vitis的Debug Configuration里手动选择target把Cortex-R5 #0和#1都勾上。这里的target列表如果像“FPD”里的“APU”和“RPU”同时存在选择时注意别选错。第三个可能比较隐蔽电源域。R5核所在的FPDFull Power Domain在低功耗模式下可能没有上电导致调试器无法访问RPU寄存器。这种情况下需要在调试前先初始化PMU或者通过Vivado硬件管理器确认FPD状态。我在ZCU102上遇到过解决方式是换一个平台BSP使能RPU所在的电源域。4.2 编译通过、运行死循环常见的内核配置隐患编译过了跑起来进了while循环但就是没有回调触发这是OpenAMP调试中最常见的情况。这时候我一般按顺序检查这几个点:第一共享内存地址是否一致。很多新手在master端定义一个宏SHM_ADDR在remote端又自己编了一个地址两个地址不一样等于一个在写信一个在别人房里等信永远等不到。排查办法在两边初始化时都打印出共享内存的虚拟地址和物理地址确认它们映射的是同一段物理内存。R5的DDR直接映射虚拟地址等于物理地址这里比较容易对比。第二中断号是否正确。OpenAMP通信依赖mailbox中断而R5核之间以及R5和A53之间的中断连接方式不同。在纯R5裸机对裸机模式下中断路径比较简单但要确认你的代码里是否调用了Xilinx的中断初始化函数把OpenAMP需要的mailbox中断号注册到GIC上。如果在rtos下开发这一步往往由驱动控制裸机下就必须自己写。模板代码里这一部分是齐全的手工建工程最容易漏。第三是缓存一致性问题。R5默认对DDR是写回write-back策略如果没有做cache flushmaster写进共享内存的数据可能还滞留在L1 cache里remote读的时候读的是老数据。OpenAMP的libmetal调用中正常的收发路径上有cache操作。但如果你自己加的私有共享变量没经过OpenAMP层那就要小心了。解决办法有二要么你在相关位置调用Xil_DCacheFlush和Xil_DCacheInvalidate要么把共享内存区域的缓存策略改为非缓存通过xil_set_tlb_attributes来配置。这块在Xilinx论坛上是问得最多的点一点都不夸张。4.3 串口异常与乱码的处理思路如果你用的是默认的uart驱动但打印出来全是乱码先查波特率是否匹配115200、8N1常见ZCU102板载UART默认。再看串口线是否接到正确的UART口ZU有多个UART不是哪个都能出调试信息。最后看R5核的时钟配置特别是EMIO或MIO分配的UART引脚是否被别的外设占用这个用Vivado里的HW manager看一眼最直接。如果打印信息半截中断那可能就是两个核都在抢同一个UART输出。R5_0和R5_1各自初始化了UART用的是同一物理串口两边的打印就会交错。解决方式是主核的日志走UART0从核的日志走UART1或者从核直接把日志通过OpenAMP发给主核由主核统一输出。这个方案在复杂项目里很实用你可以把远程的异常信息通过rpmsg通道回传这样即使R5_1死机了主核也能感知到。4.4 常见问题速查表现象可能原因处理建议编译报找不到openamp头文件BSP未勾选open_amp/libmetal库在BSP Settings里勾选库并重新生成BSP链接时报DDR区域溢出linker script分配内存不够调大MEMORY的LENGTH或降低打印信息级别下载elf后R5 PC始终为0JTAG连接不对或R5上电未完成检查启动模式、电源域、调试器target选择运行后无任何串口输出UART未初始化或引脚配置错误检查BSP中UART基地址和MIO配置确认波特率日志停在同一行不再刷新卡在等待接收消息或初始化阻塞单步调试查看PC位置和寄存器状态数据能收到但全是乱码缓存一致性问题增加flush/invalidate操作或配置共享内存为非缓存区域OpenAMP初始化失败返回错误共享内存地址或中断配置错误打印rsc_table地址、shm地址确认两端一致两个核代码都跑但互相收不到启动顺序反了确保master先初始化remote端再连接4.5 Vitis调试器的正确使用姿势最后聊一下调试器因为很多人抱怨Vitis调试R5核特别不顺问题多半出在不会正确配置launch。右键工程 - Debug As - Launch on Hardware默认会连到全部可用的target。这里要注意的是如果你在“Debug Configuration”里没有专门添加多个目标调试器可能只会运行当前焦点所在的核。在打开的配置页面里Target选项卡里勾选psu_cortexr5_0和psu_cortexr5_1两个核在“Program”选项卡里分别指定两个核对应的elf文件路径。然后再点Debug调试器会把两个elf分别下载到对应内核并同时开始运行。整个流程里最容易出错的点是在一个核的调试配置里误把另外一个核的elf也填了上去造成地址冲突。所以每次新建调试配置时务必检查每个核对应的Program栏。5. 深入OpenAMP的API与工作原理5.1 虚拟IO与资源表底层通信模型如果你只停留在“会用模板跑通”这个阶段那这篇教程的深度显然不够。我建议大家在跑通demo后再花一点时间了解OpenAMP底层是怎么设计的这样以后遇到问题才有能力自己排查。OpenAMP的核心通信机制建立在virtio标准上。Linux虚拟机里的virtio协议把“前端驱动”和“后端设备”之间的数据通过共享内存中的环形缓冲区virtqueue传递。OpenAMP把同一概念从虚拟化世界搬到了异构多核世界一个核通常是有Linux的核作为virtio的host端另一个核裸机R5作为device端两者通过一组virtqueue交换数据。初始化时remote核要提供一个“资源表”resource table里面描述了共享内存里有哪些virtqueue、每个virtqueue的地址和深度等。master核在启动remote之前就已经把资源表翻译好、把共享内存地址映射好。这个资源表在Vitis的编译过程中已经通过rsc_table.c文件生成了模板工程里自带手工建的工程需要从模板里复制或自己写。5.2 rpmsg通道与端点注册OpenAMP之上还包了一层rpmsgRemote Processor Messaging协议。它的核心概念是“通道”channel和“端点”endpoint。一个通道可以理解为两个核之间一条逻辑通信链路端点是通道两端的收件箱/发件箱。每个通道有个文本名比如“rpmsg-openamp-demo-channel”只要两端名字对得上就能通信。在你的代码里注册一个端点需要做一件事指定通道的字符串名、指定回调函数。之后当你调用rpmsg_send时底层会从virtqueue里分配一块buffer把数据copy进去然后触发对方的中断。对方在中断上下文里调用它的端点回调取出数据。整个过程对开发者来说就是一个“send”加一个“receive”回调非常直观。有一点要注意rpmsg_send是异步的。你调完send不代表对方已经收到甚至数据可能还在cache里没写入主存。如果需要确认对端已经处理完需要用到rpmsg_trysend的返回值或者让对端再发一条确认消息。这个语义在实际业务中会引发很多隐蔽Bug设计协议时可以约定所有request都要求回一个ack再继续下一步。5.3 libmetal的作用与缓存管理的坑libmetal是OpenAMP的底层抽象库它处理三件事物理内存的映射mmap、寄存器的访问ioread/iowrite、缓存操作flush/invalidate。对R5裸机来说因为地址是直接映射的内存映射这部分基本是透明的寄存器访问也可以直接用指针但缓存操作这一块是大多数人忽略的健康杀手。举个我遇到过的例子master核向共享内存写入一组传感器数据长度为几百字节写完调了rpmsg_send触发remote中断。remote核收到中断后从共享内存读数据却读到了旧值。原因就是master核的DDR写缓存尚未被flushCPU只是把数据写入了cache line还没回写DDR。解决起来也不复杂在rpmsg_send之前手动调用metal_cache_flush(shm_io, (unsigned long)buffer, len)remote读之前调用metal_cache_invalidate。Vitis的OpenAMP库正常路径上已经做了这些操作问题通常出在你“绕过框架”自己读写共享内存的私有区。这时候只能靠自己加固。一个更彻底的办法就是把这部分内存属性配置成“device”或“non-cacheable”一劳永逸避免缓存问题代价是读写性能下降、无法利用cache加速。在实时控制场景里通常只把需要高频通信的那一小段内存做成非缓存的其他数据还是走缓存策略。5.4 echo-test模板源码精读在官方“OpenAMP echo-test”模板里核心文件分布在几个目录中包括platform_info.c/h定义平台相关的地址信息共享内存地址、资源表地址等rsc_table.c/h资源表定义包含virtio设备配置和vring信息main.c应用入口调用open_amp初始化并处理循环openamp的BSP适配层由Xilinx在BSP里封装好的hil_proc相关API。我建议你在跑通后读一遍rsc_table.c里面定义了vring的数量、大小、地址对齐。这些参数在性能调优时有大用。比如默认vring buffer可能就几千字节如果你要一次传大数据包把vring size调大或者把数据拆成多个小包。另一个值得读的是platform_info.c里面的shm地址如果和你的linker script不一致你会看到一大堆莫名其妙的错误。这块是自定义工程时最容易出偏差的地方。我一般会在代码最开始加一段断言检查共享内存地址是否在linker script定义的范围内如果不在直接报错挂死避免带着错误地址继续跑。6. 进阶到一个更实用的多核通信Demo跑通echo-test只是第一步项目里真正需要的往往是一个能双向传数据、带线程保护、甚至要配合DMA来搬运数据的通信库。在这节里我分享一个官方案例之外的进阶实践用来展示怎么用OpenAMP做一个小型数据总线。6.1 自定义消息协议的设计从echo到真实业务假设我们的实际需求是R5_0负责电机控制周期1kHzR5_1负责视觉识别或传感器采集周期100Hz。两个核需要以明确的格式交换控制指令和状态数据。这时我们不再用rpmsg那套“任意字符串”的方式而是定义一个结构体typedef struct { uint32_t magic; uint32_t cmd_id; uint32_t seq; uint32_t payload_len; uint8_t payload[256]; } app_msg_t;在R5_0和R5_1的工程里都包含这个结构体定义然后端点回调里做一次强制类型转换就拿到了业务数据。这比直接用strings传递更严谨也能利用编译器的类型检查来发现错误。消息协议设计里最容易被忽略的是对齐和字节序。R5核是小端模式A53也是小端通常不需要考虑字节序问题。但结构体对齐需要明确可以用#pragma pack(push, 1)强制紧凑排列避免不同编译选项下成员对齐不一致导致解码错位。6.2 中断驱动的收包模型与主循环的平衡OpenAMP裸机接收消息有两种模式一种是中断回调数据一到立刻进入回调函数处理另一种是轮询模式在没有中断的环境下不断扫描virtqueue里的新数据。在R5上我们通常使用中断模式因为R5有GIC可以响应SPI中断。但中断模式有一个隐患如果你的回调函数里做了太多工作比如调用printf、处理大数组、甚至做浮点运算中断上下文里耗时过长会直接影响双核通信的实时性甚至触发其他中断超时。实际项目中我的做法是回调函数只做一件事——把收到的数据拷贝到一个环形缓冲区然后置一个标志位真正的业务处理放在主循环里完成。这样可以保证回调尽可能短主循环在“处理业务”和“读取新消息”之间交替。你可能会问这样主循环会不会漏掉消息只要环形缓冲区足够大且在拷贝时关闭了对应的中断优先级一般不会丢。我们可以用双缓冲的形式进一步降低被新消息覆盖的概率。6.3 性能验证与通讯时延测试辛辛苦苦搭好通信链路你还得知道它跑多快。最简单的方法是在发送端记录一个时间戳接收端收到后立刻发回一个回应发送端再记录一次时间戳除以2就是近似往返时延的一半因为消息序列和回传路径相同。在R5上获取时间戳可以直接读取全局计数器global timer在Zynq UltraScale MPSoC上这个计数器频率通常是100MHz换算精度10ns。实际测下来R5之间的rpmsg消息传输时延在cache flush开启的情况下典型值大概在几微秒到十几微秒级别跟消息大小以及cache flush带来的惩罚有关。如果你发现时延明显过高排查方向往往不是OpenAMP本身而是你加的printf、日志输出等副作用。吞吐量测试稍微复杂一点需要持续发大包。可以压一个512字节的消息测一段时间内完成多少次往返。注意不要单次发太大超过vring buffer size就得拆包了。测出来的有效吞吐量对后续业务拆分很有参考价值。7. 项目落地前的最后一次代码审查代码能跑通、时延也能接受了别急着庆祝。我在交付给产线之前一定会过一遍下面的问题清单每一个都是实际踩过的教训。7.1 检查系统的启动与复位行为你的设计要回答这几个问题两个R5核是同时上电启动还是由APU侧Linux动态加载如果是同时启动哪一个先完成初始化如果启动时remote核还没就绪master会不会一直等导致阻塞在裸机对裸机模式下最简单安全的策略是让master初始化OpenAMP后主动延时一小段时间比如200ms再开始发第一个消息给remote一个充足的准备时间而不需要等待逻辑。延时在启动阶段完全可接受复杂度几乎为零。另一个要注意的是软复位。当R5_1因为异常发生复位后它连接共享内存的上下文信息已经没了如果R5_0还在用旧地址发消息就会导致消息丢失。最佳实践是R5_1在启动时给它和R5_0之间的通信通道定义一个新的端点和通道名然后主动发送一个“re-register”的消息给R5_0让R5_0更新它本地维护的端点信息。7.2 资源表与共享内存的定期巡检建议生产环境不会像开发环境一样稳定DDR的位翻转、电源毛刺等问题在高压、高温的工况下可能偶发。OpenAMP本身没有提供内存自检机制所以我一般在主循环里做周期性的共享内存校验每隔1秒或10秒周期性地把一个固定模式的校验值写入共享内存然后远端读出来校验不一致就报错。这种廉价的“hello包”能让通信链路的健康状态随时可见。同样在监控系统里如果单片机侧发生了看门狗复位web后台应该能看到状态变化。这需要两个核把自己的状态周期性地写到共享内存的固定位置APU侧的Linux应用负责读出来展示。这些都是很小的工程细节但做与不做项目的稳定性和可维护性完全是两个级别。7.3 日志策略最后提一个大家不爱重视、但特别影响调试效率的点日志策略。因为R5核通常只有一个调试串口两个核争抢UART输出会互相干扰。我的建议很简单把系统里所有日志统一走OpenAMP通道汇聚到master核由master核统一UART输出。这样从核日志不会在物理串口上乱插系统的时序行为更可控而且可以轻易地在主核侧根据日志级别过滤不让低级别debug信息干扰高层数据。8. 从这一套流程里我学到的几件小事写到这里这篇教程的主体内容已经讲得差不多了。最后说几句不算总结的题外话纯粹是我个人在实际项目里熬出来的体会。第一很多开发者一上手就追求自己写通信协议觉得OpenAMP是“多余的重量”。但在异构多核的场景里真正难的不是“把数据从一个核发到另一个核”而是“让整个链路在所有异常情况下都保持可预期”。OpenAMP最大的价值不是你省了几百行协议代码而是你获得了端到端的确定性共享内存布局是标准的、中断处理路径是标准的、生命周期管理是标准的。这些标准意味着后来接手的人不需要重新理解一遍你的“私有协议”这对一个活着的项目来说太重要了。第二缓存一致性是你迟早要补的课。凡是做过多核通信的人十有八九都栽在cache上。建议不管你用不用OpenAMP都抽出半天时间认认真真读一遍你所用CPU的cache文档搞清楚write-back意味着什么、什么时候要flush、什么时候要invalidate。这件事花掉的时间会在你将来排查诡异bug时数十倍地赚回来。第三不要害怕读OpenAMP的源码。BSP里引用的是Xilinx适配过的open-amp和libmetal它们体积不大核心代码甚至不到几千行。花两天时间把rsc_table、virtqueue的初始化流程、以及endpoint回调的触发路径过一遍之后你搭出来的系统会明显比别人多一分从容。这一套R5裸机OpenAMP的搭建方法本质上和芯片具体型号关系不大你吃透一个平台转其他平台也就一两周的适应时间。如果你在自己的板子上照着这个流程走了一遍但卡住了不妨回头看看第4节的问题速查表大部分问题都在那里了。
返回列表