跳转到内容
新建笔记

FreeRTOS 任务创建、调度与状态管理

FreeRTOS 任务是由调度器管理的执行流。每个任务都有自己的栈和任务控制块,借助句柄访问公共 API。本文以单核、抢占式 FreeRTOS-Kernel V11.1.0 为例;合作式调度、SMP 及不同移植的行为需另查配置。

前后台循环与固定优先级抢占式任务的执行示意,均包含中断处理

图左用主循环依次处理工作,图右允许高优先级就绪任务抢占低优先级任务。图中关于前后台“实时性差”的标签需要结合负载理解:足够短且有界的循环也能满足时限;RTOS 同样不会自动消除长临界区、优先级反转和 CPU 超载。

任务优先级范围为 0 到 configMAX_PRIORITIES - 1,数值越大,任务优先级越高。这与 Cortex-M NVIC 中断优先级的数值方向不同。高优先级任务应在无工作时阻塞,让其他任务获得 CPU;持续占用 CPU 的最高优先级任务可使低优先级任务长期无法运行。

创建任务:动态内存、静态内存与寿命

跳转到“创建任务:动态内存、静态内存与寿命”

xTaskCreate() 动态申请 TCB 和栈,返回状态并可输出句柄。xTaskCreateStatic() 使用调用者提供的 TCB 和栈,成功时直接返回句柄。相应接口必须在配置中启用;静态任务的内存以及任务参数指向的对象都必须活到任务结束,不能使用即将离开函数的自动局部数组。

下面保留 LED 优先级 2、KEY 优先级 3 的配置意图,改为调度器启动前统一创建并检查失败。外部任务函数由应用实现,必须循环运行或调用 vTaskDelete(NULL),不能普通 return。

#include "FreeRTOS.h"
#include "task.h"
extern void LED_Task(void *argument);
extern void KEY_Task(void *argument);
extern void Monitor_Task(void *argument);
static TaskHandle_t led_handle;
static TaskHandle_t key_handle;
static TaskHandle_t monitor_handle;
static StaticTask_t monitor_tcb;
static StackType_t monitor_stack[256];
BaseType_t app_tasks_create_before_scheduler(void)
{
if (xTaskCreate(LED_Task, "LED_Task", 512, NULL, 2,
&led_handle) != pdPASS) {
return pdFAIL;
}
if (xTaskCreate(KEY_Task, "KEY_Task", 512, NULL, 3,
&key_handle) != pdPASS) {
vTaskDelete(led_handle);
led_handle = NULL;
return pdFAIL;
}
monitor_handle = xTaskCreateStatic(Monitor_Task, "Monitor", 256,
NULL, 1, monitor_stack,
&monitor_tcb);
if (monitor_handle == NULL) {
vTaskDelete(key_handle);
vTaskDelete(led_handle);
key_handle = NULL;
led_handle = NULL;
return pdFAIL;
}
return pdPASS;
}

此函数由应用调用一次。512 和 256 是栈元素数,不是字节数;GCC ARM_CM3 中分别为 2048 和 1024 字节。它们仍是待实测的初值,不是 HAL、打印或浮点代码的通用安全值。原学习例子中的 100 元素也不能直接作为所有任务的栈预算。应结合栈溢出检测、最坏调用路径及运行水位测量调整。

任务控制块、任务栈与向低地址增长方向的历史示意

图中栈向低地址增长符合本文 ARM_CM3 移植,但 heap_1、heap_4、静态分配等不保证图示 TCB/栈紧邻布局。任务创建顺序、栈地址高低和“最后创建的是 Task3”都不能单独推导谁先运行;调度器看就绪任务的优先级和调度策略。

删除任务与初始化任务

跳转到“删除任务与初始化任务”

vTaskDelete(other_handle) 删除指定任务,vTaskDelete(NULL) 删除调用任务自身。使用 INCLUDE_vTaskDelete = 1。任务“创建了另一个任务”不建立自动删除的父子关系:初始化任务删除后,已创建的 LED/KEY 任务仍独立存在。

自删除任务的动态内核资源通常由 Idle 任务后续回收;应用自行申请的缓冲区、持有的互斥量和外设资源不会自动得到业务层清理。删除其他任务也必须先协调其资源和通信对象,不能把强制删除当成通用取消协议。

原初始化示例在 taskENTER_CRITICAL() 后自删除,再写 taskEXIT_CRITICAL(),这破坏了成对退出的要求。更简单的方式是采用上面的启动前创建;若必须使用初始化任务,先完成初始化、处理所有失败并离开临界区,再在最后自删除,且后面不再安排需要执行的清理代码。

Tick、时间片与周期任务

跳转到“Tick、时间片与周期任务”

Tick 时刻 t1、t2、t3 与相邻 Tick 周期的示意

Tick 的名义周期为 1/configTICK_RATE_HZ1/\text{configTICK\_RATE\_HZ}。100 Hz 是 10 ms,1000 Hz 是 1 ms。原 Win32 模拟工程中的 1000 Hz、configMINIMAL_STACK_SIZE = 60 带有宿主线程栈等特定假设,不能照搬为 STM32 的堆栈或实时性依据。

调度器不是每个 Tick 轮询调用一遍所有任务。Tick 会推进超时等内核状态;任务阻塞、主动让出、创建更高优先级任务,以及 ISR 唤醒任务,也可能触发调度。开启抢占和 configUSE_TIME_SLICING 后,同优先级就绪任务可在 Tick 驱动下轮转;高优先级未阻塞任务不会因为低优先级任务“轮到时间片”而被它抢占。

vTaskDelay(n) 从当前时刻等待相对 Tick 数;xTaskDelayUntil() 以保存的基准推进周期释放时刻。后者减少“工作时间叠加进周期”的漂移,但不能保证每次都在该时刻真正获得 CPU,更不能弥补执行超时。

#include "FreeRTOS.h"
#include "task.h"
extern void sample_inputs(void);
void periodic_input_task(void *argument)
{
(void)argument;
const TickType_t period = pdMS_TO_TICKS(10);
configASSERT(period > 0);
TickType_t release = xTaskGetTickCount();
for (;;) {
sample_inputs();
/* 返回 pdFALSE 可能说明下一个释放时刻已经到达。 */
(void)xTaskDelayUntil(&release, period);
}
}

这里的接口需要 INCLUDE_xTaskDelayUntil = 1。旧版或兼容宏常见名称 vTaskDelayUntil,应按项目版本使用。短延时可能因调用发生在 Tick 边界附近而少于完整的 N 个 Tick 周期;被解除阻塞也只是进入就绪状态。

FreeRTOS 就绪、运行、阻塞和挂起状态图,包含中文九类转换及英文 API 标注

状态含义
Ready,就绪可以执行,等待获得 CPU
Running,运行当前正在占用 CPU;本文单核同一时刻只有一个运行任务
Blocked,阻塞等待延时、队列、信号量或通知等条件,可能带超时
Suspended,挂起显式暂停,不由普通等待超时恢复;调试器仍可观察它

按原图序号理解这些逻辑转换:

  1. 创建成功后进入就绪集合;若调度器已启动且允许抢占,可能很快运行。
  2. 调度器选中就绪任务,使其运行。
  3. 运行任务被更高优先级任务抢占,或在适用配置下让出同级 CPU,回到就绪状态。
  4. 运行任务请求延时或等待未就绪的同步对象,进入阻塞状态。显式 vTaskSuspend() 对应挂起,不能与阻塞混称。
  5. 等待条件满足或超时,阻塞任务重新就绪;是否立即运行还取决于优先级和调度状态。
  6. 对就绪任务调用 vTaskSuspend(),使其挂起。
  7. 对阻塞任务调用 vTaskSuspend(),取消其当前等待调度状态并挂起;应用须自行协调业务协议。
  8. 运行任务可 vTaskSuspend(NULL) 自行挂起。
  9. vTaskResume() 或符合中断约束的 xTaskResumeFromISR() 可恢复显式挂起任务。正确的 ISR 接口名以 x 开头。

这是应用层状态模型;内核内部可能使用挂起链表承载无限期等待等实现细节,不能仅凭内部链表名推断应用状态。

API关键约束
vTaskSuspend() / vTaskResume()操作指定任务的显式挂起状态;启用 INCLUDE_vTaskSuspend
xTaskResumeFromISR()ISR 版本,返回是否需要请求切换;核对中断优先级限制
vTaskSuspendAll() / xTaskResumeAll()暂停/恢复调度器,可嵌套;当前任务仍执行,中断仍可发生
vTaskDelete()删除任务;先清理和协调应用资源
vTaskDelay()以 Tick 为单位的相对阻塞等待
xTaskDelayUntil()基于上次计划释放时刻的周期等待

vTaskSuspendAll() 并不是把所有任务逐个变成 Suspended,其配对函数也不是 vTaskResume()。调度器锁定期间不要调用可能阻塞或切换任务的 API。

“恢复”不会为将来的一次挂起储存事件。如果中断先执行恢复,而任务随后才挂起,唤醒会丢失。需要保存事件时使用队列、信号量或任务通知,并明确采用计数、位或覆盖值语义。

任务临界区适合短小、不能阻塞的操作,进入退出必须严格成对。在本文 Cortex-M3 移植中,FreeRTOS 主要通过 BASEPRI 屏蔽允许调用内核的中断;更高紧迫性的中断仍能执行。因此临界区不能自动保护与这些中断共享的数据。

互斥量用于任务之间的资源所有权,可在等待时阻塞,并可提供优先级继承;ISR 不能获取或释放普通互斥量。调度器锁也不会阻止中断访问共享数据。选择方法前先说明“哪些任务和中断会访问对象”,再控制保护范围,不在临界区内打印、等待外设或阻塞。