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 在什么时候又 莫名其妙被调用 了…
那么,有没有一种让系统只需要关心自己的逻辑,不需要“逆向思维”考虑自己什么情况下被其他系统调用,也不需要来回轮询的方式呢?这就是 事件系统:
注册(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
- 作用:设置事件循环的 事件队列容量(单位:事件个数)。
- 详细说明:
- 所有 post 到该循环的 事件 (如传感器数据就绪、状态变更等)会 先存入这个队列,再由事件任务按 顺序处理。
- 这里配置为
8表示队列 最多 可缓存 8 个未处理事件。若事件产生速度超过处理速度, 队列满后新事件会被丢弃 (或根据配置返回错误)。
- 选型建议:
- 传感器事件 频率低(如每秒 1 次)→ 队列大小可设为 2~4;
- 高频 事件(如每秒 10 次以上)→ 需 增大 队列(如 8~16),避免事件丢失。
2. .task_name = "sensor_loop"
- 作用:指定事件循环对应的 任务名称(FreeRTOS 任务名)。
- 详细说明:
- 事件循环本质是一个独立的 FreeRTOS 任务,此名称用于 调试 和任务管理(如通过
freeRTOS工具查看任务状态、CPU 占用率等)。 - 命名建议:与功能绑定(如 “sensor_loop” 明确是传感器相关的事件处理任务),方便问题定位。
- 事件循环本质是一个独立的 FreeRTOS 任务,此名称用于 调试 和任务管理(如通过
- 限制:任务名长度通常 不超过 16 个字符 (ESP-IDF 默认限制),超出部分会被 截断。
3. .task_priority = 5
- 作用:设置事件循环任务的 优先级(FreeRTOS 优先级)。
- 详细说明:
- ESP32 系列的 FreeRTOS 优先级 范围为
0~23(0 是最低优先级,23 是最高优先级)。 - 这里配置为
5,属于中等优先级:- 高于 IDLE 任务(优先级 0)和低优先级后台任务(如日志打印、WiFi 扫描辅助);
- 低于核心业务任务(如中断处理、实时数据传输,优先级通常 10+)。
- ESP32 系列的 FreeRTOS 优先级 范围为
- 选型建议:
- 传感器数据处理若需 实时性(如快速响应传感器触发)→ 优先级可设为 8~12;
- 非实时 场景(如周期性上报数据)→ 优先级 3~5 即可,避免抢占核心任务资源。
4. .task_stack_size = 4096
- 作用:设置事件循环任务的 栈大小(单位:字节,ESP32 中栈以 4 字节对齐)。主要为给事件处理函数(以及事件循环运行的后台任务)用的栈空间。和前面提到的“事件队列”等无关,这是完全独立分配的内存。。
- 详细说明:
4096字节(即 4KB),是传感器事件处理的常见合理配置:- 若事件处理函数中局部变量少、调用层级浅(如仅读取传感器数据+发送事件/输出日志)→ 2KB(2048)足够;
- 若处理逻辑复杂(如数据解析、加密、多函数嵌套)→ 需增大栈(如 4KB~8KB),避免栈溢出(导致任务崩溃、系统重启)。
- 若事件处理函数中包含 JSON 解析、大数组、递归调用、WiFi/BLE 等复杂事件处理,那么就需要更大栈(12288 以上)。
5. .task_core_id = 0
- 作用:指定事件循环任务运行在 ESP32 的 哪个核心 上。
- 详细说明:
- 我使用的 ESP32-c3 为单核 MCU ,直接填入 “0” 即可。
- 如果你使用的是 ESP32-s3 等双核 MCU,可以根据用途指定核心:核心 0 通常用于处理系统任务(如 WiFi/蓝牙协议栈、IDF 后台服务);核心 1 通常用于用户自定义的高优先级任务(如实时控制、数据处理)。
第二个参数:esp_event_loop_handle_t
esp_event_loop_handle_t是 ESP-IDF 框架中 事件循环的 句柄 (Handle),本质是一个「事件循环实例的 标识符 / 指针」。后续 所有 操作(如调用esp_event_post_to()函数 发送事件 、调用esp_event_handler_instance_register_with()函数 注册事件回调 、调用esp_event_loop_delete(loop_handle)删除事件循环 )都 需要 通过这个句柄,明确指定 “操作哪个事件循环”。
注册事件处理程序
这一步中,我们将会把“将传感器数据输出”的 事件处理函数 绑定到“传感器数据更新”这个事件上,从而触发“将传感器数据输出”这个事件处理函数。
在注册事件处理函数前,我们也需要进行一定的准备工作:分别是 事件基类、事件 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(第一个参数)
- 类型:
esp_event_loop_handle_t(事件循环句柄) - 作用:指定 「哪个事件循环」 要注册这个回调。
- 详细说明:
- 对应我们之前创建的自定义事件循环 “
esp_event_loop_handle_t sensor_loop_handle;",通过句柄明确“回调属于这个循环”,而非系统默认循环或其他自定义循环。 - 只有发送到这个循环的,属于
SENSOR_EVENT事件基类的事件,才会触发该回调(实现多循环隔离)。
- 对应我们之前创建的自定义事件循环 “
- 注意:必须是已创建且有效的句柄(
esp_event_loop_create_with_args()成功返回的句柄),否则会触发ESP_ERROR_CHECK断言失败(单片机复位)。
2. SENSOR_EVENT(第二个参数)
- 类型:事件根基(官方文档上是这么说的)
- 作用:用于区分不同模块/功能的事件(类似“命名空间”)。
- 详细说明:(每个事件根基可以声明多次,但只能定义一次)
- 声明事件根基:
ESP_EVENT_DECLARE_BASE(EVENT_BASE); - 定义事件根基:
ESP_EVENT_DEFINE_BASE(EVENT_BASE); - 回调函数只会响应「该根基下」的指定事件 ID,避免不同模块的事件混淆(比如就算 事件 id 一致,发送到
SENSOR_EVENT的事件也 不会响应WIFI_EVENT根基的事件)。
- 声明事件根基:
- 规范:事件根基名称需唯一,建议与功能绑定(如
SENSOR_EVENT、BLE_EVENT),方便维护。来自官方文档: 在 ESP-IDF 中,系统事件的根基标识符为大写字母,并以
_EVENT结尾。例如,Wi-Fi事件的根基声明为WIFI_EVENT,以太网的事件根基声明为ETHERNET_EVENT等。这样一来,事件根基与常量类似(尽管根据宏ESP_EVENT_DECLARE_BASE和ESP_EVENT_DEFINE_BASE的定义,它们属于全局变量)。
3. SENSOR_EVENT_ID::SENSOR_EVENT_TEMP_UPDATED(第三个参数)
- 类型:
int32_t(事件 ID,本质是整数常量) - 作用:在同一事件根基下,唯一标识「具体的事件类型」。
- 详细说明:
SENSOR_EVENT_ID是我们 自定义的枚举(enum),SENSOR_EVENT_TEMP_UPDATED是其中 一个枚举值(比如表示“温度数据更新”事件)。- 同一根基 下可定义 多个事件 ID(如
SENSOR_EVENT_HUM_UPDATED(湿度更新)、SENSOR_EVENT_ERROR(传感器错误)),回调函数 只会 在收到「该具体 ID」的事件时触发。 - 本质是整数常量,不支持位掩码枚举!
- 可以通过
ESP_EVENT_ANY_ID一次订阅 这一事件根基 下的 所有 id 。
- 枚举定义示例:
enum class SENSOR_EVENT_ID { SENSOR_EVENT_TEMP_UPDATED, // 温度更新事件(ID 通常为 0) SENSOR_EVENT_HUM_UPDATED, // 湿度更新事件(ID 为 1) SENSOR_EVENT_ERROR // 传感器错误事件(ID 为 2) };
4. sensor_event_handler(第四个参数)
- 类型:
void* event_handler_arg - 作用:事件触发时,实际执行的「处理函数」。
- 详细说明:
- 必须是符合 ESP-IDF 回调函数原型的函数,原型固定为:
// esp_event_base.h:22 typedef void (*esp_event_handler_t)(void* event_handler_arg, esp_event_base_t event_base, int32_t event_id, void* event_data); // 当前案例中对应事件处理函数: void sensor_event_handler(void *handler_args, esp_event_base_t base, int32_t id, void *event_data) { // 从 event_data 获得传感器发布事件是传入的温度数据 float temp = *((float*)event_data); ESP_LOGI("SENSOR_EVENT", "温度 -> %.1f°C", temp); }handler_args:对应下面第 5 个参数传入的 用户自定义参数,在 订阅 时传入,和事件发布者无关;base:触发事件的 根基(此处固定为SENSOR_EVENT);id:触发事件的 具体 ID(此处固定为SENSOR_EVENT_TEMP_UPDATED);event_data:使用esp_event_post_to()函数 发送事件 时通过 参数 附带的额外数据(如温度值、传感器状态)。
- 我们的 业务逻辑(如读取温度数据、上报到服务器、更新显示屏)都写在这个
sensor_event_handler()函数里。
- 必须是符合 ESP-IDF 回调函数原型的函数,原型固定为:
5. &sensor_event_temp_handler_args(第五个参数)
-
类型:
void*(通用指针,可选参数) -
作用:向回调函数
sensor_event_handler传递「用户自定义参数」(回调的“外部配置数据”)。 -
详细说明:
sensor_event_temp_handler_args是我们定义的变量/结构体(比如存储传感器配置、目标设备句柄等),通过取地址作为void*传入后,在回调函数中可通过将handler_args参数 强制转换为原类型 使用。- 用途:避免回调函数依赖全局变量,让回调更通用、可复用(比如不同传感器的回调可传入不同配置参数)。
-
示例:
// 自定义回调参数结构体 typedef struct { int temp_threshold; // 温度阈值(比如超过 30℃ 报警) bool enable_log; // 是否打印日志 } sensor_event_handler_args_t; // 定义参数实例 sensor_event_handler_args_t sensor_event_temp_handler_args = { .temp_threshold = 30, .enable_log = true }; // 回调函数中使用参数 void sensor_event_handler(void* handler_args, esp_event_base_t base, int32_t id, void* event_data) { // 从 event_data 获得传感器发布事件是传入的温度数据 float temp = *((float*)event_data); // 从 handler_args 获得注册事件时传入的阈值数据 sensor_event_temp_handler_args_t *args = (sensor_event_temp_handler_args_t *)handler_args; // 超过输入的阈值时 报警 if (args->enable_log && temp > args->temp_threshold) { ESP_LOGI("SENSOR_EVENT", "温度超过阈值:%d℃", args->temp_threshold); } } -
可选性:若回调不需要额外参数,可传入
NULL。
6. &sensor_event_temp_handler_instance(第六个参数)
-
类型:
esp_event_handler_instance_t*(回调实例句柄指针,可选参数) -
作用:用于后续「注销该回调函数」的标识 —— 这个 “注册” 的 身份证。
-
详细说明:
- 若传入一个非
NULL的esp_event_handler_instance_t变量地址(如&sensor_event_temp_handler_instance),函数会将该回调的“实例 ID”存入这个变量,后续可通过esp_event_handler_instance_unregister_with()精准注销 该回调(而 不影响 同一事件的其他回调)。 - 若传入
NULL:表示 不需要 后续 注销 该回调。
- 若传入一个非
-
注销示例:
// 定义实例句柄 esp_event_handler_instance_t sensor_event_temp_handler_instance = NULL; // 注册时传入实例指针 ESP_ERROR_CHECK(esp_event_handler_instance_register_with( sensor_loop_handle, SENSOR_EVENT, SENSOR_EVENT_ID::SENSOR_EVENT_TEMP_UPDATED, sensor_event_handler, &sensor_event_temp_handler_args, &sensor_event_temp_handler_instance // 传入实例指针,接收实例 ID )); // 后续需要注销时(如传感器关闭) ESP_ERROR_CHECK(esp_event_handler_instance_unregister_with( sensor_loop_handle, SENSOR_EVENT, SENSOR_EVENT_ID::SENSOR_EVENT_TEMP_UPDATED, sensor_event_temp_handler_instance // 通过实例 ID 精准注销 )); -
可选性:短期运行的任务或无需注销的回调,可传入
NULL;长期运行且需要动态注销的场景(如传感器热插拔),建议传入实例指针。
事件处理函数编写注意事项
-
回调函数
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); } } }; -
回调函数的执行上下文是
sensor_loop_handle对应的事件循环任务(sensor_loop任务),因此回调中不能有长时间阻塞(如vTaskDelay(1000)),否则会影响该循环的其他事件处理。 -
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(第四个参数)
- 类型:
void*(通用指针,可选) - 作用:传递「事件附带的具体数据」(此处是温度相关数据)。
- 详细说明:
temp_event是我们事先定义的变量/结构体,存储要 随事件发送 的 实际数据 (如当前温度值):对应的回调函数struct sensor_event_temp_updated_t { float temp; };sensor_event_handler中,会将temp_event数据通过void* event_data参数传递过来。 获取温度的代码:float temp = *((float*)event_data);- 取地址 传入后,ESP-IDF 会将该数据 拷贝 到 事件队列 中(队列存储的是 数据副本 ,发送后 可修改
temp_event原值,不影响 队列中的数据); - 若事件无需附带数据,可传入
NULL。
5. sizeof(temp_event)(第五个参数)
- 类型:
size_t(无符号整数) - 作用:指定「事件附带数据的字节大小」,用于 ESP-IDF 正确拷贝数据到事件队列。
- 详细说明:
- 必须与
temp_event的实际大小一致(通过sizeof()自动计算,避免手动写死出错); - 若第四个参数是
NULL(无数据),此处需传入0;
- 必须与
- 注意:数据大小 不能超过 事件队列的“单事件最大容量”(4096 字节),否则会返回
ESP_ERR_INVALID_SIZE错误。event_data_size – [in] the size of the event data; max is 4 bytes
6. portMAX_DELAY(第六个参数)
- 类型:
TickType_t(FreeRTOS 时间片类型,本质是无符号整数) - 作用:指定「发送事件时的阻塞等待时间」(单位:FreeRTOS 时间片,1 时间片 = 系统滴答定时器周期,默认 10ms)。
- 详细说明:
portMAX_DELAY是 FreeRTOS 预定义常量,表示“无限期阻塞”:若事件队列已满(如之前配置的queue_size=8,且已有 8 个未处理事件),发送任务会一直阻塞,直到队列有空闲位置才会发送事件;- 其他常用值:
0:非阻塞发送,队列满则立即返回错误(ESP_ERR_TIMEOUT);pdMS_TO_TICKS(100):阻塞 100ms(通过宏转换为时间片),超时未发送成功则返回错误;
- 注意:阻塞期间,发送任务会释放 CPU 使用权,不会占用资源;若在中断服务函数(ISR)中调用
esp_event_post_to(),第六个参数必须为0(ISR 中不能阻塞)(这个我还没测试)。
注销事件处理程序
这一步中,我们将会注销刚才注册的 “将传感器数据输出” 事件处理函数,将其与 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