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源码

把以下文件加入工程:

  1. 核心文件:tasks.c、queue.c、list.c、timers.c(按需添加)
  2. 移植文件:portable/GCC/ARM_CM4F/port.c、portmacro.h(以Cortex-M4为例)
  3. 内存管理:portable/MemMang/heap_4.c(推荐)
  4. 配置文件: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,让界面动起来。