4、任务调度与管理:任务优先级划分、时间片轮转、抢占式调度、任务间通信
各位同学,咱们今天聊点实在的。飞控系统里,任务调度这块要是没整明白,飞机在天上分分钟给你脸色看。我做了这么多年飞控,见过太多因为调度问题炸机的案例了。说白了,调度就是决定「谁先跑、谁后跑、跑多久」的问题。
4.1 任务优先级划分——给任务排个座次
飞控里的任务,不是平起平坐的。有些任务晚1毫秒都不行,有些任务晚个几十毫秒也没事。所以,我们必须给任务排个优先级。
我个人习惯把飞控任务分成这么几档:
| 优先级等级 | 典型任务 | 响应要求 |
|---|---|---|
| 最高(0-1) | IMU数据读取、紧急保护 | <1ms |
| 高(2-3) | 姿态控制、位置控制 | 1-5ms |
| 中(4-5) | 导航解算、传感器融合 | 5-20ms |
| 低(6-7) | 日志记录、遥测发送 | 20-100ms |
嗯,这里要注意:优先级不是越高越好。你把日志任务也设成最高优先级,那IMU数据反而可能被堵住。我在项目中遇到过有人把所有任务都设成同一个优先级,结果系统直接变成了「谁抢到算谁的」,乱成一锅粥。
4.2 时间片轮转——大家轮流用CPU
同优先级的任务怎么办?总不能一直让一个任务霸占CPU吧?这时候就需要时间片轮转了。
时间片轮转,说白了就是给每个任务分一个时间片。比如每个任务跑5ms,时间到了就换下一个。你想想看,如果三个同优先级的任务,每个跑5ms,那大家都能得到执行机会。
我一般这么配置时间片:
// 时间片配置示例
#define TIME_SLICE_MS 5 // 每个任务的时间片
// 任务创建时指定时间片
osThreadDef(attitude_control, AttitudeControlTask, osPriorityHigh, 0, 512);
osThreadDef(navigation, NavigationTask, osPriorityNormal, 0, 512);
// 启动调度器
osKernelStart();
但要注意,时间片不能设得太小。太小了,任务切换的开销反而比任务执行本身还大。我建议时间片至少是任务切换时间的10倍以上。
4.3 抢占式调度——高优先级说了算
这才是飞控调度的核心。抢占式调度,就是高优先级任务可以随时打断低优先级任务。你想想看,如果飞机突然检测到姿态异常,难道还要等日志任务写完才能处理?当然不行。
抢占式调度的核心逻辑:
- 低优先级任务正在运行
- 高优先级任务就绪(比如定时器触发了)
- 调度器立即暂停低优先级任务
- 高优先级任务开始执行
- 高优先级任务完成后,低优先级任务继续
我记得有一次调试,发现姿态控制偶尔会延迟几毫秒。查了半天,原来是一个中断服务程序里调用了延时函数,把调度器给阻塞了。从那以后,我定了个规矩:中断服务程序里绝对不调用任何阻塞函数。
4.4 任务间通信——让任务说上话
任务之间总得交换数据吧?比如IMU任务读到了数据,姿态控制任务要用。怎么传?有三种常用方式:队列、信号量、事件标志组。
4.4.1 队列——数据快递员
队列就是用来传递数据的。一个任务往里放,另一个任务往外取。先进先出,不会乱。
// 创建队列
osMessageQDef(imu_queue, 10, sizeof(IMU_Data_t));
osMessageQId imu_queue_id = osMessageCreate(osMessageQ(imu_queue), NULL);
// 发送任务(IMU读取)
void IMU_Task(void const * argument) {
IMU_Data_t imu_data;
while(1) {
ReadIMU(&imu_data);
osMessagePut(imu_queue_id, (uint32_t)&imu_data, 0);
osDelay(1); // 1ms读取一次
}
}
// 接收任务(姿态控制)
void Attitude_Task(void const * argument) {
osEvent event;
IMU_Data_t *imu_data;
while(1) {
event = osMessageGet(imu_queue_id, osWaitForever);
if(event.status == osEventMessage) {
imu_data = (IMU_Data_t*)event.value.p;
// 处理IMU数据
UpdateAttitude(imu_data);
}
}
}
我个人习惯用队列传递传感器数据。为什么?因为队列自带缓冲,就算接收任务偶尔被高优先级任务抢占了,数据也不会丢。我曾经用全局变量传数据,结果数据被覆盖了,姿态解算直接飞了。
4.4.2 信号量——任务间的红绿灯
信号量不传数据,只传「信号」。比如告诉另一个任务:「我准备好了,你可以开始了」。
信号量有两种:
- 二值信号量:只有0和1,像一把钥匙。谁拿到谁执行。
- 计数信号量:可以计数,比如有3个资源,最多允许3个任务同时访问。
// 创建二值信号量
osSemaphoreDef(imu_ready_sem);
osSemaphoreId imu_ready_sem_id = osSemaphoreCreate(osSemaphore(imu_ready_sem), 0);
// IMU任务:读取完成后释放信号量
void IMU_Task(void const * argument) {
while(1) {
ReadIMU();
osSemaphoreRelease(imu_ready_sem_id); // 告诉姿态控制:数据准备好了
osDelay(1);
}
}
// 姿态控制任务:等待信号量
void Attitude_Task(void const * argument) {
while(1) {
osSemaphoreWait(imu_ready_sem_id, osWaitForever); // 等IMU数据
// 处理数据
UpdateAttitude();
}
}
4.4.3 事件标志组——多条件触发
有时候,一个任务要等好几个条件都满足了才能执行。比如姿态控制任务,要等IMU数据到了、GPS数据到了、气压计数据到了,三个条件都满足才能开始算。这时候事件标志组就派上用场了。
// 定义事件标志
#define EVENT_IMU_READY 0x0001
#define EVENT_GPS_READY 0x0002
#define EVENT_BARO_READY 0x0004
// 创建事件标志组
osEventFlagsDef(ctrl_event_flags);
osEventFlagsId_t ctrl_event_id = osEventFlagsNew(NULL);
// 各个传感器任务设置事件
void IMU_Task(void const * argument) {
while(1) {
ReadIMU();
osEventFlagsSet(ctrl_event_id, EVENT_IMU_READY);
osDelay(1);
}
}
void GPS_Task(void const * argument) {
while(1) {
ReadGPS();
osEventFlagsSet(ctrl_event_id, EVENT_GPS_READY);
osDelay(10); // GPS更新慢
}
}
// 控制任务等待所有事件
void Control_Task(void const * argument) {
uint32_t flags;
while(1) {
// 等待三个事件都发生
flags = osEventFlagsWait(ctrl_event_id,
EVENT_IMU_READY | EVENT_GPS_READY | EVENT_BARO_READY,
osFlagsWaitAll, osWaitForever);
if(flags & (EVENT_IMU_READY | EVENT_GPS_READY | EVENT_BARO_READY)) {
// 所有数据都到了,开始控制计算
CalculateControl();
}
}
}
事件标志组的好处是灵活。你可以设置「与」条件(所有事件都发生)或者「或」条件(任意一个事件发生)。我在多传感器融合的项目里特别喜欢用这个,因为不同传感器的更新频率不一样,用事件标志组可以优雅地同步它们。
4.5 实际项目中的调度策略
说了这么多理论,咱们看看实际飞控里怎么用。以我常用的PX4架构为例:
// 实际飞控任务配置
// 最高优先级:传感器数据采集
osThreadDef(sensor_hub, SensorHubTask, osPriorityRealtime, 0, 1024);
// 高优先级:姿态与位置控制
osThreadDef(attitude_controller, AttitudeControllerTask, osPriorityHigh, 0, 2048);
osThreadDef(position_controller, PositionControllerTask, osPriorityHigh, 0, 2048);
// 中优先级:导航与状态估计
osThreadDef(ekf_estimator, EKFEstimatorTask, osPriorityNormal, 0, 4096);
osThreadDef(navigator, NavigatorTask, osPriorityNormal, 0, 2048);
// 低优先级:通信与日志
osThreadDef(mavlink_sender, MavlinkSenderTask, osPriorityLow, 0, 2048);
osThreadDef(logger, LoggerTask, osPriorityLow, 0, 1024);
这个配置我用了好几年,基本没出过问题。核心思路就是:数据采集 > 控制计算 > 状态估计 > 通信日志。优先级从高到低,层层递进。
最后说一句:调度策略没有银弹。不同的飞控硬件、不同的应用场景,调度策略都不一样。我建议你从最简单的开始,先跑起来,再用示波器或者逻辑分析仪看任务的实际执行时间,慢慢调优。别一上来就想搞个完美的调度器——那玩意儿不存在。