Skip to content

多任务基本运行机制:分时复用

每个任务有一个栈空间,有一个任务控制块空间、还有优先级(数字越小,优先级越低)

任务状态

运行状态、非运行状态(挂起态、就绪态、阻塞态) image.png

1.就绪状态(Ready)

  • 处于就绪态的任务是指那些能够运行( 没有被阻塞和挂起)但是当前没有运行的任务,因为同优先级或更高优先级的任务正在运行。
  • 任务被创建之后就处于就绪状态。根据抢占式任务调度的特点,任务调度的结果有以下几种情况:
    • 如果当前没有其他处于运行状态的任务,就绪的任务就进入运行状态
    • 如果就绪任务的优先级高于或等于当前运行任务的优先级就绪的任务就进入运行状态
    • 如果就绪任务的优先级低于当前运行任务的优先级,就绪的任务继续处于就绪状态

2.运行状态(Running)

  • 占有CPU并运行的任务就处于运行状态(拥有CPU使用权)。处于运行状态的任务在空闲的时候应该让出CPU的使用权。
  • 处于运行状态的任务有两种方法主动让出CPU的使用权
    • 执行函数VTaskSuspend()进入挂起状态
    • 执行阻塞类函数进入阻塞状态运行的任务交出CPU使用权后,任务调度器可以使其他就绪状态的任务进入运行状态。

3.阻塞状态(Blocked)

  • 阻塞状态就是任务暂时让出CPU的使用权,处于一种等待的状态。运行状态的任务可以调用两类函数进入阻塞状态。
  • 由于等待信号量,消息队列,事件标志组等而处于的状态被称之为阻塞态,另外任务调用延迟函数也会处于阻塞态。
    • 时间延迟函数,如vTaskDelay()
    • 用于进程间通讯的事件请求函数,如请求信号量的函数xSemaphoreTake()

4.挂起状态(Suspended)

  • 类似阻塞态,通过调用函数 vTaskSuspend( 对指定任务进行挂起,挂起后这个任务将不被执行,只有调用函数 xTaskResume() 才可以 将这个 任务从挂起态恢复
  • 挂起状态的任务就是暂停的任务,挂起状态的任务不参与调度器的调度。其他3种状态的任务都可以通过调用函数vTaskSuspend()进入挂起状态。
  • 挂起后的状态不能自动退出挂起状态,需要在其他任务里调用VTaskResume()函数使一个挂起的任务变为就绪状态

任务调度:抢占式和合作式

任务优先级

在FreeRTOS中,每个任务都必须设置一个优先级 总的优先级个数由宏configMAX PRIORITIES定义,缺省值是56 优先级数字越低,优先级别越低,所以最低优先级是0,最高优先级是(configMAX_PRIORITIES-1) 在创建任务时就必须为任务设置初始的优先级,在任务运行起来后还可以修改优先级别 多个任务可以具有相同的优先级

空闲任务

osKernelStart()启动FreeRTOS的任务调度器时,会自动创建一个空闲任务(Idle task),空闲任务的优先级别为0。 与空闲任务相关的几个主要配置参数是: configUSE_TICK_HOOK,是否使用空闲函数的钩子函数,若配置为1,则可以利用空闲任务的钩子函数,系统空闲时做一些处理 configIDLE_SHOULD_YIELD,空闲任务是否对同优先级的用户任务主动让出CPU使用权,这会影响任务调度结果 configUSE_TICKLESS_IDLE,是否在空闲任务时关闭基础时钟,若设置为1,可实现系统的低功耗

任务调度

2.2.1任务调度方法概述 FreeRTOS有两种任务调度算法,基于优先级的抢占式(pre-emptive)调度算法和合作式调度(co-operative)算法,其中抢占式调度算法又可以使用时间片或不使用时间片。 image.png

2.2.2使用时间片的抢占式调度方法 FreeRTOS基础时钟的一个定时周期称为一个时间片(time sice),默认值为1ms。当使用时间片时,在基础时钟的每次中断里会要求进行一次上下文切换(context switching),函数xPortSysTickHandler()就是SysTick定时中断的处理函数

2.2.3不使用时间片的抢占式调度方法 使用时间片的抢占式调度方法在每个SysTick中断里都进行一次上下文切换请求,从而进行任务调度。而不使用时间片的抢占式调度算法只在以下情况下才进行任务调度: 有更高级别的任务进入就绪状态时; 运行状态的任务进入阻塞状态或挂起状态时。 所以,不使用时间片时,进行上下文切换的频率比使用时间片时低,从而可降低CPU的负担。但是,对于同优先级的任务可能会出现占用CPU时间相差很大的情况。

2.2.4合作式任务调度方法 使用合作式任务调度方法时,FreeRTOS不主动进行上下文切换,而是运行状态的任务进入阻塞状态,或显式地调用taskYIELD()函数让出CPU使用权时才进行上下文切换。 任务不会发生抢占,所以也不使用时间片。 函数taskYIELD)的作用就是主动申请进行一次上下文切换。

任务管理的相关函数

  • 创建任务函数,以动态的方式分配内存
c
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode,  
                   const char * const pcName,    /*lint !e971 Unqualified char types are allowed for strings and single characters only. */  
                   const configSTACK_DEPTH_TYPE usStackDepth,  
                   void * const pvParameters,  
                   UBaseType_t uxPriority,  
                   TaskHandle_t * const pxCreatedTask )  
{
  • 创建任务函数,以静态的方式分配内存
c
TaskHandle_t xTaskCreateStatic( TaskFunction_t pxTaskCode,  
                         const char * const pcName,    /*lint !e971 Unqualified char types are allowed for strings and single characters only. */  
                         const uint32_t ulStackDepth,  
                         void * const pvParameters,  
                         UBaseType_t uxPriority,  
                         StackType_t * const puxStackBuffer,  
                         StaticTask_t * const pxTaskBuffer )  
{
  • 删除任务
c
void vTaskDelete( TaskHandle_t xTaskToDelete )
  • 挂起任务
c
void vTaskSuspend( TaskHandle_t xTaskToSuspend )
  • 恢复任务的运行
c
void vTaskResume( TaskHandle_t xTaskToResume )
  • 开启任务调度器
c
void vTaskStartScheduler( void )
  • 挂起调度器(不禁止中断,调度器被挂起后不再进行任务切换)
c
void vTaskSuspendAll( void )
  • 恢复调度器(不会解除使用vTaskSuspend单独挂起的任务)
c
BaseType_t xTaskResumeAll( void )
  • 延迟指定节拍数,进入阻塞状态
c
void vTaskDelay( const TickType_t xTicksToDelay )
  • 用于精确延迟的,周期任务
c
void vTaskDelayUntil( TickType_t * const pxPreviousWakeTime, const TickType_t xTimeIncrement )
  • 返回当前信号的滴答信号值
c
TickType_t xTaskGetTickCount( void )
  • 终止另一个任务的延时,使其退出阻塞状态
c
BaseType_t xTaskAbortDelay( TaskHandle_t xTask ) PRIVILEGED_FUNCTION;
  • 请求进行上下文切换
c
#define taskYIELD()             portYIELD()

image.png

空闲任务,优先级0,最低。别人都不用时进入

FreeRTOS基础时钟,默认采用sys_tick作为基础时钟

延时方式

c
/* USER CODE BEGIN APPTask_LED1 */  
  TickType_t pxPreviousWakeTime = xTaskGetTickCount();  
/* Infinite loop */  
for(;;)  
{  
  LED3_TOGGLE();  
    vTaskDelayUntil(&pxPreviousWakeTime, pdMS_TO_TICKS(1000));  
}
c
osDelay(pdMS_TO_TICKS(1000));//
c
HAL_Delay(1000);//这个在任务中不要用,它是直接阻塞的

获取任务句柄 1 Task_LED1Handle = osThreadNew(APPTask_LED1, NULL, &Task_LED1_attributes); 创建任务时的返回值Task_LED1Handle就是任务句柄

2 tasks.cxTaskGetCurrentTaskHandle返回获取任务句柄,需要配置之后才能参与编译

3 tasks.cTaskHandle_t xTaskGetIdleTaskHandle( void )获取空闲任务的句柄

4 TaskHandle_t xTaskGetHandle( const char *pcNameToQuery )传入任务名称的字符串,获取句柄。运行时间比较长,如果任务名称相同,返回结果不一定

设置优先级 void vTaskPrioritySet( TaskHandle_t xTask, UBaseType_t uxNewPriority )

获取任务信息 void vTaskGetInfo( TaskHandle_t xTask, TaskStatus_t *pxTaskStatus, BaseType_t xGetFreeStackSpace, eTaskState eState )

获取任务名称字符串 char *pcTaskGetName( TaskHandle_t xTaskToQuery )

内核信息统计 UBaseType_t uxTaskGetNumberOfTasks( void )返回当前任务管理的总数

void vTaskList( char * pcWriteBuffer )返回所有任务的字符串列表信息,(太大,运行太慢,不要在发布版本使用)

UBaseType_t uxTaskGetSystemState( TaskStatus_t * const pxTaskStatusArray, const UBaseType_t uxArraySize, uint32_t * const pulTotalRunTime )获取系统内所有任务的状态

void vTaskGetRunTimeStats( char *pcWriteBuffer ) 运行时会禁掉所有中断

BaseType_t xTaskGetSchedulerState( void )返回调度器的状态

任务

中断

FreeRTOS多任务编程示例①

c
/* USER CODE BEGIN Header_APPTask_LED1 */  
/**  
  * @brief  Function implementing the Task_LED1 thread.  * @param  argument: Not used  * @retval None  *//* USER CODE END Header_APPTask_LED1 */  
void APPTask_LED1(void *argument)  
{  
  /* USER CODE BEGIN APPTask_LED1 */  
  /* Infinite loop */  for(;;)  
  {  
    LED2_TOGGLE();  
    HAL_Delay(1000);  
    //osDelay(1);  
  }  
  /* USER CODE END APPTask_LED1 */  
}  
  
/* USER CODE BEGIN Header_APPTask_LED2 */  
/**  
* @brief Function implementing the Task_LED2 thread.  
* @param argument: Not used  
* @retval None  
*/  
/* USER CODE END Header_APPTask_LED2 */  
void APPTask_LED2(void *argument)  
{  
  /* USER CODE BEGIN APPTask_LED2 */  
  /* Infinite loop */  for(;;)  
  {  
    LED3_TOGGLE();  
    HAL_Delay(500);  
    //osDelay(1);  
  }  
  /* USER CODE END APPTask_LED2 */  
}
  • 如果两个任务设置的优先级一样
    • 按不同频率闪烁
    • 原因:HAL_Delay(500);是执行空指令,不会进入阻塞状态,但是优先级相同,两个任务会轮流占用CPU,所以正常闪烁
  • 如果任务1优先级,大于任务二优先级
    • 只有LED2闪烁,LED3不闪烁
    • 原因:HAL_Delay(500);是执行空指令,不会进入阻塞状态,而且任务1有高优先级,一直连续运行,一直占用CPU,导致任务二无法执行
c
/* USER CODE BEGIN Header_APPTask_LED1 */  
/**  
  * @brief  Function implementing the Task_LED1 thread.  * @param  argument: Not used  * @retval None  *//* USER CODE END Header_APPTask_LED1 */  
void APPTask_LED1(void *argument)  
{  
  /* USER CODE BEGIN APPTask_LED1 */  
  /* Infinite loop */  for(;;)  
  {  
    LED2_TOGGLE();  
    vTaskDelay(pdMS_TO_TICKS(1000));  
  }  
  /* USER CODE END APPTask_LED1 */  
}  
  
/* USER CODE BEGIN Header_APPTask_LED2 */  
/**  
* @brief Function implementing the Task_LED2 thread.  
* @param argument: Not used  
* @retval None  
*/  
/* USER CODE END Header_APPTask_LED2 */  
void APPTask_LED2(void *argument)  
{  
  /* USER CODE BEGIN APPTask_LED2 */  
  /* Infinite loop */  for(;;)  
  {  
    LED3_TOGGLE();  
    HAL_Delay(500);  
    //vTaskDelay(pdMS_TO_TICKS(500));  
  }  
  /* USER CODE END APPTask_LED2 */  
}
  • 现象:交替闪烁

  • 任务Task LED1大部分时间处于阻塞状态,在任务Task LED1处于阻塞状态时,任务Task LED2可以获得CPU的使用权,任务Task LED1在延时结束后,可以抢占CPU的使用权,空闲任务还是无法获得CPU的使用权力

  • osDelay本质也是调用vTaskDelay,传入的是节拍数,一般一个节拍1ms,pdMS_TO_TICKS(500)节拍转换函数

  • vTaskDelay不仅能进入延时,还能使任务进入阻塞状态,让低优先级的任务也能调用

4.多任务系统一般的任务函数的设计

在使用抢占式任务调度方法时,要根据任务的重要性分配不同的优先级,在任务空闲时要让出CPU的使用权,使其他就绪状态的任务能获得CPU的使用权。

c
/* USER CODE BEGIN Header_APPTask_LED1 */  
/**  
  * @brief  Function implementing the Task_LED1 thread.  * @param  argument: Not used  * @retval None  *//* USER CODE END Header_APPTask_LED1 */  
void APPTask_LED1(void *argument)  
{  
  /* USER CODE BEGIN APPTask_LED1 */  
  /* Infinite loop */  for(;;)  
  {  
    LED2_TOGGLE();  
    vTaskDelay(pdMS_TO_TICKS(1000));  
  }  
  /* USER CODE END APPTask_LED1 */  
}  
  
/* USER CODE BEGIN Header_APPTask_LED2 */  
/**  
* @brief Function implementing the Task_LED2 thread.  
* @param argument: Not used  
* @retval None  
*/  
/* USER CODE END Header_APPTask_LED2 */  
void APPTask_LED2(void *argument)  
{  
  /* USER CODE BEGIN APPTask_LED2 */  
  /* Infinite loop */  for(;;)  
  {  
    LED3_TOGGLE();  
    vTaskDelay(pdMS_TO_TICKS(500));  
  }  
  /* USER CODE END APPTask_LED2 */  
}
  • 现象:交替闪烁 image.png
c
/* USER CODE BEGIN Header_APPTask_LED1 */  
/**  
  * @brief  Function implementing the Task_LED1 thread.  * @param  argument: Not used  * @retval None  *//* USER CODE END Header_APPTask_LED1 */  
void APPTask_LED1(void *argument)  
{  
  /* USER CODE BEGIN APPTask_LED1 */  
  TickType_t pxPreviousWakeTime = xTaskGetTickCount();  // Get the current tick count
  /* Infinite loop */  
  for(;;)  
  {  
    LED2_TOGGLE();  
    vTaskDelayUntil(&pxPreviousWakeTime,pdMS_TO_TICKS(1000));  
  }  
  /* USER CODE END APPTask_LED1 */  
}  
  
/* USER CODE BEGIN Header_APPTask_LED2 */  
/**  
* @brief Function implementing the Task_LED2 thread.  
* @param argument: Not used  
* @retval None  
*/  
/* USER CODE END Header_APPTask_LED2 */  
void APPTask_LED2(void *argument)  
{  
  /* USER CODE BEGIN APPTask_LED2 */  
  TickType_t pxPreviousWakeTime = xTaskGetTickCount();  // Get the current tick count
  /* Infinite loop */  
  for(;;)  
  {  
    LED3_TOGGLE();  
    vTaskDelayUntil(&pxPreviousWakeTime,pdMS_TO_TICKS(500));  
  }  
  /* USER CODE END APPTask_LED2 */  
}
  • vTaskDelayUntil更精准的延时函数
  • vTaskDelayUntil(&pxPreviousWakeTime,pdMS_TO_TICKS(500)); 中传入的pdMS_TO_TICKS(500)时间应该明显大于循环中要执行代码的时间,以及可能被其他任务打断而延迟的时间

2.4 FreeRTOS任务管理工具函数

获取任务句柄

  • 创建任务是,返回任务句柄
c
TaskHandle_t xTaskCreateStatic( TaskFunction_t pxTaskCode,  
                         const char * const pcName,    /*lint !e971 Unqualified char types are allowed for strings and single characters only. */  
                         const uint32_t ulStackDepth,  
                         void * const pvParameters,  
                         UBaseType_t uxPriority,  
                         StackType_t * const puxStackBuffer,  
                         StaticTask_t * const pxTaskBuffer )
  • 获取当前任务的句柄TaskHandle_t xTaskGetCurrentTaskHandle( void ),在task.c
c
#if ( ( INCLUDE_xTaskGetCurrentTaskHandle == 1 ) || ( configUSE_MUTEXES == 1 ) )  
  
    TaskHandle_t xTaskGetCurrentTaskHandle( void )  
    {  
    TaskHandle_t xReturn;  
  
       /* A critical section is not required as this is not called from  
       an interrupt and the current TCB will always be the same for any       individual execution thread. */       xReturn = pxCurrentTCB;  
  
       return xReturn;  
    }  
  
#endif /* ( ( INCLUDE_xTaskGetCurrentTaskHandle == 1 ) || ( configUSE_MUTEXES == 1 ) ) */
  • 获取空闲任务的句柄
c
#if ( INCLUDE_xTaskGetIdleTaskHandle == 1 )  
  
    TaskHandle_t xTaskGetIdleTaskHandle( void )  
    {       /* If xTaskGetIdleTaskHandle() is called before the scheduler has been  
       started, then xIdleTaskHandle will be NULL. */       configASSERT( ( xIdleTaskHandle != NULL ) );  
       return xIdleTaskHandle;    }  
#endif /* INCLUDE_xTaskGetIdleTaskHandle */
  • 传入任务名称字符串pcNameToQuery,获取任务句柄运行时间较长不建议使用,如果两个任务有相同的任务函数,返回结果不确定
c
TaskHandle_t xTaskGetHandle( const char *pcNameToQuery )

单个任务的信息函数

  • 获取任务的优先级UBaseType_t uxTaskPriorityGet( const TaskHandle_t xTask )
  • 上一个函数的终端版本UBaseType_t uxTaskPriorityGetFromISR( const TaskHandle_t xTask )
  • 设置优先级void vTaskPrioritySet( TaskHandle_t xTask, UBaseType_t uxNewPriority )
  • 获取任务信息void vTaskGetInfo( TaskHandle_t xTask, TaskStatus_t *pxTaskStatus, BaseType_t xGetFreeStackSpace, eTaskState eState )
    • TaskHandle_t xTask任务句柄
    • TaskStatus_t *pxTaskStatus:存储任务状态的结构体指针
    • BaseType_t xGetFreeStackSpace:是否返回栈空间高水位值,可以观察堆栈空间是否设置的过小,但是运行比较消耗时间
    • eTaskState eState:指定任务的状态
  • 获取任务名称的字符串char *pcTaskGetName( TaskHandle_t xTaskToQuery )
  • 获取任务高水为值UBaseType_t uxTaskGetStackHighWaterMark( TaskHandle_t xTask ),高水位值就是栈空间最少可用的剩余空间大小,单位是字。这个值偏小表示栈偏小
  • 返回任务的当前状态eTaskState eTaskGetState( TaskHandle_t xTask )

内核信息统计相关的函数

  • 返回任务当前管理的任务总数,包括就绪的、阻塞的、挂起的、虽然删除了但是没有在空闲任务里释放的任务UBaseType_t uxTaskGetNumberOfTasks( void )
  • 返回内核中所有任务的字符串列表信息,包含每个任务的名称、状态、优先级、高水位值、任务编号void vTaskList( char * pcWriteBuffer ),要传入一个足够大的字符数组
    • 这个函数使用后,会使代码占用增大,一般只在调试时使用,不建议在发布版本中使用
  • UBaseType_t uxTaskGetSystemState( TaskStatus_t * const pxTaskStatusArray, const UBaseType_t uxArraySize, uint32_t * const pulTotalRunTime )
    • 获取系统内所有任务的状态
    • TaskStatus_t * const pxTaskStatusArray结构体数组指针,数组大小必须大于等于FreeRTOS的任务数
    • const UBaseType_t uxArraySize第一个参数中,个数
    • uint32_t * const pulTotalRunTime 返回FreeRTOS运行后的总时间,如果设置为NULL则不反悔这个数据
  • void vTaskGetRunTimeStats( char *pcWriteBuffer )
    • 传入一个字符串char *pcWriteBuffer,会以文字表格的形式返回出每个任务的运行时间,包括绝对时间和占用CPU的百分比
    • 该函数运行时会禁用掉所有中断,所以不要在程序正常运行时使用这个函数,在调试阶段使用即可
  • BaseType_t xTaskGetSchedulerState( void )
    • 返回调度器的状态(三种状态)
      • 挂起
      • 未启动
      • 正在运行

2.5 FreeRTOS多任务编程示例2

  • 这两个函数找不大,需要先区chubMAX查看一下有没有定义可以开启,若没有去代码处查看,需要什么宏定义,在FreeRTOSConfig.h里面定义 image.pngimage.png

由于Cortex-M3和M4内核具有双堆栈指针,MSP主堆栈指针和PSP进程堆栈指针,或者叫PSP任务堆栈指针也是可以的。在FreeRTOS操作系统中,主堆栈指针MSP是给系统栈空间使用的,进程堆栈指针PSP是给任务栈使用的。也就是说,在FreeRTOS任务中,所有栈空间的使用都是通过PSP指针进行指向的。一旦进入了中断函数以及可能发生的中断嵌套都是用的MSP指针。这个知识点要记住它,当前可以不知道这是为什么,但是一定要记住

  • MSP主堆栈指针
    • 中断或中断嵌套
  • PSP进程堆栈指针
    • 任务

输出整理,格式化