FreeRTOS 任务调度与管理
1. FreeRTOS 任务调度器:核心概念与机制
FreeRTOS 调度器是实时操作系统的核心,负责确定哪个任务在何时获得 CPU 执行权。其设计遵循严格的优先级驱动规则。
1.1 调度器类型
FreeRTOS 支持三种调度器,通过 FreeRTOSConfig.h
中的宏进行配置:
抢占式调度器 (Preemptive Scheduler)
机制:这是最常用的模式。调度器始终运行最高优先级的就绪态任务。如果一个比当前运行任务优先级更高的任务进入就绪态,调度器会立即暂停当前任务,切换到更高优先级的任务。
配置:此模式是默认模式,无需特殊配置。
协作式调度器 (Co-operative Scheduler)
机制:任务调度发生在以下两种情况下:
一个任务显式地调用
taskYIELD()
。一个任务进入阻塞状态(例如调用
vTaskDelay()
,xQueueReceive()
等)。
特点:低优先级任务不会被高优先级任务抢占,除非它主动放弃 CPU。这能提供更可预测的执行流程,但实时性较差。
配置:在
FreeRTOSConfig.h
中定义configUSE_PREEMPTION
为0
。
带时间片的抢占式调度器 (Preemptive with Time Slicing)
机制:在抢占式调度的基础上,为相同优先级的任务引入时间片概念。多个相同优先级的任务将以时间片为单位共享 CPU 时间。
时间片:长度由系统心跳中断 (
configTICK_RATE_HZ
) 定义。例如,如果configTICK_RATE_HZ
为 1000 (1kHz),则一个时间片为 1ms。行为:调度器在每个 tick 中断 (
xPortSysTickHandler
) 中检查是否需要进行任务切换。如果当前任务的时间片用完,并且存在相同优先级的就绪任务,则会触发一次上下文切换。配置:这是默认行为。确保
configUSE_PREEMPTION
和configUSE_TIME_SLICING
均为1
。
1.2 任务状态
一个任务在任何时刻都处于以下状态之一:
运行态 (Running):任务正在 CPU 上执行。
就绪态 (Ready):任务已准备就绪,可以运行,但有一个更高或同等优先级的任务正在运行。
阻塞态 (Blocked):任务正在等待某个事件(如定时器到期、队列数据、信号量、通知等)。任务在阻塞状态下不消耗 CPU 时间。
挂起态 (Suspended):任务被显式地挂起(
vTaskSuspend()
)。它无法进入就绪态,直到被显式恢复(vTaskResume()
)。它不参与调度。
2. 任务管理相关函数详解
2.1 任务创建 (xTaskCreate
/ xTaskCreateStatic
)
c
BaseType_t xTaskCreate( TaskFunction_t pvTaskCode, // 任务函数指针const char * const pcName, // 任务描述名(用于调试)configSTACK_DEPTH_TYPE usStackDepth, // 任务堆栈深度(字数,非字节)void *pvParameters, // 传递给任务函数的参数UBaseType_t uxPriority, // 任务优先级 (0 to configMAX_PRIORITIES-1)TaskHandle_t *pxCreatedTask ); // 可选的用于传递任务句柄的指针
堆栈深度:需要根据函数调用深度和局部变量大小谨慎设置。可通过
uxTaskGetStackHighWaterMark()
函数监控堆栈使用峰值。优先级:数值越高,优先级越高。
configMAX_PRIORITIES
在FreeRTOSConfig.h
中定义,最大允许值为 32(受架构限制)。返回值:
pdPASS
表示成功,errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY
表示堆栈空间分配失败。
2.2 任务删除 (vTaskDelete
)
c
void vTaskDelete( TaskHandle_t xTaskToDelete );
可以删除其他任务或自身(参数传
NULL
)。被删除的任务的资源(堆栈、TCB)由空闲任务 (Idle Task) 负责回收。因此,删除大量任务时需确保空闲任务有执行时间。
2.3 任务延时 (vTaskDelay
/ vTaskDelayUntil
)
c
void vTaskDelay( const TickType_t xTicksToDelay ); // 相对延时void vTaskDelayUntil( TickType_t *pxPreviousWakeTime, // 指向一个变量,用于存储任务上次解除阻塞的时间const TickType_t xTimeIncrement ); // 固定的周期时间
vTaskDelay
:相对延时。调用该函数后,任务阻塞xTicksToDelay
个 tick。不适用于精确定时,因为从任务就绪到再次被调度执行存在不确定性。vTaskDelayUntil
:绝对延时。用于实现固定频率的周期性任务。它能补偿任务本身执行时间带来的误差,是更精确的选择。
2.4 优先级控制
c
void vTaskPrioritySet( TaskHandle_t xTask, UBaseType_t uxNewPriority ); UBaseType_t uxTaskPriorityGet( TaskHandle_t xTask );
可用于动态改变任务的优先级,实现诸如优先级继承协议 (Priority Inheritance Protocol) 或其他复杂调度策略。
2.5 任务挂起与恢复 (vTaskSuspend
/ vTaskResume
)
c
void vTaskSuspend( TaskHandle_t xTaskToSuspend ); void vTaskResume( TaskHandle_t xTaskToResume ); BaseType_t xTaskResumeFromISR( TaskHandle_t xTaskToResume ); // 从中断中恢复
vTaskSuspend()
和vTaskResume()
必须成对使用。xTaskResumeFromISR()
专为中断服务程序设计,其返回值pdTRUE
表示恢复的任务优先级更高,可能需要在中断退出前请求一次上下文切换(portYIELD_FROM_ISR()
)。
3. 内核实践与高级主题
3.1 空闲任务 (Idle Task) 与空闲钩子 (Idle Hook)
空闲任务是在没有其他任务运行时自动运行的优先级为 0 的任务。
可以定义一个
vApplicationIdleHook()
函数(需在FreeRTOSConfig.h
中使能configUSE_IDLE_HOOK
)来让内核在空闲任务中执行后台功能,如低功耗处理(调用WFI
指令)。警告:空闲钩子函数绝不能阻塞或挂起。
3.2 Tickless 低功耗模式
在应用场景中,当系统空闲时,可以通过关闭 SysTick 中断来让 MCU 进入深度睡眠,显著降低功耗。
通过配置
configUSE_TICKLESS_IDLE
为 1 或 2 来启用。需要根据具体 MCU 实现portSUPPRESS_TICKS_AND_SLEEP()
函数。
3.3 调度器锁定 (vTaskSuspendAll
/ xTaskResumeAll
)
调用
vTaskSuspendAll()
可以临时挂起调度器,禁止任务切换,但中断仍然使能。调用
xTaskResumeAll()
恢复调度器。如果恢复过程中有更高优先级的任务就绪,会立即触发一次切换。用于保护非线程安全的代码段或执行精密的时序操作。应尽量缩短锁定时间。
4. 综合应用实例:多任务系统与精确计时
以下实例创建了三个任务:一个高优先级任务,两个相同优先级的周期性任务。
c
#include <FreeRTOS.h> #include <task.h> #include <stdio.h>// 任务句柄 TaskHandle_t xPeriodicTaskHandle1, xPeriodicTaskHandle2;// 高优先级任务 void vHighPriorityTask(void *pvParameters) {while(1) {printf("[HP Task] Running...\n");vTaskDelay( pdMS_TO_TICKS(500) ); // 每500ms执行一次} }// 周期性任务1 (使用 vTaskDelayUntil) void vPeriodicTask1(void *pvParameters) {TickType_t xLastWakeTime;const TickType_t xFrequency = pdMS_TO_TICKS(1000); // 周期1000msxLastWakeTime = xTaskGetTickCount(); // 初始化基准时间while(1) {printf("[Task1] Tick at %lu\n", xTaskGetTickCount());// 绝对延时,精确保证1000ms周期vTaskDelayUntil(&xLastWakeTime, xFrequency);} }// 周期性任务2 (使用 vTaskDelay) void vPeriodicTask2(void *pvParameters) {while(1) {printf("[Task2] Tick at %lu\n", xTaskGetTickCount());vTaskDelay( pdMS_TO_TICKS(1000) ); // 相对延时1000ms} }void main(void) {// 创建高优先级任务xTaskCreate(vHighPriorityTask, "HP Task", 1024, NULL, 3, NULL);// 创建两个相同优先级的周期性任务xTaskCreate(vPeriodicTask1, "Periodic1", 1024, NULL, 2, &xPeriodicTaskHandle1);xTaskCreate(vPeriodicTask2, "Periodic2", 1024, NULL, 2, &xPeriodicTaskHandle2);// 启动调度器vTaskStartScheduler();// 如果调度器启动成功,不会运行到这里while(1); }
运行分析与预期输出:
高优先级任务(优先级3)将抢占两个周期性任务(优先级2)。
每500ms,高优先级任务会运行一次,打印消息。
在两个周期性任务中:
Task1
使用vTaskDelayUntil
,其打印间隔会非常稳定地接近 1000ms,因为它补偿了执行和调度延迟。Task2
使用vTaskDelay
,其打印间隔会是 1000ms + 执行延迟 + 被高优先级任务抢占的时间,因此间隔会长于且不稳定。
当
Task1
和Task2
同时就绪时,由于优先级相同,它们将共享时间片,你会在一个大概的 1000ms 周期内看到两条打印信息交错出现。