3. FreeRTOS 内核入门:任务创建与删除、任务状态机、任务优先级、vTaskDelay与调度机制
好,咱们今天正式进入 FreeRTOS 的内核世界。说实话,很多初学者一上来就被各种概念砸晕了——任务、队列、信号量、互斥量……其实你想想看,RTOS 最核心的东西就一个:任务。任务搞明白了,后面那些都是锦上添花。
我个人习惯把 FreeRTOS 比作一个公司。任务就是员工,调度器就是老板。老板怎么安排员工干活?谁优先级高谁先干?谁在摸鱼(阻塞)就换人?这就是我们今天要聊的内容。
3.1 任务创建与删除
先看怎么创建一个任务。FreeRTOS 提供了两个 API:xTaskCreate 和 xTaskCreateStatic。前者动态分配任务栈,后者静态分配。我建议新手先用动态的,省心。
// 任务函数原型
void vTaskFunction(void *pvParameters);
// 创建任务
BaseType_t xTaskCreate(
TaskFunction_t pvTaskCode, // 任务函数指针
const char * const pcName, // 任务名称(调试用)
configSTACK_DEPTH_TYPE usStackDepth, // 栈深度,单位是字(4字节)
void *pvParameters, // 传给任务函数的参数
UBaseType_t uxPriority, // 任务优先级(0 ~ configMAX_PRIORITIES-1)
TaskHandle_t *pxCreatedTask // 返回的任务句柄
);
返回值是 pdPASS 或 errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY。嗯,这里要注意:一定要检查返回值。我在项目中遇到过好几次,堆栈分配失败导致任务没创建成功,程序却继续往下跑,最后莫名其妙死机。排查了半天才发现是堆空间不够。
uxTaskGetStackHighWaterMark() 查看剩余栈空间,再逐步调小。别一开始就抠门,容易踩坑。
删除任务更简单:
// 删除当前任务
vTaskDelete(NULL);
// 删除指定任务
vTaskDelete(xTaskHandle);
注意:任务删除后,它占用的栈和TCB(任务控制块)会被释放,但如果你用了静态创建,需要自己处理内存释放。另外,千万别在中断服务函数里调用 vTaskDelete,会出问题。
3.2 任务状态机
FreeRTOS 的任务有四种状态:运行态、就绪态、阻塞态、挂起态。我画了一张图,帮你理清它们之间的关系:
说白了,任务就是在这些状态之间跳来跳去。你写代码的时候,心里要清楚:当前任务在哪个状态?下一步会跳到哪个状态? 这对调试特别有帮助。
3.3 任务优先级
FreeRTOS 支持抢占式调度,优先级高的任务会抢 CPU。优先级数值越小,优先级越低(0 是最低)。
| 优先级值 | 含义 | 典型用途 |
|---|---|---|
| 0 | 最低优先级(空闲任务) | 空闲任务、后台低功耗任务 |
| 1 ~ 3 | 低优先级 | 普通业务逻辑、数据显示 |
| 4 ~ 6 | 中优先级 | 通信处理、协议栈 |
| 7 ~ configMAX_PRIORITIES-1 | 高优先级 | 实时控制、紧急响应 |
优先级设计有个原则:够用就好,别贪多。我一般只用 3~5 个优先级层级。优先级太多,调度开销会变大,而且容易搞混。
3.4 vTaskDelay 与调度机制
vTaskDelay 是 FreeRTOS 里最常用的延时函数。它和 HAL_Delay 最大的区别是:vTaskDelay 会让任务进入阻塞态,把 CPU 让给其他任务。
// 延时 1000 个 tick(假设 configTICK_RATE_HZ = 1000,则延时 1 秒)
vTaskDelay(pdMS_TO_TICKS(1000));
// 或者直接写 tick 数
vTaskDelay(1000); // 延时 1000 个 tick
这里有个细节:vTaskDelay 指定的是从当前时刻开始,等待多少个 tick。如果你想让任务以固定周期运行,用 vTaskDelayUntil 更合适,它能避免累积误差。
TickType_t xLastWakeTime = xTaskGetTickCount();
const TickType_t xFrequency = pdMS_TO_TICKS(100); // 100ms 周期
while(1) {
// 等待下一个周期
vTaskDelayUntil(&xLastWakeTime, xFrequency);
// 执行周期性任务
// ...
}
uxTaskGetSystemState() 查看所有任务的状态和运行时间。我曾经用这个函数抓到一个任务跑飞的问题——某个任务一直处于运行态,其他任务全被饿死了。
调度机制说白了就两句话:
- 抢占式调度: 高优先级任务就绪时,立即抢占低优先级任务。
- 时间片轮转: 同优先级任务轮流使用 CPU,每个任务跑一个时间片(默认 1 个 tick)。
你想想看,如果没有抢占式调度,一个高优先级任务要等低优先级任务跑完才能执行,那实时性从何谈起?所以 FreeRTOS 默认就是抢占式,这也是大多数 RTOS 的选择。
3.5 实战:创建两个任务交替运行
最后,咱们写个简单的例子。创建两个任务,一个每 500ms 打印 "Task1",另一个每 1000ms 打印 "Task2"。
void vTask1(void *pvParameters) {
while(1) {
printf("Task1 running\n");
vTaskDelay(pdMS_TO_TICKS(500));
}
}
void vTask2(void *pvParameters) {
while(1) {
printf("Task2 running\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);
}
运行效果:Task1 每 500ms 打印一次,Task2 每 1000ms 打印一次。两个任务同优先级,靠时间片轮转和 vTaskDelay 让出 CPU。
- 任务创建用
xTaskCreate,记得检查返回值 - 任务有四种状态:运行、就绪、阻塞、挂起
- 优先级数值越小优先级越低,0 是空闲任务
vTaskDelay让任务阻塞,释放 CPU- 同优先级任务靠时间片轮转,高优先级任务抢占低优先级
嗯,这一章的内容就到这儿。任务管理是 FreeRTOS 的基石,搞懂了这些,后面学队列、信号量、互斥量就会轻松很多。记住我的一句话:写 RTOS 代码,心里时刻装着任务状态机。