目录
一、事件标志组(Event Groups)
1、事件标志组的特点
2、事件标志组与队列、信号量的区别
3、关键API函数
4、示例代码
5、优缺点
6、总结
二、任务通知(Task Notifications)
1、任务通知的特点
2、关键API函数
3、使用场景
模拟二值信号量
模拟计数型信号量
模拟消息邮箱
4、优缺点
5、小结
三、总结
在FreeRTOS中,事件标志组(Event Groups)和任务通知(Task Notifications)是用于任务间同步和通信的重要机制。它们可以有效地帮助多个任务进行事件响应和通知,从而实现不同任务之间的协作
一、事件标志组(Event Groups)
事件标志组是一个非常强大的同步工具,它允许任务使用标志位的组合来表示多个事件的状态。每个任务可以检查一个或多个事件标志位,从而决定是否继续执行。事件标志组是一种位掩码机制,任务可以使用位操作来设置、清除或检查这些标志。
事件标志组是一种用于任务间同步的机制。它是由多个事件标志组成的一个集合,可以通过设置、清除或等待特定事件标志位来控制任务的执行流。每个事件标志组的每一位标志可以表示一个具体的事件状态,用户可以通过位操作来管理这些事件。
简单来说,事件标志组就像一个整数(无符号的 16 位或 32 位整数),每一位用来表示一个事件的状态。例如,某个任务可能会根据某些事件的发生情况来执行,而这些事件的状态通过事件标志组的各个位进行管理。
1、事件标志组的特点
-
每个位表示一个事件:每一位的状态(0 或 1)表示一个事件是否发生。比如,
bit0
可能表示按键是否按下,bit1
可能表示是否收到了数据包,等等。 -
事件标志的存储:事件标志组内部使用 16 位或 32 位无符号整数来存储事件标志。在 STM32 中,通常是 32 位无符号整数。事件标志的高 8 位用于控制事件标志组的管理信息,低 24 位用于存储具体的事件标志。因此,一个事件标志组最多可以存储 24 个事件标志。
-
支持任务和中断操作:事件标志组不仅可以在任务中使用,还支持中断处理程序(ISR)操作。
-
等待某个事件或多个事件:任务可以选择等待单个事件的发生,或者等待多个事件的同时发生。并且,任务在等待某个事件的过程中,可以选择是否在事件被清除后继续等待。
-
事件标志的操作:可以设置、清除或等待事件标志,这些操作可以是独立的,也可以是组合的。
-
事件的“广播”作用:与信号量或队列不同,事件标志组可以在事件发生时唤醒多个任务,满足多个任务的条件。
2、事件标志组与队列、信号量的区别
队列和信号量:通常情况下,事件发生时只会唤醒一个任务。队列的数据被读取后就消失,信号量在被获取后会减少。
事件标志组:事件发生时,会唤醒所有符合条件的任务,相当于具有“广播”功能。被唤醒的任务有两个选择:要么继续等待事件,要么清除事件标志,避免任务再次被唤醒。
3、关键API函数
创建一个事件标志组:
xEventGroupCreate()
设置事件标志组中的某些位:
EventBits_t xEventGroupSetBits(
EventGroupHandle_t xEventGroup, // 事件标志组句柄
const EventBits_t uxBitsToSet // 需要设置的事件标志位
);该函数用于设置指定的事件标志位。如果事件发生,指定的标志位将被设置为 1,通知等待该事件的任务。
清除事件标志组中的某些位:
xEventGroupClearBits()
等待某些事件标志位:
EventBits_t xEventGroupWaitBits(
EventGroupHandle_t xEventGroup, // 事件标志组句柄
const EventBits_t uxBitsToWaitFor, // 需要等待的事件标志
const BaseType_t xClearOnExit, // 退出时是否清除标志
const BaseType_t xWaitForAllBits, // 是否等待所有标志位成立
TickType_t xTicksToWait // 等待超时时间
);
该函数用于等待指定的事件标志位。如果xWaitForAllBits
为pdTRUE
,则任务会等待所有指定的标志位,否则只等待其中任何一个标志位。如果xClearOnExit
为pdTRUE
,则在事件满足条件后清除标志位。
同步两个或更多的任务的事件标志:
EventBits_t xEventGroupSync(
EventGroupHandle_t xEventGroup, // 事件标志组句柄
const EventBits_t uxBitsToSet, // 需要设置的标志位
const EventBits_t uxBitsToWaitFor, // 需要等待的标志位
TickType_t xTicksToWait // 等待超时时间
);
该函数用于实现同步操作,它将同时设置指定的事件标志,并等待另一个任务设置指定的标志位。
使用场景:
事件驱动的任务调度:任务根据事件标志位的状态来执行。
任务同步:多个任务需要在某些条件下同时执行。
4、示例代码
#include "FreeRTOS.h"
#include "task.h"
#include "event_groups.h"
// 事件标志组句柄
EventGroupHandle_t xEventGroup;
// 定义标志位
#define BIT_0 ( 1 << 0 )
#define BIT_1 ( 1 << 1 )
// 任务1
void Task1(void *pvParameters) {
while (1) {
// 设置 BIT_0 标志位
xEventGroupSetBits(xEventGroup, BIT_0);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
// 任务2
void Task2(void *pvParameters) {
while (1) {
// 等待 BIT_0 被设置
EventBits_t uxBits = xEventGroupWaitBits(xEventGroup, BIT_0, pdTRUE, pdFALSE, portMAX_DELAY);
if (uxBits & BIT_0) {
// 响应事件
printf("Task2 received BIT_0 event\n");
}
}
}
int main(void) {
// 创建事件标志组
xEventGroup = xEventGroupCreate();
// 创建任务
xTaskCreate(Task1, "Task1", 128, NULL, 2, NULL);
xTaskCreate(Task2, "Task2", 128, NULL, 1, NULL);
// 启动调度器
vTaskStartScheduler();
for (;;);
}
解释:
xEventGroupSetBits(xEventGroup, BIT_0)
:任务1通过设置事件标志组中的BIT_0
来通知任务2。
xEventGroupWaitBits(xEventGroup, BIT_0, pdTRUE, pdFALSE, portMAX_DELAY)
:任务2等待BIT_0
标志位被设置,一旦设置,它会执行响应操作。
5、优缺点
优点:
事件标志组允许一个或多个任务等待不同的事件,并且可以组合多个标志位,支持灵活的同步机制。可减少不必要的任务切换,提高系统效率。
缺点:
可能会引入较大的内存开销,尤其是当事件标志组的位数较多时。
6、总结
事件标志组是一个强大的同步工具,适合多个任务需要根据不同事件状态来执行的场景。它提供了位操作、事件等待、事件清除等功能,能够灵活地管理任务间的同步。相比队列和信号量,事件标志组具有“广播”能力,能够同时唤醒多个任务,因此适合需要多个任务响应的场景。使用事件标志组时,开发者可以选择任务在事件发生后是否清除事件标志,确保任务的灵活性。总之,事件标志组是FreeRTOS中非常重要的同步工具,它为任务间通信提供了一种高效且灵活的方式。
二、任务通知(Task Notifications)
任务通知是另一种高效的任务间通信机制,它允许一个任务向另一个任务发送“通知”。与事件标志组不同,任务通知是基于一个简单的计数器或标志位的机制,它能够非常快速地通知任务发生了某个事件。在FreeRTOS中,任务通知是任务间通信的一种轻量级方式,可以用于同步和传递简单的状态信息。任务通知的实现通过任务控制块(TCB)中的
ulNotifiedValue
和ucNotifyState
成员来管理通知的值和状态。
1、任务通知的特点
-
不覆盖接收任务的通知值:可以选择更新通知值的部分位,而不是完全覆盖。
-
覆盖接收任务的通知值:可以选择完全覆盖接收任务的通知值。
-
更新通知值的某些位:类似于事件标志组,可以只更新通知值中的某些特定位。
-
增加接收任务的通知值:模拟计数信号量的行为,允许通知值递增。
-
类似队列、事件标志组和信号量:任务通知可以模拟二值信号量、计数型信号量、消息邮箱、事件标志组等多种功能。
2、关键API函数
xTaskNotifyGive()
:发送通知给任务。
xTaskNotifyWait()
:等待通知。
xTaskNotify()
:通知一个任务,并可以附加一个值。
xTaskNotifyAndQuery()
:向任务发送通知,并带上通知值,同时查询任务的当前通知值。
ulTaskNotifyTake()
:接收任务的通知值,适用于信号量的模拟。接收到通知后,通知值会被清零。
3、使用场景
轻量级任务间通信:任务通知通常用于需要简单同步或触发任务执行的场景。
不需要复杂状态控制的任务间同步:如果你只需要通知另一个任务某个事件发生,任务通知提供了一个非常快速和高效的机制。
模拟二值信号量
二值信号量通常用于同步任务,确保一个任务在执行时,另一个任务处于等待状态。在FreeRTOS中,任务通知可以用来模拟二值信号量。实现过程如下:
任务1:调用 xTaskNotifyGive()
向任务2发送通知,模拟释放信号量。
任务2:调用 ulTaskNotifyTake()
来等待信号量的获取,若信号量可用,则继续执行。
void Task1(void *pvParameters) {
while (1) {
// 模拟释放二值信号量
xTaskNotifyGive(xTask2Handle);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void Task2(void *pvParameters) {
while (1) {
// 模拟获取二值信号量
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
// 执行任务2
}
}
模拟计数型信号量
计数型信号量允许多个任务共享资源。任务通知可以通过增加通知值来模拟计数信号量的行为。例如:
任务1:通过 xTaskNotifyGive()
向任务2发送通知,模拟计数信号量的增加。
任务2:通过 ulTaskNotifyTake()
等函数获取信号量。
void Task1(void *pvParameters) {
while (1) {
// 模拟计数型信号量的增加
xTaskNotifyGive(xTask2Handle);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void Task2(void *pvParameters) {
while (1) {
// 获取计数信号量
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
// 执行任务2
}
}
模拟消息邮箱
任务通知也可以用来模拟消息邮箱。通过向任务发送带有数据的通知值,任务可以接收数据并进行处理。例如:
任务1:发送带有数据的通知(例如通过 xTaskNotify()
)。
任务2:接收通知并处理数据。
void Task1(void *pvParameters) {
while (1) {
uint32_t message = 42; // 模拟消息
xTaskNotify(xTask2Handle, message, eSetValueWithOverwrite);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void Task2(void *pvParameters) {
while (1) {
uint32_t receivedMessage;
xTaskNotifyWait(0, 0, &receivedMessage, portMAX_DELAY);
// 处理接收到的消息
}
}
4、优缺点
优点:
比较轻量级,开销小,适用于频繁的任务通知。不需要像事件标志组那样维护多个标志位,易于理解和实现。
缺点:
只能用来通知任务,适用场景相对简单,无法像事件标志组那样处理复杂的同步需求。
无法向ISR发送通知:任务通知不能直接从中断服务程序(ISR)中发送。
无法广播给多个任务:任务通知只能通知一个任务,不能像事件标志组那样同时通知多个任务。
无法缓存多个数据:任务通知只保存一个值,无法像队列一样缓存多个数据。
发送受阻不支持阻塞:任务通知的发送不会被阻塞,发送方不能像信号量一样等待资源。
5、小结
任务通知是FreeRTOS中一种非常高效的任务间通信机制。它的优势在于:
低开销:任务通知操作非常高效,内存占用小。
灵活性:可以模拟二值信号量、计数型信号量、消息邮箱和事件标志组等功能。
然而,它也有一些局限性:
无法广播给多个任务:任务通知只能通知一个任务。
无法缓存多个数据:每个任务通知只能携带一个值,不像队列可以缓存多个数据。
综上所述,任务通知是一个非常适合在高效同步和传递简单状态信息的场景中使用的工具,特别是在资源有限的嵌入式系统中。
三、总结
事件标志组:
用于多个任务间的事件同步,可以同时检查多个标志位。
适合需要更复杂事件组合和同步的场景。
支持位操作和多标志组合,灵活性较强。
任务通知:
用于任务间的轻量级通知,适用于简单的同步或触发任务。
速度快,资源占用小,适合高频率的通知场景。
不适合复杂的状态同步和多事件组合。
两者的选择应该根据实际的应用需求来决定。如果是较为复杂的同步需求,事件标志组更为合适;如果仅需要简单的任务通知,则任务通知更为高效。