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;
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,说明栈快溢出了,赶紧加大。
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 打交道」的中断,优先级高于系统心跳。
嗯,这个坑我后来在好几个项目里都遇到过。每次新同事接手,我都会跟他们强调一遍。现在你知道了,希望你别再踩一次。
好了,移植和配置就讲到这里。下一章咱们聊聊任务创建和调度,那才是真正开始写应用代码的时候。