3. 事件发送与接收API详解:eventSend()、eventReceive()、eventClear()

好,咱们直接进入正题。事件机制的核心,说白了就是三个API:发、收、清。我当年刚接触VxWorks时,觉得事件不就是个标志位嘛,有啥好学的?结果第一次做多任务同步就踩了坑——事件没清干净,任务一直等,系统直接卡死。嗯,从那以后我再也不敢小看这三个函数了。

3.1 eventSend() —— 事件的“发射器”

这个函数负责向指定任务发送事件。原型长这样:

STATUS eventSend
    (
    int taskId,          /* 目标任务的ID */
    UINT32 events        /* 要发送的事件掩码,按位设置 */
    )

参数其实就两个,但每个都有讲究。

taskId:目标任务的ID。你可能会问,能不能给自己发事件?当然可以。我有个习惯,在调试阶段经常用 taskIdSelf() 给自己发事件,用来模拟异步信号。

events:32位的事件掩码。每一位代表一个独立事件。比如 0x01 表示事件0,0x02 表示事件1,以此类推。你可以一次发多个事件,像这样:

eventSend(taskId, 0x01 | 0x04);  /* 同时发送事件0和事件2 */
小技巧:我个人习惯用宏定义来命名事件,比如 #define EVT_DATA_READY 0x01,这样代码可读性高很多。别直接写魔法数字,否则三个月后你自己都看不懂。

3.2 eventReceive() —— 事件的“接收器”

这个函数是事件机制的核心。任务调用它来等待特定事件。原型如下:

STATUS eventReceive
    (
    UINT32 eventMask,        /* 要等待的事件掩码 */
    UINT8  options,          /* 等待选项 */
    int    timeout,          /* 超时时间(tick数) */
    UINT32 *eventsReceived   /* 输出参数:实际收到的事件 */
    )

参数有点多,咱们一个一个拆。

eventMask:你想等哪些事件?比如你只关心事件0和事件1,那就传 0x03

options:这个参数决定了等待逻辑。常用的有:

选项 含义 说明
EVENTS_WAIT_ANY 等待任意一个事件 只要mask中任何一个事件发生,就返回
EVENTS_WAIT_ALL 等待所有事件 mask中所有事件都发生,才返回
EVENTS_RETURN_ALL 返回所有已挂起事件 常与上面两个组合使用

举个例子:

UINT32 received;
/* 等待事件0或事件1,超时100个tick */
eventReceive(0x03, EVENTS_WAIT_ANY, 100, &received);

/* 等待事件0和事件1都发生,永不超时 */
eventReceive(0x03, EVENTS_WAIT_ALL, WAIT_FOREVER, &received);
注意:我曾经犯过一个低级错误——在中断服务程序里调用 eventReceive()。这是绝对不允许的!eventReceive() 会导致任务阻塞,而ISR里不能阻塞。正确的做法是在ISR里用 eventSend() 通知任务,任务再去调用 eventReceive()。

timeout:超时时间,单位是系统tick。传 WAIT_FOREVER(即-1)表示一直等,直到事件到来。传0表示不等待,立即返回。我建议你在正式产品中尽量设置超时,别用 WAIT_FOREVER,否则一旦事件没来,任务就永远卡死了。

eventsReceived:输出参数,返回实际收到的事件掩码。这个很有用,你可以根据它判断是哪个事件触发了返回。

3.3 eventClear() —— 事件的“清理工”

这个函数用来清除任务中已挂起的事件。原型:

STATUS eventClear
    (
    int    taskId,     /* 目标任务ID */
    UINT32 eventMask   /* 要清除的事件掩码 */
    )

你可能会问:什么时候需要手动清事件?

嗯,这里有个关键点:eventReceive() 默认不会自动清除事件。如果你用 EVENTS_WAIT_ANY 收到了事件0,但事件1还在挂起状态。下次再调用 eventReceive() 时,事件1可能还在。这时候就需要手动清一下。

我举个例子:

/* 清除任务中所有事件 */
eventClear(taskIdSelf(), 0xFFFFFFFF);

/* 只清除事件0和事件1 */
eventClear(taskIdSelf(), 0x03);
避坑指南:我曾经在一个项目中,任务A发送了事件0和事件1,任务B用 EVENTS_WAIT_ALL 等待这两个事件。但任务B收到后没有清事件,第二次调用时事件还在,结果直接返回了——因为事件早就满足了。嗯,从那以后我每次收到事件后都会主动清一下,或者用 EVENTS_RETURN_ALL 配合使用。

3.4 三个API的配合使用

实际项目中,这三个函数通常是配合使用的。我给你画个典型流程:

  1. 任务A调用 eventReceive() 等待事件
  2. 任务B或ISR调用 eventSend() 发送事件
  3. 任务A被唤醒,处理事件
  4. 任务A调用 eventClear() 清理已处理的事件

代码示例:

/* 任务A:等待数据就绪事件 */
void taskA(void)
{
    UINT32 events;
    while(1)
    {
        /* 等待事件0或事件1 */
        eventReceive(0x03, EVENTS_WAIT_ANY, WAIT_FOREVER, &events);
        
        if(events & 0x01)
        {
            /* 处理事件0 */
            printf("Event 0 received\n");
        }
        if(events & 0x02)
        {
            /* 处理事件1 */
            printf("Event 1 received\n");
        }
        
        /* 清理已处理的事件 */
        eventClear(taskIdSelf(), events);
    }
}

/* 任务B:发送事件 */
void taskB(void)
{
    /* 模拟数据就绪 */
    eventSend(taskIdA, 0x01);
    taskDelay(100);
    eventSend(taskIdA, 0x02);
}
个人经验:我建议你在 eventReceive() 返回后,先检查 eventsReceived 的值,再决定怎么处理。别假设一定是你要的那个事件。尤其是在多任务环境下,可能有多个任务同时发事件,你收到的可能是个“组合包”。

好了,这三个API的用法就讲到这里。说白了,eventSend() 是发信号,eventReceive() 是等信号,eventClear() 是清信号。记住这个逻辑,你就能用好VxWorks的事件机制了。