第4章:任务优先级设计
优先级设计这事儿,说简单也简单,说复杂能让你调一整天。我刚开始做飞控那会儿,觉得优先级不就是给任务排个序嘛,高的先跑,低的后跑。直到第一次遇到无人机空中突然「卡住」——嗯,就是那种电机还在转,但姿态完全失控的状态。后来查了三天,才发现是优先级反转惹的祸。
4.1 优先级反转问题
先说说什么是优先级反转。你想想看,有三个任务:高优先级任务A、中优先级任务B、低优先级任务C。正常情况下,A应该最先执行对吧?但现实往往很骨感。
假设C先拿到了某个共享资源(比如I2C总线),然后A来了,想用同一个资源。A发现资源被占着,只能等C释放。这时候B也来了,B不需要那个资源,所以B直接抢占了C的CPU时间。结果呢?A在等C,C在等B,B在跑自己的代码。高优先级的A,反而被中优先级的B给「踩」了。
核心问题:高优先级任务间接被低优先级任务阻塞,而中优先级任务趁机「插队」。这就是优先级反转。
我在项目中遇到过最典型的案例:飞控的传感器数据采集任务(高优先级)被一个日志记录任务(低优先级)阻塞,中间还夹着一个通信任务(中优先级)。结果传感器数据延迟了50ms,飞控直接触发保护性降落。你说冤不冤?
4.2 优先级继承协议
怎么解决?一个经典方案是优先级继承协议。说白了就是:当低优先级任务C持有高优先级任务A需要的资源时,C临时「继承」A的优先级。这样B就抢不过C了,C能尽快跑完释放资源。
我习惯在FreeRTOS里这么配:
// 创建互斥量时使用优先级继承
SemaphoreHandle_t xMutex = xSemaphoreCreateMutex();
// 任务A(高优先级)
void TaskA(void *pvParameters) {
while(1) {
// 尝试获取互斥量
if(xSemaphoreTake(xMutex, portMAX_DELAY) == pdTRUE) {
// 访问共享资源
readSensorData();
xSemaphoreGive(xMutex);
}
}
}
// 任务C(低优先级)
void TaskC(void *pvParameters) {
while(1) {
xSemaphoreTake(xMutex, portMAX_DELAY);
// 写日志到SD卡(耗时操作)
writeLogData();
xSemaphoreGive(xMutex);
}
}
避坑指南:我曾经在STM32F4上用过优先级继承,发现中断服务函数里千万别用带继承的互斥量。中断上下文没有任务优先级的概念,用了反而会死锁。记住:ISR里只用信号量,别用互斥量。
4.3 优先级天花板协议
另一个方案是优先级天花板协议。这个更「暴力」一点:每个共享资源都设定一个「天花板优先级」——也就是所有可能访问这个资源的任务中,最高的那个优先级。谁拿到资源,谁的优先级就立刻升到天花板。
举个例子:
| 共享资源 | 可能访问的任务 | 天花板优先级 |
|---|---|---|
| I2C总线 | 传感器任务(50)、日志任务(20) | 50 |
| GPS数据缓冲区 | 导航任务(45)、显示任务(15) | 45 |
| PWM控制寄存器 | 控制任务(60)、校准任务(30) | 60 |
这样做的好处是:实现简单,不需要动态调整优先级。坏处是:可能造成不必要的优先级提升。比如日志任务只是偶尔写一下I2C,但每次都得顶着50的优先级跑,有点「大炮打蚊子」的感觉。
注意:优先级天花板协议在静态配置的系统中特别好用。飞控里很多资源访问模式是固定的,用天花板协议反而比继承协议更可靠。我个人在PX4的uORB通信中就见过类似的设计思路。
4.4 实际项目中的优先级分配策略
好了,理论说完了,咱们聊聊实战。飞控系统里任务优先级怎么分?我一般按这个思路来:
- 安全关键任务放最高——比如看门狗喂狗、紧急降落、故障检测。这些任务优先级设在80-90(假设0最低,100最高)。
- 控制环路任务次之——姿态控制、位置控制、电机混控。优先级设在60-75。这些任务有硬实时要求,延迟超过1ms就可能炸机。
- 传感器采集任务再低一点——IMU读取、GPS解析、气压计采集。优先级设在40-55。虽然也重要,但可以容忍偶尔的抖动。
- 通信和日志任务放最低——数传电台、SD卡写入、状态显示。优先级设在10-30。这些任务延迟几百毫秒都没事。
你可能会问:为什么控制环路比传感器采集优先级还高?嗯,这里有个细节。传感器数据通常有DMA或FIFO缓冲,丢几帧数据还能插值补回来。但控制环路一旦被阻塞,输出PWM波形就会乱,电机直接抖起来。我吃过这个亏,所以现在都是控制任务优先。
一个实用的分配原则:任务的优先级 = 任务的「破坏力」× 任务的「时效性」。破坏力越大、时效性越强,优先级越高。飞控里控制任务的破坏力是100%,日志任务最多就是丢几条数据。
最后分享一个我踩过的坑。有一次做多旋翼飞控,我把电池电压监测任务的优先级设得比控制任务还高。结果电池电压一波动,监测任务疯狂抢占CPU,控制任务被饿死,飞机直接翻了个跟头。后来我把监测任务改成每100ms执行一次,优先级降到20,问题就解决了。
优先级设计没有银弹。每个系统都有自己的脾气,关键是多测试、多观察。用RTOS的跟踪工具看看任务的实际运行时间、阻塞时间、抢占次数,比纸上谈兵靠谱得多。