实时操作系统-11-[FreeRTOS]任务通知
基本概念
FreeRTOS 从 V8.2.0 版本开始提供任务通知这个功能,每个任务都有一个 32 位的通知值,在大多数情况下,任务通知可以替代二值信号量、计数信号量、事件组,也可以替代长度为 1 的队列(可以保存一个 32位整数或指针值)。
相对于以前使用 FreeRTOS 内核通信的资源,必须创建队列、二进制信号量、计数信号量或事件组的情况,使用任务通知显然更灵活。按照 FreeRTOS 官方的说法,使用任务通知比通过信号量等 ICP 通信方式解除阻塞的任务要快 45%,并且更加省 RAM 内存空间(使用 GCC 编译器,-o2 优化级别),任务通知的使用无需创建队列。想要使用任务通知,必须将 FreeRTOSConfig.h 中的宏定义 configUSE_TASK_NOTIFICATIONS 设置为 1,其实 FreeRTOS 默认是为 1 的,所以任务通知是默认使能的。
FreeRTOS 提供以下几种方式发送通知给任务 :
- 发送通知给任务, 如果有通知未读,不覆盖通知值。
- 发送通知给任务,直接覆盖通知值。
- 发送通知给任务,设置通知值的一个或者多个位,可以当做事件组来使用。
- 发送通知给任务,递增通知值,可以当做计数信号量使用。
当然,凡是都有利弊,不然的话 FreeRTOS 还要内核的 IPC 通信机制干嘛,消息通知虽然处理更快,RAM 开销更小,但也有以下限制 :
只能有一个任务接收通知消息,因为必须指定接收通知的任务。。
只有等待通知的任务可以被阻塞,发送通知的任务,在任何情况下都不会因为发送失败而进入阻塞态。
运作机制
任务通知是属于任务中附带的资源,所以在任务被创建的时候,任务通知也被初始化 的,而在分析队列和信号量的章节中,我们知道在使用队列、信号量前,必须先创建队列和信号量,目的是为了创建队列数据结构。再来看任务通知,由于任务通知的数据结构包含在任务控制块中,只要任务存在,任务通知数据结构就已经创建完毕,可以直接使用,所以使用的时候很是方便。
任务通知可以在任务中向指定任务发送通知,也可以在中断中向指定任务发送通知, FreeRTOS 的每个任务都有一个 32 位的通知值,任务控制块中的成员变量 ulNotifiedValue就是这个通知值。只有在任务中可以等待通知,而不允许在中断中等待通知。如果任务在等待的通知暂时无效,任务会根据用户指定的阻塞超时时间进入阻塞状态,我们可以将等待通知的任务看作是消费者;其它任务和中断可以向等待通知的任务发送通知,发送通知的任务和中断服务函数可以看作是生产者,当其他任务或者中断向这个任务发送任务通知,任务获得通知以后,该任务就会从阻塞态中解除,这与 FreeRTOS 中内核的其他通信机制一致。
任务通知的数据结构
typedef struct tskTaskControlBlock {
volatile StackType_t *pxTopOfStack;
#if ( portUSING_MPU_WRAPPERS == 1 )
xMPU_SETTINGS xMPUSettings;
#endif
ListItem_t xStateListItem;
ListItem_t xEventListItem;
UBaseType_t uxPriority;
StackType_t *pxStack;
char pcTaskName[ configMAX_TASK_NAME_LEN ];
#if ( portSTACK_GROWTH > 0 )
StackType_t *pxEndOfStack;
#endif
#if ( portCRITICAL_NESTING_IN_TCB == 1 )
UBaseType_t uxCriticalNesting;
#endif
#if ( configUSE_TRACE_FACILITY == 1 )
UBaseType_t uxTCBNumber;
UBaseType_t uxTaskNumber;
#endif
#if ( configUSE_MUTEXES == 1 )
UBaseType_t uxBasePriority;
UBaseType_t uxMutexesHeld;
#endif
#if ( configUSE_APPLICATION_TASK_TAG == 1 )
TaskHookFunction_t pxTaskTag;
#endif
#if( configNUM_THREAD_LOCAL_STORAGE_POINTERS > 0 )
void *pvThreadLocalStoragePointers[ configNUM_THREAD_LOCAL_STORAGE_POINTERS ];
#endif
#if( configGENERATE_RUN_TIME_STATS == 1 )
uint32_t ulRunTimeCounter;
#endif
#if ( configUSE_NEWLIB_REENTRANT == 1 )
struct _reent xNewLib_reent;
#endif
#if( configUSE_TASK_NOTIFICATIONS == 1 )
volatile uint32_t ulNotifiedValue;//任务通知的值,可以保存一个 32 位整数或指针值。
volatile uint8_t ucNotifyState;//任务通知状态,用于标识任务是否在等待通知。
#endif
#if( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 )
uint8_t ucStaticallyAllocated;
#endif
#if( INCLUDE_xTaskAbortDelay == 1 )
uint8_t ucDelayAborted;
#endif
} tskTCB;
typedef tskTCB TCB_t;
任务通知接口函数
发送任务通知函数
我们先看一下发送通知 API 函数。这类函数比较多,有 6 个。但仔细分析会发现它们只能完成 3 种操作,每种操作有两个 API 函数,分别为带中断保护版本和不带中断保护版本。FreeRTOS 将 API 细分为带中断保护版本和不带中断保护版本是为了节省中断服务程序处理时间,提升性能。
通过前面通信机制的学习,相信大家都了解了 FreeRTOS 的风格,这里的任务通知发送函数也是利用宏定义来进行扩展的,所有的函数都是一个宏定义,在任务中发送任务通知的函数均是调用 xTaskGenericNotify()函数进行发送通知,下面来看看xTaskGenericNotify()的源码:
#if( configUSE_TASK_NOTIFICATIONS == 1 )
BaseType_t xTaskGenericNotify( TaskHandle_t xTaskToNotify,//被通知的任务句柄,指定通知的任务。
uint32_t ulValue,//发送的通知值。
eNotifyAction eAction,//枚举类型,指明更新通知值的方式
uint32_t *pulPreviousNotificationValue )//任务原本的通知值返回。
{
TCB_t * pxTCB;
BaseType_t xReturn = pdPASS;
uint8_t ucOriginalNotifyState;
configASSERT( xTaskToNotify );
pxTCB = ( TCB_t * ) xTaskToNotify;
taskENTER_CRITICAL();
{
if( pulPreviousNotificationValue != NULL ) {
/* 回传未被更新的任务通知值,保存在 pulPreviousNotificationValue 中 */
*pulPreviousNotificationValue = pxTCB->ulNotifiedValue;
}
/* 获取任务通知的状态,看看任务是否在等待通知,方便在发送通知后恢复任务 */
ucOriginalNotifyState = pxTCB->ucNotifyState;
/* 不管状态是怎么样的,反正现在发送通知,任务就收到任务通知 */
pxTCB->ucNotifyState = taskNOTIFICATION_RECEIVED;
/* 指定更新任务通知的方式 */
switch( eAction ) {
/* 通知值按位或上 ulValue。
使用这种方法可以某些场景下代替事件组,但执行速度更快。 */
case eSetBits:
pxTCB->ulNotifiedValue |= ulValue;
break;
/* 被通知任务的通知值增加 1,这种发送通知方式,参数 ulValue 未使用 */
case eIncrement:
( pxTCB->ulNotifiedValue )++;
break;
/* 将被通知任务的通知值设置为 ulValue。无论任务是否还有通知,
都覆盖当前任务通知值。使用这种方法,
可以在某些场景下代替 xQueueOverwrite()函数,但执行速度更快。 */
case eSetValueWithOverwrite:
pxTCB->ulNotifiedValue = ulValue;
break;
/* 如果被通知任务当前没有通知,则被通知任务的通知值设置为 ulValue;
在某些场景下替代长度为 1 的 xQueueSend(),但速度更快。 */
case eSetValueWithoutOverwrite:
if( ucOriginalNotifyState != taskNOTIFICATION_RECEIVED ) {
pxTCB->ulNotifiedValue = ulValue;
} else {
/* 如果被通知任务还没取走上一个通知,本次发送通知,
任务又接收到了一个通知,则这次通知值丢弃,
在这种情况下,函数调用失败并返回 pdFALSE。 */
xReturn = pdFAIL;
}
break;
/* 发送通知但不更新通知值,这意味着参数 ulValue 未使用。 */
case eNoAction:
break;
}
traceTASK_NOTIFY();
/* 如果被通知任务由于等待任务通知而挂起 */
if( ucOriginalNotifyState == taskWAITING_NOTIFICATION ) {
/* 唤醒任务,将任务从阻塞列表中移除,添加到就绪列表中 */
( void ) uxListRemove( &( pxTCB->xStateListItem ) );
prvAddTaskToReadyList( pxTCB );
/* 刚刚唤醒的任务优先级比当前任务高 */
if( pxTCB->uxPriority > pxCurrentTCB->uxPriority ) {
/* 任务切换 */
taskYIELD_IF_USING_PREEMPTION();
} else {
mtCOVERAGE_TEST_MARKER();
}
} else {
mtCOVERAGE_TEST_MARKER();
}
}
taskEXIT_CRITICAL();
return xReturn;
}
#endif
xTaskGenericNotify()函数是一个通用的任务通知发送函数,在任务中发送通知的 API 函数,如xTaskNotifyGive() 、 xTaskNotify() 、 xTaskNotifyAndQuery() , 都是以 xTaskGenericNotify() 为原型的,只不过指定的发送方式不同而已。
xTaskNotifyGive()
xTaskNotifyGive()是一个宏,宏展开是调用函数 xTaskNotify( ( xTaskToNotify ), ( 0 ), eIncrement ),即向一个任务发送通知,并将对方的任务通知值加 1。该函数可以作为二值信号量和计数信号量的一种轻量型的实现,速度更快,在这种情况下对象任务在等待任务通知的时候应该是使用函数 ulTaskNotifyTake() 而不是 xTaskNotifyWait() 。xTaskNotifyGive() 不能在中断里面使用,而是使用具有中断保护功能的 vTaskNotifyGiveFromISR() 来代替:

上面也说了此函数可以适用于二值信号量和计数信号量,那么我们逐一进行举例。
二值信号量
开始前我们想了解一下二者的区别:

首先,我们先来想一下想要完成的功能,创建了三个任务,其中两个任务是用于接收任务通知,另一个任务发送任务通知。三个任务独立运行,发送通知任务是通过检测按键的按下情况来发送通知,另两个任务获取通知,在任务通知中没有可用的通知之前就一直等待任务通知,获取到通知以后就将通知值清 0,这样子是为了代替二值信号量,任务同步成功则继续执行,然后在串口调试助手 里将运行信息打印出来。
先创建三个任务句柄,用于方便后续对任务的操作:
//接收相关
static TaskHandle_t Receive1_Task_Handle = NULL;/* Receive1_Task任务句柄 */
static TaskHandle_t Receive2_Task_Handle = NULL;/* Receive2_Task任务句柄 */
//发送相关
static TaskHandle_t Send_Task_Handle = NULL;/* Send_Task任务句柄 */
对于接收任务,我们这里想不填充,等下面讲获取的时候再进行填充,这里主要是对发送函数的调用讲解:
//Receive_Task任务主体
static void Receive1_Task(void* parameter)
{
while (1)
{
}
}
//Receive_Task任务主体
static void Receive2_Task(void* parameter)
{
while (1)
{
}
}
对于发送任务,主要通过按键按下进行发送函数API的调用:
//Send_Task任务主体
static void Send_Task(void* parameter)
{
BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
while (1)
{
/* KEY1 被按下 */
if( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON )
{
/* 原型:BaseType_t xTaskNotifyGive( TaskHandle_t xTaskToNotify ); */
xReturn = xTaskNotifyGive(Receive1_Task_Handle);
/* 此函数只会返回pdPASS */
if( xReturn == pdTRUE )
printf("\r\nReceive1_Task_Handle 任务通知发送成功!\r\n");
}
/* KEY2 被按下 */
if( Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN) == KEY_ON )
{
xReturn = xTaskNotifyGive(Receive2_Task_Handle);
/* 此函数只会返回pdPASS */
if( xReturn == pdPASS )
printf("\r\nReceive2_Task_Handle 任务通知发送成功!\r\n");
}
vTaskDelay(20);
}
}
然后是对三个任务优先级,栈空间等的创建,这里把接收的也一并创建了:
//任务创建函数
static void AppTaskCreate(void)
{
BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
taskENTER_CRITICAL(); //进入临界区
/* 创建Receive1_Task任务 */
xReturn = xTaskCreate((TaskFunction_t )Receive1_Task, /* 任务入口函数 */
(const char* )"Receive1_Task",/* 任务名字 */
(uint16_t )512, /* 任务栈大小 */
(void* )NULL, /* 任务入口函数参数 */
(UBaseType_t )2, /* 任务的优先级 */
(TaskHandle_t* )&Receive1_Task_Handle);/* 任务控制块指针 */
if(pdPASS == xReturn)
printf("创建Receive1_Task任务成功!\r\n");
/* 创建Receive2_Task任务 */
xReturn = xTaskCreate((TaskFunction_t )Receive2_Task, /* 任务入口函数 */
(const char* )"Receive2_Task",/* 任务名字 */
(uint16_t )512, /* 任务栈大小 */
(void* )NULL, /* 任务入口函数参数 */
(UBaseType_t )3, /* 任务的优先级 */
(TaskHandle_t* )&Receive2_Task_Handle);/* 任务控制块指针 */
if(pdPASS == xReturn)
printf("创建Receive2_Task任务成功!\r\n");
/* 创建Send_Task任务 */
xReturn = xTaskCreate((TaskFunction_t )Send_Task, /* 任务入口函数 */
(const char* )"Send_Task",/* 任务名字 */
(uint16_t )512, /* 任务栈大小 */
(void* )NULL,/* 任务入口函数参数 */
(UBaseType_t )4, /* 任务的优先级 */
(TaskHandle_t* )&Send_Task_Handle);/* 任务控制块指针 */
if(pdPASS == xReturn)
printf("创建Send_Task任务成功!\r\n");
vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务
taskEXIT_CRITICAL(); //退出临界区
}
完整main函数代码:
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "LED.h"
#include "Usart.h"
#include "Key.h"
/* FreeRTOS头文件 */
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
/*
* 任务句柄是一个指针,用于指向一个任务,当任务创建好之后,它就具有了一个任务句柄
* 以后我们要想操作这个任务都需要通过这个任务句柄,如果是自身的任务操作自己,那么
* 这个句柄可以为NULL。
*/
static TaskHandle_t AppTaskCreate_Handle = NULL; /* 创建任务句柄 */
//接收相关
static TaskHandle_t Receive1_Task_Handle = NULL;/* Receive1_Task任务句柄 */
static TaskHandle_t Receive2_Task_Handle = NULL;/* Receive2_Task任务句柄 */
//发送相关
static TaskHandle_t Send_Task_Handle = NULL;/* Send_Task任务句柄 */
/********************************** 内核对象句柄 *********************************/
/*
* 信号量,消息队列,事件标志组,软件定时器这些都属于内核的对象,要想使用这些内核
* 对象,必须先创建,创建成功之后会返回一个相应的句柄。实际上就是一个指针,后续我
* 们就可以通过这个句柄操作这些内核对象。
*
* 内核对象说白了就是一种全局的数据结构,通过这些数据结构我们可以实现任务间的通信,
* 任务间的事件同步等各种功能。至于这些功能的实现我们是通过调用这些内核对象的函数
* 来完成的
*
*/
/******************************* 宏定义 ************************************/
/*
* 当我们在写应用程序的时候,可能需要用到一些宏定义。
*/
//一些函数声明
static void AppTaskCreate(void);/* 用于创建任务 */
//接收相关
static void Receive1_Task(void* pvParameters);/* Receive1_Task任务实现 */
static void Receive2_Task(void* pvParameters);/* Receive2_Task任务实现 */
//发送相关
static void Send_Task(void* pvParameters);/* Send_Task任务实现 */
static void All_Function_Init(void);/* 用于初始化板载相关资源 */
int main(void)
{
BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
All_Function_Init();//硬件初始化
while (1)
{
/* 创建AppTaskCreate任务 */
xReturn = xTaskCreate((TaskFunction_t )AppTaskCreate, /* 任务入口函数 */
(const char* )"AppTaskCreate",/* 任务名字 */
(uint16_t )512, /* 任务栈大小 */
(void* )NULL,/* 任务入口函数参数 */
(UBaseType_t )1, /* 任务的优先级 */
(TaskHandle_t* )&AppTaskCreate_Handle);/* 任务控制块指针 */
/* 启动任务调度 */
if(pdPASS == xReturn)
vTaskStartScheduler(); /* 启动任务,开启调度 */
else
return -1;
}
}
//任务创建函数
static void AppTaskCreate(void)
{
BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
taskENTER_CRITICAL(); //进入临界区
/* 创建Receive1_Task任务 */
xReturn = xTaskCreate((TaskFunction_t )Receive1_Task, /* 任务入口函数 */
(const char* )"Receive1_Task",/* 任务名字 */
(uint16_t )512, /* 任务栈大小 */
(void* )NULL, /* 任务入口函数参数 */
(UBaseType_t )2, /* 任务的优先级 */
(TaskHandle_t* )&Receive1_Task_Handle);/* 任务控制块指针 */
if(pdPASS == xReturn)
printf("创建Receive1_Task任务成功!\r\n");
/* 创建Receive2_Task任务 */
xReturn = xTaskCreate((TaskFunction_t )Receive2_Task, /* 任务入口函数 */
(const char* )"Receive2_Task",/* 任务名字 */
(uint16_t )512, /* 任务栈大小 */
(void* )NULL, /* 任务入口函数参数 */
(UBaseType_t )3, /* 任务的优先级 */
(TaskHandle_t* )&Receive2_Task_Handle);/* 任务控制块指针 */
if(pdPASS == xReturn)
printf("创建Receive2_Task任务成功!\r\n");
/* 创建Send_Task任务 */
xReturn = xTaskCreate((TaskFunction_t )Send_Task, /* 任务入口函数 */
(const char* )"Send_Task",/* 任务名字 */
(uint16_t )512, /* 任务栈大小 */
(void* )NULL,/* 任务入口函数参数 */
(UBaseType_t )4, /* 任务的优先级 */
(TaskHandle_t* )&Send_Task_Handle);/* 任务控制块指针 */
if(pdPASS == xReturn)
printf("创建Send_Task任务成功!\r\n");
vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务
taskEXIT_CRITICAL(); //退出临界区
}
//Receive_Task任务主体
static void Receive1_Task(void* parameter)
{
while (1)
{
}
}
//Receive_Task任务主体
static void Receive2_Task(void* parameter)
{
while (1)
{
}
}
//Send_Task任务主体
static void Send_Task(void* parameter)
{
BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
while (1)
{
/* KEY1 被按下 */
if( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON )
{
/* 原型:BaseType_t xTaskNotifyGive( TaskHandle_t xTaskToNotify ); */
xReturn = xTaskNotifyGive(Receive1_Task_Handle);
/* 此函数只会返回pdPASS */
if( xReturn == pdTRUE )
printf("\r\nReceive1_Task_Handle 任务通知发送成功!\r\n");
}
/* KEY2 被按下 */
if( Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN) == KEY_ON )
{
xReturn = xTaskNotifyGive(Receive2_Task_Handle);
/* 此函数只会返回pdPASS */
if( xReturn == pdPASS )
printf("\r\nReceive2_Task_Handle 任务通知发送成功!\r\n");
}
vTaskDelay(20);
}
}
//初始化声明
static void All_Function_Init(void)
{
/*
* STM32中断优先级分组为4,即4bit都用来表示抢占优先级,范围为:0~15
* 优先级分组只需要分组一次即可,以后如果有其他的任务需要用到中断,
* 都统一用这个优先级分组,千万不要再分组,切忌。
*/
NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4 );
/* LED 初始化 */
LED_GPIO_Config();
/* 串口初始化 */
USART_Config();
//按键初始化
Key_GPIO_Config();
}
运行结果:

计数信号量

任务通知代替计数信号量是基于计数型信号量实验修改而来,模拟停车场工作运行。并且在 FreeRTOS 中创建了两个任务:一个是获取任务通知,一个是发送任务通知,两个任务独立运行,获取通知的任务是通过按下 KEY1 按键获取,模拟停车场停车操作,其等待时间是 0;发送通知的任务则是通过检测 KEY2 按键按下进行通知的发送,模拟停车场取车操作,并且在串口调试助手输出相应信息。
同样的这里有两个任务,一个发送,一个接收,我们为其创建两个任务句柄:
static TaskHandle_t Take_Task_Handle = NULL;/* Take_Task任务句柄 */
static TaskHandle_t Give_Task_Handle = NULL;/* Give_Task任务句柄 */
同样接收相关的下面在进行讲解,先留空白:
//Take_Task 任务主体
static void Take_Task(void* parameter)
{
uint32_t take_num = pdTRUE;/* 定义一个创建信息返回值,默认为pdPASS */
/* 任务都是一个无限循环,不能返回 */
while (1)
{
}
}
发送的通过按键来进行实现:
//Give_Task 任务主体
static void Give_Task(void* parameter)
{
BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
/* 任务都是一个无限循环,不能返回 */
while (1)
{
//如果KEY2被单击
if( Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN) == KEY_ON )
{
/* 原型:BaseType_t xTaskNotifyGive( TaskHandle_t xTaskToNotify ); */
/* 释放一个任务通知 */
xTaskNotifyGive(Take_Task_Handle);//发送任务通知
/* 此函数只会返回pdPASS */
if ( pdPASS == xReturn )
printf( "KEY2被按下,释放1个停车位!\r\n" );
}
vTaskDelay(20); //每20ms扫描一次
}
}
这样运行结果:

这里我是想像之前计数信号量那边直接调用API查看当前计数值,但是在 FreeRTOS 中,任务通知(Task Notification)本身并不是一个标准的计数信号量,它是通过ulTaskNotifyTake() 和 xTaskNotifyGive() 来模拟类似计数信号量的行为,因此,任务通知没有直接提供查看当前计数值的 API,这里声明一个全局变量进行累加的操作:
static volatile uint32_t s_notify_count = 0; // 全局计数器
//Give_Task 任务主体
static void Give_Task(void* parameter)
{
BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
/* 任务都是一个无限循环,不能返回 */
while (1)
{
//如果KEY2被单击
if( Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN) == KEY_ON )
{
/* 原型:BaseType_t xTaskNotifyGive( TaskHandle_t xTaskToNotify ); */
/* 释放一个任务通知 */
xTaskNotifyGive(Take_Task_Handle);//发送任务通知
/* 此函数只会返回pdPASS */
if ( pdPASS == xReturn )
{
s_notify_count++;
printf("\r\nKEY2被按下,释放1个停车位!\r\n" );
printf("当前计数值: %lu\r\n", s_notify_count);
}
}
vTaskDelay(20); //每20ms扫描一次
}
}
运行结果:

main函数完整代码:
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "LED.h"
#include "Usart.h"
#include "Key.h"
/* FreeRTOS头文件 */
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
//#include "semphr.h"
/*
* 任务句柄是一个指针,用于指向一个任务,当任务创建好之后,它就具有了一个任务句柄
* 以后我们要想操作这个任务都需要通过这个任务句柄,如果是自身的任务操作自己,那么
* 这个句柄可以为NULL。
*/
static TaskHandle_t AppTaskCreate_Handle = NULL; /* 创建任务句柄 */
static TaskHandle_t Take_Task_Handle = NULL;/* Take_Task任务句柄 */
static TaskHandle_t Give_Task_Handle = NULL;/* Give_Task任务句柄 */
/********************************** 内核对象句柄 *********************************/
/*
* 信号量,消息队列,事件标志组,软件定时器这些都属于内核的对象,要想使用这些内核
* 对象,必须先创建,创建成功之后会返回一个相应的句柄。实际上就是一个指针,后续我
* 们就可以通过这个句柄操作这些内核对象。
*
* 内核对象说白了就是一种全局的数据结构,通过这些数据结构我们可以实现任务间的通信,
* 任务间的事件同步等各种功能。至于这些功能的实现我们是通过调用这些内核对象的函数
* 来完成的
*
*/
/******************************* 宏定义 ************************************/
/*
* 当我们在写应用程序的时候,可能需要用到一些宏定义。
*/
//一些数据变量声明
static volatile uint32_t s_notify_count = 0; // 全局计数器
//一些函数声明
static void AppTaskCreate(void);/* 用于创建任务 */
static void Take_Task(void* pvParameters);/* Take_Task任务实现 */
static void Give_Task(void* pvParameters);/* Give_Task任务实现 */
static void All_Function_Init(void);/* 用于初始化板载相关资源 */
int main(void)
{
BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
All_Function_Init();//硬件初始化
while (1)
{
/* 创建AppTaskCreate任务 */
xReturn = xTaskCreate((TaskFunction_t )AppTaskCreate, /* 任务入口函数 */
(const char* )"AppTaskCreate",/* 任务名字 */
(uint16_t )512, /* 任务栈大小 */
(void* )NULL,/* 任务入口函数参数 */
(UBaseType_t )1, /* 任务的优先级 */
(TaskHandle_t* )&AppTaskCreate_Handle);/* 任务控制块指针 */
/* 启动任务调度 */
if(pdPASS == xReturn)
vTaskStartScheduler(); /* 启动任务,开启调度 */
else
return -1;
}
}
//任务创建函数
static void AppTaskCreate(void)
{
BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
taskENTER_CRITICAL(); //进入临界区
/* 创建Take_Task任务 */
xReturn = xTaskCreate((TaskFunction_t )Take_Task, /* 任务入口函数 */
(const char* )"Take_Task",/* 任务名字 */
(uint16_t )512, /* 任务栈大小 */
(void* )NULL, /* 任务入口函数参数 */
(UBaseType_t )2, /* 任务的优先级 */
(TaskHandle_t* )&Take_Task_Handle);/* 任务控制块指针 */
if(pdPASS == xReturn)
printf("创建Take_Task任务成功!\r\n");
/* 创建Give_Task任务 */
xReturn = xTaskCreate((TaskFunction_t )Give_Task, /* 任务入口函数 */
(const char* )"Give_Task",/* 任务名字 */
(uint16_t )512, /* 任务栈大小 */
(void* )NULL,/* 任务入口函数参数 */
(UBaseType_t )3, /* 任务的优先级 */
(TaskHandle_t* )&Give_Task_Handle);/* 任务控制块指针 */
if(pdPASS == xReturn)
printf("创建Give_Task任务成功!\r\n");
vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务
taskEXIT_CRITICAL(); //退出临界区
}
//Take_Task 任务主体
static void Take_Task(void* parameter)
{
uint32_t take_num = pdTRUE;/* 定义一个创建信息返回值,默认为pdPASS */
/* 任务都是一个无限循环,不能返回 */
while (1)
{
}
}
//Give_Task 任务主体
static void Give_Task(void* parameter)
{
BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
/* 任务都是一个无限循环,不能返回 */
while (1)
{
//如果KEY2被单击
if( Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN) == KEY_ON )
{
/* 原型:BaseType_t xTaskNotifyGive( TaskHandle_t xTaskToNotify ); */
/* 释放一个任务通知 */
xTaskNotifyGive(Take_Task_Handle);//发送任务通知
/* 此函数只会返回pdPASS */
if ( pdPASS == xReturn )
{
s_notify_count++;
printf("\r\nKEY2被按下,释放1个停车位!\r\n" );
printf("当前计数值: %lu\r\n", s_notify_count);
}
}
vTaskDelay(20); //每20ms扫描一次
}
}
//初始化声明
static void All_Function_Init(void)
{
/*
* STM32中断优先级分组为4,即4bit都用来表示抢占优先级,范围为:0~15
* 优先级分组只需要分组一次即可,以后如果有其他的任务需要用到中断,
* 都统一用这个优先级分组,千万不要再分组,切忌。
*/
NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4 );
/* LED 初始化 */
LED_GPIO_Config();
/* 串口初始化 */
USART_Config();
//按键初始化
Key_GPIO_Config();
}
vTaskNotifyGiveFromISR()
vTaskNotifyGiveFromISR()是 vTaskNotifyGive()的中断保护版本。用于在中断中向指定任务发送任务通知,并更新对方的任务通知值(加 1 操作),在某些场景中可以替代信号量操作,因为这两个通知都是不带有通知值的。

从上面的函数说明我们大概知道 vTaskNotifyGiveFromISR()函数作用,每次调用该函数都会增加任务的通知值,任务通过接收函数返回值是否大于零,判断是否获取到了通知,任务通知值初始化为 0,(如果与信号量做对比)则对应为信号量无效。当中断调用vTaskNotifyGiveFromISR()通知函数给任务的时候,任务的通知值增加,使其大于零,使其表示的通知值变为有效,任务获取有效的通知值将会被恢复。那么该函数是怎么实现的呢?下面一起来看看 vTaskNotifyGiveFromISR()函数的源码:
#if( configUSE_TASK_NOTIFICATIONS == 1 )
void vTaskNotifyGiveFromISR( TaskHandle_t xTaskToNotify,
BaseType_t *pxHigherPriorityTaskWoken )
{
TCB_t * pxTCB;
uint8_t ucOriginalNotifyState;
UBaseType_t uxSavedInterruptStatus;
configASSERT( xTaskToNotify );
portASSERT_IF_INTERRUPT_PRIORITY_INVALID();
pxTCB = ( TCB_t * ) xTaskToNotify;
/* 进入临界区 */
uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();
{
/* 保存任务通知的原始状态,
看看任务是否在等待通知,方便在发送通知后恢复任务 */
ucOriginalNotifyState = pxTCB->ucNotifyState;
/* 不管状态是怎么样的,反正现在发送通知,任务就收到任务通知 */
pxTCB->ucNotifyState = taskNOTIFICATION_RECEIVED;
/* 通知值自加,类似于信号量的释放 */
( pxTCB->ulNotifiedValue )++;
traceTASK_NOTIFY_GIVE_FROM_ISR();
/* 如果任务在阻塞等待通知 */
if( ucOriginalNotifyState == taskWAITING_NOTIFICATION ) {
/* 如果任务调度器运行中 */
if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE ) {
/* 唤醒任务,将任务从阻塞列表中移除,添加到就绪列表中 */
( void ) uxListRemove( &( pxTCB->xStateListItem ) );
prvAddTaskToReadyList( pxTCB );
} else {
/* 调度器处于挂起状态,中断依然正常发生,但是不能直接操作就绪列表
将任务加入到就绪挂起列表,任务调度恢复后会移动到就绪列表 */
vListInsertEnd( &( xPendingReadyList ),
&( pxTCB->xEventListItem ) );
}
/* 如果刚刚唤醒的任务优先级比当前任务高,
则设置上下文切换标识,等退出函数后手动切换上下文,
或者在系统节拍中断服务程序中自动切换上下文 */
if( pxTCB->uxPriority > pxCurrentTCB->uxPriority ) {
/* 设置返回参数,表示需要任务切换,在退出中断前进行任务切换 */
if( pxHigherPriorityTaskWoken != NULL ) {
*pxHigherPriorityTaskWoken = pdTRUE;
} else {
/* 设置自动切换标志 */
xYieldPending = pdTRUE;
}
} else {
mtCOVERAGE_TEST_MARKER();
}
}
}
/* 退出临界区 */
portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus );
}
#endif
xTaskNotify()
xTaskNotify()用于在任务中直接向另外一个任务发送一个事件,接收到该任务通知的任务有可能解锁。相比于 xTaskNotifyGive()函数 ,xTaskNotify()函数在发送任务通知的时候会指定一个通知值,并且用户可以指定通知值发送的方式。
任务通知在任务控制块中的定义:
#if( configUSE_TASK_NOTIFICATIONS == 1 )
volatile uint32_t ulNotifiedValue;
volatile uint8_t ucNotifyState;
#endif
xTaskNotify()函数原型:
#define xTaskNotify( xTaskToNotify, ulValue, eAction )
xTaskGenericNotify( ( xTaskToNotify ), ( ulValue ), ( eAction ), NULL )
xTaskGenericNotify()函数源码:
#if( configUSE_TASK_NOTIFICATIONS == 1 )
BaseType_t xTaskGenericNotify( TaskHandle_t xTaskToNotify, uint32_t ulValue, eNotifyAction eAction, uint32_t *pulPreviousNotificationValue )
{
TCB_t * pxTCB;
BaseType_t xReturn = pdPASS;
uint8_t ucOriginalNotifyState;
configASSERT( xTaskToNotify );
pxTCB = ( TCB_t * ) xTaskToNotify;
taskENTER_CRITICAL();
{
if( pulPreviousNotificationValue != NULL )
{
*pulPreviousNotificationValue = pxTCB->ulNotifiedValue;
}
ucOriginalNotifyState = pxTCB->ucNotifyState;
pxTCB->ucNotifyState = taskNOTIFICATION_RECEIVED;
switch( eAction )
{
case eSetBits :
pxTCB->ulNotifiedValue |= ulValue;
break;
case eIncrement :
( pxTCB->ulNotifiedValue )++;
break;
case eSetValueWithOverwrite :
pxTCB->ulNotifiedValue = ulValue;
break;
case eSetValueWithoutOverwrite :
if( ucOriginalNotifyState != taskNOTIFICATION_RECEIVED )
{
pxTCB->ulNotifiedValue = ulValue;
}
else
{
/* The value could not be written to the task. */
xReturn = pdFAIL;
}
break;
case eNoAction:
/* The task is being notified without its notify value being
updated. */
break;
}
traceTASK_NOTIFY();
/* If the task is in the blocked state specifically to wait for a
notification then unblock it now. */
if( ucOriginalNotifyState == taskWAITING_NOTIFICATION )
{
( void ) uxListRemove( &( pxTCB->xStateListItem ) );
prvAddTaskToReadyList( pxTCB );
/* The task should not have been on an event list. */
configASSERT( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) == NULL );
#if( configUSE_TICKLESS_IDLE != 0 )
{
/* If a task is blocked waiting for a notification then
xNextTaskUnblockTime might be set to the blocked task's time
out time. If the task is unblocked for a reason other than
a timeout xNextTaskUnblockTime is normally left unchanged,
because it will automatically get reset to a new value when
the tick count equals xNextTaskUnblockTime. However if
tickless idling is used it might be more important to enter
sleep mode at the earliest possible time - so reset
xNextTaskUnblockTime here to ensure it is updated at the
earliest possible time. */
prvResetNextTaskUnblockTime();
}
#endif
if( pxTCB->uxPriority > pxCurrentTCB->uxPriority )
{
/* The notified task has a priority above the currently
executing task so a yield is required. */
taskYIELD_IF_USING_PREEMPTION();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
taskEXIT_CRITICAL();
return xReturn;
}
#endif /* configUSE_TASK_NOTIFICATIONS */
xTaskNotify()函数说明:

任务通知值的状态:

这里的调用比较多,我们下面在最后进行统一举例说明,下面几个函数同样。
xTaskNotifyFromISR()
xTaskNotifyFromISR()是 xTaskNotify()的中断保护版本,真正起作用的函数是中断发送任务通知通用函数 xTaskGenericNotifyFromISR(),而 xTaskNotifyFromISR()是一个宏定义:
/**
* @brief 从中断服务程序(ISR)发送任务通知的宏
*
* @param xTaskToNotify 要通知的任务句柄
* @param ulValue 要传递的通知值(具体含义取决于eAction)
* @param eAction 通知动作类型,决定如何处理ulValue
* - eNoAction: 仅通知,不更新通知值
* - eSetBits: 按位或操作通知值
* - eIncrement: 通知值加1
* - eSetValueWithOverwrite: 覆盖通知值
* - eSetValueWithoutOverwrite: 仅在无未读通知时设置
* @param pxHigherPriorityTaskWoken 用于返回是否需要上下文切换
*
* @note 此宏实际调用xTaskGenericNotifyFromISR()函数,并将pulPreviousNotificationValue参数设为NULL
*/
#define xTaskNotifyFromISR( xTaskToNotify, \
ulValue, \
eAction, \
pxHigherPriorityTaskWoken ) \
xTaskGenericNotifyFromISR( ( xTaskToNotify ), \
( ulValue ), \
( eAction ), \
NULL, \
( pxHigherPriorityTaskWoken ) )
xTaskNotifyFromISR()函数说明:

xTaskGenericNotifyFromISR()
xTaskGenericNotifyFromISR() 是一个在中断中发送任务通知的通用函数,xTaskNotifyFromISR()、xTaskNotifyAndQueryFromISR()等函数都是以其为基础,采用宏定义的方式实现。xTaskGenericNotifyFromISR()的源码:
#if (configUSE_TASK_NOTIFICATIONS == 1)
/**
* @brief 从中断服务程序(ISR)发送通用任务通知
* @param xTaskToNotify 要通知的任务句柄
* @param ulValue 要传递的通知值
* @param eAction 通知动作类型
* @param pulPreviousNotificationValue 用于返回之前的通知值(可选)
* @param pxHigherPriorityTaskWoken 用于返回是否需要上下文切换
* @return pdPASS 成功, pdFAIL 仅当eSetValueWithoutOverwrite且已有未读通知时
*/
BaseType_t xTaskGenericNotifyFromISR(TaskHandle_t xTaskToNotify,
uint32_t ulValue,
eNotifyAction eAction,
uint32_t *pulPreviousNotificationValue,
BaseType_t *pxHigherPriorityTaskWoken)
{
TCB_t *pxTCB;
uint8_t ucOriginalNotifyState;
BaseType_t xReturn = pdPASS;
UBaseType_t uxSavedInterruptStatus;
configASSERT(xTaskToNotify);
portASSERT_IF_INTERRUPT_PRIORITY_INVALID();
pxTCB = (TCB_t *)xTaskToNotify;
/* 进入中断临界区 */
uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();
{
/* 返回之前的通知值(如果请求) */
if (pulPreviousNotificationValue != NULL) {
*pulPreviousNotificationValue = pxTCB->ulNotifiedValue;
}
/* 保存原始通知状态 */
ucOriginalNotifyState = pxTCB->ucNotifyState;
/* 标记任务已收到通知 */
pxTCB->ucNotifyState = taskNOTIFICATION_RECEIVED;
/* 根据动作类型处理通知值 */
switch (eAction) {
/* 按位或操作通知值 */
case eSetBits:
pxTCB->ulNotifiedValue |= ulValue;
break;
/* 递增通知值 */
case eIncrement:
(pxTCB->ulNotifiedValue)++;
break;
/* 覆盖写入通知值 */
case eSetValueWithOverwrite:
pxTCB->ulNotifiedValue = ulValue;
break;
/* 仅在无未读通知时写入 */
case eSetValueWithoutOverwrite:
if (ucOriginalNotifyState != taskNOTIFICATION_RECEIVED) {
pxTCB->ulNotifiedValue = ulValue;
} else {
xReturn = pdFAIL; /* 已有未读通知,写入失败 */
}
break;
/* 仅通知不修改值 */
case eNoAction:
break;
}
traceTASK_NOTIFY_FROM_ISR();
/* 如果任务正在等待通知 */
if (ucOriginalNotifyState == taskWAITING_NOTIFICATION) {
if (uxSchedulerSuspended == (UBaseType_t)pdFALSE) {
/* 将任务移出阻塞列表,加入就绪列表 */
(void)uxListRemove(&(pxTCB->xStateListItem));
prvAddTaskToReadyList(pxTCB);
} else {
/* 调度器挂起时加入待处理就绪列表 */
vListInsertEnd(&(xPendingReadyList),
&(pxTCB->xEventListItem));
}
/* 检查是否需要任务切换 */
if (pxTCB->uxPriority > pxCurrentTCB->uxPriority) {
if (pxHigherPriorityTaskWoken != NULL) {
*pxHigherPriorityTaskWoken = pdTRUE;
} else {
xYieldPending = pdTRUE;
}
} else {
mtCOVERAGE_TEST_MARKER();
}
}
}
/* 退出中断临界区 */
portCLEAR_INTERRUPT_MASK_FROM_ISR(uxSavedInterruptStatus);
return xReturn;
}
#endif /* configUSE_TASK_NOTIFICATIONS */
xTaskNotifyAndQuery()
xTaskNotifyAndQuery()与 xTaskNotify()很像,都是调用通用的任务通知发送函数xTaskGenericNotify() 来实现通知的发送,不同的是多了一个附加的参数 pulPreviousNotifyValue 用于回传接收任务的上一个通知值,函数原型:
/**
* @brief 发送任务通知并查询先前通知值的宏
*
* @param xTaskToNotify 要通知的任务句柄
* @param ulValue 要传递的通知值
* @param eAction 通知动作类型:
* - eNoAction: 仅通知不修改值
* - eSetBits: 按位或操作
* - eIncrement: 值递增
* - eSetValueWithOverwrite: 覆盖写入
* - eSetValueWithoutOverwrite: 无覆盖写入
* @param pulPreviousNotifyValue 用于返回先前的通知值
*
* @note 此宏封装了xTaskGenericNotify(),提供更简洁的查询接口
* @note 必须先确保configUSE_TASK_NOTIFICATIONS == 1
*/
#define xTaskNotifyAndQuery( xTaskToNotify, \
ulValue, \
eAction, \
pulPreviousNotifyValue ) \
xTaskGenericNotify( ( xTaskToNotify ), \
( ulValue ), \
( eAction ), \
( pulPreviousNotifyValue ) )

xTaskNotifyAndQueryFromISR()
xTaskNotifyAndQueryFromISR()是 xTaskNotifyAndQuery ()的中断版本,用于向指定的任务发送一个任务通知,并返回对象任务的上一个通知值,该函数也是一个宏定义,真正实现发送通知的是 xTaskGenericNotifyFromISR()。xTaskNotifyAndQueryFromISR()函数说明:

获取任务通知函数
获取任务通知函数只能用在任务中,没有带中断保护版本,因此只有两个API 函数: ulTaskNotifyTake() 和 xTaskNotifyWait ()。前者是为代替二值信号量和计数信号量而专门设计的,它和发送通知 API 函数 xTaskNotifyGive()、vTaskNotifyGiveFromISR()配合使用;后者是全功能版的等待通知,可以根据不同的参数实现轻量级二值信号量、计数信号量、事件组和长度为 1 的队列。
所有的获取任务通知 API 函数都带有指定阻塞超时时间参数,当任务因为等待通知而进入阻塞时,用来指定任务的阻塞时间,这些超时机制与 FreeRTOS 的消息队列、信号量、事件等的超时机制一致。
ulTaskNotifyTake()
ulTaskNotifyTake() 作为二值信号量和计数信号量的一种轻量级实现,速度更快。如果 FreeRTOS 中使用函数 xSemaphoreTake() 来获取信号量,这个时候则可以试试使用函数 ulTaskNotifyTake()来代替。
对于这个函数,任务通知值为 0,对应信号量无效,如果任务设置了阻塞等待,任务被阻塞挂起。当其他任务或中断发送了通知值使其不为 0 后,通知变为有效,等待通知的任务将获取到通知,并且在退出时候根据用户传递的第一个参数 xClearCountOnExit 选择清零通知值或者执行减一操作。
xTaskNotifyTake()在退出的时候处理任务的通知值的时候有两种方法:
- 一种是在函数退出时将通知值清零,这种方法适用于实现二值信号量;
- 另外一种是在函数退出时将通知值减 1,这种方法适用于实现计数信号量。

ulTaskNotifyTake()源码:
#if (configUSE_TASK_NOTIFICATIONS == 1)
/**
* @brief 等待并获取任务通知值
* @param xClearCountOnExit 退出时是否清零通知值
* - pdTRUE: 清零通知值
* - pdFALSE: 通知值减1(类似计数信号量)
* @param xTicksToWait 最大等待时间(ticks)
* @return 获取到的通知值(可能为0表示超时)
*/
uint32_t ulTaskNotifyTake(BaseType_t xClearCountOnExit,
TickType_t xTicksToWait)
{
uint32_t ulReturn;
/* 进入临界区 */
taskENTER_CRITICAL();
{
/* 检查当前通知值,进入临界区,先看看任务通知值是否有效,有效才能获取,无效则根据指定超时时间等待,标记一下任务状态,表示任务在等待通知。任务通知在任务初始化的时候是默认为无效的 */
// 如果通知值为 0 ,阻塞任务
// 默认初始化通知值为 0, 说明没有未读通知
if (pxCurrentTCB->ulNotifiedValue == 0UL) {
/* 设置任务为等待通知状态 */
pxCurrentTCB->ucNotifyState = taskWAITING_NOTIFICATION;
/* 如果需要阻塞等待 */
if (xTicksToWait > (TickType_t)0) {
/* 将任务添加到延迟列表 */
prvAddCurrentTaskToDelayedList(xTicksToWait, pdTRUE);
traceTASK_NOTIFY_TAKE_BLOCK();
/* 执行任务切换 */
portYIELD_WITHIN_API();
} else {
mtCOVERAGE_TEST_MARKER();
}
} else {
mtCOVERAGE_TEST_MARKER();
}
}
taskEXIT_CRITICAL();
/* 再次进入临界区处理通知值 */
taskENTER_CRITICAL();
{
traceTASK_NOTIFY_TAKE();
ulReturn = pxCurrentTCB->ulNotifiedValue;
/* 处理有效通知值 */
if (ulReturn != 0UL) {
if (xClearCountOnExit != pdFALSE) {
/* 清零通知值 */
pxCurrentTCB->ulNotifiedValue = 0UL;
} else {
/* 通知值减1(模拟计数信号量) */
pxCurrentTCB->ulNotifiedValue = ulReturn - 1;
}
} else {
mtCOVERAGE_TEST_MARKER();
}
/* 重置任务通知状态 */
pxCurrentTCB->ucNotifyState = taskNOT_WAITING_NOTIFICATION;
}
taskEXIT_CRITICAL();
return ulReturn;
}
#endif /* configUSE_TASK_NOTIFICATIONS */
二值信号量
这里其他部分代码不需要更改,主要是对接收部分的更改:
//Receive_Task任务主体
static void Receive1_Task(void* parameter)
{
while (1)
{
/* uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit, TickType_t xTicksToWait );
* xClearCountOnExit:
* pdTRUE 在退出函数的时候任务任务通知值清零,类似二值信号量
* pdFALSE 在退出函数ulTaskNotifyTakeO的时候任务通知值减一,类似计数型信号量。
*/
//获取任务通知 ,没获取到则一直等待
ulTaskNotifyTake(pdTRUE,portMAX_DELAY);
printf("Receive1_Task 任务通知获取成功!\r\n");
Toggle_LED_R();
}
}
//Receive_Task任务主体
static void Receive2_Task(void* parameter)
{
while (1)
{
/* uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit, TickType_t xTicksToWait );
* xClearCountOnExit:
* pdTRUE 在退出函数的时候任务任务通知值清零,类似二值信号量
* pdFALSE 在退出函数ulTaskNotifyTakeO的时候任务通知值减一,类似计数型信号量。
*/
//获取任务通知 ,没获取到则一直等待
ulTaskNotifyTake(pdTRUE,portMAX_DELAY);
printf("Receive2_Task 任务通知获取成功!\r\n");
Toggle_LED_R();
}
}
完整main函数代码:
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "LED.h"
#include "Usart.h"
#include "Key.h"
/* FreeRTOS头文件 */
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
/*
* 任务句柄是一个指针,用于指向一个任务,当任务创建好之后,它就具有了一个任务句柄
* 以后我们要想操作这个任务都需要通过这个任务句柄,如果是自身的任务操作自己,那么
* 这个句柄可以为NULL。
*/
static TaskHandle_t AppTaskCreate_Handle = NULL; /* 创建任务句柄 */
//接收相关
static TaskHandle_t Receive1_Task_Handle = NULL;/* Receive1_Task任务句柄 */
static TaskHandle_t Receive2_Task_Handle = NULL;/* Receive2_Task任务句柄 */
//发送相关
static TaskHandle_t Send_Task_Handle = NULL;/* Send_Task任务句柄 */
/********************************** 内核对象句柄 *********************************/
/*
* 信号量,消息队列,事件标志组,软件定时器这些都属于内核的对象,要想使用这些内核
* 对象,必须先创建,创建成功之后会返回一个相应的句柄。实际上就是一个指针,后续我
* 们就可以通过这个句柄操作这些内核对象。
*
* 内核对象说白了就是一种全局的数据结构,通过这些数据结构我们可以实现任务间的通信,
* 任务间的事件同步等各种功能。至于这些功能的实现我们是通过调用这些内核对象的函数
* 来完成的
*
*/
/******************************* 宏定义 ************************************/
/*
* 当我们在写应用程序的时候,可能需要用到一些宏定义。
*/
//一些函数声明
static void AppTaskCreate(void);/* 用于创建任务 */
static void Receive1_Task(void* pvParameters);/* Receive1_Task任务实现 */
static void Receive2_Task(void* pvParameters);/* Receive2_Task任务实现 */
static void Send_Task(void* pvParameters);/* Send_Task任务实现 */
static void All_Function_Init(void);/* 用于初始化板载相关资源 */
int main(void)
{
BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
All_Function_Init();//硬件初始化
while (1)
{
/* 创建AppTaskCreate任务 */
xReturn = xTaskCreate((TaskFunction_t )AppTaskCreate, /* 任务入口函数 */
(const char* )"AppTaskCreate",/* 任务名字 */
(uint16_t )512, /* 任务栈大小 */
(void* )NULL,/* 任务入口函数参数 */
(UBaseType_t )1, /* 任务的优先级 */
(TaskHandle_t* )&AppTaskCreate_Handle);/* 任务控制块指针 */
/* 启动任务调度 */
if(pdPASS == xReturn)
vTaskStartScheduler(); /* 启动任务,开启调度 */
else
return -1;
}
}
//任务创建函数
static void AppTaskCreate(void)
{
BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
taskENTER_CRITICAL(); //进入临界区
/* 创建Receive1_Task任务 */
xReturn = xTaskCreate((TaskFunction_t )Receive1_Task, /* 任务入口函数 */
(const char* )"Receive1_Task",/* 任务名字 */
(uint16_t )512, /* 任务栈大小 */
(void* )NULL, /* 任务入口函数参数 */
(UBaseType_t )2, /* 任务的优先级 */
(TaskHandle_t* )&Receive1_Task_Handle);/* 任务控制块指针 */
if(pdPASS == xReturn)
printf("创建Receive1_Task任务成功!\r\n");
/* 创建Receive2_Task任务 */
xReturn = xTaskCreate((TaskFunction_t )Receive2_Task, /* 任务入口函数 */
(const char* )"Receive2_Task",/* 任务名字 */
(uint16_t )512, /* 任务栈大小 */
(void* )NULL, /* 任务入口函数参数 */
(UBaseType_t )3, /* 任务的优先级 */
(TaskHandle_t* )&Receive2_Task_Handle);/* 任务控制块指针 */
if(pdPASS == xReturn)
printf("创建Receive2_Task任务成功!\r\n");
/* 创建Send_Task任务 */
xReturn = xTaskCreate((TaskFunction_t )Send_Task, /* 任务入口函数 */
(const char* )"Send_Task",/* 任务名字 */
(uint16_t )512, /* 任务栈大小 */
(void* )NULL,/* 任务入口函数参数 */
(UBaseType_t )4, /* 任务的优先级 */
(TaskHandle_t* )&Send_Task_Handle);/* 任务控制块指针 */
if(pdPASS == xReturn)
printf("创建Send_Task任务成功!\r\n");
vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务
taskEXIT_CRITICAL(); //退出临界区
}
//Receive_Task任务主体
static void Receive1_Task(void* parameter)
{
while (1)
{
/* uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit, TickType_t xTicksToWait );
* xClearCountOnExit:pdTRUE 在退出函数的时候任务任务通知值清零,类似二值信号量
* pdFALSE 在退出函数ulTaskNotifyTakeO的时候任务通知值减一,类似计数型信号量。
*/
//获取任务通知 ,没获取到则一直等待
ulTaskNotifyTake(pdTRUE,portMAX_DELAY);
printf("Receive1_Task 任务通知获取成功!\r\n");
Toggle_LED_R();
}
}
//Receive_Task任务主体
static void Receive2_Task(void* parameter)
{
while (1)
{
/* uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit, TickType_t xTicksToWait );
* xClearCountOnExit:
* pdTRUE 在退出函数的时候任务任务通知值清零,类似二值信号量
* pdFALSE 在退出函数ulTaskNotifyTakeO的时候任务通知值减一,类似计数型信号量。
*/
//获取任务通知 ,没获取到则一直等待
ulTaskNotifyTake(pdTRUE,portMAX_DELAY);
printf("Receive2_Task 任务通知获取成功!\r\n");
Toggle_LED_R();
}
}
//Send_Task任务主体
static void Send_Task(void* parameter)
{
BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
while (1)
{
/* KEY1 被按下 */
if( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON )
{
/* 原型:BaseType_t xTaskNotifyGive( TaskHandle_t xTaskToNotify ); */
xReturn = xTaskNotifyGive(Receive1_Task_Handle);
/* 此函数只会返回pdPASS */
if( xReturn == pdTRUE )
printf("\r\nReceive1_Task_Handle 任务通知发送成功!\r\n");
}
/* KEY2 被按下 */
if( Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN) == KEY_ON )
{
xReturn = xTaskNotifyGive(Receive2_Task_Handle);
/* 此函数只会返回pdPASS */
if( xReturn == pdPASS )
printf("\r\nReceive2_Task_Handle 任务通知发送成功!\r\n");
}
vTaskDelay(20);
}
}
//初始化声明
static void All_Function_Init(void)
{
/*
* STM32中断优先级分组为4,即4bit都用来表示抢占优先级,范围为:0~15
* 优先级分组只需要分组一次即可,以后如果有其他的任务需要用到中断,
* 都统一用这个优先级分组,千万不要再分组,切忌。
*/
NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4 );
/* LED 初始化 */
LED_GPIO_Config();
/* 串口初始化 */
USART_Config();
//按键初始化
Key_GPIO_Config();
}
运行结果:

计数信号量
同样别的不需要改,仅改动接收相关的代码:
//Take_Task 任务主体
static void Take_Task(void* parameter)
{
uint32_t take_num = pdTRUE;/* 定义一个创建信息返回值,默认为pdPASS */
/* 任务都是一个无限循环,不能返回 */
while (1)
{
//如果KEY1被单击
if( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON )
{
/* uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit, TickType_t xTicksToWait );
* xClearCountOnExit:
* pdTRUE 在退出函数的时候任务任务通知值清零,类似二值信号量
* pdFALSE 在退出函数ulTaskNotifyTakeO的时候任务通知值减一,类似计数型信号量。
*/
//获取任务通知 ,没获取到则不等待
take_num=ulTaskNotifyTake(pdFALSE,0);
if(take_num > 0)
printf( "\r\nKEY1被按下,成功申请到停车位,当前剩余车位为:%d \r\n", take_num - 1);
else
printf( "\r\nKEY1被按下,车位已经没有了,请按KEY2释放车位!\r\n" );
}
vTaskDelay(20); //每20ms扫描一次
}
}
完整main函数代码:
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "LED.h"
#include "Usart.h"
#include "Key.h"
/* FreeRTOS头文件 */
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
//#include "semphr.h"
/*
* 任务句柄是一个指针,用于指向一个任务,当任务创建好之后,它就具有了一个任务句柄
* 以后我们要想操作这个任务都需要通过这个任务句柄,如果是自身的任务操作自己,那么
* 这个句柄可以为NULL。
*/
static TaskHandle_t AppTaskCreate_Handle = NULL; /* 创建任务句柄 */
static TaskHandle_t Take_Task_Handle = NULL;/* Take_Task任务句柄 */
static TaskHandle_t Give_Task_Handle = NULL;/* Give_Task任务句柄 */
/********************************** 内核对象句柄 *********************************/
/*
* 信号量,消息队列,事件标志组,软件定时器这些都属于内核的对象,要想使用这些内核
* 对象,必须先创建,创建成功之后会返回一个相应的句柄。实际上就是一个指针,后续我
* 们就可以通过这个句柄操作这些内核对象。
*
* 内核对象说白了就是一种全局的数据结构,通过这些数据结构我们可以实现任务间的通信,
* 任务间的事件同步等各种功能。至于这些功能的实现我们是通过调用这些内核对象的函数
* 来完成的
*
*/
/******************************* 宏定义 ************************************/
/*
* 当我们在写应用程序的时候,可能需要用到一些宏定义。
*/
//一些数据变量声明
static volatile uint32_t s_notify_count = 0; // 全局计数器
//一些函数声明
static void AppTaskCreate(void);/* 用于创建任务 */
static void Take_Task(void* pvParameters);/* Take_Task任务实现 */
static void Give_Task(void* pvParameters);/* Give_Task任务实现 */
static void All_Function_Init(void);/* 用于初始化板载相关资源 */
int main(void)
{
BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
All_Function_Init();//硬件初始化
while (1)
{
/* 创建AppTaskCreate任务 */
xReturn = xTaskCreate((TaskFunction_t )AppTaskCreate, /* 任务入口函数 */
(const char* )"AppTaskCreate",/* 任务名字 */
(uint16_t )512, /* 任务栈大小 */
(void* )NULL,/* 任务入口函数参数 */
(UBaseType_t )1, /* 任务的优先级 */
(TaskHandle_t* )&AppTaskCreate_Handle);/* 任务控制块指针 */
/* 启动任务调度 */
if(pdPASS == xReturn)
vTaskStartScheduler(); /* 启动任务,开启调度 */
else
return -1;
}
}
//任务创建函数
static void AppTaskCreate(void)
{
BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
taskENTER_CRITICAL(); //进入临界区
/* 创建Take_Task任务 */
xReturn = xTaskCreate((TaskFunction_t )Take_Task, /* 任务入口函数 */
(const char* )"Take_Task",/* 任务名字 */
(uint16_t )512, /* 任务栈大小 */
(void* )NULL, /* 任务入口函数参数 */
(UBaseType_t )2, /* 任务的优先级 */
(TaskHandle_t* )&Take_Task_Handle);/* 任务控制块指针 */
if(pdPASS == xReturn)
printf("创建Take_Task任务成功!\r\n");
/* 创建Give_Task任务 */
xReturn = xTaskCreate((TaskFunction_t )Give_Task, /* 任务入口函数 */
(const char* )"Give_Task",/* 任务名字 */
(uint16_t )512, /* 任务栈大小 */
(void* )NULL,/* 任务入口函数参数 */
(UBaseType_t )3, /* 任务的优先级 */
(TaskHandle_t* )&Give_Task_Handle);/* 任务控制块指针 */
if(pdPASS == xReturn)
printf("创建Give_Task任务成功!\r\n");
vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务
taskEXIT_CRITICAL(); //退出临界区
}
//Take_Task 任务主体
static void Take_Task(void* parameter)
{
uint32_t take_num = pdTRUE;/* 定义一个创建信息返回值,默认为pdPASS */
/* 任务都是一个无限循环,不能返回 */
while (1)
{
//如果KEY1被单击
if( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON )
{
/* uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit, TickType_t xTicksToWait );
* xClearCountOnExit:
* pdTRUE 在退出函数的时候任务任务通知值清零,类似二值信号量
* pdFALSE 在退出函数ulTaskNotifyTakeO的时候任务通知值减一,类似计数型信号量。
*/
//获取任务通知 ,没获取到则不等待
take_num=ulTaskNotifyTake(pdFALSE,0);
if(take_num > 0)
printf( "\r\nKEY1被按下,成功申请到停车位,当前剩余车位为:%d \r\n", take_num - 1);
else
printf( "\r\nKEY1被按下,车位已经没有了,请按KEY2释放车位!\r\n" );
}
vTaskDelay(20); //每20ms扫描一次
}
}
//Give_Task 任务主体
static void Give_Task(void* parameter)
{
BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
/* 任务都是一个无限循环,不能返回 */
while (1)
{
//如果KEY2被单击
if( Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN) == KEY_ON )
{
/* 原型:BaseType_t xTaskNotifyGive( TaskHandle_t xTaskToNotify ); */
/* 释放一个任务通知 */
xTaskNotifyGive(Take_Task_Handle);//发送任务通知
/* 此函数只会返回pdPASS */
if ( pdPASS == xReturn )
{
s_notify_count++;
printf("\r\nKEY2被按下,释放1个停车位,当前剩余车位为: %lu\r\n" ,s_notify_count);
}
}
vTaskDelay(20); //每20ms扫描一次
}
}
//初始化声明
static void All_Function_Init(void)
{
/*
* STM32中断优先级分组为4,即4bit都用来表示抢占优先级,范围为:0~15
* 优先级分组只需要分组一次即可,以后如果有其他的任务需要用到中断,
* 都统一用这个优先级分组,千万不要再分组,切忌。
*/
NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4 );
/* LED 初始化 */
LED_GPIO_Config();
/* 串口初始化 */
USART_Config();
//按键初始化
Key_GPIO_Config();
}
运行结果:

xTaskNotifyWait()
xTaskNotifyWait()函数用于实现全功能版的等待任务通知,根据用户指定的参数的不同,可以灵活的用于实现轻量级的消息队列队列、二值信号量、计数信号量和事件组功能,并带有超时等待。

xTaskNotifyWait()源码:
#if (configUSE_TASK_NOTIFICATIONS == 1)
/**
* @brief 等待任务通知并可选地清除通知值的指定位
* @param ulBitsToClearOnEntry 进入时清除通知值的位掩码
* @param ulBitsToClearOnExit 退出时清除通知值的位掩码
* @param pulNotificationValue 用于返回通知值的指针(可选)
* @param xTicksToWait 最大等待时间(ticks)
* @return pdTRUE 收到通知, pdFALSE 超时
*/
BaseType_t xTaskNotifyWait(uint32_t ulBitsToClearOnEntry,
uint32_t ulBitsToClearOnExit,
uint32_t *pulNotificationValue,
TickType_t xTicksToWait)
{
BaseType_t xReturn;
/* 进入临界区 */
taskENTER_CRITICAL();
{
/* 检查当前是否有未处理的通知,只有任务当前没有收到任务通知,才会将任务阻塞 */
if (pxCurrentTCB->ucNotifyState != taskNOTIFICATION_RECEIVED) {
/* 清除指定的通知值位 */
pxCurrentTCB->ulNotifiedValue &= ~ulBitsToClearOnEntry;
/* 设置任务为等待通知状态 */
pxCurrentTCB->ucNotifyState = taskWAITING_NOTIFICATION;
/* 如果需要阻塞等待 */
if (xTicksToWait > (TickType_t)0) {
/* 将任务添加到延迟列表 */
prvAddCurrentTaskToDelayedList(xTicksToWait, pdTRUE);
traceTASK_NOTIFY_WAIT_BLOCK();
/* 执行任务切换 */
portYIELD_WITHIN_API();
} else {
mtCOVERAGE_TEST_MARKER();
}
} else {
mtCOVERAGE_TEST_MARKER();
}
}
taskEXIT_CRITICAL();
/* 再次进入临界区处理结果程序能执行到这里说明其它任务或中断向这个任务发送了通知或者任务阻塞超时,任务从阻塞态变成运行态,现在继续处理。 */
taskENTER_CRITICAL();
{
traceTASK_NOTIFY_WAIT();
/* 返回当前通知值(如果请求) */
if (pulNotificationValue != NULL) {
*pulNotificationValue = pxCurrentTCB->ulNotifiedValue;
}
/* 检查是否成功收到通知 */
if (pxCurrentTCB->ucNotifyState == taskWAITING_NOTIFICATION) {
/* 超时返回 */
xReturn = pdFALSE;
} else {
/* 清除指定的退出位 */
pxCurrentTCB->ulNotifiedValue &= ~ulBitsToClearOnExit;
xReturn = pdTRUE;
}
/* 重置任务通知状态 */
pxCurrentTCB->ucNotifyState = taskNOT_WAITING_NOTIFICATION;
}
taskEXIT_CRITICAL();
return xReturn;
}
#endif /* configUSE_TASK_NOTIFICATIONS */
示例演示较为复杂,这里不进行展开描述
详细的可以参考这本书


浙公网安备 33010602011771号