第1章:任务基础与调度机制
大家好,我是你们的老朋友。今天咱们来聊聊FreeRTOS最核心的东西——任务。说实话,我做了这么多年嵌入式开发,见过太多人一上来就写任务,结果系统跑起来各种诡异问题。其实啊,把任务基础搞明白,后面就顺了。
1.1 任务状态机:任务到底有几种状态?
先问大家一个问题:一个任务在系统里,可能处于哪些状态?
我刚开始学RTOS时,以为任务就两种状态:运行和不运行。后来被坑了一次才明白,FreeRTOS的任务状态机其实有四种状态:
- 运行态(Running):正在使用CPU,同一时刻只有一个任务处于此状态
- 就绪态(Ready):能运行但没轮到,等着CPU分配
- 阻塞态(Blocked):在等某个事件,比如延时、信号量、队列
- 挂起态(Suspended):被vTaskSuspend()挂起,除非别人调用vTaskResume(),否则永远不运行
核心要点:任务状态切换是FreeRTOS调度器工作的基础。理解了这个,你才能预测任务的行为。
我画了一张状态机图,大家一看就明白:
我的经验:调试时最常用的就是看任务状态。我曾经遇到一个任务卡死,查了半天发现它一直在阻塞态等一个永远不会来的信号量。用vTaskList()打印状态,一眼就发现问题了。
1.2 任务优先级:谁先跑?谁后跑?
优先级这东西,说白了就是给任务排个队。数值越小优先级越低,0是最低,configMAX_PRIORITIES-1是最高。
我个人习惯把优先级分成几档:
| 优先级范围 | 典型用途 | 举例 |
|---|---|---|
| 0-2 | 后台任务、空闲任务 | 系统监控、日志打印 |
| 3-5 | 普通业务任务 | 传感器采集、数据处理 |
| 6-8 | 实时性要求高的任务 | 通信协议处理、控制算法 |
| 9以上 | 紧急任务 | 中断下半部、安全保护 |
注意:优先级不是越高越好。我曾经在一个项目里把所有任务都设成高优先级,结果低优先级任务永远得不到CPU时间,系统直接卡死。这叫「优先级反转」的极端情况。
FreeRTOS的调度规则很简单:
- 高优先级任务就绪时,立即抢占低优先级任务
- 同优先级任务轮流执行(时间片轮转)
- 没有就绪的高优先级任务时,才执行低优先级任务
1.3 任务创建与删除:动手写代码
好了,理论说完了,咱们直接上代码。任务创建用xTaskCreate(),原型长这样:
BaseType_t xTaskCreate(
TaskFunction_t pvTaskCode, // 任务函数指针
const char * const pcName, // 任务名称(调试用)
configSTACK_DEPTH_TYPE usStackDepth, // 栈深度(单位:字)
void *pvParameters, // 传给任务的参数
UBaseType_t uxPriority, // 优先级
TaskHandle_t *pxCreatedTask // 任务句柄(可传NULL)
);
我写一个最简单的例子:
// 任务函数
void vTask1(void *pvParameters)
{
int count = 0;
while(1)
{
count++;
printf("Task1 running, count=%d\n", count);
vTaskDelay(pdMS_TO_TICKS(1000)); // 延时1秒
}
}
// 创建任务
TaskHandle_t xTask1Handle = NULL;
xTaskCreate(
vTask1, // 任务函数
"Task1", // 名字
128, // 栈大小
NULL, // 无参数
1, // 优先级1
&xTask1Handle // 保存句柄
);
关键点:任务函数绝对不能返回!必须用while(1)死循环。如果函数返回了,系统会调用vTaskDelete(NULL)把自己删掉,但栈资源可能没释放干净。
删除任务用vTaskDelete():
// 删除自己
vTaskDelete(NULL);
// 删除其他任务
vTaskDelete(xTask1Handle);
避坑指南:我曾经在删除任务后,还继续使用它的句柄,结果系统直接HardFault。记住:删除后句柄就失效了,最好把句柄置为NULL。
1.4 任务切换原理:调度器是怎么工作的?
任务切换,说白了就是CPU在任务之间跳来跳去。FreeRTOS用的是抢占式调度,加上时间片轮转。
切换过程分三步:
- 保存现场:把当前任务的寄存器、栈指针等压入自己的栈
- 选择下一个任务:调度器从就绪列表中挑一个优先级最高的
- 恢复现场:把新任务的寄存器从栈里弹出来,CPU继续执行
触发切换的时机有:
- 任务主动调用vTaskDelay()或等待事件
- 高优先级任务就绪(比如中断里释放了信号量)
- 时间片用完(SysTick中断触发)
我画个流程图,大家感受一下:
嗯,这里要注意:任务切换是在PendSV中断里完成的。为什么用PendSV?因为它的优先级最低,可以等所有其他中断处理完再切换,避免中断嵌套导致的问题。
总结一下:任务切换的核心就是「保存现场-选择任务-恢复现场」三步曲。你想想看,每次切换都要做这些操作,所以任务切换是有开销的。任务太多、切换太频繁,CPU时间都花在切换上了,实际干活的时间就少了。
我个人建议:任务数量控制在10个以内,切换频率别太高。我曾经在一个项目里创建了30多个任务,结果系统响应慢得不行。后来合并成8个任务,性能直接翻倍。
好了,这一章的内容就到这儿。任务基础打牢了,后面讲队列、信号量、互斥量这些通信机制时,你就能理解它们是怎么影响任务状态的。