细说STM32单片机FreeRTOS任务管理相关函数及多任务编程的实现方法

目录

一、FreeRTOS任务管理相关函数

1、FreeRTOS函数

2、FreeRTOS宏函数 

3、主要函数功能说明

(1)创建任务osThreadNew()

(2)删除任务vTaskDelete()

(3)挂起任务vTaskSuspend()

(4)恢复任务vTaskResume()

(5)启动任务调度器vTaskStartScheduler()

(6)延时函数vTaskDelay()

(7)绝对延时函数vTaskDelayUntil() 

二、多任务编程示例

1、示例功能

2、CubeMX项目设置

(1)RCC、SYS

(2)GPIO

(3)FreeRTOS

(4)NVIC

3、程序设计

(1)main()

(2)freertos.c

(3)编写任务函数代码

1)相同优先级的任务的执行

2)低优先级任务被“饿死”

3)高优先级任务主动进入阻塞状态

4)优秀的任务调度是任务照做、CPU尽量空闲

5)使用vTaskDelayUntil()函数


一、FreeRTOS任务管理相关函数

        在FreeRTOS中,任务的管理主要包括任务的创建、删除、挂起、恢复等操作,还包括任务调度器的启动、挂起与恢复,以及使任务进入阻塞状态的延迟函数等。

        FreeRTOS中任务管理相关的函数都在文件task.h中定义,在文件tasks.c中实现。在CMSIS-RTOS中还有一些函数,对FreeRTOS的函数进行了封装,也就是调用相应的FreeRTOS函数实现相同的功能,这些标准接口函数的定义在文件cmsis_os.h和cmsis_os2.h中。CubeMX生成的代码一般使用CMSIS-RTOS标准接口函数,在用户自己编写的程序中,一般直接使用FreeRTOS的函数。

1、FreeRTOS函数

        任务管理常用的一些函数及其功能描述见下。这里只列出了函数名,省略了输入/输出参数。 

分组

Free RTOS函数

函数功能描述

CMSIS-RTOS

封装函数

任务
管理

xTaskCreate()

创建一个任务,动态分配内存

osThreadNew()

xTaskCreateStatic()

创建一个任务,静态分配内存

osThreadNew()

vTaskDelete()

删除当前任务或另一个任务

osThreadTerminate()
osThreadExit()

vTaskSuspend()

挂起当前任务或另一个任务

osThreadSuspend()

vTaskResume()

恢复另一个挂起任务的运行

osThreadResume()

调度器
管理

vTaskStartScheduler()

开启任务调度器

osKernelStart()

vTaskSuspendAll()

挂起调度器,但不禁止中断。调度器被挂起后,不会再进行上下文切换

osKernelLock()

xTaskResumeAll()

恢复调度器的执行,但是不会解除用函数vTaskSuspend()单独挂起的任务的起状态

osKernelUnlock()

vTaskStepTick()

用于在tickless低功耗模式时补足系统时钟计数节拍

延时与
调度

vTaskDelay()

当前任务延时指定节拍数,并进入阻塞状态

osDelay()

vTaskDelayUntil()

当前任务延时到指定的时间,并进入阻塞状态,用于精确延时的周期性任务

osDelayUntil()

xTaskGetTickCount()

返回嘀嗒信号的当前计数值

osKernelGetTickCount()

xTaskAbortDelay()

终止另一个任务的延时,使其立刻退出阻塞状态

taskYIELD()

请求进行一次上下文切换

osThreadYield()

        FreeRTOS函数基本都有对应的CMSIS-RTOS标准函数,特别地:

  • FreeRTOS创建任务的函数有两个,xTaskCreate()用于创建动态分配内存的任务,xTaskCreateStatic()用于创建静态分配内存的任务。对应的CMSIS-RTOS标准函数osThreadNew()会根据任务的参数自动调用其中的某个函数。
  • 函数vTaskDelete()可以根据传递的参数不同,删除另一个任务或当前任务,对应的CMSIS-RTOS标准函数有两个,osThreadTerminate()用于删除另一个任务,osThreadExit()用于删除当前任务(即任务自身)。
  • 函数xTaskAbortDelay()用于终止另一个任务的延时,使其立刻退出阻塞状态,这个函数没有对应的CMSIS-RTOS函数。

2、FreeRTOS宏函数 

        除了表中的这些函数外,文件task.h中还有几个常用的宏函数,其定义代码如下,相应的函数功能见代码中的注释:

#define taskENTER_CRITICAL() portENTER_CRITICAL()			//开始临界代码段
#define taskENTER_CRITICAL_FROM_ISR() portSET_INTERRUPT_MASK_FROM_ISR()

#define taskEXIT_CRITICAL() portEXIT_CRITICAL()				//结束临界代码段
#define taskEXIT_CRITICAL_FROM_ISR( x ) portCLEAR_INTERRUPT_MASK_FROM_ISR( x )

#define taskDISABLE_INTERRUPTS() portDISABLE_INTERRUPTS()	//关闭NCU的所有可屏蔽中断
#define taskENABLE_INTERRUPTS() portENABLE_INTERRUPTS()		//使能MCU的中断
  • 宏函数taskDISABLE_INTERRUPTS()和taskENABLE_INTERRUPTS()用于关闭和开启MCU的可屏蔽中断,用于界定不受其他中断干扰的代码段。只能关闭FreeRTOS可管理的中断优先级,即参数configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY定义的最高优先级。这两个函数必须成对使用,且不能嵌套使用。
  • 函数taskENTER_CRITICAL()和taskEXIT_CRITICAL()用于界定临界(Critical)代码段。在临界代码段内,任务不会被更高优先级的任务抢占,可以保证代码执行的连续性。例如,一段代码需要通过串口上传一批数据,如果被更高优先级的任务抢占了CPU,上传的过程被打断,上传数据就可能出现问题,这时就可以将这段代码界定为临界代码段。taskENTER_CRITICAL()内部会调用关闭可屏蔽中断的函数portDISABLE_INTERRUPTS(),与宏函数taskDISABLE_INTERRUPTS()实现的功能相似。函数taskENTER_CRITICAL()和taskEXIT_CRITICAL()必须成对使用,但可以嵌套使用。
  • taskENTER_CRITICAL_FROM_ISR()是taskENTER_CRITICAL()的ISR版本,用于在中断服务例程中调用。注意,FreeRTOS的所有API函数分为普通版本和ISR版本,如果要在ISR里调用FreeRTOS的API函数,必须使用其ISR版本。

        这些宏函数实际上是执行了另外一个函数,例如,taskENTER_CRITICAL()实际上就是执行了函数portENTER_CRITICAL()。跟踪代码会发现,这些port”前缀的函数是在文件portmacro.h中定义的宏函数。文件portmacro.h中的这些宏函数如下: 

#define portSET_INTERRUPT_MASK_FROM_ISR() 		ulPortRaiseBASEPRI()
#define portCLEAR_INTERRUPT_MASK_FROM_ISR(x) 	vPortSetBASEPRI(x)
#define portDISABLE_INTERRUPTS() 				vPortRaiseBASEPRI()
#define portENABLE_INTERRUPTS() 				vPortSetBASEPRI(0)
#define portENTER_CRITICAL() 					vPortEnterCritical()
#define portEXIT_CRITICAL() 					vPortExitCritical()

        这些宏函数实际执行的函数是在文件port.c或portmacro.h中实现的,某些函数的实现代码完全是用汇编语言写的,它们是根据具体的MCU型号移植的代码。

3主要函数功能说明

(1)创建任务osThreadNew()

        例如,当CubeMX使用默认RTOS参数生成的代码时,使用函数osThreadNew()创建任务,根据任务的属性设置,osThreadNew()内部会自动调用xTaskCreate()以动态分配内存方式创建任务,或者调用xTaskCreateStatic()以静态分配内存方式创建任务。函数osThreadNew()的原型定义如下:

/// Create a thread and add it to Active Threads.
/// \param[in] func thread function.
/// \param[in] argument pointer that is passed to the thread function as start argument.
/// \param[in] attr thread attributes; NULL: default values.
/// \return thread ID for reference by other functions or NULL in case of error.
osThreadId_t osThreadNew (osThreadFunc_t func, void *argument, const osThreadAttr_t *attr);

         返回的数据是所创建任务的句柄,数据类型osThreadld_t的定义如下:

/// \details Thread ID identifies the thread.
typedef void *osThreadId_t;

        使用动态分配内存方式创建任务的函数是xTaskCreate(),其原型定义如下,每个参数的意义见代码注释:

BaseType_t xTaskCreate( TaskFunction_t pxTaskCode,	//任务函数名称
        const char * const pcName, 					//任务的备注名称
        const configSTACK_DEPTH_TYPE usStackDepth,	//栈空间大小,单位:字
        void * const pvParameters,					//传递给任务函数的参数
        UBaseType_t uxPriority,						//任务优先级
        TaskHandle_t * const pxCreatedTask )		//任务的句柄

        函数xTaskCreate()返回值的类型是BaseType_t,其值若是pdPASS,就表示任务创建成功。创建的任务的句柄是函数中的参数pxCreatedTask,其类型是TaskHandle_t。这个类型的定义与osThreadld_t的是相同的,定义如下:

typedef void TaskHandle_t;

        使用静态分配内存方式创建任务的函数是xTaskCreateStatic(),其原型定义如下,每个参数的意义见代码注释:

TaskHandle_t xTaskCreateStatic( TaskFunction_t pxTaskCode,	//任务函数名称
        const char * const pcName, 						    //任务的备注名称
        const uint32_t ulStackDepth,						//栈空间大小,单位:字
        void * const pvParameters,						    //传递给任务函数的参数
        UBaseType_t uxPriority,							    //任务优先级
        StackType_t * const puxStackBuffer,					//任务的栈空间数组
        StaticTask_t * const pxTaskBuffer )					//任务控制块存储空间

        函数xTaskCreateStatic()返回的数据类型是TaskHandle_t,返回的数据就是所创建任务的句柄。

        用户可以在启动任务调度器之前创建所有的任务,也可以在启动任务调度器之后,在一个任务的任务函数里创建其他任务。在实际编程中,若要手工编程创建任务,建议使用函数osThreadNew(),因为可以参考CubeMX生成的代码。

(2)删除任务vTaskDelete()

        删除任务的函数是vTaskDelete(),其原型定义如下:

void vTaskDelete( TaskHandle_t xTaskToDelete );

        TaskHandle_t类型的参数xTaskToDelete是需要删除的任务的句柄。如果要删除任务自己,则传递参数NULL即可。注意,如果要删除任务自己,必须在跳出任务死循环之后,在退出任务函数之前执行vTaskDelete(NULL)。

        删除任务时,FreeRTOS会自动释放系统自动分配的内存,如动态分配的栈空间和任务控制块,但是在任务内由用户自己分配的内存,需要在删除任务之前手工释放。

(3)挂起任务vTaskSuspend()

        挂起一个任务的函数是vTaskSuspend(),其原型定义如下:

void vTaskSuspend( TaskHandle_t xTaskToSuspend );

        参数xTaskToSuspend是需要挂起的任务的句柄,如果是要挂起任务自己,则传递参数UUL,被挂起的任务将不再参与任务调度,但是还存在于系统中,可以被恢复。

(4)恢复任务vTaskResume()

        恢复一个被挂起的任务的函数是vTaskResume(),其原型定义如下:

void vTaskResume( TaskHandle_t xTaskToResume );

        参数xTaskToResume是需要恢复的任务的句柄。一个被挂起的任务无法在任务函数里恢复自己。只能在其他任务的函数里恢复,所以参数不能是NULL。

(5)启动任务调度器vTaskStartScheduler()

        函数vTaskStartScheduler()可用于启动任务调度器,开始FreeRTOS的运行,其原型定义如下:

void vTaskStartScheduler( void );

        函数vTaskStartScheduler()会自动创建一个空闲任务,空闲任务的优先级为0,也就是最低先级。如果设置参数configUSE_TIMERS的值为1,也就是要使用软件定时器,还会自动创建一个时间守护任务。

(6)延时函数vTaskDelay()

        延时函数vTaskDelay()用于延时一定节拍数,它会使当前任务进入阻塞状态。任何任务都要在空闲的时候进入阻塞状态,以让出CPU的使用权,使其他低优先级的任务可以获得CPU在使用权,否则,一个高优先级的任务将总是占据CPU,导致其他低优先级的任务无法运行。

        函数vTaskDelay()的原型定义如下:

void vTaskDelay(const TickType_t xTicksToDelay);

        其中,参数xTicksToDelay是需要延时的节拍数,是基础时钟的节拍数。一般会结合宏函数pdMS_TO_TICKS(),将一个以毫秒为单位的时间转换为节拍数,然后调用vTaskDelay(),可以使延时时间不受FreeRTOS基础时钟频率变化的影响。一般地,使用延时函数进入阻塞状态的任务函数的基本代码结构如下:

void AppTask_Function(void *argument)
{
    /*任务内初始化*/
    TickType_t ticks2 = pdMS_TO_TICKS(500);		//延时时间500ms转换为节拍数
    for(;;)								        //死循环
    {
        /*死循环内的功能代码*/
        vTaskDelay(ticks2);				        //空闲的时候进行延时,进入阻塞状态
    }
    vTaskDelete(NULL);					        //如果跳出了死循环,需要在函数退出前删除任务自己
}

(7)绝对延时函数vTaskDelayUntil() 

        绝对延时函数vTaskDelayUntil()的功能与延时函数vTaskDelay()的相似,也用于延时,并且使任务进入阻塞状态。不同的是,函数vTaskDelay()的延时时间长度是相对于进入阻塞状态的时刻的,但是对于任务的死循环,一个循环的周期时间是不确定的,因为循环内执行的代码的时间长度是未知的,可能被其他任务抢占。

        若需要在任务函数内实现严格的周期性的循环,则可以使用绝对延时函数vTaskDelayUntil(),其原型定义如下:

void vTaskDelayUntil( TickType_t * const pxPreviousWakeTime, const TickType_t xTimeIncrement );

        其中,参数pxPreviousWakeTime表示上次任务唤醒时基础时钟计数器的值,参数xTimeIncrement表示相对于上次唤醒时刻延时的节拍数。函数vTaskDelayUntil()每次会自动更新pxPreviousWakeTime的值,但是在第一次调用时,需要给一个初值。

        函数vTaskDelay()和vTaskDelayUntil()的意义和区别可以用图表示:方波表示任务的周期循环,高电平表示任务运行时间,低电平表示阻塞时间,上跳沿表示唤醒时刻,下跳沿表示进入阻塞状态的时刻。

 

        可以通过函数xTaskGetTickCount()返回嘀嗒信号当前计数值,作为pxPreviousWakeTime的初值。使用vTaskDelayUntil()的任务函数的一般代码结构如下,示例代码可以使任务的循环周期为比较精确的1000ms:

void AppTask_Function(void *argument)
{
    /*任务内初始化*/
    TickType_t previouaWakeTime=xTaskGetTickCount();				//获得嘀嗒信号当前计数值
    for(;;)												            //死循环
    {
        /*死循环内的功能代码*/
        vTaskDelayUntil(&previousWakeTime,pdMS_TO_TICKS(1000));	    //循环周期1000ms
    }
    vTaskDelete(NULL);						                        //如果跳出了死循环,需要在函数退出前删除任务自己
}

二、多任务编程示例

1、示例功能

        设计一个示例以测试FreeRTOS的多任务功能。本示例继续使用旺宝红龙开发板STM32F407 ZGT6 KIT V1.0,并用到开发板上的LED1和LED2,在FreeRTOS中设计两个任务,在任务1里使LED1闪烁,在任务2里使LED2闪烁。

2、CubeMX项目设置

        在CubeMX中,我们选择STM32F407ZG创建一个项目,先完成如下的基本设置。

(1)RCC、SYS

        外部时钟,配置HCLK为168MHz;

        设置Debug接口为Serial Wire,设置基础时钟(Timebase Source)为TIM6。

(2)GPIO

         根据开发板原理图之LED电路,配置引脚PA6和PA4为GPIO推挽输出,无上拉或下拉。依次改名为LED1和LED2。

(3)FreeRTOS

  • 启用FreeRTOS并设置为CMSIS_V2接口。
  • Config parameters页面的设置默认值,默认状态下,参数configUSE_PREEMPTION的值是1;configUSE_TIME_SLICING的值,其值总是1。所以,默认状态下,FreeRTOS使用带时间片的抢占式任务调度方法。
  • Include parameters页面的设置也保持默认值,默认状态下,这一页面已经包含了vTaskDelete()、vTaskSuspend()、vTaskDelay()、vTaskDelayUntil()等函数,如果不需要使用某个,将相应的参数设置为Disabled即可。
  • 在FreeRTOS中创建两个任务,创建好的两个任务的基本参数如图。Add或Delete按钮,可以添加或删除任务;双击列表中的一个任务,就可以打开一个设置其属性的对话框。 

 

  • 两个任务的优先级都设置为osPriorityNormal,也就是具有相同的优先级。默认任务的优先级为Low;
  • 两个任务采用了不同的内存分配方式,LED1_Task使用动态分配内存,LED2_Task使用静态分配内存。使用静态分配内存时,需要设置作为栈空间的数组名称以及控制块名称。

(4)NVIC

        默认设置

3、程序设计

(1)main()

        一般,这段程序是自动生成的。 

/*删除无关的沙箱代码段*/

/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "cmsis_os.h"
#include "gpio.h"

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
void MX_FREERTOS_Init(void);

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* Configure the system clock */
  SystemClock_Config();

  /* Initialize all configured peripherals */
  MX_GPIO_Init();

  /* Init scheduler */
  osKernelInitialize();

  /* Call init function for freertos objects (in cmsis_os2.c) */
  MX_FREERTOS_Init();

  /* Start scheduler */
  osKernelStart();

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

/*省略以下代码*/

        main()函数的代码结构非常清晰,前面部分是MCU软硬件的初始化,包括HAL初始化、系统时钟配置和外设初始化。

        FreeRTOS的初始化和启动就是执行以下3个函数。

  • osKernelInitialize()是CMSIS-RTOS标准接口函数,用于初始化FreeRTOS的调度器。
  • MX_FREERTOS_Init()是FreeRTOS对象初始化函数,在freertos.c中实现,用于创建任务、信号量、互斥量等FreeRTOS中定义的对象。
  • osKernelStart()是CMSIS-RTOS标准接口函数,用于启动FreeRTOS的任务调度器。

        执行函数osKernelStart()启动FreeRTOS的任务调度器后,任务调度器就接管了CPU的控制权,函数osKernelStart()是永远不会退出的,所以不会执行后面的while()循环。

(2)freertos.c

        文件freertos.c是CubeMX生成代码时生成的文件,是实现用户功能的代码文件。在main()函数中调用的函数MX_FREERTOS_Init()就是在这个文件里实现的。

/* Private typedef -----------------------------------------------------------*/
typedef StaticTask_t osStaticThreadDef_t;				//类型符号定义

/* Definitions for defaultTask */
osThreadId_t defaultTaskHandle;
const osThreadAttr_t defaultTask_attributes = {
    .name = "defaultTask",
    .stack_size = 128 * 4,
    .priority = (osPriority_t) osPriorityLow,
};

/* Definitions for Task_LED1 */
osThreadId_t Task_LED1Handle;				        //任务Task_LED1的句柄变量
const osThreadAttr_t Task_LED1_attributes = {		//任务Task_LED1的属性
    .name = "Task_LED1",						    //任务名称
    .stack_size = 128 * 4,						    //栈空间大小,128×4字节
    .priority = (osPriority_t) osPriorityLow,		//任务名称/7任务优先级
};

/* Definitions for Task_LED2 */
osThreadId_t Task_LED2Handle;				        //任务Task_LED2的句柄变量
uint32_t Task_LED2Buffer[ 128 ];				    //任务Task_LED2的栈空间数组
osStaticThreadDef_t Task_LED2ControlBlock;		    //任务Task_LED2的任务控制块
const osThreadAttr_t Task_LED2_attributes = {		//任务Task_LED2的属性
    .name = "Task_LED2",						    //任务名称
    .cb_mem = &Task_LED2ControlBlock,			    //任务控制块
    .cb_size = sizeof(Task_LED2ControlBlock),		//任务控制块大小
    .stack_mem = &Task_LED2Buffer[0],			    //栈空间数组
    .stack_size = sizeof(Task_LED2Buffer),			//栈空间大小,单位:字节
    .priority = (osPriority_t) osPriorityLow,		//任务优先级
};

/* Private function prototypes -----------------------------------------------*/
/* USER CODE BEGIN FunctionPrototypes */

/* USER CODE END FunctionPrototypes */

void StartDefaultTask(void *argument);
void StartTask02(void *argument);				    //任务Task_LED1的任务函数
void StartTask03(void *argument);				    //任务Task_LED2的任务函数
void MX_FREERTOS_Init(void); 				        /* (MISRA C 2004 rule 8.1) */

/**
  * @brief  FreeRTOS initialization
  * @param  None
  * @retval None
  */
void MX_FREERTOS_Init(void) {
  /* Create the thread(s) */
  /* creation of defaultTask */
  defaultTaskHandle = osThreadNew(StartDefaultTask, NULL, &defaultTask_attributes);

  /* creation of Task_LED1 */
  Task_LED1Handle = osThreadNew(AppTask_LED1, NULL, &Task_LED1_attributes);

  /* creation of Task_LED2 */
  Task_LED2Handle = osThreadNew(AppTask_LED2, NULL, &Task_LED2_attributes);
}

/* USER CODE BEGIN Header_StartDefaultTask */
/**
  * @brief  Function implementing the defaultTask thread.
  * @param  argument: Not used
  * @retval None
  */
/* USER CODE END Header_StartDefaultTask */
void StartDefaultTask(void *argument)
{
  /* USER CODE BEGIN StartDefaultTask */
  /* Infinite loop */
  for(;;)
  {
    osDelay(1);
  }
  /* USER CODE END StartDefaultTask */
}

/* USER CODE BEGIN Header_AppTask_LED1 */
/**
* @brief Function implementing the LED1_Task 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(;;)
  {
    osDelay(1);
  }
  /* USER CODE END AppTask_LED1 */
}

/* USER CODE BEGIN Header_AppTask_LED2 */
/**
* @brief Function implementing the LED2_Task 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(;;)
  {
    osDelay(1);
  }
  /* USER CODE END AppTask_LED2 */
}

        文件freertos.c中有3个函数,这3个函数都是CubeMX自动生成的。

  • 函数MX_FREERTOS_Init()是FreeRTOS对象初始化函数,用于创建定义的两个任务。自动生成的只是任务函数的框架
  • 函数AppTask_LED1()是任务Task_LED1的任务函数。
  • 函数AppTask_LED2()是任务Task_LED2的任务函数。

        在freertos.c的私有变量定义部分,定义了两个任务的句柄变量和任务属性变量。任务Task_LED1采用动态分配内存方式,任务Task_LED2采用静态分配内存方式,它们的任务属性变量的赋值不同

        函数MX_FREERTOS_Init()创建了Task_LED1和Task_LED2两个任务,创建的任务用任务句柄变量表示。操作任务时,需要使用任务句柄变量作为参数,如vTaskDelete()、vTaskSuspend()等函数,都需要传递一个任务句柄变量作为输入参数。

(3)编写任务函数代码

        CubeIDE自动生成的只是任务函数的框架,里面没有可用的应用程序,用户想执行什么样的应用,就把应用代码写进任务函数。

        对任务框架及属性稍微做些修改,编写并观察带时间片的抢占式任务调度方法的特点,及vTaskDelay()和vTaskDelayUntil()等函数的使用方法。

1)相同优先级的任务的执行

        FreeRTOS使用默认的带时间片的抢占式任务调度方法,并且将两个任务的优先级都设置为osPriorityNormal。为两个任务的任务函数编写代码,使两个LED分别以不同的周期闪烁。

/* USER CODE BEGIN Header_AppTask_LED1 */
/**
* @brief Function implementing the LED1_Task 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(;;)
  {
	  HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_6);			//PA6=LED1
	  //vTaskDelayUntil(&previousWakeTime, ticks1);		//循环周期1000ms
	  //vTaskDelay(ticks1);
	  HAL_Delay(1000);									//由TIM6控制,不会使任务进入阻塞状态,而是一直处于连续运行状态
	  //osDelay(1);
  }
  /* USER CODE END AppTask_LED1 */
}

/* USER CODE BEGIN Header_AppTask_LED2 */
/**
* @brief Function implementing the LED2_Task 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(;;)
  {
	  HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_4);			//PA4=LED2
	  //vTaskDelayUntil(&previousWakeTime, ticks2);		//循环周期500ms
	  //vTaskDelay(ticks2);
	  HAL_Delay(500);
	  //osDelay(1);
  }
  /* USER CODE END AppTask_LED2 */
}

        这两个任务函数的功能很简单,就是分别使LED1和LED2以不同的周期闪烁。任务函数的for循环使用了延时函数HAL_Delay(),这个延时函数不会使任务进入阻塞状态,而是一直处于连续运行状态。

        构建项目后,我们将其下载到开发板并运行,会发现LED1和LED2都能闪烁,两个任务都可以执行。

        带时间片的抢占式任务调度器会在基础时钟每次中断时,进行一次任务调度申请,在没有其他中断处理时,就会进行任务调度。在图2-9中,时间轴上的t1、t2等时间点表示基础时钟发生中断的时间点,也就是进行任务调度的时间点,默认周期是1ms。

        因为两个任务具有相同的优先级,所以调度器使两个任务轮流占用CPU。两个任务都是连续运行的,所以每个任务每次占用CPU的时间都是一个嘀嗒信号周期,不占用CPU时,就处于就绪状态。系统中还有一个空闲任务,但是因为用户的两个任务是连续执行的,且优先级高空闲任务,所以空闲任务总是无法获得CPU的使用权,总是处于就绪状态。

2)低优先级任务被“饿死”

        再次,对程序稍作修改,将任务Task_LED2的优先级修改为osPriorityBelowNormal,任务Task_LED1的优先级仍然为osPriorityNormal。可以在CubeMX里修改后重新生成代码,也可以直接修改freertos.c中为任务Task_LED2的任务属性赋值的语句。两个任务的任务函数代码无须修改,与前面的相同。

        构建项目后,我们将其下载到开发板并运行,会发现只有LED1闪烁,而LED2不闪烁。

        Task_LED1具有高优先级,且是连续运行态,它会一直占用CPU,不会进入阻塞状态,所以低优先级的任务Task_LED2和默认任务都无法获得CPU的使用权,只能一直处于就绪状态,就好像它们被“饿死”了。

3)高优先级任务主动进入阻塞状态

        再次,对前面的程序稍作修改,使Task_LED2的优先级为osPriorityBelowNormal,任务LED1的优先级仍然为osPriorityNormal。修改后两个任务的任务函数代码如下:

/* USER CODE END Header_AppTask_LED1 */
void AppTask_LED1(void *argument)
{
  /* USER CODE BEGIN AppTask_LED1 */
  /* Infinite loop */
  TickType_t ticks1 = pdMS_To_TICKS(1000);    //时间(ms)转换为节拍数(ticks)
  for(;;)
  {
	  HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_6);			//PA6=LED1
	  //vTaskDelayUntil(&previousWakeTime, ticks1);		//循环周期1000ms
	  vTaskDelay(ticks1);
	  //HAL_Delay(1000);									//由TIM6控制,不会使任务进入阻塞状态,而是一直处于连续运行状态
	  //osDelay(1);
  }
  /* USER CODE END AppTask_LED1 */
}
 
/* USER CODE END Header_AppTask_LED2 */
void AppTask_LED2(void *argument)
{
  /* USER CODE BEGIN AppTask_LED2 */
  /* Infinite loop */
  for(;;)
  {
	  HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_4);			//PA4=LED2
	  //vTaskDelayUntil(&previousWakeTime, ticks2);		//循环周期500ms
	  //vTaskDelay(ticks2);
	  HAL_Delay(500);
	  //osDelay(1);
  }
  /* USER CODE END AppTask_LED2 */
}

        对Task_LED1的任务函数代码做了修改。pdMS_TO_TICKS()宏函数的功能是将时间(单位ms)转换为基础时钟节拍数。在for无限循环中,使用了延时函数vTaskDelay()。这个函数的作用不但包括延时,而且使当前任务进入阻塞状态,以便低优先级任务可以在任务调度时获得CPU的使用权。

        不修改任务Task_LED2的任务函数代码,在for循环中,还是使用延时函数HAL_Delay(),所以任务task_LED2还是连续运行的。

        构建项目后,我们将其下载到开发板并运行,会发现LED1和LED2都能闪烁,两个任务都可以执行。

 

        时间轴上的一个周期不再是一个嘀嗒信号周期,而是时间。

  • 在for循环里,任务Task_LED1每次执行完功能代码后,就调用vTaskDelay()函数延时1000ms,并且进入阻塞状态,所以任务Task_LED1大部分时间处于阻塞状态。
  • 虽然任务Task_LED2的优先级比任务Task_LED1的低,但是在任务Task_LED1处于阻塞状态时,任务Task_LED2可以获得CPU的使用权。此外,因为Task_LED2是连续运行的,所以它占用了CPU的大部分时间。
  • 任务Task_LED1在延时结束后,因为其优先级高,可以重新抢占CPU的使用权。
  • 因为任务Task_LED2是连续运行的,不会进入阻塞状态,默认任务仍然无法获得CPU的使用权。
4)优秀的任务调度是任务照做、CPU尽量空闲

        一个好的任务调度策略(原则),既保证任务调度照做,又保证CPU尽量空闲。

        在使用抢占式任务调度方法时,一般要根据任务的重要性分配不同的优先级,然后在任务函数里,在任务空闲时让出CPU的使用权,进入阻塞状态,以便系统进行任务调度,使其他就绪状态的任务能获得CPU的使用权。任务进入阻塞状态主要有两种方法:一种是调用延时函数,vTaskDelay();另一种是在进程间通信时,请求信号量、队列等事件。 

        继续修改上面的例子,使Task_LED2的优先级为osPriorityBelowNormal,任务Task_LED1的优先级仍然为osPriorityNormal。修改任务Task_LED2的任务函数代码:

void AppTask_LED1(void *argument)
{
  /* USER CODE BEGIN AppTask_LED1 */
  /* Infinite loop */
  TickType_t ticks1 = pdMS_TO_TICKS(1000);				//时间(ms)转换为节拍数(ticks)
  for(;;)
  {
	  HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_6);			//PA6=LED1
	  //vTaskDelayUntil(&previousWakeTime, ticks1);		//Cycle period1000ms
	  vTaskDelay(ticks1);
	  //HAL_Delay(1000);
	  //osDelay(1);
  }
  /* USER CODE END AppTask_LED1 */
}


/* USER CODE END Header_AppTask_LED2 */
void AppTask_LED2(void *argument)
{
  /* USER CODE BEGIN AppTask_LED2 */
  /* Infinite loop */
  TickType_t ticks2 = pdMS_TO_TICKS(500);
  for(;;)
  {
	  HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_4);			//PA4=LED2
	  //vTaskDelayUntil(&previousWakeTime, ticks2);		//Cycle period 500ms
	  vTaskDelay(ticks2);
	  //HAL_Delay(500);
	  //osDelay(1);
  }
  /* USER CODE END AppTask_LED2 */
}

        构建项目后,将其下载到开发板并运行,会发现LED1和LED2都能闪烁,两个任务都可以执行。

  • 任务Task_LED1在for循环里执行完功能代码后,调用函数vTaskDelay()延时1000ms,并且进入阻塞状态,让出CPU的使用权。
  • 任务Task_LED2在for循环里执行完功能代码后,也调用函数vTaskDelay()延时500ms,并且进入阻塞状态,让出CPU的使用权。
  • 任务Task_LED1和Task_LED2大部分时间处于阻塞状态,由系统的空闲任务获得CPU的使用权。

        在图示的运行时序下,用户的两个任务Task_LED1和Task_LED2大部分时间处于阻塞状态,由系统的空闲任务获得CPU的使用权。这样可以降低CPU的负荷,使任务的调度更及时。一般的FreeRTOS嵌入式系统中,CPU的大部分时间就是由空闲任务占据的,如果将参数configUSE_TICKLESS_IDLE配置为1,还可以实现系统的低功耗。 

5)使用vTaskDelayUntil()函数

        如果要在任务函数的循环中实现严格的周期性,就应该使用函数vTaskDelayUntil()。我们对上一步的程序稍作修改,在两个任务函数中使用函数vTaskDelayUntil()。修改后的任务函数代码如下:

void AppTask_LED1(void *argument)
{
  /* USER CODE BEGIN AppTask_LED1 */
  /* Infinite loop */
  TickType_t ticks1 = pdMS_TO_TICKS(1000);				//时间(ms)转换为节拍数(ticks)
  TickType_t previousWakeTime = xTaskGetTickCount();
  for(;;)
  {
	  HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_6);			//PA6=LED1
	  vTaskDelayUntil(&previousWakeTime, ticks1);		//Cycle period1000ms
	  //vTaskDelay(ticks1);
	  //HAL_Delay(1000);
	  //osDelay(1);
  }
  /* USER CODE END AppTask_LED1 */
}


void AppTask_LED2(void *argument)
{
  /* USER CODE BEGIN AppTask_LED2 */
  /* Infinite loop */
  TickType_t ticks2 = pdMS_TO_TICKS(500);
  TickType_t previousWakeTime = xTaskGetTickCount();
  for(;;)
  {
	  HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_4);			//PA4=LED2
	  vTaskDelayUntil(&previousWakeTime, ticks2);		//Cycle period 500ms
	  //vTaskDelay(ticks2);
	  //HAL_Delay(500);
	  //osDelay(1);
  }
  /* USER CODE END AppTask_LED2 */
}

        使用函数vTaskDelayUntil()延时的时间是从任务上次转入运行状态开始的绝对时间,例如,在任务Task_LED1中执行的延时语句如下:

TickType_t previousWakeTime = xTaskGetTickCount();
vTaskDelayUntil(&previousWakeTime,ticks1);    //循环周期为1000ms

        第一次执行时,我们需要通过函数xTaskGetTickCount()获取嘀嗒信号的当前计数值,作为previousWakeTime的初值。执行上面的语句,表示从previousWakeTime值开始延时ticks1个节拍,函数内会自动更新变量previousWakeTime的值,也会自动处理嘀嗒信号计数值溢出的情况。

        上述程序运行时,3个任务的运行时序如下图,可保证任务Task_LED1的主循环周期精确为1000ms,任务Task_LED2的主循环周期精确为500ms。注意,vTaskDelayUntil()的延时时间应该明显大于任务主循环内代码的执行时间,以及可能被其他任务打断而延迟的时间。