
去年接了一个EtherCAT主站项目起初想得很简单RK3568跑Linux开源igh主站一搭Motion Control一看齐活。结果真正跑起来才发现EtherCAT从站数量一多、控制周期一到1msLinux调度的抖动直接让DC同步报错示波器抓波形一看脉冲间隔忽长忽短这种状态上产线肯定被现场工程师骂。后来把方案改成了RK3568平台上的AMP架构Linux和RT-Thread各占两个核一个管界面和算法一个专职跑EtherCAT主站和硬实时IO才算是把问题彻底压住了。这篇文章就把我在RK3568上把Linux和RT-Thread混合部署的完整过程写出来不绕弯子直接讲怎么改设备树、怎么编译RT-Thread、怎么让两个系统同时跑起来以及中间踩过哪些坑。如果你跟我一样手上是RK3568的板子想用AMP模式搞LinuxRTOS的混合部署尤其是做EtherCAT主站、数据采集这类对时序敏感的场景这篇文章可以直接拿来当操作手册。全部操作基于RK3568官方SDK和RT-Thread官方BSP按步骤走第三次部署的时候三分钟真的够用。1. 为什么RK3568做AMP聊聊双系统的实际需求1.1 从EtherCAT和实时数据采集说起EtherCAT主站这个事是促使我研究AMP的最直接原因。EtherCAT的DCDistributed Clocks同步机制要求主站在每个周期准确发送数据帧周期抖动超过1us从站同步就会劣化超过几微秒部分要求高的伺服驱动器就会直接报警。而普通Linux内核的调度延迟通常在几十微秒到几毫秒之间波动即使是PREEMPT_RT实时内核也只能把抖动压到十几微秒的水平离EtherCAT的硬实时要求还是差一个量级。我在阳明、倍福这些客户现场都遇到过类似场景Linux上跑人机交互、跑视觉算法、跑EtherCAT主站三个活全挤在一个大系统里一旦CPU被某个进程占满EtherCAT周期立刻翻车。当时考虑过三种方案第一种用实时网卡加PREEMPT_RT内核硬扛结果测试效果不稳定第二种用专用EtherCAT从站控制器芯片把实时性下沉到芯片层面但灵活性差而且代码还是得跑在某个核上第三种就是AMP——把RK3568的四个核拆开两个跑Linux两个跑RT-ThreadRT-Thread专职处理EtherCAT通过核间通信把数据交给Linux。第三种方案在当时看来最合理。1.2 AMP与SMP的本质区别很多人一听到多核处理器脑子里默认就是SMP对称多处理所有核心运行同一个操作系统共同管理一套内存和中断。Linux的负载均衡机制可以把进程调度到任何一个核心上。AMP非对称多处理则完全不同系统里有多个核心每个核心运行的操作系统可以不同内存各自独立划分中断也各自管理。简单打个比方SMP就像一个公司所有人都归一个总经理调度谁有空闲就能接活AMP则是公司里分成了两个团队每个团队有自己的独立负责人团队之间只通过约定的渠道比如共享邮箱沟通。在RK3568上做AMP其实就是把4个Cortex-A55核心的其中两个摘出来让它们跑RT-Thread。剩下的两个核心继续跑Linux。两边各有独立的内存区域、独立的中断互不干扰。对比项SMPAMP操作系统单OS运行在所有核心每个核心可运行不同OS内存管理统一管理各自隔离通过共享内存通信中断处理统一由Linux管理部分中断分配到RTOS侧实时性受Linux调度影响RTOS侧可控抖动极小适用场景通用计算实时控制复杂计算并存1.3 RK3568的硬件资源如何支持AMPRK3568搭载4个Arm Cortex-A55核心主频最高2.0GHz自带GIC中断控制器和IOMMU。规格上属于中高端工业级SoC但价格控制得不错这也是很多国产工控板卡选它的原因。从AMP的角度看RK3568有几个关键特性值得注意一是四个核真正独立。每个核都有自己的PPI私有外设中断和SGI软件触发中断这套GIC架构天然支持把不同外设中断绑定到不同核心。二是IOMMU可用可不用。做AMP时RT-Thread运行的内核通常直接物理寻址不走MMU映射这样实时性反而更稳。Linux侧则继续用MMU两边互不冲突。三是内存控制器能保证不同核心访问不同物理内存区域时没有总线冲突。只要把内存划分清楚RT-Thread侧的数据访问不会拖累Linux侧性能。2. 开工前必须搞清楚的准备工作2.1 需要准备的软件和源码一个完整的RK3568 AMP项目需要准备的东西如下。RK3568的SDK包含内核和U-Boot源码。我用的是正点原子ATK-DLRK3568配套的SDKBSP版本比较新实际上你用瑞芯微官方SDK或者别家开发板的SDK都可以整体思路一致。RT-Thread源码官方GitHub仓库bsp/rockchip/rk3568目录就是官方对RK3568的支持。交叉编译工具链编译RT-Thread固件用arm-none-eabi工具链编译Linux内核则用SDK自带的aarch64交叉工具链。烧录工具RKDevTool以及对应的驱动。一块串口调试板至少两个串口通道最好有USB转串口的模块因为两个系统各自要一个调试串口。这些看起来都是标准配置但我第一次做的时候照样卡了一个小时原因就是串口数量不够。RK3568的物理UART虽然多但默认很多被分配给了蓝牙、调试等功能设备树里需要重新映射。如果你手里只有一块USB转串口就先别急着做AMP因为Linux和RT-Thread同时跑起来两边都要看日志一个串口根本忙不过来。2.2 内存和中断资源的整体规划AMP项目实施前最关键的分配工作就是内存。我以2GB内存的RK3568为例给出一个经实盘验证的分配方案。我的划分方式是这样Linux占用绝大部分内存地址范围0x00200000到0x2FFFFFFFRT-Thread预留区域从0x30000000开始预留64MB空间也就是0x30000000到0x33FFFFFF。RT-Thread固件就加载到0x30000000处运行它的堆、栈、消息队列都在这64MB内分配。之所以把RT-Thread放在0x30000000之后的高地址段是为了避免和Linux低地址段的DMA缓冲区、内核镜像冲突。Linux启动时U-Boot已经把内核解压到低地址位如果RT-Thread又占用低地址非常容易发生覆盖导致启动莫名死机。中断资源的划分也要提前想清楚。RT-Thread负责的EtherCAT网卡中断需要直接送达RT-Thread所在的核心Linux侧就不能再声明这个中断。反过来串口、USB、显示等设备的中断继续留给Linux。这部分通过修改设备树interrupt属性并绑定到具体CPU核心来实现。RK3568的GIC支持在设备树中用interrupt-affinity属性指定CPU亲和性。2.3 编译环境的搭建编译环境这块没有太多花活但有个小坑容易被新手忽略RT-Thread的编译工具链和Linux内核的交叉工具链最好分开装不要混用同一个工具链。RT-Thread的BSP默认找arm-none-eabi-gcc如果你系统里只有aarch64-linux-gnu-gcc就会出现莫名其妙的链接错误。推荐做法是各自建一个独立目录mkdir -p /opt/amp-toolchains/arm-none-eabi mkdir -p /opt/amp-toolchains/aarch64ARM官方提供的arm-none-eabi工具链可以直接下载解压到/opt/amp-toolchains/arm-none-eabi目录然后把bin目录追加到PATH环境变量。RK3568 SDK内部自带完整的编译链接环境执行build.sh时会自动使用SDK内嵌的交叉工具链所以Linux侧不需要手动设置太多环境变量。3. Linux侧的关键改动设备树和内核配置3.1 预留内存与remoteproc节点Linux侧的所有改动基本上都集中在设备树里。AMP想让Linux和RT-Thread各跑各的Linux必须先知道哪块内存是我的哪块内存不是我的。这个信息就靠reserved-memory节点来传达。在arch/arm64/boot/dts/rockchip/rk3568-evb.dtsi或者你开发板对应的dts文件里增加如下节点/ { reserved-memory { #address-cells 2; #size-cells 2; ranges; rproc_reserved: rproc-reserved30000000 { compatible shared-dma-pool; reg 0x0 0x30000000 0x0 0x4000000; no-map; }; }; rproc: remoteproc30000000 { compatible rockchip,rk3568-remoteproc; reg 0x0 0x30000000 0x0 0x4000000; memory-region rproc_reserved; firmware rt_thread.elf; status okay; }; };这段配置的作用有两层reserved-memory里的no-map属性告诉Linux内核这块物理内存映射成虚拟地址时不能使用标准内存映射流程Linux自己绝对不要去动它remoteproc节点则指向同一块区域作为RT-Thread固件的固定加载位置。有一类问题非常典型你配了memory-region但忘记加no-mapLinux启动后仍然会把这块内存纳入伙伴系统管理等到RT-Thread跑起来后Linux在某个瞬间又把这块内存分配出去了两边同时写最终系统随机死机而且死机时机完全无规律非常难排查。我第一次遇到这种问题连续查了好几天。另一个需要注意的地方是reg属性中的地址0x30000000是物理地址编译设备树时不会校验这个地址是否落在实际RAM范围内。如果你板子只有1GB内存0x40000000以下把RT-Thread放在0x30000000之后的64MB依然在RAM内。如果是1GB内存且物理内存只有0x30000000前段那就要把预留区往低地址调整避免访问到不存在的地址。3.2 内核配置项和驱动设备树只是硬件描述要让Linux真正把RT-Thread固件拉起来还得内核使能remoteproc和rpmsg框架。在RK3568 SDK默认配置里这两项通常默认编译为模块m建议直接编译进内核y。在SDK内核目录执行make menuconfig检查以下选项CONFIG_REMOTEPROCy CONFIG_RPMSGy CONFIG_RK_REMOTEPROCy CONFIG_HAVE_IMX_DSPy # 如果用到相关dsp框架则保留如果找不到RK_REMOTEPROC这个选项就要看你的SDK内核版本是否包含了Rockchip的remoteproc驱动补丁。瑞芯微的几个Release分支里都有对应驱动代码位置在drivers/remoteproc/rk_remoteproc.c。除了remoteproc框架本身还需要确认和共享内存相关的配置CONFIG_CMAy CONFIG_DMA_CMAyCMA连续内存分配器在AMP中的作用很大后面讲核间通信时你会看到RPMsg的共享内存就经常从CMA区划出来如果没有使能CMALinux侧分配连续物理内存会很困难。3.3 固件文件的放法RT-Thread编译生成的elf文件要放到Linux根文件系统的/lib/firmware/目录下文件名和remoteproc节点里firmware属性一致。设备树里写的是rt_thread.elfLinux系统启动后remoteproc驱动会去/lib/firmware/rt_thread.elf路径下找这个文件。很多第一次接触remoteproc的人会卡在这明明写了firmware属性RT-Thread也编译好了但执行启动命令时内核报Failed to request firmware错误。多数情况就是文件没放到/lib/firmware/或者权限不对。确认一下ls -l /lib/firmware/rt_thread.elf文件存在且用户有读权限才行。另外一个非常容易踩的坑是RK3568 SDK默认构建出来的根文件系统是只读的比如用ramdisk镜像你拷贝进去的文件重启就没了。解决办法是把rootfs改成可写的ext4格式或者在make menuconfig的rootfs配置里勾选可写选项。我自己是直接把内核cmdline里的root指定为SD卡或eMMC的ext4分区这样固件文件可以持久保存。4. RT-Thread侧的准备让嵌入式RTOS跑在指定核心上4.1 RT-Thread BSP的获取与配置RT-Thread这边的工作主要是两件事配置核心启动参数、调整链接脚本。先从获取BSP开始。官方RT-Thread仓库里有rk3568的BSP路径是bsp/rockchip/rk3568直接clone下来git clone https://github.com/RT-Thread/rt-thread.git cd rt-thread/bsp/rockchip/rk3568这个BSP顶层目录有scons构建所需的SConstruct和Kconfig文件可以直接用env工具RT-Thread官方出的开发环境配置也可以手动修改rtconfig.h。如果你是RT-Thread的新手我建议直接用RT-Thread Studio它内置了对rk3568 BSP的GUI配置入口操作起来更直观。但我个人在实际项目里更偏好命令行scons因为CI和批量编译时命令行更可控。4.2 为AMP修改的宏定义与链接脚本RT-Thread BSP默认情况下是按SMP模式设计的其实不是RT-Thread的rk3568 BSP默认只使用一个核心。现在要做AMP要让RT-Thread只在自己的核心上启动同时明确目标核心ID。关键配置如下在rtconfig.h中#define RT_HW_CACHE_LINE_BYTES 64 #define RT_AMP_ENABLE 1 #define RT_AMP_CPU_ID 2RT_AMP_CPU_ID指定RT-Thread运行在哪个核上。在常见的双系统方案里CPU0和CPU1跑LinuxCPU2和CPU3留给RT-Thread所以填2。配合核心绑定RT-Thread的中断控制器驱动也需要相应配置。RK3568的GIC驱动在RT-Thread BSP的librk3568目录里要确保中断亲和性设置到目标核心否则会出现RT-Thread申请的中断被GIC默认路由到CPU0上而CPU0正在跑Linux中断服务函数永远无法被调用的尴尬局面。链接脚本的调整比较关键。打开bsp/rockchip/rk3568/linker_scripts/link.lds不同版本的BSP路径可能有差异把内存段基址设置成0x30000000. 0x30000000; .text : { *(.text) *(.text*) }这里0x30000000必须和Linux设备树里的reg属性一致否则出现一种情况Linux把固件加载到0x30000000但RT-Thread的链接脚本把自己的代码段放在0x20000000启动后PC指针跳到一个空地址系统马上荒废掉。4.3 构建出RT-Thread固件修改完上述配置后执行构建scons -j8生成的rtthread.elf就是我们需要拷贝到Linux侧的固件文件。有人习惯在RT-Thread里开FinSH控制台这会用到串口调试终端注意在BSP配置里把串口号确认清楚。别让RT-Thread的调试串口和Linux的调试串口共用同一个物理串口否则两个系统同时往一个串口输出日志你看到的就是一堆乱码混叠根本不知道谁是谁。串口的在设备树里的分配也要提前确定。RK3568的UART2往往是调试串口那我就在设备树里把UART2留给Linux让RT-Thread使用UART3。这样两边各看各的日志互不干扰。5. 真正的3分钟部署启动顺序和实操步骤5.1 编译Linux量产固件RT-Thread固件准备好之后回头编译Linux侧。这一步要保证设备树的改动、内核配置的改动真正进入最终的烧录镜像。在RK3568 SDK根目录执行./build.sh kernel ./build.sh uboot ./build.sh # 打包所有分区打包完成后在rockdev目录下找到update.img或对应的分区镜像。烧录时可以用RKDevTool将整个update.img写入eMMC/SD卡也可以只烧录boot.img和dtb分区。实际开发中我只烧boot.img和dtb分区就能迭代调试速度比全量烧写快很多。因为加了一个remoteproc节点设备树dtb的体积变大如果U-Boot对fdt分区有大小限制可能还需要调整U-Boot的环境变量或分区容量。这个具体数值看开发板手册不同品牌的板卡差异较大。5.2 打包RT-Thread固件Linux启动后进入系统先看remoteproc设备是否存在ls /sys/class/remoteproc/正常情况下会看到remoteproc0这个目录它对应的就是设备树里定义的rproc节点。如果没有这个目录说明remoteproc驱动没有加载成功优先检查内核配置和设备树节点是否正常匹配。然后把RT-Thread固件拷进rootfscp rtthread.elf /lib/firmware/ sync注意如果你用的是SD卡作为rootfs因为ext4可写直接cp就行。如果是ramdisk系统启动后文件系统在内存里cp能进去但重启后就会丢失需要改成可写rootfs才能固件持久保存。5.3 上电启动与验证一切准备就绪上电。Linux正常启动后进入shell手动启动RT-Threadecho start /sys/class/remoteproc/remoteproc0/state此时观察两块串口调试输出Linux串口上应该看到remoteproc加载固件、释放复位信号的打印同时出现类似于rk_remoteproc: Measured 1.0的日志RT-Thread串口上则应该看到RT-Thread的启动banner包括版本号、系统时钟信息以及接下来运行的应用日志。如果一切正常你会发现RT-Thread侧完全没有Linux的调度延迟困扰。RT-Thread的tick中断一启动一个专门做主站任务的线程就按周期循环执行整个过程非常稳定。有一个细节启动RT-Thread之后Linux内核日志里可能出现警告信息提示某个CPU核心被offline了这是正常现象并不是错误。Linux原本管理4个核你把其中部分核交给RT-Thread后Linux内部的CPU热插拔逻辑会感知到状态变化并打印提示。不要看到这行日志就以为出bug了。6. 两个系统的沟通RPMsg核间通信6.1 RPMsg基本原理两个系统都跑起来了不代表事情做完。最关键的问题浮出水面Linux要拿EtherCAT的数据RT-Thread要接收Linux发下来的运动控制指令两边怎么通信一种最简单的思路是直接访问共享内存在内存里放一个结构体两边都读写。但是裸共享内存存在明显的同步问题如果Linux正在写数据RT-Thread恰好也在写同一块区域数据就会错乱。你可以在应用层自己加锁但嵌入式场景下加锁本身又会引入新的延迟和不确定性。RPMsg就是专门解决这个问题的。它基于virtio的虚拟队列机制在共享内存上抽象出一条消息通道。Linux侧的rproc框架已经实现了virtio/rpmsg的完整链路RT-Thread侧也有对应的rpmsg实现。用户需要做的只是发起收发。可以这么理解共享内存就像一间公共办公室RPMsg就是办公室里的邮件收发室。你不直接进办公室翻东西而是把信投到收发室由收发室决定什么时候把信送到对应的人手里。6.2 Linux侧如何发送和接收RT-Thread固件启动成功后Linux侧会创建设备节点/dev/rpmsg0ls /dev/rpmsg*要测试能否正常通信直接用标准文件读写接口#include fcntl.h #include unistd.h #include stdio.h #include string.h int main(void) { int fd open(/dev/rpmsg0, O_RDWR); if (fd 0) { perror(open rpmsg0); return -1; } char buf[128] {0}; const char *msg hello rt-thread; write(fd, msg, strlen(msg) 1); memset(buf, 0, sizeof(buf)); read(fd, buf, sizeof(buf)); printf(recv: %s\n, buf); close(fd); return 0; }如果read阻塞无法返回确认RT-Thread侧是否把应答消息通过rpmsg发回来了。6.3 RT-Thread侧如何对接RT-Thread侧要在应用代码里注册rpmsg端点和回调函数。RT-Thread的rpmsg组件初始化完成后Linux发来的消息会进入你注册的回调里你在回调中处理后再调用发送接口回复给Linux。#include rtthread.h #include rpmsg.h static int rpmsg_cb(void *data, rt_uint32_t len) { rt_kprintf(recv from linux: %s\n, (char *)data); rpmsg_send(hello linux); return 0; } static int app_init(void) { rpmsg_service_init(amp-pingpong, rpmsg_cb); return 0; } INIT_APP_EXPORT(app_init);比较重要的是内存拷贝策略。RPMsg消息缓冲区通常有限如果你的交互数据量很大建议在共享内存区单独开一块自定义缓冲区RPMsg只传一个指针或偏移量真正的数据放在共享内存里。这样RPMsg通道始终保持轻量、低延迟不会因为拷贝大数据而阻塞实时任务。7. 调试技巧与踩坑记录7.1 怎么快速定位死机是哪个核心引起的AMP系统最大的调试痛点就是一旦死机你不知道是哪一边先出的问题。我的做法是给Linux和RT-Thread各安排了一个GPIO控制的LED心跳指示。Linux侧每500ms翻转一次某个GPIORT-Thread侧每100ms翻转另一个GPIO。如果LED1还在闪LED2停了那问题一定出在RT-Thread侧反之则是Linux侧先Abort。这个方法看似土但在双系统联调时非常高效。设备挂掉后你不用看一大段复杂日志直接看两个LED状态就能缩小排查范围。7.2 内存冲突的排查内存冲突是AMP最常见的故障。Linux侧因为有MMU访问到非法地址时内核会打印一条oopsrt-thread侧则可能表现为某个线程异常或者干脆无响应。排查方法是先确认reserved-memory里的范围是否被Linux占用。在Linux shell执行cat /proc/iomem | grep -i reserved cat /proc/iomem | grep 30000000如果发现0x30000000区域出现在某个Linux驱动的注册地址里说明CMA或DMA缓冲区把这块区域分配出去了需要回头检查reserved-memory节点有没有带上no-map属性。在一些特殊场景下Linux的CMA默认区域会从低地址开始分配有可能已经占用了0x30000000附近的空间。这种情况下要么把预留区改到更高地址要么通过内核cmdline里的cma参数调整Linux可用内存的起始位置。# 在某款4GB板卡上我们把CMA区域固定到0x10000000~0x20000000之间 cma256M0x100000007.3 EtherCAT实时性实测数据我在这套方案上跑了IGH的EtherCAT主站测试RT-Thread侧每1ms周期发送一帧连续运行48小时用逻辑分析仪抓取SYNC信号得出的抖动数据最大抖动±0.9us绝大多数周期抖动在±0.2us以内。这个结果和纯RT-Thread单系统跑EtherCAT基本没有区别。同样的测试Linux侧用PREEMPT_RT内核裸跑1ms周期抖动峰值从几十us到上百us不等满载时甚至能到几百us。这也是为什么我坚决推荐AMP方案。当然这个数据是在板载网卡驱动经过优化、RT-Thread侧任务优先级配置合理的前提下测出来的。如果RT-Thread里同时跑了多个高优先级任务EtherCAT线程的外设中断响应时间可能会拉长需要根据自己的应用场景测试确认。7.4 几个容易忽略的坑第一个坑是U-Boot阶段的CPU核心启动问题。RK3568的U-Boot默认会把4个核心全部启动并在ATF阶段等待Linux接管。如果你在设备树里禁用了部分核心U-Boot侧必须保持一致的配置否则AMP启动时会发现RT-Thread要用的核心被U-Boot占用造成启动顺序的异常。第二个坑是固件格式。RT-Thread编译出来的是标准ELF格式remoteproc框架支持直接加载ELF但ELF文件里包含调试信息和符号表体积偏大。如果你希望减少加载时间可以转换成bin格式但这时需要在链接脚本里精确控制内存布局并且remoteproc节点里要额外指定entry地址。我用ELF格式居多省心。第三个坑是共享内存的缓存一致性问题。Linux侧有MMU和CacheRT-Thread侧直接物理寻址也有Cache两边同时访问共享内存时如果配置不当会一边看到新数据一边看到旧数据。解决方式是在共享内存的异常不配置时把两个核对该区域的页表属性都设置成Non-Cacheable或者使用DMA API做显式缓存同步。尽量在初始化阶段一次性设置好运行过程中切换缓存策略会导致不可预期的延迟尖峰。还有一个和正点原子rk3568开发板特别相关的点它板载的以太网驱动、EtherCAT功能是否能和AMP共存取决于网卡中断是否被路由到RT-Thread所在的核心。我测试的板子是双千兆网口一个网口留给Linux另一个网口的所有中断都要配置到CPU2/CPU3上并且RT-Thread里要正确初始化这个网卡的驱动。这一步如果没做好RT-Thread侧网卡一直初始化失败但又查不出硬件问题耗时最大。先通过设备树把中断亲和性设置好再用串口打印确认中断是否到达RT-Thread核心。我在实际部署这套方案的时候第一次完整跑下来大概花了两个星期主要是踩了内存no-map和中断亲和性这两个大坑。但部署环境固定下来之后后续每次从编译到双系统跑起来确实可以控制在3分钟左右。这也正是这个标题里3分钟的底气来源——前期的配置工作做好后续的部署就是固定流水线真正麻烦的事情是一次性的。如果你计划在自己的RK3568板子上做类似的LinuxRT-Thread混合部署我建议先从最小系统开始先让RT-Thread独立跑通串口打印和GPIO翻转再接入Linux的remoteproc框架最后再上EtherCAT这类复杂的实时业务。一步一步来踩坑的时候不会迷路。整个项目做完我的体会是CPU核是可以商量着分的操作系统的边界要看你的约束条件在哪里。EtherCAT这种硬实时任务放在专用的实时核上Linux负责它擅长的复杂生态和业务编排各司其职远比强行在单个系统里解决所有问题来得清爽。