一、EventGroup 事件组
EventGroup 是 FreeRTOS 中用于任务同步的一种机制。事件组本质上是一组事件标志位,每一个 bit 可以表示一个事件是否发生。
例如:
#define EVENT_UART_RX (1UL << 0) #define EVENT_CAN_RX (1UL << 1) #define EVENT_ADC_DONE (1UL << 2) #define EVENT_FLASH_OK (1UL << 3)这几个 bit 可以放在同一个 EventGroup 中,用来表示不同事件:
bit0:串口接收事件 bit1:CAN 接收事件 bit2:ADC 采样完成事件 bit3:Flash 操作完成事件任务可以通过xEventGroupWaitBits()等待一个或多个事件位。
当配置为等待任意 bit 时,只要任务关心的事件中有任意一个发生,任务就会被唤醒,这相当于逻辑或。
当配置为等待所有 bit 时,只有任务关心的所有事件都发生后,任务才会被唤醒,这相当于逻辑与。
EventGroup 的事件位具有保持作用,不要求多个事件在同一时刻发生。例如 bit0 先被置位,bit1 后被置位,只要两个 bit 最终都满足,等待所有 bit 的任务就可以被唤醒。
需要注意的是,EventGroup 只表示事件状态,不传递具体数据。如果需要传递数据内容,应使用队列;如果只是中断一对一通知任务,可以使用任务通知;如果是保护共享资源,应使用 Mutex。
另外,EventGroup 并不是所有 bit 都可以给用户使用。FreeRTOS 内部会占用高 8 位。通常情况下:
EventBits_t 为 32 位时,用户一般可用 24 个事件 bit EventBits_t 为 16 位时,用户一般可用 8 个事件 bit所以实际工程中建议从 bit0 开始依次定义事件位,不要随意使用高位 bit。
二、EventGroup 创建
使用事件组前,需要先创建 EventGroup。
#include "FreeRTOS.h" #include "event_groups.h" #define EVENT_UART_RX (1UL << 0) #define EVENT_CAN_RX (1UL << 1) #define EVENT_ADC_DONE (1UL << 2) static EventGroupHandle_t g_event_group = NULL; void App_EventGroupInit(void) { g_event_group = xEventGroupCreate(); if(g_event_group == NULL) { Error_Handler(); } }xEventGroupCreate()创建成功后,会返回一个事件组句柄。后续设置事件位、等待事件位,都需要使用这个句柄。
三、等待任意一个事件
下面这个例子表示:任务等待 UART 或 CAN 任意一个事件发生。
#define EVENT_UART_RX (1UL << 0) #define EVENT_CAN_RX (1UL << 1) extern EventGroupHandle_t g_event_group; void CommTask(void *argument) { EventBits_t bits; while(1) { bits = xEventGroupWaitBits(g_event_group, EVENT_UART_RX | EVENT_CAN_RX, pdTRUE, // 退出时清除已满足的 bit pdFALSE, // 等任意一个 bit portMAX_DELAY); if(bits & EVENT_UART_RX) { // 处理串口接收事件 } if(bits & EVENT_CAN_RX) { // 处理 CAN 接收事件 } } }这里:
xWaitForAllBits = pdFALSE;表示等待任意一个 bit。
只要EVENT_UART_RX或EVENT_CAN_RX任意一个事件发生,CommTask就会被唤醒。
四、等待所有事件都发生
下面这个例子表示:任务需要等待文件名正确、Flash 擦除完成、CRC 校验正确,三个条件全部满足后才继续执行。
#define EVENT_FILE_NAME_OK (1UL << 0) #define EVENT_FLASH_ERASE_OK (1UL << 1) #define EVENT_CRC_OK (1UL << 2) #define EVENT_UPGRADE_ALL_OK (EVENT_FILE_NAME_OK | \ EVENT_FLASH_ERASE_OK | \ EVENT_CRC_OK) extern EventGroupHandle_t g_event_group; void UpgradeTask(void *argument) { EventBits_t bits; while(1) { bits = xEventGroupWaitBits(g_event_group, EVENT_UPGRADE_ALL_OK, pdTRUE, // 退出时清除 pdTRUE, // 等所有 bit portMAX_DELAY); if((bits & EVENT_UPGRADE_ALL_OK) == EVENT_UPGRADE_ALL_OK) { // 文件名正确、擦除完成、CRC 正确 // 可以进入下一步升级流程 } } }这里:
xWaitForAllBits = pdTRUE;表示必须所有指定 bit 都被置位,等待条件才成立。
这类用法适合:
多个条件最终都要满足 但这些条件不一定在同一时刻发生五、xEventGroupWaitBits() 函数说明
函数原型:
EventBits_t xEventGroupWaitBits( const EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToWaitFor, const BaseType_t xClearOnExit, const BaseType_t xWaitForAllBits, TickType_t xTicksToWait );这个函数用于等待事件组中的一个或多个事件位。
1. xEventGroup
事件组句柄,由xEventGroupCreate()创建。
2. uxBitsToWaitFor
指定任务要等待哪些 bit,也就是任务关心哪些事件。
例如:
EVENT_UART_RX | EVENT_CAN_RX表示任务关心 UART 接收事件和 CAN 接收事件。
3. xClearOnExit
xClearOnExit表示当等待条件满足并且函数返回时,是否自动清除对应事件位。
如果设置为:
xClearOnExit = pdTRUE;表示函数因为事件满足而返回前,会清除uxBitsToWaitFor中已经满足的事件位。
如果设置为:
xClearOnExit = pdFALSE;表示函数返回时不改变事件组中的 bit。
例如:
xEventGroupWaitBits(g_event_group, EVENT_UART_RX, pdTRUE, pdFALSE, pdMS_TO_TICKS(100));如果 100ms 内EVENT_UART_RX发生了,函数返回前会清除EVENT_UART_RX。
如果 100ms 到了还没发生,函数超时返回,不会因为xClearOnExit去清除EVENT_UART_RX。
使用xClearOnExit = pdTRUE时要注意一个问题:
xEventGroupWaitBits(g_event_group, EVENT_UART_RX | EVENT_CAN_RX, pdTRUE, pdFALSE, portMAX_DELAY);这表示等待 UART 或 CAN 任意一个事件。
如果 UART 和 CAN 两个 bit 都已经置位,那么函数返回前可能会把两个 bit 都清除。所以必须根据返回值bits及时判断并处理对应事件。
如果希望更可控,也可以设置:
xClearOnExit = pdFALSE;然后处理完事件后手动清除:
xEventGroupClearBits(g_event_group, EVENT_UART_RX);4. xWaitForAllBits
xWaitForAllBits用来决定等待条件是逻辑或还是逻辑与。
如果设置为:
xWaitForAllBits = pdFALSE;表示uxBitsToWaitFor中任意一个 bit 被置位,等待条件就满足。
如果设置为:
xWaitForAllBits = pdTRUE;表示必须所有指定 bit 都被置位,等待条件才满足。
5. xTicksToWait
xTicksToWait表示当等待条件不满足时,任务最多阻塞等待多少 tick。
例如:
0表示不等待,立即检查事件位,条件不满足就立即返回。
pdMS_TO_TICKS(100)表示最多等待 100ms。
portMAX_DELAY通常表示一直等待,直到条件满足。
6. 返回值
xEventGroupWaitBits()的返回值类型是:
EventBits_t返回值表示函数返回时事件组中的 bit 状态。使用时应该根据返回值判断具体是哪一个事件发生。
例如:
EventBits_t bits; bits = xEventGroupWaitBits(g_event_group, EVENT_UART_RX | EVENT_CAN_RX, pdTRUE, pdFALSE, portMAX_DELAY); if(bits & EVENT_UART_RX) { // UART 事件发生 } if(bits & EVENT_CAN_RX) { // CAN 事件发生 }即使xClearOnExit设置为pdTRUE,返回值bits仍然可以用来判断本次是哪些事件满足了条件。
六、软件定时器
软件定时器是 FreeRTOS 提供的一种定时机制,它基于系统 Tick 和 Timer Service Task 实现,不需要每个定时功能都占用一个硬件定时器。
软件定时器创建后,启动定时器。当经过设定的 tick 数后,FreeRTOS 会在 Timer Service Task 中调用用户注册的回调函数。
软件定时器的定时精度与系统 Tick 周期有关,同时也会受到任务调度、Timer Service Task 优先级和系统负载影响。
软件定时器适合低精度定时任务,例如:
LED 周期闪烁 通信超时检测 按键消抖 状态机超时 心跳包发送不适合高精度实时控制,例如:
PWM 输出 ADC 精确定时采样 电机电流环 微秒级延时七、软件定时器相关配置
使用 FreeRTOS 软件定时器,需要在FreeRTOSConfig.h中打开相关配置。
常见配置如下:
#define configUSE_TIMERS 1 #define configTIMER_TASK_PRIORITY 2 #define configTIMER_QUEUE_LENGTH 10 #define configTIMER_TASK_STACK_DEPTH 256 #define configTICK_RATE_HZ 1000含义:
configUSE_TIMERS: 是否启用软件定时器。 configTIMER_TASK_PRIORITY: Timer Service Task 的优先级。 configTIMER_QUEUE_LENGTH: 软件定时器命令队列长度。 configTIMER_TASK_STACK_DEPTH: Timer Service Task 的栈大小。 configTICK_RATE_HZ: 系统 Tick 频率。例如:
#define configTICK_RATE_HZ 1000表示 1 秒产生 1000 个 Tick,也就是:
1 tick = 1ms实际工程中,通常使用pdMS_TO_TICKS()把毫秒转换为 tick。
八、单次定时器
单次定时器启动后,定时时间到达时只执行一次回调函数。回调执行完后,定时器停止,但不会自动删除。
如果需要再次使用,可以重新调用:
xTimerStart();或者:
xTimerReset();如果不再使用,需要调用:
xTimerDelete();示例代码:
#include "FreeRTOS.h" #include "task.h" #include "timers.h" static TimerHandle_t g_one_shot_timer = NULL; static void OneShotTimerCallback(TimerHandle_t xTimer) { (void)xTimer; /* 单次定时器到期后执行一次这里。 回调执行完后,定时器停止,但不会自动删除。 注意:不要在软件定时器回调中写耗时操作。 */ LED_Toggle(); } void App_OneShotTimerCreate(void) { g_one_shot_timer = xTimerCreate("OneShot", pdMS_TO_TICKS(1000), pdFALSE, NULL, OneShotTimerCallback); if(g_one_shot_timer == NULL) { Error_Handler(); } if(xTimerStart(g_one_shot_timer, 0) != pdPASS) { Error_Handler(); } }这个例子表示:
启动后 1000ms 执行一次 OneShotTimerCallback() 执行完后定时器停止 定时器对象不会自动删除如果想再次启动:
xTimerStart(g_one_shot_timer, 0);如果不再使用:
xTimerDelete(g_one_shot_timer, 0);九、周期定时器
周期定时器启动后,会按照设置的周期反复执行回调函数,直到用户调用xTimerStop()停止,或者调用xTimerDelete()删除。
示例代码:
#include "FreeRTOS.h" #include "task.h" #include "timers.h" static TimerHandle_t g_period_timer = NULL; static void PeriodTimerCallback(TimerHandle_t xTimer) { (void)xTimer; /* 周期执行。 注意:不要在这里写耗时操作。 */ LED_Toggle(); } void App_PeriodTimerCreate(void) { g_period_timer = xTimerCreate("PeriodTimer", pdMS_TO_TICKS(500), pdTRUE, NULL, PeriodTimerCallback); if(g_period_timer == NULL) { Error_Handler(); } if(xTimerStart(g_period_timer, 0) != pdPASS) { Error_Handler(); } }这个例子表示:
每 500ms 执行一次 PeriodTimerCallback() 直到用户调用 xTimerStop() 或 xTimerDelete()停止周期定时器:
xTimerStop(g_period_timer, 0);删除周期定时器:
xTimerDelete(g_period_timer, 0);十、软件定时器回调注意事项
软件定时器的回调函数不是在硬件中断中执行,而是在 FreeRTOS 的 Timer Service Task 中执行。
因此,软件定时器回调函数中不要执行耗时操作,也不要长时间阻塞。
不建议在回调函数中做这些事情:
Flash 擦除 大量数据处理 长时间 while 循环 等待队列、信号量、事件组 调用长时间阻塞的 API因为所有软件定时器回调都由同一个 Timer Service Task 调用。如果某个回调函数执行时间过长,会影响其他软件定时器的回调执行。
比较推荐的做法是:
软件定时器回调中只做简单标志置位或通知任务 真正耗时的处理放到任务中完成例如:
static void TimeoutCallback(TimerHandle_t xTimer) { (void)xTimer; xEventGroupSetBits(g_event_group, EVENT_TIMEOUT); }然后在任务中处理超时事件。
十一、EventGroup 和软件定时器的典型组合
在工程中,EventGroup 和软件定时器经常配合使用。
例如 IAP 升级超时检测:
收到升级开始命令 ↓ 启动单次软件定时器 ↓ 等待升级数据帧 ↓ 每收到一帧合法数据,重置软件定时器 ↓ 如果长时间没有收到下一帧,软件定时器超时 ↓ 定时器回调设置 EVENT_TIMEOUT 事件位 ↓ UpgradeTask 被唤醒,进入超时错误处理这种设计的好处是:
软件定时器负责判断是否超时 EventGroup 负责通知任务发生了超时事件 任务负责真正的错误处理定时器回调中只设置事件位,不做复杂处理。
十二、总结
EventGroup 适合处理多个事件条件的同步。每一个 bit 可以表示一个事件,任务可以等待任意一个 bit,也可以等待所有指定 bit。EventGroup 只表示事件状态,不传递具体数据。
xEventGroupWaitBits()是等待事件位的核心 API。其中uxBitsToWaitFor指定等待哪些 bit,xClearOnExit决定返回时是否清除事件位,xWaitForAllBits决定等待任意 bit 还是所有 bit,xTicksToWait决定最大阻塞时间。函数返回值EventBits_t可以用来判断具体哪些事件发生。
软件定时器是 FreeRTOS 基于系统 Tick 和 Timer Service Task 实现的定时机制,适合低精度定时任务。单次定时器到期后执行一次回调并停止,但不会自动删除;周期定时器到期后会自动重新装载并继续运行。
软件定时器回调函数中不应执行耗时或长时间阻塞的操作。更好的工程设计是:回调函数中只设置事件位或通知任务,真正的数据处理、Flash 操作、协议处理等耗时工作放到任务中完成。
一句话总结:
EventGroup 用来等多个事件条件,软件定时器用来做低精度超时和周期触发,两者配合可以很好地实现任务同步和超时控制。