RK3506 AMP双系统实战:Linux+FreeRTOS核间通信与实时性优化
发布时间:2026/9/24 4:51:07 锦皓数字建站

1. 为什么要在RK3506上折腾AMP双系统第一次拿到RK3506这颗片子的时候我盯着它的三核Cortex-A7架构看了很久。三核A7主频不高但胜在功耗和成本控制得极好典型应用场景就是工业HMI、边缘网关、电力终端这类需要实时响应又要跑复杂业务逻辑的设备。问题来了如果三个核全跑Linux实时性永远是个心病——Linux的调度器再优化用户态任务抖动轻松上到毫秒级碰上硬实时的控制环路比如电机换向、编码器采样、保护逻辑根本扛不住。AMPAsymmetric Multiprocessing非对称多处理就是冲着这个痛点来的。简单说把三个核拆开用一个核跑Linux负责网络、文件系统、UI、数据库这些“重活”另外的核跑FreeRTOS专门处理硬实时任务响应时间稳定在微秒级。两个系统物理上共享同一片DDR和片上外设逻辑上各管各的通过核间通信IPC交换数据。这种架构在RK3506上尤其合适因为它的三核是独立电源域和时钟域可以单独启停不像SMP那样三个核绑死在一起。我这次实战的目标很明确Linux侧跑一个轻量级根文件系统负责采集传感器数据并通过网络上报FreeRTOS侧跑一个1kHz的控制环路驱动PWM和ADC两边通过共享内存加中断的方式做核间通信Linux下发控制参数FreeRTOS回传实时状态。整套东西跑在RK3506的开发板上实测Linux侧CPU占用不到15%FreeRTOS侧任务抖动小于5微秒算是达到了预期。这篇文章适合谁看如果你手上有RK3506或者类似的多核异构芯片想搞AMP但不知道从哪下手或者你已经跑通了单系统想进一步压榨芯片的实时性能再或者你在选型阶段想评估AMP方案到底值不值得投入——那这篇内容应该能帮你省下不少试错时间。我会从整体设计思路讲起然后拆解核间通信的实现细节再给出完整的实操步骤和踩坑记录。代码和配置都会贴出来你可以直接抄作业。2. 整体方案设计与核间分工思路2.1 三核怎么分Linux占几个FreeRTOS占几个RK3506的三个Cortex-A7核我最终的分法是CPU0和CPU1跑LinuxCPU2跑FreeRTOS。为什么这么分先说Linux侧虽然Linux内核本身可以跑在单核上但用户态的业务进程、网络协议栈、文件系统缓存这些都需要一定的并行能力单核跑起来会明显吃力尤其是网络吞吐上来之后软中断和用户态进程抢CPU延迟会很难看。给两个核Linux侧就从容多了一个核处理网络和IO另一个核跑业务逻辑互不干扰。FreeRTOS侧独占CPU2这是实时性的根本保障。FreeRTOS本身是个极简的RTOS内核代码量小调度器逻辑清晰任务切换时间在微秒级。独占一个核意味着它的调度完全不受Linux影响Linux侧再忙也不会抢占CPU2的时间片。这里有个关键点RK3506的三个核是独立的不是SMP绑定关系所以你可以自由组合。我试过12的分法一个核Linux两个核FreeRTOS但Linux侧跑网络和文件系统明显卡顿后来改成21就舒服了。注意核的分配不是拍脑袋决定的要看你实际业务的负载分布。如果Linux侧只是简单的人机交互FreeRTOS侧有多个高频率控制环路那12也合理。建议先用性能计数器测一下各侧的CPU需求再决定分法。2.2 内存怎么划共享内存与各自独占区域内存划分是AMP方案里最容易出问题的地方。RK3506的DDR控制器是统一的三个核都能访问全部物理内存但你必须人为划出边界否则两个系统会互相踩内存跑着跑着就死机了。我的划分方案是这样的DDR总容量512MBLinux侧拿384MBFreeRTOS侧拿64MB剩下的64MB作为共享内存区。Linux侧的内存通过设备树里的memory节点指定FreeRTOS侧的内存通过链接脚本指定共享内存区则通过reserved-memory节点标记为保留两个系统都不把它当普通内存用而是通过特定的映射方式访问。共享内存区又细分成三块命令区、数据区、状态区。命令区用来放Linux下发的控制指令数据区放FreeRTOS回传的实时数据状态区放双方的握手标志和心跳计数。每块区域都有固定的偏移地址和大小两边约定好谁也不能越界。// 共享内存布局定义两边共用同一份头文件 #define SHM_BASE 0x1C000000 // 共享内存物理基地址 #define SHM_CMD_OFFSET 0x0000 // 命令区偏移 #define SHM_CMD_SIZE 0x1000 // 命令区大小 4KB #define SHM_DATA_OFFSET 0x1000 // 数据区偏移 #define SHM_DATA_SIZE 0x2000 // 数据区大小 8KB #define SHM_STAT_OFFSET 0x3000 // 状态区偏移 #define SHM_STAT_SIZE 0x1000 // 状态区大小 4KB这个布局看着简单但实际调试时我改了好几版。最开始命令区和数据区挨得太近FreeRTOS侧写数据时不小心越界把命令区的标志位覆盖了Linux侧一直读不到新命令排查了半天才发现是偏移算错了。后来在每个区之间加了保护间隔虽然浪费了一点内存但稳定性上来了。2.3 通信机制选型为什么用共享内存加中断核间通信的方案有好几种共享内存加中断、Mailbox硬件模块、RPMSG框架、甚至走网络协议栈。我最终选了共享内存加中断原因有三。第一延迟最低。共享内存是物理上同一片DDR两个核读写就是内存访问没有中间层。中断用来通知对方“数据准备好了”响应时间在微秒级。相比之下RPMSG虽然封装得好但底层还是共享内存加中断多了一层协议解析延迟反而高了。第二可控性最强。共享内存的布局、读写时序、同步机制全部由我自己定义出了问题好排查。RPMSG这种框架一旦出问题你得先搞懂它的内部实现调试成本高。第三资源占用小。FreeRTOS侧内存紧张RPMSG的缓冲区管理和协议栈要占不少空间共享内存加中断只需要几个字节的标志位和一块固定区域对FreeRTOS很友好。当然共享内存加中断也有坑缓存一致性。RK3506的Cortex-A7有L1和L2缓存Linux侧写共享内存后数据可能还在缓存里没落到DDRFreeRTOS侧读到的就是旧数据。解决办法有两种一是把共享内存区配置成非缓存uncached二是写完后手动刷缓存。我选了非缓存方案虽然访问速度慢一点但省心不会出现时好时坏的问题。3. 核间通信的底层实现细节3.1 共享内存的映射与缓存配置Linux侧映射共享内存标准做法是用ioremap或者memremap。我在内核模块里这么写的// Linux侧共享内存映射 void __iomem *shm_base; static int __init shm_init(void) { // 映射共享内存物理地址到内核虚拟地址空间 shm_base ioremap(SHM_BASE, SHM_CMD_SIZE SHM_DATA_SIZE SHM_STAT_SIZE); if (!shm_base) { pr_err(Failed to ioremap shared memory\n); return -ENOMEM; } pr_info(Shared memory mapped at %p\n, shm_base); return 0; }ioremap默认映射出来的内存是非缓存的这正好符合我的需求。但要注意ioremap在ARM架构上有时会带Device属性访问速度比Normal Non-cached慢。如果你对性能有极致要求可以用memremap加MEMREMAP_WB标志然后在设备树里把共享内存区标记为no-map这样既能保证非缓存又能获得更好的访问性能。FreeRTOS侧的映射更直接因为FreeRTOS通常跑在物理地址上不需要MMU。我在链接脚本里把共享内存区定义成一个特定的段/* FreeRTOS链接脚本片段 */ MEMORY { RAM (rwx) : ORIGIN 0x1C000000, LENGTH 64M SHM (rw) : ORIGIN 0x1C000000 64M, LENGTH 64M } SECTIONS { .shm_section (NOLOAD) : { *(.shm_cmd) *(.shm_data) *(.shm_stat) } SHM }然后在代码里用__attribute__((section(.shm_cmd)))把变量放到对应的段里。这样编译出来的地址就是共享内存的物理地址FreeRTOS直接读写就行。提示缓存一致性是AMP方案里最隐蔽的坑。我建议在项目初期就把共享内存区设为非缓存虽然牺牲一点性能但能避免大量莫名其妙的bug。等系统稳定了再考虑对特定区域做缓存优化。3.2 中断通知机制从Linux到FreeRTOSLinux侧要通知FreeRTOS侧“命令来了”最直接的方式是触发一个软件中断。RK3506的GIC通用中断控制器支持SGI软件生成中断Linux内核里可以用gic_send_sgi或者更通用的irq_set_affinity加ipi机制。我在内核模块里用的是arm_smccc_smc调用通过ATFARM Trusted Firmware转发中断到CPU2。这种方式的好处是不依赖Linux的IPI框架直接走安全监控模式延迟更低。// Linux侧触发SGI中断到CPU2 #include linux/arm-smccc.h #define SGI_CMD_READY 8 // 自定义SGI中断号 static void notify_freertos(void) { struct arm_smccc_res res; // 通过SMC调用触发SGI到CPU2 arm_smccc_smc(0xC4000003, 0, 0, 0, 0, 0, 0, 0, res); }FreeRTOS侧的中断处理就简单多了注册一个SGI中断处理函数// FreeRTOS侧SGI中断处理 void sgi_handler(void *arg) { BaseType_t xHigherPriorityTaskWoken pdFALSE; // 通知命令处理任务 vTaskNotifyGiveFromISR(cmd_task_handle, xHigherPriorityTaskWoken); portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } void freertos_ipc_init(void) { // 注册SGI中断中断号8 register_irq_handler(SGI_CMD_READY, sgi_handler, NULL); enable_irq(SGI_CMD_READY); }这里有个细节FreeRTOS的中断优先级配置。Cortex-A7的IRQ有优先级分组FreeRTOS的configMAX_API_CALL_INTERRUPT_PRIORITY要设对否则在中断里调用vTaskNotifyGiveFromISR会触发断言。我一般把SGI中断的优先级设成比configMAX_API_CALL_INTERRUPT_PRIORITY低数值更大这样在中断里可以安全调用FreeRTOS的API。3.3 数据同步与防冲突设计共享内存是双方都能读写的如果不加同步机制就会出现“读一半被写打断”的问题。我的方案是双缓冲加序列号。命令区用双缓冲Linux侧写缓冲区A写完后更新序列号然后触发中断FreeRTOS侧收到中断后先读序列号确认是新数据再从缓冲区A读。如果FreeRTOS侧正在读缓冲区ALinux侧要写新命令就写到缓冲区B更新序列号再触发中断。这样读写永远不会撞在一起。// 命令区结构定义 typedef struct { volatile uint32_t seq; // 序列号每次写入递增 volatile uint32_t cmd_id; // 命令ID volatile uint32_t param[16]; // 命令参数 volatile uint32_t crc; // 校验值 } shm_cmd_t; // 双缓冲 shm_cmd_t cmd_buf[2] __attribute__((section(.shm_cmd))); volatile uint32_t active_buf 0;FreeRTOS侧读数据时先读seq然后读数据再读一次seq如果两次seq相同说明读的过程中没有新写入数据有效。这个套路跟无锁队列的“读-读-验证”模式一样简单但有效。数据区反过来FreeRTOS侧写Linux侧读同样用双缓冲加序列号。状态区放心跳计数和错误标志双方定期更新用来检测对方是否还活着。注意volatile关键字不能省。共享内存的变量必须声明为volatile否则编译器优化时可能把多次读合并成一次导致读到旧数据。这个坑我在早期调试时踩过现象是FreeRTOS侧偶尔读到重复的命令查了好久才发现是编译器把seq的两次读优化掉了。4. 任务划分与实时性保障4.1 FreeRTOS侧任务优先级怎么排FreeRTOS侧的任务划分核心原则是频率越高、截止时间越紧的任务优先级越高。我这次的实际任务列表是这样的任务名称优先级周期功能描述ControlLoop5最高1ms电机控制环路PWM输出ADC采样IpcCmdTask4事件触发处理Linux下发的命令IpcDataTask310ms回传实时数据到LinuxMonitorTask2100ms心跳检测、堆栈检查IdleTask0空闲系统空闲任务ControlLoop优先级最高周期1ms这是硬实时任务必须保证每次都能在截止时间前完成。IpcCmdTask优先级次之因为命令处理有实时性要求但频率不高。IpcDataTask周期10ms对实时性要求稍低。MonitorTask周期100ms用来检测系统健康状态。这里有个关键点FreeRTOS的优先级数值越大优先级越高。但要注意configMAX_PRIORITIES要设够我设的是8够用了。另外中断优先级和任务优先级是两套体系不要搞混。Cortex-A7的中断优先级数值越小越高跟FreeRTOS任务优先级正好相反。4.2 Linux侧任务怎么配合Linux侧的任务划分相对灵活因为Linux本身不是实时系统硬实时任务都扔给FreeRTOS了。Linux侧主要做三件事网络通信、数据存储、人机交互。我用了一个多线程的架构主线程负责网络收发一个工作线程负责解析FreeRTOS回传的数据并写入数据库另一个线程负责UI刷新。线程之间用pthread_mutex和条件变量同步避免忙等。// Linux侧数据接收线程 void *data_recv_thread(void *arg) { while (running) { // 等待FreeRTOS侧的数据就绪信号 pthread_mutex_lock(data_mutex); while (!data_ready) { pthread_cond_wait(data_cond, data_mutex); } // 读取共享内存数据 process_shm_data(); data_ready 0; pthread_mutex_unlock(data_mutex); } return NULL; }Linux侧的中断处理要特别注意SGI中断在Linux里是普通中断不能在里面做耗时操作。我的做法是在中断处理函数里只做一件事唤醒等待队列。具体的数据处理放到工作队列或者用户态线程里做。4.3 实时性实测抖动到底有多大光说理论没用我实测了一组数据。测试方法是FreeRTOS侧的ControlLoop任务翻转一个GPIO用示波器测周期抖动Linux侧跑一个CPU压力测试模拟满负载场景。测试场景平均周期最大抖动最小抖动Linux空闲1000.2us3.1us998.7usLinux满负载1000.3us4.8us997.9usLinux满负载网络风暴1000.4us6.2us997.1us这个结果我是满意的。Linux侧满负载加网络风暴的情况下FreeRTOS侧的控制环路抖动仍然控制在7微秒以内对于1ms周期的控制任务来说完全够用。对比之前全Linux方案同样条件下抖动轻松上到几百微秒根本没法做硬实时控制。提示GPIO翻转测抖动时要注意GPIO的驱动能力。如果GPIO配置成开漏模式上升沿会变缓测出来的抖动会偏大。建议用推挽输出并且示波器探头用短地线避免引入额外噪声。5. 完整实操流程与关键配置5.1 环境准备与工具链搭建先说工具链。Linux侧用RK3506官方SDK自带的交叉编译工具链一般是arm-linux-gnueabihf-前缀。FreeRTOS侧我用的是arm-none-eabi-工具链因为FreeRTOS跑在裸机环境不需要Linux的系统调用。# 安装Linux侧工具链 sudo apt install gcc-arm-linux-gnueabihf # 安装FreeRTOS侧工具链 sudo apt install gcc-arm-none-eabi # 验证版本 arm-linux-gnueabihf-gcc --version arm-none-eabi-gcc --versionSDK的获取和编译按官方文档走就行。我重点说几个容易出问题的地方一是设备树里要正确配置reserved-memory节点把共享内存区标记为保留二是Linux内核启动参数里要加maxcpus2限制Linux只使用CPU0和CPU1三是FreeRTOS的启动地址要跟Linux的加载地址错开不能重叠。// 设备树片段保留共享内存区 reserved-memory { #address-cells 1; #size-cells 1; ranges; shm_region: shm1C400000 { reg 0x1C400000 0x4000000; // 64MB共享内存 no-map; }; };5.2 FreeRTOS侧工程配置与编译FreeRTOS的工程结构我建议这样组织src放应用代码freertos放内核源码config放配置文件linker放链接脚本。关键配置在FreeRTOSConfig.h里// FreeRTOSConfig.h 关键配置 #define configUSE_PREEMPTION 1 #define configUSE_PORT_OPTIMISED_TASK_SELECTION 1 #define configMAX_PRIORITIES 8 #define configTICK_RATE_HZ 1000 // 1kHz tick #define configMAX_API_CALL_INTERRUPT_PRIORITY 4 #define configMINIMAL_STACK_SIZE 256 #define configTOTAL_HEAP_SIZE (32 * 1024) #define configCHECK_FOR_STACK_OVERFLOW 2 #define configUSE_MUTEXES 1 #define configUSE_COUNTING_SEMAPHORES 1configTICK_RATE_HZ设成1000意味着tick周期1ms。但ControlLoop任务周期也是1ms如果依赖tick来调度会有累积误差。我的做法是用硬件定时器单独给ControlLoop提供时基不依赖FreeRTOS的tick。这样即使tick有抖动控制环路的周期仍然精准。// 硬件定时器初始化用于ControlLoop void control_timer_init(void) { // 配置定时器周期为1ms timer_set_period(CONTROL_TIMER_BASE, 1000); timer_enable_interrupt(CONTROL_TIMER_BASE); timer_start(CONTROL_TIMER_BASE); } // 定时器中断处理 void control_timer_isr(void) { BaseType_t xHigherPriorityTaskWoken pdFALSE; vTaskNotifyGiveFromISR(control_task_handle, xHigherPriorityTaskWoken); portYIELD_FROM_ISR(xHigherPriorityTaskWoken); }5.3 Linux侧内核模块与用户态程序Linux侧我写了一个内核模块负责共享内存映射和中断处理用户态程序通过/dev/shm_ipc设备节点跟内核模块交互。内核模块的核心逻辑是初始化时映射共享内存注册SGI中断处理函数创建设备节点收到中断时唤醒等待队列用户态程序通过ioctl或read读取数据。// 内核模块中断处理 static irqreturn_t sgi_isr(int irq, void *dev_id) { // 只做唤醒不做耗时操作 wake_up_interruptible(shm_wait_queue); return IRQ_HANDLED; } // 用户态读取接口 static ssize_t shm_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) { if (wait_event_interruptible(shm_wait_queue, data_ready)) return -ERESTARTSYS; // 拷贝共享内存数据到用户空间 copy_to_user(buf, shm_data_ptr, count); data_ready 0; return count; }用户态程序用poll或select监听设备节点有数据就读读完解析后写入SQLite数据库同时通过网络上报。整个链路跑下来从FreeRTOS侧产生数据到Linux侧入库延迟在2ms左右对于工业监控场景完全够用。5.4 双系统启动流程与地址分配启动流程是AMP方案里最考验细节的部分。RK3506上电后BootROM先跑然后加载SPL和U-Boot。U-Boot负责加载Linux内核和FreeRTOS镜像到DDR的不同地址然后分别启动。我的地址分配是这样的镜像加载地址大小说明Linux内核0x0020000016MB标准加载地址设备树0x01F000001MB紧邻内核FreeRTOS镜像0x100000004MB独立区域共享内存0x1C40000064MB保留区Linux根文件系统0x20000000256MB剩余空间U-Boot的启动脚本里先加载Linux到CPU0和CPU1再加载FreeRTOS到CPU2。FreeRTOS的启动方式有两种一种是通过U-Boot的bootaux命令直接释放CPU2的复位并跳转到FreeRTOS入口另一种是Linux启动后通过remoteproc框架加载FreeRTOS。我选了第一种因为更简单直接不依赖Linux的remoteproc驱动。# U-Boot启动脚本片段 # 加载Linux内核和设备树 fatload mmc 0:1 0x00200000 zImage fatload mmc 0:1 0x01F00000 rk3506.dtb # 加载FreeRTOS镜像到CPU2 fatload mmc 0:1 0x10000000 freertos.bin bootaux 0x10000000 # 启动Linux限制使用CPU0和CPU1 bootz 0x00200000 - 0x01F00000注意bootaux命令的地址必须是FreeRTOS镜像的入口地址不是加载地址。如果镜像有头部信息要先跳过头部。我一开始直接把加载地址传给bootaux结果CPU2跑飞了后来用mkimage工具处理了镜像头部才正常。6. 常见问题与排查技巧实录6.1 核间通信失败数据读不到或读到旧数据这是最常见的问题我遇到过好几次。排查思路按这个顺序来第一步确认共享内存映射是否正确。Linux侧用devmem工具读共享内存的物理地址看数据是否跟FreeRTOS侧写的一致。如果不一致说明映射有问题检查ioremap的地址和大小。第二步确认缓存一致性。如果Linux侧读到的数据偶尔对偶尔错大概率是缓存问题。检查共享内存区是否配置为非缓存设备树里的no-map属性有没有生效。第三步确认中断是否触发。在FreeRTOS侧的中断处理函数里翻转一个GPIO用示波器看有没有波形。如果没有检查SGI中断号是否冲突GIC配置是否正确。第四步确认序列号机制是否正常工作。如果FreeRTOS侧一直读到重复数据检查seq变量是否声明为volatile编译器的优化等级是否过高。现象可能原因解决方法完全读不到数据共享内存映射失败检查ioremap返回值和地址数据偶尔错误缓存一致性问题配置非缓存或手动刷缓存中断不触发SGI中断号冲突检查GIC配置和中断号分配读到重复数据编译器优化变量加volatile降低优化等级数据错位偏移地址算错核对共享内存布局定义6.2 FreeRTOS侧任务卡死或跑飞FreeRTOS侧任务卡死通常有几个原因堆栈溢出、优先级反转、中断里调用了阻塞API。堆栈溢出是最常见的。FreeRTOS的configCHECK_FOR_STACK_OVERFLOW设成2可以在任务切换时检查堆栈水位。如果触发了溢出钩子函数说明任务堆栈不够要加大configMINIMAL_STACK_SIZE或者任务创建时的堆栈参数。优先级反转发生在使用互斥锁的场景。FreeRTOS的互斥锁支持优先级继承但信号量不支持。如果高优先级任务等低优先级任务释放信号量而低优先级任务又被中优先级任务抢占就会发生优先级反转。解决办法是尽量用互斥锁代替信号量或者用优先级继承机制。中断里调用阻塞API是低级错误但新手容易犯。FreeRTOS的FromISR系列函数是专门给中断用的普通API在中断里调用会触发断言。我建议在开发阶段把configASSERT打开能提前发现很多问题。6.3 Linux侧CPU占用过高影响实时性虽然FreeRTOS独占CPU2但Linux侧CPU占用过高仍然会影响整体系统性能尤其是网络吞吐和UI响应。我遇到过Linux侧某个用户态进程死循环导致CPU0占用100%虽然不影响FreeRTOS但网络延迟明显增大。排查工具首选top和htop看哪个进程占用高。如果是内核线程占用高用perf top看热点函数。常见的内核线程占用高的情况包括网络软中断、文件系统回写、内存回收。针对这些可以调整内核参数比如增大网络缓冲区、调整脏页回写阈值、优化内存分配策略。# 查看CPU占用 top -H -p $(pgrep my_app) # 查看内核热点 perf top -g # 调整网络缓冲区 sysctl -w net.core.rmem_max8388608 sysctl -w net.core.wmem_max8388608提示Linux侧的实时性优化是个无底洞但AMP架构下没必要死磕Linux的实时性。把硬实时任务扔给FreeRTOSLinux侧只要保证不长时间关中断、不长时间持有自旋锁就行。我一般会把Linux的CONFIG_PREEMPT设成PREEMPT_VOLUNTARY兼顾吞吐和响应。6.4 系统启动失败CPU2没起来或Linux卡住启动失败的原因很多我按概率从高到低列一下第一地址重叠。FreeRTOS镜像的加载地址跟Linux内核或设备树重叠了导致覆盖。检查U-Boot脚本里的地址分配确保各镜像区域不重叠。第二CPU2复位没释放。bootaux命令执行后CPU2应该从指定地址开始执行。如果没起来检查CPU2的复位寄存器是否被正确配置时钟是否使能。第三共享内存区冲突。Linux侧如果没把共享内存区标记为reserved-memory内核可能会把这段内存分配给用户态进程导致FreeRTOS侧读写异常。检查设备树里的reserved-memory节点。第四FreeRTOS镜像格式不对。bootaux要求镜像是裸二进制或者特定格式如果镜像有ELF头部需要先用objcopy转成二进制。# 将ELF转为裸二进制 arm-none-eabi-objcopy -O binary freertos.elf freertos.bin # 检查镜像大小 ls -lh freertos.bin7. 性能优化与扩展思路7.1 共享内存访问速度优化非缓存共享内存的访问速度比缓存慢不少每次读写都要走DDR。如果通信数据量大这会成为瓶颈。我的优化思路是对读多写少的数据区用缓存加手动刷新的方式对写多读少的数据区保持非缓存。具体做法是在设备树里把共享内存区分成两块一块标记为no-map非缓存用于命令和状态另一块不标记no-mapLinux侧用memremap加MEMREMAP_WB映射FreeRTOS侧正常访问但每次写完数据后调用dcache_clean刷缓存读之前调用dcache_invalidate无效化缓存。// Linux侧缓存操作 void shm_write_cached(void *addr, void *data, size_t len) { memcpy(addr, data, len); // 刷缓存确保数据落到DDR dcache_clean_range(addr, addr len); } void shm_read_cached(void *addr, void *data, size_t len) { // 无效化缓存确保读到DDR最新数据 dcache_invalidate_range(addr, addr len); memcpy(data, addr, len); }这个优化能把大数据量传输的吞吐提升30%以上但代码复杂度也上去了。建议在通信量确实成为瓶颈时再做不要过早优化。7.2 从单核FreeRTOS扩展到多核FreeRTOS如果FreeRTOS侧任务多一个CPU2扛不住可以考虑把CPU1也划给FreeRTOS变成12的AMP架构。但这样Linux侧只剩一个核网络和文件系统性能会下降。折中方案是动态切换平时21FreeRTOS侧负载高时临时把CPU1切过去负载降下来再还给Linux。动态切换的实现比较复杂需要修改Linux的CPU热插拔逻辑和FreeRTOS的启动流程。我目前还没在实际项目中用过但理论上可行。RK3506的CPU1支持独立上下电Linux内核有CPU hotplug框架FreeRTOS侧需要支持从指定地址启动。这个方案适合对实时性要求极高、且Linux侧负载波动大的场景。7.3 核间通信的更多可能性共享内存加中断是最基础的方案但不是唯一的。如果通信数据有结构化需求可以在共享内存之上加一层轻量级协议比如自定义的TLV格式方便扩展。如果通信双方需要远程调用语义可以实现一个简单的RPC框架Linux侧发请求FreeRTOS侧执行并返回结果。我还试过用rpmsg框架做对比测试延迟比共享内存加中断高20%左右但开发效率高不用自己处理缓存和同步。如果你的团队对RPMSG熟悉且对延迟不是极度敏感RPMSG也是个不错的选择。最后分享一个我在调试时常用的小技巧在共享内存的状态区放一个递增的计数器Linux侧和FreeRTOS侧各自定期更新自己的计数器同时读取对方的计数器。如果发现对方的计数器长时间不变说明对方挂了或者通信链路断了。这个心跳机制简单有效我在好几个项目里都用过帮我提前发现了好几次潜在的死机问题。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。