本专栏将对FreeRTOS进行快速讲解,带你了解并使用FreeRTOS的各部分内容。适用于快速了解FreeRTOS并进行开发、突击面试、对新手小白非常友好。期待您的后续关注和订阅!
目录
任务通知是什么?
在FreeRTOS中,任务通知是一种任务间的通信机制,用于通知一个任务某个事件发生了。为了更好地理解任务通知,我们可以将其比喻成日常生活中的一些场景。
1 任务通知定义
1.1 定义
任务通知是用来通知任务的。任务控制块(Task Control Block, TCB)中的结构体成员变量 ulNotifiedValue
就是这个通知值。当任务需要接收通知时,它会等待这个通知值的变化。与使用队列、信号量、事件标志组不同,使用任务通知不需要额外创建一个结构体,通过任务控制块内置的成员变量即可实现直接通信。
1.2 通知值
任务都有一个控制块(Task Control Block, TCB),其中包含一个通知值ulNotifiedValue
。这个通知值是一个32位无符号整数,用于任务间通信时传递数据或状态。
1.3 通知状态
每个任务都有一个通知状态ucNotifyState
,用于表示当前通知的状态。通知状态有三种:
taskNOT_WAITING_NOTIFICATION
:任务未等待通知。taskWAITING_NOTIFICATION
:任务在等待通知。taskNOTIFICATION_RECEIVED
:任务已接收到通知。
#define taskNOT_WAITING_NOTIFICATION ( ( uint8_t ) 0 ) // 任务未等待通知
#define taskWAITING_NOTIFICATION ( ( uint8_t ) 1 ) // 任务在等待通知
#define taskNOTIFICATION_RECEIVED ( ( uint8_t ) 2 ) // 任务已接收到通知
1.4 举例示范
假设有一个办公室,里面有一个老板和一个员工。老板需要通知员工去做某件事。老板可以通过多种方式通知员工,比如发电子邮件、发短信或者打电话。在FreeRTOS中,任务通知就像老板发电子邮件、短信或者打电话来通知员工去做事情一样。
- 任务通知:老板通过发通知(比如电子邮件)告诉员工去做一件事情。
- 任务接收通知:员工收到通知后去执行任务。
2 任务通知优缺点
2.1 优势
优点 | 描述 |
---|---|
高效性 | 速度快:任务通知是FreeRTOS中最快的任务间通信机制,直接在任务控制块(TCB)中操作,无需复杂的数据结构管理。 内存开销小:任务通知不需要额外的内存分配和管理,只使用任务控制块中的成员变量,极大地减少了内存开销。 |
简单性 | API简单易用:任务通知API函数设计简单,易于理解和使用。包括发送通知和接收通知两类函数,使用起来非常方便。 无需额外结构:与队列、信号量、事件标志组不同,任务通知不需要额外创建和管理数据结构,简化了开发过程。 |
灵活性 | 多种更新方式:任务通知值可以通过多种方式更新,如覆盖、不覆盖、设置某个位或增加通知值。开发者可以根据具体需求选择合适的通知方式。 多用途:任务通知可以模拟信号量、消息邮箱和事件标志组,灵活应用于不同的任务同步和通信场景。 |
实时系统 | 低延迟:由于任务通知的高效性和简单性,使用任务通知进行任务间通信和同步可以实现低延迟,有助于提高系统的实时性能。 |
2.2 劣势
缺点 | 描述 |
---|---|
通信单一性 | 单一接收者:任务通知只能发送给一个特定的任务,无法广播给多个任务。这限制了其在需要一对多通信场景中的应用。 |
数据传递有限 | 无法缓存多个数据:任务通知只能保存一个通知值,无法像队列那样缓存多个数据。这意味着最新的通知值会覆盖之前的通知值。 无法发送复杂数据:任务通知主要用于发送简单的数值或标志,无法传递复杂的数据结构。 |
中断限制 | 无法发送数据给ISR:任务通知无法直接用于中断服务例程(ISR)之间的通信,但ISR可以使用任务通知发送数据给任务。ISR没有任务控制块,所以无法接收任务通知。 |
阻塞限制 | 发送方无法阻塞等待:在任务通知机制中,发送方无法进入阻塞状态等待接收方处理通知。这在某些需要发送方等待接收方处理完毕的场景中是一个限制。 |
3 特点及更新方式
3.1 特点
任务通知是FreeRTOS中用于任务间通信和同步的一种高效机制,具有以下特点:
特点 | 描述 | 类似机制 |
---|---|---|
不覆盖接收任务的通知值 | 任务通知值可以像队列一样,不覆盖接收任务的现有通知值。 | 类似队列 |
覆盖接收任务的通知值 | 任务通知值可以像队列一样,覆盖接收任务的现有通知值,确保只有最新的通知值保留。 | 类似队列 |
更新接收任务通知值的一个或多个bit | 任务通知值的特定位可以被设置或清除,类似于事件标志组的操作,方便处理多种状态。 | 类似事件标志组 |
增加接收任务的通知值 | 任务通知值可以累加,类似于信号量的计数功能,方便进行计数操作。 | 类似信号量 |
因此只要合理、灵活地利用任务通知的特点,可以在某些场合中替代队列、信号量和事件标志组,实现高效的任务间通信和同步。通过灵活利用任务通知,可以在一些场合中替代队列、信号量和事件标志组,以简化代码,提高运行效率。
3.2 更新方式
-
计数值(数值累加,类似信号量):
- 描述:通知值在原基础上进行累加,适用于计数型信号量的实现。
- 应用场景:用于计数、累加操作,如计数信号量。
-
相应位置一(类似事件标志组):
- 描述:更新通知值的特定位,将相应位置一。类似于事件标志组,用于标识多个状态。
- 应用场景:用于事件标志管理和多状态标识。
-
任意数值(支持覆盖和不覆盖,类似队列):
- 描述:通知值可以被新的值覆盖,也可以选择不覆盖。类似于队列的消息传递机制。
- 应用场景:用于简单的消息传递和数据更新。
4 相关API函数
4.1 发送通知API函数
以下为发送通知API函数,简单去前三个函数进行介绍。
函数 | 描述 |
xTaskNotify() | 发送通知,带有通知值 |
xTaskNotifyAndQuery() | 发送通知,带有通知值并且保留接收任务的原通知值 |
xTaskNotifyGive() | 发送通知,不带通知值 |
xTaskNotifyFromISR() | 在中断中发送任务通知 |
xTaskNotifyAndQueryFromISR() | |
vTaskNotifyGiveFromISR() |
(1)xTaskNotify()
函数作用:发送通知,带有通知值
函数原型:
BaseType_t xTaskNotify(
TaskHandle_t xTaskToNotify, // 要通知的任务句柄
uint32_t ulValue, // 要发送的通知值
eNotifyAction eAction // 通知值的更新方式
);
参数解析:
参数 | 描述 |
---|---|
xTaskToNotify | 要通知的任务句柄 |
ulValue | 要发送的通知值 |
eAction | 通知值的更新方式(eSetBits 、eIncrement 等) |
返回值:
返回 pdPASS
表示发送通知成功,pdFAIL
表示发送通知失败。
函数举例:
xTaskNotify(TaskB_Handle, 0x01, eSetBits); // 发送通知,更新位0
(2)xTaskNotifyAndQuery()
函数作用:发送通知,带有通知值并且保留接收任务的原通知值。
函数原型:
BaseType_t xTaskNotifyAndQuery(
TaskHandle_t xTaskToNotify, // 要通知的任务句柄
uint32_t ulValue, // 要发送的通知值
eNotifyAction eAction, // 通知值的更新方式
uint32_t *pulPreviousNotifyValue // 指向接收任务的原通知值的指针
);
参数解析:
参数 | 描述 |
---|---|
xTaskToNotify | 要通知的任务句柄 |
ulValue | 要发送的通知值 |
eAction | 通知值的更新方式(eSetBits 、eIncrement 等) |
pulPreviousNotifyValue | 用于保存接收任务的原通知值的指针 |
返回值:
返回 pdPASS
表示发送通知成功,pdFAIL
表示发送通知失败。
函数举例:
// 示例:发送通知并查询之前的通知值
uint32_t previousValue;
xTaskNotifyAndQuery(TaskB_Handle, 0x01, eSetBits, &previousValue);
(3)xTaskNotifyGive()
函数作用:发送通知,不带通知值。
函数原型:
BaseType_t xTaskNotifyGive(
TaskHandle_t xTaskToNotify // 要通知的任务句柄
);
参数解析:
参数 | 描述 |
---|---|
xTaskToNotify | 要通知的任务句柄 |
返回值:
返回 pdPASS
表示发送通知成功,pdFAIL
表示发送通知失败。
函数举例:
// 示例:发送通知,不带通知值
xTaskNotifyGive(TaskB_Handle);
4.2 接受通知API函数
(1)ulTaskNotifyTake()
函数作用:获取任务通知,可以设置在退出此函数的时候将任务通知值清零或者减一。当任务通知用作二值信号量或者计数信号量的时候,使用此函数来获取信号量。
函数原型:
uint32_t ulTaskNotifyTake(
BaseType_t xClearCountOnExit, // 指定在成功接收通知后,将通知值清零或减一
TickType_t xTicksToWait // 阻塞等待任务通知值的最大时间
);
参数解析:
参数 | 描述 |
---|---|
xClearCountOnExit | 指定在成功接收通知后,将通知值清零(pdTRUE )或减一(pdFALSE ) |
xTicksToWait | 阻塞等待任务通知值的最大时间(以Tick为单位) |
返回值:
返回接收成功时的任务通知值,如果接收失败则返回0。
函数举例:
// 示例:接收任务通知,成功接收后将通知值清零
uint32_t notificationValue;
notificationValue = ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
// 检查是否成功接收到通知
if (notificationValue > 0) {
// 成功接收到通知,执行相应操作
printf("Notification received with value: %u\n", notificationValue);
} else {
// 未接收到通知,处理错误情况
printf("Failed to receive notification.\n");
}
(2)xTaskNotifyWait()
函数作用:获取任务通知,比ulTaskNotifyTake()
更为复杂,可获取通知值和清除通知值的指定位。当任务通知用作事件标志组或队列时,使用此函数来获取通知。
函数原型:
BaseType_t xTaskNotifyWait(
uint32_t ulBitsToClearOnEntry, // 等待前清零指定任务通知值的比特位
uint32_t ulBitsToClearOnExit, // 成功等待后清零指定的任务通知值比特位
uint32_t *pulNotificationValue,// 用来取出通知值的指针
TickType_t xTicksToWait // 阻塞等待任务通知值的最大时间
);
参数解析:
参数 | 描述 |
---|---|
ulBitsToClearOnEntry | 等待前清零指定任务通知值的比特位(旧值对应位清0) |
ulBitsToClearOnExit | 成功等待后清零指定的任务通知值比特位(新值对应位清0) |
pulNotificationValue | 用来取出通知值的指针(如果不需要取出通知值,可设为NULL) |
xTicksToWait | 阻塞等待任务通知值的最大时间(以Tick为单位) |
返回值:
返回 pdTRUE
表示等待任务通知成功,pdFALSE
表示等待任务通知失败。
函数举例:
// 示例:接收任务通知并清除相应位,成功接收后将通知值存储在notificationValue中
uint32_t notificationValue;
BaseType_t result;
result = xTaskNotifyWait(0x00, 0xFF, ¬ificationValue, portMAX_DELAY);
// 检查是否成功接收到通知
if (result == pdTRUE) {
// 成功接收到通知,执行相应操作
printf("Notification received with value: %u\n", notificationValue);
} else {
// 未接收到通知,处理错误情况
printf("Failed to receive notification.\n");
}
5 代码实例
5.1 任务通知模拟队列
实验设计: 使用任务通知来模拟队列,将数据从一个任务发送到另一个任务。发送任务将数据放入通知值中,接收任务读取通知值并处理数据。
任务功能:
- 任务1(发送任务):模拟数据发送,将数据发送到任务通知值中。
- 任务2(接收任务):接收任务通知值中的数据并处理。
代码示例:
#include "FreeRTOS.h"
#include "task.h"
#include <stdio.h>
// 任务句柄
TaskHandle_t SendTask_Handle;
TaskHandle_t ReceiveTask_Handle;
// 任务函数声明
void SendTask(void *pvParameters);
void ReceiveTask(void *pvParameters);
int main(void)
{
// 创建发送任务
xTaskCreate((TaskFunction_t)SendTask, "SendTask", 128, NULL, 2, &SendTask_Handle);
// 创建接收任务
xTaskCreate((TaskFunction_t)ReceiveTask, "ReceiveTask", 128, NULL, 2, &ReceiveTask_Handle);
// 启动调度器
vTaskStartScheduler();
// 主函数不会再执行到这里
while(1);
}
// 发送任务:将数据发送到任务通知值中
void SendTask(void *pvParameters)
{
uint32_t data = 0;
while(1)
{
// 发送通知,带有数据
xTaskNotify(ReceiveTask_Handle, data, eSetValueWithOverwrite);
printf("SendTask: Sent data %u\n", data);
data++;
// 模拟发送间隔
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
// 接收任务:接收任务通知值中的数据并处理
void ReceiveTask(void *pvParameters)
{
uint32_t receivedData;
while(1)
{
// 等待接收任务通知值中的数据
xTaskNotifyWait(0x00, 0x00, &receivedData, portMAX_DELAY);
printf("ReceiveTask: Received data %u\n", receivedData);
}
}
5.2 任务通知模拟信号量
实验设计: 使用任务通知来模拟信号量,任务1产生信号量并发送通知,任务2接收通知并使用信号量执行相应操作。
任务功能:
- 任务1(信号量产生任务):模拟信号量产生并发送通知。
- 任务2(信号量使用任务):接收通知并使用信号量。
代码示例:
#include "FreeRTOS.h"
#include "task.h"
#include <stdio.h>
// 任务句柄
TaskHandle_t ProduceTask_Handle;
TaskHandle_t ConsumeTask_Handle;
// 任务函数声明
void ProduceTask(void *pvParameters);
void ConsumeTask(void *pvParameters);
int main(void)
{
// 创建信号量产生任务
xTaskCreate((TaskFunction_t)ProduceTask, "ProduceTask", 128, NULL, 2, &ProduceTask_Handle);
// 创建信号量使用任务
xTaskCreate((TaskFunction_t)ConsumeTask, "ConsumeTask", 128, NULL, 2, &ConsumeTask_Handle);
// 启动调度器
vTaskStartScheduler();
// 主函数不会再执行到这里
while(1);
}
// 信号量产生任务:产生信号量并发送通知
void ProduceTask(void *pvParameters)
{
while(1)
{
// 发送通知,产生信号量
xTaskNotifyGive(ConsumeTask_Handle);
printf("ProduceTask: Produced signal\n");
// 模拟信号量产生间隔
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
// 信号量使用任务:接收通知并使用信号量
void ConsumeTask(void *pvParameters)
{
while(1)
{
// 等待接收信号量通知
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
printf("ConsumeTask: Consumed signal\n");
}
}
5.3 任务通知模拟事件标志组
实验设计: 使用任务通知来模拟事件标志组,任务1和任务2设置事件标志,任务3等待所有事件标志被设置后执行操作。
任务功能:
- 任务1(事件标志1任务):设置事件标志1。
- 任务2(事件标志2任务):设置事件标志2。
- 任务3(事件处理任务):等待所有事件标志被设置后执行操作。
代码示例:
#include "FreeRTOS.h"
#include "task.h"
#include <stdio.h>
// 定义事件标志位
#define EVENT_BIT_1 (1 << 0)
#define EVENT_BIT_2 (1 << 1)
// 任务句柄
TaskHandle_t EventTask1_Handle;
TaskHandle_t EventTask2_Handle;
TaskHandle_t EventTask3_Handle;
// 任务函数声明
void EventTask1(void *pvParameters);
void EventTask2(void *pvParameters);
void EventTask3(void *pvParameters);
int main(void)
{
// 创建事件标志1任务
xTaskCreate((TaskFunction_t)EventTask1, "EventTask1", 128, NULL, 2, &EventTask1_Handle);
// 创建事件标志2任务
xTaskCreate((TaskFunction_t)EventTask2, "EventTask2", 128, NULL, 2, &EventTask2_Handle);
// 创建事件处理任务
xTaskCreate((TaskFunction_t)EventTask3, "EventTask3", 128, NULL, 2, &EventTask3_Handle);
// 启动调度器
vTaskStartScheduler();
// 主函数不会再执行到这里
while(1);
}
// 事件标志1任务:设置事件标志1
void EventTask1(void *pvParameters)
{
while(1)
{
// 发送通知,设置事件标志1
xTaskNotify(EventTask3_Handle, EVENT_BIT_1, eSetBits);
printf("EventTask1: Set event bit 1\n");
// 模拟事件产生间隔
vTaskDelay(pdMS_TO_TICKS(2000));
}
}
// 事件标志2任务:设置事件标志2
void EventTask2(void *pvParameters)
{
while(1)
{
// 发送通知,设置事件标志2
xTaskNotify(EventTask3_Handle, EVENT_BIT_2, eSetBits);
printf("EventTask2: Set event bit 2\n");
// 模拟事件产生间隔
vTaskDelay(pdMS_TO_TICKS(3000));
}
}
// 事件处理任务:等待所有事件标志被设置后执行操作
void EventTask3(void *pvParameters)
{
uint32_t eventBits;
while(1)
{
// 等待所有事件标志被设置
xTaskNotifyWait(0x00, 0x00, &eventBits, portMAX_DELAY);
// 检查是否所有事件标志都被设置
if ((eventBits & (EVENT_BIT_1 | EVENT_BIT_2)) == (EVENT_BIT_1 | EVENT_BIT_2))
{
printf("EventTask3: All event bits set, processing events\n");
// 清除所有事件标志
xTaskNotifyStateClear(EventTask3_Handle);
}
}
}
本专栏将对FreeRTOS进行快速讲解,带你了解并使用FreeRTOS的各部分内容。期待诸君的关注和订阅!