ESP-IDF+vscode开发ESP32第十二讲——event

目录

前言

一、事件循环库是什么?

[1.1 解决的核心问题](#1.1 解决的核心问题)

[1.2 包含的核心要素](#1.2 包含的核心要素)

[1.3 与线程任务的区别](#1.3 与线程任务的区别)

[1.4 适用场景](#1.4 适用场景)

二、工程代码

[2.1 event.c](#2.1 event.c)

[2.2 event.h](#2.2 event.h)

[2.3 main.c](#2.3 main.c)

[2.4 代码解释](#2.4 代码解释)

[2.4.1 一种任务的通知方式。](#2.4.1 一种任务的通知方式。)

三、结果展示


前言

本章主要介绍ESP32的事件循环库的使用,这是一个系统功能,不是必须学习和使用项。但是该功能再某些方面具有很便利的应用,还是值得去学习的。

ESP-IDF版本是6.0.1。


一、 事件循环库是什么?

整体来说,它就像是 ESP32 内部的一套**"异步消息派发与处理系统"** 。它允许不同的软件模块(如 Wi-Fi 驱动、以太网、定时器或你自己的业务代码)之间通过"发布-订阅"的模式进行通信,而不需要彼此直接调用,从而实现代码的松耦合

1.1 解决的核心问题

在嵌入式开发中,很多操作都是"异步"的。比如你调用**esp_wifi_connect()** 发起 Wi-Fi 连接,这个函数会立刻返回,但真正的连接过程可能需要几百毫秒甚至更久。

  • 没有事件循环时:你可能需要在一个 while 死循环里不断轮询 Wi-Fi 的状态,这会严重浪费 CPU 资源,甚至导致系统卡死。
  • 有了事件循环后:你只需要提前"订阅"一个"Wi-Fi 连接成功"的事件。当底层驱动连上 Wi-Fi 后,会自动向事件循环"发布"这个消息,你的代码就会被自动触发执行。

1.2 包含的核心要素

  1. 事件循环(Event Loop):
    本质上是一个长期运行的 FreeRTOS 专属任务(Task)。它内部维护着一个消息队列,专门负责监听和分发事件。
  2. 事件(Event):
    被传递的消息。每个事件都有两个身份标识:
    • 事件根基(Event Base):相当于"姓氏",代表事件的类别
    • 事件 ID(Event ID):相当于"名字",代表具体的事件
  3. 事件处理程序(Event Handler):
    你编写的回调函数。当特定的事件发生时,事件循环会调用这个函数来处理业务逻辑。
  4. 注册与发布(Register & Post):
    • 注册:告诉事件循环,"当发生 A 事件时,请执行 B 函数"。
    • 发布:向事件循环的队列里投递一个事件消息。

1.3 与线程任务的区别

线程任务拥有独立的栈空间和优先级。调度器会根据优先级进行抢占式调度或同优先级时间片轮转,多个任务可以在 ESP32 的双核上实现真正的物理并行。

事件循环库本质上是一个基于"发布-订阅"模式的消息分发机制。它通常由一个专用的后台任务(Task)和内部的消息队列组成。当某个组件调用 esp_event_post 发布事件时,事件会被放入队列,随后由事件循环任务按顺序取出,并同步调用已注册的回调函数(Handler)。事件处理程序是在事件循环任务的上下文中串行执行的,而不是并发执行的。

维度 线程任务 (FreeRTOS Task) 事件循环库 (Event Loop Library)
执行机制 抢占式调度,多任务可真正并行 消息队列驱动,回调函数在单一任务中串行执行
资源开销 较大,每个任务需独立分配栈空间(通常数KB) 较小,仅需维护一个任务和队列,内存占用更低
响应延迟 极低,高优先级任务可微秒级抢占 CPU 中等,受队列深度和其他回调阻塞影响,通常为毫秒级

1.4 适用场景

事件循环库最核心的使用场景是:处理异步的、非计算密集型的系统状态变化和模块间通信。它特别适合用来解决那些"你不知道它什么时候会发生,但发生后需要通知多个模块"的情况。比如以下场景:

网络状态管理(最经典场景)

Wi-Fi 或蓝牙的连接过程是典型的异步状态机。从开始连接、认证成功、获取 IP 到意外断开,整个过程可能持续几百毫秒甚至更久。使用事件循环,你可以在连接成功时触发业务逻辑,而不用在主线程里死循环轮询,避免系统卡死。

外设中断的"安全搬运"

在 ESP32 中,中断服务程序(ISR)要求执行极快,严禁调用可能阻塞的 API(如延时、打印、内存分配)。事件循环可以将中断作为一个"事件"快速投递出去,然后在事件循环的专属任务上下文中安全地执行复杂的业务逻辑(如按键消抖后的具体动作)。

模块间的解耦通信

当多个独立的模块需要响应同一个动作时,事件循环非常高效。例如,当设备收到一条新的传感器数据时,日志模块需要记录、UI 模块需要刷新屏幕、网络模块需要上传云端。通过发布一个"数据就绪"事件,这三个模块可以各自独立订阅并处理,彼此之间完全不需要知道对方的存在。

其他有关详细内容可查看官方文档《事件循环库

二、工程代码

创建好新工程,自定义组件event,再自定义组件的CMakeLists.txt中添加事件库的依赖:

bash 复制代码
idf_component_register(SRCS "event.c"
                    INCLUDE_DIRS "include"
                    REQUIRES esp_event)

先看代码,再来解释

2.1 event.c

cpp 复制代码
#include <stdio.h>
#include "event.h"

static const char* TAG = "event";

ESP_EVENT_DEFINE_BASE(MY_EVENT_BASE);
esp_event_loop_handle_t loop_handle;

void my_event_handler1(void* event_handler_arg,esp_event_base_t event_base,int32_t event_id,void* event_data);
void my_event_handler2(void* event_handler_arg,esp_event_base_t event_base,int32_t event_id,void* event_data);
void default_event_handler1(void* event_handler_arg,esp_event_base_t event_base,int32_t event_id,void* event_data);
void default_event_handler2(void* event_handler_arg,esp_event_base_t event_base,int32_t event_id,void* event_data);
void task_event_source(void* args);

void event_init(void) 
{
    esp_event_loop_args_t loop_args = {
        .queue_size = 5, // 事件队列大小
        .task_name = "event_loop_task", // 事件循环任务名称
        //它的优先级应该低于底层的通信驱动(如 Wi-Fi、蓝牙),但高于普通的应用业务任务(如传感器采集、数据处理)。
        .task_priority = 20, // 事件循环任务优先级
        .task_stack_size = 2048, // 事件循环任务栈大小
        .task_core_id = tskNO_AFFINITY, // 事件循环任务核心绑定
    };
    ESP_ERROR_CHECK(esp_event_loop_create(&loop_args, &loop_handle));
    ESP_ERROR_CHECK(esp_event_loop_create_default()); // 创建默认事件循环

    ESP_ERROR_CHECK(esp_event_handler_instance_register_with(loop_handle, MY_EVENT_BASE, MY_EVENT_ID1, my_event_handler1, NULL, NULL));
    ESP_ERROR_CHECK(esp_event_handler_instance_register_with(loop_handle, MY_EVENT_BASE, MY_EVENT_ID2, my_event_handler2, NULL, NULL));
    ESP_ERROR_CHECK(esp_event_handler_instance_register(MY_EVENT_BASE, MY_EVENT_ID1, default_event_handler1, NULL, NULL));
    ESP_ERROR_CHECK(esp_event_handler_instance_register(MY_EVENT_BASE, MY_EVENT_ID2, default_event_handler2, NULL, NULL));


    esp_event_dump(stdout); // 输出事件系统状态
    vTaskDelay(pdMS_TO_TICKS(100));
    TaskHandle_t task_event_hdl;
    xTaskCreate(task_event_source, "task_event_source", 4096, NULL, 10, &task_event_hdl);
    xTaskNotifyGive(task_event_hdl); // 启动事件源任务
}
void my_event_handler1(void* event_handler_arg,esp_event_base_t event_base,int32_t event_id,void* event_data)
{
    // event_handler_arg: 注册事件处理程序时传递的参数
    // event_base: 事件根基
    // event_id: 事件ID
    // event_data: 事件数据
    ESP_LOGI(TAG, "自定义事件1:Received event: base=%s, id=%d, data=%s", event_base, event_id, (char*)event_data);
    vTaskDelay(pdMS_TO_TICKS(1000));
}
void my_event_handler2(void* event_handler_arg,esp_event_base_t event_base,int32_t event_id,void* event_data)
{
    ESP_LOGI(TAG, "自定义事件2:Received event: base=%s, id=%d, data=%s", event_base, event_id, (char*)event_data);
    vTaskDelay(pdMS_TO_TICKS(1000));
}
void default_event_handler1(void* event_handler_arg,esp_event_base_t event_base,int32_t event_id,void* event_data)
{
    ESP_LOGI(TAG, "默认事件1:Received event: base=%s, id=%d, data=%s", event_base, event_id, (char*)event_data);
    vTaskDelay(pdMS_TO_TICKS(1000));
}
void default_event_handler2(void* event_handler_arg,esp_event_base_t event_base,int32_t event_id,void* event_data)
{
    ESP_LOGI(TAG, "默认事件2:Received event: base=%s, id=%d, data=%s", event_base, event_id, (char*)event_data);
    vTaskDelay(pdMS_TO_TICKS(1000));
}

void task_event_source(void* args)
{
    ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
    char* event_data = "Hello, ESP32 Event!";
    ESP_ERROR_CHECK(esp_event_post_to(loop_handle, MY_EVENT_BASE, MY_EVENT_ID1, event_data, strlen(event_data) + 1, portMAX_DELAY));
    ESP_ERROR_CHECK(esp_event_post(MY_EVENT_BASE, MY_EVENT_ID1, event_data, strlen(event_data) + 1, portMAX_DELAY));
    while(1)
    {
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

2.2 event.h

cpp 复制代码
#ifndef EVENT_H
#define EVENT_H

#include <stdio.h>               // 输入输出函数
#include <string.h>              // 字符串处理函数
#include "esp_log.h"             // ESP32日志函数
#include "FreeRTOS/FreeRTOS.h"   // FreeRTOS函数
#include "FreeRTOS/task.h"       // FreeRTOS任务管理函数
#include "FreeRTOS/semphr.h"     // FreeRTOS信号量管理函数
#include "esp_event.h"

ESP_EVENT_DECLARE_BASE(MY_EVENT_BASE);
extern esp_event_loop_handle_t loop_handle;
void event_init(void);
enum {
    MY_EVENT_ID1 = 1,
    MY_EVENT_ID2 = 2,
};
#endif                          // EVENT_H

2.3 main.c

cpp 复制代码
#include <stdio.h>
#include "user.h"
#include "event.h"

void app_main(void)
{
    char* event_data = "goodbye, ESP32 Event!!!";
    CONSOLE_REPL_INIT(); // 初始化控制台REPL环境
    event_init(); // 初始化事件系统
    while(1)
    {
        vTaskDelay(pdMS_TO_TICKS(1000));
        ESP_ERROR_CHECK(esp_event_post_to(loop_handle, MY_EVENT_BASE, MY_EVENT_ID2, event_data, strlen(event_data) + 1, portMAX_DELAY));
        ESP_ERROR_CHECK(esp_event_post( MY_EVENT_BASE, MY_EVENT_ID2, event_data, strlen(event_data) + 1, portMAX_DELAY));
    }
}

2.4 代码解释

首先在event.c中创建了一个事件循环loop_args。这些创建信息和创建任务是一致的,具有名称、优先级、栈控件。另外再加上事件队列,刚好符合前面说明的**事件循环库本质上由一个专用的后台任务(Task)和内部的消息队列组成。**自定义一个事件循环后跟着一个默认的事件循环。默认的事件循环不需要提供任何信息。

接下来就要将事件处理程序注册到事件循环中了,一个处理程序可以注册到多个事件循环中,一个事件循环可以注册多个事件(不超过事件队列数量)。

再注册的时候事件的根基和ID,它们负责给一个事件程序绑定 " 姓名 "。为了方便外部函数调用,事件循环库提供了宏以便声明和定义事件根基。具体请看 《事件定义与事件声明》。我这自定义事件循环中注册了两个事件,默认事件循环中也注册了两个事件。

后面就是创建了一个任务,这里介绍一种任务的通知方式。

2.4.1 一种任务的通知方式。

xTaskNotifyGive 和 **ulTaskNotifyTake**是 FreeRTOS 中一对经典的"黄金搭档",它们共同构成了任务通知机制的核心。

简单来说,它们的作用是让一个任务给另一个任务发信号。这对函数最常被用来替代传统的"信号量(Semaphore)",但比信号量更轻量、速度更快(性能提升约45%)且节省内存。

  • xTaskNotifyGive(发送方) :每调用一次,就给目标任务的通知计数器 +1
  • ulTaskNotifyTake(接收方):如果计数器是 0 就原地等待;如果大于 0,就将其清零(或减 1)并继续干活。

xTaskNotifyGive(发送通知)

这个函数的作用是将指定任务的通知值加 1。如果该任务因为等待通知而处于阻塞状态,它会被立刻唤醒。

  • 函数原型:BaseType_t xTaskNotifyGive( TaskHandle_t xTaskToNotify );
  • 核心参数:xTaskToNotify:目标任务的任务句柄。
  • 返回值:总是返回 pdPASS
  • 注意:这个函数只能在普通的任务(Task)中调用。如果你需要在中断(ISR)里发送通知,必须使用带 FromISR 后缀的版本:vTaskNotifyGiveFromISR()

ulTaskNotifyTake(获取通知)

这个函数用于等待并获取通知。它会根据通知值是否大于 0 来决定是继续执行还是进入阻塞(等待)状态。

  • 函数原型:uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit, TickType_t xTicksToWait );
  • 核心参数:
    • xClearCountOnExit(退出时如何清除计数)
      • 传入 pdTRUE:清零模式。相当于二值信号量。任务被唤醒后,通知值会被直接重置为 0。
      • 传入 pdFALSE:减一模式。相当于计数信号量。任务被唤醒后,通知值会减 1(例如原来是 5,取出一个后变成 4)。
    • xTicksToWait(最大等待时间)
      • 传入 0:不等待,立刻返回。
      • 传入 portMAX_DELAY:死等,直到收到通知为止。
  • 返回值:返回调用该函数之前的通知值。如果返回 0,说明没有收到通知(超时了);如果返回非 0,说明成功获取到了通知。

回到原文,再任务函数中,向自定义循环和默认循环中各发布了一个事件。

main.c函数中,初始化了事件库,并在循环中重复着向自定义循环和默认循环中各发布一个事件。

三、结果展示

可以看到,默认循环库sys_evt中有两个事件,自定义循环库event_loop_task中也有两个事件。和我们创建的类型是一致的。其他的删除、注销功能可以去查看官方资料。

相关推荐
诗水人间4 小时前
VsCode 中使用Copilot调用Deepseek V4模型
ide·vscode·copilot
梦想家加一5 小时前
vscode为什么下载了汉化插件却不生效
ide·vscode·编辑器
多云的夏天6 小时前
IDE-VSCODE-Continue + DeepSeek V4
ide·vscode·编辑器·deepseek
Robot_Nav7 小时前
Claude Code cli 以及vscode版本的各种命令参考手册
ide·vscode·编辑器
屋外雨大,惊蛰出没1 天前
Vscode自动生成类图
ide·vscode·编辑器·类图绘制
qq_14030341441 天前
vscode过滤文件
ide·vscode·编辑器
2501_915921431 天前
使用Swift和Xcode创建简单iOS应用完整教程
ide·vscode·ios·个人开发·xcode·swift·敏捷流程
skywalk81632 天前
发布vscode插件到 VS Code 市场流程
ide·vscode·编辑器
牙牙要健康2 天前
Windows 下为 VSCode 配置 Anaconda:从零安装 Python 环境到完整配置教程
windows·vscode·python