ゲーム開発部 (⸝⸝ >ヮ<) !

esp-idf 框架 - 事件循环系统入门 - 逐参数解析 (以 ESP32-c3 为例)

#Esp32 #嵌入式

如果已经了解什么是事件系统,仅作为 api 文档可以跳转到自定义事件循环

事件系统解决了什么问题?

如果你曾经是 Arduino 用户,一定对下面这种写法(伪代码)不陌生:

连接Wifi()
while(Wifi还没有被连接){
  等待1秒()
}
启动Web服务器()

由于 Wifi 连接时间的不确定性,我们不得不让程序阻塞下来对 Wifi 状态进行轮询。这种写法虽然同样实现了功能,但是当我们的程序逐渐复杂后,这种写法会导致代码 难以扩展许多个 管理不同功能组件的 Manager 相互轮询 ,层层耦合——就像是让 10 个课代表收全班学生的作业并在收齐后上交给老师,但学生不能自己提交,需要课代表一遍一遍的逐个询问, 系统复杂且效率低下

我们来梳理一下逻辑:我们想要实现的功能本质是 「当 Wifi 连接成功后启动 Web 服务器」 ,那么有没有一种办法让 Wifi 管理器通过更加 主动 的方式来 通知 Web 服务器呢?一种办法是直接在 Wifi 管理器的代码中 连接成功 的位置 加入调用 Web 服务器 启动的代码——但是这相当于在一个系统中加入了 不属于本系统的逻辑 代码(在 Wifi 管理器中加入了 Web 服务器的逻辑),在后续的维护中会因为 高耦合度 而更加痛苦—— 很难分析 每个系统的 api 在什么时候又 莫名其妙被调用 了…

那么,有没有一种让系统只需要关心自己的逻辑,不需要“逆向思维”考虑自己什么情况下被其他系统调用,也不需要来回轮询的方式呢?这就是 事件系统

sequenceDiagram participant Web服务器 participant Mqtt服务器 participant 事件循环系统 participant Wifi管理器 Web服务器->>事件循环系统: 注册「Wifi连接成功事件」→ 绑定Web服务器启动函数 事件循环系统-->>Web服务器: 注册成功(回调函数已关联) Mqtt服务器->>事件循环系统: 注册「Wifi连接成功事件」→ 绑定Mqtt服务器启动函数 事件循环系统-->>Mqtt服务器: 注册成功(回调函数已关联) Wifi管理器->>Wifi管理器: 执行初始化逻辑(完成Wifi连接准备) Wifi管理器->>事件循环系统: 发送「Wifi连接成功事件」 事件循环系统->>事件循环系统: 检测到事件触发,匹配关联的回调函数 事件循环系统->>Web服务器: 调用Web服务器启动函数 Web服务器-->>事件循环系统: Web服务器启动完成 事件循环系统->>Mqtt服务器: 调用Mqtt服务器启动函数 Mqtt服务器-->>事件循环系统: Mqtt服务器启动完成
注册(Wifi连接成功事件, Web服务器启动函数)
注册(Wifi连接成功事件, Mqtt服务器启动函数)

Wifi管理器{
  初始化(){
    发送(Wifi连接成功事件)
  }
}

通过上面的例子,我们应该能够理解事件系统的使用流程了:先将 事件处理函数 (比如 “启动 web 服务器” 和 “启动 mqtt 服务器” 都是 “wifi 连接成功事件” 的事件处理函数)注册 到 “wifi 连接成功事件” 上,然后让 wifi 管理器在连接成功时 发送 一条 “wifi 连接成功事件” ——这样不仅逻辑清晰(每个系统只需要考虑 自己在什么情况下要做什么 ,以及 要向外发送什么事件不需要考虑具体关联了哪些外部系统 ),也避免了相互轮询的阻塞和复杂程序。例如,只需要 wifi 系统发出 “wifi 连接成功事件” , “启动 web 服务器” 和 “启动 mqtt 服务器” 这两个函数就会自动被调用。


自定义事件循环

使用自定义事件循环包含以下流程:

下面,我将会通过一个“传感器数据更新时 输出传感器数据”的案例来演示事件循环系统。

初始化

这一步中,我们要专为“传感器事件”创建一个名为 sensor_loop 的事件循环。
在初始化之前,我们需要做出一些准备工作:创建一个esp_event_loop_args_t类型的事件循环配置结构体和esp_event_loop_handle_t类型的事件循环句柄。

  esp_event_loop_args_t sensor_loop_args = {
      .queue_size = 8
      .task_name = "sensor_loop",
      .task_priority = 5,
      .task_stack_size = 4096,
      .task_core_id = 0
  };
  esp_event_loop_handle_t sensor_loop_handle;

完成后,我们把他们的地址传入esp_event_loop_create()函数来创建事件循环任务。

  ESP_ERROR_CHECK(esp_event_loop_create(&sensor_loop_args, &sensor_loop_handle));

下面我们来分析这两个参数的作用:

第一个参数:esp_event_loop_args_t

1. .queue_size = 8
2. .task_name = "sensor_loop"
3. .task_priority = 5
4. .task_stack_size = 4096
5. .task_core_id = 0

第二个参数:esp_event_loop_handle_t


注册事件处理程序

这一步中,我们将会把“将传感器数据输出”的 事件处理函数 绑定到“传感器数据更新”这个事件上,从而触发“将传感器数据输出”这个事件处理函数。
在注册事件处理函数前,我们也需要进行一定的准备工作:分别是 事件基类、事件 ID、事件处理函数、回调参数、实例指针。例如,下面这段代码让 sensor_loop_handle 对应的事件循环,在收到 SENSOR_EVENT 基类下的 SENSOR_EVENT_TEMP_UPDATED 事件时,自动执行 sensor_event_handler 回调函数,并传入回调参数。(看不懂没关系,下面就解释)

  ESP_ERROR_CHECK(esp_event_handler_instance_register_with(
      sensor_loop_handle,                         // 自定义循环句柄
      SENSOR_EVENT,                               // 事件基类
      SENSOR_EVENT_ID::SENSOR_EVENT_TEMP_UPDATED, // 事件ID
      sensor_event_handler,                       // 回调函数
      &sensor_event_temp_handler_args,            // 回调参数
      &sensor_event_temp_handler_instance         // 实例指针
      ));

官方文档翻译:esp_event_handler_instance_register_with 向特定循环注册一个事件处理程序实例。

参数解释

1. sensor_loop_handle(第一个参数)
2. SENSOR_EVENT(第二个参数)
3. SENSOR_EVENT_ID::SENSOR_EVENT_TEMP_UPDATED(第三个参数)
4. sensor_event_handler(第四个参数)
5. &sensor_event_temp_handler_args(第五个参数)
6. &sensor_event_temp_handler_instance(第六个参数)

事件处理函数编写注意事项

  1. 回调函数 sensor_event_handler 必须是全局函数或静态函数(如果是类的成员函数,可以先创建一个 static 函数用于绑定事件,然后把 this 指针通过 handler_args 发给事件被触发时被调用的那个 static 函数):

    class SensorManager {
    public:
        // 构造函数等
    
        // 需要被调用的非静态成员函数
        void onSensorEvent(esp_event_base_t base, int32_t id, void* event_data) {
          // 实际事件处理逻辑,可访问对象成员
        }
    
    private:
        // 事件循环句柄(对象成员)
        esp_event_loop_handle_t sensor_loop_handle;
        float m_temp = 0.0f;                    // 温度数据(对象成员)
        float m_hum = 0.0f;                     // 湿度数据(对象成员)
    
        // 注册事件回调的函数
        void registerEventCallback() {
          ESP_ERROR_CHECK(esp_event_handler_instance_register_with(
              sensor_loop_handle,
              SENSOR_EVENT,
              SENSOR_EVENT_ID::SENSOR_EVENT_TEMP_UPDATED,
              staticEventHandler,     // 静态成员函数(回调桥梁)
              this,                   // 传入this指针(作为handler_args)
              NULL
          ));
        }
    
        // 静态成员函数(回调桥梁,无this指针,原型匹配ESP-IDF要求)
        static void staticEventHandler(void* handler_args,
                                       esp_event_base_t base,
                                       int32_t id,
                                       void* event_data) {
          // 关键:将handler_args强制转换为SensorManager*(即this指针)
          SensorManager* self =
            static_cast<SensorManager*>(handler_args);
          if (self != nullptr) {
            // 成功调用到这个对象的非静态成员函数(实际处理逻辑)
            self->onSensorEvent(base, id, event_data);
          }
        }
    
    };
    
  2. 回调函数的执行上下文是 sensor_loop_handle 对应的事件循环任务(sensor_loop 任务),因此回调中不能有长时间阻塞(如 vTaskDelay(1000)),否则会影响该循环的其他事件处理。

  3. event_data 是发送事件时传入的临时数据,回调中若需长期使用,需拷贝到本地(避免原数据被释放)。


发送事件

这一步中,我们将会把「温度更新事件」及附带的 temp_event 数据 发送 到 sensor_loop_handle 对应的 事件循环 中。

ESP_ERROR_CHECK(
  esp_event_post_to(sensor_loop_handle,
                    ENSOR_EVENT,
                    SENSOR_EVENT_ID::SENSOR_EVENT_TEMP_UPDATED,
                    &temp_event,
                    sizeof(temp_event),
                    portMAX_DELAY));

esp_event_post_to 将事件发布到指定的事件循环。

参数解释

1. sensor_loop_handle(第一个参数)

(完全同上)

2. SENSOR_EVENT(第二个参数)

(完全同上)

3. SENSOR_EVENT_ID::SENSOR_EVENT_TEMP_UPDATED(第三个参数)

(完全同上)

4. &temp_event(第四个参数)
5. sizeof(temp_event)(第五个参数)
6. portMAX_DELAY(第六个参数)

注销事件处理程序

这一步中,我们将会注销刚才注册的 “将传感器数据输出” 事件处理函数,将其与 SENSOR_EVENT_ID::SENSOR_EVENT_TEMP_UPDATED 事件解绑。

ESP_ERROR_CHECK(esp_event_handler_instance_unregister_with(
    unreg_test_args->loop_handle,               // 自定义循环句柄
    SENSOR_EVENT,                               // 事件基类
    SENSOR_EVENT_ID::SENSOR_EVENT_TEMP_UPDATED, // 事件ID
    unreg_test_args->instance                // 实例指针(可选,用于后续注销)
    ));

esp_event_handler_instance_unregister_with 从特定的事件循环中注销一个处理程序实例。

参数解释

1. sensor_loop_handle(第一个参数)

(完全同上)

2. SENSOR_EVENT(第二个参数)

(完全同上)

3. SENSOR_EVENT_ID::SENSOR_EVENT_TEMP_UPDATED(第三个参数)

(完全同上)

4. unreg_test_args->instance(第四个参数)

参见 #6-sensor_event_temp_handler_instance 第六个参数

删除事件循环

这一步中,我们将会删除之前创建的自定义事件循环 sensor_loop_handle,释放其占用的资源。

ESP_ERROR_CHECK(esp_event_loop_delete(sensor_loop_handle));

esp_event_loop_delete 删除一个事件循环。

参数解释

1. sensor_loop_handle(第一个参数)

(完全同上)


esp-idf 框架默认事件循环

在复杂度较低的 小型项目 中(例如简单的 LED 控制、按键响应、低频率传感器数据上报(如 1 秒 / 次)),我们往往可以 直接使用 idf 框架自带 的默认事件循环,让代码更加简洁。

参见 官方文档 - 默认事件循环
API 对应表:(参数除了不用指定 handler 外,其他都与用户事件循环相同)

用户事件循环
esp_event_loop_create()
esp_event_loop_delete()
esp_event_handler_register_with()
esp_event_handler_unregister_with()
esp_event_post_to()
默认事件循环
esp_event_loop_create_default(void)
esp_event_loop_delete_default(void)
esp_event_handler_register()
esp_event_handler_unregister()
esp_event_post()

测试时使用的代码(非常乱没什么用仅供自己查阅拼贴起来方便):eventloop.cpp