一个最简单的任务函数
FreeRTOS中任务是一个永远不会退出的 C 函数,因此通常是作为无限循环实现,其不允许以任何方式从实现函数中返回,如果一个任务不再需要,可以显示的将其删除,其典型的任务函数结构如下所示
/**
* @brief 任务函数
* @retval None
*/
void ATaskFunction(void *pvParameters)
{
/*初始化或定义任务需要使用的变量*/
int iVariable = 0;
for(;;)
{
/*完成任务的功能代码*/
}
/*跳出循环的任务需要被删除*/
vTaskDelete(NULL);
}
创建任务函数
/**
* @brief 动态分配内存创建任务函数,需将宏configSUPPORT_DYNAMIC_ALLOCATION配置为 1
* @param pxTaskCode:指向任务函数的指针
* @param pcName:任务名称,单纯用于辅助调试,最大长度configMAX_TASK_NAME_LEN
* @param usStackDepth:任务堆栈大小, 注意字(word)为单位
* @param pvParameters:传递给任务函数的参数
* @param uxPriority:任务优先级, 范围: 0 ~ configMAX_PRIORITIES - 1
* @param pxCreatedTask:任务句柄,可通过该句柄进行删除/挂起任务等操作
* @retval pdTRUE:创建成功,errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY:内存不足创建失败
*/
BaseType_t xTaskCreate(TaskFunction_t pxTaskCode,
const char * const pcName,
unsigned short usStackDepth,
void *pvParameters,
UBaseType_t uxPriority,
TaskHandle_t *pxCreatedTask);
/**
* @brief 静态分配内存创建任务函数,需将宏configSUPPORT_STATIC_ALLOCATION配置为 1
* @param pxTaskCode:任务函数
* @param pcName:任务名称
* @param usStackDepth:任务栈深度,单位为字(word)
* @param pvParameters:任务参数
* @param uxPriority:任务优先级
* @param puxStackBuffer:任务栈空间数组
* @param pxTaskBuffer:任务控制块存储空间
* @retval 创建成功的任务句柄
*/
TaskHandle_t xTaskCreateStatic(TaskFunction_t pxTaskCode,
const char * const pcName,
uint32_t ulStackDepth,
void *pvParameters,
UBaseType_t uxPriority,
StackType_t * const puxStackBuffer,
StaticTask_t * const pxTaskBuffer);
上述两个任务创建函数有如下几点不同,之后如无特殊需要将一律使用动态分配内存的方式创建任务或其他实例
- xTaskCreateStatic创建任务时需要用户指定
任务的任务控制块以及任务的栈空间所需的内存
,而xTaskCreate创建任务其存储空间被动态分配,无需用户指定 - xTaskCreateStatic创建任务函数的返回值为成功创建的任务句柄,而xTaskCreate成功创建任务的句柄需要以参数形式提前定义并指定,同时其函数返回值仅表示任务创建成功/失败
动态创建任务函数内部实现 (此函数创建的任务会立刻进入就绪态, 由任务调度器调度运行)
- 申请堆栈内存&任务控制块内存
- TCB结构体成员赋值
- 添加新任务到就绪列表中
任务控制块结构体成员介绍
typedef struct tskTaskControlBlock
{
volatile StackType_t *pxTopOfStack; /* 任务栈栈顶, 必须为TCB的第一个成员 */
ListItem_t xStateListItem; /* 任务状态列表项 */
ListItem_t xEventListItem; /* 任务事件列表项 */
UBaseType_t uxPriority; /* 任务优先级, 数值越大, 优先级越大 */
StackType_t *pxStack; /* 任务栈起始地址 */
char pcTaskName[ configMAX_TASK_NAME_LEN ]; /* 任务名字 */
…
省略很多条件编译的成员
} tskTCB;
typedef struct tskTaskControlBlock *TaskHandle_t;
*TaskHandle_t
和tskTCB
都是任务控制块
任务栈栈顶: 在任务切换时的任务上下文保存、任务恢复息息相关
每个任务都有属于自己的任务控制块, 类似身份证
任务优先级
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);
延时函数
学习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);
为什么会有空闲任务?
概述
FreeRTOS 调度器决定在任何时刻处理器必须保持有一个任务运行,当用户创建的所有任务都处于阻塞状态不能运行时,空闲任务就会被运行
空闲任务是一个优先级为0(最低优先级)的非常短小的循环,其优先级为 0 保证了不会影响到具有更高优先级的任务进入运行态,一旦有更高优先级的任务进入就绪态,空闲任务就会立刻切出运行态
空闲任务何时被创建?当调用 vTaskStartScheduler() 启动调度器时就会自动创建一个空闲任务,如下图所示,另外空闲任务还负责将分配给已删除任务的内存释放掉
空闲任务钩子函数
空闲任务有一个钩子函数,可以通过配置 configUSE_IDLE_HOOK 参数为 Enable 启动空闲任务的钩子函数,如果是使用STM32CubeMX软件生成的工程则会自动生成空闲任务钩子函数,当调度器调度内核进入空闲任务时就会调用钩子函数
通常空闲任务钩子函数主要被用于下方函数体内部注释列举的几种情况,如下所述为空闲任务钩子函数典型的任务函数结构
/**
* @brief 空闲任务钩子函数
* @retval NULL
*/
void vApplicationIdleHook(void)
{
/*
1.执行低优先级,或后台需要不停处理的功能代码
2.测试系统处理裕量(内核执行空闲任务时间越长表示内核越空闲)
3.将处理器配置到低功耗模式(Tickless模式)
*/
}
删除任务
一个任务不再需要时,需要显示调用 vTaskDelete() API函数将任务删除,该函数需要传入要删除任务的句柄这个参数(传入NULL时表示删除自己),函数声明如下所述
/**
* @brief 任务删除函数,需将宏`INCLUDE_vTaskDelete`配置为 1
* @param pxTaskToDelete:要删除的任务句柄,NULL表示删除自己
* @retval None
*/
void vTaskDelete(TaskHandle_t pxTaskToDelete);
用于删除已被创建的任务, 被删除的任务将从就绪态任务列表、阻塞态任务列表、挂起态任务列表和事件列表中移除
注意:
- 当传入的参数为 NULL, 则代表删除任务自身 (当前正在运行的任务 )
- 空闲任务会负责释放被删除任务中由系统分配的内存, 但是由用户在任务删除前申请的内存, 则需要由用户在任务被删除前提前释放, 否则将导致内存泄露
删除任务函数的内部实现过程
- 获取所要删除任务的控制块: 通过传入的任务句柄, 判断所需要删除哪个任务, NULL代表删除自身
- 将被删除任务, 移除所在列表: 将该任务在所在列表中移除, 包括: 就绪、阻塞、挂起、事件等列表
- 判断所需要删除的任务: 如果删除任务自身, 需先添加到等待删除列表, 内存释放将在空闲任务执行,如果删除其他任务, 释放内存, 任务数量–
- 更新下个任务的阻塞时间: 更新下一个任务的阻塞超时时间, 以防被删除的任务就是下一个阻塞超时的任务