第1章:FreeRTOS移植与工程搭建
各位同学,咱们今天正式开始搞FreeRTOS。说实话,我最早接触RTOS的时候,也觉得这东西挺玄乎的。后来在几个项目里摔过跟头,才慢慢摸到门道。今天这一章,咱们就把FreeRTOS的移植和第一个任务跑起来,这是后面所有实战的基础。
1.1 FreeRTOS源码结构
先看看FreeRTOS的源码长什么样。你从官网或者GitHub上下载下来,解压后会发现这么几个核心目录:
- Source/ — 核心源码,所有RTOS的调度、队列、信号量都在这里
- Source/include/ — 头文件,你写应用时#include的那些
- Source/portable/ — 移植层,不同MCU的适配代码
- Demo/ — 官方示例工程,参考价值很大
我个人习惯,拿到源码后先看FreeRTOSConfig.h这个文件。它就像RTOS的开关面板,配置不对,后面全白搭。我在一个项目中就吃过这个亏——忘了配置堆大小,任务创建直接返回NULL,查了半天。
核心文件速查表:
| 文件 | 作用 |
|---|---|
| tasks.c | 任务创建、删除、调度核心 |
| queue.c | 队列与消息传递 |
| timers.c | 软件定时器 |
| event_groups.c | 事件组 |
| port.c / portmacro.h | 移植层,与硬件相关 |
1.2 移植步骤
移植说白了,就是把FreeRTOS嫁接到你的芯片上。不同MCU的移植细节不一样,但核心步骤是通用的。我以STM32为例,给你拆解一下:
- 准备基础工程 — 先有一个能点灯的裸机工程,时钟、GPIO都调通
- 复制源码 — 把Source/目录下的文件加到工程里
- 选择移植层 — 在portable/里找到对应MCU的文件夹,比如ARM_CM3、ARM_CM4F
- 配置FreeRTOSConfig.h — 设置堆大小、任务优先级数量、时钟节拍频率
- 实现滴答定时器 — 提供一个1ms中断,调用xPortSysTickHandler()
- 实现SVC和PendSV中断 — 这两个是RTOS切换任务的关键
- 编译调试 — 先编译通过,再跑个空任务验证
注意:我曾经在移植时忘了配置中断优先级分组。FreeRTOS要求中断优先级分组为4(即4位抢占优先级,0位子优先级)。如果设置不对,调度器会跑飞。这个坑我踩过,你千万别踩。
嗯,这里要特别说一下FreeRTOSConfig.h里的几个关键配置:
#define configUSE_PREEMPTION 1 // 抢占式调度
#define configCPU_CLOCK_HZ ( ( unsigned long ) 72000000 ) // 主频
#define configTICK_RATE_HZ ( ( TickType_t ) 1000 ) // 1ms一个tick
#define configMAX_PRIORITIES ( 5 ) // 优先级数量
#define configMINIMAL_STACK_SIZE ( ( unsigned short ) 128 ) // 最小栈大小
#define configTOTAL_HEAP_SIZE ( ( size_t ) ( 15 * 1024 ) ) // 堆大小15KB
你想想看,堆大小设得太小,任务创建会失败;设得太大,RAM不够用。我一般先给个保守值,跑起来后用uxTaskGetStackHighWaterMark()看看实际用了多少,再调整。
1.3 创建第一个任务
移植搞定后,咱们来创建第一个任务。说白了,任务就是一个永不返回的C函数,里面通常是个死循环。
void vTask1(void *pvParameters)
{
while(1)
{
// 任务代码写在这里
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
vTaskDelay(pdMS_TO_TICKS(500)); // 延时500ms
}
}
void vTask2(void *pvParameters)
{
while(1)
{
// 另一个任务
printf("Task2 is running\n");
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
然后在main函数里创建并启动调度器:
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
// 创建任务
xTaskCreate(vTask1, "Task1", 128, NULL, 1, NULL);
xTaskCreate(vTask2, "Task2", 128, NULL, 2, NULL);
// 启动调度器
vTaskStartScheduler();
// 正常情况下不会跑到这里
while(1);
}
小技巧:任务栈大小单位是字(word),不是字节。在32位MCU上,128个字就是512字节。我建议每个任务至少给128个字,复杂任务给256或512。用uxTaskGetStackHighWaterMark()可以查看剩余栈空间,帮你找到最优值。
1.4 任务状态切换
任务在FreeRTOS里有四种状态:运行态、就绪态、阻塞态、挂起态。你想想看,CPU只有一个核,同一时刻只能跑一个任务。那其他任务在干嘛?它们就在这几个状态之间跳来跳去。
| 状态 | 说明 | 进入方式 |
|---|---|---|
| 运行态(Running) | 正在使用CPU | 调度器选中该任务 |
| 就绪态(Ready) | 可以运行,但CPU被别的任务占着 | 任务创建后、从阻塞态恢复 |
| 阻塞态(Blocked) | 等待某个事件或延时 | 调用vTaskDelay()、等待队列/信号量 |
| 挂起态(Suspended) | 被暂停,不参与调度 | 调用vTaskSuspend() |
状态切换的典型场景是这样的:
- 任务A正在运行,调用
vTaskDelay(100),进入阻塞态 - 调度器从就绪态任务中选一个优先级最高的,比如任务B,让它运行
- 100ms后,任务A的延时结束,回到就绪态
- 如果任务B此时也阻塞了,调度器就让任务A运行
我在一个按摩仪项目里就遇到过一个问题:两个任务优先级设反了,导致高优先级的任务一直占着CPU,低优先级的任务饿死了。后来我把按摩电机控制和按键扫描的任务优先级调了一下,问题就解决了。说白了,优先级设计是个经验活,多试几次就明白了。
记住这个原则:任务优先级不要超过5个,太多反而增加调度开销。我一般用3-4个优先级就够用了。
小结
这一章咱们把FreeRTOS的源码结构、移植步骤、第一个任务的创建、以及任务状态切换都过了一遍。你跟着做一遍,把LED闪起来,就算入门了。下一章咱们会深入任务调度机制,看看FreeRTOS到底是怎么决定「下一个该谁跑」的。
有什么问题,欢迎交流。咱们下章见。
公众号:蓝海资料掘金营,微信deep3321