2. FreeRTOS内核源码获取与移植:从官网下载源码、关键文件说明、移植步骤详解
好,咱们正式开始动手了。这一章我带你走一遍FreeRTOS的源码获取和移植流程。说实话,很多初学者卡在这一步,不是因为技术难,而是因为不知道哪些文件有用、哪些可以删。我当年第一次移植的时候,对着那一堆文件夹愣了半天。
别急,咱们一步步来。
2.1 从官网下载源码
先找到源头。FreeRTOS的官方仓库在GitHub上,搜索"FreeRTOS/FreeRTOS"就能找到。我个人习惯直接下载最新的LTS版本,稳定、坑少。
下载后解压,你会看到这样的目录结构:
FreeRTOS/
├── FreeRTOS/
│ ├── Source/
│ │ ├── include/ // 头文件
│ │ ├── portable/ // 移植层代码
│ │ ├── list.c
│ │ ├── tasks.c
│ │ ├── queue.c
│ │ ├── timers.c
│ │ ├── event_groups.c
│ │ └── croutine.c
│ └── Demo/ // 官方示例
└── FreeRTOS-Plus/ // 扩展组件
嗯,这里要注意:我们真正需要的是 FreeRTOS/Source/ 目录下的内容。Demo文件夹只是参考,别直接往项目里塞。
2.2 关键文件说明
源码里文件不少,但核心的就那么几个。我挑最重要的三个给你讲讲。
list.c —— 内核的"骨架"
链表是FreeRTOS的基石。任务调度、延时队列、事件等待,底层全是链表操作。list.c 实现了双向循环链表,代码量不大,但极其精悍。
我在项目中遇到过一个问题:任务莫名其妙卡死,查了两天,最后发现是链表节点插入时临界区保护没做好。说白了,链表操作必须关中断,否则一个任务在插入,另一个在删除,直接崩给你看。
核心函数一览:
vListInitialise()—— 初始化链表vListInsertEnd()—— 尾部插入vListInsert()—— 按优先级插入uxListRemove()—— 移除节点
tasks.c —— 任务管理的"大脑"
这是FreeRTOS最核心的文件,没有之一。任务创建、删除、挂起、恢复、调度,全在这里。代码量也是最大的,大概有4000多行。
你想想看,一个操作系统最核心的功能是什么?就是让多个任务"看起来"在同时运行。tasks.c 干的就是这件事。它维护了任务控制块(TCB)、就绪列表、延时列表,还有那个著名的调度器。
我曾经调试过一个bug:任务优先级设了10个,结果高优先级任务一直占着CPU,低优先级任务饿死了。查了半天,发现是 vTaskDelay() 的参数传错了,导致任务根本没进阻塞态。嗯,这种低级错误,犯过一次就记住了。
个人建议:刚开始学的时候,别急着看所有代码。先把 xTaskCreate()、vTaskStartScheduler()、vTaskDelay() 这三个函数搞明白,剩下的慢慢来。
queue.c —— 任务间通信的"桥梁"
任务之间怎么传数据?靠队列。queue.c 实现了队列和信号量(信号量本质上是长度为1的队列)。
队列的核心机制是"生产者-消费者"模型。一个任务往队列里写,另一个任务从队列里读。如果队列满了,写任务可以阻塞等待;如果队列空了,读任务也可以阻塞等待。
我记得有一次,两个任务通过队列传数据,数据量一大就丢包。最后发现是队列长度设得太小,而且写任务没有做超时处理。改大队列深度,加上超时重传,问题就解决了。
避坑指南:我曾经在中断服务函数里调用 xQueueReceive() 并设置了阻塞时间,结果系统直接死机。记住:中断里只能用 xQueueReceiveFromISR(),而且不能阻塞。
2.3 移植步骤详解
好了,理论说完了,咱们动手移植。以STM32F4系列为例,我带你走一遍完整流程。
第一步:准备基础工程
先用STM32CubeMX生成一个基础工程,配置好时钟、GPIO、串口。确保LED能闪烁,串口能打印。这一步是验证硬件没问题。
第二步:复制FreeRTOS源码
在你的工程目录下新建一个 FreeRTOS/ 文件夹,然后把以下内容复制进去:
Source/目录下的所有.c文件(list.c, tasks.c, queue.c, timers.c, event_groups.c, croutine.c)Source/include/目录下的所有头文件Source/portable/目录下找到对应MCU的移植文件
对于STM32F4,你需要从 portable/ 里找到:
portable/GCC/ARM_CM4F/—— 如果你用GCC编译器portable/MemMang/—— 内存管理方案,选一个
内存管理方案怎么选?
| 方案 | 特点 | 适用场景 |
|---|---|---|
| heap_1.c | 只能分配,不能释放 | 任务数量固定,不动态删除 |
| heap_2.c | 可分配可释放,但可能碎片 | 任务大小相近的场景 |
| heap_4.c | 可分配可释放,合并相邻空闲块 | 推荐!大多数场景 |
| heap_5.c | 支持多个不连续内存区域 | 外部RAM + 内部RAM混合使用 |
我个人习惯用 heap_4.c,它解决了碎片问题,而且代码量不大,适合大多数嵌入式项目。
第三步:配置FreeRTOSConfig.h
这是移植中最关键的一步。FreeRTOSConfig.h 是FreeRTOS的配置文件,你需要根据你的MCU和需求来设置。我贴一个常用的配置模板:
#ifndef FREERTOS_CONFIG_H
#define FREERTOS_CONFIG_H
/* 基础配置 */
#define configUSE_PREEMPTION 1 // 抢占式调度
#define configUSE_IDLE_HOOK 0 // 空闲钩子
#define configUSE_TICK_HOOK 0 // 滴答钩子
#define configCPU_CLOCK_HZ ( ( unsigned long ) 168000000 ) // 主频168MHz
#define configTICK_RATE_HZ ( ( TickType_t ) 1000 ) // 系统时钟1ms
#define configMAX_PRIORITIES ( 5 ) // 最大优先级
#define configMINIMAL_STACK_SIZE ( ( unsigned short ) 128 ) // 最小栈大小
#define configTOTAL_HEAP_SIZE ( ( size_t ) ( 30 * 1024 ) ) // 堆大小30KB
#define configMAX_TASK_NAME_LEN ( 16 ) // 任务名最大长度
/* 功能开关 */
#define configUSE_16_BIT_TICKS 0 // 32位系统用0
#define configIDLE_SHOULD_YIELD 1 // 空闲任务让出CPU
#define configUSE_MUTEXES 1 // 使用互斥量
#define configUSE_RECURSIVE_MUTEXES 1 // 使用递归互斥量
#define configUSE_COUNTING_SEMAPHORES 1 // 使用计数信号量
#define configUSE_TIMERS 1 // 使用软件定时器
/* 中断配置 */
#define configPRIO_BITS 4 // STM32用4位优先级
#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY 0x0F
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5
#define configKERNEL_INTERRUPT_PRIORITY ( configLIBRARY_LOWEST_INTERRUPT_PRIORITY << 4 )
#define configMAX_SYSCALL_INTERRUPT_PRIORITY ( configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << 4 )
/* 断言 */
#define configASSERT( x ) if( ( x ) == 0 ) { taskDISABLE_INTERRUPTS(); for( ;; ); }
#endif /* FREERTOS_CONFIG_H */
避坑指南:我曾经把 configCPU_CLOCK_HZ 设错了,结果系统时钟快了10倍,任务调度乱成一锅粥。一定要确认你的MCU主频是多少,STM32F407是168MHz,F103是72MHz,别搞混了。
第四步:实现移植接口
FreeRTOS需要几个底层接口才能跑起来:
- SysTick中断处理 —— 在
stm32f4xx_it.c中调用xPortSysTickHandler() - PendSV中断处理 —— 用于上下文切换,移植文件里已经实现了
- SVC中断处理 —— 用于启动第一个任务,移植文件里已经实现了
你只需要在SysTick中断里加一行代码:
void SysTick_Handler(void)
{
if (xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED)
{
xPortSysTickHandler();
}
}
嗯,这里要注意:调度器启动之前,SysTick中断也会触发,所以要先判断调度器状态。
第五步:编写测试代码
移植完成后,写一个简单的测试程序验证一下:
void vTask1(void *pvParameters)
{
while(1)
{
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
vTaskDelay(pdMS_TO_TICKS(500));
}
}
void vTask2(void *pvParameters)
{
while(1)
{
printf("Task2 is running\n");
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART2_UART_Init();
xTaskCreate(vTask1, "Task1", 128, NULL, 1, NULL);
xTaskCreate(vTask2, "Task2", 128, NULL, 2, NULL);
vTaskStartScheduler();
while(1);
}
如果LED以500ms间隔闪烁,串口每秒打印一次"Task2 is running",恭喜你,移植成功了!
2.4 知识体系总览
为了让你对整个移植流程有个全局认识,我画了一张图:
这张图把整个移植流程串起来了。从源码获取,到理解关键文件,再到一步步移植,最后验证。你跟着这个流程走,基本不会出大问题。
最后说一句:移植这件事,第一次做觉得麻烦,第二次就轻车熟路了。关键是理解每个文件的作用,而不是机械地复制粘贴。等你移植过两三个平台,就会发现FreeRTOS的设计真的很优雅。
公众号:蓝海资料掘金营,微信deep3321