3. 任务管理基础:任务创建与删除、任务优先级、任务状态切换
各位同学,欢迎来到第三章。这一章我们聊聊任务管理。
说实话,任务管理是RTOS的魂。你想想看,一个裸机程序,无非就是大循环里跑几个函数。但到了RTOS里,事情变得不一样了——你要同时管理好几个任务,谁先跑、谁后跑、谁暂停、谁退出。这就像指挥一个交响乐团,每个乐手都得听你的。
我刚开始用RTOS时,总觉得任务管理很简单。不就是创建几个任务,设个优先级嘛。结果第一次做电机控制项目,就踩了个大坑。嗯,咱们慢慢聊。
3.1 任务创建与删除
先说说怎么创建一个任务。在FreeRTOS里,我们用xTaskCreate()这个API。它的原型长这样:
BaseType_t xTaskCreate(
TaskFunction_t pvTaskCode, // 任务函数指针
const char * const pcName, // 任务名称(调试用)
configSTACK_DEPTH_TYPE usStackDepth, // 栈深度
void *pvParameters, // 任务参数
UBaseType_t uxPriority, // 任务优先级
TaskHandle_t *pxCreatedTask // 任务句柄
);
参数看着多,其实常用的就几个。我一般这么用:
void vMotorControlTask(void *pvParameters)
{
// 电机控制逻辑
for(;;)
{
// 读取编码器
// 计算PID
// 输出PWM
vTaskDelay(pdMS_TO_TICKS(1)); // 1ms周期
}
}
void vCreateTasks(void)
{
TaskHandle_t xMotorHandle = NULL;
xTaskCreate(
vMotorControlTask, // 任务函数
"MotorCtrl", // 任务名
256, // 栈大小(单位:字)
NULL, // 无参数
3, // 优先级
&xMotorHandle // 句柄
);
}
小提示:栈大小怎么定?我有个笨办法——先给个较大的值(比如512字),跑起来后用uxTaskGetStackHighWaterMark()看看实际用了多少,再慢慢往下调。别一开始就给太小,任务栈溢出可是很难查的。
删除任务就更简单了。任务自己调用vTaskDelete(NULL)就能把自己干掉。或者别的任务拿着句柄,也能删掉它:
// 任务自己退出
void vSomeTask(void *pvParameters)
{
// 做点事
vTaskDelete(NULL); // 把自己删了
}
// 别的任务删它
void vOtherTask(void *pvParameters)
{
// 等某个条件满足
vTaskDelete(xMotorHandle); // 删掉电机控制任务
}
注意:删除任务前,一定要确保它占用的资源已经释放了。比如动态分配的内存、打开的外设、互斥量等等。我曾经有个同事,删任务前没关定时器,结果任务没了,定时器中断还在跑,系统直接崩了。
3.2 任务优先级
优先级这东西,说白了就是给任务排个队。数字越大,优先级越高。FreeRTOS支持0到configMAX_PRIORITIES-1的优先级。
在电机控制里,优先级怎么分配?我一般这么干:
| 优先级 | 任务类型 | 说明 |
|---|---|---|
| 4(最高) | 电流环控制 | 实时性要求最高,通常10-50us周期 |
| 3 | 速度环控制 | 次高,1ms左右周期 |
| 2 | 位置环控制 | 再次之,10ms左右周期 |
| 1 | 通信任务 | 比如CAN、串口通信 |
| 0(最低) | 后台任务 | 日志、状态显示等 |
你可能会问:为什么电流环优先级最高?因为电机控制里,电流环的响应速度直接决定了系统的稳定性。电流环慢了,电机可能就抖起来了。我见过一个项目,电流环周期从50us改成100us,电机就开始嗡嗡响。
核心原则:实时性要求越高的任务,优先级越高。但别把所有任务都设成高优先级,那样低优先级的任务就永远没机会跑了。这叫「优先级反转」的极端情况。
3.3 任务状态切换
任务在RTOS里有四种状态:就绪、运行、阻塞、挂起。我画个图你感受一下:
创建 → 就绪 → 运行 → 阻塞
↑ ↓ ↓
└──────┴──────┘
挂起 ←─────────
说白了就是:
- 就绪:任务准备好了,就等CPU来跑它
- 运行:CPU正在跑这个任务
- 阻塞:任务在等某个事件,比如延时、信号量、队列
- 挂起:任务被强制暂停,不参与调度
状态切换的典型场景:
void vMotorTask(void *pvParameters)
{
for(;;)
{
// 运行状态:执行电机控制
vMotorControl();
// 阻塞状态:等待1ms
vTaskDelay(pdMS_TO_TICKS(1));
// 延时结束后,回到就绪状态
// 调度器选它时,又变成运行状态
}
}
挂起状态比较特殊。我一般在调试时用:
// 挂起某个任务
vTaskSuspend(xMotorHandle);
// 恢复它
vTaskResume(xMotorHandle);
经验之谈:挂起任务时要注意,如果任务正拿着某个互斥量,其他等这个互斥量的任务也会被卡住。我曾经调试一个电机同步问题,挂起了速度环任务,结果位置环任务一直在等速度环释放互斥量,整个系统都卡死了。嗯,从那以后我挂起任务前都会检查资源占用情况。
3.4 避坑指南
最后,分享几个我踩过的坑:
- 任务栈别给太小:特别是任务里有局部大数组或者递归调用时。我建议至少给256字起步。
- 优先级别设太高:高优先级任务如果一直不阻塞,低优先级任务就永远没机会跑。这叫「饥饿」。
- 删除任务前检查资源:动态分配的内存、打开的文件、占用的外设,都得释放干净。
- 挂起任务要谨慎:特别是任务间有依赖关系时,挂起一个可能导致连锁反应。
好了,这一章就到这里。任务管理是RTOS的基础,也是你写好电机控制程序的关键。下一章我们聊聊任务间通信,那才是真正让多个任务协同工作的精髓。