参考教程:【正点原子】手把手教你学FreeRTOS实时系统_哔哩哔哩_bilibili
一、任务挂起和恢复的API函数
1、概述
(1)任务挂起和恢复的本质就是调用FreeRTOS的API函数。
API函数 | 描述 |
vTaskSuspend() | 挂起任务 |
vTaskResume() | 恢复被挂起的任务 |
xTaskResumeFromISR() | 在中断中恢复被挂起的任务 |
(2)挂起任务类似暂停任务,它可重新恢复,但删除任务则是将任务永远删除,除非是重新创建任务(任务也会重头开始执行),否则任务将永远消失。
(3)带“FromISR”后缀的函数是在中断函数中专用的API函数。
2、任务挂起函数
(1)任务挂起函数的接口定义:
void vTaskSuspend
(
TaskHandle_t xTaskToSuspend /* 待挂起任务的任务句柄 */
)
(2)此函数用于挂起任务,使用时需将宏INCLUDE_vTaskSuspend配置为1。
(3)无论优先级如何,被挂起的任务都将不再被执行,直到任务被恢复。
(4)当传入的参数为NULL,则代表挂起任务自身,即当前正在运行的任务。
3、任务恢复函数(任务中恢复)
(1)任务恢复函数(任务中恢复)的接口定义:
void vTaskResume
(
TaskHandle_t xTaskToResume /* 待恢复任务的任务句柄 */
)
(2)使用该函数时,宏INCLUDE_vTaskSuspend必须定义为1。
(3)任务无论被vTaskSuspend函数挂起多少次,只需在其它任务中调用vTakResume函数恢复一次,被恢复的任务就会进入就绪态。
4、任务恢复函数(中断中恢复)
(1)任务恢复函数(中断中恢复)的接口定义:
BaseType_t xTaskResumeFromISR
(
TaskHandle_t xTaskToResume /* 待恢复任务的任务句柄 */
)
返回值 | 描述 |
pdTRUE | 任务恢复后需要进行任务切换 |
pdFALSE | 任务恢复后不需要进行任务切换 |
(2)使用该函数时,宏INCLUDE_vTaskSuspend和INCLUDE_xTaskResumeFromISR必须定义为1。
(3)该函数专用于中断服务函数中,用于解挂被挂起任务。
(4)中断服务程序中要调用freeRTOS的API函数则中断优先级不能高于FreeRTOS所管理的最高优先级,建议将所有优先级位指定为抢占优先级位,不留下任何优先级位作为子优先级位。
二、任务挂起和恢复实验
1、原理图与实验目标
(1)原理图:
(2)实验目标:
①设计4个任务——start_task、task1、task2、task3:
[1]start_task:用于创建其它三个任务,然后删除自身。
[2]task1:实现LED1状态反转。
[3]task2:实现LED2状态反转。
[4]task3:按下按键1,若task1非挂起,则挂起task1,否则任务中恢复task1;按下按键2,若task2非挂起,则挂起task2。
②设计中断服务程序,对射式红外传感器检测到非透光物体(迅速拿开遮挡物),若task2挂起,则外部中断中恢复task2。
③预期实验现象:
[1]程序下载到板子上后,两个LED灯闪烁。
[2]按下一次按键1后,LED1停止闪烁,接着再按下按键1,LED1恢复闪烁。
[3]按下一次按键2后,LED2停止闪烁,接着再遮挡红外线(迅速拿开遮挡物),LED2恢复闪烁。
2、实验步骤
(1)将上一章中“任务创建和删除的动态方法实验”的工程文件夹复制一份,在拷贝版中进行实验。
(2)在FreeRTOSConfig.h文件中将宏INCLUDE_xTaskResumeFromISR配置为1。(另一个宏INCLUDE_vTaskSuspend已配置为1,无需新增)
#define INCLUDE_vTaskSuspend 1
(3)修改task3的函数实现,并增加两个全局变量用作记录task1和task2的挂起状态。
char task1_state = 0; //记录task1挂起状态(0为非挂起,1为挂起)
char task2_state = 0; //记录task2挂起状态(0为非挂起,1为挂起)
void task3(void)
{
uint8_t key = 0;
while(1)
{
key = Key_GetNum(); //读取按键键值
if(key == 1)
{
if(task1_state == 0) //若task1非挂起
{
vTaskSuspend(task1_handler); //挂起task1
task1_state = 1; //更改其状态为挂起
}
else if(task1_state == 1) //若task1挂起
{
vTaskResume(task1_handler); //恢复task1
task1_state = 0; //更改其状态为非挂起
}
}
if(key == 2)
{
if(task2_state == 0) //若task2非挂起
{
vTaskSuspend(task2_handler); //挂起task2
task2_state = 1; //更改其状态为挂起
}
}
vTaskDelay(10); //延时(自我阻塞)10ms
}
}
(4)在stm32教程中找到“对射式红外传感器计次”实验,复制其中的CountSensor.c文件和CountSensor.h文件,将其添加到本实验的项目中,如下图所示。
(5)校核CountSensor.c文件中的代码并作修改,需要声明task2的任务句柄和挂起状态为外部变量,更改NVIC中断优先级分组为分组4,然后更改中断的抢占优先级(尽可能大,否则可能会出现“卡死”现象),同时在外部中断函数中做逻辑判断,当进入中断后,如果task2的状态为挂起,则为其解挂,并根据解挂函数的返回值判断是否需要进行任务切换(由于有FreeRTOS相关的代码,故还需包含FreeRTOS.h和Task.h文件)。
#include "stm32f10x.h" // Device header
#include "FreeRTOS.h"
#include "Task.h"
uint16_t CountSensor_Count; //全局变量,用于计数
extern TaskHandle_t task2_handler; //声明外部变量,task2任务的句柄
extern char task2_state; //声明外部变量,task2任务的挂起状态
/**
* 函 数:计数传感器初始化
* 参 数:无
* 返 回 值:无
*/
void CountSensor_Init(void)
{
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //开启GPIOB的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); //开启AFIO的时钟,外部中断必须开启AFIO的时钟
/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure); //将PB14引脚初始化为上拉输入
/*AFIO选择中断引脚*/
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource14);//将外部中断的14号线映射到GPIOB,即选择PB14为外部中断引脚
/*EXTI初始化*/
EXTI_InitTypeDef EXTI_InitStructure; //定义结构体变量
EXTI_InitStructure.EXTI_Line = EXTI_Line14; //选择配置外部中断的14号线
EXTI_InitStructure.EXTI_LineCmd = ENABLE; //指定外部中断线使能
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; //指定外部中断线为中断模式
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; //指定外部中断线为下降沿触发
EXTI_Init(&EXTI_InitStructure); //将结构体变量交给EXTI_Init,配置EXTI外设
/*NVIC中断分组*/
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4); //配置NVIC为分组4
//即抢占优先级范围:0~15,响应优先级范围:0
//此分组配置在整个工程中仅需调用一次
//若有多个中断,可以把此代码放在main函数内,while循环之前
//若调用多次配置分组的代码,则后执行的配置会覆盖先执行的配置
/*NVIC配置*/
NVIC_InitTypeDef NVIC_InitStructure; //定义结构体变量
NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn; //选择配置NVIC的EXTI15_10线
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //指定NVIC线路使能
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 12; //指定NVIC线路的抢占优先级为12
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 12; //指定NVIC线路的响应优先级为12
NVIC_Init(&NVIC_InitStructure); //将结构体变量交给NVIC_Init,配置NVIC外设
}
/**
* 函 数:获取计数传感器的计数值
* 参 数:无
* 返 回 值:计数值,范围:0~65535
*/
uint16_t CountSensor_Get(void)
{
return CountSensor_Count;
}
/**
* 函 数:EXTI15_10外部中断函数
* 参 数:无
* 返 回 值:无
* 注意事项:此函数为中断函数,无需调用,中断触发后自动执行
* 函数名为预留的指定名称,可以从启动文件复制
* 请确保函数名正确,不能有任何差异,否则中断函数将不能进入
*/
void EXTI15_10_IRQHandler(void)
{
if (EXTI_GetITStatus(EXTI_Line14) == SET) //判断是否是外部中断14号线触发的中断
{
/*如果出现数据乱跳的现象,可再次判断引脚电平,以避免抖动*/
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_14) == 0)
{
CountSensor_Count ++; //计数值自增一次
if(task2_state == 1) //若task2挂起
{
BaseType_t xYieldRequired;
xYieldRequired = xTaskResumeFromISR(task2_handler); //恢复task2
if(xYieldRequired == pdTRUE)
portYIELD_FROM_ISR(xYieldRequired); //任务切换
task2_state = 0; //更改其状态为非挂起
}
}
EXTI_ClearITPendingBit(EXTI_Line14); //清除外部中断14号线的中断标志位
//中断标志位必须清除
//否则中断将连续不断地触发,导致主程序卡死
}
}
(6)修改main.c文件,在其中添加如下代码。
#include "stm32f10x.h" // Device header
#include "OLED.h"
#include "FreeRTOS.h"
#include "task.h"
#include "FreeRTOS_experiment.h"
#include "Key.h"
#include "LED.h"
#include "CountSensor.h"
int main(void)
{
/*模块初始化*/
CountSensor_Init(); //对射式红外传感器初始化
Key_Init(); //Key初始化
LED_Init(); //LED初始化
FreeRTOS_Test();
while (1)
{
}
}
(7)程序完善好后点击“编译”,然后将程序下载到开发板上。
3、程序执行流程
(1)main函数全流程:
①初始化对射式红外传感器模块、按键模块、LED模块。
②调用FreeRTOS_Test函数。
(2)测试函数全流程:
①创建任务start_task。
②开启任务调度器。
(3)多任务调度执行阶段(发生在开启任务调度器以后):
①start_task任务函数首先进入临界区,在临界区中start_task任务不会被其它任务打断(否则创建task1后,由于task1的优先级较高,start_task任务会被打断),接着start_task任务依次创建任务task1、task2、task3,然后删除自身,接着退出临界区,让出CPU资源。
②三个任务中task3的优先级最高,故优先执行task3,假设先不按下任何按键,task3未接收到任何键码,不做出其它行为,直接自我阻塞10ms,此时task2的优先级最高(除开已阻塞的task3),故执行task2,LED2的状态翻转后,task2自我阻塞500ms,此时未阻塞的就绪任务仅剩task1,故执行task1,LED1的状态翻转后,task1自我阻塞500ms。待task3的自我阻塞时间结束,task3重新抢占CPU,task3自我阻塞后,待task2的自我阻塞时间结束,task2重新抢占CPU,待task1的自我阻塞时间结束,task1重新抢占CPU,以此往复,实现双LED灯闪烁。
③在某次执行task3时,若task1和task2均未挂起,按下按键1,task1将会被挂起。(按下按键1的过程中因为按键消抖使用了vTaskDelay函数,task3会进行短暂的自我阻塞,图中未示出,不过由于task3的优先级最高,阻塞时间结束后task3可重新抢占CPU)
④在某次执行task3时,若task1挂起、task2未挂起,按下按键1,task1将会被恢复。(按下按键1的过程中因为按键消抖使用了vTaskDelay函数,task3会进行短暂的自我阻塞,图中未示出,不过由于task3的优先级最高,阻塞时间结束后task3可重新抢占CPU)
⑤在某次执行task3时,若task1和task2均未挂起,按下按键2,task2将会被挂起。(按下按键2的过程中因为按键消抖使用了vTaskDelay函数,task3会进行短暂的自我阻塞,图中未示出,不过由于task3的优先级最高,阻塞时间结束后task3可重新抢占CPU)
⑥在用非透光物品遮挡红外线时(迅速拿开遮挡物),若task2挂起、task1未挂起,且task1正在运行,则task1将会被暂时打断,CPU进入外部中断服务程序,task2将会被恢复。
三、任务挂起和恢复详细过程(源码剖析)
1、vTaskSuspend函数
void vTaskSuspend( TaskHandle_t xTaskToSuspend )
{
TCB_t * pxTCB;
traceENTER_vTaskSuspend( xTaskToSuspend );
taskENTER_CRITICAL();
{
pxTCB = prvGetTCBFromHandle( xTaskToSuspend ); //获取任务控制块,为NULL则代表挂起自身
traceTASK_SUSPEND( pxTCB );
//将要挂起的任务从相应的状态列表中移除
if( uxListRemove( &( pxTCB->xStateListItem ) ) == ( UBaseType_t ) 0 )
{
taskRESET_READY_PRIORITY( pxTCB->uxPriority );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
//将要挂起的任务从相应的事件列表中移除
if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL )
{
( void ) uxListRemove( &( pxTCB->xEventListItem ) );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
vListInsertEnd( &xSuspendedTaskList, &( pxTCB->xStateListItem ) ); //将状态列表项尾插到挂起列表
#if ( configUSE_TASK_NOTIFICATIONS == 1 ) //被挂起的任务不需要再等待任务通知
{
BaseType_t x;
for( x = ( BaseType_t ) 0; x < ( BaseType_t ) configTASK_NOTIFICATION_ARRAY_ENTRIES; x++ )
{
if( pxTCB->ucNotifyState[ x ] == taskWAITING_NOTIFICATION )
{
pxTCB->ucNotifyState[ x ] = taskNOT_WAITING_NOTIFICATION;
}
}
}
#endif /* if ( configUSE_TASK_NOTIFICATIONS == 1 ) */
#if ( configNUMBER_OF_CORES > 1 )
{
if( xSchedulerRunning != pdFALSE ) //判断任务调度器是否正在运行
{
prvResetNextTaskUnblockTime(); //更新下一个任务的阻塞超时时间,防止被挂起的任务是下一个阻塞结束的任务
if( taskTASK_IS_RUNNING( pxTCB ) == pdTRUE )
{
if(pxTCB->xTaskRunState == ( BaseType_t ) portGET_CORE_ID())
{
configASSERT( uxSchedulerSuspended == 0 );
vTaskYieldWithinAPI();
}
else
{
prvYieldCore( pxTCB->xTaskRunState );
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* #if ( configNUMBER_OF_CORES > 1 ) */
}
taskEXIT_CRITICAL();
#if ( configNUMBER_OF_CORES == 1 )
{
UBaseType_t uxCurrentListLength;
if( xSchedulerRunning != pdFALSE ) //判断任务调度器是否正在运行
{
taskENTER_CRITICAL();
{
prvResetNextTaskUnblockTime(); //更新下一个任务的阻塞超时时间,防止被挂起的任务是下一个阻塞结束的任务
}
taskEXIT_CRITICAL();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
if( pxTCB == pxCurrentTCB ) //判断要挂起的任务是不是调用挂起函数的任务
{
if( xSchedulerRunning != pdFALSE ) //判断任务调度器是否正在运行
{
configASSERT( uxSchedulerSuspended == 0 );
portYIELD_WITHIN_API(); //执行一次任务切换
}
else
{
//获取当前被挂起的任务总数
uxCurrentListLength = listCURRENT_LIST_LENGTH( &xSuspendedTaskList );
if( uxCurrentListLength == uxCurrentNumberOfTasks )
{
pxCurrentTCB = NULL; //如果所有任务都被挂起,当前没有运行的任务(空闲任务除外)
}
else
{
vTaskSwitchContext(); //寻找优先级最高的任务(就绪态)做下一时刻要执行的任务
}
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* #if ( configNUMBER_OF_CORES == 1 ) */
traceRETURN_vTaskSuspend();
}
2、vTaskResume函数
void vTaskResume( TaskHandle_t xTaskToResume )
{
TCB_t * const pxTCB = xTaskToResume; //获取需要恢复的任务的句柄
traceENTER_vTaskResume( xTaskToResume );
configASSERT( xTaskToResume );
#if ( configNUMBER_OF_CORES == 1 )
//需要恢复的任务不能是正在执行的任务,更不能是调用恢复函数的任务
if( ( pxTCB != pxCurrentTCB ) && ( pxTCB != NULL ) )
#else
if( pxTCB != NULL )
#endif
{
taskENTER_CRITICAL(); //进入临界区
{
if( prvTaskIsTaskSuspended( pxTCB ) != pdFALSE ) //判断任务是否在挂起列表中
{
traceTASK_RESUME( pxTCB );
(void) uxListRemove( &( pxTCB->xStateListItem ) ); //移除状态列表项
prvAddTaskToReadyList( pxTCB ); //将任务添加进就绪列表中
//若恢复的任务优先级较高,需要执行一次任务切换
taskYIELD_ANY_CORE_IF_USING_PREEMPTION( pxTCB );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
taskEXIT_CRITICAL(); //退出临界区
}
else
{
mtCOVERAGE_TEST_MARKER();
}
traceRETURN_vTaskResume();
}
3、xTaskResumeFromISR函数
BaseType_t xTaskResumeFromISR( TaskHandle_t xTaskToResume )
{
BaseType_t xYieldRequired = pdFALSE; //返回值初始化
TCB_t * const pxTCB = xTaskToResume; //获取需要恢复的任务的句柄
UBaseType_t uxSavedInterruptStatus; //用于保存中断状态
traceENTER_xTaskResumeFromISR( xTaskToResume );
configASSERT( xTaskToResume );
//检测调用FreeRTOS的API函数的中断优先级是否在管理范围内以及是否全部设置为抢占式优先级位
portASSERT_IF_INTERRUPT_PRIORITY_INVALID();
uxSavedInterruptStatus = taskENTER_CRITICAL_FROM_ISR(); //保存当前中断的状态
{
if( prvTaskIsTaskSuspended(pxTCB) != pdFALSE ) //判断任务是否在挂起列表中
{
traceTASK_RESUME_FROM_ISR( pxTCB );
if( uxSchedulerSuspended == ( UBaseType_t ) 0U ) //判断调度器是否挂起
{
#if ( configNUMBER_OF_CORES == 1 )
{
if( pxTCB->uxPriority > pxCurrentTCB->uxPriority ) //判断要挂起的任务优先级是否高于当前运行的任务
{
xYieldRequired = pdTRUE; //标记置为pdTRUE,后续需要根据该标志判断是否执行一次任务切换
xYieldPendings[ 0 ] = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* #if ( configNUMBER_OF_CORES == 1 ) */
( void ) uxListRemove( &( pxTCB->xStateListItem ) ); //将任务移出挂起列表
prvAddTaskToReadyList( pxTCB ); //将任务添加进就绪列表
}
else
{
//将任务插入等待就绪列表,待任务调度器挂起后再处理
vListInsertEnd( &( xPendingReadyList ), &( pxTCB->xEventListItem ) );
}
#if ( ( configNUMBER_OF_CORES > 1 ) && ( configUSE_PREEMPTION == 1 ) )
{
prvYieldForTask( pxTCB );
if( xYieldPendings[ portGET_CORE_ID() ] != pdFALSE )
{
xYieldRequired = pdTRUE;
}
}
#endif /* #if ( ( configNUMBER_OF_CORES > 1 ) && ( configUSE_PREEMPTION == 1 ) ) */
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
//将中断状态重新设置回去
taskEXIT_CRITICAL_FROM_ISR( uxSavedInterruptStatus );
traceRETURN_xTaskResumeFromISR( xYieldRequired );
return xYieldRequired; //返回标志,该标志用于判断是否需要任务切换
}