4. VxWorks任务间通信与同步

在VxWorks中,任务是最基本的调度单位,但一个工业控制设备往往需要多个任务协同工作:采集任务读取ADC或现场总线数据,控制任务执行PID运算,执行任务驱动继电器或伺服,通信任务负责上传数据。这些任务之间必须交换数据并协调时序,否则会出现数据覆盖、控制滞后或执行抖动。VxWorks提供了五类核心机制:信号量、消息队列、管道、事件寄存器和共享内存。理解它们的语义差异,比记住API更重要。

五类机制的定位与选择

信号量用于互斥和同步,不传递数据。二值信号量保护共享资源,计数信号量用于任务间的事件通知。工业控制中,采集任务完成一次采样后释放一个计数信号量,控制任务等待该信号量后再读取数据,这是最典型的“生产者-消费者”同步。

消息队列传递离散消息,每条消息有固定长度,支持优先级排队。适合传递命令、报警、状态帧等结构化数据。例如执行任务向控制任务发送“阀门开度设定值”消息,消息体包含阀门编号和开度百分比。

管道是字节流通信,类似文件读写,适合不定长数据。在VxWorks中管道由pipeDevCreate()创建,任务通过read()/write()操作。它比消息队列更灵活,但需要自行处理消息边界。

事件寄存器用于任务等待多个事件中的任意一个或全部。每个任务有32位事件寄存器,通过eventReceive()等待事件组合。工业控制中,一个任务可能需要同时等待“采集完成”和“通信就绪”两个事件,事件寄存器比多个信号量更高效。

共享内存是最高效的数据交换方式,多个任务直接读写同一块内存。但必须配合信号量或事件寄存器实现互斥和同步,否则会出现读写竞争。共享内存适合传递大块数据,如波形缓冲区、图像帧或批量采集数据。

VxWorks任务间通信与同步机制全景 采集任务 tAcq / 优先级 120 周期 10ms 读取ADC 控制任务 tCtrl / 优先级 110 PID运算 输出控制量 执行任务 tExec / 优先级 130 驱动继电器/伺服 共享内存区 g_adcBuf[64](双缓冲区,每区32点) 采集任务写入 · 控制任务读取 · 由二值信号量 semBuf 保护 计数信号量 semData 采集完成 → 释放 控制任务 → 等待 消息队列 cmdQ 控制任务 → 发送 执行任务 → 接收 事件寄存器 evtGrp EVT_ACQ_DONE (bit0) EVT_CMD_READY (bit1) 管道 日志/调试 字节流 写入 读取 发送命令 接收命令 释放 等待 事件通知 同步时序: ① 采集任务每10ms写入共享内存并释放semData → ② 控制任务等待semData后读取数据、执行PID → ③ 控制任务将控制量写入cmdQ并置位EVT_CMD_READY → ④ 执行任务等待事件后从cmdQ取命令驱动输出 注:semBuf用于保护共享内存读写互斥;事件寄存器用于多条件等待,避免执行任务轮询。

工业控制同步案例:采集与执行任务的闭环

假设一个温度控制设备,采集任务tAcq每10ms读取一次热电偶ADC值,控制任务tCtrl执行PID运算,执行任务tExec驱动加热继电器。三者优先级分别为120、110、130(数值越小优先级越高)。

采集任务完成一次采样后,将数据写入共享内存g_adcBuf的双缓冲区,然后释放计数信号量semData。控制任务阻塞在semTake(semData, WAIT_FOREVER),被唤醒后读取共享内存,执行PID计算,得到控制量u(k),再通过消息队列cmdQ发送给执行任务。执行任务阻塞在msgQReceive(cmdQ, ...),收到消息后驱动继电器。

这里有一个关键细节:共享内存的读写必须互斥。如果采集任务正在写入缓冲区A,控制任务同时读取缓冲区A,就会读到半新半旧的数据。解决方案是使用二值信号量semBuf保护共享内存,或者采用双缓冲区加“写指针切换”的方式,让采集任务写A区时控制任务读B区,下一次交换。双缓冲区配合信号量,既保证了数据一致性,又减少了任务阻塞时间。

/* 共享内存与同步对象定义 */
#define BUF_SIZE   32
float g_adcBuf[2][BUF_SIZE];
int   g_wrIdx = 0;          /* 当前写入缓冲区索引 */
SEM_ID semData;             /* 计数信号量:采集完成通知 */
SEM_ID semBuf;              /* 二值信号量:共享内存互斥 */
MSG_Q_ID cmdQ;              /* 消息队列:控制量下发 */

/* 采集任务 */
void tAcq(void)
{
    int idx;
    while (1) {
        idx = g_wrIdx;
        semTake(semBuf, WAIT_FOREVER);
        for (int i = 0; i < BUF_SIZE; i++) {
            g_adcBuf[idx][i] = read_adc();
        }
        semGive(semBuf);
        g_wrIdx = 1 - idx;          /* 切换缓冲区 */
        semGive(semData);            /* 通知控制任务 */
        taskDelay(1);                /* 10ms周期 */
    }
}

/* 控制任务 */
void tCtrl(void)
{
    float setpoint = 50.0f, u;
    while (1) {
        semTake(semData, WAIT_FOREVER);
        semTake(semBuf, WAIT_FOREVER);
        float pv = g_adcBuf[1 - g_wrIdx][BUF_SIZE - 1];
        semGive(semBuf);
        u = pid_calc(setpoint, pv);  /* PID运算 */
        msgQSend(cmdQ, (char *)&u, sizeof(u), WAIT_FOREVER, MSG_PRI_NORMAL);
    }
}

/* 执行任务 */
void tExec(void)
{
    float u;
    while (1) {
        if (msgQReceive(cmdQ, (char *)&u, sizeof(u), WAIT_FOREVER) != ERROR) {
            set_heater(u);           /* 驱动继电器 */
        }
    }
}

上述代码中,semData的初始计数为0,采集任务每次释放后计数加1,控制任务取走后减1。如果控制任务处理较慢,信号量计数会累积,但不会丢失通知。实际工程中通常将semData初始计数设为0,并在系统启动时先启动采集任务,再启动控制任务,避免控制任务首次等待超时。

事件寄存器与多条件等待

如果执行任务需要同时等待“控制量就绪”和“使能信号有效”两个条件,使用两个信号量会导致任务需要两次semTake,代码复杂且容易死锁。事件寄存器可以一次性等待多个事件:

#define EVT_CMD_READY   0x01
#define EVT_ENABLE      0x02

void tExec(void)
{
    UINT32 events;
    while (1) {
        events = eventReceive(EVT_CMD_READY | EVT_ENABLE,
                              EVT_ANY, WAIT_FOREVER, NULL);
        if (events & EVT_CMD_READY) {
            /* 取命令并执行 */
        }
        if (events & EVT_ENABLE) {
            /* 使能输出 */
        }
    }
}

eventReceive的第二个参数EVT_ANY表示任意一个事件到达即返回,EVT_ALL表示必须全部到达。工业控制中,安全联锁通常使用EVT_ALL,确保所有条件满足才动作;而报警处理使用EVT_ANY,任一报警触发即响应。

管道与共享内存的适用边界

管道适合传输不定长、流式的数据,例如将调试日志从多个任务汇聚到一个日志任务。管道是字节流,没有消息边界,因此需要在数据中自行加入长度字段或分隔符。共享内存适合大块、高频的数据交换,例如每1ms采集256点波形数据,用消息队列复制数据开销太大,共享内存加信号量是唯一合理选择。

选择建议:互斥保护用二值信号量;事件通知用计数信号量;结构化命令用消息队列;多条件等待用事件寄存器;大块高频数据用共享内存+信号量;流式日志用管道。不要用消息队列传递大数组,也不要用共享内存传递小命令。
常见陷阱:在中断服务程序中只能使用semGive、msgQSend等非阻塞API,不能使用semTake或msgQReceive。共享内存的互斥信号量必须在所有访问路径上成对出现,遗漏一次就会导致数据竞争。事件寄存器的32位中,高8位被系统保留,用户任务只能使用低24位。

综合来看,VxWorks的五种通信机制各有明确边界。工业控制中,采集与执行任务之间的同步通常采用“共享内存+计数信号量+消息队列”的组合:共享内存传递批量数据,计数信号量实现周期同步,消息队列传递控制命令。理解每种机制的语义和开销,才能设计出确定性强、抖动小的实时控制软件。