第四章:实时操作系统移植:FreeRTOS在M核上的移植、Linux在A核上的裁剪、双核OS协同调度

好,咱们进入正题。这一章讲的是双核系统里最核心的活儿——把两个操作系统分别跑起来,还得让它们好好配合。说实话,我当年第一次做这个的时候,心里也没底。M核跑FreeRTOS,A核跑裁剪后的Linux,听起来简单,做起来全是坑。

4.1 FreeRTOS在M核上的移植:别小看这一步

很多人觉得FreeRTOS移植就是改几个宏定义,配一下时钟就完事了。嗯,我刚开始也这么想,结果第一次上板子,任务调度死活不跑。后来发现是中断优先级配置的问题。

移植FreeRTOS到M核,说白了就是让操作系统知道三件事:

  • 时钟节拍从哪来——通常用SysTick定时器
  • 中断怎么进怎么出——PendSV和SVC要处理好
  • 栈指针怎么切换——M核的MSP和PSP要分清楚

我个人习惯先把FreeRTOSConfig.h里的配置项过一遍。这里有个关键点:

核心配置项(别漏了)

  • configCPU_CLOCK_HZ:主频一定要写对,我见过有人少写一个0,结果任务跑得比乌龟还慢
  • configTICK_RATE_HZ:一般设1000,也就是1ms一个tick。医疗设备里我建议用1000,精度够用
  • configMAX_PRIORITIES:别设太大,5-8个优先级足够了。设多了反而浪费RAM

然后就是中断处理。M核的中断向量表里,PendSV_HandlerSysTick_Handler这两个函数名必须跟FreeRTOS里的名字对上。我踩过这个坑——名字拼写错了,编译不报错,但调度器根本不工作。

// 正确的写法示例
void xPortPendSVHandler(void) __attribute__((alias("PendSV_Handler")));
void xPortSysTickHandler(void) __attribute__((alias("SysTick_Handler")));

小技巧:移植完成后,先跑一个最简单的任务——点个LED。如果LED能按预期闪烁,说明调度器活了。别一上来就跑复杂逻辑,容易把自己搞晕。

4.2 Linux在A核上的裁剪:做减法比做加法难

Linux裁剪,说白了就是去掉你不需要的东西。医疗设备里,A核跑Linux主要是为了处理网络、文件系统、UI这些重活儿。但完整的Linux内核太大了,动不动几十兆,得瘦身。

我记得第一次给一个监护仪做Linux裁剪,目标是把内核压到5MB以内。当时我删了又删,最后发现——文件系统才是大头

裁剪Linux,我建议从这几个方向入手:

  1. 内核配置裁剪:用make menuconfig,把不需要的驱动、协议栈全关掉。比如医疗设备一般不需要蓝牙、Wi-Fi(除非你的设备需要),那就关掉。
  2. 文件系统瘦身:用BusyBox代替完整的GNU工具链。BusyBox一个二进制文件就能提供几十个命令,省空间。
  3. 启动时间优化:去掉不必要的内核模块,用initramfs代替完整的根文件系统挂载。

注意:裁剪时千万别把内存管理、进程调度这些核心功能给关了。我见过有人为了省空间,把CFS调度器换成了简陋的O(1)调度器,结果系统响应变得极其不稳定。医疗设备里,稳定性是第一位的。

举个例子,一个典型的裁剪后Linux内核配置大概长这样:

# 裁剪后的内核配置示例
CONFIG_EMBEDDED=y
CONFIG_SMP=y
CONFIG_PREEMPT=y
# 去掉不必要的驱动
# CONFIG_SOUND is not set
# CONFIG_WLAN is not set
# 保留必要的文件系统
CONFIG_EXT4_FS=y
CONFIG_TMPFS=y

你想想看,一个完整的Linux内核有几千个配置项。我个人的做法是:先拿一个最小配置(比如qemu的defconfig),然后按需往上加。这样比从完整配置往下删要安全得多。

4.3 双核OS协同调度:让两个系统说上话

好,现在M核跑着FreeRTOS,A核跑着裁剪后的Linux。问题是——它们怎么通信?怎么协同工作?

双核协同调度,核心就三个字:共享内存。两个核通过一块共用的内存区域交换数据。但这里有个大坑——缓存一致性问题

我记得有一次,A核往共享内存里写了一个数据,M核读到的却是旧值。查了两天才发现,是A核的L1缓存没刷出去。从那以后,我每次操作共享内存都会手动刷缓存。

双核通信的典型架构

  • 共享内存区域:划分一块固定的物理内存,两个核都能访问
  • 核间中断(IPI):一个核写完数据后,通过中断通知另一个核
  • 信号量/自旋锁:防止两个核同时写同一个地址

具体实现上,我推荐用消息队列的方式。A核把任务描述符写到共享内存的队列里,然后发一个IPI给M核。M核收到中断后,从队列里取出任务,执行完再把结果写回去。

// 共享内存中的消息结构体示例
typedef struct {
    uint32_t cmd_id;      // 命令ID
    uint32_t data_len;    // 数据长度
    uint8_t  data[256];   // 数据内容
    uint32_t status;      // 执行状态
} shared_msg_t;

// 注意:这个结构体必须按1字节对齐
// 否则两个核的编译器可能对成员偏移量理解不一致
__attribute__((packed))

避坑指南:我曾经在共享内存里放了一个结构体,A核用GCC编译,M核用ARMCC编译。结果两个编译器对结构体对齐的理解不一样,数据全乱了。后来我强制所有共享结构体都用__attribute__((packed)),问题解决。

双核协同调度还有一个关键点——任务划分。哪些任务跑在A核上,哪些跑在M核上?我一般遵循这个原则:

任务类型 建议运行核 原因
实时控制(如PID调节) M核 FreeRTOS中断响应快,确定性高
数据采集(如ADC读取) M核 需要微秒级定时,Linux做不到
网络通信(如TCP/IP) A核 Linux网络栈成熟,协议支持全
用户界面(如LCD显示) A核 需要图形库和文件系统支持
安全监控(如看门狗) M核 独立于Linux,防止Linux死机后无人看管

最后说一句——双核协同不是万能的。如果两个核之间的通信太频繁,反而会拖慢整体性能。我建议把通信频率控制在1kHz以内,也就是每1ms交换一次数据就够了。医疗设备里,大部分控制环路跑在100Hz到1kHz之间,这个频率完全够用。

嗯,这一章的内容就这些。下一章咱们聊聊实际案例——一个便携式心电监护仪的双核实现。到时候我会把代码和调试过程都过一遍,保证干货满满。