我们从 事件组数据结构、核心API源码解析、应用场景 和 示例Demo 四个维度,结合 FreeRTOS v10.4.3 源码及流程图,详细讲解事件组(Event Group)的实现原理和应用。
1. 事件组核心原理
1.1 事件组数据结构
源码位置:FreeRTOS/Source/include/event_groups.h
typedef struct EventGroupDef_t {EventBits_t uxEventBits; // 当前事件位(32位)List_t xTasksWaitingForBits; // 等待事件位的任务链表uint8_t ucEventGroupNumber; // 调试用的事件组编号
} EventGroup_t;
- uxEventBits:32位无符号整数,每位(bit)代表一个事件(BIT0~BIT31)。
- xTasksWaitingForBits:链表保存因等待事件而阻塞的任务。
1.2 事件组操作流程
2. 源码级实现分析
2.1 创建事件组
源码位置:xEventGroupCreate()
EventGroupHandle_t xEventGroupCreate( void ) {EventGroup_t *pxEventBits = pvPortMalloc( sizeof( EventGroup_t ) );pxEventBits->uxEventBits = 0; // 初始事件位清零vListInitialise( &( pxEventBits->xTasksWaitingForBits ) );return pxEventBits;
}
- 关键点:动态分配内存并初始化事件位和任务链表。
2.2 设置事件位
源码位置:xEventGroupSetBits()
EventBits_t xEventGroupSetBits( EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToSet ) {ListItem_t *pxListItem;List_t *pxList = &( pxEventGroup->xTasksWaitingForBits );// 原子操作设置事件位pxEventGroup->uxEventBits |= uxBitsToSet;// 遍历等待链表,检查是否有任务满足条件pxListItem = listGET_HEAD_ENTRY( pxList );while( pxListItem != listGET_END_MARKER( pxList ) ) {// 获取任务TCB并判断条件是否满足if( ( pxTask->uxBitsToWaitFor & pxEventGroup->uxEventBits ) == pxTask->uxBitsToWaitFor ) {// 唤醒任务并从链表中移除xTaskResumeFromISR( pxTask );}pxListItem = listGET_NEXT( pxListItem );}return pxEventGroup->uxEventBits;
}
- 核心逻辑:设置指定位,唤醒所有等待条件满足的任务。
2.3 等待事件位
源码位置:xEventGroupWaitBits()
EventBits_t xEventGroupWaitBits( EventGroupHandle_t xEventGroup,const EventBits_t uxBitsToWaitFor,BaseType_t xClearOnExit,BaseType_t xWaitForAllBits,TickType_t xTicksToWait ) {EventBits_t uxReturn;// 检查当前事件位是否满足条件uxReturn = pxEventGroup->uxEventBits;if ( ( ( uxReturn & uxBitsToWaitFor ) == uxBitsToWaitFor ) && ( xWaitForAllBits != pdFALSE ) ) {// 条件满足,直接返回} else if ( ( uxReturn & uxBitsToWaitFor ) != 0 && ( xWaitForAllBits == pdFALSE ) ) {// 条件满足(任一bit)} else {// 条件不满足,任务进入阻塞链表vTaskSuspend( xTaskGetCurrentTaskHandle() );prvAddTaskToWaitingList( pxEventGroup, uxBitsToWaitFor, xWaitForAllBits );xTaskResumeAll();}return uxReturn;
}
- 阻塞逻辑:若事件位不满足条件,任务挂起并加入等待链表。
3. 事件组应用场景
3.1 多任务同步
- 场景:多个任务需要等待不同事件触发后才执行。
// 任务1等待BIT0 | BIT1
xEventGroupWaitBits(xEventGroup, BIT0 | BIT1, pdTRUE, pdTRUE, portMAX_DELAY);// 任务2设置BIT0,任务3设置BIT1
xEventGroupSetBits(xEventGroup, BIT0);
xEventGroupSetBits(xEventGroup, BIT1);
3.2 复合事件触发
- 场景:任务需要同时满足多个条件(如传感器数据就绪+网络连接成功)。
// 等待BIT2(传感器就绪)和BIT3(网络连接)
xEventGroupWaitBits(xEventGroup, BIT2 | BIT3, pdTRUE, pdTRUE, portMAX_DELAY);
3.3 超时事件处理
- 场景:任务在指定时间内等待事件,超时后执行备用逻辑。
EventBits_t uxBits = xEventGroupWaitBits(xEventGroup, BIT4, pdTRUE, pdTRUE, 1000);
if ((uxBits & BIT4) != 0) {// 正常处理
} else {// 超时处理
}
4. 示例Demo:多传感器数据采集
4.1 场景描述
- 任务1:温度传感器数据采集(触发BIT0)。
- 任务2:湿度传感器数据采集(触发BIT1)。
- 任务3:等待BIT0和BIT1同时就绪后处理数据。
4.2 完整代码
#include "FreeRTOS.h"
#include "task.h"
#include "event_groups.h"#define BIT0 (1 << 0) // 温度就绪
#define BIT1 (1 << 1) // 湿度就绪EventGroupHandle_t xEventGroup;void vTempSensorTask(void *pvParams) {while (1) {// 模拟温度采集vTaskDelay(pdMS_TO_TICKS(100));xEventGroupSetBits(xEventGroup, BIT0);}
}void vHumiditySensorTask(void *pvParams) {while (1) {// 模拟湿度采集vTaskDelay(pdMS_TO_TICKS(200));xEventGroupSetBits(xEventGroup, BIT1);}
}void vDataProcessorTask(void *pvParams) {while (1) {// 等待BIT0和BIT1同时就绪xEventGroupWaitBits(xEventGroup, BIT0 | BIT1, pdTRUE, pdTRUE, portMAX_DELAY);// 处理数据printf("Data Ready: Temp + Humidity\n");}
}int main(void) {xEventGroup = xEventGroupCreate();xTaskCreate(vTempSensorTask, "Temp", 128, NULL, 1, NULL);xTaskCreate(vHumiditySensorTask, "Humi", 128, NULL, 1, NULL);xTaskCreate(vDataProcessorTask, "Proc", 128, NULL, 2, NULL);vTaskStartScheduler();return 0;
}
4.3 运行结果
- 温度任务每100ms设置BIT0。
- 湿度任务每200ms设置BIT1。
- 数据处理任务每200ms(当BIT0和BIT1均被设置时)打印一次数据。
5. 关键设计技巧
5.1 事件位复用
- 技巧:使用宏定义管理事件位:
#define TEMP_READY_BIT (1 << 0) #define NET_CONNECT_BIT (1 << 1)
5.2 原子操作优化
- 源码实现:FreeRTOS使用
taskENTER_CRITICAL()
和taskEXIT_CRITICAL()
保护事件位操作。
5.3 中断安全版本
- API:使用
xEventGroupSetBitsFromISR()
在中断中设置事件位:void ADC_ISR() {BaseType_t xHigherPriorityTaskWoken = pdFALSE;xEventGroupSetBitsFromISR(xEventGroup, BIT2, &xHigherPriorityTaskWoken);portYIELD_FROM_ISR(xHigherPriorityTaskWoken); }
6. 事件组 vs 信号量/队列
特性 | 事件组 | 信号量 |
---|---|---|
同步对象数量 | 最多32个独立事件 | 单一计数器 |
等待条件 | 按位与/或逻辑 | 计数器大于0 |
资源消耗 | 较高(维护任务链表) | 较低 |
适用场景 | 多条件复杂同步 | 单一资源管理 |
总结
FreeRTOS 事件组通过 位操作 和 任务链表管理 实现高效的多事件同步,其核心优势在于:
- 灵活的事件组合:支持按位与(
xWaitForAllBits=pdTRUE
)或按位或(xWaitForAllBits=pdFALSE
)触发。 - 低延迟唤醒:事件位更新后立即唤醒符合条件的任务。
- 中断安全:提供
FromISR
版本用于中断上下文。
在实际项目中,事件组常用于 多传感器数据融合、复合状态机触发 等需要多条件协作的场景。