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

目录

前言

一、事件循环库是什么?

[1.1 解决的核心问题](#1.1 解决的核心问题 "#1.1%20%E8%A7%A3%E5%86%B3%E7%9A%84%E6%A0%B8%E5%BF%83%E9%97%AE%E9%A2%98")

[1.2 包含的核心要素](#1.2 包含的核心要素 "#1.2%20%E5%8C%85%E5%90%AB%E7%9A%84%E6%A0%B8%E5%BF%83%E8%A6%81%E7%B4%A0")

[1.3 与线程任务的区别](#1.3 与线程任务的区别 "#1.3%20%E4%B8%8E%E7%BA%BF%E7%A8%8B%E4%BB%BB%E5%8A%A1%E7%9A%84%E5%8C%BA%E5%88%AB")

[1.4 适用场景](#1.4 适用场景 "#1.4%20%E9%80%82%E7%94%A8%E5%9C%BA%E6%99%AF")

二、工程代码

[2.1 event.c](#2.1 event.c "#2.1%20event.c")

[2.2 event.h](#2.2 event.h "#2.2%20event.h")

[2.3 main.c](#2.3 main.c "#2.3%20main.c")

[2.4 代码解释](#2.4 代码解释 "#2.4%20%E4%BB%A3%E7%A0%81%E8%A7%A3%E9%87%8A")

[2.4.1 一种任务的通知方式。](#2.4.1 一种任务的通知方式。 "#2.4.1%C2%A0%E4%B8%80%E7%A7%8D%E4%BB%BB%E5%8A%A1%E7%9A%84%E9%80%9A%E7%9F%A5%E6%96%B9%E5%BC%8F%E3%80%82")

三、结果展示


前言

本章主要介绍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 一种任务的通知方式。

xTaskNotifyGiveulTaskNotifyTake 是 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中也有两个事件。和我们创建的类型是一致的。其他的删除、注销功能可以去查看官方资料。

相关推荐
凉、介2 小时前
KVM + QEMU 虚拟化
笔记·学习·嵌入式·arm·qemu·虚拟化·kvm
dddwjzx17 小时前
嵌入式Linux C应用编程入门——文件IO进阶
嵌入式
2023自学中21 小时前
imx6ull 开发板, mame 模拟器,运行游戏 测试
linux·游戏·嵌入式·开发板
dddwjzx1 天前
嵌入式Linux C应用编程入门——文件IO
嵌入式
fzm52981 天前
车载ECU单元测试技术与应用研究
c语言·自动化测试·单元测试·嵌入式·白盒测试
用户120487221613 天前
Linux驱动编译与加载
linux·嵌入式
用户805533698033 天前
Input 子系统架构:Core、Handler、Driver 三层是怎么协作的
linux·嵌入式
用户805533698033 天前
RK-Forge外设系列开篇 - 把板子从「能启动」变成「能用」:Ethernet/SPI/MMC 三个纯接线外设
linux·github·嵌入式
神奇啊龙4 天前
我的第一个 TinyGo 项目:ESP32-C3 + DHT11 + SSD1306
物联网·嵌入式