2. FreeRTOS移植与配置:从源码到跑起来
好,咱们进入正题。上一章聊了RTOS的基本概念,这一章我们亲手把FreeRTOS移植到目标板上。说实话,移植这件事,很多新手觉得高深莫测,其实说白了就是「把别人的代码,放到你的环境里编译通过,再跑起来」。
我刚开始做移植时,也踩过不少坑。有一次折腾了整整两天,最后发现是时钟配置错了。嗯,这种经历,我相信你以后也会有(笑)。
2.1 FreeRTOS源码结构
先看看FreeRTOS的源码长什么样。你从官网或GitHub下载后,解压出来大概是这样的:
FreeRTOS/
├── FreeRTOS/
│ ├── Source/
│ │ ├── tasks.c // 任务管理核心
│ │ ├── queue.c // 队列
│ │ ├── list.c // 链表
│ │ ├── timers.c // 软件定时器
│ │ ├── event_groups.c // 事件组
│ │ ├── stream_buffer.c // 流缓冲区
│ │ ├── croutine.c // 协程(很少用)
│ │ └── include/ // 头文件
│ └── portable/ // 移植层
│ ├── MemMang/ // 内存管理方案
│ ├── RVDS/ // ARM编译器
│ ├── GCC/ // GCC编译器
│ ├── IAR/ // IAR编译器
│ └── ... // 其他平台
└── FreeRTOS-Plus/ // 扩展组件
我个人习惯把源码分成三层来看:
- 核心层:tasks.c、queue.c这些,跟硬件无关,基本不用改
- 移植层:portable目录下的东西,这是移植工作的重点
- 配置层:FreeRTOSConfig.h,这个文件决定了系统的行为
小技巧:移植时,你只需要关注portable目录下对应你编译器的那一份文件。比如用GCC,就看GCC文件夹里的port.c和portmacro.h。其他文件夹可以删掉,省得看着眼花。
2.2 移植步骤:三步走
移植FreeRTOS,我总结为三步。你跟着走,基本不会出大问题。
第一步:准备基础工程
先让你的板子能跑裸机程序。点亮一个LED,或者串口打印个"Hello World"。这一步很重要——如果裸机都跑不起来,就别谈RTOS了。
我记得有一次帮客户移植,对方说「系统一直死机」。我过去一看,裸机下的时钟树就配错了。嗯,这种问题RTOS也救不了你。
第二步:添加FreeRTOS源码
把以下文件加入工程:
- 核心文件:tasks.c、queue.c、list.c、timers.c(按需添加)
- 移植文件:portable/GCC/ARM_CM4F/port.c、portmacro.h(以Cortex-M4为例)
- 内存管理:portable/MemMang/heap_4.c(推荐)
- 配置文件:FreeRTOSConfig.h
注意:不要一股脑把所有.c文件都加进去。比如croutine.c,我做了这么多年项目,一次都没用过。加多了反而编译报错,自找麻烦。
第三步:配置中断与时钟
这是最关键的步骤。FreeRTOS需要一个心跳——系统时钟节拍(Tick)。通常用硬件定时器来实现。
以STM32为例,在SysTick中断里调用:
void SysTick_Handler(void)
{
// 告诉FreeRTOS:一个tick过去了
if (xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED)
{
xPortSysTickHandler();
}
}
为什么要有这个判断?因为系统启动时,调度器还没开始工作。如果这时候就调用xPortSysTickHandler,可能会出问题。我曾经见过有人没加这个判断,结果系统一启动就进HardFault。
2.3 配置文件详解:FreeRTOSConfig.h
这个文件是FreeRTOS的灵魂。我每次做新项目,第一件事就是打开它,一项一项过一遍。
下面是我常用的配置模板:
/* 基础配置 */
#define configUSE_PREEMPTION 1 // 抢占式调度
#define configUSE_TIME_SLICING 1 // 时间片轮转
#define configUSE_PORT_OPTIMISED_TASK_SELECTION 1 // 硬件优化
/* 任务配置 */
#define configMAX_PRIORITIES 5 // 最大优先级数
#define configMINIMAL_STACK_SIZE 128 // 最小栈大小(字)
#define configTOTAL_HEAP_SIZE 10240 // 总堆大小(字节)
/* 内核功能 */
#define configUSE_TICKLESS_IDLE 0 // 低功耗模式
#define configUSE_TASK_NOTIFICATIONS 1 // 任务通知
#define configUSE_MUTEXES 1 // 互斥量
#define configUSE_COUNTING_SEMAPHORES 1 // 计数信号量
#define configUSE_RECURSIVE_MUTEXES 1 // 递归互斥量
#define configUSE_QUEUE_SETS 0 // 队列集
这里有几个关键点,我重点说一下:
- configMAX_PRIORITIES:不是越大越好。优先级越多,调度开销越大。我一般设5~32之间。你想想看,一个系统里真需要32个不同优先级的任务吗?很少。
- configTOTAL_HEAP_SIZE:这个值取决于你的RAM大小。设太大,编译不过;设太小,运行时动态分配内存会失败。我习惯先设一个较大的值,编译看看RAM占用,再调整。
- configUSE_TICKLESS_IDLE:低功耗项目才开。普通项目关掉,省得引入不必要的复杂度。
避坑指南:我曾经在一个项目里把configMINIMAL_STACK_SIZE设成了64(字节),结果任务一启动就栈溢出。后来才发现,这个值是以「字」为单位的。在32位处理器上,1个字=4字节。所以128字=512字节。嗯,这个坑我帮你踩过了。
2.4 内存管理方案:选对才是王道
FreeRTOS提供了5种内存管理方案,都在portable/MemMang目录下。它们的名字很直接:heap_1.c到heap_5.c。
| 方案 | 特点 | 适用场景 |
|---|---|---|
| heap_1 | 只能分配,不能释放 | 极简系统,任务创建后永不删除 |
| heap_2 | 可分配可释放,但碎片化 | 已废弃,不推荐使用 |
| heap_3 | 包装了malloc/free | 编译器自带堆管理,线程安全 |
| heap_4 | 合并相邻空闲块,防碎片 | 最推荐,通用场景 |
| heap_5 | 支持多个不连续内存区域 | 外部RAM+内部RAM混合使用 |
我个人99%的项目都用heap_4。为什么?因为它能合并相邻的空闲内存块,碎片问题比heap_2好得多。说白了,就是「用完释放,释放后还能合并成大块,下次还能用」。
heap_1最简单,但只能分配不能释放。如果你确定系统里所有任务、队列、信号量都是一次性创建、永不删除,那heap_1完全够用。我早期做的一个传感器项目,就用的heap_1,跑了三年没出过问题。
heap_5呢?我只有在用外部SDRAM时才考虑。比如一个项目需要同时从内部SRAM和外部SDRAM分配内存,那就得用heap_5,调用vPortDefineHeapRegions()来定义多个区域。
我的建议:新手直接上heap_4。等你对内存管理有了深刻理解,再考虑其他方案。别一开始就想着「我要最灵活的」,结果把自己绕进去了。
2.5 验证移植是否成功
移植完了,怎么知道对不对?写个最简单的测试:
void vTask1(void *pvParameters)
{
while(1)
{
// 任务1:每隔500ms翻转LED
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
vTaskDelay(pdMS_TO_TICKS(500));
}
}
void vTask2(void *pvParameters)
{
while(1)
{
// 任务2:每隔1000ms打印信息
printf("Task2 is running\r\n");
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
int main(void)
{
HAL_Init();
SystemClock_Config();
xTaskCreate(vTask1, "Task1", 128, NULL, 1, NULL);
xTaskCreate(vTask2, "Task2", 128, NULL, 1, NULL);
vTaskStartScheduler(); // 启动调度器
// 正常情况下不会执行到这里
while(1);
}
如果LED以500ms闪烁,同时串口每秒打印一次信息,恭喜你,移植成功了。
如果没反应?别慌。先检查这几个地方:
- SysTick中断是否使能了?
- FreeRTOSConfig.h里的configCPU_CLOCK_HZ是否跟实际时钟一致?
- 中断优先级分组是否配置正确?FreeRTOS要求使用4位优先级分组(即NVIC_PriorityGroup_4)
嗯,移植这件事,说难不难,说简单也不简单。关键是把每一步都理解透,而不是盲目复制粘贴。下一章,我们聊聊如何在FreeRTOS上跑LVGL,让界面动起来。