下载 FreeRTOS
 

出色的 RTOS & 嵌入式软件

通过 FreeRTOS 通知减少 RAM 占用空间并加速执行

简介

队列和信号量 是所有操作系统提供的教科书特性。 FreeRTOS 新开发人员因为熟悉而使用它们。 不过,在大多数用例中, FreeRTOS 直接任务通知 提供了一个更小、最快可达 45% 的信号量替代方案,而 FreeRTOS 消息缓冲区和流缓冲区 为队列提供了更小且更快的替代方案。 这篇博客介绍了如何使用直接任务通知代替信号量来创建更小更快的应用程序。

架构良好的 FreeRTOS 应用程序很少需要使用信号量。


背景

FreeRTOS 2002 年发布的 V1.2.0 将信号量 API 实现为一组调用队列 API 的宏,引入了信号量功能。 这种设计选择的优点是添加信号量功能,而不增加代码大小(当闪存通常比现在小时,这一点很重要) ,但它的缺点是使信号量成为非典型的重对象,因为它们继承了队列的所有综合功能。 例如,队列能真正做到线程和优先级感知,包括事件机制和等待发送到队列和从队列接收任务的优先级排序列表。 一些信号量用例受益于这种综合功能,但最常见的信号量用例则不需要。 因此,在寻找驱动程序库使用的精益事件机制时,我们选择不重写信号量代码,而是为那些最常见的用例显式地创建一个新的原语。 该原语是直接任务通知——从现在开始,仅称为“通知“。


什么是直接任务通知?

大多数任务间通信方法通过中间对象,如队列、信号量或事件组。 发送任务写入通信对象,接收任务从通信对象读取。 当使用直接任务通知时,顾名思义,发送任务直接向接收任务发送通知,而无需中间对象。

图 1 :通过中间对象进行通信



图 2 :无需中间对象进行通信


从 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();
}
}
}
列表 1


列表 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 );
}
列表 2


使用通知将值从 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;
}
列表 3


最后,列表 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 );
}
列表 4


结论

FreeRTOS 是一款成熟的产品,经过近 20 年的发展,继续演进,从而包括针对我们所了解的最常见用例的定制可选功能。 这些功能包括直接任务通知、消息缓冲区和流缓冲区。 开发人员应在旧的 FreeRTOS 功能之前使用这些量身定制的功能,因为它们更小、更快,但新 FreeRTOS 开发人员经常忽略它们,因为这些概念没有出现在标准操作系统文本中。 为特定用例定制功能意味着限制适用用例的数量。 更灵活的原始 FreeRTOS 功能仍可用于涵盖所有用例——但在大多数应用程序中,使用队列和信号量等综合功能,可能是例外而不是常态。

作者简介

Richard Barry 于 2003 年创立了 FreeRTOS 项目,在他的公司 Real Time Engineers Ltd 中花了十多年时间来开发和推广 FreeRTOS ,现在他作为 Amazon Web Services 的首席工程师在一个更大的团队中继续优化 FreeRTOS 。 Richard 以一级荣誉毕业于计算机实时系统专业,因对嵌入式技术发展的贡献而被授予荣誉博士学位。 Richard 还直接参与创办了几家公司,并撰写了几本书籍。
查看此作者的文章
FreeRTOS 论坛 获得来自专家的行业领先支持,并与全球同行合作。 查看论坛
Copyright (C) Amazon Web Services, Inc. or its affiliates. All rights reserved.