3、FreeRTOS移植与配置:从源码到跑起来

好,咱们进入正题。这一章我带你手把手把 FreeRTOS 移植到你的 MCU 上。说实话,移植这事儿,说难不难,说简单也不简单。我见过不少工程师,代码写得挺溜,一到移植就卡壳。其实说白了,就是没搞懂 FreeRTOS 到底需要 MCU 给它提供什么。

3.1 FreeRTOS源码结构:别被文件夹吓到

我第一次打开 FreeRTOS 源码包时,也懵了一下。文件夹套文件夹,看着头大。但你仔细看,其实就三大块:

  • 核心源码:在 FreeRTOS/Source/ 下,就几个 .c 文件。tasks.c、queue.c、list.c 这些是必须的。timers.c 和 event_groups.c 看需求加。
  • 内存管理:在 Source/portable/MemMang/ 下,有 heap_1.c 到 heap_5.c。我项目里最常用 heap_4.c,它支持碎片合并,稳定。
  • 移植层:在 Source/portable/ 下,按编译器(GCC、IAR、Keil)和架构(ARM_CM3、ARM_CM4F)分目录。你找到自己 MCU 对应的那个文件夹就行。

嗯,这里要注意:别把整个源码包都扔进工程。你只需要核心源码 + 一个 heap 文件 + 对应的移植文件。我见过有人把整个 portable 目录都加进去,编译报一堆错,何必呢。

3.2 移植步骤详解:三步走

移植说白了就三步:改中断、配时钟、设堆栈。咱们一个一个来。

3.2.1 中断管理:别让 FreeRTOS 和你的中断打架

FreeRTOS 接管了 PendSV 和 SysTick 中断。这两个中断的优先级有讲究。我踩过最大的坑就在这里。

在 ARM Cortex-M 内核上,FreeRTOS 要求:

  • PendSV 优先级:设为最低
  • SysTick 优先级:可以设任意值,但必须低于 configMAX_SYSCALL_INTERRUPT_PRIORITY
  • 其他中断:如果你要在 ISR 里调用 FreeRTOS API,优先级必须低于 configMAX_SYSCALL_INTERRUPT_PRIORITY

你需要在 port.c 里找到这段代码,确认它设对了:

// 在 xPortStartScheduler() 中
// 设置 PendSV 和 SysTick 优先级
portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;
警告:我曾经在一个项目里,把 SysTick 优先级设得比某个外设中断还高。结果那个外设中断里调用了 xQueueSendFromISR(),系统直接死机。查了两天才发现是优先级冲突。记住:在 ISR 里调 FreeRTOS API,中断优先级必须低于 configMAX_SYSCALL_INTERRUPT_PRIORITY

3.2.2 时钟配置:SysTick 是心脏

FreeRTOS 靠 SysTick 中断来产生系统心跳。你需要告诉它 SysTick 的时钟频率。在 FreeRTOSConfig.h 里:

#define configCPU_CLOCK_HZ        ( ( unsigned long ) 72000000 )  // 你的 MCU 主频
#define configTICK_RATE_HZ        ( ( TickType_t ) 1000 )         // 1ms 一个 tick

我个人习惯把 tick 频率设为 1000Hz,也就是 1ms 一个 tick。这样延时函数 vTaskDelay(10) 就是 10ms,直观。但如果你对实时性要求极高,可以设到 10000Hz,不过要注意 SysTick 中断开销会变大。

我记得有一次,客户说系统跑着跑着就卡了。我一看,他把 configCPU_CLOCK_HZ 写成了 72MHz 的数值,但实际 MCU 跑在 48MHz。SysTick 计时全乱套了。嗯,这种低级错误,检查时钟配置就能避免。

3.2.3 堆栈设置:给任务留够空间

每个任务都有自己的栈。FreeRTOS 用 configMINIMAL_STACK_SIZE 来定义最小栈大小。对于 Cortex-M3/M4,我建议至少设 128 个字(512 字节)。

#define configMINIMAL_STACK_SIZE    ( ( unsigned short ) 128 )

但实际项目中,你得根据任务复杂度来调。我一般这样估算:

  • 简单任务(只做标志位翻转):128 字够用
  • 中等任务(带局部数组、调用几个函数):256 字
  • 复杂任务(带 printf、浮点运算):512 字起步

怎么知道栈够不够?用 uxTaskGetStackHighWaterMark() 这个 API。它返回任务栈剩余的最小空间。如果返回值接近 0,说明栈快溢出了,赶紧加大。

小技巧:我习惯在调试阶段,每个任务都调用一次 uxTaskGetStackHighWaterMark(),把结果打印出来。这样就能精确知道每个任务实际用了多少栈,避免浪费 RAM。

3.3 FreeRTOSConfig.h 关键配置项:别乱改

这个文件是 FreeRTOS 的「总开关」。我挑几个最重要的说:

配置项 说明 我的建议值
configUSE_PREEMPTION 是否使用抢占式调度 1(工业控制必须抢占)
configUSE_TIME_SLICING 是否启用时间片轮转 1(同优先级任务轮流跑)
configMAX_PRIORITIES 最大优先级数 5 或 7(别设太大,浪费 RAM)
configTOTAL_HEAP_SIZE FreeRTOS 管理的堆大小 根据你的 RAM 大小,留 20-30% 给系统
configUSE_MUTEXES 是否使用互斥量 1(防止优先级反转)
configCHECK_FOR_STACK_OVERFLOW 栈溢出检查 2(最严格检查,调试时必开)

你想想看,如果 configMAX_PRIORITIES 设成 32,每个优先级都要占一个位图,RAM 开销不小。工业控制场景,5 到 7 个优先级完全够用。我一般分:紧急中断级、高优先级控制任务、中等优先级通信任务、低优先级显示任务、空闲任务。

3.4 我踩过的坑:Tick中断优先级设置

这个坑我必须单独拿出来说。因为它太隐蔽了,而且一旦踩到,系统表现非常诡异——有时候跑得好好的,有时候突然死机。

事情是这样的:有一次我做一款运动控制器,用了 FreeRTOS + 一个高优先级的外部中断(用来捕获编码器信号)。编码器中断里调用了 xQueueSendFromISR()。系统在低负载时一切正常,一旦电机高速运转,编码器中断频繁触发,系统就挂了。

查了两天,最后发现是 Tick 中断优先级的问题。我把 SysTick 优先级设成了 0(最高),而编码器中断优先级是 1。按照 FreeRTOS 的规则,SysTick 中断不能比任何调用 FreeRTOS API 的中断优先级高。因为 SysTick 中断里会做任务切换,如果它抢占了编码器中断,而编码器中断又在操作队列,数据就乱套了。

解决方案很简单:把 SysTick 优先级降到比编码器中断低。比如编码器中断优先级设 2,SysTick 设 3。这样 SysTick 不会打断编码器中断,编码器中断可以安全地调用 FreeRTOS API。

核心原则:在 Cortex-M 上,FreeRTOS 的 Tick 中断(SysTick)优先级,必须低于所有在 ISR 中调用 FreeRTOS API 的中断优先级。说白了,就是让那些「需要跟 FreeRTOS 打交道」的中断,优先级高于系统心跳。

嗯,这个坑我后来在好几个项目里都遇到过。每次新同事接手,我都会跟他们强调一遍。现在你知道了,希望你别再踩一次。

好了,移植和配置就讲到这里。下一章咱们聊聊任务创建和调度,那才是真正开始写应用代码的时候。