ARTICLE DETAIL

资讯详情

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

ZYNQ双核AMP:Linux与FreeRTOS的OpenAMP异构通信实践

ZYNQ双核AMP:Linux与FreeRTOS的OpenAMP异构通信实践 第一次在ZYNQ上把Linux和FreeRTOS同时跑起来的时候我心里其实是有点忐忑的。折腾ZYNQ的人大多都听说过“双核A9”但真要用起来很多人第一反应就是SMP跑一个Linux拉到——确实Linux对SMP支持得天衣无缝根本不用你操心核间调度。可一旦你开始做真实产品就会发现很多时候你希望一个核跑Linux处理网络协议栈、文件系统、UI交互另一个核跑FreeRTOS专注电机控制、采集时序、实时响应。这种“一个芯片里住着两个操作系统”的玩法就是典型的AMP非对称多处理。而OpenAMP就是Xilinx官方推荐、也是目前最成熟的那座桥。这篇文章写的是2018.3版本的工具链Vivado 2018.3、PetaLinux 2018.3、Xilinx SDK 2018.3加上Xilinx维护的OpenAMP开源仓库。版本虽老但这一版OpenAMP已经进入Linux内核主线4.14设备树写法、remoteproc框架、rpmsg通信模型都相对稳定非常适合拿来做入门AMP的跳板。整篇文章适合有两三个月ZYNQ开发经验、想从单核裸机或单Linux往上再走一步的工程师我会尽量把从硬件工程、PetaLinux配置、FreeRTOS程序编写到实际加载通信的整个链路摊开讲。1. 先把OpenAMP这套东西搞明白再动手1.1 为什么非要用LinuxFreeRTOS这种“一核一系统”的组合很多人问我两个A9核跑同一个Linux不就行了内核自带SMP两个核自动负载均衡省事多了。但做产品的人会告诉你SMP在大部分场景确实省事可就怕你碰上“既要又要”的需求——既要Linux的生态又要硬实时的响应。比如一个稍微复杂点的运动控制系统上位机通过网口给指令Linux那边跑Modbus/TCP、Web配置页面、日志存储这些都是再舒服不过的活儿。但底层的编码器采集、PID计算、PWM输出要求的是微妙级抖动Linux那边一个中断延迟就把你波形搞花了。你要是把实时任务放到独立核上跑FreeRTOS把Linux单纯当成一个“超级外设”来用两边互不干扰开发难度和稳定性都会好很多。这就是AMP的核心思路按实时性需求把任务拆开每个核分配最合适的操作系统。另一个很实际的理由是生态隔离。很多工业现场要求控制部分通过安全认证、代码可审计如果控制逻辑和UI逻辑混在同一个Linux内核里评审会很麻烦。AMP架构下实时控制代码就是一个独立小固件逻辑清晰测试方便认证路径也短。1.2 OpenAMP到底拆成了几块各管什么活OpenAMP是开源异步多处理器框架2018年之后Xilinx把它和Linux内核原生的remoteproc/rpmsg机制做了深度整合。它主要由三部分组成libmetal、open-amp、以及Linux内核里的remoteproc/rpmsg驱动框架。libmetal是硬件抽象层。它向上提供统一的设备访问接口、内存映射、DMA、原子操作、睡眠唤醒这类基础能力让open-amp可以跨平台跑在Linux用户态、Linux内核态、裸机、RTOS上。open-amp则是核心协议栈封装了remoteproc远程固件加载、virtio虚拟设备、rpmsg消息传递三套机制。Linux侧通过remoteproc框架把FreeRTOS的elf固件加载进DDR通过rpmsg建立共享内存上的消息通道FreeRTOS侧则跑一个轻量的open-amp库作为remote端响应Linux发来的IPI中断和virtio请求。拿生活里的场景类比Linux是“包工头”FreeRTOS是“老师傅”。包工头负责把老师傅的图纸elf固件放进工位DDR指定地址然后敲敲门IPI中断说开工两个师傅之间信息沟通靠一条共享走廊共享内存走廊两头各挂一个信箱vring一个投递一个收取这就叫virtio。2018.3这个版本Xilinx把OpenAMP demo代码、devicetree片段、meta-openamp层都维护在GitHub上分支就是对应当年的发布版本。你直接从2018.3分支拉代码比自己从零写设备树和裸机初始化省下至少一周的调试时间。2. 整体设计思路与硬件工程准备2.1 开发板和工具链你真不用非得买ZedBoard我手上用的是Zynq-70201GB DDR3板子型号是ZedBoard。其实你只要有Zynq-7000系列开发板、SD卡、USB-UART线就行Zybo、MicroZed、黑金、米联客的都大同小异。7020的双核A9跑LinuxFreeRTOS绰绰有余没必要上Zynq UltraScale那套MPSoC架构和操作会复杂不少那边多出R5核和A53核虽然原理相通但翻车概率高。工具链要严格对版本号不然OpenAMP仓库里2018.3分支的代码和PetaLinux不匹配编译链会出莫名其妙的问题。我用的就是Vivado 2018.3 Xilinx SDK 2018.3PetaLinux 2018.3对应内核xlnx_rebase_v4.14Xilinx/open-amp、libmetal、meta-openamp的2018.3分支有一点要说在前面2018.3版本的硬件描述文件还是.hdf后缀不是新版的.xsa在SDK里通过File - Export Hardware导出。习惯新版的人别搞混。2.2 Vivado工程里的关键配置地址空间要提前“分家”硬件工程本身不复杂PS端使能UART1、SD0、ENET0、DDR只要跑过Hello World的人都会配。真正需要提前规划的是DDR地址空间划分。既然要让Linux和FreeRTOS各占一块互不干扰的内存你就要像分房产一样在硬件设计之初就把边界划好。以7020的1GB DDR为例我的划分习惯是Linux占0x00000000~0x3EBFFFFF大约1007MBFreeRTOS从0x3EC00000开始往上留约20MB空间给固件、共享内存和资源表。划分逻辑其实就一句话Linux越稳越好FreeRTOS需要多少给多少。Linux那边跑内核、跑文件系统、跑网络对内存的需求远大于一个精简RTOS所以大头给它。FreeRTOS这边一个简单控制固件编译出来可能也就几百KB预留20MB已经是富余到奢侈了将来加协议栈、加缓冲队列都有空间。这里有必要提醒一下共享内存的地址最好靠近DDR高地址端且不要和FreeRTOS固件镜像重叠。具体三个段分别是FreeRTOS固件elf加载地址、vring共享缓冲区地址、资源表resource table地址。这三个地址后面要写进设备树、写进FreeRTOS的链接脚本任何一处没对齐通信就会失败而且失败的方式通常是“系统一卡日志空白”。2.3 FSBL要选支持双核启动的版本别拿默认编译随便用OpenAMP跑通的另一个关键前置条件是CPU1的启动。Zynq系统上电后FSBL会启动CPU0但默认不启动CPU1。要让CPU1运行FreeRTOS必须在FSBL里显式地把CPU1的入口地址写进0xFFFFFFF0寄存器然后发送SEV事件唤醒它。Xilinx官方OpenAMP demo的BSP里其实已经带了修改好的FSBL细节是在fsbl_main.c里增加了一段逻辑检测到某个magic值demo里常用0x13579BDF之类的标记就去启动CPU1。如果是自己从零新建FSBL你需要在main函数末尾加上类似这样的代码#define CPU1_START_ADDR 0xFFFFFFF0 #define SEV() __asm__(sev) void StartCpu1(void) { volatile uint32_t *cpu1_start (volatile uint32_t *)CPU1_START_ADDR; *cpu1_start 0x3EC00000; /* CPU1入口地址与FreeRTOS链接脚本保持一致 */ dmb(); SEV(); }不要小看这段代码很多人Linux起来了、FreeRTOS也编译出来了但CPU1就是不动最后查来查去发现FSBL压根没释放CPU1。FSBL是整个启动链的第一个环节它没干的事后面U-Boot和Linux是不会替你干的。很多教程说“用SDK里现成FSBL就行”但那个“现成”是指跑Linux的CPU0单核场景做AMP必须换成支持CPU1启动的版本。最简单的做法是直接拉OpenAMP example工程里的fsbl源码编译或者用meta-openamp里的补丁对官方FSBL打补丁再编译。我踩过这个坑所以在这里啰嗦三遍也不嫌多。3. PetaLinux端环境搭建与配置3.1 创建PetaLinux工程的正确姿势PetaLinux 2018.3安装好之后先创建一个空工程再把Vivado导出的HDF导进来。命令不复杂petalinux-create -t project --template zynq --name amp_linux cd amp_linux petalinux-config --get-hw-description/path/to/hdf_dir --silentconfig这里有个小体感2018.3的petalinux-config相对慢特别是第一次解析HDF的时候屏幕上会刷大量硬件信息别以为卡死了。等它落到命令行提示符再用petalinux-config -c kernel打开内核配置菜单。内核配置里要确保这几项打开缺一个remoteproc的设备节点都起不来CONFIG_REMOTEPROCy CONFIG_ZYNCQ_REMOTEPROCy CONFIG_RPMSGy CONFIG_RPMSG_VIRTIOy CONFIG_RPMSG_USER_DEV_DRIVERy其中ZYNCQ_REMOTEPROC是Zynq平台remoteproc驱动RPMSG_USER_DEV_DRIVER会在/dev下生成rpmsg0字符设备方便你用echo/cat直接测试通信。强烈建议把这个驱动编成模块而不是编进内核因为调试时你会频繁地rmmod/modprobe。3.2 设备树里必须写清楚的三个节点PetaLinux的设备树主要改project-spec/meta-user/recipes-bsp/device-tree/files/system-user.dtsi。OpenAMP能不能跑通设备树是重中之重要写的节点主要就三类保留内存reserved-memory、remoteproc设备节点、以及rpmsg虚拟设备节点。可以直接参考meta-openamp仓库2018.3分支里的zynq-openamp.dtsi我摘一段关键结构说明reserved-memory { #address-cells 1; #size-cells 1; ranges; rproc_0_reserved: rproc3ec00000 { compatible shared-dma-pool; reg 0x3ec00000 0x1400000; no-map; }; }; remoteproc0: remoteproc0 { compatible xlnx,zynq-remoteproc; firmware freertos_demo.elf; reg 0x3ec00000 0x10000000; vring0 0x3ef10000; vring1 0x3ef14000; ... };重点解释几个字段no-map;表示这段内存从Linux的页表映射中拿掉。Linux看到这块地址时不会去cache、不会去分配彻底“让路”给FreeRTOS。如果不写no-map后果就是Linux的cache和FreeRTOS的写操作互相污染调试时你会看到rpmsg消息读出来是乱码、固件偶尔加载失败非常折磨人。firmware属性指定了要加载的FreeRTOS固件文件名对应/lib/firmware/freertos_demo.elf。改设备树的时候记得文件要放对位置。vring0/vring1是共享内存里两个环形缓冲区的地址。Linux通过virtio向这两个vring读写数据FreeRTOS端也必须在相同地址建vring。这是两边唯一的信息通道地址必须一字不差。reg里填的0x3ec00000和长度0x10000000只是个示意实际值要覆盖FreeRTOS固件、vring、资源表所在的整个区域让驱动知道它管理的remote内存范围。这里我建议你直接把meta-openamp里的dtsi拿过来改成自己的地址别自己从空文件开始写。设备树少了某个属性问题表现往往是“模块加载成功但start报错”日志还含含糊糊查起来会让你怀疑人生。3.3 PetaLinux构建与BOOT.BIN打包设备树改完之后执行构建petalinux-build构建完成之后把FreeRTOS的elf固件拷贝到镜像根文件系统的/lib/firmware目录下。常规做法是mkdir -p project-spec/meta-user/recipes-app/freertos-firmware/files cp freertos_demo.elf project-spec/meta-user/recipes-app/freertos-firmware/files/再写一个简单的bbappend把它安装进rootfs。如果嫌麻烦也可以等系统启动后通过scp拷贝到/lib/firmware但要是开机自动加载的需求还是放rootfs里干净。最后打包BOOT.BINpetalinux-package --boot --fsbl zynq_fsbl.elf --fpga system.bit --u-boot --kernel --force这个命令会把FSBL、bitstream、U-Boot、Linux内核打包成一个BOOT.BIN同时生成boot.scr。注意FSBL一定要用支持AMP启动的那个版本别用默认替代。打包完成后把BOOT.BIN、image.ub、boot.scr三个文件拷到SD卡第一分区FAT32格式即可。4. FreeRTOS端程序编写与编译4.1 SDK工程里怎么把OpenAMP库“塞”进去FreeRTOS端的代码我选择在Xilinx SDK 2018.3里编译因为BSP、编译器、调试器一条龙省去折腾arm-none-eabi工具链的时间。新建一个Application ProjectOS Platform选择freertos然后需要把open-amp和libmetal的源码同时加进工程里。拉代码的姿势git clone -b 2018.3 https://github.com/Xilinx/open-amp.git git clone -b 2018.3 https://github.com/Xilinx/libmetal.git然后在SDK里分别把open-amp/lib、libmetal/lib下面的源码添加进工程目录并把头文件路径配好open-amp/lib/include、libmetal/lib/include、以及裸机BSP生成的include目录。还有一个不起眼但很容易漏的配置编译宏METAL_INTERNAL和一些平台相关的宏比如METAL_MAX_DEVICE_REGIONS、VIRTIO_MAX_QUEUES。如果你编译时报这些宏未定义去libmetal仓库的README里找它要求的配置项逐个加到SDK工程的编译选项里即可。4.2 写一个能跑通的FreeRTOS通信程序FreeRTOS端的核心流程其实很固定初始化libmetal、定义资源表、注册vdev、启动rpmsg端点、然后轮询收消息。以官方echo_test为例缩略后的主流程是这样#include openamp/open_amp.h #include metal/device.h #include metal/sys.h #define SHM_BASE_ADDR 0x3EF00000 #define SHM_SIZE 0x100000 static struct metal_device *shm_device; static struct virtio_device *vdev; static struct rpmsg_device *rpdev; static void rpmsg_read_cb(struct rpmsg_device *rpdev, void *data, int len, uint32_t src, void *priv) { rpmsg_send(rpdev, src, data, len); /* 原样回显 */ } int main(void) { metal_init(); metal_device_open(generic, shm, shm_device); /* 注册共享内存区域 */ metal_io_init(shm_device-io, SHM_BASE_ADDR, SHM_SIZE, -1); /* 通过资源表初始化virtio */ vdev rproc_virtio_create_vdev(...); rpdev rpmsg_virtio_create_rpmsg_device(vdev, ...); rpmsg_register_callback(rpdev, RPMSG_READ_CB, rpmsg_read_cb); /* 告诉Linuxremote已经就绪 */ rproc_virtio_wait_remote_ready(vdev); while (1) { /* 处理收到的rpmsg消息 */ rpmsg_virtio_rx_callback(vdev); vTaskDelay(1); } }这段代码只是一个骨架。实际还要处理resource table的初始化、virtio队列参数的填充、IPI中断回调注册等等。这些内容在Xilinx/open-amp仓库的examples/echo_test目录里都有现成的直接基于它改比从零写靠谱得多。我个人的建议是第一次跑通之前不要改任何逻辑就用官方echo_test原封不动编译等printf能在共享内存上回显了再把自己的业务逻辑往里面加。4.3 资源表和链接脚本决定了FreeRTOS是否“藏得住”资源表resource table是整个OpenAMP握手的基础里面记录了这个remote固件如何使用内存、占用哪些中断、分配了哪些vring。它的结构通常长这样#define NUM_VRINGS 2 #define VRING0_ADDR 0x3EF10000 #define VRING1_ADDR 0x3EF14000 #define VRING_ALIGN 64 static struct fw_rsc_vdev rproc_vdev { .id VIRTIO_ID_RPMSG, .num_of_vrings NUM_VRINGS, .vring { { VRING0_ADDR, VRING_ALIGN, 256, 0, 0 }, { VRING1_ADDR, VRING_ALIGN, 256, 0, 0 } }, };注意vring数量、地址、对齐、buffer size这些必须和Linux侧设备树里的vring0/vring1完全一致。不对齐virtio找翻队列地址不一致Linux往你预期的地方写数据FreeRTOS却从另一个地址读消息全部丢失。2018.3版的open-amp还把资源表符号resource_table硬编码导出Linux端remoteproc驱动会解析这个符号来找到资源表这个符号不能删、不能改小写否则加载时直接报no resource table found。链接脚本方面最简单是在SDK默认lscript.ld基础上把FreeRTOS的堆栈段整个放到0x3EC00000起始的区域保证和Linux设备树以及FSBL里的CPU1入口地址一致。同时确认__heap_start、__heap_end之间的区域不要和vring地址重叠。我见过有人在1GB DDR的板子把堆栈地址配到0x20000000结果Linux一启动就直接把FreeRTOS代码覆盖了整个系统像得了癫痫一样反复重启。5. 启动、加载与双核通信验证5.1 从SD卡启动到加载FreeRTOS固件的完整链路整个启动链路是这样的Zynq上电 - BootROM读SD卡 - FSBL执行 - FSBL启动CPU0跑U-Boot - U-Boot加载Linux内核和设备树 - Linux启动 - 用户在内核态触发remoteproc加载FreeRTOS固件 - 固件在CPU1上跑起来。Linux起来之后进入终端确认设备树节点是否生效ls /sys/class/remoteproc/remoteproc0/正常情况下你能看到firmware、state、name等文件。然后开始加载固件echo freertos_demo.elf /sys/class/remoteproc/remoteproc0/firmware echo start /sys/class/remoteproc/remoteproc0/state执行echo start后Linux端remoteproc驱动解析elf、把代码段数据段拷贝到0x3EC00000开始的地址、写CPU1启动入口、发SEV唤醒CPU1。你会在串口上看到FreeRTOS的启动日志比如官方demo里会打印一行hello。那一刻的心情感慨估计只有从零把AMP跑通过的人才能体会。如果一切顺利cat /sys/class/remoteproc/remoteproc0/state应该显示running。5.2 用rpmsg跑通一次echo眼见为实加载固件之后再加载rpmsg用户态驱动模块modprobe rpmsg_user_dev_driver这时查看/dev下应该有rpmsg0设备节点。然后开启两个终端一个负责读一个负责写。先开读cat /dev/rpmsg0再开写echo hello from linux /dev/rpmsg0如果整个链路没问题FreeRTOS会把这条消息原样回显在cat终端里你会看到一行hello from linux。这行回显意味着Linux把消息写进vring通过IPI通知CPU1FreeRTOS从中读取并回写又一次IPI通知LinuxLinux从vring读回来——整个共享内存中断的闭环彻底打通了。我也试过用dmesg看内核日志能看到类似virtio_rpmsg_bus: virtio0: creating channel 30 addr 0x0的信息代表rpmsg通道建立成功。这些日志对后续排查非常有价值。5.3 能不能开机自动加载FreeRTOS固件很多产品场景下希望Linux一启动CPU1就自动跑起来不需要手动echo。有几条路可以走第一条是让U-Boot在启动Linux之前就把FreeRTOS固件加载好然后Linux的remoteproc驱动检测到remote端已经就绪直接进入running状态。需要把FreeRTOS固件打包成U-Boot可识别的镜像.bin格式在U-Boot环境变量里加一条fatload mmc 0 0x3EC00000 freertos.bin之类再跳转CPU1。第二条是Linux启动后通过systemd服务自动执行echo start /sys/class/remoteproc/remoteproc0/state。好处是逻辑直观出问题随时手动停掉适合调试阶段。第三条是在内核rootfs的init脚本里加上加载步骤。我实际项目中用的是第二条稳定、低耦合而且哪天不想让RTOS跑了删个服务就行。6. 常见问题排查与避坑记录6.1 那些让我通宵过的问题逐条拆给你看问题一echo start之后卡死系统无反应。这是最典型的共享内存未正确保留。检查设备树reserved-memory节点是否加了no-map;没有的话Linux会把这块区域映射成cacheableFreeRTOS写入的数据被CPU0的cache视线挡住virtio状态永远不一致。其次检查vring地址和FreeRTOS端资源表是否完全一致地址差一个字节都可能死锁。问题二固件可以start但rpmsg收不到任何回显。这种情况先dmesg | tail看virtio通道有没有建立。如果日志里没有channel信息大概率是资源表里virtio设备ID和Linux端预期不匹配或者vring大小或对齐值不一致。2018.3的默认vring对齐值是64缓冲256个描述符如果你改过两边必须同步改。问题三FreeRTOS固件加载失败提示找不到resource table。确认elf里有没有导出resource_table符号。有时候编译器优化会把符号给优化掉或者你改了链接脚本导致符号所在段被丢弃。用nm freertos_demo.elf | grep resource_table看一下没有的话在链接脚本里强制保留该符号。问题四固件能加载但CPU1跑飞串口无输出。大概率是FSBL没正确设置CPU1入口地址或者入口地址和FreeRTOS链接脚本不一致。请回查FSBL里0xFFFFFFF0寄存器写入的地址值和lscript.ld里向量表地址。如果FSBL是默认版本请更换为OpenAMP demo配套的FSBL。问题五Linux起来后/sys/class/remoteproc下没有设备节点。这是设备树没配对。用ls /proc/device-tree/remoteproc0/检查节点是否存在存在的话检查compatible是否为xlnx,zynq-remoteproc驱动有没有编进内核。驱动没编进去就去看内核配置里的CONFIG_ZYNCQ_REMOTEPROC。问题六第一次start成功stop之后再start却失败。remoteproc框架对remote固件的行为有状态约束。FreeRTOS端收到stop后如果没有做完整的资源释放第二次start时两边状态机对不上。调试阶段最省事的办法是直接重启Linux再加载一遍产品化再做优雅的reset流程。6.2 常规文档里不会写的几条经验经验一地址规划表一定要做成文档贴在工位上。我把0x3EC00000到0x3FFFFFFF这段内存的每一块用途都写在了一个表格里包括固件镜像、vring0、vring1、资源表、共享数据缓冲区甚至标注了每个地址在设备树、FreeRTOS链接脚本、FSBL三处各自出现的位置。后面改一次方案先改表再对着表改三处代码。别问我为什么吃这么多亏才养成这习惯。经验二调试时先用官方demo验证整个链路再动自己的业务代码。很多人喜欢一上来就写自己复杂的协议结果通信没通根本分不清是OpenAMP的问题还是业务逻辑的问题。我现在的套路是第一天只求把echo_test跑通第二天把FreeRTOS端改成LED翻转让Linux定时发指令第三天再上真正的业务数据。分步走每走一步都有明确的可观测信号。经验三两个系统的打印信息别混在同一个串口。调AMP时Linux日志和FreeRTOS日志如果打到同一个串口两边打印一混你根本分不清谁是谁。我建议Linux用UART1FreeRTOS用UART0或者FreeRTOS的日志通过rpmsg回传Linux再打印。隔离好日志通道调试效率直接翻倍。经验四学会从Linux侧看remote端的状态。cat /sys/class/remoteproc/remoteproc0/state只能看到running/offline想深入定位问题就用echo 4 /proc/sys/kernel/printk打开内核debug日志remoteproc的加载细节会全部打出来。再看dmesg里virtio、rpmsg通道的建立过程基本能定位八成的握手问题。这个内容后续还可以往几个方向扩展。比如可以把rpmsg消息改造成自定义协议在Linux侧写一个守护进程FreeRTOS侧定义好命令字两个系统之间形成一套稳定可靠的远程调用通道再比如把共享内存区域单独划一块出来做高速数据交换绕开rpmsg小消息的限制适合图像传输、波形数据回传这类大流量场景甚至可以在FreeRTOS侧把网络协议栈也跑起来让CPU1直接收网口数据CPU0只做管理面。每次扩展我自己的体会都是只要地址规划好OpenAMP这套框架的想象力确实比想象中更大。先别急着追求花哨功能把2018.3这个版本的echo_test跑通你就已经掌握了ZYNQ双核世界里最关键的那把钥匙。
返回列表