4. VxWorks任务间通信与同步

在飞行控制软件中,多个任务往往以不同周期并发运行:姿态解算任务可能每 5 ms 执行一次,导航滤波任务每 10 ms 执行一次,而遥测下传任务可能每 100 ms 才运行一次。这些任务之间必须交换数据、协调执行顺序、共享传感器与执行机构资源。VxWorks 提供了信号量、消息队列、管道、事件和信号等机制来完成这些工作,而选择哪一种机制,取决于数据量、实时性要求以及任务之间的耦合方式。

一、五种通信与同步机制的分工

信号量(Semaphore)是 VxWorks 中最基础的同步原语,分为二值信号量、计数信号量和互斥信号量。二值信号量常用于任务间的事件通知,例如中断服务程序(ISR)在采集到一帧 IMU 数据后释放信号量,姿态解算任务阻塞等待该信号量后立即读取数据。计数信号量适合管理多个相同资源,例如三个串口缓冲区。互斥信号量(Mutex)则专门用于保护临界区,它带有优先级继承机制,是解决优先级反转的关键工具。

消息队列(Message Queue)用于在任务之间传递带类型的数据块。VxWorks 的 msgQCreate() 创建队列时需指定最大消息数和每条消息的最大字节数。发送方使用 msgQSend(),接收方使用 msgQReceive(),两者都可以设置超时。消息队列适合传递结构化的飞行数据包,例如将传感器融合结果发送给控制律任务。

管道(Pipe)是 VxWorks 提供的虚拟 I/O 设备,通过 pipeDevCreate() 创建,使用 write() 和 read() 操作。管道本质上是字节流,适合传递不定长数据或与标准 I/O 兼容的场景,但在硬实时控制回路中不如消息队列直观。

事件(Event)机制允许一个任务等待多个事件中的任意一个或全部。VxWorks 的 eventReceive() 可以设置事件掩码和等待模式(AND/OR),适合需要同时响应多种触发条件的任务,例如飞控任务既等待“传感器就绪”事件,也等待“地面指令到达”事件。

信号(Signal)类似于 UNIX 信号,用于异步通知。VxWorks 支持 sigqueue() 携带少量数据,但信号处理函数运行在接收任务的上下文中,且信号本身不提供排队保证,因此在高可靠性飞控中通常只用于异常告警,而不用于常规数据传递。

VxWorks 飞控任务通信与同步机制全景 任务层 IMU采集任务 周期 5ms / 优先级 10 姿态解算任务 周期 5ms / 优先级 15 控制律任务 周期 5ms / 优先级 20 导航滤波任务 周期 10ms / 优先级 25 遥测下传任务 周期 100ms / 优先级 60 ISR 中断服务 IMU 数据就绪 通信与同步机制 二值信号量 semBCreate / semTake / semGive — 事件通知 互斥信号量(Mutex) semMCreate — 优先级继承,保护临界区 消息队列 msgQCreate / msgQSend / msgQReceive — 数据传递 管道 pipeDevCreate / write / read — 字节流 事件 eventSend / eventReceive — 多条件等待 信号 sigqueue / signal — 异步异常通知 共享资源 IMU 数据缓冲区 双缓冲 / 互斥保护 舵机 PWM 寄存器 互斥信号量保护 导航状态向量 消息队列传递 遥测数据包 管道 / 消息队列 地面指令队列 消息队列接收 故障告警标志 事件 / 信号 ⚠ 优先级反转风险:低优先级任务持有互斥量时被中优先级任务抢占

二、优先级反转与互斥量的必要性

优先级反转是实时系统中最隐蔽也最危险的问题之一。假设低优先级任务 L 先获得互斥量并进入临界区,此时高优先级任务 H 就绪并试图获取同一互斥量,H 被阻塞。如果此时中优先级任务 M 就绪,由于 M 的优先级高于 L,M 会抢占 L 的执行,导致 L 无法释放互斥量,H 也就无法继续运行。结果是高优先级的 H 被中优先级的 M 间接阻塞,系统响应时间变得不可预测。

VxWorks 的互斥信号量通过优先级继承解决这一问题:当 H 阻塞在 L 持有的互斥量上时,L 的优先级被临时提升到 H 的级别,使 M 无法抢占 L。L 释放互斥量后,优先级恢复原值,H 立即获得互斥量并继续执行。在飞控软件中,所有可能被多个不同优先级任务访问的共享数据结构,都应使用 semMCreate() 创建的互斥信号量保护,而不是用二值信号量。

三、飞行控制任务同步实战

下面以一个典型的飞控数据流为例,说明如何组合使用这些机制。系统包含 IMU 采集任务、姿态解算任务、控制律任务和遥测任务。IMU 采集任务通过 SPI 读取传感器数据,写入双缓冲区,然后释放一个二值信号量通知姿态解算任务。姿态解算任务等待信号量,读取缓冲区数据,完成姿态解算后,将姿态角写入共享结构体,并通过消息队列发送给控制律任务。控制律任务接收消息队列中的姿态数据,结合遥控指令计算舵面偏转量,写入 PWM 寄存器。遥测任务则通过另一个消息队列接收各任务的状态快照,打包后通过管道发送到串口。

/* 创建互斥信号量保护共享姿态数据 */
SEM_ID semAttitude = semMCreate(SEM_Q_PRIORITY | SEM_INVERSION_SAFE);

/* 创建消息队列:最多 10 条消息,每条 64 字节 */
MSG_Q_ID msgQCtrl = msgQCreate(10, 64, MSG_Q_PRIORITY);

/* 姿态解算任务:写入共享数据 */
semTake(semAttitude, WAIT_FOREVER);
attitude.roll  = roll;
attitude.pitch = pitch;
attitude.yaw   = yaw;
semGive(semAttitude);

/* 发送给控制律任务 */
ATTITUDE_MSG msg;
msg.roll = roll; msg.pitch = pitch; msg.yaw = yaw;
msgQSend(msgQCtrl, (char *)&msg, sizeof(msg), WAIT_FOREVER, MSG_PRI_NORMAL);

/* 控制律任务:接收姿态数据 */
ATTITUDE_MSG rxMsg;
if (msgQReceive(msgQCtrl, (char *)&rxMsg, sizeof(rxMsg), WAIT_FOREVER) != ERROR) {
    /* 计算控制律并输出 PWM */
}

在这个例子中,互斥信号量保护的是姿态结构体的读写一致性,消息队列负责任务间的数据传递,二值信号量负责 ISR 与任务之间的同步。三者各司其职,不能互相替代。如果只用消息队列传递姿态数据,那么姿态解算任务和控制律任务之间就存在数据拷贝开销;如果只用共享内存加互斥量,则控制律任务需要轮询或额外同步。实际工程中通常根据数据更新频率和实时性要求混合使用。

实践建议:在 VxWorks 中,互斥信号量只能在任务上下文中使用,不能在 ISR 中获取或释放。ISR 与任务之间的同步应使用二值信号量,且 ISR 中只能调用 semGive() 而不能调用 semTake()。消息队列的 msgQSend() 可以在 ISR 中使用,但必须使用 NO_WAIT 选项,否则会阻塞中断。

四、机制选择与参数配置要点

选择通信机制时,可以从以下几个维度判断:数据是“通知”还是“内容”?如果只是通知对方“有事发生”,用二值信号量或事件;如果要传递具体数据,用消息队列或管道。数据是否有固定长度和类型?有则用消息队列,无则用管道。是否需要等待多个条件?用事件。是否需要保护共享资源?用互斥信号量。是否需要跨网络或与主机通信?考虑使用 VxWorks 的 socket 或 RPC,而不是本地消息队列。

参数配置方面,消息队列的长度和消息大小需要根据最坏情况下的数据积压量确定。例如控制律任务每 5 ms 接收一次姿态数据,如果姿态解算任务偶尔延迟 20 ms,则队列中最多积压 4 条消息,队列长度至少设为 5 或 6。互斥信号量的选项应包含 SEM_INVERSION_SAFE 以启用优先级继承,同时使用 SEM_Q_PRIORITY 使等待队列按优先级排序。二值信号量的初始状态通常设为 SEM_EMPTY,由 ISR 或生产者任务释放。

常见陷阱:在中断服务程序中调用 msgQSend() 时若未使用 NO_WAIT,可能导致中断上下文阻塞,引发系统崩溃。同样,在互斥信号量保护的临界区内调用可能阻塞的函数(如 msgQReceive()),会显著延长临界区时间,增加高优先级任务的等待延迟。临界区应尽可能短,只包含必要的共享数据读写操作。

最后需要强调的是,VxWorks 的信号量、消息队列和事件都支持超时参数。在飞控任务中,等待超时不应设为 WAIT_FOREVER 而不加监控,否则一旦生产者任务异常,消费者任务将永久阻塞。合理的做法是设置一个略大于正常周期的超时值,例如 5 ms 周期的任务设置 20 ms 超时,超时后记录故障计数并进入安全处理流程。这样既保证了实时性,也提供了故障检测与恢复的能力。