FreeRTOS基础篇(七)——事件组和任务通知

前言:
   本篇文章介绍FreeRTOS的移植方法,以stm32f103举例。

1 事件组(Event Group)

1.1 介绍

  事件组是一种允许一个或多个任务等待多个“事件”中某些或全部发生的同步机制。它使用一个24位(STM32上通常为23位可用)的位图来表示不同的事件标志(bit flags),每个bit可代表一个特定事件。
  典型用途:

  • 等待多个条件中的任意一个发生(如:按键按下 OR 定时器超时)
  • 等待多个条件全部满足(如:网络连接 + 时间同步 + 配置加载完成)

1.2 API

  核心函数。

函数 功能
xEventGroupCreate() 创建事件组
xEventGroupSetBits() / xEventGroupSetBitsFromISR() 设置某个事件标志位(可在任务或中断中调用)
xEventGroupWaitBits() 等待指定的事件位被设置
xEventGroupClearBits() 清除某些事件位
vEventGroupDelete() 删除事件组

1.3 示例

  任务等待多个事件任一发生。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include "FreeRTOS.h"
#include "event_groups.h"

#define BIT_0 (1 << 0)
#define BIT_1 (1 << 1)

EventGroupHandle_t xEventGroup;

void vTask_A(void *pvParameters)
{
while(1)
{
// 等待 BIT_0 或 BIT_1 被设置,且收到后不清除(false),不等待超时(portMAX_DELAY)
EventBits_t uxBits = xEventGroupWaitBits(
xEventGroup,
BIT_0 | BIT_1, // 等待这两个位中的任意一个
pdTRUE, // 收到后自动清除这些位?
pdFALSE, // pdFALSE: 任一位满足即可;pdTRUE: 所有位都必须满足
portMAX_DELAY // 永久等待
);

if(uxBits & BIT_0)
{
printf("事件 BIT_0 发生\n");
}

if(uxBits & BIT_1)
{
printf("事件 BIT_1 发生\n");
}
}
}

// 中断或其他任务触发事件
void vSetEventFromISR(void)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xEventGroupSetBitsFromISR(xEventGroup, BIT_0, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

 &esmp;不适合传输大量数据,仅用于 状态/事件通知

2 任务通知(Task Notifications)

2.1 介绍

  任务通知是FreeRTOS提供的最快速的任务间通信机制。 每个任务都有一个内置的通知值( ulNotifiedValue )和通知状态 ,无需额外创建对象(如队列、信号量),直接通过API向目标任务发送通知。
  性能优势:

  • 比队列快 ~45%
  • 占用内存更少(不需要动态分配结构体)
  • 最多可替代二值信号量、计数信号量、事件组、简单消息传递

2.2 三种模式

模式 描述
eSetBits 将通知值按位或操作(类似事件组)
eIncrement 递增通知值(类似释放信号量)
eSetValueWithOverwrite 直接设置通知值(覆盖旧值

2.3 API

函数 功能
xTaskNotifyGive(xTaskToNotify) 递增目标任务的通知值(等价于释放一个信号量)
ulTaskNotifyTake(pdTRUE, timeout) 获取通知值并清零或减一(类似 xSemaphoreTake()
ulTaskNotifyValueClear(xTask, ulBitsToClear) 清除通知值中的某些位
xTaskNotify(xTaskToNotify, ulValue, eAction) 发送通知,支持多种操作模式
xTaskNotifyWait(ulBitsToClearOnEntry, ulBitsToClearOnExit, pulNotificationValue, xTicksToWait) 等待通知到达

所有函数都 无需额外通信对象 ,直接通过任务句柄操作。

2.4 示例

  用任务通知替代二值信号量。假设有一个中断需要唤醒某个任务处理数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
TaskHandle_t xHandler = NULL;

// 任务:等待通知
void vWaitingTask(void *pvParameters)
{
while(1)
{
// 等待通知(进入时不清除任何位,退出时清除全部)
uint32_t ulNotifiedValue;
if (xTaskNotifyWait(0, 0xFFFFFFFF, &ulNotifiedValue, portMAX_DELAY) == pdPASS)
{
printf("任务被唤醒,通知值: %lu\n", ulNotifiedValue);
// 处理事件...
}
}
}

// 中断服务程序(例如 UART 接收完成)
void USART1_IRQHandler(void)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;

// 清中断标志等操作...
char c = USART1->DR;

// 直接通知任务(使用 eNoAction 表示只唤醒,不修改值)
xTaskNotifyFromISR(xHandler, 0, eNoAction, &xHigherPriorityTaskWoken); // 通过任务句柄通知任务。

portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

// 启动任务
int main()
{
xTaskCreate(vWaitingTask, "WaitTask", 1000, NULL, 2, &xHandler); // 绑定任务和句柄。
vTaskStartScheduler();
}

 &esmp;模拟计数信号量(Counting Semaphore)。
  使用 eIncrement 模式可以实现类似 xSemaphoreGiveFromISR() 的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void vProducerISR(void)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xTaskNotifyFromISR(xWorkerTask, 0, eIncrement, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

void vConsumerTask(void *pvParameters)
{
while(1)
{
// 等待通知(相当于take)
ulTaskNotifyTake(pdTRUE, portMAX_DELAY); // pdTRUE: 返回前将通知值减1,通知值通过函数返回值获取。
printf("处理一个工作项\n");
}
}

  再来一个例子,一个任务(SenderTask)通过 xTaskNotify() 发送一个数值。另一个任务(ReceiverTask)通过 ulTaskNotifyTake() 接收并处理这个值。没有使用任何硬件中断,纯软件任务间通信。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 任务句柄
TaskHandle_t xReceiverTask = NULL;

// 发送任务
void vSenderTask(void *pvParameters)
{
uint32_t ulNotificationValue = 0;
const TickType_t xDelay = pdMS_TO_TICKS(1000); // 每秒发送一次

while(1)
{
// 准备发送的通知值(例如:递增的计数)
ulNotificationValue++;

printf("Sender: Sending notification value %lu to Receiver.\n", ulNotificationValue);

// 发送通知值给接收任务
// eAction = eSetValueWithOverwrite 表示直接设置值(覆盖旧值)
xTaskNotify(xReceiverTask, ulNotificationValue, eSetValueWithOverwrite);

// 延迟1秒
vTaskDelay(xDelay);
}
}

// 接收任务
void vReceiverTask(void *pvParameters)
{
uint32_t ulReceivedValue = 0;

while(1)
{
// 等待任务通知(pdTRUE 表示获取后清零计数)
ulReceivedValue = ulTaskNotifyTake(pdTRUE, portMAX_DELAY); // 阻塞等待

printf("Receiver: Received notification value %lu.\n", ulReceivedValue);
}
}

// 主函数(假设在支持FreeRTOS的平台如STM32、ESP32或模拟环境)
int main(void)
{
// 创建接收任务(先创建,获取句柄)
xTaskCreate(vReceiverTask, "Receiver", configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY + 2, &xReceiverTask);
// 创建发送任务
xTaskCreate(vSenderTask, "Sender", configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY + 1, NULL);
}

  这段代码实现了两个FreeRTOS任务通过任务通知进行通信的简单示例:vSenderTask 每秒递增一个数值并使用 xTaskNotify 将其发送给 vReceiverTask,接收任务通过 ulTaskNotifyTake 阻塞等待并获取该值,从而实现无需队列或信号量的高效任务间数据传递,整个过程不涉及中断。