1. ZYNQ双核通信,到底解决什么问题
做嵌入式开发的老哥们应该都有这种经历:产品功能一多,单核处理器就有点喘不过气。这边要跑人机交互和网络协议栈,那边还要实时响应电机控制、数据采集,你争我抢,稍不留神就出现任务超时。ZYNQ这颗芯片出现在视野里的频率越来越高,就是因为它把ARM Cortex-A9双核和FPGA可编程逻辑放在同一颗芯片上,天生就是为这种“既要又要”的场景准备的。
ZYNQ-7000系列内部有两个ARM Cortex-A9硬核处理器,这是它最值钱的地方之一。很多初学者一开始只用了其中一个核,另一个核闲着没事干,这在小项目里没问题,但等你做的产品稍微复杂一点,比如要做EtherCAT主站、要做视觉引导、要跑算法又要实时控制的时候,单核会非常吃力。这时你就会想:能不能让CPU0跑Linux处理人机界面和网络协议,让CPU1跑FreeRTOS处理实时任务?这就是异构双核通信的典型应用场景。
但问题是,两个核各跑各的系统,怎么让它们协同工作?怎么互相传递数据?这就需要一套成熟的通信机制,而在ZYNQ平台上,OpenAMP就是目前最主流的解决方案。
这篇文章我基于2018.3版本的工具链,从零开始给你搭建一套完整的OpenAMP开发环境,把Linux+FreeRTOS的双核通信跑通。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.3,PetaLinux也是2018.3,对应的Linux内核是4.14版本。这套组合是当时非常稳定的组合,网上资料丰富,踩坑后的解决方案也最容易搜到。
如果你用的是更新的版本,比如2020.2或2021.1,整体流程类似但会有细节差异,比如PetaLinux的配置命令、设备树文件的写法、OpenAMP驱动的加载方式都会有调整。我见过不少人在新版上折腾不出来,退回2018.3反而很快跑通了。
2018.3版本的核心组件清单如下表所示:
| 组件 | 版本/说明 |
|---|---|
| Vivado | 2018.3,用于硬件工程设计和FSBL生成 |
| PetaLinux | 2018.3,用于Linux内核、根文件系统和设备树构建 |
| Linux内核 | 4.14,Xilinx维护的分支,内建OpenAMP驱动 |
| U-Boot | 2018.01,第二级引导程序 |
| FreeRTOS | Xilinx提供的移植版本,基于9.0或10.0 |
| OpenAMP | 集成在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中加载FSBL,FSBL初始化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: rproc@3e800000 { 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侧中断控制器是GIC,SPI中断号从32开始编号。Xilinx的OpenAMP例程通常使用SPI中断号61,对应的硬件中断号是93,也就是32加61。
这里有个关键点:中断号对不对,从Linux的启动日志里基本看不出来,一定要在FreeRTOS侧的中断服务函数里加上调试打印或者状态翻转,否则中断是否触发完全无感。
5. 启动流程配置与联调经验
5.1 启动顺序为什么这么重要
ZYNQ上Linux+FreeRTOS的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的双核通信是个大话题,这篇文章从环境搭建到代码实现再到问题排查,尽量把关键环节都过了一遍。如果你的项目里既有界面和网络的需求,又有实时的控制任务,这套方案是可以认真考虑的方向。