Skip to content

ChibiOS 事件系统

liuzikai edited this page Jan 17, 2022 · 4 revisions

本文介绍 ChibiOS 事件系统(Event)。

前置阅读:ChibiOS 线程编写

什么是事件 Event

当一个线程需要等待某些触发条件时,一种实现方式是主动查询,如果条件没有被触发,则过一段时间后再次查询。这种模式有两个缺点:一是浪费 CPU 资源,操作系统必须周期性唤醒线程,运行线程代码;二是响应时间慢,例如,查询间隔为 10ms,则条件触发后,可能 10ms (甚至更长)之后,线程被唤醒,才开始响应。如果降低查询间隔,则更多的 CPU 资源被浪费在无效的查询上。

一个不太适用于主动查询的触发条件的例子,是用户按下按键。用户按下按键并不是一个高频发生的事件(相对于开发板 120MHz 来说),但对响应速度却有一定要求(按下按键后最好能尽快响应)。

对于这一类触发条件,事件(Event)则较为适用。ChibiOS 实现了事件子系统,简单来说,一个线程可以将自己放入暂停状态,要求某个事件发生时,操作系统将自己唤醒。而事件发生的广播,可以在其他线程或者是 callback 中发出。这样,在事件发生之前,操作系统就不必运行此线程,而事件发生后也可以第一时间唤醒线程。

ChibiOS 对于事件系统的介绍:ChibiOS free embedded RTOS - RT Events,其中有两个概念值得进一步的澄清:

  • Event Flags:eventflags_t 类型(实际 uint32_t),事件携带的数据,可用于传递额外信息。
  • Event Mask:eventmask_t 类型(实际 uint32_t),在 event listener 中使用。当一个 listener 注册了多个事件时,用于判断哪些事件触发,和 event source 或者其他 listener 无关。

定义事件源 Event Source

我们也继续沿用 class static variable 的风格(详见程序架构演进史)。在头文件中声明一个 event_source_t:

class Vision {
public:
    static event_source_t expected_shoot_time_updated_event;
}

在 cpp 文件中实际定义并初始化:

EVENTSOURCE_DECL(Vision::expected_shoot_time_updated_event);

广播事件 Broadcast Event

在需要广播事件,告知所有接受者的地方。如果需要传递 flags,使用 chEvtBroadcastFlags/chEvtBroadcastFlagsI,前者适用于无锁普通线程,后者适用于 I-Locked State(详见ChibiOS 关键区与锁)。

chSysLockFromISR();  /// --- ENTER I-Locked state. DO NOT use LOG, printf, non I-Class functions or return ---
{
    // ...
    chEvtBroadcastFlagsI(&expected_shoot_time_updated_event, (uint32_t) expected_shoot_time);
}
chSysUnlockFromISR();  /// --- EXIT I-Locked state ---

expected_shoot_time 作为需要传递的信息,以 flags 的形式传递。

如果不需要传递 flags,使用 chEvtBroadcast/chEvtBroadcastI,它们等同于 flags 为 0。

事件接收者 Event Listener

在需要接收事件的线程中,定义一个 event_listener_t 和 eventmask_t。关于线程的编写方式,参考 ChibiOS 线程编写

class VisionShootingThread : public chibios_rt::BaseStaticThread<256> {

    event_listener_t expected_shoot_time_updated_listener;
    static constexpr eventmask_t EXPECTED_SHOOT_TIME_UPDATED_EVENT_MASK = (1U << 0U);

    void main() final;
};

static VisionShootingThread vision_shooting_thread;

Event mask 需要使用 one-hot 定义(有且只有一个 bit 是 1,OR 起来不重叠,也可使用 macro EVENT_MASK(0) 这样定义)。

在 cpp 文件中,以以下基本结构定义线程主体:

ShootLG::VisionShootingThread ShootLG::vision_shooting_thread;

void ShootLG::VisionShootingThread::main() {
    setName("ShootLG_Vision");

    chEvtRegisterMask(&Vision::expected_shoot_time_updated_event, &expected_shoot_time_updated_listener EXPECTED_SHOOT_TIME_UPDATED_EVENT_MASK);

    while (!shouldTerminate()) {

        chEvtWaitAny(EXPECTED_SHOOT_TIME_UPDATED_EVENT_MASK);
        eventflags_t flags = chEvtGetAndClearFlags(&expected_shoot_time_updated_listener);

        // Do something
}

chEvtRegisterMaskexpected_shoot_time_updated_event 注册给 expected_shoot_time_updated_listener,并设置其 event mask 为 EXPECTED_SHOOT_TIME_UPDATED_EVENT_MASK

chEvtWaitAny 用于等待该 event 的发生,chEvtGetAndClearFlags 用于获取 event flag。

在这里,event mask 只是给当前 listener 使用的,和 event source 或者其他 listener 无关。当一个 listener 需要等待多个 event 时,mask 就能发挥作用了。

Listener 监听多个 Event

void UserI::UserActionThread::main() {
    setName("UserI_Action");

    chEvtRegisterMask(&Remote::mouse_press_event, &mouse_press_listener, MOUSE_PRESS_EVENT);
    chEvtRegisterMask(&Remote::key_press_event, &key_press_listener, KEY_PRESS_EVENT_MASK);

    while (!shouldTerminate()) {

        eventmask_t events = chEvtWaitOne(MOUSE_PRESS_EVENT_MASK | KEY_PRESS_EVENT_MASK);

        if (events & MOUSE_PRESS_EVENT_MASK) {
            // Do something
        } 

        if (events & KEY_PRESS_EVENT_MASK) {
            // Do something
        }

        // ...
    }
}

chEvtWaitAny 等待 OR 起来的其中一个事件发生,如果有多个事件“同时”发生(对于这个线程来说,在暂停期间,ChibiOS 调度到这个线程前发生的,均是“同时”),则一起返回(events 是 ORed event masks)。

chEvtWaitOne 等待其中一个事件发生,如果有多个事件“同时”发生,返回其中的一个,下一次调用这个函数时返回下一个(events 只有一个 bit 是 1),可用于逐个处理 event。

类似的还有 chEvtWaitAll、带 timeout 的版本等。

Event Flags 叠加

值得注意的是,event flags 会以 OR 的方式叠加,直到被 listener 清除(chEvtGetAndClearFlags)。

例如,当一个 listener 监听多个 event,且多于一个 event broadcast 时带 flags,则多个 event 同时发生时,flags OR 叠加。

类似的,如果一个 event 在被 listener 处理前又发生了一次,flags OR 叠加。

这个特性可被用于一些特殊场合,例如,两个按键先后按下,每个按键以 one-hot flag 表示,则两个 flag OR 叠加,得到的 flags 中就有两个 1 bits,可以一并被处理。

在使用 flags 传递信息时,需要留意 OR 叠加的效果。Listener 监听多个 event 时,也需要注意不同 event 的 flags OR 叠加的效果。

更新历史

  • 2021.07.12 初始版本。liuzikai
Clone this wiki locally