目录
任务通知
优势
限制
任务状态和通知值
事件通知函数
xTaskNotifyGive/xTaskNotifyTake
xTaskNotify/xTaskNotifyWait
应用场景:传输计数值
应用场景:传输任意值
使用队列、信号量、事件组等方法时,无法知道发送方身份。使用任务通知时,可以明确指定:通知哪个任务。
效率更高。
使用任务通知来发送事件、数据给某个任务时,效率更高。比队列、信号量、事件组都有优势。
更节省内存。
使用其他方法时都要先创建对应的结构体,使用任务通知时无需额外创建结构体。
不能发送数据给ISR
ISR并没有任务结构体,所以无法使用任务通知的功能给ISR发送数据。但是ISR可以使用任务通知的功能,发数据给任务。
数据只能给该任务独享
使用队列、信号量、事件组时,数据保存在结构体中,其他任务、ISR都可以访问结构体的数据。
使用任务通知时,数据存放入目标任务中,只有它可以访问这些数据。
在日常工作中,这个限制影响不大。因为很多场合是从多个数据源把数据发给某个任务,而不是把一个数据源的数据发给多个任务。
无法缓存数据
使用队列时,假设队列深度为N,则它可以保存N个数据。
使用任务通知时,任务结构体只有一个任务通知值,只能保存一个数据。
无法广播给多个任务
使用事件组时,可以同时给多个任务发送事件。
使用任务通知时,只能发一个任务。
如果发送受阻,发送方无法进入阻塞状态等待
假设队列已满,使用xQueueSendToBack()给队列发送数据时,任务可以进入阻塞状态等待发送完成。
使用任务通知时,计数接双方无法接收数据,发送方也无法阻塞等待,只能立即返回错误。
每个人物都有一个结构体:TCB。里面有两个成员:uint8_t ucNotifyState(表示通知状态)、uint32_t ulNotifiedValue(表示通知值)。
typedef struct tskTaskControlBlock { ...... /* configTASK_NOTIFICATION_ARRAY_ENTRIES = 1 */ volatile uint32_t ulNotifiedValue[ configTASK_NOTIFICATION_ARRAY_ENTRIES ]; volatile uint8_t ucNotifyState[ configTASK_NOTIFICATION_ARRAY_ENTRIES ]; ...... } tskTCB;
ucNotifyState有三种取值:
##define taskNOT_WAITING_NOTIFICATION ( ( uint8_t ) 0 ) /* 默认状态,任务没有在等待通知 */ ##define taskWAITING_NOTIFICATION ( ( uint8_t ) 1 ) /* 任务在等待通知 */ ##define taskNOTIFICATION_RECEIVED ( ( uint8_t ) 2 ) /* 任务接收到了通知 */
ulNotifiedValue可以有很多类型:
计数值、位(类似事件组)、任意数值。
使用任务通知,可以实现轻量级的队列(长度为1)、邮箱(覆盖的队列)、计数型信号量、二进制信号量、事件组。
简化版,调用简单 | 专业版,参数很多 | |
发出通知 | xTaskNotifyGive xTaskNotifyGiveFromISR |
xTaskNotify xTaskNotifyFromISR |
取出通知 | xTaskNotifyTake | xTaskNotifyWait |
xTaskNotifyGive / xTaskNotifyGiveFromISR都是直接给其他任务发送通知,使得通知值+1,并使得通知状态变为taskNOTIFICATION_RECEIVED,表示有数据了,待处理。
xTaskNotifyTake取出通知值。可以实现轻量级的、高效的二进制信号量、计数型信号量。
如果通知值为0,则阻塞(可以指定超时时间)。
如果通知值大于0,任务从阻塞态进入就绪态。
在该函数返回之前,还可以做一些清理工作:把通知值-1,或清零通知值。
/* xTaskToNotify:任务句柄 */
/* 返回值:只能是pdPASS */
BaseType_t xTaskNotifyGive( TaskHandle_t xTaskToNotify );
/* xTaskToNotify:任务句柄 */
/* pxHigherPriorityTaskWoken:
被通知的任务可能正处于阻塞状态。此函数发出通知后,会把它从阻塞状态切换为就绪态。
如果被唤醒任务的优先级高于当前任务的优先级,则*pxHigherPriorityTaskWoken为pdTRUE,表示在中断返回前要进行任务切换
*/
void vTaskNotifyGiveFromISR( TaskHandle_t xTaskHandle, BaseType_t *pxHigherPriorityTaskWoken );
/* xClearCountOnExit:函数返回前是否清零。
pdTRUE:清零通知值。
pdFALSE:如果通知值大于0,则通知值减一。
xTicksToWait:任务进入阻塞态的超时时间,等待通知值大于0。
0:不等待,立即返回。
portMAX_DELAY:一直等待,直到通知值大于0。
其他值:Tick Count。可用pdMS_TO_TICKS()。
返回值:
函数返回前清零/减一通知值。
如果xTicksToWait非0,则返回值有两种情况:
大于0:在超时前,通知值被增加了
等于0:一直没有其他任务增加通知值,最后超时返回0
*/
uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit, TickType_t xTicksToWait);
xTaskNotify可以使用不同参数实现各类功能,如:
让接收任务的通知值+1:此时xTaskNotify()等同于xTaskNotifyGive()
设置接收任务的通知值的某一位、某些位。此时为轻量级、更高效的事件组
把一个新值写入接收任务的通知值:上一次的通知值被读走后,写入才成功。此时为轻量级、长度为1的队列
用一个新值覆盖接收任务的通知值:无论上一次的通知值是否被读走,覆盖都成功。此时为轻量级邮箱,类似xQueueOverwrite()函数
xTaskNotifyWait可以取出任务通知:
可以让任务等待(可以指定超时时间),等待任务状态为taskNOTIFICATION_RECEIVED
可以在函数进入、退出时清除通知值的指定位
/*
xTaskToNotify:任务句柄
ulValue:怎么使用ulValue,由参数eAction决定
eAction:看表
返回值:
pdPASS:成功,大部分调用都会成功
pdFALL:当eAction为eSetValueWithoutOverwrite,并且通知状态为taskNOTIFICATION_RECEIVED
*/
BaseType_t xTaskNotify( TaskHandle_t xTaskToNotify, uint32_t ulValue, eNotifyAction eAction );
BaseType_t xTaskNotifyFromISR( TaskHandle_t xTaskToNotify, uint32_t ulValue, eNotifyAction eAction, BaseType_t *pxHigherPriorityTaskWoken );
/*
ulBitsToClearOnEntry:在该函数的入口处要清除通知值的哪些位?通知状态非taskNOTIFICATION_RECEIVED的情况下才会清除。
如0x01表示清除通知值的位0,0xffff ffff即ULONG_MAX表示清除所有位。
ulBitsToClearOnExit:在该函数的出口处要清除通知值的哪些位?非超时退出时才会清除。
如0x01表示清除通知值的位0,0xffff ffff即ULONG_MAX表示清除所有位。
pulNotificationValue:取出通知值。在函数退出时且ulBitsToClearOnExit清除前,把通知值赋给*pulNotificationValue。不需要则设为NULL。
xTicksToWait:任务进入阻塞态的超时时间,在等待通知状态变为taskNOTIFICATION_RECEIVED。
0:不等待,立刻返回
portMAX_DELAY:一直等待,直到通知状态变为taskNOTIFICATION_RECEIVED。
其他值:Tick Count。可使用pdMS_TO_TICKS()。
返回值:
pdPASS:成功获得了通知。
可能是调用函数前,通知状态为taskNOTIFICATION_RECEIVED
可能是阻塞期间,通知状态为taskNOTIFICATION_RECEIVED
pdFALL:没有得到通知。
*/
BaseType_t xTaskNotifyWait( uint32_t ulBitsToClearOnEntry, uint32_t ulBitsToClearOnExit, uint32_t *pulNotificationValue, TickType_t xTicksToWait );
eNotifyAction取值 | 说明 |
eNoAction | 仅仅是更新通知状态为taskNOTIFICATION_RECEIVED,未使用ulValue。 这个选项相当于轻量级的、更高效的二进制信号量。 |
eSetBits | 通知值 = 原来的通知值 | ulValue。 这个选项相当于轻量级的、更高效的事件组。 |
eIncrement | 通知值 = 原来的通知值 + 1,未使用ulValue。 这个选项相当于轻量级的、更高效的二进制信号量、计数型信号量。相当于xTaskNotifyGive()函数。 |
eSetValueWithoutOverwrite | 不覆盖。 如果通知状态为taskNOTIFICATION_RECEIVED,则此次调用xTaskNotify不做任何事,返回pdFALL。 如果通知状态不为taskNOTIFICATION_RECEIVED,则通知值 = ulValue。 |
eSetValueWithOverwrite | 覆盖。 不管通知状态是否为taskNOTIFICATION_RECEIVED,通知值 = ulValue。 |
创建2个任务:
发送任务:把数据写入唤醒缓冲区,使用xTaskNotifyGive()让通知值+1。
接收任务:使用ulTaskNotifyTake()取出通知值。
int main( void )
{
prvSetupHardware();
/* 创建1个任务用于发送任务通知,优先级为2 */
xTaskCreate( vSenderTask, "Sender", 1000, NULL, 2, NULL );
/* 创建1个任务用于接收任务通知,优先级为1 */
xTaskCreate( vReceiverTask, "Receiver", 1000, NULL, 1, &xRecvTask );
/* 启动调度器 */
vTaskStartScheduler();
/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
return 0;
}
static void vSenderTask(void *pvParameters)
{
int i;
int cnt_tx = 0;
int cnt_ok = 0;
int cnt_err = 0;
char c;
const TickType_t xTicksToWait = pdMS_TO_TICKS( 20UL );
/* 无限循环 */
for(;;)
{
for (i = 0; i < 3: i++)
{
/*放入数据 */
c = 'a' + cnt_tx;
txbuf_put(c);
cnt_tx++;
/* 发出任务通知 */
if(xTaskNotifyGive(xRecvTask) == pdPASS)
printf("xTaskNotifyGive %d time:OK, val:%c\r\n", cnt_ok++, c);
else
printf("xTaskNotifyGive %d time:ERR\r\n", cnt_err++);
}
vTaskDelay(xTicksToWait);
}
}
static void vReceiverTask(void *pvParameters)
{
int cnt_ok = 0;
char c;
int notify_val;
/* 无限循环 */
for(;;)
{
/* 得到了任务通知,让通知值清零 */
notify_val = ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
/* 打印字符 */
printf("ulTaskNotifyTake OK:%d, data:", cnt_ok++);
while(notify_val--)
{
txbuf_get(&c);
printf("%c", c);
}
printf("\r\n");
}
}
实验现象:
xTaskNotifyGive 0 time:OK, val:a
xTaskNotifyGive 1 time:OK, val:b
xTaskNotifyGive 2 time:OK, val:c
ulTaskNotifyTake OK:0, data:abc
xTaskNotifyGive 3 time:OK, val:d
xTaskNotifyGive 4 time:OK, val:e
xTaskNotifyGive 5 time:OK, val:f
ulTaskNotifyTake OK:1, data:def
...
创建2个任务:
发送任务:把数据通过xTaskNotify()发送给其他任务。
接收任务:使用ulTaskNotifyWait()取出通知值。
int main( void )
{
prvSetupHardware();
/* 创建1个任务用于发送任务通知,优先级为2 */
xTaskCreate( vSenderTask, "Sender", 1000, NULL, 2, NULL );
/* 创建1个任务用于接收任务通知,优先级为1 */
xTaskCreate( vReceiverTask, "Receiver", 1000, NULL, 1, &xRecvTask );
/* 启动调度器 */
vTaskStartScheduler();
/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
return 0;
}
static void vSenderTask(void *pvParameters)
{
int i;
int cnt_tx = 0;
int cnt_ok = 0;
int cnt_err = 0;
char c;
const TickType_t xTicksToWait = pdMS_TO_TICKS( 20UL );
/* 无限循环 */
for(;;)
{
for (i = 0; i < 3: i++)
{
/*放入数据 */
c = 'a' + cnt_tx;
cnt_tx++;
/* 发出任务通知 */
/*
发给谁? xRecvTask
发什么? c
能否覆盖? 不能,eSetValueWithoutOverwrite
*/
if(xTaskNotify(xRecvTask, (uint32_t)c, eSetValueWithoutOverwrite) == pdPASS)
printf("xTaskNotifyGive %d time:OK, val:%c\r\n", cnt_ok++, c);
else
printf("xTaskNotifyGive %d time:ERR, val:%c\r\n", cnt_err++, c);
}
vTaskDelay(xTicksToWait);
}
}
static void vReceiverTask(void *pvParameters)
{
int cnt_ok = 0;
uint32_t ulValue;
BaseType_t xResult;
/* 无限循环 */
for(;;)
{
/* 等待数据 */
xResult = ulTaskNotifyWait( 0, // 发送任务会存入新数据,无需接收方在进入函数时清零
0, // 发送任务会存入新数据,无需接收方在进入函数时清零
&ulValue, /* 保存读出的数据 */
portMAX_DELAY); /* 一直等待数据 */
/* 打印字符 */
if(xResult == pdPASS)
printf("ulTaskNotifyTake OK:%d, data:%c\r\n", cnt_ok++, (char)ulValue);
}
}
实验现象:
xTaskNotifyGive 0 time:OK, val:a
xTaskNotifyGive 0 time:ERR, val:b
xTaskNotifyGive 1 time:ERR, val:c
ulTaskNotifyTake OK:0, data:a
xTaskNotifyGive 1 time:OK, val:d
xTaskNotifyGive 2 time:ERR, val:e
xTaskNotifyGive 3 time:ERR, val:f
ulTaskNotifyTake OK:1, data:d
...
本工程使用了xTaskNotify/xTaskNotifyWait实现了轻量级的队列(队列长度为1),代码更简单:无需创建队列,消耗内存更少,效率更高。
队列是个公开的资源,任何任务、ISR都可以使用它:可以存入数据、取出数据。而在该程序中,发送任务只能给指定的任务发送通知,目标明确;接收任务只能从自己的通知值中得到数据,来源明确。
注意:任务通知值只有一个,数据可能丢失,设计程序时要考虑这点。