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 的名义周期为 。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 周期;被解除阻塞也只是进入就绪状态。
状态及九类转换
跳转到“状态及九类转换”
| 状态 | 含义 |
|---|---|
| Ready,就绪 | 可以执行,等待获得 CPU |
| Running,运行 | 当前正在占用 CPU;本文单核同一时刻只有一个运行任务 |
| Blocked,阻塞 | 等待延时、队列、信号量或通知等条件,可能带超时 |
| Suspended,挂起 | 显式暂停,不由普通等待超时恢复;调试器仍可观察它 |
按原图序号理解这些逻辑转换:
- 创建成功后进入就绪集合;若调度器已启动且允许抢占,可能很快运行。
- 调度器选中就绪任务,使其运行。
- 运行任务被更高优先级任务抢占,或在适用配置下让出同级 CPU,回到就绪状态。
- 运行任务请求延时或等待未就绪的同步对象,进入阻塞状态。显式
vTaskSuspend()对应挂起,不能与阻塞混称。 - 等待条件满足或超时,阻塞任务重新就绪;是否立即运行还取决于优先级和调度状态。
- 对就绪任务调用
vTaskSuspend(),使其挂起。 - 对阻塞任务调用
vTaskSuspend(),取消其当前等待调度状态并挂起;应用须自行协调业务协议。 - 运行任务可
vTaskSuspend(NULL)自行挂起。 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 不能获取或释放普通互斥量。调度器锁也不会阻止中断访问共享数据。选择方法前先说明“哪些任务和中断会访问对象”,再控制保护范围,不在临界区内打印、等待外设或阻塞。