目录
一、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)高优先级任务主动进入阻塞状态
一、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() | |
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的使用权。