【FreeRTOS空闲钩子函数、优先级函数、删除函数及调度器算法】

空闲钩子函数

空闲任务

当任务处于阻塞状态时,任务无法运行,因此调度器无法选择这些任务。必须始终至少有一个任务可以进入运行状态(即使在FreeRTOS的特殊低功耗功能时也是如此,在这种情况下,执行FreeRTOS的微控制器将被置于低功耗模式)。

为了确保这种情况,当调用vTaskStartScheduler() 时,调度器会自动创建一个空闲任务。空闲任务除了在一个循环中等待之外几乎不执行其他任何操作。

空闲任务具有尽可能低的优先级(优先级为0),以确保它永远不会阻止更高优先级的应用程序任务进入运行状态。然而,这并不妨碍应用程序设计者根据需要创建与空闲任务共享优先级的任务。

在空闲钩子函数中,可以监测系统资源,如内存使用情况、CPU 使用率等,以便进行系统性能分析;也可以进行低功耗管理,当系统空闲时,将硬件设备切换到低功耗模式,节省能源。

空闲优先级任务的行为

空闲任务钩子函数

用途

通过使用空闲钩子(或空闲回调)函数直接在空闲任务中添加应用程序的相关功能。空闲钩子函数是一种函数,它在空闲任务循环的每次迭代中自动由空闲任务调用。空闲任务钩子的常见用途包括:

执行低优先级、后台或连续处理功能,而无需为此目的创建应用程序任务,所带来额外的RAM开销。

测量空闲处理能力(仅在所有更高优先级的应用程序任务没有工作要执行时,空闲任务才会运行。测量分配给空闲任务的处理时间,可以测量空闲处理能力)。

将处理器置于低功耗模式,提供了一种简单且自动的方法,在没有应用程序处理要执行时,节省功耗。

限制

空闲任务钩子函数必须遵循以下规则:

空闲任务钩子函数绝对不能阻塞或挂起自己。注意:以任何方式阻塞空闲任务都可能导致没有任务可以进入运行状态的情况。

如果一个应用程序任务使用vTaskDelete() API函数删除自己,必须在合理的时间段内将空闲任务钩子返回给调用者。这是因为任务被删除后,空闲任务负责清理内核资源。如果空闲任务永久保持在空闲钩子函数中,则无法进行这种清理。

在 FreeRTOS 配置文件中,需要将configUSE_IDLE_HOOK设置为 1,才能启用空闲钩子函数。

在 FreeRTOS 系统中,空闲钩子函数是可选的,不是必须使用。只有在有特定需求,如进行低功耗管理、系统资源监测等时,才会启用空闲钩子函数。

空闲任务钩子函数必须具有下面代码所示的名称和原型:

cpp 复制代码
void vApplicationIdleHook( void );
cpp 复制代码
static TickType_t xIdleStartTime;
static uint32_t ulIdleCount = 0;

// 空闲钩子函数:统计空闲时间
void vApplicationIdleHook( void )
{
    xIdleStartTime = xTaskGetTickCount();  // 获取当前系统节拍数
    ulIdleCount++;  // 空闲计数累加
    
    // 可选:每1秒打印一次空闲统计(需确保打印函数线程安全)
    static TickType_t xLastPrintTime = 0;
    if (xTaskGetTickCount() - xLastPrintTime >= pdMS_TO_TICKS(1000)) {
        xLastPrintTime = xTaskGetTickCount();
        printf("Idle Count: %lu\n", ulIdleCount);
        ulIdleCount = 0;
    }
}

空闲钩子函数与调度器的关系

空闲钩子函数与任务调度器关系紧密。任务调度器负责管理系统中所有任务的调度,根据任务的优先级和就绪状态,决定哪个任务获得 CPU 资源。当任务调度器发现没有其他就绪任务时,会调用空闲钩子函数,此时系统处于空闲状态。空闲钩子函数执行完毕后,会返回任务调度器,调度器继续监测任务状态,等待新的任务进入就绪态或有任务解除阻塞。

任务优先级设置函数

使用vTaskPrioritySet() API 函数用于在调度器启动后更改任务的优先级。只有将FreeRTOSConfig.h中的INCLUDE_vTaskPrioritySet设置为1 时该函数可用,该函数的原型如下所示:

cpp 复制代码
void vTaskPrioritySet( 
    TaskHandle_t xTask,        // 要修改优先级的任务句柄
    UBaseType_t uxNewPriority  // 要设置的新优先级
);
参数 含义
xTask - 目标任务的句柄(创建任务时通过 xTaskCreate()pxCreatedTask 参数获取); - 若传入 NULL,表示修改当前正在运行的任务(即调用此函数的任务)自身的优先级。
uxNewPriority 新的优先级值,范围:0 ~ configMAX_PRIORITIES - 1configMAX_PRIORITIESFreeRTOSConfig.h 中定义,默认一般为 5~32);注意:FreeRTOS 中数值越大,优先级越高。

修改其他任务的优先级

cpp 复制代码
#include "FreeRTOS.h"
#include "task.h"

// 定义两个任务句柄
TaskHandle_t xSensorTaskHandle = NULL;  // 传感器采集任务
TaskHandle_t xProcessTaskHandle = NULL; // 数据处理任务

// 传感器采集任务(低优先级)
void vSensorTask( void *pvParameters )
{
    for( ;; )
    {
        // 常规采集:低频率、低优先级
        printf("常规采集传感器数据...\n");
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

// 数据处理任务(主控逻辑)
void vProcessTask( void *pvParameters )
{
    uint8_t ucEmergencyFlag = 0; // 紧急事件标志

    for( ;; )
    {
        // 检测是否触发紧急事件(比如传感器阈值超限)
        if( ucEmergencyFlag == 1 )
        {
            // 紧急事件:提升传感器采集任务优先级(从1→4)
            vTaskPrioritySet(xSensorTaskHandle, 4);
            printf("紧急事件触发,传感器任务优先级提升为4\n");
            ucEmergencyFlag = 0; // 重置标志
        }
        else
        {
            // 无紧急事件:恢复传感器任务优先级(4→1)
            vTaskPrioritySet(xSensorTaskHandle, 1);
        }

        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

// 创建任务
void vStartTasks( void )
{
    // 创建传感器任务(初始优先级1)
    xTaskCreate(vSensorTask, "SensorTask", 1024, NULL, 1, &xSensorTaskHandle);
    // 创建处理任务(优先级2,高于传感器初始优先级)
    xTaskCreate(vProcessTask, "ProcessTask", 1024, NULL, 2, &xProcessTaskHandle);
}

关键注意事项

1. 优先级范围限制
  • 新优先级 uxNewPriority 必须小于 configMAX_PRIORITIES(在 FreeRTOSConfig.h 中配置),超出范围会导致未定义行为(多数版本会自动截断到合法范围,但不建议依赖);
  • 不要设置为负数,UBaseType_t 是无符号类型,负数会被解析为超大正数,可能导致任务优先级异常升高。
2. 调度器抢占行为
  • 若修改后的任务优先级高于当前运行的任务,调度器会立即切换到该任务(抢占式调度),当前任务被挂起;
  • 若修改后的优先级低于当前任务,当前任务会继续运行,直到有更高优先级任务就绪。
3. 中断中禁止直接调用
  • vTaskPrioritySet() 不能在中断服务函数(ISR)中直接调用(属于任务级 API);
  • 中断中需使用替代 API:xTaskPrioritySetFromISR()
4. 避免优先级反转
  • 若低优先级任务持有互斥锁,此时提升高优先级任务,可能导致优先级反转(高优先级任务等待低优先级任务释放锁);
  • 解决方案:使用 FreeRTOS 的优先级继承互斥锁(xSemaphoreCreateMutexRecursive ()),或调整优先级时确保锁已释放。
5. 空闲任务优先级
  • 空闲任务优先级为 tskIDLE_PRIORITY(默认 0),不要将其他任务优先级设为 0 后长期占用 CPU,否则会阻塞空闲任务(导致空闲钩子函数无法执行、内存清理失败等)。

任务优先级获取函数

使用uxTaskPriorityGet() 函数查询任务的优先级。只有在FreeRTOSConfig.h中将INCLUDE_uxTaskPriorityGet设置为1时,uxTaskPriorityGet() API函数才可用。该函数的原型如下所示:

cpp 复制代码
UBaseType_t uxTaskPriorityGet( 
    TaskHandle_t xTask  // 要查询优先级的任务句柄
);
含义
xTask - 目标任务的句柄(创建任务时通过 xTaskCreate()pxCreatedTask 参数获取);- 若传入 NULL,表示查询当前正在运行的任务(调用此函数的任务)自身的优先级。
返回值 UBaseType_t 类型的无符号整数,返回目标任务当前生效的优先级 ,范围:0 ~ configMAX_PRIORITIES - 1configMAX_PRIORITIESFreeRTOSConfig.h 中定义)。

获取其他任务的优先级

cpp 复制代码
#include "FreeRTOS.h"
#include "task.h"

// 子任务句柄
TaskHandle_t xSensorTaskHandle = NULL;

// 传感器采集任务
void vSensorTask( void *pvParameters )
{
    for( ;; )
    {
        printf("传感器任务运行中...\n");
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

// 主控任务:查询并动态调整子任务优先级
void vMasterTask( void *pvParameters )
{
    UBaseType_t uxSensorPriority;

    for( ;; )
    {
        // 1. 查询传感器任务的当前优先级
        uxSensorPriority = uxTaskPriorityGet(xSensorTaskHandle);
        printf("传感器任务当前优先级:%u\n", uxSensorPriority);

        // 2. 若优先级 < 4,则提升到4;否则恢复为1
        if(uxSensorPriority < 4)
        {
            vTaskPrioritySet(xSensorTaskHandle, 4);
            printf("传感器任务优先级已提升至4\n");
        }
        else
        {
            vTaskPrioritySet(xSensorTaskHandle, 1);
            printf("传感器任务优先级已恢复至1\n");
        }

        vTaskDelay(pdMS_TO_TICKS(2000));
    }
}

// 创建任务
void vStartTasks( void )
{
    // 创建传感器任务(初始优先级1)
    xTaskCreate(vSensorTask, "SensorTask", 1024, NULL, 1, &xSensorTaskHandle);
    // 创建主控任务(优先级2)
    xTaskCreate(vMasterTask, "MasterTask", 1024, NULL, 2, NULL);
}

任务删除函数

使用vTaskDelete() API 函数用于删除自己或者其他任务。需要的注意事项:

  1. 只有将FreeRTOSConfig.h文件中的INCLUDE_vTaskDelete设置为1 时,该API 函数才可以使用。

2.运行时不断创建和删除任务可能会产生内存碎片的风险。如果通过动态内存分配创建的任务后来自行删除,那么空闲任务将负责释放用于该任务的内存,例如已删除任务的数据结构和堆栈。在这种情况下,应用程序不应完全剥夺空闲任务的所有处理时间,这一点非常重要。

  1. 当任务被删除时,由内核本身分配给任务的内存才会自动释放。
cpp 复制代码
void vTaskDelete( TaskHandle_t xTaskToDelete );

xTaskToDelete:待删除任务的 句柄(TaskHandle_t)

  • 若传入 NULL:表示 删除当前正在运行的任务(自身)(最常用场景)。
  • 若传入其他任务的句柄:表示删除指定的其他任务(需确保该任务存在且句柄有效)。

删除其他任务(通过句柄)

cpp 复制代码
#include "FreeRTOS.h"
#include "task.h"

TaskHandle_t xTaskHandle; // 保存目标任务句柄

// 目标任务函数
void vTargetTask( void *pvParameters )
{
    while( 1 ) {
        // 目标任务逻辑...
    }
}

// 控制任务(用于删除目标任务)
void vControlTask( void *pvParameters )
{
    // 1. 创建目标任务(保存句柄)
    xTaskCreate(
        vTargetTask,
        "TargetTask",
        1024,
        NULL,
        tskIDLE_PRIORITY + 1,
        &xTaskHandle // 传入句柄地址,保存任务句柄
    );

    while( 1 ) {
        if( 某个删除条件满足 ) {
            // 2. 检查目标任务是否存在
            if( eTaskGetState( xTaskHandle ) != eDeleted ) {
                // 3. (可选)通过信号量/任务通知让目标任务释放资源
                // xSemaphoreGive( xTargetSem ); // 示例:让目标任务释放锁
                // vTaskDelay( pdMS_TO_TICKS( 10 ) ); // 等待目标任务释放资源

                // 4. 删除目标任务
                vTaskDelete( xTaskHandle );
                xTaskHandle = NULL; // 置空句柄,避免后续误用
            }
            vTaskDelay( pdMS_TO_TICKS( 1000 ) );
        }
    }
}

FreeRTOS的三种任务调度算法

任务的调度算法配置

调 度 算 法 决 定 哪 个 就 绪 状 态 任 务 转 换 到 运 行 状 态 的 软 件 程 序 。可 以 使 用configUSE_PREEMPTION和configUSE_TIME_SLICING配置常量来更改算法。这两个常量在FreeRTOSConfig.h中定义。

configUSE_TICKLESS_IDLE配置常量也会影响调度算法,因为使用该配置常量可能会导致在较长一段时间内完全关闭滴答中断。configUSE_TICKLESS_IDLE是专为必须尽量减少功耗的应用程序提供的一种高级选项。

调度模式 抢占能力 同优先级任务切换规则 核心配置宏
协作式调度 无抢占(任务主动让权) 仅当前任务主动放弃 CPU 时切换 configUSE_PREEMPTION = 0
不带时间片的抢占式调度 高优先级抢占低优先级 仅当前任务主动放弃 CPU 时切换 configUSE_PREEMPTION = 1``configUSE_TIME_SLICING = 0
带时间片的抢占式调度 高优先级抢占低优先级 按固定时间片轮流执行 configUSE_PREEMPTION = 1``configUSE_TIME_SLICING = 1

三种模式关键差异对比

对比维度 协作式调度 不带时间片的抢占式 带时间片的抢占式
高优先级任务响应 延迟不可控(必须等低优先级让权) 立即响应(抢占) 立即响应(抢占)
同优先级任务执行 依赖主动让权,易饿死 依赖主动让权,易饿死 时间片轮转,公平执行
上下文切换开销 极小(仅主动让权时切换) 较小(仅抢占和主动让权时切换) 较大(抢占 + 时间片切换)
实时性等级 极低 中高 最高
共享资源同步难度 简单(无抢占) 中等(仅抢占需同步) 复杂(抢占 + 时间片均需同步)
相关推荐
小曹要微笑1 天前
队列集详解
freertos·队列·队列集
wzfj123452 天前
FreeRTOS 学习方法
freertos
Zeku4 天前
20260103 - Linux平台总线LED驱动架构深度解析
stm32·freertos·linux驱动开发·linux应用开发
Zeku5 天前
20260102 - Linux驱动设计的思想
stm32·freertos·linux驱动开发·linux应用开发
Zeku5 天前
20260103 - Linux总线设备驱动模型学习笔记
stm32·freertos·linux驱动开发·linux应用开发
brave and determined5 天前
ESP32 FreeRTOS (day1)入门教程 (ESP-IDF版):从超级循环到多任务的系统化思维
操作系统·esp32·freertos·任务·任务调度器·任务控制块·嵌入式设计
森旺电子6 天前
STM32 上下文详解
stm32·单片机·嵌入式硬件·freertos
Zeku6 天前
20251231 - Linux 字符设备驱动开发笔记:分层设计
stm32·freertos·linux驱动开发·linux应用开发
Zeku6 天前
20251230 - 为什么Linux驱动开发中必须要用到ioremap来访问硬件?
stm32·freertos·linux驱动开发·linux应用开发