FreeRTOS教程2 任务管理

1、准备材料

正点原子stm32f407探索者开发板V2.4

STM32CubeMX软件(Version 6.10.0

Keil µVision5 IDE(MDK-Arm

野火DAP仿真器

XCOM V2.6串口助手

一个滑动变阻器

逻辑分析仪nanoDLA

2、学习目标

本文主要学习FreeRTOS任务管理的相关知识,包括FreeRTOS创建/删除任务、任务状态、任务优先级、延时函数、空闲任务和任务调度方法等知识

3、前提知识

3.1、任务函数长什么样?

FreeRTOS中任务是一个永远不会退出的 C 函数,因此通常是作为无限循环实现,其不允许以任何方式从实现函数中返回,如果一个任务不再需要,可以显示的将其删除,其典型的任务函数结构如下所示

/**
  * @brief  任务函数
  * @retval None
  */
void ATaskFunction(void *pvParameters)  
{
	/*初始化或定义任务需要使用的变量*/
	int iVariable = 0;
	
	for(;;)
	{
		/*完成任务的功能代码*/
	
	}
	/*跳出循环的任务需要被删除*/
	vTaskDelete(NULL);
}

3.2、创建一个任务

FreeRTOS提供了三个函数来创建任务(其中名为 xTaskCreateRestricted() 的函数仅供高级用户使用,并且仅与 FreeRTOS MPU 端口相关,故此处不涉及该函数),具体的函数声明如下所示

/**
  * @brief  动态分配内存创建任务函数
  * @param  pvTaskCode:任务函数
  * @param  pcName:任务名称,单纯用于辅助调试
  * @param  usStackDepth:任务栈深度,单位为字(word)
  * @param  pvParameters:任务参数
  * @param  uxPriority:任务优先级
  * @param  pxCreatedTask:任务句柄,可通过该句柄进行删除/挂起任务等操作
  * @retval pdTRUE:创建成功,errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY:内存不足创建失败
  */
BaseType_t xTaskCreate(TaskFunction_t pvTaskCode,
					   const char * const pcName,
					   unsigned short usStackDepth,
					   void *pvParameters,
					   UBaseType_t uxPriority,
					   TaskHandle_t *pxCreatedTask);

/**
  * @brief  静态分配内存创建任务函数
  * @param  pvTaskCode:任务函数
  * @param  pcName:任务名称
  * @param  usStackDepth:任务栈深度,单位为字(word)
  * @param  pvParameters:任务参数
  * @param  uxPriority:任务优先级
  * @param  puxStackBuffer:任务栈空间数组
  * @param  pxTaskBuffer:任务控制块存储空间
  * @retval 创建成功的任务句柄
  */
TaskHandle_t xTaskCreateStatic(TaskFunction_t pvTaskCode,
							   const char * const pcName,
							   uint32_t ulStackDepth,
							   void *pvParameters,
							   UBaseType_t uxPriority,
							   StackType_t * const puxStackBuffer,
							   StaticTask_t * const pxTaskBuffer);

上述两个任务创建函数有如下几点不同,之后如无特殊需要将一律使用动态分配内存的方式创建任务或其他实例

  1. xTaskCreateStatic 创建任务时需要用户指定任务栈空间数组和任务控制块的存储空间,而 xTaskCreate 创建任务其存储空间被动态分配,无需用户指定
  2. xTaskCreateStatic 创建任务函数的返回值为成功创建的任务句柄,而 xTaskCreate 成功创建任务的句柄需要以参数形式提前定义并指定,同时其函数返回值仅表示任务创建成功/失败

3.3、任务都有哪些状态?

在FreeRTOS应用中往往会存在多个任务,但是对于单核的STM32等单片机而言,同一时刻只会有一个任务运行,因此对于一个任务来说要么其处于运行状态,要么处于非运行状态,而对于任务的非运行状态又细分为以下三种状态(尚不考虑被删除的任务)

① 阻塞状态:一个任务正在等待某个事件发生,调用可以进入阻塞状态的API函数可以使任务进入阻塞状态,等待的事件通常为以下两种事件

  1. 时间相关事件:如 vTaskDelay() 或 vTaskDelayUntil(),处于运行状态的任务调用这两个延时函数就会进入阻塞状态,等待延时时间结束后会进入就绪状态,待任务调度后又会进入运行状态

  2. 同步相关事件:例如尝试进行读取空队列、尝试写入满队列、尝试获取尚未被释放的二值信号量等等操作都会使任务进入阻塞状态,这些同步事件会在后面的章节详细讲解

② 挂起状态:一个任务暂时脱离调度器的调度,挂起状态的任务对调度器来说不可见

  1. 让一个任务进入挂起状态的唯一方法是调用 vTaskSuspend() API函数
  2. 将一个任务从挂起状态唤醒的唯一方法是调用 vTaskResume() API函数(在中断中应调用挂起唤醒的中断安全版本vTaskResumeFromISR() API函数)
/**
  * @brief  挂起某个任务
  * @param  pxTaskToSuspend:被挂起的任务的句柄,通过传入NULL来挂起自身
  * @retval None
  */
void vTaskSuspend(TaskHandle_t pxTaskToSuspend);

/**
  * @brief  将某个任务从挂起状态恢复
  * @param  pxTaskToResume:正在恢复的任务的句柄
  * @retval None
  */
void vTaskResume(TaskHandle_t pxTaskToResume);

/**
  * @brief  vTaskResume的中断安全版本
  * @param  pxTaskToResume:正在恢复的任务的句柄
  * @retval 返回退出中断之前是否需要进行上下文切换(pdTRUE/pdFALSE)
  */
BaseType_t xTaskResumeFromISR(TaskHandle_t pxTaskToResume);

③ 就绪状态:一个任务处于未运行状态但是既没有阻塞也没有挂起,处于就绪状态的任务当前尚未运行,但随时可以进入运行状态

下图为一个任务在四种不同状态(阻塞状态、挂起状态、就绪状态和运行状态)下完整的状态转移机制图 (注释1)

在程序中可以使用 eTaskGetState() API 函数利用任务的句柄查询任务当前处于什么状态,任务的状态由枚举类型 eTaskState 表示,具体如下所示

/**
  * @brief  查询一个任务当前处于什么状态
  * @param  pxTask:要查询任务状态的任务句柄,NULL查询自己
  * @retval 任务状态的枚举类型
  */
eTaskState eTaskGetState(TaskHandle_t pxTask);

/*任务状态枚举类型返回值*/
typedef enum
{
	eRunning = 0,	/* 任务正在查询自身的状态,因此肯定是运行状态 */
	eReady,			/* 就绪状态 */
	eBlocked,		/* 阻塞状态 */
	eSuspended,		/* 挂起状态 */
	eDeleted,		/* 正在查询的任务已被删除,但其 TCB 尚未释放 */
	eInvalid		/* 无效状态 */
} eTaskState;

3.4、任务优先级

FreeRTOS每个任务都拥有一个自己的优先级,该优先级可以在创建任务时以参数的形式传入,也可以在需要修改时通过 vTaskPrioritySet() API函数动态设置优先级

任务优先级的设置范围为1~(configMAX_PRIORITIES-1),任务设置的优先级数字越大优先级越高,设置优先级时可以直接使用数字进行设置,也可以使用内核定义好的枚举类型设置,另外可以使用 uxTaskPriorityGet() API函数获取任务的优先级,如下所示列出了部分优先级枚举类型定义

/*cmsis_os2.c中的定义*/
typedef enum {
  osPriorityNone          =  0,         ///< No priority (not initialized).
  osPriorityIdle          =  1,         ///< Reserved for Idle thread.
  osPriorityLow           =  8,         ///< Priority: low
  osPriorityNormal        = 24,         ///< Priority: normal
  osPriorityAboveNormal   = 32,         ///< Priority: above normal
  osPriorityHigh          = 40,         ///< Priority: high
  osPriorityRealtime      = 48,         ///< Priority: realtime
  osPriorityISR           = 56,         ///< Reserved for ISR deferred thread.
} osPriority_t;

任务的优先级主要决定了在任务调度时,多个任务同时处于就绪态时应该让哪个任务先执行,FreeRTOS调度器则保证了任何时刻总是在所有可运行的任务中选择具有最高优先级的任务,并将其进入运行态,如下所述为上述提到的两个设置和获取任务优先级函数的具体声明

/**
  * @brief  设置任务优先级
  * @param  pxTask:要修改优先级的任务句柄,通过NULL改变任务自身优先级
  * @param  uxNewPriority:要修改的任务优先级
  * @retval None
  */
void vTaskPrioritySet(TaskHandle_t pxTask, UBaseType_t uxNewPriority);

/**
  * @brief  获取任务优先级
  * @param  pxTask:要获取任务优先级的句柄,通过NULL获取任务自身优先级
  * @retval 任务优先级
  */
UBaseType_t uxTaskPriorityGet(TaskHandle_t pxTask);

3.5、延时函数

学习STM32时经常会使用到HAL库的延时函数HAL_Delay(),FreeRTOS也同样提供了vTaskDelay() 和 vTaskDelayUntil() 两个 API延时函数,如下所述

/**
  * @brief  延时函数
  * @param  xTicksToDelay:延迟多少个心跳周期
  * @retval None
  */
void vTaskDelay(TickType_t xTicksToDelay);

/**
  * @brief  延时函数,用于实现一个任务固定执行周期
  * @param  pxPreviousWakeTime:保存任务上一次离开阻塞态的时刻
  * @param  xTimeIncrement:指定任务执行多少心跳周期
  * @retval None
  */
void vTaskDelayUntil(TickType_t *pxPreviousWakeTime, TickType_t xTimeIncrement);

上述两个延时函数与 HAL_Delay() 作用都是延时,但是FreeRTOS延时函数 API 可以让任务进入阻塞状态,而 HAL_Delay() 不具有该功能,因此如果一个任务需要使用延时,一般应该使用 FreeRTOS 的 API 函数让任务进入阻塞状态等待延时结束,处于阻塞状态的任务便可以让出内核处理其他任务

对于 vTaskDelayUntil() API函数的 pxPreviousWakeTime 参数一般通过 xTaskGetTickCount() API函数获取,该函数作用为获取滴答信号当前计数值,具体如下所述

/**
  * @brief  获取滴答信号当前计数值
  * @retval 滴答信号当前计数值
  */
TickType_t xTaskGetTickCount(void);

/**
  * @brief  获取滴答信号当前计数值的中断安全版本
  */
TickType_t xTaskGetTickCountFromISR(void);

/**
  * @brief  周期任务函数结构
  * @retval None
  */
void APeriodTaskFunction(void *pvParameters)  
{
	/*获取任务创建后的滴答信号计数值*/
	TickType_t pxPreviousWakeTime = xTaskGetTickCount();
	
	for(;;)
	{
		/*完成任务的功能代码*/
		
		/*任务周期500ms*/
		vTaskDelayUntil(&pxPreviousWakeTime, pdMS_TO_TICKS(500));
	}
	/*跳出循环的任务需要被删除*/
	vTaskDelete(NULL);
}

当一个任务因为延时函数或者其他同步事件进入阻塞状态后,可以通过 xTaskAbortDelay() API 函数终止任务的阻塞状态,即使事件任务等待尚未发生,或者任务进入时指定的超时时间阻塞状态尚未过去,都会使其进入就绪状态,具体函数描述如下所述

/**
  * @brief  终止任务延时,退出阻塞状态
  * @param  xTask:操作的任务句柄
  * @retval pdPASS:任务成功从阻塞状态中删除,pdFALSE:任务不属于阻塞状态导致删除失败
  */
BaseType_t xTaskAbortDelay(TaskHandle_t xTask);

3.6、为什么会有空闲任务?

3.6.1、概述

FreeRTOS 调度器决定在任何时刻处理器必须保持有一个任务运行,当用户创建的所有任务都处于阻塞状态不能运行时,空闲任务就会被运行

空闲任务是一个优先级为0(最低优先级)的非常短小的循环,其优先级为 0 保证了不会影响到具有更高优先级的任务进入运行态,一旦有更高优先级的任务进入就绪态,空闲任务就会立刻切出运行态

空闲任务何时被创建?当调用 vTaskStartScheduler() 启动调度器时就会自动创建一个空闲任务,如下图所示,另外空闲任务还负责将分配给已删除任务的内存释放掉

3.6.2、空闲任务钩子函数

空闲任务有一个钩子函数,可以通过配置 configUSE_IDLE_HOOK 参数为 Enable 启动空闲任务的钩子函数,如果是使用STM32CubeMX软件生成的工程则会自动生成空闲任务钩子函数,当调度器调度内核进入空闲任务时就会调用钩子函数

通常空闲任务钩子函数主要被用于下方函数体内部注释列举的几种情况,如下所述为空闲任务钩子函数典型的任务函数结构

/**
  * @brief  空闲任务钩子函数
  * @retval NULL
  */
void vApplicationIdleHook(void)
{
	/*
		1.执行低优先级,或后台需要不停处理的功能代码
		2.测试系统处理裕量(内核执行空闲任务时间越长表示内核越空闲)
		3.将处理器配置到低功耗模式(Tickless模式)
	*/
}

除了空闲任务钩子函数外,FreeRTOS提供了一系列钩子函数供用户选择使用,具体读者可查看FreeRTOS教程1 基础知识文章“4.1.3、外设参数配置”小节参数列表中的“Hook function related definitions”,使用之前只需在STM32CubeMX中启用相关参数,然后在生成的代码中找到钩子函数使用即可

3.7、删除任务

一个任务不再需要时,需要显示调用 vTaskDelete() API函数将任务删除,该函数需要传入要删除任务的句柄这个参数(传入NULL时表示删除自己),函数声明如下所述

/**
  * @brief  任务删除函数
  * @param  pxTaskToDelete:要删除的任务句柄,NULL表示删除自己
  * @retval None
  */
void vTaskDelete(TaskHandle_t pxTaskToDelete);

3.8、任务调度方法

调度器保证了总是在所有可运行的任务中选择具有最高优先级的任务,并将其进入运行态,根据 configUSE_PREEMPTION (使用抢占调度器) 和 configUSE_TIME_SLICING (使用时间片轮询) 两个参数的不同,FreeRTOS涉及三种不同的调度方法

  1. 时间片轮询的抢占式调度方法(configUSE_PREEMPTION=1,configUSE_TIME_SLICING=1)
  2. 不用时间片轮询的抢占式调度方法(configUSE_PREEMPTION=1,configUSE_TIME_SLICING=0)
  3. 协作式调度方法(configUSE_PREEMPTION=0)

本文只介绍抢占式调度方法(后续所有文章全部采用时间片轮询的抢占式调度方法),不涉及协作式的调度方法

什么是时间片?

FreeRTOS基础时钟的一个定时周期称为一个时间片,所以其长度由 configTICK_RATE_HZ 参数决定,默认情况下为1000HZ(也即1ms)

对于时间片轮询的抢占式调度方法,其在任务调度过程中一般满足以下两点要求

  1. 高优先级的任务可以抢占低优先级的任务
  2. 同等优先级的任务根据时间片轮流执行

对于不用时间片轮询的抢占式调度方法,其在任务调度过程中一般满足以下两点要求

  1. 高优先级的任务同样可以抢占低优先级的任务
  2. 同等优先级的任务不会按照时间片轮流执行,可能出现任务间占用处理器时间相差很大的情况

任务调度主要是由任务调度器 scheduler 负责,其由 FreeRTOS 内核管理,用户一般无需控制任务调度器,但是 FreeRTOS 也给用户提供了启动、停止、挂起和恢复三个常见的控制 scheduler 的 API 函数,具体如下所述

/**
  * @brief  启动调度器
  * @retval None
  */
void vTaskStartScheduler(void);

/**
  * @brief  停止调度器
  * @retval None
  */
void vTaskEndScheduler(void);

/**
  * @brief  挂起调度器
  * @retval None
  */
void vTaskSuspendAll(void);

/**
  * @brief  恢复调度器
  * @retval 返回是否会导致发生挂起的上下文切换(pdTRUE/pdFALSE)
  */
BaseType_t xTaskResumeAll(void);

除了任务被时间片轮询切换或者高优先级抢占发生切换两种常见的调度方式外,还有其他的调度方式,比如任务自愿让出处理器给其他任务使用等函数,这些函数将在后续 “中断管理” 章节中被详细介绍,这里简单了解即可,如下所述

/**
  * @brief  让位于另一项同等优先级的任务
  * @retval None
  */
void taskYIELD(void);

/**
  * @brief  ISR 退出时是否执行上下文切换(汇编)
  * @param  xHigherPriorityTaskWoken:pdFASLE不请求上下文切换,反之请求上下文切换
  * @retval None
  */
portEND_SWITCHING_ISR(xHigherPriorityTaskWoken);

/**
  * @brief  ISR 退出时是否执行上下文切换(C语言)
  * @param  xHigherPriorityTaskWoken:pdFASLE不请求上下文切换,反之请求上下文切换
  * @retval None
  */
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);

3.9、工具函数

任务相关的实用工具函数较多,官方网站上一共列出了23个 API 函数,这里笔者仅简单介绍一些可能常用的 API 函数,如果读者有其他希望了解的函数,可以自行前往 FreeRTOS/API 引用/任务实用程序 中了解

另外读者应注意,如果要使用下方某些函数则可能需要在CubeMX的FREERTOS/Include parameters参数配置页面中勾选启用对应的API函数,具体可查看FreeRTOS教程1 基础知识文章"4.1.3、外设参数配置"小节下方的参数表格

3.9.1、获取任务信息

/**
  * @brief  获取一个任务的信息,需启用参数configUSE_TRACE_FACILITY(默认启用)
  * @param  xTask:需要查询的任务句柄,NULL查询自己
  * @param  pxTaskStatus:用于存储任务状态信息的TaskStatus_t结构体指针
  * @param  xGetFreeStackSpace:是否返回栈空间高水位值
  * @param  eState:指定查询信息时任务的状态,设置为eInvalid将自动获取任务状态
  * @retval None
  */
void vTaskGetInfo(TaskHandle_t xTask,
				  TaskStatus_t *pxTaskStatus,
				  BaseType_t xGetFreeStackSpace,
				  eTaskState eState);

/**
  * @brief  获取当前任务句柄
  * @retval 返回当前任务句柄
  */
TaskHandle_t xTaskGetCurrentTaskHandle(void);

/**
  * @brief  获取任务句柄(运行时间较长,不宜大量使用)
  * @param  pcNameToQuery:要获取任务句柄的任务名称字符串
  * @retval 返回指定查询任务的句柄
  */
TaskHandle_t xTaskGetHandle(const char *pcNameToQuery);

/**
  * @brief  获取空闲任务句柄
  * @注意:需要设置 INCLUDE_xTaskGetIdleTaskHandle 为1,在CubeMX中不可调,需自行定义
  * @retval 返回空闲任务句柄
  */
TaskHandle_t xTaskGetIdleTaskHandle(void);

/**
  * @brief  获取一个任务的高水位值(任务栈空间最少可用剩余空间大小,单位为字(word))
  * @param  xTask:要获取高水位值任务的句柄,NULL查询自己
  * @retval 
  */
UBaseType_t uxTaskGetStackHighWaterMark(TaskHandle_t xTask);

/**
  * @brief  获取一个任务的任务名称字符串
  * @param  xTaskToQuery:要获取名称字符串的任务的句柄,NULL查询自己
  * @retval 返回一个任务的任务名称字符串
  */
char* pcTaskGetName(TaskHandle_t xTaskToQuery);

3.9.2、获取内核信息

/**
  * @brief  获取系统内所有任务状态,为每个任务返回一个TaskStatus_t结构体数组
  * @param  pxTaskStatusArray:数组的指针,数组每个成员都是TaskStatus_t类型,用于存储获取到的信息
  * @param  uxArraySize:设置数组pxTaskStatusArray的成员个数
  * @param  pulTotalRunTime:返回FreeRTOS运行后总的运行时间,NULL表示不返回该数据
  * @retval 返回实际获取的任务信息条数
  */
UBaseType_t uxTaskGetSystemState(TaskStatus_t * const pxTaskStatusArray,
								 const UBaseType_t uxArraySize,
								 unsigned long * const pulTotalRunTime);

/**
  * @brief  返回调度器状态
  * @retval 0:被挂起,1:未启动,2:正在运行
  */
BaseType_t xTaskGetSchedulerState(void);

/**
  * @brief  获取内核当前管理的任务总数
  * @retval 返回内核当前管理的任务总数
  */
UBaseType_t uxTaskGetNumberOfTasks(void);

/**
  * @brief  获取内核中所有任务的字符串列表信息
  * @param  pcWriteBuffer:字符数组指针,用于存储获取的字符串信息
  * @retval None
  */
void vTaskList(char *pcWriteBuffer);

3.9.3、其他函数

/**
  * @brief  获取一个任务的标签值
  * @param  xTask:要获取任务标签值的任务句柄,NULL表示获取自己的标签值
  * @retval 返回任务的标签值
  */
TaskHookFunction_t xTaskGetApplicationTaskTag(TaskHandle_t xTask); 

/**
  * @brief  获取一个任务的标签值的中断安全版本函数
  */
TaskHookFunction_t xTaskGetApplicationTaskTagFromISR(TaskHandle_t xTask);

/**
  * @brief  设置一个任务的标签值,标签值保存在任务控制块中
  * @param  xTask:要设置标签值的任务的句柄,NULL表示设置自己
  * @param  pxTagValue:要设置的标签值
  * @retval None
  */
void vTaskSetApplicationTaskTag(TaskHandle_t xTask, 
								TaskHookFunction_t pxTagValue);

4、实验一:尝试任务基本操作

4.1、实验目的

  1. 创建一个任务 TASK_GREEN_LED ,每 100ms 改变一次 GREEN_LED 的状态
  2. 使用静态内存分配创建一个任务 TASK_RED_LED ,每 500ms 改变一次 RED_LED 的状态
  3. 创建一个任务 TASK_KEY_SCAN ,用于实现按键扫描功能,当开发板上的 KEY2 按键按下时删除任务 TASK_GREEN_LED ,当开发板上的 KEY1 按键按下时挂起任务 TASK_RED_LED ,当开发板上的 KEY0 按键按下时恢复任务 TASK_RED_LED

4.2、CubeMX相关配置

首先读者应按照FreeRTOS教程1 基础知识章节配置一个可以正常编译通过的 FreeRTOS 空工程,然后在此空工程的基础上增加本实验所提出的要求

本实验需要初始化开发板上 GREEN_LED 和 RED_LED 两个 LED 灯作为显示,具体配置步骤请阅读“STM32CubeMX教程2 GPIO输出 - 点亮LED灯”,注意虽开发板不同但配置原理一致,如下图所示

本实验需要初始化开发板上 KEY2、KEY1 和 KEY0 用户按键做普通输入,具体配置步骤请阅读“STM32CubeMX教程3 GPIO输入 - 按键响应”,注意虽开发板不同但配置原理一致,如下图所示

本实验需要初始化 USART1 作为输出信息渠道,具体配置步骤请阅读“STM32CubeMX教程9 USART/UART 异步通信”,如下图所示

单击 Middleware and Software Packs/FREERTOS ,在 Configuration 中单击 Tasks and Queues 选项卡,首先双击默认任务修改其参数,然后单击 Add 按钮按要求增加另外两个任务,由于按键扫描任务比闪烁 LED 灯任务重要,因此将其优先级配置为稍高,配置好的界面如下图所示

假设之前配置空工程时已经配置好了 Clock Configuration 和 Project Manager 两个页面,接下来直接单击 GENERATE CODE 按钮生成工程代码即可

4.3、添加其他必要代码

按照 “STM32CubeMX教程9 USART/UART 异步通信” 实验 “6、串口printf重定向” 小节增加串口 printf 重定向代码,具体不再赘述

打开 freertos.c 文件夹,按要求增加三个任务的实现代码,其中阻塞延时函数 osDelay() 为 vTaskDelay() 函数的包装版本,具体源代码如下所述

/*GREEN_LED闪烁任务函数*/
void TASK_GREEN_LED(void *argument)
{
  /* USER CODE BEGIN TASK_GREEN_LED */
  /* Infinite loop */
  for(;;)
  {
	//每隔100ms闪烁一次GREEN_LED
	HAL_GPIO_TogglePin(GREEN_LED_GPIO_Port, GREEN_LED_Pin);
	printf("TASK_GREEN_LED, GREEN LED BLINK!\r\n");
    osDelay(pdMS_TO_TICKS(100));
  }
  /* USER CODE END TASK_GREEN_LED */
}

/*RED_LED闪烁任务函数*/
void TASK_RED_LED(void *argument)
{
  /* USER CODE BEGIN TASK_RED_LED */
  /* Infinite loop */
  for(;;)
  {
	//每隔500ms闪烁一次RED_LED
	HAL_GPIO_TogglePin(RED_LED_GPIO_Port, RED_LED_Pin);
	printf("TASK_RED_LED, RED LED BLINK!\r\n");
    osDelay(pdMS_TO_TICKS(500));
  }
  /* USER CODE END TASK_RED_LED */
}

/*KEY_SCAN按键扫描任务函数*/
void TASK_KEY_SCAN(void *argument)
{
  /* USER CODE BEGIN TASK_KEY_SCAN */
  uint8_t key_value = 0;
  /* Infinite loop */
  for(;;)
  {
	key_value = 0;
	//按键KEY2按下
	if(HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin) == GPIO_PIN_RESET)
		key_value = 3;
	//按键KEY1按下
	if(HAL_GPIO_ReadPin(KEY1_GPIO_Port,KEY1_Pin) == GPIO_PIN_RESET)
		key_value = 2;
	//按键KEY0按下
	if(HAL_GPIO_ReadPin(KEY0_GPIO_Port,KEY0_Pin) == GPIO_PIN_RESET)
		key_value = 1;
	
	if(key_value != 0)
	{
		if(key_value == 3)
		{
			printf("\r\n\r\nKEY2 PRESSED, Delete TASK_GREEN_LED!\r\n\r\n");
			//此处可使用vTaskDelete(task_GREEN_LEDHandle),但要注意不能重复删除句柄
			osThreadTerminate(task_GREEN_LEDHandle);
		}
		else if(key_value == 2)
		{
			printf("\r\n\r\nKEY1 PRESSED, Suspend TASK_RED_LED!\r\n\r\n");
			vTaskSuspend(task_RED_LEDHandle);
		}
		else if(key_value == 1)
		{
			printf("\r\n\r\nKEY0 PRESSED, Resume TASK_RED_LED!\r\n\r\n");
			vTaskResume(task_RED_LEDHandle);
		}
		//有按键按下就进行按键消抖
		osDelay(300);
	}
	else
		osDelay(10);
  }
  /* USER CODE END TASK_KEY_SCAN */
}

当实现三个任务的函数体之后就不需要其他任何操作了,因为任务的创建、调用等工作的程序代码 STM32CubeMX 软件已经自动生成了,这里为方便初学者理解做一下简单介绍,之后便不再重复介绍

打开工程项目中 main.c 文件,我们可以发现在主函数 main() 中调用了 MX_FREERTOS_Init() 函数,该函数中已经自动创建了我们在 STM32CubeMX 软件中创建的三个任务,其中 osThreadNew() 函数为 xTaskCreate() / xTaskCreateStatic() 的包装函数,如下图所示

4.4、烧录验证

烧录程序,打开串口助手,可以发现串口上源源不断地输出 TASK_GREEN_LED 和 TASK_RED_LED 运行的提示,每输出5次 TASK_GREEN_LED 然后就会输出1次 TASK_RED_LED,同时开发板上的红色和绿色LED灯也不停闪烁

当按下开发板上的 KEY2 按键,串口提示删除 TASK_GREEN_LED ,之后会发现只有 TASK_RED_LED 运行的串口输出;当按下开发板上的 KEY1 按键,串口提示挂起 TASK_RED_LED,之后 TASK_RED_LED 会停止执行;最后按下开发板上的 KEY0 按键,串口提示恢复 TASK_RED_LED,TASK_RED_LED 恢复运行

上述整个过程串口输出信息如下图所示

如果不操作按键,其任务流程应该如下所述

  1. 在 t1 时刻,调度器刚刚开始运行,其浏览任务列表发现有两个进入就绪态的任务,即刚刚创建好的任务 TASK_GREEN_LED 和 TASK_RED_LED,由于两个任务优先级均相同,但是 TASK_GREEN_LED 先建立,因此调度器决定先执行该任务,TASK_GREEN_LED 调用了延时函数 osDelay() 让任务进入阻塞状态,然后调度器发现还有就绪的任务,于是切换到任务 TASK_RED_LED ,同理执行到延时函数让任务进入了阻塞状态
  2. 在 t2 时刻,调度器发现任务列表里已经没有就绪的任务(两个任务都进入了阻塞状态),于是选择执行空闲任务
  3. 在 t3 时刻,任务 TASK_GREEN_LED 延时结束,从阻塞状态进入就绪状态,由于任务 TASK_GREEN_LED 优先级高于空闲任务,因此该任务抢占空闲任务进入运行状态,执行完函数体再次遇到延时函数 osDelay() 让任务进入阻塞状态,然后不断重复步骤3的过程
  4. 在 t7 时刻,任务 TASK_GREEN_LED 和 TASK_RED_LED 同时延时结束,从阻塞状态进入就绪状态,然后调度器重复步骤1的过程

上述任务流程图具体如下图所示

4.5、探讨延时函数特性

如果将任务 TASK_GREEN_LED 和 TASK_RED_LED 函数体内的延时函数 osDelay() 更改为 HAL 库的延时函数 HAL_Delay() 函数 ,根据“3.5、延时函数”小节内容可知,HAL_Delay() 函数不会使任务进入阻塞状态

值得注意的是这两个任务目前优先级相同,均为 osPriorityNormal ,因此根据 “3.8、任务调度方法” 小节内容可知,采用时间片轮询的抢占式调度方式对于同等优先级的任务采用时间片轮询执行,所以如果不操作按键,只修改延时函数后的任务流程应该如下图所述

从图上可以看出,由于任务不会进入阻塞状态,因此两个同等优先级的任务会按照时间片轮流执行,而空闲函数则不会得到执行

4.6、任务被饿死了

接着上面所述,假设将任务 TASK_RED_LED 的优先级修改为 osPriorityBelowNormal,该优先级低于任务 TASK_GREEN_LED 的优先级,然后保持延时函数为 HAL_Delay() 函数不变,并且不操作按键,其任务流程应该如下所述

从图上可以看出,由于任务不会进入阻塞状态,因此高优先级的任务会一直得到执行,从而将低优先级的任务饿死了,所以在实际使用中,任务应该使用能够进入阻塞状态的延时函数

4.7、使用 vTaskDelayUntil()

根据 ”4.4、烧录验证“ 小节任务流程图可知,对任务延时并不能达到让任务以固定周期执行,如果读者希望能够让一个任务严格按照固定周期执行,可以使用 vTaskDelayUntil() 函数实现,修改任务函数如下所示

/*GREEN_LED闪烁任务函数*/
void TASK_GREEN_LED(void *argument)
{
  /* USER CODE BEGIN TASK_GREEN_LED */
  TickType_t previousWakeTime = xTaskGetTickCount();
  /* Infinite loop */
  for(;;)
  {
	//进入临界段
	taskENTER_CRITICAL();
	//每隔100ms闪烁一次GREEN_LED
	HAL_GPIO_TogglePin(GREEN_LED_GPIO_Port, GREEN_LED_Pin);
	printf("TASK_GREEN_LED, GREEN LED BLINK!\r\n");
	//退出临界段
	taskEXIT_CRITICAL();
    //也可使用osDelayUntil(pdMS_TO_TICKS(100));
    vTaskDelayUntil(&previousWakeTime, pdMS_TO_TICKS(100));
  }
  /* USER CODE END TASK_GREEN_LED */
}

/*RED_LED闪烁任务函数*/
void TASK_RED_LED(void *argument)
{
  /* USER CODE BEGIN TASK_RED_LED */
  TickType_t previousWakeTime = xTaskGetTickCount();
  /* Infinite loop */
  for(;;)
  {
	//进入临界段
	taskENTER_CRITICAL();
	//每隔500ms闪烁一次RED_LED
	HAL_GPIO_TogglePin(RED_LED_GPIO_Port, RED_LED_Pin);
	printf("TASK_RED_LED, RED LED BLINK!\r\n");
	//退出临界段
	taskEXIT_CRITICAL();
	//也可使用osDelayUntil(pdMS_TO_TICKS(500));
	vTaskDelayUntil(&previousWakeTime, pdMS_TO_TICKS(100));
  }
  /* USER CODE END TASK_RED_LED */
}

由于 TASK_GREEN_LED 100ms 执行一次,TASK_RED_LED 500ms 执行一次,所以存在同时执行的情况,可能会导致串口输出数据出错,因此这里使用了临界段保护串口输出程序,临界段相关知识将在后续FreeRTOS教程3 中断管理文章中介绍到

使用逻辑分析仪采集红色和绿色两个 LED 灯引脚电平变化,可以发现其执行周期与设置一致,误差可以接受,具体如下图所示

与单纯使用延时函数的程序做对比,可以发现只使用延时函数的任务执行周期误差较大,无法做到固定周期运行,具体如下图所示

5、实验二:获取任务信息

5.1、实验目的

  1. 创建任务 TASK_ADC,该任务通过 ADC1 的 IN5 通道周期采集电位器的电压值,并通过串口输出采集到的 ADC 值;
  2. 创建任务 TASK_KEY_SCAN ,当按键 KEY2 按下时根据任务句柄获取单个任务的信息并通过串口输出到串口助手上;当按键 KEY1 按下时获取每个任务的高水位值并通过串口输出到串口助手上;当按键 KEY0 按下时获取系统任务列表并通过串口输出到串口助手上;

5.2、CubeMX相关配置

同样读者应按照FreeRTOS教程1 基础知识章节配置一个可以正常编译通过的FreeRTOS空工程,然后在此空工程的基础上增加本实验所提出的要求

本实验需要初始化开发板上 KEY2、KEY1 和 KEY0 用户按键做普通输入,具体配置步骤请阅读“STM32CubeMX教程3 GPIO输入 - 按键响应”,注意虽开发板不同但配置原理一致,如下图所示

本实验需要初始化 USART1 作为输出信息渠道,具体配置步骤请阅读“STM32CubeMX教程9 USART/UART 异步通信”,如下图所示

单击 Analog 中的 ADC1 ,勾选 IN5 ,在下方的参数配置中仅将 IN5 的采样时间修改为 15Cycles 即可,对 ADC 单通道采集感兴趣的读者可以阅读“STM32CubeMX教程13 ADC - 单通道转换”实验,如下图所示

单击 Middleware and Software Packs/FREERTOS ,在 Configuration 中单击 Tasks and Queues 选项卡,首先双击默认任务修改其参数,然后单击 Add 按钮按要求增加另外一个任务,配置好的界面如下图所示

由于需要使用到一些获取信息的函数,有些默认情况下并不能使用,需要用户配置参数将其加入到编译中,因此需要做以下两个操作

  1. 在 Config parameters 中启用 USE_TRACE_FACILITY 参数和 USE_STATS_FORMATTING_FUNCTIONS 参数,目的是为了使用 vTaskList() API 函数
  2. 在生成的工程代码中找到 FreeRTOSConfig.h 文件,在用户代码区域添加下述代码,目的是为了使用获取空闲任务句柄 xTaskGetIdleTaskHandle() API 函数
#define INCLUDE_xTaskGetIdleTaskHandle     1

配置 Clock Configuration 和 Project Manager 两个页面,接下来直接单击 GENERATE CODE 按钮生成工程代码即可

5.3、添加其他必要代码

首先添加串口 printf 重定向函数,不再赘述,然后打开 freertos.c 文件,添加需要使用到的 ADC 的头文件,如下所述

/*添加头文件*/
#include "adc.h"

最后根据实验目的编写程序完成 TASK_ADC 和 TASK_KEY_SCAN 两个任务,具体如下所示

/*ADC周期采集任务*/
void TASK_ADC(void *argument)
{
  /* USER CODE BEGIN TASK_ADC */
    TickType_t previousWakeTime = xTaskGetTickCount();
  /* Infinite loop */
  for(;;)
  {
	//开始临界代码段,不允许任务调度
	taskENTER_CRITICAL();
	HAL_ADC_Start(&hadc1);
	if(HAL_ADC_PollForConversion(&hadc1,200)==HAL_OK)
	{
		uint32_t val=HAL_ADC_GetValue(&hadc1);
		uint32_t Volt=(3300*val)>>12;
		printf("val:%d, Volt:%d\r\n",val,Volt);
	}
	//结束临界代码段,重新允许任务调度
	taskEXIT_CRITICAL();
	//500ms周期
	vTaskDelayUntil(&previousWakeTime, pdMS_TO_TICKS(500));
  }
  /* USER CODE END TASK_ADC */
}

/*按键扫描KEY_SCAN任务*/
void TASK_KEY_SCAN(void *argument)
{
	/* USER CODE BEGIN TASK_KEY_SCAN */
	uint8_t key_value = 0;
	TaskHandle_t taskHandle = task_ADCHandle;
	/* Infinite loop */
	for(;;)
	{
		key_value = 0;
		//按键KEY2按下
		if(HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin) == GPIO_PIN_RESET)
			key_value = 3;
		//按键KEY1按下
		if(HAL_GPIO_ReadPin(KEY1_GPIO_Port,KEY1_Pin) == GPIO_PIN_RESET)
			key_value = 2;
		//按键KEY0按下
		if(HAL_GPIO_ReadPin(KEY0_GPIO_Port,KEY0_Pin) == GPIO_PIN_RESET)
			key_value = 1;
		//如果有按键按下
		if(key_value != 0)
		{
			if(key_value == 3)
			{
				taskHandle = task_ADCHandle;
				TaskStatus_t taskInfo;
				//是否获取高水位值
				BaseType_t getFreeStackSpace = pdTRUE;  	
				//当前的状态,设置为eInvalid将自动获取任务状态
				eTaskState taskState = eInvalid; 		
				//获取任务信息					
				vTaskGetInfo(taskHandle, &taskInfo, getFreeStackSpace, taskState);	
				//开始临界代码段,不允许任务调度	
				taskENTER_CRITICAL();			
				printf("\r\n--- KEY2 PRESSED ---\r\n");
				printf("Task_Info: Show task info,Get by vTaskGetInfo();\r\n");
				printf("Task Name = %s\r\n", (uint8_t *)taskInfo.pcTaskName);
				printf("Task Number = %d\r\n", (uint16_t)taskInfo.xTaskNumber);
				printf("Task State = %d\r\n", taskInfo.eCurrentState);
				printf("Task Priority = %d\r\n", (uint8_t)taskInfo.uxCurrentPriority);
				printf("High Water Mark = %d\r\n\r\n", taskInfo.usStackHighWaterMark);
				//结束临界代码段,重新允许任务调度
				taskEXIT_CRITICAL();
			}
			else if(key_value == 2)
			{
				//开始临界代码段,不允许任务调度	
				taskENTER_CRITICAL();
				printf("\r\n--- KEY1 PRESSED ---\r\n");
				//获取空闲任务句柄
				taskHandle = xTaskGetIdleTaskHandle();
				//获取任务高水位值				
				UBaseType_t hwm = uxTaskGetStackHighWaterMark(taskHandle);
				printf("Idle Task'Stack High Water Mark = %d\r\n", (uint16_t)hwm);
				//Task_ADC的任务句柄
				taskHandle=task_ADCHandle;				
				hwm = uxTaskGetStackHighWaterMark(taskHandle);
				printf("Task_ADC'Stack High Water Mark = %d\r\n", (uint16_t)hwm);
				//Task_KEY_SCAN的任务句柄
				taskHandle=task_KEY_SCANHandle;				
				hwm = uxTaskGetStackHighWaterMark(taskHandle);
				printf("Task_KEY_SCAN'Stack High Water Mark = %d\r\n", (uint16_t)hwm);
				//获取系统任务个数
				UBaseType_t taskNum=uxTaskGetNumberOfTasks();  
				printf("There are now %d tasks in total!\r\n\r\n", (uint16_t)taskNum);
				//结束临界代码段,重新允许任务调度
				taskEXIT_CRITICAL();
			}
			else if(key_value == 1)
			{
				//开始临界代码段,不允许任务调度	
				taskENTER_CRITICAL();
				printf("\r\n--- KEY0 PRESSED ---\r\n");
				char infoBuffer[300];
				//获取任务列表
				vTaskList(infoBuffer);
				printf("%s\r\n\r\n",infoBuffer);
				//结束临界代码段,重新允许任务调度
				taskEXIT_CRITICAL();
			}
			//按键消抖
			osDelay(300);
		}
		else
			osDelay(10);
	}
	/* USER CODE END TASK_KEY_SCAN */
}

5.4、烧录验证

烧录程序,打开串口助手,可以发现串口上源源不断地输出 TASK_ADC 采集到的 ADC 值,首先从一端缓慢旋转滑动变阻器直到另一端,可以发现采集到的 ADC 值从 0 逐渐变为最大值 4095 ,表示 ADC 采集任务正常运行

按下 KEY2 按键,串口会输出任务 TASK_ADC 的相关信息,包括任务名称、任务数量、任务状态、任务优先级和任务栈高水位值等信息

按下 KEY1 按键,串口会输出空闲任务、 ADC 采集任务和按键扫描任务三个任务的高水位值,同时会输出系统中一共存在的任务数量

为什么有4个任务?

按下 KEY0 按键,串口会以列表形式输出系统中的所有任务,可以看到第4个任务是名为 Tmr Svc 的定时器守护任务,vTaskList() API 函数会将每个任务以 “Task_Name \tX\t25\t128\t2\r\n" 形式写入缓存数组中,从左往右依次表示任务名称、任务状态(X:运行,R:就绪,B:阻塞)、任务优先级、栈空间高水位置和任务编号

上述整个过程串口输出信息如下图所示

6、注释详解

注释1:图片来源 Mastering_the_FreeRTOS_Real_Time_Kernel-A_Hands-On_Tutorial_Guide.pdf

参考资料

STM32Cube高效开发教程(基础篇)

Mastering_the_FreeRTOS_Real_Time_Kernel-A_Hands-On_Tutorial_Guide.pdf

热门相关:我爸没说的那件事   18岁性感女王的性丑闻   鬣狗2020   小鸡快跑2:鸡块新时代   父子国语