第二章 FreeRTOS移植与配置:从源码到跑起来

好,咱们进入正题。上一章聊了RTOS的基本概念,这一章咱们要动真格的了——把FreeRTOS移植到你的目标板子上,并且把配置调好。说实话,移植这件事,很多新手觉得高大上,其实说白了就是“把别人的代码放到你的环境里,让它能编译通过、跑得起来”。

我在做制氧机项目的时候,第一次移植FreeRTOS花了整整两天。后来熟练了,半小时搞定。差别在哪?就是对源码结构和配置项的理解深度。今天我把这些经验全盘托出。

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

你从官网或者GitHub下载FreeRTOS源码包,解压后看到一堆文件夹。别慌,核心就三个部分:

  • Source:内核源码,这是灵魂
  • Portable:移植层,针对不同MCU的适配代码
  • Demo:示例工程,参考用的

我个人习惯,只把Source和Portable里对应我芯片的文件夹拿出来,Demo基本不看——因为Demo工程往往太复杂,夹杂了很多演示功能,反而容易把人搞晕。

Source目录下的核心文件就这几个:

文件名 作用
tasks.c 任务管理,创建、删除、调度全在这
queue.c 队列和信号量的实现
timers.c 软件定时器
event_groups.c 事件组,多任务同步用的
list.c 内核链表,所有调度器的基础
portable/MemMang/heap_4.c 内存管理,我推荐用heap_4
我的经验:heap_4.c是大多数场景的最佳选择。它支持碎片合并,不像heap_2那样容易产生内存碎片。我在制氧机项目里一直用heap_4,从来没出过内存问题。

2.2 移植步骤:三步走,不迷路

移植FreeRTOS,说白了就三步。别想复杂了:

  1. 复制源码:把Source目录和对应的Portable目录拷到你的工程里
  2. 配置FreeRTOSConfig.h:这是核心,后面细讲
  3. 实现滴答定时器中断:给系统一个心跳

嗯,这里要注意。很多人卡在第三步。滴答定时器中断里要调用xPortSysTickHandler(),这个函数在port层里已经实现了,你只需要在中断服务函数里调用它就行。

我曾经遇到一个坑:中断优先级设置不对,导致调度器不工作。后来发现是FreeRTOS要求滴答定时器的中断优先级不能高于configMAX_SYSCALL_INTERRUPT_PRIORITY。这个配置项,我后面会讲。

2.3 FreeRTOSConfig.h配置详解:每个宏都有它的脾气

这个文件是整个移植的灵魂。我见过太多人直接复制粘贴,结果系统跑起来各种诡异问题。咱们一个一个看:

基础配置

#define configUSE_PREEMPTION          1
#define configUSE_IDLE_HOOK           0
#define configUSE_TICK_HOOK           0
#define configCPU_CLOCK_HZ            ( ( unsigned long ) 72000000 )
#define configTICK_RATE_HZ            ( ( TickType_t ) 1000 )
#define configMAX_PRIORITIES          ( 5 )
#define configMINIMAL_STACK_SIZE      ( ( unsigned short ) 128 )
#define configTOTAL_HEAP_SIZE         ( ( size_t ) ( 15 * 1024 ) )
  • configUSE_PREEMPTION:1表示抢占式调度,0表示协作式。制氧机这种实时性要求高的,必须用1。
  • configCPU_CLOCK_HZ:你的MCU主频。我用的STM32F103是72MHz,这里就填72000000。
  • configTICK_RATE_HZ:系统心跳频率。1000Hz就是1ms一个tick。别设太高,否则中断开销太大。
  • configTOTAL_HEAP_SIZE:给FreeRTOS分配的总堆大小。制氧机项目里我给了15KB,够用。
注意:configTOTAL_HEAP_SIZE不是越大越好。你给RTOS的堆多了,应用程序的堆就少了。要根据实际任务数量和栈需求来算。我一般先给一个保守值,跑起来后用vTaskGetRunTimeStats()观察内存使用,再调整。

功能裁剪配置

#define configUSE_TIMERS              1
#define configUSE_CO_ROUTINES         0
#define configUSE_MUTEXES             1
#define configUSE_RECURSIVE_MUTEXES   1
#define configUSE_COUNTING_SEMAPHORES 1
#define configUSE_QUEUE_SETS          0

这些配置决定了你能用哪些RTOS功能。我的原则是:用到的才开,用不到的关掉,省ROM。

  • configUSE_TIMERS:软件定时器,制氧机里用来做周期性检测,必须开
  • configUSE_MUTEXES:互斥量,防止优先级翻转,建议开
  • configUSE_CO_ROUTINES:协程,老掉牙的东西了,别开

中断优先级配置

#define configPRIO_BITS               4
#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY   0x0f
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5
#define configKERNEL_INTERRUPT_PRIORITY         ( configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )
#define configMAX_SYSCALL_INTERRUPT_PRIORITY    ( configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )

这部分最容易出错。说白了,FreeRTOS需要知道哪些中断可以调用API函数,哪些不行。

configMAX_SYSCALL_INTERRUPT_PRIORITY:优先级高于这个值的中断,不能调用任何FreeRTOS API。为什么?因为这些中断会抢占调度器,如果它们调用了API,可能导致内核数据结构损坏。

我曾经犯过一个错:把一个高优先级的中断里调用了xQueueSendFromISR,结果系统随机死机。查了两天才发现是优先级设置问题。从那以后,我所有中断里调用API的,优先级都设得比configMAX_SYSCALL_INTERRUPT_PRIORITY低。

2.4 堆栈设置:算清楚,别省

堆栈设置是新手最容易忽略的地方。任务栈设小了,系统跑着跑着就崩了;设大了,内存浪费。

任务栈大小估算

每个任务创建时都要指定栈大小,单位是字(word),不是字节。在32位MCU上,一个字是4字节。

// 创建任务
xTaskCreate(
    vTaskFunction,       // 任务函数
    "TaskName",          // 任务名
    256,                 // 栈大小,单位是字,即1024字节
    NULL,                // 参数
    1,                   // 优先级
    NULL                 // 任务句柄
);

怎么估算栈大小?我有个土办法:

  1. 先给一个较大的值,比如512字(2KB)
  2. 跑起来后,用uxTaskGetStackHighWaterMark()查看剩余栈空间
  3. 根据剩余空间调整,一般留30%的余量
关键点:任务函数里如果调用了printf、sprintf这类函数,栈消耗会很大。我在制氧机项目里,一个负责显示的任务栈给了1024字,因为要格式化字符串。而一个简单的传感器读取任务,128字就够了。

中断栈与任务栈

FreeRTOS里,中断使用的是被中断任务的栈。也就是说,如果一个任务正在运行,来了中断,中断处理函数用的就是这个任务的栈空间。

这意味着什么?如果你的中断嵌套很深,或者中断里调用了很多函数,那么每个任务的栈都要留出足够的中断处理空间。我一般会在每个任务的栈基础上,额外加200字给中断用。

嗯,这里有个小技巧:把中断处理做得尽量短,把耗时操作放到任务里做。这样既能保证实时性,又能减少栈压力。

空闲任务栈

FreeRTOS会自动创建一个空闲任务,它的栈大小由configMINIMAL_STACK_SIZE定义。这个值不能太小,因为空闲任务里可能会执行钩子函数、内存回收等操作。

我建议configMINIMAL_STACK_SIZE至少设128字(512字节)。如果你开了空闲钩子,建议设到256字以上。

避坑指南:我曾经把configMINIMAL_STACK_SIZE设成64字,结果系统运行几分钟后死机。调试发现是空闲任务栈溢出,破坏了内核数据。从那以后,我至少给128字,稳得很。

2.5 移植验证:跑起来才算数

配置完了,怎么知道移植成功了?我一般跑两个测试:

  1. 任务创建测试:创建两个任务,一个闪灯,一个打印信息。能跑起来,说明基本移植成功。
  2. 栈溢出检测:开启configCHECK_FOR_STACK_OVERFLOW,设为2(最严格模式),然后故意让任务栈溢出,看钩子函数能不能触发。
// 栈溢出钩子函数
void vApplicationStackOverflowHook( TaskHandle_t xTask, char *pcTaskName )
{
    // 到这里说明栈溢出了
    // 可以在这里点亮一个错误指示灯
    // 或者记录错误日志
    for( ;; );
}

这个钩子函数一定要实现,否则链接会报错。我在制氧机项目里,一旦触发栈溢出,就点亮红色故障灯,同时通过串口输出错误信息,方便调试。

好了,这一章的内容就这些。移植FreeRTOS其实不难,关键是理解每个配置项的含义,以及它们在实际项目中会带来什么影响。下一章咱们聊任务创建与调度机制,到时候我会用制氧机的实际任务来举例,更有意思。