任务(Task)是 FreeRTOS 中最核心的概念。每个任务都是一个独立的执行流,有自己的栈空间和优先级,调度器负责决定哪个任务获得 CPU。
任务状态
任务永远处于以下四种状态之一:
| 状态 | 含义 |
|---|---|
| 运行态(Running) | 当前正在占用 CPU 执行 |
| 就绪态(Ready) | 可以运行,正在等待 CPU |
| 阻塞态(Blocked) | 等待某个事件(延时到期、队列数据、信号量等) |
| 挂起态(Suspended) | 被显式挂起,调度器完全不考虑它 |
状态转换图
创建
│
▼
┌──────┐ 等待事件 ┌────────┐
│ 就绪 │──────────▶│ 阻塞 │
│(Ready)│◀──────────│(Blocked)│
└──┬───┘ 事件到达 └────────┘
│ │
调度器选中│ │(被抢占/时间片到)
▼ │
┌──────┐ │
│ 运行 │─────────────────┘
│(Run) │
└──┬───┘
│ vTaskSuspend()
▼
┌────────┐
│ 挂起 │ vTaskResume()
│(Suspend)│──────────▶ 就绪态
└────────┘
阻塞态的两种等待
任务阻塞时,通常在等两类事件:
- 时间事件:延时到期(如
vTaskDelay(500)),或到达绝对时间 - 同步事件:数据到达队列、信号量被释放、事件组 bit 被置位、收到任务通知等
任务可以同时等两种事件:设置超时时间。例如”等队列数据最多等 100ms”,100ms 内数据到了就处理,到了还没到就超时退出阻塞。
挂起态
进入挂起态只能通过
vTaskSuspend(),退出只能通过vTaskResume()或xTaskResumeFromISR()。大多数应用不使用挂起态。
创建任务
BaseType_t xTaskCreate(
TaskFunction_t pvTaskCode, // 任务函数指针(函数名)
const char * const pcName, // 任务名称(调试用)
configSTACK_DEPTH_TYPE usStackDepth, // 栈深度(字数,不是字节数)
void *pvParameters, // 传给任务的参数
UBaseType_t uxPriority, // 优先级(0最低,configMAX_PRIORITIES-1最高)
TaskHandle_t *pxCreatedTask // 出参:任务句柄(后续操作用)
);
返回值:pdPASS 创建成功;errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY(即 pdFAIL)堆内存不足。
栈深度单位
usStackDepth的单位是 字(word),不是字节。在 32 位 MCU(如 Cortex-M)上 1 字 = 4 字节。usStackDepth=128实际分配 512 字节栈空间。
六种创建 API
| API | 内存分配方式 | 说明 |
|---|---|---|
xTaskCreate() | 动态分配(堆) | 最常用 |
xTaskCreateStatic() | 静态分配(用户提供内存) | 不使用堆 |
xTaskCreateRestricted() | 动态+MPU限制 | 需要内存保护单元 |
xTaskCreateRestrictedStatic() | 静态+MPU限制 | |
xTaskCreateAffinitySet() | 动态+指定核心 | SMP 多核用 |
xTaskCreateStaticAffinitySet() | 静态+指定核心 | SMP 多核用 |
名称带
Static的版本需要你预先提供栈和 TCB 的内存,不从堆分配;不带Static的从堆里动态分配。
任务函数模板
void vMyTask(void *pvParameters)
{
// pvParameters 就是创建时传入的参数,可以强转成需要的类型
int myParam = (int)pvParameters;
for(;;) // 任务必须是死循环,不能 return
{
// 任务逻辑
vTaskDelay(pdMS_TO_TICKS(1000));
}
// 如果确实要退出,必须调用 vTaskDelete(NULL) 删除自己
vTaskDelete(NULL);
}
空闲任务(Idle Task)
调用 vTaskStartScheduler() 时,内核自动创建一个优先级为 0 的空闲任务,它永远处于就绪态(死循环),保证系统至少有一个任务能运行。
空闲任务的职责:
- 释放被删除任务的 TCB 和栈内存(如果任务自己删除自己,内存由空闲任务回收)
- 执行空闲钩子函数(如果配置了)
- 如果没有其他任务就绪,CPU 就跑空闲任务(可进入低功耗模式)
空闲钩子函数
通过 vApplicationIdleHook() 可以在空闲任务每次循环中插入自定义逻辑:
void vApplicationIdleHook(void)
{
// 每次空闲任务循环都会调用这里
// 可以放:低功耗进入、简单指示灯、CPU 利用率统计
}
需要在 FreeRTOSConfig.h 中设置 configUSE_IDLE_HOOK = 1。
空闲钩子的红线
- 绝不能阻塞或挂起(否则系统可能没有可运行任务)
- 如果应用任务调用了
vTaskDelete()删除自己,空闲钩子必须尽快返回,让空闲任务有机会清理内存- 尽量短小,不要做耗时操作
优先级管理
// 获取任务优先级,传 NULL 表示获取当前任务自己的优先级
UBaseType_t uxPriority = uxTaskPriorityGet(NULL);
// 设置任务优先级
vTaskPrioritySet(xTask1Handle, uxPriority + 1);
- 优先级范围:
0(最低)到configMAX_PRIORITIES - 1(最高) - 超过最大值会被自动截断到
configMAX_PRIORITIES - 1 - FreeRTOS 支持任务在运行中动态修改自己或其他任务的优先级
删除任务
// 需要 INCLUDE_vTaskDelete = 1
vTaskDelete(xTask1Handle); // 删除指定任务
vTaskDelete(NULL); // 删除自己
内存释放
- 内核自动分配的内存(TCB+栈)由空闲任务负责回收
- 任务自己分配的内存(
pvPortMalloc、打开的文件、外设句柄等)不会自动释放,必须在删除前手动释放- 因此,频繁创建/删除任务容易导致内存碎片,建议用”任务池”模式或静态创建
调度算法
FreeRTOS 是固定优先级抢占式调度器,通过两个配置项控制调度行为:
| 配置宏 | 值 | 效果 |
|---|---|---|
configUSE_PREEMPTION | 1 | 启用抢占(高优先级任务就绪后立即抢占低优先级) |
configUSE_PREEMPTION | 0 | 协作式(任务必须主动让出 CPU) |
configUSE_TIME_SLICING | 1 | 同优先级时间片轮转 |
configUSE_TIME_SLICING | 0 | 同优先级不轮转,直到任务主动阻塞或让出 |
三种调度组合:
| 调度方式 | 抢占 | 时间片 | 特点 |
|---|---|---|---|
| 抢占式+时间片 | ✅ | ✅ | 默认配置,最常用 |
| 抢占式+无时间片 | ✅ | ❌ | 高优先级立即抢占,同优先级不自动切换 |
| 协作式 | ❌ | 任意 | 必须任务主动让出,不推荐新项目使用 |
抢占式调度的关键规则
- 高优先级永远先执行:只要有高优先级任务处于就绪态,低优先级任务就不会获得 CPU
- 抢占随时发生:不仅在 Tick 中断中,中断退出时、释放信号量时、发送队列时都可能触发抢占
- 同优先级轮转:开启时间片后,同优先级任务每个 Tick 切换一次(时间片长度 = 1 / configTICK_RATE_HZ)
延时:vTaskDelay vs vTaskDelayUntil
// 相对延时:从调用时起,延迟 N 个 tick
vTaskDelay(pdMS_TO_TICKS(500));
// 绝对延时:保证固定周期执行,不会漂移
vTaskDelayUntil(&xLastWakeTime, pdMS_TO_TICKS(500));
周期性任务用 vTaskDelayUntil
vTaskDelay(500)是”这次执行完,再等 500ms”,如果任务执行本身花了时间,周期会越来越漂移。vTaskDelayUntil()记录上次唤醒时间,下次唤醒以这个时间点为基准计算,保证绝对周期精度。
下一步:03-队列
相关:04-中断处理 | 09-内存管理 | 10-FreeRTOSConfig配置 | 返回目录