RK3568混合部署实战:AMP架构下RT-Thread与Linux双系统协同设计 📅 发布时间:2026/8/19 14:52:27 👁 浏览次数: 1. 项目缘起为什么要在RK3568上玩混合部署最近在折腾一个边缘计算网关的项目核心需求是在一个设备上既要跑一个实时性要求高的数据采集和控制任务又要运行一个功能丰富的Web管理界面和数据库服务。如果全用Linux实时性任务在系统负载高时难免有延迟抖动如果全用RT-Thread那些复杂的网络服务和图形界面开发起来又比较费劲。这不就想到了混合部署这条路子——让两个系统各司其职跑在同一块芯片上。手头正好有迅为电子的iTOP-3568开发板核心是瑞芯微的RK3568。这颗芯片有四个Cortex-A55核心性能足够而且官方BSP支持比较完善是尝试这种“一芯两用”玩法的理想平台。RT-Thread以其出色的实时性和小巧的内核著称Linux则拥有庞大的软件生态把它们俩凑到一块听起来就像让一个严谨的工科生和一个创意十足的设计师搭档干活理论上能迸发出不小的能量。但说实话一开始我心里也没底。虽然知道有AMP非对称多处理器这种架构但具体到RK3568这块板子上怎么把两个系统的镜像烧进去内存怎么划分两个系统之间怎么“打电话”通信启动流程谁来主导这些细节在官方文档里往往一笔带过或者散落在不同的地方。这次折腾就是要把这条从零开始的路蹚明白把踩过的坑和最终跑通的方案记录下来。2. 混合部署的基石理解AMP架构与RK3568的硬件底子在单颗RK3568上同时运行RT-Thread和Linux并不是靠虚拟机或者容器技术而是依赖于芯片底层的一种硬件能力——AMP模式。要玩转这个得先对硬件和基础概念有点数。2.1 什么是AMP非对称多处理器你可以把RK3568的四个A55核心想象成一个四人间宿舍。在通常的Linux系统中就像宿舍长Linux内核管理着所有四个人给大家分配任务进程/线程大家共用客厅的内存、卫生间的外设。这就是SMP对称多处理器模式。而AMP模式则是把这个四人间从中间隔开变成两个独立的单间。我们指定一个核心比如CPU0给RT-Thread让它独占这个房间和一部分家具内存、外设。剩下的三个核心CPU1-3和另一部分家具则留给Linux。两个系统有各自独立的“房间”执行环境互不干扰。RT-Thread在自己的房间里可以专心致志地处理那些要求定时精准、响应快的任务比如每毫秒读取一次传感器数据而Linux则在它的空间里从容地运行Nginx、Python甚至数据库这些大家伙。这种隔离带来了确定性的实时性能因为RT-Thread的核心不会被Linux的调度器抢占或打扰。同时两个系统又能通过一些共享的“传话筒”如共享内存、邮箱中断进行必要的通信协同工作。2.2 RK3568为混合部署提供了哪些硬件支持RK3568的硬件设计为这种玩法开了绿灯这是我们能实现混合部署的前提多核启动与电源管理芯片的BootROM支持从多个核心启动。我们可以配置让CPU0从某个地址比如存放RT-Thread镜像的地方开始执行而让CPU1-3从另一个地址比如Linux内核的入口启动。同时每个核心可以独立地被上电、下电或复位这为系统间隔离和故障恢复提供了可能。内存控制器与地址空间RK3568的DDR内存地址空间是统一的例如0x00a00000 - 0xFFFFFFFF。在AMP模式下我们需要在系统启动前就通过配置文件或编译脚本明确地、物理地划分出一块内存给RT-Thread例如0x00a00000 - 0x01ffffff另一块给Linux0x02000000 - 0xFFFFFFFF。两个系统在各自的“领地”内活动不能越界访问否则会导致内存访问错误或系统崩溃。这个划分是静态的在运行时一般不会改变。中断控制器GIC中断是系统响应外部事件的关键。RK3568使用GICv2中断控制器。在AMP配置下我们需要仔细分配中断号。例如将某个GPIO中断、定时器中断分配给RT-Thread处理而将网卡、USB等设备的中断分配给Linux。这通常在设备树Device Tree中进行配置告诉每个系统它拥有哪些中断资源。外设与IO映射和内存类似芯片上的外设UART, I2C, SPI, GPIO等其寄存器都有特定的物理地址。在AMP中我们必须决定哪个系统“掌管”哪个外设。例如可能将UART0分配给RT-Thread用于调试打印将UART1、UART2分配给Linux将某组I2C和GPIO分配给RT-Thread连接传感器另一组给Linux。这种分配同样需要在设备树中清晰定义避免两个系统同时去配置同一个外设寄存器造成冲突和不可预知的行为。注意硬件资源的划分内存、中断、外设是AMP方案设计中最关键、也最容易出错的一步。划分不合理会导致系统无法启动或功能异常。一个基本原则是确保任何硬件资源在任意时刻最多只被一个系统主动访问和控制。3. 实战准备构建双系统镜像与划分硬件资源理论清楚了接下来就是动手。我们需要准备两个系统的可执行文件并告诉它们各自的“地盘”在哪里。3.1 RT-Thread系统侧的准备对于RT-Thread我们通常需要编译生成一个rtthread.bin文件。这里以迅为提供的BSP为例。获取与配置BSP从迅为官方或RT-Thread GitHub仓库获取rt-thread/bsp/rockchip/rk3568的代码。进入BSP目录重点修改board/Kconfig和board/SConscript等文件。我们的目标不是让RT-Thread驱动整个开发板而是驱动我们分配给它的那部分资源。关键配置链接脚本与内存定义修改链接脚本通常是board/linker_scripts.ld明确指定RT-Thread的代码、数据存放的物理地址。例如MEMORY { RAM (rwx) : ORIGIN 0x00a00000, LENGTH 24M /* RT-Thread独占24MB内存 */ }在rtconfig.h或board.h中通过RT_HW_HEAP_BEGIN和RT_HW_HEAP_END来定义RT-Thread的动态内存堆范围必须落在上面定义的RAM区域内。在board.c的rt_hw_board_init()函数中只初始化分配给RT-Thread的那些外设比如特定的UART、GPIO、定时器。对于Linux管理的外设不做任何操作。编译与生成使用scons命令进行编译。最终在BSP目录下生成rtthread.bin或rtthread.elf。这个文件包含了RT-Thread内核、我们编写的应用代码以及初始化数据其加载地址就是我们链接脚本中指定的ORIGIN(RAM)。3.2 Linux系统侧的准备Linux侧的工作主要围绕设备树Device Tree展开。设备树是描述硬件资源的一块数据Bootloader通常是U-Boot会把它传递给Linux内核。在AMP场景下我们需要准备一个“裁剪过”的设备树。获取Linux内核与标准设备树使用迅为提供的Linux SDK其中包含内核源码和针对iTOP-3568的标准设备树文件例如rk3568-itop-3568.dts。修改设备树以适配AMPCPU节点在/cpus节点下将分配给RT-Thread的核心如cpu0的状态status设置为disabled。这样Linux内核在启动时就会忽略这个核心不会去初始化它。cpus { cpu0: cpu0 { device_type cpu; compatible arm,cortex-a55; reg 0x0 0x0; enable-method psci; status disabled; /* 关键让Linux忽略CPU0 */ }; cpu1: cpu1 { status okay; /* Linux管理CPU1-3 */ }; // ... cpu2, cpu3 };内存节点修改/memory节点将Linux可用的内存范围调整为划分后的部分。例如如果RT-Thread占用了0x00a00000 - 0x01ffffff24MB那么Linux的内存就从0x02000000开始。memorya00000 { device_type memory; reg 0x0 0x02000000 0x0 0xfe000000; /* 起始 0x02000000, 大小 ~254MB */ };外设节点将分配给RT-Thread的外设节点全部status disabled;。例如如果UART0给RT-Thread就在uart0节点中添加status disabled;。确保Linux不会去驱动这些设备。保留内存Reserved Memory这是一个非常重要的步骤我们需要明确告诉Linux有一块内存即RT-Thread使用的区域已经被占用了内核和用户程序绝对不能使用。这通过/reserved-memory节点实现。reserved-memory { #address-cells 2; #size-cells 2; ranges; rtos_reserved: rtosa00000 { reg 0x0 0x00a00000 0x0 0x02000000; /* RT-Thread的24MB地盘 */ no-map; /* 关键告诉Linux不要映射此区域完全保留 */ }; };编译设备树使用内核的DTC工具编译修改后的.dts文件生成.dtb文件make dtbs。3.3 制作统一的Firmware镜像现在我们有rtthread.bin和linux.dtb以及Linux内核镜像Image、根文件系统rootfs.img。我们需要一个“总指挥”来安排它们各就各位。这个总指挥通常是U-Boot但镜像的打包格式需要遵循Rockchip的约定。Rockchip平台通常使用其专属的rkbin工具和loader来初始化DDR并加载镜像。我们需要创建一个统一镜像包其中按顺序包含了Loader(如rk3568_loader_v1.xx.bin): Rockchip的初阶引导。U-Boot(u-boot.itb): 主引导程序。RT-Thread镜像(rtthread.bin): 放置在约定的物理地址如0x00a00000。Linux内核镜像(Image): 放置在另一个地址如0x02000000。设备树(linux-amp.dtb): 放置在紧邻内核的位置。根文件系统(rootfs.img): 可以是单独分区也可以打包进镜像。可以使用tools/mkimage脚本或编写一个打包脚本来完成这个工作确保每个组件在最终镜像中的偏移地址是准确的。最终生成一个firmware.img文件用于烧录。4. 启动流程深度解析从芯片上电到双系统并行混合部署的启动链比单系统要复杂理解每一步有助于调试。BootROM阶段芯片上电后内置的ROM代码运行。它会根据Boot引脚配置从指定的存储介质如eMMC的特定扇区加载第一级Loader到SRAM中执行。Loader阶段第一级Loader来自rkbin初始化最基本的外设如时钟、DDR控制器然后将第二级Loader通常是U-Boot SPL和U-Boot Proper加载到DDR中。U-Boot阶段U-Boot Proper开始执行。它从存储设备或网络加载我们打包好的firmware.img到DDR的临时缓冲区。U-Boot解析镜像格式将rtthread.bin拷贝到其指定的物理地址0x00a00000。将Image和linux-amp.dtb拷贝到它们约定的加载地址如0x02000000和0x04000000。关键一步启动RT-Thread。U-Boot通过ARM的SMC安全监控调用或PSCI电源状态协调接口命令将CPU0从“等待事件”状态唤醒并跳转到rtthread.bin的入口地址0x00a00000开始执行。此时CPU0开始独立运行RT-Thread。启动Linux在完成RT-Thread启动或并行地U-Boot使用booti或bootm命令将CPU1-3唤醒并跳转到Linux内核的入口地址Image的加载地址同时将设备树linux-amp.dtb的地址传递给内核。Linux内核开始在其分配的内存中启动它从设备树中知道自己只管理CPU1-3以及部分内存和外设。双系统并行运行此时CPU0独立运行RT-ThreadCPU1-3运行Linux。两个系统在物理内存和硬件资源上完全隔离。RT-Thread会初始化自己的串口、定时器、任务调度器等。Linux内核会进行解压、设备树解析、驱动初始化、挂载根文件系统最终启动用户空间的init进程。踩坑实录启动顺序很重要务必先启动RT-Thread再启动Linux。因为如果Linux先启动它可能会对所有的CPU核心进行一些底层的初始化比如缓存、MMU这可能会干扰到后来在CPU0上运行的RT-Thread。让RT-Thread先“占住”CPU0是一种更稳妥的做法。5. 核心挑战实现RT-Thread与Linux间的通信系统跑起来了但它们是两个孤岛。要让它们协同工作必须建立通信机制。这里介绍两种最常用、最实用的方法。5.1 基于共享内存Shared Memory的数据交换这是最高效的通信方式适合传输大量数据或状态信息。原理是在物理内存中划出一小块区域例如1MB这块内存在两个系统的页表里都映射到各自的虚拟地址空间并且都能读写。实现步骤定义共享内存区域在设备树的/reserved-memory节点中除了RT-Thread的独占区域再增加一块shared_memory: shared1f000000 { reg 0x0 0x1f000000 0x0 0x00100000; /* 1MB共享内存 */ no-map; /* Linux不自动映射需驱动处理 */ };Linux侧驱动开发编写一个内核模块shmem.ko。在模块初始化时通过ioremap或memremap将这块物理内存0x1f000000映射到内核的虚拟地址空间。可以暴露一个字符设备/dev/shmem给用户空间让应用程序能够读写这块内存。或者更简单点直接通过/proc/iomem告知应用层这块内存的物理地址让应用层通过/dev/mem来访问需注意安全。RT-Thread侧访问在RT-Thread中由于我们通常使用物理地址直接访问关闭MMU或使用恒等映射我们可以直接定义一个指针指向0x1f000000这个物理地址然后像操作普通数组一样读写数据。同步机制共享内存本身没有同步能力。为了避免两个系统同时写造成数据混乱需要引入简单的软件同步机制比如自旋锁或信号量。可以在共享内存的开头定义几个变量作为“锁”。由于RK3568是ARMv8-A支持原子操作如LDREX/STREX指令可以在RT-Thread和Linux内核模块中分别实现基于原子操作的锁来保护共享数据区。实操心得共享内存的地址一定要在设备树中预留好并且确保两个系统映射的物理地址一致。首次测试时可以先在共享内存中定义一个简单的结构体包含一个计数器和一个锁变量。RT-Thread每秒递增计数器Linux应用每秒读取并打印。这是验证通信链路是否打通的最快方法。5.2 基于中断Interrupt的事件通知共享内存解决了数据“是什么”的问题中断则解决了“什么时候有数据”或“什么时候该做什么”的问题。我们可以利用一个GPIO引脚产生边沿信号或者使用一个共享的片上邮箱Mailbox硬件如果RK3568支持来触发跨系统中断。这里以更通用的GPIO中断为例硬件连接选择一个未被其他功能占用的GPIO引脚例如GPIO0_A0。用一根杜邦线将其连接到一个空闲的GPIO引脚例如GPIO0_A1。实际上在同一个芯片内部我们只需要在软件上配置一个GPIO为输出另一个为输入即可物理上它们内部是连通的。Linux侧配置发送端将GPIO0_A0配置为输出模式。当Linux需要通知RT-Thread时例如新的控制命令已写入共享内存就通过写GPIO寄存器将该引脚电平拉高或拉低。RT-Thread侧配置接收端在RT-Thread的设备树或板级配置中将GPIO0_A1配置为中断输入模式上升沿或下降沿触发。在RT-Thread中编写对应的中断服务函数ISR。当检测到引脚电平变化时ISR被触发。在ISR中进行必要的处理例如读取共享内存中的命令或者设置一个信号量/事件标志唤醒一个高优先级的任务来处理具体事务。关键点需要在设备树中确保这个GPIO的中断号是分配给RT-Thread的而不是Linux。这在前面的设备树裁剪中已经完成。双向通知同理可以再用另一组GPIO实现从RT-Thread到Linux的中断通知。避坑指南GPIO中断是共享内存通信的完美补充。但要注意中断去抖。在软件中可以在中断触发后延迟几毫秒再读取GPIO状态进行确认或者在ISR中暂时关闭该中断由任务处理完后再重新开启避免短时间内多次触发中断导致系统负载过高。6. 调试技巧与常见问题排查混合部署的调试是“双线作战”需要一些特别的工具和思路。串口调试为两个系统分配独立的串口是最理想的。例如UART0给RT-Thread的consoleUART1给Linux的console。这样你可以通过两个串口终端分别观察两个系统的启动日志和打印信息。如果只有一个串口可以尝试让RT-Thread和Linux分时复用但会非常混乱不推荐。LED与GPIO调试法在关键代码路径如RT-Thread启动完成、任务开始运行、收到中断设置不同的GPIO电平用示波器或逻辑分析仪观察波形是判断执行流和时序的硬核方法。常见启动失败问题现象RT-Thread或Linux卡住没有任何输出。排查检查内存划分这是头号嫌疑犯。确认rtthread.bin的链接地址、Linux设备树中的memory节点和reserved-memory节点三者定义的地址范围没有重叠且都在有效的DDR地址空间内。检查U-Boot加载地址使用U-Boot的md内存显示命令在RT-Thread和Linux的加载地址处查看内容确认镜像是否正确加载。例如在U-Boot中执行md 0x00a00000看开头几个字节是不是RT-Thread的魔数或可执行代码。检查CPU状态在U-Boot中使用smc或psci命令手动尝试启动CPU0到指定地址观察是否有反应。使用cpu info或类似命令查看各核心状态。简化测试先尝试只启动RT-Thread在U-Boot中不启动Linux确保RT-Thread能独立运行。再尝试只启动Linux在设备树中不禁用CPU0确保Linux能正常运行。最后再组合。通信失败问题现象共享内存数据不同步或中断无法触发。排查共享内存在Linux内核启动后通过/proc/iomem查看预留内存区域是否成功。在Linux用户空间尝试用devmem工具直接读写共享内存的物理地址看是否能操作。在RT-Thread侧在初始化时向共享内存写入一个特定的魔数如0xDEADBEEF然后在Linux侧读取验证。中断首先确保GPIO引脚配置正确没有其他功能复用。在Linux侧通过sysfs/sys/class/gpio手动设置GPIO输出电平同时用示波器测量物理引脚电压确认输出有效。在RT-Thread侧将GPIO中断服务函数改为最简单的翻转一个LED或打印一句话先确认中断是否能被触发。性能与稳定性问题现象系统运行一段时间后RT-Thread任务周期抖动或Linux侧性能下降。排查缓存一致性这是AMP架构的经典难题。如果两个系统都需要访问共享内存必须处理缓存。对于RK3568的Cortex-A55需要确保在访问共享内存前后执行缓存维护操作。在Linux侧使用dma_alloc_coherent分配的内存或使用flush_dcache_range/invalidate_dcache_range。在RT-Thread侧如果开启了数据缓存也需要使用CP15或CMSIS相关的缓存维护指令如SCB_CleanDCache_by_Addr。最省事的办法是在设备树中将共享内存区域标记为no-map和non-cacheable但会损失一些性能。内存访问冲突再次检查设备树确保所有外设的寄存器区域只被一个系统控制。任何重叠都可能导致随机崩溃。折腾这么一圈下来当你在两个串口终端分别看到RT-Thread的msh 和Linux的rootrk3568:~#提示符时那种成就感是单系统启动无法比拟的。混合部署不是银弹它引入了复杂性但在对实时性和丰富生态有双重需求的场景下它提供了一个非常优雅的解决方案。对于RK3568这样的多核平台这无疑是释放其全部潜力的一种高级玩法。