3. 任务创建与管理:任务函数原型、任务优先级、任务堆栈配置、任务删除与挂起
好,咱们进入第三章。任务创建与管理,这可以说是RTOS里最基础、也最核心的操作了。你想想看,整个无人机飞控系统,说白了就是一堆任务在跑来跑去。有的负责传感器读取,有的负责姿态解算,有的负责电机控制。怎么把这些任务组织好、管理好,直接决定了你的飞控稳不稳、会不会炸机。
我个人习惯,在讲具体API之前,先让大家理解一个概念:任务到底是什么?在RTOS眼里,任务就是一个无限循环的函数,加上它自己的栈空间、优先级和一些控制块信息。嗯,就这么简单。
3.1 任务函数原型
先看任务函数的写法。在FreeRTOS里,任务函数长这样:
void vTaskFunction(void *pvParameters)
{
// 参数转换
uint32_t ulParam = (uint32_t)pvParameters;
// 任务初始化代码(只执行一次)
sensor_init();
// 无限循环
for(;;)
{
// 任务主体代码
read_sensor_data();
vTaskDelay(pdMS_TO_TICKS(10)); // 10ms周期
}
}
注意几个关键点:
- 返回值必须是void——任务函数永远不会返回,返回就意味着任务结束了,系统会帮你清理,但一般不建议这么干
- 参数是一个void指针——你可以传任何东西进去,整数、结构体指针都行。我在项目中经常传一个结构体指针,里面放任务需要的配置参数
- 函数体必须是一个无限循环——这是RTOS任务的典型模式。如果任务只执行一次,那执行完就会被删除
3.2 任务优先级
优先级是RTOS里最容易出问题的地方。我记得刚做无人机那会儿,就因为优先级配错了,导致飞控在急转弯时直接失控。后来查了三天才发现,是姿态解算任务的优先级比传感器读取任务低,导致数据更新不及时。
FreeRTOS的优先级规则很简单:数值越大,优先级越高。比如:
#define PRIORITY_IDLE 0 // 空闲任务
#define PRIORITY_LOW 1 // 低优先级
#define PRIORITY_MEDIUM 2 // 中优先级
#define PRIORITY_HIGH 3 // 高优先级
#define PRIORITY_CRITICAL 4 // 关键任务
在无人机系统里,我一般这样分配:
| 任务类型 | 优先级 | 说明 |
|---|---|---|
| 电机控制 | 最高(如5) | 实时性要求最高,1ms以内必须响应 |
| 姿态解算 | 高(如4) | 需要及时更新姿态数据 |
| 传感器读取 | 中高(如3) | 周期性读取,可容忍少量延迟 |
| 遥控器接收 | 中(如2) | 人操作频率有限,优先级不用太高 |
| 日志记录 | 低(如1) | 后台任务,有空才做 |
3.3 任务堆栈配置
堆栈配置是个技术活。配大了浪费内存,配小了直接栈溢出炸机。我见过太多新手在这上面栽跟头了。
先看创建任务的API:
TaskHandle_t xTaskCreate(
TaskFunction_t pvTaskCode, // 任务函数指针
const char * const pcName, // 任务名称(调试用)
configSTACK_DEPTH_TYPE usStackDepth, // 堆栈深度,单位是字(word)
void *pvParameters, // 参数
UBaseType_t uxPriority, // 优先级
TaskHandle_t *pxCreatedTask // 任务句柄
);
堆栈大小怎么算?我给大家一个经验公式:
- 简单任务(只调用几个函数):128-256字(512-1024字节)
- 中等任务(有局部数组、调用库函数):256-512字
- 复杂任务(有递归、大数组、浮点运算):512-1024字
重要:FreeRTOS的堆栈单位是字(word),不是字节!在32位MCU上,1个字=4字节。所以usStackDepth=128,实际分配的是512字节。
我常用的调试方法:先给一个较大的堆栈(比如512字),运行一段时间后用uxTaskGetStackHighWaterMark()查看实际使用量,然后根据结果调整。比如:
// 在任务循环中定期检查
UBaseType_t uxHighWaterMark = uxTaskGetStackHighWaterMark(NULL);
if(uxHighWaterMark < 50) // 剩余少于50字,危险!
{
// 记录警告日志
log_warning("Task stack low: %d", uxHighWaterMark);
}
3.4 任务删除与挂起
任务的生命周期管理,说白了就是什么时候让任务干活,什么时候让它歇着。
任务删除:
// 删除自己
vTaskDelete(NULL);
// 删除其他任务
vTaskDelete(xTaskHandle);
注意:被删除的任务的堆栈和TCB会被释放回堆。但如果你用了动态内存分配,记得在任务退出前自己释放。
任务挂起与恢复:
// 挂起任务
vTaskSuspend(xTaskHandle);
// 恢复任务
vTaskResume(xTaskHandle);
// 从中断中恢复任务
xTaskResumeFromISR(xTaskHandle);
挂起和删除的区别:
- 挂起:任务还在内存里,只是不参与调度。可以随时恢复,开销小
- 删除:任务彻底没了,资源释放。下次要用得重新创建
在无人机里,我经常用挂起来做省电模式。比如检测到无人机悬停时,把一些非关键任务挂起,只保留姿态控制和电机控制。等用户开始操作了,再恢复其他任务。
void vPowerSaveMode(void)
{
// 挂起非关键任务
vTaskSuspend(xLogTaskHandle);
vTaskSuspend(xLEDTaskHandle);
vTaskSuspend(xTelemetryTaskHandle);
// 降低主控频率
system_clock_set(48 MHz);
}
void vNormalMode(void)
{
// 恢复所有任务
vTaskResume(xLogTaskHandle);
vTaskResume(xLEDTaskHandle);
vTaskResume(xTelemetryTaskHandle);
// 恢复主控频率
system_clock_set(168 MHz);
}
核心要点总结:
- 任务函数必须是一个无限循环,参数用void指针传递
- 优先级数值越大越优先,关键任务(如电机控制)给最高优先级
- 堆栈大小先给大再调小,用HighWaterMark函数辅助测量
- 删除会释放资源,挂起只是暂停调度,按需选择
嗯,这一章的内容就这些。任务创建与管理是RTOS的基石,你把这些搞明白了,后面讲任务同步、消息队列什么的,就顺理成章了。下一章我们聊聊任务间通信,那可是无人机数据流的命脉。