(七))
一、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串口接收事件 bit1CAN 接收事件 bit2ADC 采样完成事件 bit3Flash 操作完成事件任务可以通过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. xClearOnExitxClearOnExit表示当等待条件满足并且函数返回时是否自动清除对应事件位。如果设置为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. xWaitForAllBitsxWaitForAllBits用来决定等待条件是逻辑或还是逻辑与。如果设置为xWaitForAllBits pdFALSE;表示uxBitsToWaitFor中任意一个 bit 被置位等待条件就满足。如果设置为xWaitForAllBits pdTRUE;表示必须所有指定 bit 都被置位等待条件才满足。5. xTicksToWaitxTicksToWait表示当等待条件不满足时任务最多阻塞等待多少 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指定等待哪些 bitxClearOnExit决定返回时是否清除事件位xWaitForAllBits决定等待任意 bit 还是所有 bitxTicksToWait决定最大阻塞时间。函数返回值EventBits_t可以用来判断具体哪些事件发生。软件定时器是 FreeRTOS 基于系统 Tick 和 Timer Service Task 实现的定时机制适合低精度定时任务。单次定时器到期后执行一次回调并停止但不会自动删除周期定时器到期后会自动重新装载并继续运行。软件定时器回调函数中不应执行耗时或长时间阻塞的操作。更好的工程设计是回调函数中只设置事件位或通知任务真正的数据处理、Flash 操作、协议处理等耗时工作放到任务中完成。一句话总结EventGroup 用来等多个事件条件软件定时器用来做低精度超时和周期触发两者配合可以很好地实现任务同步和超时控制。