FreeRtos
简述
FreeRTOS 从版本 V8.2.0
开始提供任务通知这个功能,每个任务都有一个32位的通知值。按照 FreeRTOS 官方的说法,使用消息通知比通过二进制信号量方式解除阻塞任务快 45%, 并且更加省内存(无需创建队列)。
FreeRTOS 提供以下几种方式发送通知给任务 :
- 发送消息给任务, 如果有通知未读, 不覆盖通知值
- 发送消息给任务,直接覆盖通知值
- 发送消息给任务,设置通知值的一个或者多个位
- 发送消息给任务,递增通知值
通过对以上方式的合理使用,可以在一定场合下替代原本的信号量,队列等。
当然,消息通知也有其限制 :
- 通知只能一对一,因为通知必须指定任务
- 等待通知的任务可以被阻塞, 但是发送消息的任务,任何情况下都不会被阻塞等待
分析的源码版本是 v9.0.0
通知 API
FreeRTOS 关于任务通知的 API 如下
API | 功能 |
---|---|
xTaskNotifyGive() | 发送通知,没有通知值 (信号量类型) |
ulTaskNotifyTake() | 获取通知,(对应 Give) |
xTaskNotify() | 发送通知, 带通知值 |
xTaskNotifyAndQuery() | 发送通知,带通知值,并且返回原通知值 |
xTaskNotifyWait() | 等待通知 |
vTaskNotifyGiveFromISR() | xTaskNotifyGive 的中断版本 |
xTaskNotifyAndQueryFromISR() | xTaskNotifyAndQuery 的中断版本 |
xTaskNotifyFromISR() | ulTaskNotifyTake 的中断版本 |
xTaskNotifyStateClear() | 清除所有未读消息 |
可能你会想,消息通知就一个发送一个接收 API 不就好了,为什么要搞出这么多个 API ?
实际上, 以上的 API,有的是宏定义,而如此实现是方便特定情况下使用,比如用通知去实现轻量化的二进制信号量,计数信号量,队列等。
数据结构
方便下文叙述,先介绍下实现的相关变量定义。
为了实现任务通知,任务控制块 TCB_t 结构体中有两个任务通知的相关变量, 默认情况下, 任务通知这个功能是打开的,也就是宏 configUSE_TASK_NOTIFICATIONS
设置为 1
源码
#if( configUSE_TASK_NOTIFICATIONS == 1 )
volatile uint32_t ulNotifiedValue;
volatile uint8_t ucNotifyState;
#endif
- 变量
ulNotifiedValue
存储任务通知的数值, 初始化为 0。 - 变量
ucNotifyState
存储当前任务通知的状态,对应存在以下三种状态
// 1 没有未读通知,任务没有等待通知
#define taskNOT_WAITING_NOTIFICATION ( ( uint8_t ) 0 )
// 2 任务等待通知
#define taskWAITING_NOTIFICATION ( ( uint8_t ) 1 )
// 3 通知等待任务读取
#define taskNOTIFICATION_RECEIVED ( ( uint8_t ) 2 )
该变量初始化为 taskNOT_WAITING_NOTIFICATION
。
文章开头提到发送任务通知的几种方式,对应系统源码中定义了如下 5 种命令类型 :
typedef enum
{
// 1 发送通知,但是没有更新通知值
eNoAction = 0,
// 2 发送通知,将新通知值与原通知值或操作(置位)
eSetBits,
// 3 发送通知,原通知值加 1
eIncrement,
// 4 发送通知,直接修改通知值(不过上次通知是否已经读取)
eSetValueWithOverwrite,
// 5 发送通知,如果没有未读消息,设置通知值
eSetValueWithoutOverwrite
} eNotifyAction;
轻量级二进制信号量
我在 << FreeRTOS 信号量 >> 一文中举过一个例子,用二进制信号量进行同步。
这里,我们使用任务通知来实现同样的任务同步功能。
先看例子源码 :
// 等待通知的任务句柄
static TaskHandle_t xTaskToNotify = NULL;
void vATask( void * pvParameters )
{
uint32_t ulNotificationValue;
// 设置等待通知阻塞时间
const TickType_t xMaxBlockTime = pdMS_TO_TICKS( 200 );
// 获取任务句柄
xTaskToNotify = xTaskGetCurrentTaskHandle();
// ...
for (;;) {
// ...
// 等待通知
ulNotificationValue = ulTaskNotifyTake( pdTRUE,
xMaxBlockTime );
if( ulNotificationValue > 0 ) {
// 收到通知
} else {
// 超时
}
}
}
// 中断 发送通知
void vTimerISR( void * pvParameters )
{
static signed BaseType_t xHigherPriorityTaskWoken;
xHigherPriorityTaskWoken = pdFALSE;
// 发送通知
vTaskNotifyGiveFromISR( xTaskToNotify,
&xHigherPriorityTaskWoken );
// 传递参数判断是否有高优先级任务就绪
// 判断是否需要触发任务切换
portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}
上面例子, 任务调用 API ulTaskNotifyTake
等待中断发送通知,中断中调用 API vTaskNotifyGiveFromISR
给任务发送通知, 如果是在任务中发送消息,则调用 API vTaskNotifyGive
。
例子中的任务通知实现了二进制型号量的任务同步功能。
下面分析下任务通知这个功能如何实现信号量获取和释放。
获取信号量
任务中,调用了函数 ulTaskNotifyTake
等待通知,相当于尝试获取信号量。
为了实现二进制信号量,函数的第一个参数设置为pdTRUE
, 在接收到通知后,读取并清除通知值(设置为0)。(此处可以对比后续的计数信号量)
第二参数是阻塞等待时间。
直接查看该函数的实现 :
uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit,
TickType_t xTicksToWait )
{
uint32_t ulReturn;
taskENTER_CRITICAL();
{
// 如果通知值为 0 ,阻塞任务
// 默认初始化通知值为 0, 说明没有未读通知
if( pxCurrentTCB->ulNotifiedValue == 0UL )
{
// 标记任务状态 : 等待消息通知
pxCurrentTCB->ucNotifyState = taskWAITING_NOTIFICATION;
// 挂起任务等待消息或者超时
if( xTicksToWait > ( TickType_t ) 0 )
{
prvAddCurrentTaskToDelayedList( xTicksToWait, pdTRUE );
portYIELD_WITHIN_API();
}
}
}
taskEXIT_CRITICAL();
// 到此处,有通知 或者 等待超时
taskENTER_CRITICAL();
{
// 判断通知值
ulReturn = pxCurrentTCB->ulNotifiedValue;
if( ulReturn != 0UL )
{
// 接收到通知
// 第一个参数为真,读取后清零通知值
if( xClearCountOnExit != pdFALSE )
{
pxCurrentTCB->ulNotifiedValue = 0UL;
}
else
{
// 否则 通知值减 1
pxCurrentTCB->ulNotifiedValue = ulReturn - 1;
}
}
// 恢复任务通知状态变量
pxCurrentTCB->ucNotifyState = taskNOT_WAITING_NOTIFICATION;
}
taskEXIT_CRITICAL();
return ulReturn;
}
对于这个函数,任务通知值为0, 对应信号量无效,如果任务设置了阻塞等待,任务被阻塞挂起。当其他任务或中断发送通知修改了通知值使其不为0后,信号量变为有效,等待通知的任务会读取通知,根据传递的第一个参数清零通知值或者执行递减操作。
对于二进制信号量,信号量读取一次后就失效,所以直接清零。
释放信号量
例子中是在中断中发送通知,所以必须调用带有FromISR
后缀的API。发送通知调用的函数是 vTaskNotifyGiveFromISR
, 对应函数名,也可以看出是一个释放信号量的操作。
对该函数进行简化说明,看看其实如何给出信号量的
void vTaskNotifyGiveFromISR( TaskHandle_t xTaskToNotify,
BaseType_t *pxHigherPriorityTaskWoken )
{
TCB_t * pxTCB;
uint8_t ucOriginalNotifyState;
UBaseType_t uxSavedInterruptStatus;
pxTCB = ( TCB_t * ) xTaskToNotify;
// 中断优先级临时设置
uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();
{
// 保存任务通知的原状态
ucOriginalNotifyState = pxTCB->ucNotifyState;
// 更新通知状态 : 收到通知
pxTCB->ucNotifyState = taskNOTIFICATION_RECEIVED;
// Give 类似返回信号量, 直接递增通知值
( pxTCB->ulNotifiedValue )++;
// 判断被通知任务是否正在阻塞等待通知
if( ucOriginalNotifyState == taskWAITING_NOTIFICATION )
{
// 如果任务调度器运行中
if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )
{
// 把等待任务移到就绪链表
( void ) uxListRemove( &( pxTCB->xStateListItem ) );
prvAddTaskToReadyList( pxTCB );
}
else
{
// 调度器挂起,中断依然正常发生,但是不能直接操作就绪链表
// 加入到就绪挂起链表,任务调度恢复后会移动到就绪链表
vListInsertEnd( &( xPendingReadyList ), &( pxTCB->xEventListItem ) );
}
// 被通知任务优先级高
if( pxTCB->uxPriority > pxCurrentTCB->uxPriority )
{
// 设置返回参数,标识需要任务切换
if( pxHigherPriorityTaskWoken != NULL )
{
*pxHigherPriorityTaskWoken = pdTRUE;
}
else
{
// 如果用户没有设置参数,则使用系统全局变量
// 目的都是说明,需要任务切换
xYieldPending = pdTRUE;
}
}
}
}
// 恢复中断的优先级
portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus );
}
每次调用该函数都会递增任务通知值。
在开头的例子中, 任务通过接收函数返回值是否大于零,判断是否获取到了通知,任务通知值初始化为0, 对应表达信号量无效。
当任务或者中断调用发送函数给出信号量时,递增通知值,使其大于零,使其表示的信号量变为有效,恢复阻塞等待的任务。
如果是在任务中调用,则调用的接口变为 xTaskNotifyGive
, 查看该接口,发现实际是一个宏
#define xTaskNotifyGive( xTaskToNotify )
xTaskGenericNotify( ( xTaskToNotify ), ( 0 ), eIncrement, NULL )
该宏对一个比较通用的函数进行了封装,xTaskGenericNotify
这个函数实际上实现了所有任务通知的方式,通过在调用的时候指定命令类型。
该函数调用说明 :
BaseType_t xTaskGenericNotify(
/*通知任务句柄*/
TaskHandle_t xTaskToNotify,
/*新通知值*/
uint32_t ulValue,
/*发送通知命令类型*/
eNotifyAction eAction,
/*任务原本通知值返回*/
uint32_t *pulPreviousNotificationValue );
该函数实现如下,
BaseType_t xTaskGenericNotify( TaskHandle_t xTaskToNotify,
uint32_t ulValue, eNotifyAction eAction,
uint32_t *pulPreviousNotificationValue )
{
TCB_t * pxTCB;
BaseType_t xReturn = pdPASS;
uint8_t ucOriginalNotifyState;
pxTCB = ( TCB_t * ) xTaskToNotify;
taskENTER_CRITICAL();
{
// 返回任务原来的通知值到传递参数
if( pulPreviousNotificationValue != NULL )
{
*pulPreviousNotificationValue = pxTCB->ulNotifiedValue;
}
// 保存原来的任务通知状态
ucOriginalNotifyState = pxTCB->ucNotifyState;
// 设置新的任务通知状态 : 收到消息
pxTCB->ucNotifyState = taskNOTIFICATION_RECEIVED;
switch( eAction )
{
case eSetBits :
// 置位 : 原通知值与新通知值进行或
pxTCB->ulNotifiedValue |= ulValue;
break;
case eIncrement :
// 增加 : 原通知值加 1
( pxTCB->ulNotifiedValue )++;
break;
case eSetValueWithOverwrite :
// 覆盖 : 新通知值直接覆盖
pxTCB->ulNotifiedValue = ulValue;
break;
case eSetValueWithoutOverwrite :
//不覆盖 : 不覆盖未读消息
if( ucOriginalNotifyState != taskNOTIFICATION_RECEIVED )
{
// 没有未读消息,设置通知值
pxTCB->ulNotifiedValue = ulValue;
}
else
{
// 有未读消息, 不覆盖通知值
xReturn = pdFAIL;
}
break;
case eNoAction:
// 不修改通知值
break;
}
// 如果被通知任务由于等待任务通知而挂起
if( ucOriginalNotifyState == taskWAITING_NOTIFICATION )
{
// 唤醒任务, 插入就绪链表
( void ) uxListRemove( &( pxTCB->xStateListItem ) );
prvAddTaskToReadyList( pxTCB );
#if( configUSE_TICKLESS_IDLE != 0 )
{
//更新下一个阻塞任务超时时间
prvResetNextTaskUnblockTime();
}
#endif
// 被通知任务优先级比当前任务高
if( pxTCB->uxPriority > pxCurrentTCB->uxPriority )
{
// 触发 PendSV , 切换高优先级任务运行
taskYIELD_IF_USING_PREEMPTION();
}
}
}
taskEXIT_CRITICAL();
return xReturn;
}
该函数基本流程如下
- 保存原有通知值,写入传入的指针
- 根据命令类型设置通知值
- 判断被通知任务是否等待通知,根据优先级判断是否需要唤醒
- 返回操作结果
如果调用函数的时候传递了读取旧值的指针, 函数会把原有通知值写到该指针所指内存,之后按照命令类型更新通知值。
回到这一部分二进制信号量,看到该宏调用上面这个函数时,传递的命令类型是 eIncrement
, 也就是在原有通知值基础上递增1, 这样看来,就和 xTaskNotifyGiveFromISR
的效果一样了。
轻量级计数信号量
上面提到二进制信号量,在被通知任务,也就是获取信号量的任务获取了信号量后,会把通知值直接设置为0,这对应了二进制信号量的特点 : 不管任务或者中断调用了几次通知发送函数递增通知值,只要被通知任务读取了一次通知,就会直接把该值清零
。
而计数信号量不同在于读取一次通知后不会直接把通知值清零,而是递减1,因此,任务被通知几次,对应被通知任务就可以执行读取几次,直到通知值递减为0。
到此,我们基本直到,为了实现计数信号量,只需要简单地修改下二进制信号量的获取函数的第一个参数,就可以了。
ulNotificationValue = ulTaskNotifyTake( pdFALSE,/*调用递减不直接清0*/
xMaxBlockTime );
第一个参数设置为 pdFALSE
, 函数ulTaskNotifyTake
不会直接把通知值清零,而是每调用一次递减1,直到0为止。
可以查看上文该函数的实现。
轻量级事件标记组
二进制信号量或者计数信号量只能通知任务一个事件,如果有两种不同的事件,他们就无法实现了。这时候就需要利用事件分组了。
举个应用例子,
一个处理串口事件的任务,串口事件包括接收和发送,对应在其中断中发送通知,我们利用任务通知实现事件分组如下实现 :
/定义事件位标记
#define TX_BIT 0x01
#define RX_BIT 0x02
//任务句柄
static TaskHandle_t xHandlingTask;
// 发送事件中断
void vTxISR( void )
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
// ...
// 发送通知
xTaskNotifyFromISR( xHandlingTask,
TX_BIT,
eSetBits,
&xHigherPriorityTaskWoken );
portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}
//接收事件中断
void vRxISR( void )
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
// ...
// 发送通知
xTaskNotifyFromISR( xHandlingTask,
RX_BIT,
eSetBits,
&xHigherPriorityTaskWoken );
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 )
{
prvProcessTx();
}
if( ( ulNotifiedValue & RX_BIT ) != 0 )
{
prvProcessRx();
}
}
else
{
prvCheckForErrors();
}
}
}
前面介绍到一个通知发送函数 xTaskGenericNotify
, 可以设置命令类型,上面例子中,使用的命令类型是 eSetBits
。在开头可以看到对应每个事件, 用一个bit 去对应指代他,当该事件发生时,发送通知,并且置位通知值的对应位,这样,被通知任务就可以根据通知值的位区分出什么事件通知。
前面实现信号量提到的接收通知的函数是 ulTaskNotifyTake
, 该函数判断是否有未读通知是根据通知值是否为零,相对来说,该函数实现主要是针对信号量那种类型。
例子中任务调用的等待函数,xTaskNotifyWait
,该函数判断是否有通知是依据另外一个变量 ucNotifyState
, 算起来,这里,通知值才算真正承载了有用的通知内容。
该函数的参数说明 :
BaseType_t xTaskNotifyWait(
/*接收通知前清除通知值指定位 对应 1 的bit清除*/
uint32_t ulBitsToClearOnEntry,
/*接收通知后清除通知值指定位*/
uint32_t ulBitsToClearOnExit,
/*接收到的通知值*/
uint32_t *pulNotificationValue,
/*等待时间*/
TickType_t xTicksToWait );
具体看看该函数实现 :
BaseType_t xTaskNotifyWait( uint32_t ulBitsToClearOnEntry, uint32_t ulBitsToClearOnExit, uint32_t *pulNotificationValue, TickType_t xTicksToWait )
{
BaseType_t xReturn;
taskENTER_CRITICAL();
{
// 判断有没有未读通知
if( pxCurrentTCB->ucNotifyState != taskNOTIFICATION_RECEIVED )
{
// 接收通知前,清除通知值中的指定位
pxCurrentTCB->ulNotifiedValue &= ~ulBitsToClearOnEntry;
// 设置任务通知状态 : 等待消息通知
pxCurrentTCB->ucNotifyState = taskWAITING_NOTIFICATION;
// 挂起任务等待通知或者超时
if( xTicksToWait > ( TickType_t ) 0 )
{
prvAddCurrentTaskToDelayedList( xTicksToWait, pdTRUE );
portYIELD_WITHIN_API();
}
}
}
taskEXIT_CRITICAL();
taskENTER_CRITICAL();
{
if( pulNotificationValue != NULL )
{
// 返回通知值 (超时情况下,该值并没有被更新)
*pulNotificationValue = pxCurrentTCB->ulNotifiedValue;
}
// 通过任务通知状态,判断是否收到通知
if( pxCurrentTCB->ucNotifyState == taskWAITING_NOTIFICATION )
{
// 超时,没有收到通知
xReturn = pdFALSE;
}
else
{
// 接收通知后,清除通知值的指定位
pxCurrentTCB->ulNotifiedValue &= ~ulBitsToClearOnExit;
// 返回接收到通知
xReturn = pdTRUE;
}
// 设置任务通知状态
pxCurrentTCB->ucNotifyState = taskNOT_WAITING_NOTIFICATION;
}
taskEXIT_CRITICAL();
return xReturn;
}
调用该函数, 在接收到新的通知值前, 会根据第一个参数清除通知值上的特定位(第一个参数为1的位,对应通知值清0)。
接收到通知后,读取通知值保存到参数 *pulNotificationValue
后,会根据第二个参数清除通知值上对应位的值。
在事件分组这个例子中,任务接收到事件通知后,通过通知值上置的位判断什么事件发生了,然后清除通知值,等待下一次事件发生,置位通知。
轻量级消息邮箱
把通知值作为内容,任务通知相当于是一个深度为1的队列。给任务发送通知就相当于投递了。
另外,如果邮件内容大于32bit,也可以把指针作为内容发送出去,接收任务读取通知值后,转换为指针读取实际内容。
思路如此,不做赘述。