任务管理与调度
理解任务状态机与优先级,掌握抢占式与协作式调度、时间片轮转,学会 vTaskDelay 与 vTaskDelayUntil 的区别,认识空闲任务与钩子函数。
理解任务状态机与优先级,掌握抢占式与协作式调度、时间片轮转,学会 vTaskDelay 与 vTaskDelayUntil 的区别,认识空闲任务与钩子函数。
vTaskSuspend 显式移出调度,不参与调度直到被 resume。configMAX_PRIORITIES-1 的整数优先级,数值越大优先级越高(与某些 RTOS 相反)。调度器永远运行当前就绪任务中优先级最高者。多个任务可共享同一优先级,此时它们之间按时间片轮转。configUSE_PREEMPTION=1 启用,是保证实时性的关键,也是绝大多数场景的默认选择。configUSE_PREEMPTION=0 时启用。任务只有在主动调用 taskYIELD() 或阻塞式 API(如 vTaskDelay、队列等待)时才让出 CPU,不会被强制打断。切换点可控、无需担心共享数据竞争,但一个"跑飞"的任务会饿死其他所有任务,实时性差。configUSE_TIME_SLICING=1 启用。当多个任务处于同一优先级时,每来一个系统节拍(Tick)就轮流切换,使它们均分 CPU。若关闭时间片,同优先级任务会一直运行到主动阻塞才切换。vTaskDelete 删除任务的内存(TCB 和栈),也是 Tickless 低功耗和运行时统计的挂钩点。它必须存在且不能被阻塞太久。vApplicationIdleHook() 在空闲任务每圈调用,可用于喂狗或进入低功耗;vApplicationTickHook() 在每个 Tick 中断里调用,运行在中断上下文,必须极短。需分别用 configUSE_IDLE_HOOK、configUSE_TICK_HOOK 开启。vTaskDelay(n) 是"相对延时":从调用那一刻起再阻塞 n 个 tick,累积误差会漂移。vTaskDelayUntil(&last, period) 是"绝对延时":以上次唤醒时刻为基准精确间隔 period,适合需要严格固定周期的采样任务,不受任务本身执行时间波动影响。TaskHandle_t 本质就是指向 TCB 的指针。所有任务管理 API(挂起、恢复、改优先级、查询)都通过句柄操作对应 TCB。理解四种状态之间的迁移,是排查"任务不运行"问题的基础。下图展示了完整的状态转换:
阻塞是"我在等某件事,最多等这么久"——一定伴随超时(可以是 portMAX_DELAY 永久等待)。挂起是"把我从调度里拿走",不参与任何调度,直到别人调用 vTaskResume() 把它唤醒。阻塞是任务自己主动进入的,挂起通常是被其他任务操控的。
假设任务每次执行体耗时不定(2~8ms),要求严格每 100ms 采样一次。用 vTaskDelay 会导致周期漂移,用 vTaskDelayUntil 才能锁定精确周期:
/* ❌ 相对延时:实际周期 = 执行时间 + 100ms,会漂移 */ void SampleTask_Bad(void *arg) { for (;;) { do_sample(); /* 耗时 2~8ms 不定 */ vTaskDelay(pdMS_TO_TICKS(100)); /* 从此刻再等 100ms */ } /* → 真实周期 102~108ms,抖动 */ } /* ✅ 绝对延时:以上次唤醒点为基准,锁定精确 100ms 周期 */ void SampleTask_Good(void *arg) { TickType_t xLastWake = xTaskGetTickCount(); /* 记录起点 */ const TickType_t period = pdMS_TO_TICKS(100); for (;;) { do_sample(); /* 阻塞到 xLastWake + period,并自动更新 xLastWake */ vTaskDelayUntil(&xLastWake, period); } /* → 真实周期恒为 100ms */ }
FreeRTOS V10.4 起 vTaskDelayUntil() 被推荐替换为返回布尔值的 xTaskDelayUntil(),后者在"延时时间已经过去"时返回 pdFALSE,便于检测周期超载。旧名仍向后兼容。pdMS_TO_TICKS(ms) 宏把毫秒换算成 tick 数,永远用它而不要手写数字,代码才能不依赖 configTICK_RATE_HZ。
#include "FreeRTOS.h" #include "task.h" TaskHandle_t workerHandle = NULL; void WorkerTask(void *arg) { for (;;) { do_work(); vTaskDelay(pdMS_TO_TICKS(50)); } } void ManagerTask(void *arg) { /* 动态创建一个任务;返回 pdPASS 表示成功 */ if (xTaskCreate(WorkerTask, "WORK", 256, NULL, 2, &workerHandle) != pdPASS) { printf("create failed: heap not enough\r\n"); } vTaskDelay(pdMS_TO_TICKS(1000)); vTaskSuspend(workerHandle); /* 暂停 worker */ vTaskDelay(pdMS_TO_TICKS(1000)); vTaskResume(workerHandle); /* 恢复 worker */ /* 动态调整优先级 */ vTaskPrioritySet(workerHandle, 3); vTaskDelay(pdMS_TO_TICKS(2000)); vTaskDelete(workerHandle); /* 删除,内存由空闲任务回收 */ workerHandle = NULL; vTaskDelete(NULL); /* 删除自己 */ }
删除其他任务时,其 TCB 和栈由空闲任务异步回收,因此删除后不会立刻释放内存——若空闲任务长期得不到运行,内存不会归还。删除任务自己(传 NULL)时也是如此。所以低优先级的空闲任务不应被长时间饿死。此外,删除任务前若它持有互斥量或未关闭外设,会造成资源泄漏,需自行清理。
安全关键领域常禁止动态分配。xTaskCreateStatic() 让你自己提供栈和 TCB 内存,编译期即确定,运行期零堆分配:
/* 需 configSUPPORT_STATIC_ALLOCATION = 1 */ #define STACK_WORDS 256 static StackType_t xStack[STACK_WORDS]; /* 用户自备栈 */ static StaticTask_t xTCB; /* 用户自备 TCB */ void setup(void) { TaskHandle_t h = xTaskCreateStatic( WorkerTask, "WORK", STACK_WORDS, NULL, 2, xStack, &xTCB); /* 返回句柄,永不为空 */ }
FreeRTOS 任务有四态:运行、就绪、阻塞(等事件+超时)、挂起(脱离调度)。优先级数值越大越高,调度器永远运行最高优先级的就绪任务。抢占式(configUSE_PREEMPTION=1)让高优先级就绪即抢占,保证实时性,是默认选择;协作式只在主动让出时切换。同优先级任务在时间片轮转(configUSE_TIME_SLICING)下均分 CPU。vTaskDelay 是相对延时会漂移,vTaskDelayUntil(新名 xTaskDelayUntil)是绝对延时锁定精确周期,采样任务必用后者,并用 pdMS_TO_TICKS() 换算。任务管理 API:xTaskCreate(动态)/xTaskCreateStatic(静态,零堆分配)创建,vTaskDelete 删除(内存由空闲任务异步回收)、vTaskSuspend/Resume 挂起恢复、vTaskPrioritySet 改优先级。空闲任务是优先级 0 的自动任务,负责回收内存并挂载 vApplicationIdleHook 等钩子,不能被长期饿死。