简介
队列和信号量 是所有操作系统提供的教科书特性。 FreeRTOS 新开发人员因为熟悉而使用它们。 不过,在大多数用例中, FreeRTOS 直接任务通知 提供了一个更小、最快可达 45% 的信号量替代方案,而 FreeRTOS 消息缓冲区和流缓冲区 为队列提供了更小且更快的替代方案。 这篇博客介绍了如何使用直接任务通知代替信号量来创建更小更快的应用程序。
架构良好的 FreeRTOS 应用程序很少需要使用信号量。
背景
FreeRTOS 2002 年发布的 V1.2.0 将信号量 API 实现为一组调用队列 API 的宏,引入了信号量功能。 这种设计选择的优点是添加信号量功能,而不增加代码大小(当闪存通常比现在小时,这一点很重要) ,但它的缺点是使信号量成为非典型的重对象,因为它们继承了队列的所有综合功能。 例如,队列能真正做到线程和优先级感知,包括事件机制和等待发送到队列和从队列接收任务的优先级排序列表。 一些信号量用例受益于这种综合功能,但最常见的信号量用例则不需要。 因此,在寻找驱动程序库使用的精益事件机制时,我们选择不重写信号量代码,而是为那些最常见的用例显式地创建一个新的原语。 该原语是直接任务通知——从现在开始,仅称为“通知“。
什么是直接任务通知?
大多数任务间通信方法通过中间对象,如队列、信号量或事件组。 发送任务写入通信对象,接收任务从通信对象读取。 当使用直接任务通知时,顾名思义,发送任务直接向接收任务发送通知,而无需中间对象。
从 FreeRTOS V10.4.0 开始,每个任务都有一组通知。 在此之前,每个任务只有一个通知。 每个通知包括一个 32 位值和一个布尔状态,它们总共只消耗 5 个字节的 RAM。
正如任务可以阻塞二进制信号量以等待其变为“可用”一样,任务也可以阻塞通知以等待其状态变为“待定”。 同样,正如任务可以阻塞计数信号量以等待其计数变为非零一样,任务也可以阻塞通知以等待其值变为非零。 下面的第一个示例演示了这种场景。
通知不仅可以传达事件,还可以通过多种方式传达数据。 下面的第二个示例演示如何使用通知发送 32 位值。
使用通知将中断与任务同步的示例
下面的列表 1 显示了阻塞通知的任务结构。 如果任务在信号量上阻塞,则它将调用 xSemaphoreTake() API 函数,但由于任务正在使用通知,它将调用ulTaskNotifyTake() API 函数。 ulTaskNotifyTake () 始终使用索引 0 处的通知。 使用 ulTaskNotifyTakeIndexed () 代替 ulTaskNotifyTake () 可在任何特定数组索引处使用通知。
static void vNotifiedTask( void *pvParameters )
{
for( ;; )
{
/* Wait to receive a notification sent directly to this task.
The first parameter is set to pdFALSE, which makes the call
replicate the behavior of a counting semaphore. Set the
parameter to pdTRUE to replicate the behavior of a binary
semaphore. The second parameter is set to portMAX_DELAY,
which makes the task block indefinitely to wait for the
notification. That is done to simplify the example – real
applications should not block indefinitely as that prevents
the task recovering from error conditions. */
if( ulTaskNotifyTake( pdFALSE, portMAX_DELAY ) != 0 )
{
/* The task received a notification – do whatever is
necessary to process the received event. */
DoSomething();
}
}
}
列表 2 显示了发送通知的中断的结构。
static uint32_t vNotifyingISR( void )
{
BaseType_t xHigherPriorityTaskWoken;
/* The xHigherPriorityTaskWoken parameter must be initialized
to pdFALSE as it will get set to pdTRUE inside the interrupt
safe API function if calling the API function unblocks a task
that has a higher priority than the task in the running state
(the task this ISR interrupted). */
xHigherPriorityTaskWoken = pdFALSE;
/* Send a notification directly to the task that will perform
any processing necessitated by this interrupt. */
vTaskNotifyGiveFromISR( /* The handle of the task to which
the notification is being sent. */
xHandlerTask,
&xHigherPriorityTaskWoken );
/* If xHigherPriorityTaskWoken is now pdTRUE then calling
portYIELD_FROM_ISR() will result in a context switch, and
this interrupt will return directly to the unblocked task.
The FAQ "why is there a separate API for use in interrupts"
describes why it is done this way. */
portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}
使用通知将值从 ISR 发送到任务的示例
下面的示例通过演示如何使用通知发送数据来扩展通知的使用,而不仅仅是复制信号量行为。 发送数据的额外开销最小。
列表 3 显示返回模拟到数字 (ADC) 转换结果的函数结构。 调用该函数的任务在阻塞状态等待转换结果,因此它不消耗任何 CPU 周期。 结果从转换结束中断服务程序 (ISR) 发送给它。 此种情况需要使用稍微复杂的 xTaskNotify() 和 xTaskNotifyWait() API 函数。 如前所述,xTaskNotify () 和 xTaskNotifyWait () 在通知数组索引 0 处的通知进行操作。 使用 xTaskNotifyIndexed () 和 xTaskNotifyWaitIndexed () 对数组中的任何特定索引进行操作。
#define MAX_ADC_CHANNELS
/* Holds the handle of a task to notify when an ADC conversion
ends on any given ADC channel. */
static TaskHandle_t xBlockedTasks[ MAX_ADC_CHANNELS ] = { 0 };
ErrorCode_t xGetADC( uint8_t ucChannel,
uint32_t *pulConversionResult )
{
ErrorCode_t xErrorCode = SUCCESS;
/* Check the ADC channel is not already in use. */
taskENTER_CRITICAL();
{
if( xBlockedTasks[ ucChannel ] != NULL )
{
/* A task is already waiting for a result from
this channel. */
xErrorCode = CHANNEL_IN_USE;
}
else
{
/* Store the handle of the calling task so it can
be notified when the conversion is complete. This
is cleared back to NULL by the conversion end
interrupt. */
xBlockedTasks[ ucChannel ] = xTaskGetCurrentTaskHandle();
}
}
taskEXIT_CRITICAL();
if( xErrorCode == SUCCESS )
{
/* Ensure the calling task does not already have a
notification pending. xTaskNotifyStateClear() clears
the state of the notification at array index 0. Use
xTaskNotifyStateClearIndexed() to clear the state of
a notification at a specific array index. */
xTaskNotifyStateClear( NULL );
/* Start the ADC conversion. */
StartADCConversion( ucChannel );
/* Block to wait for the conversion result. */
xResult = xTaskNotifyWait(
/* The new ADC value will overwrite the old
value, so there is no need to clear any bits
before or after waiting for the new
notification value. */
0,
0,
/* The address of the variable in which to
store the result. */
pulConversionResult,
/* Wait indefinitely. Again this is only done
to keep the example simple. Production code
should never block indefinitely as doing so
prevents the task from recovering from
errors. */
portMAX_DELAY );
/* If not using an infinite block time then check xResult
to see why xTaskNotifyWait() returned. Production code
should not use an infinite block time as doing so prevents
the task recovering from an error.*/
}
return xErrorCode;
}
最后,列表 4 显示了使用通知将转换结果发送到等待任务的中断服务程序结构。
/* The interrupt service routine (ISR) that executes each time
an ADC conversion completes. It is assumed the xBlockedTasks[]
array used in Listing 3 is in scope for use by this ISR.*/
void ADC_ConversionEndISR( void )
{
Uint8_t ucChannel;
uint32_t ulConversionResult;
BaseType_t xHigherPriorityTaskWoken = pdFALSE, xResult;
/* This ISR handles all ADC channels. Determine which
channel needs servicing. */
ucChannel = ADC_GetChannelNumber();
if( ucChannel < MAX_ADC_CHANNELS )
{
/* Read the conversion result to clear the interrupt. */
ulConversionResult = ADC_ReadResult( ucChannel );
/* Is a task waiting for a result from channel
ucChannel? */
if( xBlockedTasks[ ucChannel ] != NULL )
{
/* Send a notification, and the ADC conversion
result, directly to the waiting task. */
xTaskNotifyFromISR( /* xTaskToNotify parameter. */
xBlockedTasks[ ucChannel ],
/* ulValue parameter. */
ulConversionResult,
/* eAction parameter. */
eSetValueWithoutOverwrite,
&xHigherPriorityTaskWoken );
/* There is no longer a task waiting for a result
from channel ucChannel. */
xBlockedTasks[ ucChannel ] = NULL;
}
}
/* As normal – see comments in code Listing 2. */
portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}
结论
FreeRTOS 是一款成熟的产品,经过近 20 年的发展,继续演进,从而包括针对我们所了解的最常见用例的定制可选功能。 这些功能包括直接任务通知、消息缓冲区和流缓冲区。 开发人员应在旧的 FreeRTOS 功能之前使用这些量身定制的功能,因为它们更小、更快,但新 FreeRTOS 开发人员经常忽略它们,因为这些概念没有出现在标准操作系统文本中。 为特定用例定制功能意味着限制适用用例的数量。 更灵活的原始 FreeRTOS 功能仍可用于涵盖所有用例——但在大多数应用程序中,使用队列和信号量等综合功能,可能是例外而不是常态。