目录
2.4 函数 vTaskNotifyGiveFromISR()
2.6 函数 xTaskNotifyAndQueryFromISR()
在比较新的版本开始,FreeRTOS 新增了任务通知(Task Notifictions)这个功能,可以使用任务通知来代替信号量、消息队列、事件标志组这些东西。使用任务通知的话效率会更高。
1. 任务通知简介
任务通知在 FreeRTOS 中是一个可选的功能,要使用任务通知的话就需要将宏 configUSE_TASK_NOTIFICATIONS 定义为 1。
#if(configUSE_TASK_NOTIFICATIONS==1)
volatile uint32_t ulNotifiedValue[configTASK_NOTIFICATION_ARRAY_ENTRIES];
volatile uint8_t ucNotifyState[configTASK_NOTIFICATION_ARRAY_ENTRIES];
#endif
任务通知就是用来通知任务的,任务控制块中的结构体成员变量 ulNotifiedValue 就是这个通知值。
使用队列、信号量、事件标志组时都需要另外创建一个结构体,通过中间的结构体进行间接通信!
但是使用任务通知时,任务结构体 TCB 中就包含了内部对象,可以直接接收别人发过来的 “通知”;(其中包括任务通知状态和任务通知值都属于任务控制块其中的结构体成员变量)
FreeRTOS 的每个任务都有一个 32 位的通知值,任务控制块中的成员变量 ulNotifiedValue 就是这个通知值。任务通知是一个事件,假如某个任务通知的接收任务因为等待任务通知而阻塞话,向这个接收任务发送任务通知以后就会解除这个任务的阻塞状态(任务通知归其根本是一个事件,任务等待这个事件到来,如果任务因为等待这个事件而进入阻塞的话,那么向接收任务发送这个事件就会解除任务的阻塞状态)。也可以更新任务的任务通知值,任务通知可以通过如下方法更新接收任务的通知值:
- 不覆盖接收任务的通知值(如果上次发送给接收任务的通知还没被处理)。---任务通知值在存放在TCB中的成员变量中,也就是说如果这个成员变量有值,现在接收到的这个成员变量不写进去,如果没有值我再写进去!
- 覆盖接收任务的通知值。---不管这个成员变量中是否有值,都写进去!
- 更新接收任务通知值的一个或多个 bit。---类似于事件标志组!
- 增加接收任务的通知值。---类似于计数型信号量!
合理、灵活的使用上面这些更改任务通知值的方法可以在一些场合中替代队列、二值信号量、计数型信号量和事件标志组。
任务通知的发送使用函数 xTaskNotify() 或者 xTaskNotifyGive()(还有此函数的中断版本)来完成,这个通知值会一直被保存着,直到接收任务调用函数 xTaskNotifyWait() 或者 ulTaskNotifyTake() 来获取这个通知值。假如接收任务因为等待任务通知而阻塞的话那么在接收到任务通知以后就会解除阻塞态。
任务通知虽然可以提高速度,并且减少 RAM 的使用,但是任务通知也是有使用限制的:
- FreeRTOS 的任务通知只能有一个接收任务,其实大多数的应用都是这种情况。(无法广播给多个任务!)
- 接收任务可以因为接收任务通知而进入阻塞态,但是发送任务不会因为任务通知发送失败而阻塞!(发送方无法进入阻塞状态等待!)
- 无法发送数据给中断,ISR 没有任务结构体,所以无法给中断发送数据;但是中断可以使用任务通知的功能发送数据给任务!
- 无法缓存多个数据,任务通知是通过更新任务通知值来发送数据的,但是任务结构体中只有一个任务通知值结构体成员变量,只能保持一个数据!
任务通知值的更新方式有多种类型:
计数值(数值累加,类似信号量)
相应位置一(类似事件标志组)
任意数值(支持覆写和不覆写,类似队列)
任务通知状态:
#define taskNOT_WAITING_NOTIFICATION ((uint8_t)0) 任务未等待通知(默认初始状态)
#define taskWAITING_NOTIFICATION ((uint8_t)1) 任务在等待通知(接收方已经全部准备好了,等待发送方给通知)
#define taskNOTIFICATION_RECEIVED ((uint8_t)2) 任务在等待接收(发送方已经发出去了,等待接收方接收)
2. 发送任务通知
发送任务通知的函数有 6 个:
函数:
xTaskNotify() 发送通知,带有通知值并且不保留接收任务原通知值,用在任务中。
xTaskNotifyFromISR() 发送通知,函数 xTaskNotify() 的中断版本。
xTaskNotifyGive() 发送通知,不带通知值并且不保留接收任务的通知值,此函数会将接收任务的通知值加一,用在任务中。
vTaskNotifyGiveFromISR() 发送通知,函数 xTaskNotifyGive() 的中断版本。
xTaskNotifyAndQuery() 发送通知,带有通知值并且保留接收任务的原通知值,用在任务中。
xTaskNotifyAndQueryFromISR() 发送通知,函数 xTaskNotifyAndQuery() 的中断版本,用在中断服务函数中。
2.1 函数 xTaskNotify()
此函数用于发送任务通知,此函数发送任务通知的时候带有通知值,此函数是个宏,真正执行的函数是 xTaskGenericNotify(),函数原型如下:
BaseType_t xTaskNotify(TaskHandle_t xTaskToNotify,
uint32_t ulValue,
eNotifyAction eAction)
参数:
xTaskToNotify: 任务句柄,指定任务通知是发送给哪个任务的。
ulValue: 任务通知值。
eAction: 任务通知更新的方法,eNotifyAction 是个枚举类型,在文件 task.h 中有如下定义:
typedef enum
{
eNoAction=0,
eSetBits, //更新指定的位 bit
eIncrement, //通知值加一
eSetValueWithOverwrite, //覆写的方式更新通知值
eSetValueWithoutOverwrite //不覆写通知值
}eNotifyAction;
此参数可以选择枚举类型中的任意一个,不同的应用环境其选择也不同。
返回值:
pdFAIL: 当参数 eAction 设置为 eSetValueWithoutOverwrite(不覆写) 的时候,如果任务通知值没有更新成功就返回 pdFAIL。
pdPASS: eAction 设置为其他选项的时候统一返回 pdPASS。
2.2 函数 xTaskNotifyFromISR()
此函数用于发送任务通知,是函数 xTaskNotify() 的中断版本,此函数是个宏,真正执行的是函数 xTaskGenericNotifyFromISR(),此函数原型如下:
BaseType_t xTaskNotify(TaskHandle_t xTaskToNotify,
uint32_t ulValue,
eNotifyAction eAction,
BaseType_t* pxHigherPriorityTaskWoken);
参数:
xTaskToNotify: 任务句柄,指定任务通知是发送给哪个任务的。
ulValue: 任务通知值。
eAction: 任务通知更新的方法。
pxHigherPriorityTaskWoken: 记退出此函数以后是否进行任务切换,这个变量的值函数会自动设置,用户不用进行设置,用户只需要提供一个变量来保存这个值就可以了。
当此值为 pdTRUE 的时候在退出中断服务函数之前一定要进行一次任务切换。
返回值:
pdFAIL: 当参数 eAction 设置为 eSetValueWithoutOverwrite(不覆写) 的时候,如果任务通知值没有更新成功就返回 pdFAIL。
pdPASS: eAction 设置为其他选项的时候统一返回 pdPASS。
2.3 函数 xTaskNotifyGive()
发送任务通知,相对于函数 xTaskNotify(),此函数发送任务通知的时候不带有通知值。此函数只是将任务通知值简单的加一,此函数是个宏,真正执行的是函数 xTaskGenericNotify(),此函数原型如下:
BaseType_t xTaskNotifyGive(TaskHandle_t xTaskToNotify);
参数:
xTaskToNotify: 任务句柄,指定任务通知是发送给哪个任务的。
返回值:
pdPASS: 此函数只会返回 pdPASS。
2.4 函数 vTaskNotifyGiveFromISR()
此函数为 xTaskNotifyGive() 的中断版本,用在中断服务函数中,函数原型如下:
void vTaskNotifyGiveFromISR(TaskHandle_t xTaskHandle,
BaseType_t* pxHigherPriorityTaskWoken);
参数:
xTaskToNotify: 任务句柄,指定任务通知是发送给哪个任务的。
pxHigherPriorityTaskWoken: 记退出此函数以后是否进行任务切换,这个变量的值函数会自动设置,用户不用进行设置,用户只需要提供一个变量来保存这个值就可以了。
当此值为 pdTRUE 的时候在退出中断服务函数之前一定要进行一次任务切换。
返回值:
无。
2.5 函数 xTaskNotifyAndQuery()
此函数比 xTaskNotify() 多一个参数,此参数用来保存更新前的通知值。此函数是个宏,真正执行的是函数 xTaskGenericNotify(),此函数原型如下:
BaseType_t xTaskNotify(TaskHandle_t xTaskToNotify,
uint32_t ulValue,
eNotifyAction eAction,
uint32_t* pulPreviousNotificationValue);
参数:
xTaskToNotify: 任务句柄,指定任务通知是发送给哪个任务的。
ulValue: 任务通知值。
eAction: 任务通知更新的方法。
pulPreviousNotificationValue: 用来保存更新前的任务通知值。
返回值:
pdFAIL: 当参数 eAction 设置为 eSetValueWithoutOverwrite 的时候,如果任务通知值没有更新成功就返回 pdFAIL。
pdPASS: eAction 设置为其他选项的时候统一返回 pdPASS。
2.6 函数 xTaskNotifyAndQueryFromISR()
此函数 xTaskNotifyAndQuery() 的中断版本,用在中断服务函数中。此函数同样是个宏,真正执行的是函数 xTaskGenericNotifyFromISR(),此函数原型如下:
BaseType_t xTaskNotify(TaskHandle_t xTaskToNotify,
uint32_t ulValue,
eNotifyAction eAction,
uint32_t* pulPreviousNotificationValue,
BaseType_t* pxHigherPriorityTaskWoken);
参数:
xTaskToNotify: 任务句柄,指定任务通知是发送给哪个任务的。
ulValue: 任务通知值。
eAction: 任务通知更新的方法。
pulPreviousNotificationValue: 用来保存更新前的任务通知值。
pxHigherPriorityTaskWoken: 记退出此函数以后是否进行任务切换,这个变量的值函数会自动设置,用户不用进行设置,用户只需要提供一个变量来保存这个值就可以了。
当此值为 pdTRUE 的时候在退出中断服务函数之前一定要进行一次任务切换。
返回值:
pdFAIL: 当参数 eAction 设置为 eSetValueWithoutOverwrite 的时候,如果任务通知值没有更新成功就返回 pdFAIL。
pdPASS: eAction 设置为其他选项的时候统一返回 pdPASS。
3. 任务通知通用发送函数
3.1 任务级任务通知通用发送函数
任务通知发送函数:xTaskNotify()、xTaskNotifyGive() 和 xTaskNotifyAndQuery(),这三个函数最终调用的都是函数 xTaskGenericNotify()!
BaseType_t xTaskGenericNotify( TaskHandle_t xTaskToNotify, //任务句柄
uint32_t ulValue, //任务通知值
eNotifyAction eAction, //任务通知更新方式
uint32_t * pulPreviousNotificationValue )//保存更新前的
//任务通知值
{
TCB_t * pxTCB;
BaseType_t xReturn = pdPASS;
uint8_t ucOriginalNotifyState;
configASSERT( xTaskToNotify );
pxTCB = ( TCB_t * ) xTaskToNotify;
taskENTER_CRITICAL();
{
if( pulPreviousNotificationValue != NULL ) (1) 判断此参数是否有效,此参数保存更新前的任务通知值
{
*pulPreviousNotificationValue = pxTCB->ulNotifiedValue; (2) 如果此参数有效,就用此参数保存更新前的任务通知值
}
ucOriginalNotifyState = pxTCB->ucNotifyState; (3) 保存任务通知状态,因为下面会修改这个状态,后续我们根据这个状态来判断是否将任务从阻塞态解除
pxTCB->ucNotifyState = taskNOTIFICATION_RECEIVED; (4) 更新任务通知状态为 taskNOTIFICATION_RECEIVED
switch( eAction ) //根据不同更新方式做不同的处理
{
case eSetBits : (5) 将指定的位置1,也就是更新接收任务通知值的一个或多个bit
pxTCB->ulNotifiedValue |= ulValue;
break;
case eIncrement : (6) 将任务通知值加一
( pxTCB->ulNotifiedValue )++;
break;
case eSetValueWithOverwrite : (7) 直接覆写原来的任务通知值
pxTCB->ulNotifiedValue = ulValue;
break;
case eSetValueWithoutOverwrite : (8) 判断原来的任务通知值是否被处理,如果已经被处理了就更新任务通知值
if( ucOriginalNotifyState != taskNOTIFICATION_RECEIVED ) //任务通知值不等于原先接收到的值,证明原先的值被处理了
{
pxTCB->ulNotifiedValue = ulValue; //更新任务通知值
}
else
{
xReturn = pdFAIL; 没有被处理,标记 xReturn 为 pdFAIL
}
break;
case eNoAction:
break;
}
traceTASK_NOTIFY();
//如果任务因为等待任务通知而进入阻塞态的话就需要解除阻塞
if( ucOriginalNotifyState == taskWAITING_NOTIFICATION ) (9) 根据接收任务之前保存的状态值来判断是否有任务需要解除阻塞,如果更新前任务处于 taskWAITING_NOTIFICATION 状态的话说明有任务因为等待任务通知值而进入了阻塞态。
{
( void ) uxListRemove( &( pxTCB->xStateListItem ) ); (10) 将任务从状态列表中移除
prvAddTaskToReadyList( pxTCB ); (11) 将任务重新添加到就绪列表中
/******************************************************************/
/********************省略相关的条件编译代码************************/
/******************************************************************/
if( pxTCB->uxPriority > pxCurrentTCB->uxPriority ) (12) 判断刚刚解除阻塞的任务的优先级是否比当前正在运行的任务的优先级高,如果是的话需要进行一次任务切换
{
//解除阻塞的任务优先级比当前运行的任务优先级高,所以需要进行
//任务切换。
taskYIELD_IF_USING_PREEMPTION();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
taskEXIT_CRITICAL();
return xReturn; (13) 返回 xReturn的值,pdFAIL 或者 pdPASS
}
3.2 中断级任务通知发送函数
中断级任务通知发送函数也有三个,分别为: xTaskNotifyFromISR() 、xTaskNotifyAndQueryFromISR()和 vTaskNotifyGiveFromISR()。其中函数 xTaskNotifyFromISR() 和 xTaskNotifyAndQueryFromISR()最终调用的都是函数 xTaskGenericNotifyFromISR();
BaseType_t xTaskGenericNotifyFromISR(TaskHandle_t xTaskToNotify,
uint32_t ulValue,
eNotifyAction eAction,
uint32_t* pulPreviousNotificationValue,
BaseType_t* pxHigherPriorityTaskWoken)
参数:
xTaskToNotify: 任务句柄,指定任务通知是发送给哪个任务的。
ulValue: 任务通知值。
eAction: 任务通知更新的方法。
pulPreviousNotificationValue: 用来保存更新前的任务通知值。
pxHigherPriorityTaskWoken: 记退出此函数以后是否进行任务切换,这个变量的值函数会自动设置,用户不用进行设置,用户只需要提供一个变量来保存这个值。
当此值为 pdTRUE 的时候在退出中断服务函数之前一定要进行一次任务切换。
返回值:
pdFAIL: 当参数 eAction 设置为 eSetValueWithoutOverwrite 的时候,如果任务通知值没有更新成功就返回 pdFAIL。
pdPASS: eAction 设置为其他选项的时候统一返回 pdPASS。
BaseType_t xTaskGenericNotifyFromISR( TaskHandle_t xTaskToNotify,
uint32_t ulValue,
eNotifyAction eAction,
uint32_t * pulPreviousNotificationValue,
BaseType_t * pxHigherPriorityTaskWoken )
{
TCB_t * pxTCB;
uint8_t ucOriginalNotifyState;
BaseType_t xReturn = pdPASS;
UBaseType_t uxSavedInterruptStatus;
configASSERT( xTaskToNotify );
portASSERT_IF_INTERRUPT_PRIORITY_INVALID();
pxTCB = ( TCB_t * ) xTaskToNotify;
uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();
{
if( pulPreviousNotificationValue != NULL ) (1) 判断参数是否有效,如果参数有效就用此参数保存更新前的任务通知值。
{
*pulPreviousNotificationValue = pxTCB->ulNotifiedValue;
}
ucOriginalNotifyState = pxTCB->ucNotifyState; (2) 保存任务通知状态,因为下面会修改这个状态,后续我们根据这个状态来确定是否将任务解除阻塞态
pxTCB->ucNotifyState = taskNOTIFICATION_RECEIVED; (3) 更新任务通知状态 taskNOTIFICATION_RECEIVED
switch( eAction ) (4) 根据不同通知值来做不同的处理
{
case eSetBits :
pxTCB->ulNotifiedValue |= ulValue;
break;
case eIncrement :
( 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;
}
traceTASK_NOTIFY_FROM_ISR();
//如果任务因为等待任务通知而进入阻塞态的话就需要解除阻塞
if( ucOriginalNotifyState == taskNOTIFICATION_RECEIVED) (5) 判断是否有任务需要解除阻塞,如果在任务通知值更新前处于 taskNOTIFICATION_RECEIVED 的话就说明有任务因为等待任务通知而进入阻塞态。
{
configASSERT( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) ==
NULL );
if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE ) (6) 判断任务调度器是否上锁,如果调度器没有上锁的话就将任务从状态列表中移除,然后添加到就绪列表中
{
( void ) uxListRemove( &( pxTCB->xStateListItem ) );
prvAddTaskToReadyList( pxTCB );
}
else (7) 如果任务调度器上锁的话就将任务添加到列表 xPendingReadyList
{
vListInsertEnd( &( xPendingReadyList ), &( pxTCB->xEventListItem ) );
}
if( pxTCB->uxPriority > pxCurrentTCB->uxPriority ) (8) 判断任务解除阻塞的任务优先级是否比当前的任务优先级高
{
//解除阻塞的任务优先级比当前运行任务的优先级高,所以需要标记
//在退出中断服务函数的时候需要做任务切换。
if( pxHigherPriorityTaskWoken != NULL )
{
*pxHigherPriorityTaskWoken = pdTRUE;
}
else
{
xYieldPending = pdTRUE;
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
}
portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus );
return xReturn;
}
4. 获取任务通知
获取任务通知的函数有两个:
函数:
ulTaskNotifyTake() 获取任务通知,可以设置在退出此函数的时候将任务通知值清零或者减一。当任务通知用作二值信号量或者计数型信号量的时候使用此函数来获取信号量。
xTaskNotifyWait() 等待任务通知,比 ulTaskNotifyTake() 更为强大。
4.1 函数 ulTaskNotifyTake()
此函数为获取任务通知函数,当任务通知用作二值信号量或者计数型信号量的时候可以使用此函数来获取信号量,函数原型如下:
uint32_t ulTaskNotifyTake(BaseType_t xClearCountOnExit,
TickType_t xTicksToWait);
参数:
xClearCountOnExit: 参数为 pdFALSE 的话在退出函数 ulTaskNotifyTake() 的时候任务通知值减一,类似计数型信号量;
当此参数为 pdTRUE 的话在退出函数的时候任务通知值清零,类似二值信号量。
xTickToWait: 阻塞时间。
返回值:
任何值: 任务通知值减少或者清零之前的值。
uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit, TickType_t xTicksToWait )
{
uint32_t ulReturn;
taskENTER_CRITICAL();
{
if( pxCurrentTCB->ulNotifiedValue == 0UL ) (1) 判断任务通知值是否等于0,如果为0说明还没有接收到任务通知
{
pxCurrentTCB->ucNotifyState = taskWAITING_NOTIFICATION; (2) 修改任务通知状态位 taskWAITING_NOTIFICATION
if( xTicksToWait > ( TickType_t ) 0 ) (3) 如果阻塞时间不为 0 的话就将任务添加到延时列表中,并且进行一次任务调度。
{
prvAddCurrentTaskToDelayedList( xTicksToWait, pdTRUE );
traceTASK_NOTIFY_TAKE_BLOCK();
portYIELD_WITHIN_API();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
taskEXIT_CRITICAL();
taskENTER_CRITICAL();
{
traceTASK_NOTIFY_TAKE();
ulReturn = pxCurrentTCB->ulNotifiedValue; (4) 如果任务通知值不为 0 的话就先获取任务通知值。
if( ulReturn != 0UL ) (5) 任务通知值不为 0 意味着任务通知值大于0
{
if( xClearCountOnExit!= pdFALSE ) (6) 如果参数 xClearCountOnExit 不为 pdFASLE,就先将任务通知值清零。
{
pxCurrentTCB->ulNotifiedValue = 0UL;
}
else
{
pxCurrentTCB->ulNotifiedValue = ulReturn - 1; (7) 如果参数 xClearCountOnExit 为 pdFASLE,那就先将任务通知值减一。
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
pxCurrentTCB->ucNotifyState = taskNOT_WAITING_NOTIFICATION; (8) 更新任务通知值为 taskNOT_WAITING_NOTIFICATION。
}
taskEXIT_CRITICAL();
return ulReturn;
}
4.2 函数 xTaskNotifyWait()
此函数也是用来获取任务通知的,不过此函数比 ulTaskNotifyTake() 更为强大,不管任务通知用作二值信号量、计数型信号量、队列和事件标志组中的哪一种,都可以使用此函数来获取任务通知。但是当任务通知用作二值信号量和计数型信号量的时候推荐使用函数 ulTaskNotifyTake()。此函数原型如下:
BaseType_t xTaskNotifyWait(uint32_t ulBitsToClearOnEntry,
uint32_t ulBitsToClearOnExit,
uint32_t* pulNotificationValue,
TickType_t xTicksToWait);
参数:
ulBitsToClearOnEntry: 当没有接收到任务通知的时候将任务通知值与此参数的取反值进行按位与运算,当此参数为 0xffffffff 或者 ULONG_MAX 的时候就会将任务通知值清零。
ulBitsToClearOnExit: 如果接收到了任务通知,在做完相应的处理退出函数之前将任务通知值与此参数的取反值进行按位与运算,当此参数为 0xffffffff 或者 ULONG_MAX 的时候就会将任务通知值清零。
pulNotificationValue: 此参数用来保存任务通知值。
xTickToWait: 阻塞时间。
返回值:
pdTRUE: 获取到了任务通知。
pdFALSE: 任务通知获取失败。
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 ) (1) 任务通知值不为 taskNOTIFICATION_RECEIVED
{
pxCurrentTCB->ulNotifiedValue &= ~ulBitsToClearOnEntry; (2) 将任务通知值与 ulBitsToClearOnEntry 取反值进行按位与运算
pxCurrentTCB->ucNotifyState = taskWAITING_NOTIFICATION; (3) 任务通知状态改为 taskWAITING_NOTIFICATION
if( xTicksToWait > ( TickType_t ) 0 ) (4) 如果阻塞时间大于 0 的话就将任务添加到延时列表中,并且进行一次任务切换。
{
prvAddCurrentTaskToDelayedList( xTicksToWait, pdTRUE );
traceTASK_NOTIFY_WAIT_BLOCK();
portYIELD_WITHIN_API();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
taskEXIT_CRITICAL();
taskENTER_CRITICAL();
{
traceTASK_NOTIFY_WAIT();
if( pulNotificationValue != NULL ) (5) 如果任务通知状态为 taskNOTIFICATION_RECEIVED,并且参数 pulNotificationValue 有效的话就保存任务通知值。
{
*pulNotificationValue = pxCurrentTCB->ulNotifiedValue;
}
if( pxCurrentTCB->ucNotifyState == taskWAITING_NOTIFICATION ) (6) 如果任务通知的状态又变为 taskWAITING_NOTIFICATION 的话就标记 xRetur 为 pdFALSE。
{
xReturn = pdFALSE;
}
else
{
pxCurrentTCB->ulNotifiedValue &= ~ulBitsToClearOnExit; (7) 如果任务通知的状态一直为 taskNOTIFICATION_RECEIVED 的话就将任务通知的值与参数 ulBitsToClearOnExit的取反值进行按位与运算,并且标记 xReturn 为 pdTRUE 表示获取任务通知成功。
xReturn = pdTRUE;
}
pxCurrentTCB->ucNotifyState = taskNOT_WAITING_NOTIFICATION; (8) 标记任务通知状态为 taskNOT_WAITING_NOTIFICATION
}
taskEXIT_CRITICAL();
return xReturn;
}
5. 任务通知模拟二值信号量实验
使用任务通知代替二值信号量的时候任务解除阻塞态的时间要快 45%,并且需要的 RAM 也很少。
二值信号量就是值最大为 1 的信号量,这也是名字中 “二值” 的来源。当任务通知用于替代二值信号量的时候任务通知值就会代替信号量值,函数 ulTaskNotifyTake() 就可以代替信号量获取函数 xSemaphoreTake(),函数 ulTaskNotifyTake() 的参数 xClearCountOnExit 设置为 pdTRUE。这样在每次获取任务通知的时候模拟的信号量值就会清零。
uint32_t ulTaskNotifyTake(BaseType_t xClearCountOnExit,
TickType_t xTicksToWait);
参数:
xClearCountOnExit: 参数为 pdFALSE 的话在退出函数 ulTaskNotifyTake() 的时候任务通知值减一,类似计数型信号量;
当此参数为 pdTRUE 的话在退出函数的时候任务通知值清零,类似二值信号量。
xTickToWait: 阻塞时间。
返回值:
任何值: 任务通知值减少或者清零之前的值。
函数 xTaskNotifyGive() 和 xTaskNotifyGiveFromISR() 用于替代函数 xSemaphoreGive() 和 xSemaphoreGiveFromISR()。
BaseType_t xTaskNotifyGive(TaskHandle_t xTaskToNotify);
参数:
xTaskToNotify : 发送给哪个任务的任务句柄
5.1 实验程序
本实验在 —— 二值信号量实验的基础上修改而来;FreeRTOS_信号量之二值信号量-CSDN博客
5.1.1 main.c
#include "stm32f4xx.h"
#include "FreeRTOS.h" //这里注意必须先引用FreeRTOS的头文件,然后再引用task.h
#include "task.h" //存在一个先后的关系
#include "LED.h"
#include "LCD.h"
#include "Key.h"
#include "usart.h"
#include "delay.h"
#include "string.h"
#include "beep.h"
#include "malloc.h"
#include "timer.h"
#include "queue.h"
#include "semphr.h"
//任务优先级
#define START_TASK_PRIO 1 //用于创建其他两个任务
//任务堆栈大小
#define START_STK_SIZE 256
//任务句柄
TaskHandle_t StartTask_Handler;
//任务函数
void start_task(void *pvParameters);
//任务优先级
#define TASK1_TASK_PRIO 2 //控制 LED0 闪烁,提示系统正在运行
//任务堆栈大小
#define TASK1_STK_SIZE 256
//任务句柄
TaskHandle_t Task1Task_Handler;
//任务函数
void task1_task(void *pvParameters);
//任务优先级
#define DATAPROCESS_TASK_PRIO 3 //指令处理函数
//任务堆栈大小
#define DATAPROCESS_STK_SIZE 256
//任务句柄
TaskHandle_t DataProcess_Handler;
//任务函数
void DataProcess_task(void *pvParameters);
//二值信号量句柄
SemaphoreHandle_t BinarySemaphore; //二值信号量句柄
//用于命令解析用的命令值
#define LED1ON 1
#define LED1OFF 2
#define BEEPON 3
#define BEEPOFF 4
#define COMMANDERR 0xFF
//函数 LowerToCap 用于将串口发送过来的命令中的小写字母统一转换成大写字母,
//这样就可以在发送命令的时候不用区分大小写,因为开发板会统一转换成大写。
//将字符串中的小写字母转换为大写
//str:要转换的字符串
//len:字符串长度
void LowerToCap(u8 *str,u8 len)
{
u8 i;
for(i=0;i<len;i++)
{
//判断字符串的ASCII码是否位于96到123之间
if((96<str[i])&&(str[i]<123)) //小写字母
{
//ASCII码是一种用于表示字符的编码系统。在ASCII码中,每个字符都被赋予一个唯一的整数值。
//大写字母的ASCII码值是65到90
//小写字母的ASCII码值是97到122 所以一旦确定ASCII码值位于小写字母的范畴内,只需要将ASCII码值减去32即可转换为大写
str[i] = str[i] - 32; //转换为大写
}
}
}
//函数 CommandProcess 用于将接收到的命令字符串转换成命令值,比如说命令“LED1ON”转换成命令值就是 0(宏LED1ON为 0)
//命令处理函数,将字符串命令转换成命令值
//str:命令
//返回值:0xFF,命令错误;其他值,命令值
u8 CommandProcess(u8 *str)
{
u8 CommandValue = COMMANDERR;
if(strcmp((char*)str,"LED1ON")==0) //strcmp 字符串比较函数
//这个函数会比较两个参数;比较时,会以字符的ASCII值进行比较
//如果str1的ASCII码值小于str2,返回一个负数;反之,返回一个正数;
//如果str1的ASCII码值等于str2,返回 0,此时,if判断语句成立
CommandValue = LED1ON; //设置的LED1ON的宏为1,也就是在串口输入1,if判断语句成立
else if(strcmp((char*)str,"LED1OFF")==0)
CommandValue = LED1OFF; //在串口输入2,if判断语句成立
else if(strcmp((char*)str,"BEEPON")==0)
CommandValue = BEEPON; //在串口输入3,if判断语句成立
else if(strcmp((char*)str,"BEEPOFF")==0)
CommandValue = BEEPOFF; //在串口输入4,if判断语句成立
return CommandValue;
}
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4); //设置系统中断优先级
delay_init(168);
uart_init(115200);
LED_Init();
KEY_Init();
BEEP_Init();
LCD_Init();
my_mem_init(SRAMIN); //初始化内部内存池
POINT_COLOR=RED;
LCD_ShowString(10,10,200,16,16,"ATK STM32F407");
LCD_ShowString(10,30,200,16,16,"FreeRTOS Example");
LCD_ShowString(10,50,200,16,16,"Task Notify Binary Semaphore");
LCD_ShowString(10,70,200,16,16,"Command Data:");
//创建开始任务
xTaskCreate((TaskFunction_t )start_task, //任务函数
(const char* )"start_task", //任务名称
(uint16_t )START_STK_SIZE, //任务堆栈大小
(void* )NULL, //传递给任务函数的参数
(UBaseType_t )START_TASK_PRIO, //任务优先级
(TaskHandle_t* )&StartTask_Handler); //任务句柄
vTaskStartScheduler(); //开启任务调度
}
//开始任务任务函数
void start_task(void *pvParameters)
{
taskENTER_CRITICAL(); //进入临界区
//这里不需要再创建二值信号量了
//因为任务通知需要的任务通知状态和任务通知值都在TCB的成员变量中
//创建Task1任务
xTaskCreate((TaskFunction_t )task1_task, //任务函数
(const char* )"task1_task", //任务名称
(uint16_t )TASK1_STK_SIZE, //任务堆栈大小
(void* )NULL, //传递给任务函数的参数
(UBaseType_t )TASK1_TASK_PRIO, //任务优先级
(TaskHandle_t* )&Task1Task_Handler); //任务句柄
//创建Task2任务
xTaskCreate((TaskFunction_t )DataProcess_task, //任务函数
(const char* )"DataProcess_task", //任务名称
(uint16_t )DATAPROCESS_STK_SIZE, //任务堆栈大小
(void* )NULL, //传递给任务函数的参数
(UBaseType_t )DATAPROCESS_TASK_PRIO, //任务优先级
(TaskHandle_t* )&DataProcess_Handler); //任务句柄
vTaskDelete(StartTask_Handler); //删除开始任务
taskEXIT_CRITICAL(); //退出临界区
}
//Task1任务
//控制 LED0 闪烁,提示系统正在运行
void task1_task(void *pvParameters)
{
while(1)
{
LED0=!LED0;
vTaskDelay(500); //延时500ms,也就是500个时钟节拍
}
}
//DataProcess_task函数
//指令处理任务,根据接收到的指令来控制不同的外设
void DataProcess_task(void *pvParameters)
{
u8 len=0;
u8 CommandValue=COMMANDERR;
u32 NotifyValue;
u8 *CommandStr;
POINT_COLOR=BLUE;
while(1)
{
NotifyValue = ulTaskNotifyTake(pdTRUE,portMAX_DELAY); //获取任务通知
//返回值:任务通知值减少或者清零之前的值
//第一个参数:参数为 pdFALSE 的话在退出 ulTaskNotifyTake 的时候任务通知值减一,类似于计数型信号量
// 参数为 pdTRUE 的话在退出 ulTaskNotifyTake 的时候任务通知值清零,类似于二值信号量
//第二个参数:阻塞时间
if(NotifyValue==1) //函数返回值为1,表示清零之前的任务通知值为1,说明任务通知有效
{
len=USART_RX_STA&0x3fff; //得到此次接收到的数据长度
//接收状态
//bit15, 接收完成标志
//bit14, 接收到0x0d
//bit13~0, 接收到的有效字节数目
CommandStr=mymalloc(SRAMIN,len+1); //申请内存 指针指向申请内存的首地址
sprintf((char*)CommandStr,"%s",USART_RX_BUF); //打印接收缓存区,把接收缓存区的数据保存到CommandStr中
CommandStr[len]='\0'; //加上字符串结尾符号
//CommandStr 是个指针,长度为len,数组是从下角标 0 开始的,所以len就表示数组的最后一个
LowerToCap(CommandStr,len); //将字符串转换成大写
CommandValue=CommandProcess(CommandStr); //命令解析,也就是获取上面定义的宏 1 2 3 4
if(CommandValue!=COMMANDERR)//if判断语句成立,表示CommandValue不等于0xFF,那也就是 LED1ON、LED1OFF、BEEPON、BEEPOFF 其中一个指令
{
LCD_Fill(10,90,210,110,WHITE); //清除显示区域
LCD_ShowString(10,90,200,16,16,CommandStr); //在LCD上显示命令
printf("命令为:%s\r\n",CommandStr);
switch(CommandValue)
{
case LED1ON:
LED1=0;
break;
case LED1OFF:
LED1=1;
break;
case BEEPON:
BEEP=1;
break;
case BEEPOFF:
BEEP=0;
break;
}
}
else
{//当命令错误的时候开发板会向串口调试助手发送命令错误的提示信息
//比如我们发送 LED1_off 这个命令,串口助手会显示:无效的命令,请重新输入!!
printf("无效的命令,请重新输入!!\r\n");
}
USART_RX_STA = 0;
memset(USART_RX_BUF,0,USART_REC_LEN); //串口接收缓冲区清零
myfree(SRAMIN,CommandStr); //释放内存
}
else //else表示清零之前的任务通知值为0,表示任务通知无效
{
vTaskDelay(10); //延时10ms,也就是10个时钟节拍
}
}
}
5.1.2 usart.c
extern TaskHandle_t DataProcess_Handler; //接收任务通知的任务句柄
//QueueHandle_t queue.h 中定义
void USART1_IRQHandler(void) //串口1中断服务程序
{
u8 Res;
//xHigherPriorityTaskWoken:用来标记退出此函数以后是否进行任务切换,这个变量的值由三个函数来设置,用户不再进行设置
//用户只需要提供一个变量来保存这个值就可以了。
//但是切记要注意:当此值为 pdTURE 的时候在退出中断服务函数之前一定要进行一次任务切换。
BaseType_t xHigherPriorityTaskWoken; //BaseType_t 也在 queue.h 中定义
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) //接收中断(接收到的数据必须是0x0d 0x0a结尾)
{
Res =USART_ReceiveData(USART1);//(USART1->DR); //读取接收到的数据
if((USART_RX_STA&0x8000)==0)//接收未完成
{
if(USART_RX_STA&0x4000)//接收到了0x0d
{
if(Res!=0x0a)USART_RX_STA=0;//接收错误,重新开始
else USART_RX_STA|=0x8000; //接收完成了
}
else //还没收到0X0D
{
if(Res==0x0d)USART_RX_STA|=0x4000;
else
{
USART_RX_BUF[USART_RX_STA&0X3FFF]=Res;
USART_RX_STA++;
if(USART_RX_STA>(USART_REC_LEN-1))USART_RX_STA=0;//接收数据错误,重新开始接收
}
}
}
}
//释放二值信号量
//指令通过串口发送给开发板,串口中断用来释放二值信号量,任务用来不断获取信号量
//任务一旦获取到信号量,就会从串口接收缓冲区中提取这些指令,然后根据这些指令控制相应的外设
if((USART_RX_STA&0x8000)&&(DataProcess_Handler!=NULL)) //串口接收到数据,并且接收任务通知的任务有效
{
vTaskNotifyGiveFromISR(DataProcess_Handler,&xHigherPriorityTaskWoken); //调用在中断中发送任务通知函数
//函数第一个参数:发送通知,指明任务通知是发送给哪个任务的
//函数第二个参数:标记是否需要进行任务切换
portYIELD_FROM_ISR(xHigherPriorityTaskWoken); //如果需要的话进行一次任务切换
} //二值信号量用来实现同步的意思就是说:保证中断先释放信号量,然后任务在获取信号量;
}
6. 任务通知模拟计数型信号量实验
不同于二值信号量,计数型信号量值可以大于 1,这个最大值在创建信号量的时候可以设置。当计数型信号量有效的时候任务可以获取计数型信号量,信号量值只要大于 0 就表示计数型信号量有效。
当任务通知用作计数型信号量的时候获取信号量相当于获取任务通知值,使用函数 ulTakeNotifyTake() 来代替函数 xSemaphoreTake()。函数 ulTaskNotifyTake() 的参数 xClearOnExit 要设置为 pdFALSE,这样每次获取任务通知成功以后任务通知值就会减一。使用任务通知发送函数 xTaskNotifyGive() 和 vTaskNotifyGiveFromISR() 来代替计数型信号量释放函数 xSemaphoreGive() 和 xSemaphoreGiveFromISR()。
6.1 实验程序
本实验在 —— 计数型信号量实验的基础上修改而来;FreeRTOS_信号量之计数型信号量-CSDN博客
6.1.1 main.c
#include "stm32f4xx.h"
#include "FreeRTOS.h" //这里注意必须先引用FreeRTOS的头文件,然后再引用task.h
#include "task.h" //存在一个先后的关系
#include "LED.h"
#include "LCD.h"
#include "Key.h"
#include "usart.h"
#include "delay.h"
#include "string.h"
#include "beep.h"
#include "malloc.h"
#include "timer.h"
#include "queue.h"
#include "semphr.h"
//任务优先级
#define START_TASK_PRIO 1 //用于创建其他两个任务
//任务堆栈大小
#define START_STK_SIZE 256
//任务句柄
TaskHandle_t StartTask_Handler;
//任务函数
void start_task(void *pvParameters);
//任务优先级
#define SEMAPGIVE_TASK_PRIO 2 //获取按键状态,当 KEY_UP 键按下去以后就释放任务通知
//任务堆栈大小
#define SEMAPGIVE_STK_SIZE 256
//任务句柄
TaskHandle_t SemapGiveTask_Handler;
//任务函数
void SemapGive_task(void *pvParameters);
//任务优先级
#define SEMAPTAKE_TASK_PRIO 3 //获取任务通知,当获取任务通知成功以后就刷新 LCD 指定区域的背景色
//任务堆栈大小
#define SEMAPTAKE_STK_SIZE 256
//任务句柄
TaskHandle_t SemapTakeTask_Handler;
//任务函数
void SemapTake_task(void *pvParameters);
//LCD刷屏时使用的颜色
int lcd_discolor[14] = { WHITE, BLACK, BLUE, BRED,
GRED, GBLUE, RED, MAGENTA,
GREEN, CYAN, YELLOW,BROWN,
BRRED, GRAY};
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4); //设置系统中断优先级分组4
delay_init(168);
uart_init(115200);
LED_Init();
KEY_Init();
BEEP_Init();
LCD_Init();
my_mem_init(SRAMIN); //初始化内部内存池
POINT_COLOR=RED;
LCD_ShowString(30,10,200,16,16,"ATK STM32F407");
LCD_ShowString(30,30,200,16,16,"FreeRTOS Example");
LCD_ShowString(30,50,200,16,16,"Task Notify Count Semaphore");
LCD_ShowString(30,70,200,16,16,"ATOM@ALIENTEK");
LCD_ShowString(30,90,200,16,16,"2023/11/06");
POINT_COLOR=BLACK;
LCD_DrawRectangle(5,110,234,314);
LCD_DrawLine(5,130,234,130);
POINT_COLOR=RED;
LCD_ShowString(54,111,200,16,16,"Notify Value: 0");
POINT_COLOR=BLUE;
//创建开始任务
xTaskCreate((TaskFunction_t )start_task, //任务函数
(const char* )"start_task", //任务名称
(uint16_t )START_STK_SIZE, //任务堆栈大小
(void* )NULL, //传递给任务函数的参数
(UBaseType_t )START_TASK_PRIO, //任务优先级
(TaskHandle_t* )&StartTask_Handler); //任务句柄
vTaskStartScheduler(); //开启任务调度
}
//开始任务任务函数
void start_task(void *pvParameters)
{
taskENTER_CRITICAL();
//这里不用创建计数型信号量,任务通知所需要任务通知值和任务通知状态存储在TCB中
//创建释放信号量任务
xTaskCreate((TaskFunction_t )SemapGive_task,
(const char* )"semapgive_task",
(uint16_t )SEMAPGIVE_STK_SIZE,
(void* )NULL,
(UBaseType_t )SEMAPGIVE_TASK_PRIO,
(TaskHandle_t* )&SemapGiveTask_Handler);
//创建获取信号量任务
xTaskCreate((TaskFunction_t )SemapTake_task,
(const char* )"semaptake_task",
(uint16_t )SEMAPTAKE_STK_SIZE,
(void* )NULL,
(UBaseType_t )SEMAPTAKE_TASK_PRIO,
(TaskHandle_t* )&SemapTakeTask_Handler);
vTaskDelete(StartTask_Handler); //删除开始任务
taskEXIT_CRITICAL();
}
//释放计数型信号量任务函数
void SemapGive_task(void *pvParameters)
{
u8 key,i=0;
while(1)
{
key=KEY_Scan(0); //扫描按键
if(SemapTakeTask_Handler!=NULL)
//SemapTakeTask_Handler 表示获取任务通知的任务句柄,该任务句柄不为空,表示有任务正在等待信号量,也可以说计数型信号量的计数值大于0
//如果该句柄不为空,那么按键KEY_UP发送任务通知,解除该任务的阻塞态!使得任务得以继续执行!
{
switch(key) //key 取值为 1 2 3 4 0,0表示无按键按下
{
case WKUP_PRES: //key 等于4
xTaskNotifyGive(SemapTakeTask_Handler); //发送任务通知,不带有通知值
break;
}
}
i++;
if(i==50)
{
i=0;
LED0=!LED0;
}
vTaskDelay(10); //延时10ms,也就是10个时钟节拍
}
}
//获取计数型信号量任务函数
void SemapTake_task(void *pvParameters)
{
u8 num;
uint32_t NotifyValue;
while(1)
{
NotifyValue = ulTaskNotifyTake(pdFALSE,portMAX_DELAY); //获取任务通知
//返回值:任务通知值减少或者清零之前的值。
num++;
LCD_ShowxNum(166,111,NotifyValue-1,3,16,0); //显示当前任务通知值
//因为返回值获取的是任务通知值减少或者清零之前的值,第一个参数设置为 pdFASLE,表示计数值减减,那么退出函数之前的值就是还没有减减的值
//所以当前的任务通知值就是 NotifyValue-1;
LCD_Fill(6,131,233,313,lcd_discolor[num%14]); //刷屏
LED1=!LED1;
vTaskDelay(1000); //延时1s,也就是1000个时钟节拍
}
}
7. 任务通知模拟消息邮箱实验
任务通知也可用来向任务发送数据,但是相对于用队列发送消息,任务通知向任务发送消息会受到很多限制!
- 1. 只能发送 32 位的数据值。
- 2. 消息被保存为任务的任务通知值,而且一次只能保存一个任务通知值,相当于队列长度为 1。
因此说任务通知可以模拟一个轻量级的消息邮箱而不是一个轻量级的消息队列。任务通知值就是消息邮箱的值。
BaseType_t xTaskNotify(TaskHandle_t xTaskToNotify,
uint32_t ulValue,
eNotifyAction eAction)
参数:
xTaskToNotify: 任务句柄,指定任务通知是发送给哪个任务的。
ulValue: 任务通知值。
eAction: 任务通知更新的方法,eNotifyAction 是个枚举类型,在文件 task.h 中有如下定义:
typedef enum
{
eNoAction=0,
eSetBits, //更新指定的位 bit
eIncrement, //通知值加一
eSetValueWithOverwrite, //覆写的方式更新通知值
eSetValueWithoutOverwrite //不覆写通知值
}eNotifyAction;
此参数可以选择枚举类型中的任意一个,不同的应用环境其选择也不同。
返回值:
pdFAIL: 当参数 eAction 设置为 eSetValueWithoutOverwrite(不覆写) 的时候,如果任务通知值没有更新成功就返回 pdFAIL。
pdPASS: eAction 设置为其他选项的时候统一返回 pdPASS。
发送数据可以使用函数 xTaskNotify() 或者 xTaskNotifyFromISR(),函数的参数 eAction 设置 eSetValueWithOverwrite 或者 eSetValueWithoutOverwrite。如果参数 eAction 为 eSetValueWithOverwrite 的话不管接收任务的通知值是否已经被处理,这个通知值都会被更新。如果参数 eAction 为 eSetValueWithoutOverwrite 的话上一个通知值还没有被处理,那么新的任务通知值就不会被更新。如果要读取任务通知值的话就使用函数 xTaskNotifyWait()。
BaseType_t xTaskNotifyWait(uint32_t ulBitsToClearOnEntry,
uint32_t ulBitsToClearOnExit,
uint32_t* pulNotificationValue,
TickType_t xTicksToWait);
参数:
ulBitsToClearOnEntry: 当没有接收到任务通知的时候将任务通知值与此参数的取反值进行按位与运算,当此参数为 0xffffffff 或者 ULONG_MAX 的时候就会将任务通知值清零。
ulBitsToClearOnExit: 如果接收到了任务通知,在做完相应的处理退出函数之前将任务通知值与此参数的取反值进行按位与运算,当此参数为 0xffffffff 或者 ULONG_MAX 的时候就会将任务通知值清零。
pulNotificationValue: 此参数用来保存任务通知值。
xTickToWait: 阻塞时间。
返回值:
pdTRUE: 获取到了任务通知。
pdFALSE: 任务通知获取失败。
本实验设计三个任务:start_task、task1_task、Keyprocess_task 这三个任务的任务功能如下:
start_task:用来创建其他 2 个任务。
task1_task:读取按键的键值,然后将按键值作为任务通知发送给任务 Keyprocess_task。
Keyprocess_task :按键处理任务,读取任务通知值,根据不同的通知值做相应的处理。
实验需要三个按键 KEY_UP、KEY2 和 KEY0,不同的按键对应不同的按键值,任务 task1_task() 会将这些值作为任务通知发送给任务 Keyprocess_task 。FreeRTOS_队列-CSDN博客
7.1 实验程序
7.1.1 main.c
#include "stm32f4xx.h"
#include "FreeRTOS.h" //这里注意必须先引用FreeRTOS的头文件,然后再引用task.h
#include "task.h" //存在一个先后的关系
#include "LED.h"
#include "LCD.h"
#include "Key.h"
#include "usart.h"
#include "delay.h"
#include "string.h"
#include "beep.h"
#include "malloc.h"
#include "queue.h"
#include "limits.h"
//任务优先级
#define START_TASK_PRIO 1 //用于创建其他两个任务
//任务堆栈大小
#define START_STK_SIZE 256
//任务句柄
TaskHandle_t StartTask_Handler;
//任务函数
void start_task(void *pvParameters);
//任务优先级
#define TASK1_TASK_PRIO 2 //读取按键的键值,将键值发送到队列Key_Queue中,检查队列的剩余容量;相当于写队列
//任务堆栈大小
#define TASK1_STK_SIZE 256
//任务句柄
TaskHandle_t Task1Task_Handler;
//任务函数
void task1_task(void *pvParameters);
//任务优先级
#define KEYPROCESS_TASK_PRIO 3 //相当于读队列,读取队列Key_Queue中的消息,根据不同的消息值做出相应的处理
//任务堆栈大小
#define KEYPROCESS_STK_SIZE 256
//任务句柄
TaskHandle_t Keyprocess_Handler;
//任务函数
void Keyprocess_task(void *pvParameters);
//LCD刷屏时使用的颜色
int lcd_discolor[14]={ WHITE, BLACK, BLUE, BRED,
GRED, GBLUE, RED, MAGENTA,
GREEN, CYAN, YELLOW, BROWN,
BRRED, GRAY};
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4); //设置中断优先级分组为4
delay_init(168);
uart_init(115200);
LED_Init();
KEY_Init();
BEEP_Init();
LCD_Init();
my_mem_init(SRAMIN); //初始化内部内存池
POINT_COLOR = RED;
LCD_ShowString(10,10,200,16,16,"ATK STM32F407");
LCD_ShowString(10,30,200,16,16,"FreeRTOS Example");
LCD_ShowString(10,50,200,16,16,"Task Notify Maibox");
LCD_ShowString(10,70,220,16,16,"KEY_UP:LED1 KEY2:BEEP");
LCD_ShowString(10,90,200,16,16,"KEY0:Refresh LCD");
POINT_COLOR = BLACK;
LCD_DrawRectangle(5,125,234,314); //画矩形
//创建开始任务
xTaskCreate((TaskFunction_t )start_task, //任务函数
(const char* )"start_task", //任务名称
(uint16_t )START_STK_SIZE, //任务堆栈大小
(void* )NULL, //传递给任务函数的参数
(UBaseType_t )START_TASK_PRIO, //任务优先级
(TaskHandle_t* )&StartTask_Handler);//任务句柄
vTaskStartScheduler(); //开启任务调度
}
//开始任务的任务函数
void start_task(void *pvParameters)
{
taskENTER_CRITICAL();
//通过任务通知实现消息邮箱是不用创建消息队列的,所需的结构体均存储在 TCB 任务控制块中。
//创建Task1任务
xTaskCreate((TaskFunction_t )task1_task, //任务函数
(const char* )"task1_task", //任务名称
(uint16_t )TASK1_STK_SIZE, //任务堆栈大小
(void* )NULL, //传递给任务函数的参数
(UBaseType_t )TASK1_TASK_PRIO, //任务优先级
(TaskHandle_t* )&Task1Task_Handler);//任务句柄
//创建Task2任务--按键处理任务
xTaskCreate((TaskFunction_t )Keyprocess_task, //任务函数
(const char* )"Keyprocess_task", //任务名称
(uint16_t )KEYPROCESS_STK_SIZE, //任务堆栈大小
(void* )NULL, //传递给任务函数的参数
(UBaseType_t )KEYPROCESS_TASK_PRIO, //任务优先级
(TaskHandle_t* )&Keyprocess_Handler); //任务句柄
vTaskDelete(StartTask_Handler); //删除开始任务
taskEXIT_CRITICAL(); //退出临界区
}
//task1任务函数
//将键值通过任务通知的方式发送给Keyprocess_Handler,采用覆写的方式发送
void task1_task(void *pvParameters)
{
u8 key,i=0;
BaseType_t err;
while(1)
{
key=KEY_Scan(0); //按键扫描 KEY0返回1,KEY1返回2,KEY2返回3,KEY_UP返回4,无按键按下返回0
if((Keyprocess_Handler!=NULL)&&(key)) //Keyprocess_Handler不为空,意味着有任务在等待信号量,接收任务需要等待任务通知,并且有按键被按下
{
err=xTaskNotify((TaskHandle_t )Keyprocess_Handler, //接收任务通知的任务句柄
(uint32_t )key, //任务通知值
(eNotifyAction )eSetValueWithOverwrite); //覆写的方式发送任务通知
//pdFAIL:当参数eAction设置eSetValueWithoutOverwrite(不覆写)的时候,不覆盖意味着原本的任务通知值还没有被处理,那么新的任务通知值就要悬挂等待,如果任务通知值没有被更新成功就返回 pdFAIL
//pdFALSE:eAction设置为其他选项的时候统一返回 pdPASS。
if(err==pdFAIL) //覆写的情况下统一返回 pdPASS 表示任务通知发送成功!
{
printf("任务通知发送失败!\r\n");
}
}
i++;
if(i==50)
{
i=0;
LED0=!LED0;
}
vTaskDelay(10); //延时10ms,也就是10个时钟节拍
}
}
//KeyProcess_task函数
//接收按键的键值,根据不同的任务通知做出对应的处理
void Keyprocess_task(void *pvParameters)
{
u8 num;
uint32_t NotifyValue;
BaseType_t err;
while(1)
{
//获取任务通知值函数
err = xTaskNotifyWait((uint32_t )0x00, //当没有接收到任务通知的时候将任务通知值与此参数的取反值进行按位与运算,当此参数为 0xffffffff 或者 ULONG_MAX 的时候就会将任务通知值清零。
//进入函数的时候不清除任务的bit
(uint32_t )ULONG_MAX, //如果接收到了任务通知,在做完相应的处理退出函数之前将任务通知值与此参数的取反值进行按位与运算,当此参数为 0xffffffff 或者 ULONG_MAX 的时候就会将任务通知值清零。
//退出函数的时候清除所有的bit
(uint32_t* )&NotifyValue, //此参数用来保存任务通知值
(TickType_t )portMAX_DELAY); //阻塞时间
//返回值:pdTRUE:获取到了任务通知
// pdFALSE:任务通知获取失败
if(err==pdTRUE) //返回值为 pdTRUE,表示获取任务通知成功
{
switch((u8)NotifyValue) //此参数 NotifyValue 用来保存任务值
{
case WKUP_PRES: //KEY_UP控制LED1
LED1=!LED1;
break;
case KEY2_PRES: //KEY2控制蜂鸣器
BEEP=!BEEP;
break;
case KEY0_PRES: //KEY0刷新LCD背景
num++;
LCD_Fill(126,111,233,313,lcd_discolor[num%14]);
break;
}
}
}
}
8. 任务通知模拟事件标志组实验
事件标志组其实就是一组二进制事件标志(位),每个事件标志位的具体意义由应用程序编写者来决定。当一个任务等待事件标志组中的某几个标志(位)的时候可以进入阻塞态,当任务因为等待事件标志(位)而进入阻塞态以后这个任务就不会消耗 CPU。
当任务通知用作事件标志组的话任务通知值就相当于事件组,这个时候任务通知值的每个 bit 用作事件标志(位)。函数 xTaskNotifyWait() 代替事件标志组中的 API 函数 xEventGroupBits()。函数 xTaskNotify() 和 xTaskNotifyFromISR()(函数的参数 eAction 为 eSetBits)代替事件标志组中的 API 函数 xEventGroupSetBits() 和 xEventGroupSetBitsFromISR()。
8.1 实验程序
本实验在 —— 事件标志组实验的基础上修改而来;FreeRTOS_事件标志组-CSDN博客
8.1.1 main.c
#include "stm32f4xx.h"
#include "FreeRTOS.h" //这里注意必须先引用FreeRTOS的头文件,然后再引用task.h
#include "task.h" //存在一个先后的关系
#include "LED.h"
#include "LCD.h"
#include "Key.h"
#include "usart.h"
#include "delay.h"
#include "string.h"
#include "beep.h"
#include "malloc.h"
#include "timer.h"
#include "exti.h"
#include "limits.h"
//任务优先级
#define START_TASK_PRIO 1 //用于创建其他任务
//任务堆栈大小
#define START_STK_SIZE 256
//任务句柄
TaskHandle_t StartTask_Handler;
//任务函数
void start_task(void *pvParameters);
//任务优先级
#define EVENTSETBIT_TASK_PRIO 2 //读取按键值,根据不同的按键值将事件标志组中相应的事件位置 1 ,用来模拟事件的发生。
//任务堆栈大小
#define EVENTSETBIT_STK_SIZE 256
//任务句柄
TaskHandle_t EventSetBit_Handler;
//任务函数
void eventsetbit_task(void *pvParameters);
//任务优先级
#define EVENTGROUP_TASK_PRIO 3 //同时等待事件标志组中的多个事件位,当这些事件位都置 1 的话就执行相应的处理,例程中的处理是刷新 LCD 指定区域的背景色。
//任务堆栈大小
#define EVENTGROUP_STK_SIZE 256
//任务句柄
TaskHandle_t EventGroupTask_Handler;
//任务函数
void eventgroup_task(void *pvParameters);
//事件标志组是个类似于数组的形式,数组是以下角标的形式进行定义的
//EVENTBIT_0 操作数组下角标为 0 的位,1左移0位,也就是把数组第一位设置为1
//EVENTBIT_1 操作数组下角标为 1 的位,1左移1位,也就是把数组第二位设置为1
//EVENTBIT_2 操作数组下角标为 2 的位,1左移2位,也就是把数组第三位设置为1
#define EVENTBIT_0 (1<<0) //事件位
#define EVENTBIT_1 (1<<1)
#define EVENTBIT_2 (1<<2)
//LCD刷屏时使用的颜色
int lcd_discolor[14]={ WHITE, BLACK, BLUE, BRED,
GRED, GBLUE, RED, MAGENTA,
GREEN, CYAN, YELLOW,BROWN,
BRRED, GRAY };
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
delay_init(168);
uart_init(115200);
LED_Init();
KEY_Init();
EXTIX_Init(); //初始化外部中断
LCD_Init();
my_mem_init(SRAMIN); //初始化内部内存池
POINT_COLOR=RED;
LCD_ShowString(30,10,200,16,16,"ATK STM32F407");
LCD_ShowString(30,30,200,16,16,"FreeRTOS Example");
LCD_ShowString(30,50,200,16,16,"Task Notify Event Group");
LCD_ShowString(30,70,200,16,16,"ATOM@ALIENTEK");
LCD_ShowString(30,90,200,16,16,"2023/11/06");
POINT_COLOR = BLACK;
LCD_DrawRectangle(5,130,234,314); //画一个矩形
POINT_COLOR = BLUE;
LCD_ShowString(30,110,220,16,16,"Event Group Value:0");
//创建开始任务
xTaskCreate((TaskFunction_t)start_task, //任务函数
(const char* )"start_task", //任务名称
(uint16_t )START_STK_SIZE, //任务堆栈大小
(void* )NULL, //传递给任务函数的参数
(UBaseType_t )START_TASK_PRIO, //任务优先级
(TaskHandle_t* )&StartTask_Handler);//任务句柄
vTaskStartScheduler(); //开启任务调度
}
//开始任务任务函数
void start_task(void *pvParameters)
{
taskENTER_CRITICAL();
//通过任务通知模拟事件标志组是不需要创建事件标志组的;任务通知所需的结构体保存在任务控制块TCB中;
//创建设置事件位的任务
xTaskCreate((TaskFunction_t)eventsetbit_task, //任务函数
(const char* )"eventsetbit_task", //任务名称
(uint16_t )EVENTSETBIT_STK_SIZE, //任务堆栈大小
(void* )NULL, //传递给任务函数的参数
(UBaseType_t )EVENTSETBIT_TASK_PRIO, //任务优先级
(TaskHandle_t* )&EventSetBit_Handler);//任务句柄
//创建事件标志组处理任务
xTaskCreate((TaskFunction_t)eventgroup_task, //任务函数
(const char* )"eventgroup_task", //任务名称
(uint16_t )EVENTGROUP_STK_SIZE, //任务堆栈大小
(void* )NULL, //传递给任务函数的参数
(UBaseType_t )EVENTGROUP_TASK_PRIO, //任务优先级
(TaskHandle_t* )&EventGroupTask_Handler);//任务句柄
vTaskDelete(StartTask_Handler); //删除开始任务
taskEXIT_CRITICAL();
}
//设置事件位的任务
//读取按键值,根据不同的按键值将事件标志组中相应的事件位置 1 ,用来模拟事件的发生。
void eventsetbit_task(void *pvParameters)
{
u8 key,i;
while(1)
{
if(EventGroupTask_Handler!=NULL) //如果任务句柄不为空,表示有任务处于等待任务通知的状态;如果给接收任务发送任务通知,那么该任务将继续执行!
{
key = KEY_Scan(0);
switch(key)
{
case KEY1_PRES:
//发送任务通知函数
xTaskNotify((TaskHandle_t )EventGroupTask_Handler, //接收任务通知的任务句柄
(uint32_t )EVENTBIT_1, //要更新的位bit
(eNotifyAction )eSetBits); //更新方法为更新指定的位bit
break;
case KEY2_PRES:
//发送任务通知函数
xTaskNotify((TaskHandle_t )EventGroupTask_Handler, //接收任务通知的任务句柄
(uint32_t )EVENTBIT_2, //要更新的位bit
(eNotifyAction )eSetBits); //更新方法为更新指定的位bit
break;
}
}
i++;
if(i==50)
{
i=0;
LED0=!LED0;
}
vTaskDelay(10); //延时10ms,也就是10个时钟节拍
}
}
//事件标志组处理任务
//同时等待事件标志组中的多个事件位,当这些事件位都置 1 的话就执行相应的处理,例程中的处理是刷新 LCD 指定区域的背景色。
void eventgroup_task(void *pvParameters)
{
u8 num=0,eventvalue;
static u8 event0flag,event1flag,event2flag;
uint32_t NotifyValue;
BaseType_t err;
while(1)
{
//获取任务通知值
err=xTaskNotifyWait((uint32_t )0x00, //进入函数的时候不清除任务bit
(uint32_t )ULONG_MAX, //退出函数的时候清除所有的bit
(uint32_t* )&NotifyValue, //保存任务通知值
(TickType_t )portMAX_DELAY); //设置阻塞时间为 portMAX_DELAY,只要获取不到任务通知值就一直等待
if(err==pdPASS) //任务通知获取成功
{
if((NotifyValue&EVENTBIT_0)!=0) //NotifyValue表示获取的任务通知值,EVENTBIT_0表示事件标志组的第0位设置为1
//想要 if 判断语句成立,那么就必须保证接收到的任务通知值 NotifyValue 第 0 位为 1,也就表示位 0 对应的事件成立!
{
event0flag=1;
}
else if((NotifyValue&EVENTBIT_1)!=0) //事件1发生
{
event1flag=1;
}
else if((NotifyValue&EVENTBIT_2)!=0) //事件2发生
{
event2flag=1;
}
eventvalue=event0flag|(event1flag<<1)|(event2flag<<2); //模拟事件标志组值
printf("任务通知值为:%d\r\n",eventvalue);
LCD_ShowxNum(174,110,eventvalue,1,16,0); //在LCD上显示当前的事件值
if((event0flag==1)&&(event1flag==1)&&(event2flag==1)) //三个事件同时发生
{
num++;
LED1=!LED1;
LCD_Fill(6,131,233,313,lcd_discolor[num%14]);
event0flag=0; //标志清零
event1flag=0;
event2flag=0;
}
}
else
{
printf("任务通过获取失败\r\n");
}
}
}
8.1.2 exit.c
#include "exti.h"
#include "delay.h"
#include "key.h"
#include "FreeRTOS.h"
#include "task.h"
//外部中断初始化程序
//初始化PE2~4,PA0为中断输入.
void EXTIX_Init(void)
{
NVIC_InitTypeDef NVIC_InitStructure;
EXTI_InitTypeDef EXTI_InitStructure;
KEY_Init(); //按键对应的IO口初始化
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE);//使能SYSCFG时钟
SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOE, EXTI_PinSource4);//PE4 连接到中断线4
/* 配置EXTI_Line4 */
EXTI_InitStructure.EXTI_Line = EXTI_Line4;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; //中断事件
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; //下降沿触发
EXTI_InitStructure.EXTI_LineCmd = ENABLE; //中断线使能
EXTI_Init(&EXTI_InitStructure); //配置
NVIC_InitStructure.NVIC_IRQChannel = EXTI4_IRQn; //外部中断4
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x06;//抢占优先级6
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x00; //子优先级0
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能外部中断通道
NVIC_Init(&NVIC_InitStructure); //配置
}
#define EVENTBIT_0 (1<<0)
//任务句柄
extern TaskHandle_t EventGroupTask_Handler;
//外部中断4服务程序
void EXTI4_IRQHandler(void)
{
BaseType_t xHigherPriorityTaskWoken; //定义变量标记是否进行任务切换
delay_xms(20); //消抖
if(KEY0==0)
{
xTaskNotifyFromISR((TaskHandle_t )EventGroupTask_Handler, //发送任务通知,指定任务通知是发送给哪个任务的。
(uint32_t )EVENTBIT_0, //任务通知值---要更新的位bit
(eNotifyAction )eSetBits, //任务通知更新的方法---更新指定的bit
(BaseType_t* )xHigherPriorityTaskWoken); //标记是否进行任务切换
/*如果函数xTaskResumeFromISR()返回值为pdTRUE,那么说明要恢复的这个
任务的任务优先级等于或者高于正在运行的任务(被中断打断的任务),所以在
退出中断的时候一定要进行上下文切换!*/
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
EXTI_ClearITPendingBit(EXTI_Line4);//清除LINE4上的中断标志位
}