ARTICLE DETAIL

资讯详情

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

ZYNQ双核通信:基于OpenAMP的Linux+FreeRTOS异构多核开发指南

ZYNQ双核通信:基于OpenAMP的Linux+FreeRTOS异构多核开发指南 1. ZYNQ双核通信到底解决什么问题做嵌入式开发的老哥们应该都有这种经历产品功能一多单核处理器就有点喘不过气。这边要跑人机交互和网络协议栈那边还要实时响应电机控制、数据采集你争我抢稍不留神就出现任务超时。ZYNQ这颗芯片出现在视野里的频率越来越高就是因为它把ARM Cortex-A9双核和FPGA可编程逻辑放在同一颗芯片上天生就是为这种“既要又要”的场景准备的。ZYNQ-7000系列内部有两个ARM Cortex-A9硬核处理器这是它最值钱的地方之一。很多初学者一开始只用了其中一个核另一个核闲着没事干这在小项目里没问题但等你做的产品稍微复杂一点比如要做EtherCAT主站、要做视觉引导、要跑算法又要实时控制的时候单核会非常吃力。这时你就会想能不能让CPU0跑Linux处理人机界面和网络协议让CPU1跑FreeRTOS处理实时任务这就是异构双核通信的典型应用场景。但问题是两个核各跑各的系统怎么让它们协同工作怎么互相传递数据这就需要一套成熟的通信机制而在ZYNQ平台上OpenAMP就是目前最主流的解决方案。这篇文章我基于2018.3版本的工具链从零开始给你搭建一套完整的OpenAMP开发环境把LinuxFreeRTOS的双核通信跑通。2018.3虽然是好几年前的版本了但整个架构思路和配置方法放到现在依然适用很多老项目的维护也还在用这套流程。适合刚接触ZYNQ双核开发的工程师也适合那些已经在用Linux单核、想扩展实时性的老手参考。2. 动手之前先把OpenAMP的底细摸清2.1 AMP与SMP的区别你想清楚了吗在做ZYNQ双核开发之前先得搞清楚SMP和AMP这两个概念的区别。SMP是Symmetric Multi-Processing对称多处理两个核对等对称共同管理一套操作系统Linux内核原生的SMP支持就是干这个的启动时两个核都跑Linux共同分担负载。AMP则是Asymmetric Multi-Processing非对称多处理两个核跑不同的系统各有各的分工。ZYNQ做双核通信玩的其实是AMP模式一个核跑Linux另一个核跑FreeRTOS或者裸机程序。两者独立运行通过共享内存和中断机制互相通信。Symmetric多处理系统SMP就像公司里两个能力相同的员工共同负责同一个项目彼此之间的协作紧密。而非对称多处理系统AMP更像是一个产品经理负责对外沟通协调一个技术专家负责具体技术攻坚各管一摊通过定期会议同步进度。ZYNQ上的双核通信本质就是让产品经理Linux核和技术专家FreeRTOS核各司其职通过高效的沟通机制协同工作。明白了这个区别你才能理解OpenAMP在这套架构中扮演的角色。2.2 OpenAMP和它的三个关键组成OpenAMP全称是Open Asymmetric Multi-Processing是Xilinx在Linux内核源码树里维护的一套开源框架专门解决异构多核通信问题。它不是一个单一模块而是一套组合方案主要由三部分构成remoteproc、rpmsg和libmetal。remoteproc负责远程处理器的生命周期管理。说白了就是Linux怎么把FreeRTOS的固件加载到CPU1上去、怎么启动它、怎么在需要的时候把它停下来。它有一套完整的状态机从固件验证到资源分配再到处理器启动都有标准化的处理流程。rpmsg是Remote Processor Messaging负责两个核之间的消息传递。它建立在共享内存和中断机制之上提供一种类似Socket的通信接口让应用层不需要关心底层共享内存的具体位置和中断的具体配置直接收发消息就行。libmetal则是一个硬件访问抽象层把共享内存、中断、IO操作这些东西统一封装让用户程序可以通过标准API访问底层硬件。这套组合拳打下来你在Linux侧写一个应用程序就可以像访问普通文件或者Socket一样和另一个核上的FreeRTOS进行通信了。2.3 2018.3版本的技术栈组成选择2018.3版本有它特定的技术背景。这个版本对应的Vivado是2018.3PetaLinux也是2018.3对应的Linux内核是4.14版本。这套组合是当时非常稳定的组合网上资料丰富踩坑后的解决方案也最容易搜到。如果你用的是更新的版本比如2020.2或2021.1整体流程类似但会有细节差异比如PetaLinux的配置命令、设备树文件的写法、OpenAMP驱动的加载方式都会有调整。我见过不少人在新版上折腾不出来退回2018.3反而很快跑通了。2018.3版本的核心组件清单如下表所示组件版本/说明Vivado2018.3用于硬件工程设计和FSBL生成PetaLinux2018.3用于Linux内核、根文件系统和设备树构建Linux内核4.14Xilinx维护的分支内建OpenAMP驱动U-Boot2018.01第二级引导程序FreeRTOSXilinx提供的移植版本基于9.0或10.0OpenAMP集成在Linux内核和设备树中不需要单独下载3. 搭建环境的完整实操流程3.1 第一大步用Vivado把硬件工程准备好在Vivado里新建一个基于ZYNQ的硬件工程芯片型号根据自己的板卡来选我用的是xc7z020clg400-1这颗芯片也就是ZYNQ-7020。如果你用的是其他型号操作完全一样不影响后续流程。硬件工程的关键点在于PS侧的配置。ZYNQ PS侧的配置中有几个配置项特别重要。首先是UART至少开启一个串口用来做控制台输出和调试。其次是SD卡接口这个必须有因为后面U-Boot、内核和设备树都要从SD卡引导。再就是DDR配置按照你的板卡实际DDR型号和容量来设置配置错了启动必挂。如果你想让Linux和FreeRTOS之间通过GPIO或者中断互通有无这一步也要预先在PS-PL配置里使能相应的EMIO或中断控制器。不过做基础的OpenAMP通信暂时用不到PL侧逻辑保持最简配置就行。生成硬件工程后导出硬件描述文件也就是那个.xsa文件或者.hdf文件。这个文件包含了整个硬件系统的描述信息后面PetaLinux和FSBL的生成都要依赖它。3.2 第二大步制作FSBL并将其添加到工程中FSBL是First Stage Boot Loader第一级引导程序它是ZYNQ启动流程中不可或缺的一环。ZYNQ的启动流程是BootROM从SD卡或QSPI中加载FSBLFSBL初始化DDR和时钟然后加载U-Boot或裸机应用程序。在Vivado里创建FSBL的过程是点击菜单栏的File - Export - Export Hardware导出硬件描述文件然后File - Launch SDK打开SDK开发环境。新建一个Application Project模板选择Zynq FSBL编译生成elf文件。这个步骤有个容易被忽略的坑生成FSBL时一定要确保硬件描述文件中的配置是正确的特别是DDR型号和大小。如果DDR配置错了FSBL初始化内存时序不对后面U-Boot起不来还是小事FreeRTOS加载到DDR里跑飞了排查起来特别闹心。我调试的时候遇到过类似情况那时候用的是MT41K256M16HA-125颗粒时序参数缺了复位配置结果FSBL起不来。后来换了型号才跑正常。在生成FSBL时必须检查DDR型号和参数是否完整。FSBL工程的源码可以在SDK中查看它会根据硬件描述文件自动生成初始化代码一般不需要手动修改。3.3 第三大步通过PetaLinux构建Linux侧系统PetaLinux是Xilinx提供的嵌入式Linux开发套件用起来比直接手动配置内核要省心很多。创建一个PetaLinux工程然后导入上一步导出的硬件描述文件。petalinux-create -t project --name zynq_amp_demo cd zynq_amp_demo petalinux-config --get-hw-description/path/to/export/hw配置界面出来后重点检查两处一是Image Packaging Configuration下选择SD卡启动方式二是确定根文件系统类型我一般选ext4方便调试也方便直接挂载读写。然后需要进行内核配置确保OpenAMP相关驱动被编译进内核。虽然4.14内核里默认带了这些驱动但仍需检查确认一下。petalinux-config -c kernel在内核配置菜单中依次检查以下选项是否开启Device Drivers - Remoteproc drivers - ZYNQ_REMOTEPROC Device Drivers - RPMSG drivers - RPMSG_VIRTIOZYNQ_REMOTEPROC选项负责管理CPU1的生命周期RPMSG_VIRTIO则是rpmsg通信的核心驱动这两个都必须编译进内核不能只编译成模块否则后面加载顺序会出问题。接下来配置设备树。设备树在OpenAMP通信中起着关键作用它告诉Linux内核CPU1的固件放在哪里、共享内存基地址在哪里、使用哪个中断号。2018.3版本的PetaLinux设备树文件位于项目目录下的subsystems/linux/device-tree目录中也可以直接修改project-spec/meta-user/recipes-bsp/device-tree/files/system-user.dtsi。/ { reserved-memory { #address-cells 1; #size-cells 1; ranges; rproc_0_reserved: rproc3e800000 { compatible shared-dma-pool; no-map; reg 0x3e800000 0x1000000; }; }; remoteproc0: remoteproc0 { compatible xlnx,zynq-remoteproc-1.0; firmware freertos.elf; vring0 0x3e800000; vring1 0x3e804000; reg 0x3e808000 0x10000, 0x3e880000 0x10000; }; };这块讲几个细节。reserved-memory节点的作用是给CPU1预留一块物理内存Linux内核在启动后不会使用这块区域避免两个系统踩踏。我预留的是从0x3E800000开始的16MB区域如果只做简单的消息通信这个大小足够了你要是跑更复杂的算法可以再调整。vring0和vring1是virtio ring的共享内存区域这两个地址必须在预留内存范围内。reg属性定义了两块区域第一块是CPU1的入口地址第二块是共享内存的基地址这两块也要在预留范围内。配置好设备树还要把编译好的FreeRTOS固件拷贝到PetaLinux的根文件系统中。固件需要放到/lib/firmware目录下并命名为freertos.elf这样remoteproc驱动启动时就能找到它。petalinux-build petalinux-package --boot --fsbl zynq_fsbl.elf --fpga --u-boot --force编译完成后生成BOOT.BIN、image.ub等文件拷贝到SD卡中Linux系统就能启动了。3.4 第四大步编译FreeRTOS工程FreeRTOS侧的工程Xilinx提供了专门的移植版本可以在Xilinx的GitHub仓库中找到。下载后在SDK中新建一个Application Project选择FreeRTOS模板然后把下载的FreeRTOS源码整合进去。FreeRTOS工程的配置有几个关键点处理器必须选择Cortex-A9MPx1也就是只使用CPU1这一个核堆大小根据你的任务数量和消息大小来定建议至少16KB起步tick rate选择1000Hz保证实时调度的精度。设置好这些还需要编写OpenAMP的通信相关代码。FreeRTOS侧使用OpenAMP库需要把libmetal和open-amp的源码也加进工程里这个我会在第4节详细说。FreeRTOS工程编译后输出的elf文件就是后面要拷贝到Linux根文件系统里的那个freertos.elf。4. 双核通信代码实现细节4.1 RPMsg通道的建立与消息收发OpenAMP的通信核心机制就是rpmsg。使用流程跟Socket通信很像需要先建立通道然后收发数据。Linux侧的代码如下#include openamp/open_amp.h #include metal/alloc.h #include rpmsg_lite.h #define SHARED_MEM_POOL_SIZE (1024 * 1024) #define RPMSG_SERVICE_NAME amp-demo struct rpmsg_device *rpdev; struct rpmsg_endpoint *epend; static void rpmsg_read_cb(struct rpmsg_endpoint *ept, void *data, size_t len, uint32_t src, void *priv) { printf(received: %s\n, (char *)data); rpmsg_send(ept, ack from Linux, 15); } int main(void) { void *shared_mem; struct rpmsg_virtio_device *rvdev; struct metal_io_region *io; ... }这段代码的核心逻辑是初始化OpenAMP库和libmetal创建一个rpmsg端点注册收到消息后的回调函数然后进入主循环等待消息。这里需要提醒的是共享内存池的大小要根据你的业务消息最大长度来定不是越大越好太大浪费内存太小又会限制消息长度。FreeRTOS侧的代码结构类似也需要初始化OpenAMP并创建rpmsg端点然后在RTOS的一个独立任务中循环接收和发送数据。void vOpenAMPTask(void *pvParameters) { struct rpmsg_lite_instance *rpmsg_lite; struct rpmsg_lite_endpoint *my_ept; uint32_t src_addr; char buf[256]; rpmsg_lite rpmsg_lite_init((void *)SHM_BASE_ADDR, 0, 0, 0); my_ept rpmsg_lite_create_ept(rpmsg_lite, RPMSG_SERVICE_NAME, rpmsg_read_cb, NULL, src_addr); while (1) { // 任务循环按需发送数据 vTaskDelay(pdMS_TO_TICKS(100)); } }4.2 共享内存地址要理清楚共享内存是最容易出问题的地方。发生通信异常时十有八九就是地址没对上。安全的排查方式是把三处地址对照检查Linux侧设备树中定义的reg属性里的共享内存基地址、dts里reserved-memory的物理地址、FreeRTOS侧代码里的SHM_BASE_ADDR宏定义这三处必须一致。我在前面示例中用的是0x3E808000这只是个示例地址。你在实际项目中要根据自己的硬件布局来确定但原则是一致的。地址不对的表现也很有意思不是完全没反应而是偶尔能收到一两条消息然后就卡死了。因为Linux已经向那块区域写入数据但FreeRTOS读的是另一块内存区域读到的全是垃圾数据。4.3 中断通知机制的配置rpmsg通信除了共享内存还需要中断机制来通知对方有新消息到达。Linux侧通过remoteproc驱动配置好SPI中断FreeRTOS侧则需要初始化GIC并注册对应的中断服务函数。在设备树中需要通过interrupts属性指定中断号。ZYNQ的PS侧中断控制器是GICSPI中断号从32开始编号。Xilinx的OpenAMP例程通常使用SPI中断号61对应的硬件中断号是93也就是32加61。这里有个关键点中断号对不对从Linux的启动日志里基本看不出来一定要在FreeRTOS侧的中断服务函数里加上调试打印或者状态翻转否则中断是否触发完全无感。5. 启动流程配置与联调经验5.1 启动顺序为什么这么重要ZYNQ上LinuxFreeRTOS的AMP模式启动顺序是一个关键的考虑因素必须先启动Linux等Linux完全起来后再手动加载FreeRTOS固件。因为FreeRTOS的固件放在Linux的根文件系统中Linux没起来之前这个文件根本访问不到。另外FreeRTOS使用的共享内存区域需要Linux预留好如果Linux还没初始化完就启动FreeRTOS两个系统会争抢同一块内存区域。启动顺序确定了在操作上体现为系统启动后不会自动加载固件而是需要手动执行一条命令来加载。echo freertos.elf /sys/class/remoteproc/remoteproc0/firmware echo start /sys/class/remoteproc/remoteproc0/state第一条命令告诉remoteproc驱动使用哪个固件文件第二条命令触发固件加载和CPU1启动。执行成功后FreeRTOS就开始运行了两个核之间的通信也随之建立。5.2 如何验证双核通信已经打通通信跑通了没有不能靠肉眼猜需要有明确的验证手段。我通常分成三步来验证。先确认CPU1已经启动。执行以下命令看remoteproc的状态cat /sys/class/remoteproc/remoteproc0/state状态应该是running表示CPU1已经在运行FreeRTOS了。再验证共享内存可见性。在Linux侧用devmem命令直接读取预留内存区域的内容能看到FreeRTOS写入的固定模式数据就说明共享内存通路是通的。最后测试双向收发。在Linux侧运行一个测试程序周期性地向CPU1发送消息并在回调函数中接收CPU1的回应。在FreeRTOS侧的任务中收到消息后打印一串标识并通过rpmsg回复如果Linux侧能一直稳定收到回复且持续运行一段时间不崩溃说明双核通信基本稳定。5.3 联调过程中的关键调试手段联调阶段调试手段是省时的关键。我常用的思路是用串口打点、用共享内存做镜像观察区和检查Linux内核日志。两个核分别接一个串口是最好的方式CPU0的Linux控制台一个串口CPU1的FreeRTOS打印一个串口两边各自的运行状态一目了然。如果硬件上只有一个串口可以考虑用GPIO翻转加示波器观察关键节点的状态变化但比较费劲不如加个串口省心。Linux内核日志也是重要的参考来源加载固件和启动CPU1时内核会打印remoteproc相关的日志通过dmesg可以查看到底卡在哪一步。6. 这里有几个我实际踩过的坑6.1 固件加载失败提示文件不存在系统执行固件加载命令时提示找不到文件首先检查路径和文件名是否正确。注意固件需要放在根文件系统/lib/firmware目录下并且文件名要和echo命令里写的一致。很多文件系统镜像打包时会改变文件位置或权限可以先用ls确认文件确实存在同时确认这个内核带有固件查找路径的配置。6.2 共享内存地址冲突导致系统卡死这类问题表现最诡异有时候不是启动时崩而是运行一段时间后整个系统直接hang住。排查思路是确认Linux的预留内存是否与内核启动时使用的内存区域冲突、检查Linux内核启动参数中的memmap参数是否正确设置以及在设备树中是否正确配置no-map属性。还有一个笨办法但很有效在FreeRTOS侧没有启用MMU操作共享内存会直接影响物理地址。在Linux侧先写一部分数据到共享内存中FreeRTOS侧读取时设置一个特殊值来判读是否读到有效数据两边对照就能判断是不是地址冲突的问题。6.3 中断不触发消息收不到消息发出去后对方没有任何反应多半是中断配置有问题。排查时先确认设备树中的中断号是否正确再确认FreeRTOS侧的GIC初始化是否完成最后检查两端是不是用的同一套中断逻辑。有一个比较隐蔽的问题当FreeRTOS侧的GIC没有正确配置时中断永远进不了中断处理函数。可以用一个周期性的定时器中断来验证GIC是否正常工作如果在FreeRTOS任务里能看到定时器中断触发说明GIC配置没问题问题出在rpmsg中断上。6.4 开机启动流程不稳定的风险曾经有段时间我想优化启动流程想着不需要手动敲命令直接在Linux的启动脚本里自动加载FreeRTOS固件。结果发现系统启动时自动加载的时序很微妙如果在内核的某些子系统初始化完成之前就启动CPU1偶尔会发生莫名其妙的死锁。后来采用了一种更稳妥的方式在系统启动完成并进入用户态之后通过一个启动脚本延时几秒再加载固件。虽然没有完全做到自动加载无缝启动但稳定性大幅提升。如果业务要求启动速度可以再研究内核启动流程和remoteproc驱动的加载时机找到更早但安全的加载点。7. 2018.3版本到新版本迁移的思路先用2018.3版本把流程跑通、把原理吃透这是学习阶段最好的状态。但实际产品落地时你可能会因为需要使用新版Vivado的特性或者需要支持更新的外设而不得不升级到新版工具链。迁移的时候有几个地方需要特别留意。新版本PetaLinux的内核和设备树结构与旧版有较多变化。以2020.1版本为例设备树的语法、remoteproc驱动的绑定方式、PetaLinux的命令都有调整网上找的旧教程基本不能直接套用需要按新版本的文档重新配置。Xilinx官方发布过OpenAMP的完整示例工程如果学习的话建议先跑官方工程对流程有全局认识后再结合自身需求进行修改。官方示例中关于共享内存布局、中断分配、设备树配置的信息是可以作为参考依据的核对清楚后自己配置起来也不会有什么大问题。FreeRTOS侧的移植也可能遇到接口变化。各版本OpenAMP和libmetal库的API存在差异升级后需要检查之前的回调函数定义、端点创建接口是否还能编译通过。8. 给你的最后建议8.1 按这个顺序学习上手最快学习路径建议是这样先用Vivado生成最小硬件工程完成FSBL制作和PetaLinux的构建确保Linux能在ZYNQ上独立跑起来然后再配置设备树加入OpenAMP节点把官方例程中的FreeRTOS固件放进去先跑通官方自带的程序最后再自己写应用层的消息收发代码把流程彻底吃透。按这个顺序走每步都有根基即使出了问题也好定位。8.2 从单核到双核思路要跟着换ZYNQ双核开发跟普通的单片机或者纯Linux开发不太一样很多问题都是因为系统性的隔离没做好。共享内存的地址分配、中断的底层配置这部分对Linux内核的底层知识掌握程度提出了要求。建议先花时间看看设备树语法、阅读一下remoteproc驱动的源码有了对整个启动流程和内存布局的全局理解后续开发会更方便。不少工程师刚开始做双核通信时容易紧张害怕把系统搞崩。其实放心大胆试就行崩了大不了重新烧固件多试几次对系统的理解就会更深入。我做这套开发的第三周就彻底搞明白了“预留内存”、“rproc状态切换”和“消息回调”之间的联系整个通信链路在自己脑子里形成了一个完整闭环。ZYNQ的双核通信是个大话题这篇文章从环境搭建到代码实现再到问题排查尽量把关键环节都过了一遍。如果你的项目里既有界面和网络的需求又有实时的控制任务这套方案是可以认真考虑的方向。
返回列表