4. 车载控制任务设计与多任务编程

车载控制器与通用嵌入式设备最大的区别在于:它必须同时处理多种时间尺度、多种触发来源的控制逻辑。例如,电机转矩闭环需要每 1 ms 稳定执行一次,CAN 报文接收是随机到达的事件,故障诊断可能每 100 ms 轮询一次,而整车状态上报则是 500 ms 的慢周期。如果把这些逻辑全部塞进一个超级循环,任何一次阻塞都会破坏控制周期。VxWorks 作为实时操作系统,提供的正是把不同时间约束拆解为独立任务、再通过通信与优先级机制协调它们的能力。

任务创建:从控制需求到任务实体

在 VxWorks 中,任务由 taskSpawn() 创建,其原型为:

int taskSpawn
(
    char  *name,        /* 任务名,便于调试识别 */
    int    priority,    /* 优先级,0 最高,255 最低 */
    int    options,     /* 选项,如 VX_FP_TASK 支持浮点 */
    int    stackSize,   /* 栈大小,字节 */
    FUNCPTR entryPt,    /* 入口函数 */
    int    arg1..arg10  /* 最多 10 个整型参数 */
);

车载控制任务设计的第一步,是把控制需求映射为任务清单。以某电机控制器为例,可拆分为:1 ms 电流环任务、10 ms 速度环任务、事件驱动的 CAN 接收任务、100 ms 故障诊断任务、500 ms 状态上报任务。每个任务需要明确周期、最坏执行时间、优先级、栈需求和通信对象。栈大小不能凭感觉给,1 ms 任务若调用浮点运算和 CAN 发送,建议至少 8 KB;仅做整数比较的诊断任务 4 KB 通常足够。创建时若返回 ERROR,常见原因是栈不足或优先级非法,应通过 taskIdVerify() 确认任务是否真正进入就绪态。

任务间通信:共享数据与同步的取舍

VxWorks 提供多种通信机制,车载场景中最常用的是信号量、消息队列和共享内存配合信号量。选择依据是数据形态与实时性要求:

  • 二进制信号量:用于任务同步或中断与任务之间的通知,例如 CAN 中断服务程序释放信号量,接收任务获取后解析报文。
  • 互斥信号量:用于保护共享数据结构,支持优先级继承,能缓解优先级反转。车载中多个任务读写同一份电机状态结构体时必须使用。
  • 消息队列:用于传递变长或带边界的数据块,例如把故障码从诊断任务发送给上报任务。队列满时的阻塞策略要明确,控制路径上通常选择 NO_WAIT 并记录丢包计数。
  • 共享内存 + 信号量:适合高频、大数据量的状态交换,例如 1 ms 任务写入电流采样值,10 ms 任务读取。写入方持互斥信号量更新,读取方持信号量拷贝快照,避免读到半更新数据。
中断服务程序中不能调用可能阻塞的 API。ISR 中应只做 semGive() 或 msgQSend(..., NO_WAIT),把耗时解析放到任务上下文完成。

优先级分配策略

VxWorks 采用基于优先级的抢占式调度,同优先级任务按时间片轮转。车载控制任务的优先级分配应遵循“截止期越短、优先级越高”的速率单调原则,同时考虑阻塞时间。一个可参考的分配如下:

任务周期/触发建议优先级理由
电流环1 ms 周期50截止期最短,必须抢占其他任务
CAN 接收事件触发60报文到达需及时取走,避免硬件溢出
速度环10 ms 周期80中等实时性
故障诊断100 ms 周期120可容忍一定延迟
状态上报500 ms 周期150非实时关键

优先级数字越小越高。分配后必须做可调度性检查:所有任务的最坏执行时间之和,在考虑阻塞和中断开销后,不能超过对应周期的可用裕量。若电流环最坏执行 200 μs、速度环 300 μs、诊断 500 μs,则 1 ms 窗口内电流环自身 200 μs,加上被高优先级中断和信号量阻塞的余量,仍应保留至少 30% 空闲,否则一旦负载波动就会丢周期。

周期任务与事件触发任务的实现

周期任务的标准写法是“延时—执行”循环,但必须使用 taskDelay() 而非忙等。关键细节是:taskDelay() 的参数是系统时钟节拍数,若系统节拍为 1000 Hz,则 1 个节拍为 1 ms。对于 1 ms 电流环,若节拍精度不足,应改用辅助时钟或硬件定时器中断触发。下面是一个 10 ms 速度环任务的骨架:

void speedLoopTask(void)
{
    while (1)
    {
        semTake(gSpeedDataMutex, WAIT_FOREVER);
        /* 读取转速、计算 PI 输出 */
        semGive(gSpeedDataMutex);

        /* 10 个节拍 = 10 ms(节拍 1000 Hz) */
        taskDelay(10);
    }
}

事件触发任务则通常阻塞在信号量或消息队列上,收到事件后立即处理。CAN 接收任务的典型结构是:ISR 收到报文后 semGive(gCanRxSem),任务 semTake(gCanRxSem, WAIT_FOREVER) 后从驱动缓冲区读取并解析。这里要注意“信号量计数”与“实际报文数”的匹配:若一次中断可能对应多帧,应使用计数信号量或消息队列,避免漏处理。

车载控制多任务结构与通信关系 0 ms 1 ms 10 ms 100 ms 500 ms 电流环任务 周期 1 ms · 优先级 50 CAN 接收任务 事件触发 · 优先级 60 速度环任务 周期 10 ms · 优先级 80 故障诊断任务 周期 100 ms · 优先级 120 状态上报任务 周期 500 ms · 优先级 150 共享数据区 电机状态结构体(互斥信号量保护) CAN 报文缓冲区(计数信号量) 故障码消息队列 CAN 中断 ISR semGive 通知任务 信号量同步 调度原则: 截止期越短 → 优先级越高;高优先级任务可抢占低优先级任务;共享数据必须用互斥信号量保护,中断只做通知不做耗时处理。

周期任务与事件触发任务混用时,最容易被忽视的是“优先级反转”。假设速度环任务持有互斥信号量访问电机状态,此时低优先级诊断任务也申请同一信号量而被阻塞,若诊断任务恰好持有另一个被速度环需要的资源,就可能出现高优先级任务等待低优先级任务的情况。VxWorks 的互斥信号量支持优先级继承,能缓解这一问题,但前提是共享资源确实用互斥信号量而非二进制信号量保护。另一个实践要点是周期任务的相位错开:如果 1 ms、10 ms、100 ms 任务同时从 0 时刻启动,会在某些节拍上叠加执行,造成瞬时负载尖峰。可以在任务启动时加入不同的初始 taskDelay(),例如速度环先延时 3 个节拍、诊断任务先延时 17 个节拍,把执行点分散开。

调试多任务系统时,可用 taskShow() 或 WindShell 的 i 命令查看各任务状态、优先级和栈使用峰值。若某任务长期处于 PEND 状态,优先检查信号量是否被遗漏释放;若栈使用接近上限,应增大栈或减少局部大数组。

最后,任务设计要形成闭环验证:用示波器或 GPIO 翻转测量 1 ms 任务的实际抖动,用 taskDelay 的返回值判断是否被异常唤醒,用消息队列的丢包计数判断事件处理是否跟得上。只有周期抖动、栈余量、通信丢包三项都在可接受范围内,多任务设计才算真正落地。