下面是任务通知得一些特点:
1:我们使用队列、信号量、事件组等等方法时,并不知道对方是谁。使用任务通知时,可以明确指定:通 知哪个任务。
2:使用任务通知时,任务结构体TCB中就包含了内部对象,可以直接接收别人发过来的"通知",使用队列、信号量、事件组时,我们都要事先创建对应的结构体,双方通过中间的结构体通信。
下面是任务通知的优势和局限:
任务通知的优势:
效率更高:使用任务通知来发送事件、数据给某个任务时,效率更高。比队列、信号量、事件组都 有大的优势。
更节省内存:使用其他方法时都要先创建对应的结构体,使用任务通知时无需额外创建结构体。 任务通知的限制:
不能发送数据给ISR: ISR并没有任务结构体,所以无法使用任务通知的功能给ISR发送数据。但是ISR可以使用任务通知 的功能,发数据给任务。
数据只能给该任务独享 使用队列、信号量、事件组时,数据保存在这些结构体中,其他任务、ISR都可以访问这些数据。 使用任务通知时,数据存放入目标任务中,只有它可以访问这些数据。 在日常工作中,这个限制影响不大。因为很多场合是从多个数据源把数据发给某个任务,而不是把 一个数据源的数据发给多个任务。
无法缓冲数据 使用队列时,假设队列深度为N,那么它可以保持N个数据。 使用任务通知时,任务结构体中只有一个任务通知值,只能保持一个数据。 无法广播给多个任务 使用事件组可以同时给多个任务发送事件。
使用任务通知,只能发个一个任务。 如果发送受阻,发送方无法进入阻塞状态等待 假设队列已经满了,使用 xQueueSendToBack() 给队列发送数据时,任务可以进入阻塞状态等待 发送完成。
使用任务通知时,即使对方无法接收数据,发送方也无法阻塞等待,只能即刻返回错误。
创建的任务函数都具有TCB结构体,这个结构体中含有两个链表成员,介绍如下图所示:(使用任务通知的时候,就会使用到这两个成员)。
任务通知有两套函数,如下图所示的两套函数:(简单版是从专业版简化出来的)
函数的入口参数的使用,参考手册就可以,可以实现各种轻量级的使用。任务通知的通知值可以有很多种:计数值、 位(类似事件组)、 任意数值。
这样实现有两种有点:不要再在主函数中创建任何通道,可以将数据对应的传输到指定的任务函数中,只需要传入对应的任务函数的句柄就可以了。
下面代码是使用简化版的任务通知的示例:
xTaskNotifyGive()函数每次调用,使得通知值加一 并使得通知状态变为"pending",也就是 taskNOTIFICATION_RECEIVED ,表示有数据了、待处理。
ulTaskNotifyTake()任务函数的返回值就是”通知值“,函数具体返回值状况要看函数入口参数的配置,配置形式如下图所示:(如果函数第一个入口参数设置为:pdTRUE,那么函数只task一次通知值,就会删除通知值)
static void vCheckTask1( void *pvParameters )
{
uint16_t data=1;
for( ;; )
{
data++;
printf("task1 complete\r\n");
xQueueSend(QueueUsart1,&data,0);
for(int i=0;i<10;i++)
xTaskNotifyGive(vCheckTask2_hander);
vTaskDelay(10);
}
}
static void vCheckTask2( void *pvParameters )
{
uint16_t data=2;
uint32_t data2;
for( ;; )
{
data2=ulTaskNotifyTake(pdFALSE,portMAX_DELAY);
data++;
printf("data2:%d\r\n",data2);
xQueueSend(QueueUsart2,&data,0);
for(int i=0;i<10;i++)
{
xTaskNotifyGive(vCheckTask3_hander);
}
vTaskDelay(10);
}
}
static void vCheckTask3( void *pvParameters )
{
uint16_t data1,data2;
uint32_t data3;
while(1)
{
data3=ulTaskNotifyTake(pdFALSE,portMAX_DELAY);
printf("data3:%d\r\n",data3);
xQueueReceive(QueueUsart1,&data1,0);
xQueueReceive(QueueUsart2,&data2,0);
printf("data1:%d,data2:%d\r\n",data1,data2);
vTaskDelay(10);
}
}
int main( void )
{
prvSetupHardware();
QueueUsart1=xQueueCreate(2,sizeof(int));
QueueUsart2=xQueueCreate(2,sizeof(int));
xTaskCreate( vCheckTask1, "Check1", 100, NULL,1, &vCheckTask1_hander);
xTaskCreate( vCheckTask2, "Check2", 100, NULL,2, &vCheckTask2_hander);
xTaskCreate( vCheckTask3, "Check3", 100, NULL,3, &vCheckTask3_hander);
vTaskStartScheduler();
return 0;
}
使用debug调试的结果如下图所示:
任务通知实现的轻量级队列有以下的优点和缺点。
优点:代码简单,无需创建信号量,消耗内存更少,效率更高。
缺点:传输的数据只能是一个32位的数据,接收任务只能从自己的通知值中得到 数据,来源明确。
这次使用的是全功能的任务通知函数,xTaskNotify()函数中有的入口参数中有是否覆盖数据的选项,这个入口参数在循环调用xTaskNotify()函数传入数据的时候,会使用到这个是否覆盖的入口参数。下面是测试代码:
static void vCheckTask1( void *pvParameters )
{
uint16_t data=1;
for( ;; )
{
data++;
printf("task1 complete\r\n");
xQueueSend(QueueUsart1,&data,0);
xTaskNotify(vCheckTask3_hander,data,eSetValueWithOverwrite);
vTaskDelay(10);
}
}
static void vCheckTask2( void *pvParameters )
{
uint16_t data=2;
uint32_t data2;
for( ;; )
{
data++;
printf("task2 complete\r\n");
xQueueSend(QueueUsart2,&data,0);
xTaskNotify(vCheckTask3_hander,data,eSetValueWithOverwrite);
vTaskDelay(10);
}
}
static void vCheckTask3( void *pvParameters )
{
uint16_t data1,data2;
uint32_t data3[2];
while(1)
{
xTaskNotifyWait(0,0,&data3[0],portMAX_DELAY);
xTaskNotifyWait(0,0,&data3[1],portMAX_DELAY);
xQueueReceive(QueueUsart1,&data1,0);
xQueueReceive(QueueUsart2,&data2,0);
printf("data3:%d,data4:%d\r\n",data3[0],data3[1]);
printf("data1:%d,data2:%d\r\n",data1,data2);
vTaskDelay(10);
}
}
int main( void )
{
prvSetupHardware();
QueueUsart1=xQueueCreate(2,sizeof(int));
QueueUsart2=xQueueCreate(2,sizeof(int));
xTaskCreate( vCheckTask1, "Check1", 100, NULL,1, &vCheckTask1_hander);
xTaskCreate( vCheckTask2, "Check2", 100, NULL,2, &vCheckTask2_hander);
xTaskCreate( vCheckTask3, "Check3", 100, NULL,3, &vCheckTask3_hander);
vTaskStartScheduler();
return 0;
}
下面是debug的测试结果: