FreeRTOS 基础系列文章
基本对象
FreeRTOS——任务
FreeRTOS——队列
FreeRTOS——信号量
FreeRTOS——互斥量
FreeRTOS——任务通知
FreeRTOS——流和消息缓冲区
FreeRTOS——软件定时器
FreeRTOS——事件组
内存管理
FreeRTOS——静态与动态内存分配
FreeRTOS——堆内存管理
FreeRTOS——栈溢出保护
代码组织
FreeRTOS——源代码组织
FreeRTOS——创建新的项目
FreeRTOS——配置文件
FreeRTOS——任务通知
任务通知
从 FreeRTOS V8.2.0 开始可用 从 V10.4.0 开始支持每个任务的多个通知 |
每个RTOS任务都有一个任务通知数组。每个任务通知都有一个通知状态,可以是“挂起”或“未挂起”,以及一个32位的通知值。configTASK_NOTIFICATION_ARRAY_ENTRIES
设置任务通知数组中的索引数。在FreeRTOS V10.4.0之前,任务只有一个任务通知,而不是一个通知数组。
直接到任务通知是直接发送到任务的事件。而不是通过中间对象(如队列、事件组或信号量)间接到任务。向任务发送“直接到任务”通知将目标任务的通知状态设置为“挂起”。就像一个任务可以阻塞在中间对象(例如信号量)上,以等待信号量可用一样,一个任务可以阻塞在一个任务通知上,以等待该通知的状态变为挂起。
向任务发送直接到任务通知还可以选择通过以下方式之一更新目标通知的值:
- 无论接收任务是否读取了被覆盖的值,都会覆盖该值。
- 覆盖该值,但前提是接收任务已读取被覆盖的值。
- 在值中设置一位或多位。
- 增加(加一)值。
调用 xTaskNotifyWait()
/xTaskNotifyWaitIndexed()
读取通知值会将该通知的状态清除为“未挂起”。通知状态也可以通过调用xTaskNotifyStateClear()
/xTaskNotifyStateClearIndexed()
显式设置为“未挂起” 。
注意: 数组中的每个通知都是独立运行的 —— 一个任务一次只能阻塞数组中的一个通知,并且不会被发送到任何其他数组索引的通知解除阻塞。
RTOS 任务通知功能默认启用,可以通过在FreeRTOSConfig.h
中将 configUSE_TASK_NOTIFICATIONS
设置为 0
从构建中排除(每个任务每个数组索引节省 8 个字节)。
重要说明:FreeRTOS Stream
和 Message Buffers
在数组索引 0
处使用任务通知。如果您想在调用 Stream
或 Message Buffer
API 函数时保持任务通知的状态,则在数组索引大于 0
处使用任务通知。
性能优势和使用限制
任务通知的灵活性允许在需要创建单独队列、二进制信号量、计数信号量或事件组的地方使用任务通知。与使用中间对象(例如二进制信号量)解除任务阻塞相比,使用直接通知解除阻塞的 RTOS 任务的速度要快 45% ,并且使用的 RAM 更少。正如预期的那样,这些性能优势需要一些用例限制:
-
RTOS 任务通知只能在只有一个任务可以作为事件的接收者时使用。然而,在现实世界的大多数用例中都满足这种条件,例如中断解除一个阻塞的任务,该任务处理中断接收到的数据。
-
在使用 RTOS 任务通知代替队列的情况下:虽然接收任务可以在阻塞状态等待通知(因此不消耗任何 CPU 时间),但如果发送不能立即完成,则发送任务不能在阻塞状态下等待发送完成。
用例
通知使用 xTaskNotifyIndexed()
和 xTaskNotifyGiveIndexed()
API 函数(及其中断安全等效项)来发送,并保持挂起状态直到接收的 RTOS 任务调用 xTaskNotifyWaitIndexed()
或 ulTaskNotifyTakeIndexed()
API 函数。这些 API 函数中的每一个都有一个没有“Indexed
”后缀的等效函数。非“Indexed
”版本始终对数组索引 0
处的任务通知进行操作。例如 xTaskNotifyGive( TargetTask ) 等效于 xTaskNotifyGiveIndexed( TargetTask, 0 ) —— 这两者都增加了任务句柄 TargetTask 所引用任务的索引0
处的任务通知。
用作轻量级二进制信号量
二进制信号量是一个最大计数为1的信号量,因此得名“二进制”。一个任务只能在信号量可用时“获得”信号量,而信号量只有在计数为1时才可用。
当使用任务通知代替二进制信号量时,使用接收任务的 通知值代替二进制信号量的计数值,并且使用 ulTaskNotifyTake()
(或 ulTaskNotifyTakeIndexed()
)API 函数代替信号量的 xSemaphoreTake()
API 函数。ulTaskNotifyTake()
函数的 xClearOnExit
参数设置为 pdTRUE
,因此每次获得通知时计数值都会返回零 —— 模拟二进制信号量。
同样,使用 xTaskNotifyGive()
(或 xTaskNotifyGiveIndexed()
)或 vTaskNotifyGiveFromISR()
(或 vTaskNotifyGiveIndexedFromISR()
)函数代替信号量的 xSemaphoreGive()
和 xSemaphoreGiveFromISR()
函数。
请参阅下面的示例。
/* 这是一个通用外设驱动程序中的传输函数的示例。一个RTOS任务调用传输函数,
然后在阻塞状态中等待(这样就不用占用CPU时间),直到收到传输完成的通知。
传输由DMA执行,DMA结束中断用于通知任务。 */
/* 存储将在传输完成时通知的任务的句柄 */
static TaskHandle_t xTaskToNotify = NULL;
/* 目标任务的任务通知数组中要使用的索引。 */
const UBaseType_t xArrayIndex = 1;
/* 外设驱动的传输函数。 */
void StartTransmission( uint8_t *pcData, size_t xDataLength )
{
/* 此时xTaskToNotify应该为NULL,因为没有传输正在进行。
如果有必要,可以使用互斥量来保护对外设的访问。 */
configASSERT( xTaskToNotify == NULL );
/* 存储调用任务的句柄。 */
xTaskToNotify = xTaskGetCurrentTaskHandle();
/* 启动传输 —— 当传输完成时产生一个中断。 */
vStartTransmit( pcData, xDatalength );
}
/*-----------------------------------------------------------*/
/* 传输结束中断 */
void vTransmitEndISR( void )
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
/* 此时xTaskToNotify不应该是NULL,因为传输正在进行中。 */
configASSERT( xTaskToNotify != NULL );
/* 通知任务传输已完成。 */
vTaskNotifyGiveIndexedFromISR( xTaskToNotify,
xArrayIndex,
&xHigherPriorityTaskWoken );
/* 没有正在进行的传输,所以没有任务需要通知。 */
xTaskToNotify = NULL;
/* 如果xHigherPriorityTaskWoken现在被设置为pdTRUE,那么应该执行
一个上下文切换,以确保中断直接返回到最高优先级的任务。 用于此目的
的宏取决于使用的端口,可能称为portEND_SWITCHING_ISR()。*/
portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}
/*-----------------------------------------------------------*/
/* 发起传输的任务随后进入阻塞状态(因此不消耗任何CPU时间)来等待它完成。 */
void vAFunctionCalledFromATask( uint8_t *ucDataToTransmit,
size_t xDataLength )
{
uint32_t ulNotificationValue;
const TickType_t xMaxBlockTime = pdMS_TO_TICKS( 200 );
/* 通过调用上面所示的函数来启动传输。 */
StartTransmission( ucDataToTransmit, xDataLength );
/* 等待传输完成的通知。注意,第一个参数是pdTRUE,它的作用是将任务
的通知值清除回0,使通知值像一个二进制信号量(而不是计数信号量)。 */
ulNotificationValue = ulTaskNotifyTakeIndexed( xArrayIndex,
pdTRUE,
xMaxBlockTime );
if( ulNotificationValue == 1 )
{
/* 传输如期结束。 */
}
else
{
/* 调用ulTaskNotifyTake()超时。 */
}
}
用作轻量级计数信号量
计数信号量是一种信号量,其计数值可以为零到创建信号量时设置的最大值。如果信号量可用,任务只能“获取”信号量,并且信号量仅在其计数大于零时才可用。只有当信号量可用时,任务才能“获得”信号量,而信号量只有在其计数大于0时才可用。
当使用任务通知代替计数信号量时,使用接收任务的通知值代替计数信号量的计数值,并且使用 ulTaskNotifyTake()
(或 ulTaskNotifyTakeIndexed()
)API 函数代替信号量的 xSemaphoreTake()
API 函数。ulTaskNotifyTake()
函数的 xClearOnExit
参数设置为 pdFALSE
,因此每次接收通知时计数值仅递减(而不是清除) —— 模拟计数信号量。
同样,使用 xTaskNotifyGive()
(或 xTaskNotifyGiveIndexed()
)或 vTaskNotifyGiveFromISR()
(或 vTaskNotifyGiveIndexedFromISR()
)函数代替信号量的 xSemaphoreGive()
和 xSemaphoreGiveFromISR()
函数。
下面的第一个示例使用接收任务的通知值作为计数信号量。第二个示例提供了更实用、更高效的实现。
示例1:
/* 中断处理程序不直接处理中断,而是将处理延迟到一个高优先级的RTOS任务。
ISR使用RTOS任务通知来解除对RTOS任务的阻塞,并增加RTOS任务的通知值。 */
void vANInterruptHandler( void )
{
BaseType_t xHigherPriorityTaskWoken;
/* 清除中断。 */
prvClearInterruptSource();
/* xHigherPriorityTaskWoken必须初始化为pdFALSE。 如果调用
vTaskNotifyGiveFromISR()解除了处理任务的阻塞,并且处理任务
的优先级高于当前正在运行的任务的优先级,那么
xHigherPriorityTaskWoken将被自动设置为pdTRUE。 */
xHigherPriorityTaskWoken = pdFALSE;
/* 解除处理任务的阻塞,使任务能够执行中断所需的任何处理。
xHandlingTask是任务的句柄,它是在创建任务时获得的。
vTaskNotifyGiveFromISR()增加接收任务的通知值。 */
vTaskNotifyGiveFromISR( xHandlingTask, &xHigherPriorityTaskWoken );
/* 如果xHigherPriorityTaskWoken现在被设置为pdTRUE,
则强制进行上下文切换。 用于完成此操作的宏依赖于端口,
可能称为portEND_SWITCHING_ISR。 */
portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}
/*-----------------------------------------------------------*/
/* 阻塞的任务,等待外设需要服务的通知。 */
void vHandlingTask( void *pvParameters )
{
BaseType_t xEvent;
const TickType_t xBlockTime = pdMS_TO_TICS( 500 );
uint32_t ulNotifiedValue;
for( ;; )
{
/* 阻塞以等待通知。 在这里,RTOS任务通知被用作计数信号量。
每次ISR调用vTaskNotifyGiveFromISR()时,任务的通知值都会
增加,每次RTOS任务调用ulTaskNotifyTake()时,通知值都会减
少 —— 所以实际上保存了未完成中断的数量。第一个参数被设置
为pdFALSE,因此通知值只是递减而不是清除为零,并且一次处理
一个延迟中断事件。参见下面的示例2以获得更实用的方法。 */
ulNotifiedValue = ulTaskNotifyTake( pdFALSE,
xBlockTime );
if( ulNotifiedValue > 0 )
{
/* 执行中断所需的任何处理。 */
xEvent = xQueryPeripheral();
if( xEvent != NO_MORE_EVENTS )
{
vProcessPeripheralEvent( xEvent );
}
}
else
{
/* 在预期时间内没有收到通知。 */
vCheckForErrorConditions();
}
}
}
示例2:
这个例子展示了一个更实用和更有效的RTOS任务的实现。在这个实现中,ulTaskNotifyTake()返回的值用于知道有多少未处理的ISR事件必须被处理,允许在每次调用ulTaskNotifyTake()时将RTOS任务的通知计数清除回零。中断服务程序(ISR)被假定为如上例1所示。
/* 目标任务的任务通知数组中要使用的索引。 */
const UBaseType_t xArrayIndex = 0;
/* 阻塞的任务,等待外设需要服务的通知。 */
void vHandlingTask( void *pvParameters )
{
BaseType_t xEvent;
const TickType_t xBlockTime = pdMS_TO_TICS( 500 );
uint32_t ulNotifiedValue;
for( ;; )
{
/* 和以前一样,阻塞以等待来自ISR的通知。然而,这一次第一个
参数被设置为pdTRUE,将任务的通知值清除为0,这意味着在再次
调用ulTaskNotifyTake()之前,必须处理每个未完成的延迟中断
事件。 */
ulNotifiedValue = ulTaskNotifyTakeIndexed( xArrayIndex,
pdTRUE,
xBlockTime );
if( ulNotifiedValue == 0 )
{
/* 在预期时间内没有收到通知。 */
vCheckForErrorConditions();
}
else
{
/* ulNotifiedValue 保存了未完成中断数量的计数。
依次处理每一个。 */
while( ulNotifiedValue > 0 )
{
xEvent = xQueryPeripheral();
if( xEvent != NO_MORE_EVENTS )
{
vProcessPeripheralEvent( xEvent );
ulNotifiedValue--;
}
else
{
break;
}
}
}
}
}
用作轻量级事件组
事件组是一组二进制标志(或位),应用程序编写者可以为每个标志(或位)赋值。RTOS任务可以进入阻塞状态,等待组内的一个或多个标志变为有效状态。RTOS任务处于阻塞状态时不占用CPU时间。
当使用任务通知代替事件组时,使用接收任务的通知值代替事件组,接收任务通知值中的位用作事件标志,并使用 xTaskNotifyWait()
API 函数代替事件组的 xEventGroupWaitBits()
API 函数。
同样,使用 xTaskNotify()
和 xTaskNotifyFromISR()
API 函数(将其eAction
参数设置为eSetBits
)来分别代替 xEventGroupSetBits()
和 xEventGroupSetBitsFromISR()
函数来设置位。
与 xEventGroupSetBitsFromISR()
相比,xTaskNotifyFromISR()
具有显着的性能优势,因为 xTaskNotifyFromISR()
完全在 ISR 中执行,而 xEventGroupSetBitsFromISR()
必须将某些处理推迟到 RTOS 守护进程任务。
与使用事件组时不同的是,接收任务不能指定只在同时有多个位的组合处于有效状态时才解除阻塞状态。相反,当任何位变为有效状态时,任务将被解除阻塞,并且必须自行测试位组合。
请参阅下面的示例:
/* 这个例子演示了一个单独的RTOS任务用来处理两个独立的中断服务程序产生的
事件 —— 一个发送中断和一个接收中断。许多外围设备将使用相同的处理程序,在
这种情况下,外围设备的中断状态寄存器可以简单地与接收任务的通知值按位或。
首先,位被定义来表示每个中断源。 */
#define TX_BIT 0x01
#define RX_BIT 0x02
/* 将从中断接收通知的任务的句柄。句柄是在创建任务时获得的。 */
static TaskHandle_t xHandlingTask;
/*-----------------------------------------------------------*/
/* 发送中断服务程序的实现。 */
void vTxISR( void )
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
/* 清除中断源。 */
prvClearInterrupt();
/* 通过在任务的通知值中设置TX_BIT来通知任务传输完成。 */
xTaskNotifyFromISR( xHandlingTask,
TX_BIT,
eSetBits,
&xHigherPriorityTaskWoken );
/* 如果xHigherPriorityTaskWoken现在被设置为pdTRUE,那么应该执行一个
上下文切换,以确保中断直接返回到最高优先级的任务。 用于此目的的宏取决
于使用的端口,可能称为portEND_SWITCHING_ISR()。 */
portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}
/*-----------------------------------------------------------*/
/* 接收中断服务例程的实现是一样的,除了在接收任务的通知值中设置的位不同。 */
void vRxISR( void )
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
/* 清除中断源。 */
prvClearInterrupt();
/* 通过在任务的通知值中设置RX_BIT来通知任务接收已经完成。 */
xTaskNotifyFromISR( xHandlingTask,
RX_BIT,
eSetBits,
&xHigherPriorityTaskWoken );
/* 如果xHigherPriorityTaskWoken现在被设置为pdTRUE,那么应该执行一个
上下文切换,以确保中断直接返回到最高优先级的任务。 用于此目的的宏取决
于使用的端口,可能称为portEND_SWITCHING_ISR()。 */
portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}
/*-----------------------------------------------------------*/
/* 任务的实现,接收中断服务程序通知。 */
static void prvHandlingTask( void *pvParameter )
{
const TickType_t xMaxBlockTime = pdMS_TO_TICKS( 500 );
BaseType_t xResult;
for( ;; )
{
/* 等待被中断通知。 */
xResult = xTaskNotifyWait( pdFALSE, /* 不要在进入时清除位。 */
ULONG_MAX, /* 在退出时清除所有位。 */
&ulNotifiedValue, /* 存储通知值。 */
xMaxBlockTime );
if( xResult == pdPASS )
{
/* 收到通知。查看设置了哪些位。 */
if( ( ulNotifiedValue & TX_BIT ) != 0 )
{
/* TX ISR 设置了一个位。 */
prvProcessTx();
}
if( ( ulNotifiedValue & RX_BIT ) != 0 )
{
/* RX ISR 设置了一个位。 */
prvProcessRx();
}
}
else
{
/* 未在预期时间内收到通知。 */
prvCheckForErrors();
}
}
}
用作轻量级邮箱
RTOS 任务通知可用于向任务发送数据,但其方式比 RTOS 队列要严格得多,因为:
- 只能发送 32 位值
- 该值保存为接收任务的通知值,且任一时刻只能有一个通知值
因此,习惯用语“轻量级邮箱”优先于“轻量级队列”使用。任务的通知值是邮箱值。
使用 xTaskNotify()
(或 xTaskNotifyIndexed()
)和 xTaskNotifyFromISR()
(或 xTaskNotifyIndexedFromISR()
)API 函数将数据发送到任务,其 eAction
参数设置为 eSetValueWithOverwrite
或 eSetValueWithoutOverwrite
。如果 eAction
设置为 eSetValueWithOverwrite
,则接收任务的通知值会更新,即使接收任务已经有挂起的通知。如果 eAction
设置为 eSetValueWithoutOverwrite
,则接收任务的通知值仅在接收任务没有挂起的通知时才更新 —— 因为更新通知值会覆盖先前的值。
任务可以使用 xTaskNotifyWait()
(或 xTaskNotifyWaitIndexed()
)读取自己的通知值。