任务(Task)是FreeRTOS中供用户使用的最核心的功能,本文将介绍任务创建与使用相关的基础内容。
本文接上篇:《FreeRTOS入门(01):基础说明与使用演示》
创建任务主要使用 xTaskCreate
这个函数:
// 创建成功会返回pdPASS(1),失败通常返回errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY(-1),即内存不足
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode, // 任务函数
const char * const pcName, // 任务名称
// 长度由FreeRTOSConfig.h中configMAX_TASK_NAME_LEN定义
const configSTACK_DEPTH_TYPE usStackDepth, // 该任务栈深度(栈大小),对于32位架构一个深度为四字节
void * const pvParameters, // 传递给任务的参数
UBaseType_t uxPriority, // 任务优先级,值越大优先级越高
// 最大值为FreeRTOSConfig.h中configMAX_PRIORITIES - 1
TaskHandle_t * const pxCreatedTask ) // 任务句柄,后续可以用过该句柄来操作任务
任务需要使用 vTaskStartScheduler();
来调度运行,通常将该行语句放在程序主循环前面,正常情况下程序将在这里无限循环。
下面是个基础的创建和使用任务的演示,使用CH32V307的FreeRTOS项目模板方式创建项目,替换 main.c
为下面内容:
#include "debug.h"
#include "FreeRTOS.h" // 引入头文件
#include "task.h" // 引入头文件
/* Task1相关参数与任务处理函数 */
void task1_task(void *pvParameters)
{
while(1)
{
printf("TickCount: %u\r\n", xTaskGetTickCount()); // 打印FreeRTOS时间刻
vTaskDelay(250); // FreeRTOS的延时函数
// FreeRTOSConfig.h中configTICK_RATE_HZ为500,即每2ms调度一次
// 所以这里的延时时间是 250 * 2 = 500ms
}
}
/* 主函数 */
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
SystemCoreClockUpdate();
Delay_Init();
USART_Printf_Init(115200);
xTaskCreate(task1_task, "task1", 256, NULL, 5, NULL); // 创建一个任务
vTaskStartScheduler(); // 任务调度,任务将在这里根据情况开始运行,程序将在这里无序循环
while(1){} // 程序不会运行到这里
}
同一个任务的函数可以用来创建不同的任务,因为默认情况下不同任务有不同的堆栈:
#include "debug.h"
#include "FreeRTOS.h" // 引入头文件
#include "task.h" // 引入头文件
/* Task1相关参数与任务处理函数 */
void task(void *pvParameters)
{
const char *pcTaskText = pvParameters;
while(1)
{
printf("%s\r\n", pcTaskText);
vTaskDelay(500);
}
}
/* 主函数 */
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
SystemCoreClockUpdate();
Delay_Init();
USART_Printf_Init(115200);
xTaskCreate(task, "task1", 256, "task 1 run", 5, NULL); // 创建一个任务
xTaskCreate(task, "task2", 256, "task 2 run", 5, NULL); // 创建一个任务
vTaskStartScheduler(); // 任务调度,任务将在这里根据情况开始运行,程序将在这里无序循环
while(1){} // 程序不会运行到这里
}
任务也可以动态的创建:
#include "debug.h"
#include "FreeRTOS.h" // 引入头文件
#include "task.h" // 引入头文件
/* Task2相关参数与任务处理函数 */
void task2(void *pvParameters){
while(1)
{
printf("task 2 run.\r\n");
vTaskDelay(500);
}
}
/* Task1相关参数与任务处理函数 */
void task1(void *pvParameters)
{
volatile size_t i = 0; // 使用volatile关键词防止编译器优化
while(1)
{
printf("task 1 run.\r\n");
i++;
if (i == 3) { // task1执行3次后
xTaskCreate(task2, "task2", 256, NULL, 5, NULL); // 动态创建任务
}
vTaskDelay(1000);
}
}
/* 主函数 */
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
SystemCoreClockUpdate();
Delay_Init();
USART_Printf_Init(115200);
printf("FreeRTOS Kernel Version:%s\r\n",tskKERNEL_VERSION_NUMBER); // 打印FreeRTOS内核版本号
xTaskCreate(task1, "task1", 256, NULL, 5, NULL); // 创建一个任务
vTaskStartScheduler(); // 任务调度,任务将在这里根据情况开始运行,程序将在这里无序循环
while(1){} // 程序不会运行到这里
}
除了使用 xTaskCreate
创建任务,还有一些其它方法可以创建任务,主要是一些使用静态内存分配方式创建任务的方法,目前来说除非要求非常高的场合,一般没必要使用这些方式。
通常来说FreeRTOS使用时都会用到一个定时器(常用SysTick),定时器定时产生中断,FreeRTOS每次中断时累加系统时间 Tick
(就是前面例子中使用 xTaskGetTickCount()
获取的内容),并检查和调度任务运行。
FreeRTOSConfig.h
文件中 configTICK_RATE_HZ
参数用于设置系统中断频率。在上面项目中默认值为 500 ,表示每秒中断 500 次,即每秒 Tick
值加 500 ,每两次调度发生间隔为 2ms ,这 2ms 其实就是每次被调度到执行的任务工作的时间。
任务在OS中各个阶段通常使用下面状态转换图表示:
Running
即表示当前正在运行的任务; Ready
表示已经准备好但没有时间可以分配给它运行的任务; Blocked
表示正在等待时间或者其它资源可用的任务(比如前面的vTaskDelay方法就会使任务进入Blocked状态); Suspended
主要是手动使用函数挂起的任务。
除了手动创建的任务,系统还有个空闲任务(优先级为0),在没有用户任务执行的适合空闲任务将会执行。空闲任务中会进行一些资源释放等操作,通常开发是最好保证空闲任务能够定期被调度到。
这里有一个问题,如果当前有多个任务处于就绪状态( Ready
),那么应该调度哪个任务执行?
在上面项目的默认设定下:
configUSE_PREEMPTION
= 1 (在 FreeRTOSConfig.h
文件中定义)configUSE_TIME_SLICING
= 1 (没有主动定义,默认值为1)configUSE_TICKLESS_IDLE
= 0 (没有主动定义,默认值为0)configIDLE_SHOULD_YIELD
= 0 (在 FreeRTOSConfig.h
文件中定义)除了上面的调度逻辑,还有一个逻辑是在运行 vTaskStartScheduler();
语句前添加的任务,高优先级的会先运行,同优先级的后添加的将先运行。
任务创建后可以通过一些函数来控制任务状态,主要可以实现改变任务优先级、删除任务、挂起与恢复任务、阻塞任务等功能。
需要注意的是下面出现的任务控制功能可能并不会在相关函数执行后立即生效(比如任务删除自身),这种情况下需要等到下一次任务调度或是空闲任务执行后才会真正生效。
前面例子中有出现延时函数,FreeRTOS提供了延时函数主要有下面两种:
// 从进入vTaskDelay函数到函数返回总时间至少为xTicksToDelay个系统Tick时间
void vTaskDelay(const TickType_t xTicksToDelay)
// 实际编写程序时可以使用 pdMS_TO_TICKS( value_in_ms ) 这个个宏将毫秒时间装换成Tick
// 比如 pdMS_TO_TICKS(500) 就可以获得500ms时间对应的Tick值
// 从pxPreviousWakeTime系统时间开始至少经过xTimeIncrement个系统时间
BaseType_t xTaskDelayUntil(TickType_t * const pxPreviousWakeTime, const TickType_t xTimeIncrement)
void vTaskDelayUntil(TickType_t * const pxPreviousWakeTime, const TickType_t xTimeIncrement)
// 上面函数中需要用到的系统时间 pxPreviousWakeTime 可以使用下面两个函数获取
TickType_t xTaskGetTickCount(void) // 在普通任务中使用
TickType_t xTaskGetTickCountFromISR(void) // 在中断服务程序中使用
通常来说如果想要设置任务每次执行完成到下次开始执行的延时可以使用第一种延时方式;如果想要任务每次开始执行时间间隔相等那就需要使用第二种方式(周期性任务)。
下面的各种任务控制功能都需要用到任务句柄。任务句柄可以理解为一个结构体,用来保存任务相关的各种内容,所以我们可以通过它来控制指定的任务。
任务句柄类型是 TaskHandle_t
,可以使用下面方式在创建任务时获得:
TaskHandle_t Task_Handler; // 声明任务句柄对象
int main(void)
{
xTaskCreate(task_func, "name", 256, NULL, 5, &Task_Handler); // 创建任务并获得任务句柄
}
除了在创建时获得任务句柄,也可以使用下面函数获得已创建的任务的任务句柄:
// 获取当前任务任务句柄
// FreeRTOSConfig.h中INCLUDE_xTaskGetCurrentTaskHandle 设置为1才能使用
TaskHandle_t xTaskGetCurrentTaskHandle(void)
// 通过任务名称获取任务句柄
// FreeRTOSConfig.h中INCLUDE_xTaskGetHandle设置为1才能使用
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)
// xTask可以填入NULL,表示执行该函数的任务自身
// uxNewPriority为欲设的任务优先级,值越大优先级越高,最大值为FreeRTOSConfig.h中configMAX_PRIORITIES - 1
可以使用下面函数来删除任务:
void vTaskDelete(TaskHandle_t xTaskToDelete)
// xTaskToDelete可以填入NULL,表示执行该函数的任务自身
挂起与恢复任务主要由下面函数进行操作:
// 挂起任务
void vTaskSuspend(TaskHandle_t xTaskToSuspend)
// xTaskToSuspend可以填入NULL,表示执行该函数的任务自身
// 恢复任务
void vTaskResume(TaskHandle_t xTaskToResume) // 在普通任务中使用
BaseType_t xTaskResumeFromISR(TaskHandle_t xTaskToResume) // 在中断服务程序中使用
// 通常恢复任务只能恢复别人,自己不能恢复自己(因为已经被挂起了)
// 暂停所有任务调度
void vTaskSuspendAll(void)
// 恢复所有任务调度
BaseType_t xTaskResumeAll(void)
强制任务离开阻塞状态主要就下面函数进行操作:
BaseType_t xTaskAbortDelay(TaskHandle_t xTask)
// 如果xTask不在阻塞状态则返回pdFAIL,否则返回pdPASS
FreeRTOS调度器启动时,会自动创建空闲任务,以确保始终存在一个能够运行的任务。空闲任务处于最低优先级(0),以确保如果有更高的优先级应用程序任务处于准备就绪状态时空闲任务不会占用任何CPU时间。
空闲任务负责释放删除自身的任务的内存。除了这个功能,空闲任务还可以有一个由用户实现具体功能的钩子函数 void vApplicationIdleHook(void)
,该函数会在每次空闲任务运行时被调用。
要使用空闲任务钩子函数,需要在 FreeRTOSConfig.h
文件中定义 configUSE_IDLE_HOOK
值为 1
,然后实现钩子函数:
void vApplicationIdleHook(void) {
// TDOD
}
必须确保钩子函数不能调用任何可能导致空闲任务阻塞的 API 函数 (例如,vTaskDelay () ,或带有阻塞时间的队列或信号量函数 )
空闲钩子函数比较常用的功能有下面一些:
FreeRTOS中任务的基础使用还是比较简单的。实际项目中通常会有多个任务,任务间多数时候会需要配合工作,这些时候就需要用到 队列、信号量、互斥量 等功能了,这些内容将在后面的文章中进行介绍。