干嵌入式这些年,Linux和RTOS的搭配一直是绕不开的话题。这次要聊的RK3568开发板AMP双系统混合部署,就是把一颗四核Cortex-A55处理器拆成两个阵营:Linux拿3个核跑应用、显示和网络,RT-Thread独占1个核跑硬实时任务。两边共享同一块DDR内存,用核间通信交换数据,互不干扰又紧密协作。
这种玩法最典型的场景就是工业控制器。一台设备里既要有HMI界面、文件系统、EtherCAT主站协议栈、网络调试,又要有微秒级响应的伺服控制、高速IO采集和运动控制周期任务。Linux生态确实香,但普通桌面Linux在负载上来之后,调度延迟抖到几毫秒甚至更差,拿去做伺服控制就是灾难。RT-Thread实时性好,但让它去搞复杂文件系统、跑图形界面、接一堆外设驱动也不现实。AMP方案正好取长补短。
如果你手上有RK3568板子,想跑Linux+RT-Thread混合部署,或者纯新手想搞明白AMP到底怎么落地,这篇博文可以直接照着做。我不会给那种“官方手册翻译版”的内容,全部是按我实际开发经验梳理出的完整步骤:从方案选型、环境准备、3分钟快速部署,到设备树内存预留、CPU隔离、RT-Thread端适配,再到调试技巧和常见坑,一条链路讲清楚。
1. 为什么要在RK3568上做AMP双系统:项目需求与方案选型
1.1 什么样场景逼我做AMP:Linux太慢,裸机太单薄
先说结论:只有当你同时需要“Linux的生态”和“RTOS的确定性”时,才值得上AMP。否则单跑Linux或者单跑RT-Thread省事得多。
我之前做过一个八轴运动控制器,需求很典型:上位机通过网口或USB下发G代码,系统要解析路径、做插补运算,同时通过EtherCAT总线给伺服驱动器发周期同步指令。EtherCAT周期任务通常要求125微秒到1毫秒的同步周期,抖动必须控制在几十微秒以内,不然电机就会抖。这个任务单靠Linux进程调度很难保证,就算打RT-Preempt补丁,在核多、中断频繁的情况下,抖动依然不可控。另一个痛点是人机交互:触摸屏HMI、实时曲线显示、配方管理、远程升级,这些用Linux做太成熟了,你根本没必要自己造轮子。
如果不用AMP,常见的替代方案有两种。一种是外挂MCU跑实时任务,Linux和MCU之间走串口、SPI或者并口通信。这种方案问题也很明显:通信延迟不稳定,数据量大时容易成为瓶颈,而且增加一颗MCU既是成本也是故障点。另一种是Linux直接上RT-Preempt补丁,把内核变成抢占式实时内核。但工业现场对确定性要求高的任务,尤其像EtherCAT这种需要独立时间戳和高速总线控制的任务,还是不够“硬”。
AMP方案就是在同一颗SoC里,把实时任务放到独立的一个核上,用轻量级RTOS跑到最底层的控制逻辑。RK3568有4个A55核,性能在嵌入式SoC里属于能上台面的水平,拿1个核跑RT-Thread,3个核跑Linux,操作系统的负载压力都不大,核间通信靠共享内存和消息队列,延迟是纳秒级到微秒级,比外部MCU通信快两三个数量级。
1.2 为什么选RK3568而不是ZYNQ/STM32MP157
真正做选型的时候,我对比过ZYNQ-7010、STM32MP157、全志T113和RK3568。各有优劣,但从实际落地效率来看,RK3568在性能和资料生态之间平衡得最好。
| 平台 | 核心资源 | AMP支持程度 | 落地成本 | 适合场景 |
|---|---|---|---|---|
| STM32MP157 | 双核A7 + M4 | 原生支持OpenAMP,资料丰富 | 低 | 小规模实时控制、入门教学 |
| ZYNQ-7010 | 双核A9 + FPGA(内部无RTOS核,需外挂逻辑) | 可做异构,但需要FPGA开发能力 | 高 | 高性能信号处理、自定义硬件逻辑 |
| 全志T113 | 双核A7 + RISC-V/RClass? | 生态较弱,适配难度高 | 中 | 低功耗场景 |
| RK3568 | 四核A55 + G52 GPU + 0.8T NPU | Rockchip官方remoteproc/rpmsg方案,RT-Thread侧有适配 | 中低 | 工业控制器、机器人、边缘计算 |
RK3568最吸引我的点有三。第一,4核A55足够拿来“挥霍”,分出1个核给RT-Thread后,剩下3个核跑Linux应用照样很顺畅,不像双核A7那样分一个核出来Linux就剩半条命。第二,Rockchip的SDK里已经有AMP和remoteproc的底层支持,不需要自己从零写底层调度逻辑,设备树配好,镜像编译好,部署路径很清晰。第三,板卡价格已经被各开发板厂商打得很低了,几百块就能买到带全套SDK和原理图的板子,工程上试错成本几乎为零。
当然,ZYNQ这种FPGA+ARM的异构方案在高级运动控制场景也有优势,如果你想在FPGA里做硬件插补或者自定义编码器接口,它依然是更好的选择。但如果你只想“快速把RTOS和Linux跑起来”,ZYNQ的学习曲线和工具链复杂度会让你崩溃,而RK3568是开箱即用的路线。
1.3 AMP、SMP、BMP三者的区别
做AMP的人很多,真正把概念讲清楚的少。我顺便把多核处理的三种典型模式一次性说透。
SMP(对称多处理)是一个操作系统管理所有CPU核,任务由内核调度器动态分配到任意核上。普通Linux就是这样,你看到4个核都显示有负载,但其实只有一个内核镜像在统一调度。SMP优点是整体CPU利用率高,缺点是单任务的实时性不确定,因为你不知道它下一次会被调度到哪个核,也不知道会不会被其他任务挤掉。
BMP(捆绑多处理)是SMP的变种,它允许不同核跑不同的任务域,但仍然是同一个内核统一管理。比如用Linux的isolcpus参数把核3隔离开,专门跑某个实时进程,本质上还是Linux内核在管这个核,只是其他任务不让往里放。这个方案可以用来做软实时,但硬实时场景还是不够彻底。
AMP(非对称多处理)是两个或更多个独立操作系统各自管理自己的核,每个OS有独立的内核镜像、独立的内存空间、独立的中断处理。RK3568的AMP方案中,Linux管理0/1/2核,RT-Thread管理3号核,两边通过共享内存和核间中断通信。AMP的优势是实时任务完全不受Linux调度器影响,劣势是资源静态划分,不能像SMP那样动态负载均衡。但对实时控制器来说,静态划分换来确定性,这是值得的。
2. 3分钟快速部署:从编译固件到双系统跑通的完整链路
2.1 准备材料与镜像结构
先倒个冷水:标题里的“3分钟”是指从拿到已经配置好的SDK镜像和RT-Thread固件开始算,到你看到两个系统同时跑起来,确实可以做到3分钟。但第一次接触板卡、自己从头编译整个SDK,那肯定不止3分钟,那是另一码事。所以这个快速部署路径,本质上是在你手里已经有可用材料的前提下,用最短路径验证“AMP双系统”的效果。
你需要准备的材料如下:
- 一块RK3568开发板,正点原子、讯为、飞凌等常见板卡均可,SDK差别不大。我用的开发板带4GB LPDDR4,如果你的是2GB版本,内存预留地址和大小需要相应调整。
- 一条USB转串口调试线,115200波特率,用于看Linux和RT-Thread的日志输出。
- 确认板卡的启动方式,一般SD卡启动或EMMC烧录。快速迭代建议SD卡,不用反复擦写EMMC。
- 安装了Ubuntu的编译主机,用于编译Linux内核、设备树和RT-Thread固件。
- 官方SDK里已经编译好的boot分区镜像、kernel镜像、rootfs镜像,以及一个编译好的RT-Thread固件。RT-Thread固件可以先用官方demo的elf,跑通链路后再改成你自己的工程。
整体镜像结构上,Linux侧照常使用U-Boot、boot.img、kernel、dtb、rootfs,RT-Thread固件要么放在rootfs的/lib/firmware目录下,由Linux侧的remoteproc框架在运行时加载;要么由U-Boot在启动阶段直接加载到预留内存地址。这两种方式我后面都会讲到,快速部署阶段建议用第一种,调试最方便。
2.2 编译Linux侧固件和RT-Thread固件
Linux侧不用多说,RK3568的SDK编译流程是标准化的,一般一条命令全自动编完:
cd SDK ./build.sh lunch # 选择对应板卡配置 ./build.sh uboot ./build.sh kernel ./build.sh rootfs ./build.sh updateimg这里啰嗦一句,如果你不希望重新编整个Linux系统,只想改设备树和内核,那编译完kernel后把boot.img重新打一下包就行,不用碰rootfs。
RT-Thread固件编译前需要确认两件事:一是bsp是否对应RK3568,二是OpenAMP/共享内存的配置是否正确。以RT-Thread标准BSP为例,在环境里执行:
cd bsp/rockchip/rk3568 scons --menuconfig scons -j8编译产物是rtthread.elf和rtthread.bin。rtthread.elf是带符号表的镜像,调试时看栈回溯很关键,remoteproc加载固件时也推荐用elf格式,因为elf里包含了加载地址信息。rtthread.bin是纯二进制镜像,适合在U-Boot环境下直接加载。
RT-Thread侧重点要配置内存地址,尤其是Vring、资源表和共享内存地址,这三个地址必须和Linux侧设备树里预留的内存完全对得上。具体配置项我放到第4章详细讲,你第一步只要会用默认demo跑起来,先别改动。
2.3 3分钟部署实操:加载RT-Thread固件的最小步骤
假设你手上已经有一块能正常启动Linux的RK3568板卡,rootfs里已经放好了rtthread.elf,那么完整的最小部署路径如下:
第一步,板卡上电,进入Linux系统。记住,Linux端一定要已经放好对应板卡配置的dtb,并且内核开启了remoteproc支持。一般情况下Rockchip的默认内核配置里都已经编译进去了,不需要你折腾。
# 确认remoteproc设备是否注册成功 ls /sys/class/remoteproc/ # 应该看到一个或多个remoteprocX目录 cat /sys/class/remoteproc/remoteproc0/name # 输出类似 rockchip,rk3568-remoteproc第二步,把RT-Thread固件路径写入sysfs,并启动:
# 将固件复制到/lib/firmware目录 cp /path/to/rtthread.elf /lib/firmware/ # 告诉remoteproc要加载哪个固件 echo rtthread.elf > /sys/class/remoteproc/remoteproc0/firmware # 启动remoteproc echo start > /sys/class/remoteproc/remoteproc0/state # 观察内核日志 dmesg | tail -30正常情况下,你会看到remoteproc框架解析elf、加载镜像到预留内存、将核3从Linux中hot-unplug出来、启动RT-Thread的过程。RT-Thread自己的打印信息会输出到调试串口(或者你配置的另一个UART口),看到类似[RT-Thread] Starting amp demo这种日志,就说明双系统已经跑起来了。
第三步,验证两个系统之间的通信。如果你跑的是官方demo,Linux端通常有个rpmsg用户空间测试程序,执行一下能收到RT-Thread发过来的消息。没有现成程序的话,用devmem命令直接读共享内存区域也能判断RT-Thread是否正常工作:
# 比如共享内存基地址在0xA0000000,RT-Thread启动后会写入一个magic devmem 0xA0000000 32 # 如果输出不是0xFFFFFFFF,说明RT-Thread已经活了整个过程用不了3分钟。如果你用的是开发板厂商出厂就带了AMP demo的镜像,那更简单,串口控制台敲两三条命令就完事。这个快速路径的价值在于,它让你先在“黑盒可用”的状态下建立信心,再去深入理解配置原理。
假设你已经跑通了上面的步骤,接下来才是真正的“硬核”部分:搞懂为什么这样配,以及将来怎么改成自己的工程。
3. Linux侧核心配置:内存预留、CPU隔离与设备树修改
3.1 预留内存:reserved-memory节点写法
在Linux侧做AMP的第一步,是把一块物理内存从Linux的内存管理器中隔离出来。这块内存RT-Thread要用,Linux内核不能碰。如果忘了隔离,Linux的页面分配器极有可能把这块内存分配出去,然后RT-Thread往里写数据,接着就是各种神仙打架般的崩溃,最恐怖的是这种崩溃还不是必现的,偶尔跑几个小时才崩一次,排查起来非常头疼。
设备树里预留内存的标准写法如下,我以一块从0xA0000000开始、大小32MB的预留区域为例:
/ { reserved-memory { #address-cells = <2>; #size-cells = <2>; ranges; amp_rtthread: amp-rtthread@a0000000 { compatible = "shared-dma-pool"; reg = <0x0 0xA0000000 0x0 0x02000000>; no-map; }; }; };关键点就是reg里的起始地址和大小,以及no-map属性。no-map的意思是这块区域不建立页表映射,Linux侧在MMU层面完全看不见这32MB,RT-Thread物理地址直接访问就行,双方不会由于页表缓存导致一致性问题。
大小怎么算?你需要覆盖四块内容:RT-Thread的代码段和数据段(一般8MB到16MB足够)、RT-Thread内核堆栈和线程堆栈、共享内存区域、OpenAMP需要的Vring缓冲区(一般是4KB到64KB)。总之,32MB是一个比较稳的尺寸,想省内存给Linux的话,16MB也能跑通,但需要仔细排布,别到时候RT-Thread堆空间不够导致malloc溢出。
地址怎么选?RK3568的DDR地址空间从0x00000000开始,外设寄存器映射在0xFDC00000以上。开发板常见的DDR配置有1GB/2GB/4GB/8GB,如果板子是4GB内存,那物理地址0x80000000到0xFFFFFFFF是高位内存区域。工程上建议把预留内存放在Linux常用内存对应的低位区间(比如0x08000000到0x80000000之间),或者放在DDR靠后的高位区间,取决于你的内存大小。以4GB板子为例,我习惯用0xA0000000,避免和内核的线性映射区挤在一起,也方便U-Boot加载固件时做地址换算。
3.2 CPU隔离:让一个A55核完全交给RT-Thread
内存隔离好了,接下来是CPU隔离。我们要让Linux完全不知道核3的存在,不对它做调度,不让普通任务跑上去,同时也要让内核不要往核3发定时器中断、处理器间中断和RCU回调。
最简单的做法是在内核启动参数里加一行:
maxcpus=3这会让内核在启动过程中最多只初始化3个CPU,核3在Linux视角里直接不存在。这样最干净,但我必须提醒你,maxcpus=3会导致CPU核编号在启动早期就固定下来,某些CHAINED中断控制器或者GIC驱动会在启动时假设CPU掩码,个别SDK可能会有兼容性问题。实际测试下来,大部分RK3568 SDK没问题,但如果你遇到内核启动崩溃,可以先查是不是maxcpus太激进了。
更温和的隔离做法是保留4核,但把核3从调度器中摘出去:
isolcpus=3 nohz_full=3 rcu_nocbs=3isolcpus=3告诉进程调度器默认不要往核3上放任务,nohz_full=3让核3上的内核时钟事件尽量延后,减少周期性tick中断,rcu_nocbs=3把RCU回调线程从核3上挪走。这套组合拳能显著降低核3上的内核活动。但它依然存在一个问题:别的核发起reschedule IPI时,目标核如果没有任务,一般不会触发,但系统级的事件(比如全局定时器、性能计数器中断)还是会进入到核3。如果你想追求确定性,推荐在RT-Thread启动后,Linux侧再用cpu hotplug方式将核3下线:
echo 0 > /sys/devices/system/cpu/cpu3/online这样核3就从Linux的online集合里彻底消失了。remoteproc框架在启动RT-Thread的过程中其实会自动执行类似操作,所以你手动操作的时候要注意:别在RT-Thread已经跑起来后,又把核3重新online成Linux核,那会直接把RT-Thread的系统状态冲掉。
我个人的建议是:开发阶段先用isolcpus+rcu_nocbs这套组合,不彻底下线核3,这样Linux系统不会因为少了一个核而行为异常;产品阶段再将核3通过hot-unplug下线,固化和RT-Thread的边界。
3.3 remoteproc/rpmsg框架与设备树节点
remoteproc是Linux内核里专门用于管理远程处理器的框架,它负责加载固件、启动远程处理器、通知远程处理器停止,以及处理优雅停机。rpmsg则是建立在remoteproc之上的消息通信框架,通过共享内存和Vring实现端到端消息传递,对用户来说就像操作一个虚拟串口。
在RK3568的设备树中,remoteproc节点通常是系统默认存在的,但默认状态可能是disabled,需要由板级dts显式启用并配置内存地址。下面以常见RK3568 SDK写法为例,不同SDK细节会有差异,但思路一致:
&remoteproc { compatible = "rockchip,rk3568-remoteproc"; firmware-name = "rtthread.elf"; memory-region = <&_rtthread>; status = "okay"; };firmware-name就是默认的固件文件名,memory-region指向你第3.1节里预留的内存节点。如果SDK里remoteproc节点由某个平台驱动直接管理,这里可能还会多一项vring配置:
vring0 = <&_vring0>; vring1 = <&_vring1>;这些Vring缓冲区的地址必须和RT-Thread编译时的配置严格一致。我见过太多人在这里栽跟头:Linux侧设备树里预留的是0xA0000000,RT-Thread编译时OpenAMP配置却写的是0xA1000000,结果两边消息永远收不到,还找不到原因,因为日志显示两个系统都“正常”跑着,只是互相不说话。
改完设备树后,重新编译内核和dtb,烧录启动。启动后重点检查一下:
cat /proc/iomem | grep -A1 reserved如果看到类似a0000000-bfffffff : amp_rtthread的打印,说明预留内存生效了。还要确认remoteproc设备树节点状态变成了okay。
到了这一步,Linux侧的骨架就搭完了:内存隔离开、CPU隔离开、remoteproc框架接管固件加载。接下来就看RT-Thread那边怎么配合了。
4. RT-Thread端适配:从纯净工程到可通信的AMP核
4.1 RT-Thread侧OpenAMP移植
RT-Thread作为AMP的远端处理器,需要把自己编译成能独立运行在单核上的完整RTOS镜像。RT-Thread本身对OpenAMP有原生支持,在menuconfig里找到OpenAMP相关的软件包,启用rproxy/rpmsg配置后,再把板级地址配置改成和Linux侧预留的内存一致就行。
配置项中最关键的几个参数:
| 配置项 | 典型值 | 说明 |
|---|---|---|
| CPU ID | 3 | 表示RT-Thread运行在第3号核,用于核间中断和目标核识别 |
| Memory Base | 0xA0000000 | 与Linux侧reserved-memory的reg地址一致 |
| Memory Size | 32MB | 与Linux侧预留大小一致,超过会踩到Linux的内存 |
| VRING0/VRING1 | 0xA0000000 + offset | 必须落在预留内存内,不能和代码段、堆重叠 |
| Resource Table | 0xA0000000 + offset | 存放资源表,remoteproc启动时依赖它协商Vring |
RT-Thread侧的具体修改点通常在bsp的board.c或link.lds里,你需要把RT-Thread的镜像加载地址设置为预留内存的起点(或略偏移一点),把堆空间也分配在预留内存里。工程实践中有个习惯:把RT-Thread镜像放在预留内存的低地址区域,比如0xA0000000,共享内存放在中间偏移,Vring放在最后。这样做的好处是内存布局直观,两边一旦对不上,用串口读一下地址内容就能定位。
编译确认无误后,会重新生成rtthread.elf。这里有个小建议:正式开发时编译出来elf后,先用readelf检查一下节区地址是否落在了你预设的预留范围内,别等到加载时才发现溢出了。
4.2 共享内存与核间通信
OpenAMP的rpmsg适合传输“消息”,数据量小、格式灵活。但在实际工业控制里,大部分场景是“RT-Thread周期采样,Linux周期下发参数”,比如RT-Thread每1毫秒采集一次编码器数据并写入共享内存,Linux从共享内存中读到最新值去刷新HMI界面;Linux下发一条速度指令,RT-Thread读到后立刻执行。这种场景用自定义共享内存结构体会更高效,不用每笔小数据都走一次rpmsg通道。
我常用的共享内存结构体大概长这样:
#define SHM_BASE 0xA1000000 #define SHM_MAGIC 0x47414D50 /* "GAMP" */ typedef struct { uint32_t magic; /* 魔数,用于握手 */ uint32_t cmd; /* Linux -> RT-Thread 的控制命令 */ uint32_t status; /* RT-Thread -> Linux 的状态反馈 */ int32_t joint_speed[8]; /* 速度指令 */ int32_t joint_pos[8]; /* 位置反馈 */ uint32_t heartbeat; /* RT-Thread心搏计数,Linux侧可用来判断RT-Thread是否活着 */ } amp_shm_t; #define AMP_SHM ((volatile amp_shm_t *)(SHM_BASE))RT-Thread侧在初始化阶段把magic写入共享内存,Linux侧用户空间程序通过devmem或mmap读到magic非0后,确认RT-Thread已经上线,然后开始周期轮询status字段。RT-Thread侧则在周期任务里不断更新状态和位置数据,同时检查cmd有没有新指令。
Linux用户空间程序如果只是调试,用devmem命令最方便:
# 写入magic验证:如果RT-Thread能看到并回写,则说明共享内存通路正常 devmem 0xA1000000 32 0x47414D50 # 读回RT-Thread状态 devmem 0xA1000000 32生产环境不建议直接用/dev/mem裸操作,更稳妥的是写一个UIO驱动或者普通字符设备驱动,把物理地址映射到用户空间,同时加一些访问权限控制。但原理完全一样,本质都是在共享内存上做无锁读写,配合内存屏障保证顺序。
这里必须提一个关键点:两个CPU访问同一块共享内存,怎么保证数据一致性?RK3568的A55四核共享同一个L2缓存,同一个簇内的缓存一致性是由硬件监听的,实测下来直接读写共享内存大部分时候没问题。但从工程严谨性出发,我仍然强烈建议把共享内存区域配置为non-cacheable,或者在每次读写后显式执行缓存清理指令(DSB/ISB)。否则某个深夜你可能会面对这样一个诡异bug:Linux侧写一个变量,RT-Thread侧读出来却是旧值,因为数据还留在cache里没写回DDR。这种问题用仿真器和示波器都很难抓,只能从机制上堵塞。
4.3 典型业务:用RT-Thread跑EtherCAT周期任务
聊完通信,我们落到一个实际业务上:EtherCAT。做工业控制的人对EtherCAT一定不陌生,它基于以太网,但和普通以太网不同,EtherCAT主站要求极其严格的时间同步和周期确定性。常见做法有两种流派。
一种是把IGH主站跑在Linux上,配合RT-Preempt补丁,让主站的周期任务在RapidIO上下文里运行。这种方案省事,因为IGH的主站逻辑、网卡驱动、邮箱协议都是现成的,你只需要在Linux里编写应用层,把控制指令和状态数据透传给运动规划模块。但缺点是抖动比较大,负载一高、网络一忙,周期就可能被挤掉,严重时EtherCAT分布式时钟会导致伺服报警。
另一种就是我们现在说的AMP方案:让RT-Thread独占一个核,在这个核上实现EtherCAT主站的核心周期逻辑,网卡中断由RT-Thread直接接管,周期任务通过RT-Thread的定时器调度,实测抖动通常能控制到个位数微秒;Linux核只负责跑界面、文件系统、远程调试。两边通过共享内存或者rpmsg交换PDO数据。这个结构下,IGH主站跑在Linux侧其实是作为“上位配置工具”存在的,真正的周期主站逻辑已经移到了RT-Thread侧,所以标题搜索结果里“适配RK3568的EtherCAT IGH主站驱动”实际上可能把两种路线都提到了,但真正要拿到高确定性,AMP是更合适的答案。
如果你真的要在AMP下跑IGH主站,还有一个变通方案:Linux侧跑IGH主站,但把网卡中断和发送周期任务绑定到RT-Thread独占的核上?这里要小心,因为网卡驱动是Linux的设备模型,RT-Thread无法直接接管它,跨系统共享一个网卡设备很不现实。所以我的建议是:周期任务的核心尽量放RT-Thread裸核上,Linux侧只做非周期应用,这样系统边界最干净,也好排查故障。
5. 调试技巧与踩坑实录:AMP项目里最常见的那些坑
5.1 怎么确认RT-Thread真的独占了一个核
跑通之后,第一个要确认的是:RT-Thread是不是真的“独占”了核3。光看日志说“I am running”不够,你要看Linux侧有没有往这个核塞过东西。
核3被remoteproc接管并hot-unplug后,在Linux侧执行:
cat /sys/devices/system/cpu/online # 正常情况下只会输出0-2,而不是0-3另外多观察几次:
cat /proc/interrupts | grep CPU3正常情况下CPU3那一列应该全部是0,或者CPU3直接不出现在表格里。如果你看到CPU3上还有大量Local timer中断、Rescheduling中断或者IPI中断,说明核3并没有被真正隔离干净,RT-Thread的实时性会受影响。
还有一个排查技巧:在Linux侧跑一个CPU烧录程序,同时打开RT-Thread的日志窗口,观察RT-Thread的心跳是否稳定。如果心跳间隔出现明显的毛刺,比如原本1ms的心跳抖到2ms,先用上面的方法查一下Linux侧是不是有中断打到了核3。干扰源通常来自高优先级全局定时器,或者未经过隔离配置的RCU线程。
5.2 内存冲突与缓存一致性
AMP排错时最费时间的往往不是逻辑错误,而是内存问题。我遇到过最典型的一个bug是这样的:Linux侧设备树里预留了内存,但U-Boot阶段忘了给U-Boot本身也做相同配置,导致U-Boot在启动Linux之前,把内核镜像解压到了预留内存区域。结果是RT-Thread的镜像被Linux内核镜像覆盖,RT-Thread永远启动不起来。这种问题不会在日志里报错,只会表现为“一切看起来正常,但RT-Thread就是卡死”。
排查方法其实很简单:Linux启动后用devmem读一下预留内存地址,确认里面的内容到底是RT-Thread镜像和解压后的内核数据,还是全F的空白区域。如果发现预留内存的内容被Linux内核覆盖了,基本可以断定是U-Boot阶段的布局没对齐。
缓存一致性问题多发生在Linux侧使用DMA外设的场合。比如你让GPU或VPU往共享内存区域写入数据,但这些硬件访问的是物理内存,跳过了CPU cache,如果共享内存区域不是non-cacheable,CPU读到的还是旧值。解决方案就是在设备树里把共享内存区域标记为no-map同时加上shared-dma-pool属性,或者让DMA使用cache maintenance操作,在每次DMA传输完成后执行dma_sync_single_for_cpu。
另外一点,写的共享内存代码一定要带volatile或者用原子操作包装,防止编译器优化导致数据只在寄存器里转圈。RT-Thread端和Linux端编译优化级别越高,越容易出现这种幽灵bug,我建议共享内存相关的代码文件统一用-O0或者-O1编译,虽然性能差点,但排错成本能降低一大截。
5.3 启动顺序与串口日志
AMP的启动顺序有两条路线,调试方式完全不同。
路线一是U-Boot先启动RT-Thread,再启动Linux。在U-Boot命令行环境里,把rtthread.bin加载到预留内存地址,然后跳转执行,等RT-Thread初始化完,再继续boot Linux。这种方式适合“上电后实时任务必须立即工作”的场景,缺点是RT-Thread先跑时,Linux还没起来,你只能通过独立串口看RT-Thread日志,不方便集中调试。
路线二是Linux先完全启动,再由remoteproc加载RT-Thread。这是最常用的开发调试路径,因为Linux环境健全,文件系统、网络、SSH都能用,加载固件、重启RT-Thread都只需要敲命令。缺点就是Linux启动花几秒钟,如果设备对“上电即响应”要求严格,就需要改走路线一。
串口日志是一个很容易被忽略的大坑。RK3568开发板一般只有一个UART调试串口,如果你让Linux和RT-Thread同时把日志打到同一个串口,会看到两者互相穿插,简直无法阅读。我的建议是:RT-Thread运行时把日志输出单独接到另一个UART口(或者复用某个空闲引脚,设备树里配置成uart功能),用另一根串口线去读。两台日志软件同步记录,时间戳对比起来就很方便。
如果板子实在没有多余UART,还有一个土办法:RT-Thread侧日志前统一加一个固定的[RTT]前缀,Linux侧日志不打印或加[LINUX]前缀,然后输出到同一串口,用文本过滤工具一拆就清晰了。虽然丑,但应急管用。
5.4 避坑速查表:从现象直接到解决方向
最后整理一个速查表,把AMP项目里最容易遇到的现象、可能原因和排查方向都列出来。这个表我强烈建议收藏,调试时直接对着找:
| 故障现象 | 可能原因 | 排查方向与命令 |
|---|---|---|
| RT-Thread固件加载后无任何输出 | 固件路径错误、地址被占、remoteproc没注册 | 检查dmesg,确认/lib/firmware存在,确认/sys/class/remoteproc下有设备 |
| RT-Thread起来几秒后崩溃 | 预留内存被Linux覆盖 | 用devmem读预留内存起始地址,比对镜像内容与RT-Thread符号表 |
| Linux系统开机后频繁panic | reserved-memory跟DDR初始化配置冲突 | 检查U-Boot的memreserve,确保与设备树一致 |
| 两边日志严重穿插,无法阅读 | 共用串口 | 换独立UART,或用带前缀过滤的方式 |
| rpmsg消息发不出去 | Vring地址不一致 | 两端交叉确认Vring0/Vring1地址,重新编译 |
| Linux核3出现大量中断 | CPU隔离没生效 | 检查isolcpus/nohz_full参数,检查是否执行了hot-unplug |
| 共享内存读到的值忽对忽错 | 缓存一致性 | 把共享内存区域设为non-cacheable,或增加内存屏障 |
| RT-Thread实时任务抖动大 | 核隔离不彻底,IPI干扰 | 用/proc/interrupts统计CPU3中断,确认remoteproc已下线核3 |
| U-Boot加载RT-Thread后Linux引导失败 | U-Boot没有为预留内存做memreserve | 在U-Boot配置中增加内存保留节点,地址匹配设备树 |
| RT-Thread能启动但不更新共享内存数据 | 没写明volatile或访问了错误地址 | 检查编译优化级别,用readelf确认段地址 |
5.5 一个建议:先用官方demo跑通,再自己改
如果只能给一个建议,我会说:第一次做RK3568 AMP,千万不要一上来就自己从零搭工程。Rockchip的SDK里通常带着一份现成的AMP demo,里面有编译好的RT-Thread固件、设备树文件、Linux端测试程序,整个链路已经验证过是通的。先把这个demo原封不动地烧进去,确认你能看到两个系统跑起来,再进行定制。
定制顺序也不要有一步到位的想法。先把RT-Thread固件换成自己编译的版本,但设备树和Linux侧不变,验证是不是“只要换固件就能通”;再把共享内存区域换成自己的结构体,验证通信;最后再改CPU隔离、改U-Boot,每一步之间留好验证点。AMP的问题大部分是配置不同步造成的,分层验证能让问题隔离在一个很小的范围内。
我第一次在RK3568上调AMP时,花了整整一个下午排查一个“RT-Thread偶尔启动失败”的问题,最后发现是U-Boot的FDT加载地址和预留内存地址重合了,U-Boot启动Linux时把设备树写到了RT-Thread的镜像区域。这种地址重叠问题靠看日志完全看不出来,只有把内存布局打印出来一个个对比。后来我把开发板的U-Boot、内核、设备树、RT-Thread四个镜像的内存占用画在一张图里,才真正理解了AMP内存管理的核心——所有问题,最后都是内存布局问题。