第二章 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 |
2.2 移植步骤:三步走,不迷路
移植FreeRTOS,说白了就三步。别想复杂了:
- 复制源码:把Source目录和对应的Portable目录拷到你的工程里
- 配置FreeRTOSConfig.h:这是核心,后面细讲
- 实现滴答定时器中断:给系统一个心跳
嗯,这里要注意。很多人卡在第三步。滴答定时器中断里要调用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,够用。
功能裁剪配置
#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 // 任务句柄
);
怎么估算栈大小?我有个土办法:
- 先给一个较大的值,比如512字(2KB)
- 跑起来后,用
uxTaskGetStackHighWaterMark()查看剩余栈空间 - 根据剩余空间调整,一般留30%的余量
中断栈与任务栈
FreeRTOS里,中断使用的是被中断任务的栈。也就是说,如果一个任务正在运行,来了中断,中断处理函数用的就是这个任务的栈空间。
这意味着什么?如果你的中断嵌套很深,或者中断里调用了很多函数,那么每个任务的栈都要留出足够的中断处理空间。我一般会在每个任务的栈基础上,额外加200字给中断用。
嗯,这里有个小技巧:把中断处理做得尽量短,把耗时操作放到任务里做。这样既能保证实时性,又能减少栈压力。
空闲任务栈
FreeRTOS会自动创建一个空闲任务,它的栈大小由configMINIMAL_STACK_SIZE定义。这个值不能太小,因为空闲任务里可能会执行钩子函数、内存回收等操作。
我建议configMINIMAL_STACK_SIZE至少设128字(512字节)。如果你开了空闲钩子,建议设到256字以上。
2.5 移植验证:跑起来才算数
配置完了,怎么知道移植成功了?我一般跑两个测试:
- 任务创建测试:创建两个任务,一个闪灯,一个打印信息。能跑起来,说明基本移植成功。
- 栈溢出检测:开启
configCHECK_FOR_STACK_OVERFLOW,设为2(最严格模式),然后故意让任务栈溢出,看钩子函数能不能触发。
// 栈溢出钩子函数
void vApplicationStackOverflowHook( TaskHandle_t xTask, char *pcTaskName )
{
// 到这里说明栈溢出了
// 可以在这里点亮一个错误指示灯
// 或者记录错误日志
for( ;; );
}
这个钩子函数一定要实现,否则链接会报错。我在制氧机项目里,一旦触发栈溢出,就点亮红色故障灯,同时通过串口输出错误信息,方便调试。
好了,这一章的内容就这些。移植FreeRTOS其实不难,关键是理解每个配置项的含义,以及它们在实际项目中会带来什么影响。下一章咱们聊任务创建与调度机制,到时候我会用制氧机的实际任务来举例,更有意思。