咋样更轻松的理解FreeRTOS中的事件组?
支付宝“集五福”活动是一项广受欢迎的春节互动游戏。参与者通过完成不同任务获取五类福卡:爱国福、富强福、和谐福、友善福和敬业福,每张福卡对应二进制编码中的特定位置:
- 爱国福:位0
- 富强福:位1
- 和谐福:位2
- 友善福:位3
- 敬业福:位4
参与者每次获得一张福卡,系统会自动将其对应的二进制位置1。福卡获取方式多样,包括扫福字、好友互赠、蚂蚁森林任务等。
当集齐所有五张福卡(二进制表示为0b11111)时,即可在支付宝平台兑换奖金。兑换成功后,系统会清空当前福卡记录(重置为0b00000),参与者可重新开始新一轮的集福活动。
上面描述的内容,就像EventGroup一样。好多任务独立运行,输出状态结果到事件组中,一个任务检测事件组状态,当状态满足条件组合后,开始执行,并清空相关事件组状态位。
一. 事件组概述
1.1 功能作用
FreeRTOS的事件组(Event Group)是一种高效的线程间通信机制,它允许任务之间通过事件标志(event flags)进行同步和通信。事件组的主要功能包括:
-
多事件同步:一个任务可以等待多个事件中的任意一个或全部发生
-
广播通知:一个任务可以设置事件标志,同时唤醒多个等待这些事件的任务
-
低资源消耗:相比使用多个信号量或消息队列,事件组更加节省内存
-
原子操作:事件标志的设置和清除是原子操作,无需额外的同步机制
事件组特别适合以下场景:
-
一个任务需要等待多个条件同时满足
-
多个任务需要等待同一个事件发生
-
需要通知多个任务某个事件已发生
1.2 事件组的定义
在FreeRTOS中,事件组由EventGroupHandle_t类型的句柄表示,本质上是一个包含多个事件标志(bit)的数据结构。每个事件标志可以看作是一个二进制信号,可以被设置或清除。
FreeRTOS的事件组实现具有以下特点:
-
每个事件组包含24个可用的事件标志(bit)
-
事件标志编号从0开始(最低有效位)到23(最高有效位)
-
事件组的操作是线程安全的
-
支持任务阻塞等待特定的事件组合
二. 事件组的使用
2.1 创建事件组
在使用事件组前,必须先创建它:
EventGroupHandle_t xEventGroupCreate(void);
此函数创建一个新的事件组并返回其句柄。如果返回NULL,表示创建失败(通常是因为内存不足)。
2.2 设置事件标志
设置事件标志有两种方式:
a.设置指定bit:
EventBits_t xEventGroupSetBits(EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet);
-
xEventGroup: 事件组句柄
-
uxBitsToSet: 要设置的bit掩码(如0x01设置bit0,0x05设置bit0和bit2)
-
返回值:设置后的事件组值
b.从ISR中设置bit:
BaseType_t xEventGroupSetBitsFromISR(EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet,
BaseType_t *pxHigherPriorityTaskWoken);
-
参数与xEventGroupSetBits类似
-
pxHigherPriorityTaskWoken: 用于指示是否有高优先级任务被唤醒
-
返回pdPASS表示成功,pdFAIL表示失败
2.3 等待事件标志
任务可以等待一个或多个事件标志:
EventBits_t xEventGroupWaitBits(const EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToWaitFor,
const BaseType_t xClearOnExit,
const BaseType_t xWaitForAllBits,
TickType_t xTicksToWait);
-
xEventGroup: 事件组句柄
-
uxBitsToWaitFor: 要等待的bit掩码
-
xClearOnExit: 如果为pdTRUE,则在返回前清除等待的bit
-
xWaitForAllBits: 如果为pdTRUE,则等待所有指定bit置位;为pdFALSE则等待任一bit置位
-
xTicksToWait: 等待的最大时间(ticks),portMAX_DELAY表示无限等待
-
返回值:满足条件的事件标志(可能包含未请求的bit)
2.4 清除事件标志
清除事件标志:
EventBits_t xEventGroupClearBits(EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToClear);
-
xEventGroup: 事件组句柄
-
uxBitsToClear: 要清除的bit掩码
-
返回值:清除前的事件组值
从ISR中清除bit:
EventBits_t xEventGroupClearBitsFromISR(EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToClear);
2.5 获取事件组当前值
EventBits_t xEventGroupGetBits(EventGroupHandle_t xEventGroup);
EventBits_t xEventGroupGetBitsFromISR(EventGroupHandle_t xEventGroup);
2.6 销毁事件组
当不再需要事件组时,应销毁它以释放资源:
void vEventGroupDelete(EventGroupHandle_t xEventGroup);
注意:销毁前应确保没有任务在等待该事件组。
三. 事件组使用示例
下面是一个完整的事件组使用示例,展示三个任务如何使用事件组进行同步:
#include "FreeRTOS.h"
#include "task.h"
#include "event_groups.h"
// 定义事件标志
#define TASK1_BIT (1 << 0) // bit0
#define TASK2_BIT (1 << 1) // bit1
#define ALL_SYNC_BITS (TASK1_BIT | TASK2_BIT)
EventGroupHandle_t xEventGroup;
void vTask1(void *pvParameters) {
const TickType_t xDelay200ms = pdMS_TO_TICKS(200);
for(;;) {
// 模拟任务1的工作
vTaskDelay(xDelay200ms);
printf("Task1 完成工作,设置bit0\\n");
xEventGroupSetBits(xEventGroup, TASK1_BIT);
// 等待两个任务都完成
printf("Task1 等待两个任务完成…\\n");
xEventGroupWaitBits(xEventGroup, ALL_SYNC_BITS, pdTRUE, pdTRUE, portMAX_DELAY);
printf("Task1: 两个任务都已完成!\\n");
}
}
void vTask2(void *pvParameters) {
const TickType_t xDelay300ms = pdMS_TO_TICKS(300);
for(;;) {
// 模拟任务2的工作
vTaskDelay(xDelay300ms);
printf("Task2 完成工作,设置bit1\\n");
xEventGroupSetBits(xEventGroup, TASK2_BIT);
// 等待两个任务都完成
printf("Task2 等待两个任务完成…\\n");
xEventGroupWaitBits(xEventGroup, ALL_SYNC_BITS, pdTRUE, pdTRUE, portMAX_DELAY);
printf("Task2: 两个任务都已完成!\\n");
}
}
void vTaskMonitor(void *pvParameters) {
EventBits_t uxBits;
for(;;) {
// 等待任一事件发生
uxBits = xEventGroupWaitBits(xEventGroup, ALL_SYNC_BITS, pdFALSE, pdFALSE, portMAX_DELAY);
if((uxBits & TASK1_BIT) != 0) {
printf("Monitor: 收到Task1完成信号\\n");
}
if((uxBits & TASK2_BIT) != 0) {
printf("Monitor: 收到Task2完成信号\\n");
}
}
}
int main(void) {
// 创建事件组
xEventGroup = xEventGroupCreate();
if(xEventGroup == NULL) {
printf("无法创建事件组!\\n");
return 1;
}
// 创建任务
xTaskCreate(vTask1, "Task1", configMINIMAL_STACK_SIZE, NULL, 2, NULL);
xTaskCreate(vTask2, "Task2", configMINIMAL_STACK_SIZE, NULL, 2, NULL);
xTaskCreate(vTaskMonitor, "Monitor", configMINIMAL_STACK_SIZE, NULL, 1, NULL);
// 启动调度器
vTaskStartScheduler();
return 0;
}
四. 事件组使用注意事项
4.1 资源限制:
- FreeRTOS事件组只有24个可用bit(0-23)
- 使用前应合理规划bit分配,避免冲突
4.2 性能考虑:
- 事件组操作通常是原子操作,但等待操作可能导致任务阻塞
- 在ISR中使用FromISR版本函数
4.3 同步问题:
- 事件标志是"或"的关系,多个任务可能同时响应同一事件
- 对于关键操作,应结合其他同步机制(如互斥量)
4.4 内存管理:
- 不再使用的事件组应及时删除以避免内存泄漏
- 确保删除事件组时没有任务在等待它
4. 5 调试技巧:
- 为每个事件bit定义有意义的宏名称
- 在复杂系统中,记录事件组的设置和等待情况有助于调试
4. 6 设计建议:
- 避免过度依赖事件组实现复杂逻辑,保持简单
- 考虑事件组的生命周期,确保创建和销毁的时机正确
- 在高优先级任务中谨慎使用无限等待,可能导致低优先级任务饥饿
小结:通过合理使用事件组,可以有效地实现任务间的同步和通信,构建更加清晰高效的FreeRTOS应用程序。
评论前必须登录!
注册