https://www.freertos.org/zh-cn-cmn-s/Documentation/00-OverviewFreeRTOS 官方文档: https://www.freertos.org/zh-cn-cmn-s/Documentation/00-Overview

FreeRTOS任务管理简介

FreeRTOS 核心是多任务调度,任务管理则是对任务的「创建、调度、状态切换、删除」等全生命周期操作的管控,核心目标是让多个任务按优先级有序共享 CPU 资源,实现实时响应。

一、任务的核心概念

任务是什么?

  • 任务是 FreeRTOS 中最小的执行单元,本质是一段独立的无限循环代码(通常包含业务逻辑,如 “LED 闪烁”“数据处理”),拥有自己的栈空间(存储局部变量、寄存器状态)和优先级
void taskfunction(void *argument)
{
	for(;;)
	{
		printf("task runing!\n");
		//延时	TICK_RATE_HZ ---1000
		vTaskDelay(1000);
	}
}

任务的优先级

FreeRTOS 任务优先级是 任务调度的核心依据,直接决定任务抢占 CPU 的权限 —— 高优先级任务能打断低优先级任务执行,同优先级任务按时间片轮流执行。

#define configMAX_PRIORITIES                     ( 56 )
  • 默认范围:0(最低优先级)~ configMAX_PRIORITIES - 1(最高优先级);
  • 数值含义:数值越大,优先级越高(与部分 RTOS 相反,需重点注意);

任务管理核心 API(常用操作) 

一、创建任务

创建任务
xTaskCreate
xTaskCreateStatic

1.xTaskCreate()函数

函数原型:
BaseType_t xTaskCreate(	TaskFunction_t pxTaskCode,
							const char * const pcName,		/*lint !e971 Unqualified char types are allowed for strings and single characters only. */
							const configSTACK_DEPTH_TYPE usStackDepth,
							void * const pvParameters,
							UBaseType_t uxPriority,
							TaskHandle_t * const pxCreatedTask )
函数功能:
动态创建任务(推荐,资源自动分配 / 释放)
参数说明:
TaskFunction_t pxTaskCode
类型:TaskFunction_t 是 FreeRTOS 定义的任务函数指针类型,原型为 void (*TaskFunction_t)(void *pvParameters);
const char * const pcName
类型:常量字符串指针(双重 const 表示字符串内容和指针本身都不可修改);
作用:任务名称,仅用于 调试和追踪(如通过调试工具查看任务列表),对任务运行无实际影响;
要求:长度建议不超过 configMAX_TASK_NAME_LEN(FreeRTOSConfig.h 中配置,默认 16 个字符);
const configSTACK_DEPTH_TYPE usStackDepth
类型:configSTACK_DEPTH_TYPE 是 FreeRTOS 定义的栈深度类型(本质是 uint16_t 或 uint32_t,由移植层决定);
作用:指定任务栈的大小(单位:StackType_t 元素个数,而非字节数);
可通过 configCHECK_FOR_STACK_OVERFLOW 启用栈溢出检测,避免栈太小导致崩溃。
void * const pvParameters
类型:void* 通用指针(可传递任意类型数据);
作用:创建任务时,向任务函数传递参数(任务函数的唯一参数);
无需传递参数:设为 NULL(最常用);
传递单个变量:直接取变量地址(需确保变量生命周期足够);
传递多个参数:用结构体封装,传递结构体地址;
UBaseType_t uxPriority
类型:UBaseType_t 是 FreeRTOS 定义的无符号基础类型(32 位架构下为 uint32_t);
作用:指定任务的优先级;
高优先级任务会抢占低优先级任务,同优先级任务按时间片轮流执行;
避免将过多任务设为最高优先级(会导致低优先级任务 “饿死”)。
TaskHandle_t * const pxCreatedTask
类型:TaskHandle_t(任务句柄类型)的指针,属于「输出参数」;
作用:任务创建成功后,FreeRTOS 会将该任务的句柄写入该指针指向的变量,后续可通过该句柄操作任务(如挂起、删除、修改优先级);
需后续操作任务:定义 TaskHandle_t 变量,传递其地址(如 &xLEDTaskHandle);
无需后续操作:设为 NULL(无需保存句柄)。

2.xTaskCreateStatic()函数

函数原型:
TaskHandle_t xTaskCreateStatic(	TaskFunction_t pxTaskCode,
									const char * const pcName,		/*lint !e971 Unqualified char types are allowed for strings and single characters only. */
									const uint32_t ulStackDepth,
									void * const pvParameters,
									UBaseType_t uxPriority,
									StackType_t * const puxStackBuffer,
									StaticTask_t * const pxTaskBuffer )
函数功能:
动态创建的 xTaskCreate() 核心区别是:任务栈和任务控制块(TCB)的内存需用户手动分配(静态内存,如全局数组、静态数组),不依赖 FreeRTOS 动态堆。
参数说明:
TaskFunction_t pxTaskCode
类型:TaskFunction_t 是 FreeRTOS 定义的任务函数指针类型,原型为 void (*TaskFunction_t)(void *pvParameters);
const char * const pcName
类型:常量字符串指针(双重 const 表示字符串内容和指针本身都不可修改);
作用:任务名称,仅用于 调试和追踪(如通过调试工具查看任务列表),对任务运行无实际影响;
要求:长度建议不超过 configMAX_TASK_NAME_LEN(FreeRTOSConfig.h 中配置,默认 16 个字符);
const configSTACK_DEPTH_TYPE usStackDepth
类型:configSTACK_DEPTH_TYPE 是 FreeRTOS 定义的栈深度类型(本质是 uint16_t 或 uint32_t,由移植层决定);
作用:指定任务栈的大小(单位:StackType_t 元素个数,而非字节数);
可通过 configCHECK_FOR_STACK_OVERFLOW 启用栈溢出检测,避免栈太小导致崩溃。
void * const pvParameters
类型:void* 通用指针(可传递任意类型数据);
作用:创建任务时,向任务函数传递参数(任务函数的唯一参数);
无需传递参数:设为 NULL(最常用);
传递单个变量:直接取变量地址(需确保变量生命周期足够);
传递多个参数:用结构体封装,传递结构体地址;
UBaseType_t uxPriority
类型:UBaseType_t 是 FreeRTOS 定义的无符号基础类型(32 位架构下为 uint32_t);
作用:指定任务的优先级;
高优先级任务会抢占低优先级任务,同优先级任务按时间片轮流执行;
避免将过多任务设为最高优先级(会导致低优先级任务 “饿死”)。
StackType_t * const puxStackBuffer
类型:StackType_t(任务栈元素类型,32 位架构下 = uint32_t)的指针;
作用:指向用户提前静态分配的任务栈缓冲区(如全局数组、静态数组),FreeRTOS 会直接使用该缓冲区作为任务栈,不再从动态堆分配;
要求:
必须是静态内存(全局变量、static 变量),不能是局部变量(函数退出后内存销毁,任务运行崩溃)。
StaticTask_t * const pxTaskBuffer
类型:StaticTask_t(静态任务控制块类型)的指针;
作用:指向用户提前静态分配的任务控制块(TCB),TCB 用于存储任务的优先级、栈指针、状态等核心信息,FreeRTOS 不再从动态堆分配 TCB;
要求:
必须是静态内存(全局变量、static 变量),不能是局部变量。

二、删除任务

vTaskDelete()函数

函数原型
void vTaskDelete( TaskHandle_t xTaskToDelete )
函数功能:
从 RTOS 内核管理中移除任务。要删除的任务将从所有就绪、 阻塞、挂起和事件列表中移除。
参数说明:
TaskHandle_t xTaskToDelete
要删除的任务的句柄。如果传递 NULL,会删除调用任务。
注:空闲任务负责释放由 RTOS 内核分配给已删除任务的 内存。因此,如果应用程序调用了 
vTaskDelete(),请务必确保空闲任务获得足够的微控制器处理时间。任务代码分配的内存不会自动释放, 应在任务删除之前手动释放。

三、任务的挂起和恢复

1.vTaskSuspend()函数

函数原型
void vTaskSuspend( TaskHandle_t xTaskToSuspend )
函数功能:
挂起任意任务。无论任务优先级如何,任务被挂起后将永远无法获取任何微控制器处理时间
对 vTaskSuspend的调用不会累积次数
参数说明:
TaskHandle_t xTaskToSuspend
被挂起的任务句柄。传递空句柄将导致调用任务被挂起。

2.vTaskResume()函数

函数原型
void vTaskResume( TaskHandle_t xTaskToResume )
函数功能:
恢复已挂起的任务,因一次或多次调用 vTaskSuspend() 而挂起的任务可通过单次调用
参数说明:
TaskHandle_t xTaskToResume
需要恢复挂起任务的句柄

四、任务延时

1.vTaskDelay()函数

函数原型
void vTaskDelay( const TickType_t xTicksToDelay )
函数功能:
调用该函数后,当前任务会放弃CPU使用权,进入阻塞状态,直到指定的tick数过去后才会重新进入就绪
状态,等待调度器再次调度。
注:vTaskDelay实现的是相对延时,即从调用函数的时刻开始计算延时时间

参数说明:
const TickType_t xTicksToDelay
表示阻塞的时钟节拍数(tick),而非实际时间(毫秒/秒)
延时时间 = 节拍数 × 系统节拍周期(configTICK_RATE_HZ 决定)
//#define configTICK_RATE_HZ                       ((TickType_t)1000)

2.vTaskDelayUntil()函数

函数原型
void vTaskDelayUntil( TickType_t * const pxPreviousWakeTime, 
                    const TickType_t xTimeIncrement )
函数功能:
将任务延迟到指定时间。此函数可以由周期性任务使用, 来确保恒定的执行频率。
参数说明:
TickType_t * const pxPreviousWakeTime
类型:TickType_t(系统节拍计数类型,32 位架构下为 uint32_t)的指针;
作用:
输入:存储「上一次任务唤醒 / 执行的时刻」(第一次调用需初始化);
输出:函数内部自动更新为「本次任务唤醒的时刻」,供下次调用使用;
要求:
必须是任务内的静态变量(static)或全局变量(需保持生命周期,记录上一次时间);
第一次调用前必须初始化(用 xTaskGetTickCount() 获取当前系统节拍数)。
const TickType_t xTimeIncrement
类型:TickType_t,表示任务的「固定执行间隔」(单位:系统节拍 Tick);
换算:实际间隔时间 = xTimeIncrement × 节拍周期(默认 1Tick=1ms,xTimeIncrement=1000 对应 1 秒);
要求:间隔必须大于任务单次执行的耗时(否则任务会一直运行,无阻塞时间)。

绝对延时 vs 相对延时

  • vTaskDelay(x)(相对延时):从「函数调用时刻」开始计时,延时 x 个节拍后唤醒。若任务被高优先级任务抢占,实际执行间隔会变长(累计误差);
    • eg:任务 A 每 1000ms 执行一次,若某次被高优先级任务抢占 200ms,下次执行间隔会变成 1200ms;
  • vTaskDelayUntil(&prev, x)(绝对延时):以「上一次唤醒时刻」为基准,计算下一次唤醒时刻(prev + x),确保任务执行间隔严格为 x 个节拍,无累计误差;
    • eg:任务 A 上一次唤醒时刻是 t0,调用后会阻塞到 t0 + 1000 时刻唤醒,即使被抢占,后续仍按固定间隔执行。
对比维度 vTaskDelayUntil(&xLastWakeTime, xPeriod) vTaskDelay(xTicks)
延时类型 绝对延时(基于上一次唤醒时刻) 相对延时(基于当前调用时刻)
累计误差 无(严格按固定周期执行) 有(被抢占或业务耗时会导致间隔变长)
适用场景 周期性任务(如数据采集、设备控制、定时刷新) 非周期性延时(如 LED 闪烁、等待某个事件后延时)
参数要求 需初始化 static 变量记录上一次唤醒时间 直接传入延时节拍数,无额外初始化
任务阻塞逻辑 阻塞到「上一次唤醒时刻 + 周期」 阻塞「当前时刻 + 延时」个节拍

五、任务状态转换

FreeRTOS 任务有 5 种核心状态,调度器的核心工作就是触发状态切换,确保高优先级任务优先执行。

typedef enum
{
	eRunning = 0,	//任务自身查询自己的状态时,返回此值(仅当前正在占用 CPU 执行的任务会自报该状态)
	eReady,			//任务已就绪,等待 CPU 调度(处于「就绪列表」或「挂起就绪列表」,具备执行条件但暂未获得 CPU)
	eBlocked,		//任务阻塞状态(因等待资源、信号量、延时等主动放弃 CPU,需等待特定事件触发才能恢复就绪)
	eSuspended,		//任务挂起状态:1. 主动调用 vTaskSuspend() 挂起;2. 阻塞状态且超时时间为无限(portMAX_DELAY)
	eDeleted,		//任务已被删除(调用 vTaskDelete()),但对应的 TCB(任务控制块)尚未被系统回收释放
	eInvalid			//无效状态(用于错误处理)
} eTaskState;
状态 说明 触发条件示例
就绪态(Ready) 任务已具备执行条件,等待调度器分配 CPU 任务创建后、阻塞延时结束、被唤醒
运行态(Running) 任务正在占用 CPU 执行代码(同一时刻,仅 1 个任务处于运行态) 调度器选择就绪态中最高优先级任务
阻塞态(Blocked) 任务暂时无法执行(如延时、等待队列 / 信号量),不占用 CPU 资源 vTaskDelay()xQueueReceive()
挂起态(Suspended) 任务被强制暂停,需通过特定 API 唤醒,不占用 CPU 资源 vTaskSuspend() 调用
休眠态(Deleted) 任务被删除,资源(栈、堆)被释放(仅动态创建的任务支持) vTaskDelete() 调用

六、任务调度

什么是任务调度?

FreeRTOS 中的任务调度,本质是「操作系统内核(调度器)按预设规则,在多个任务间分配 CPU 执行权」的过程 —— 简单说就是 “决定哪个任务该占用 CPU 运行、哪个任务该暂停等待”,最终实现 “多任务并发执行” 的效果(实际是 CPU 快速切换任务,宏观上看起来多个任务同时运行)。

FreeRTOS中开启任务调度的函数是 vTaskStartScheduler() ,但在CubeMX中被封装为osKernelStart()
osStatus_t osKernelStart (void) {
  osStatus_t stat;

  if (IS_IRQ()) {
    stat = osErrorISR;
  }
  else {
    if (KernelState == osKernelReady) {
      KernelState = osKernelRunning;
      vTaskStartScheduler();
      stat = osOK;
    } else {
      stat = osError;
    }
  }

  return (stat);
}

任务调度机制(核心原理)

FreeRTOS 调度器的核心是「抢占式调度 + 时间片调度」,确保实时性:

  • 抢占式调度(默认启用)

    • 高优先级任务就绪时,会立即抢占低优先级任务的 CPU 资源(无论低优先级任务是否执行完);
    • 例:任务 A(优先级 5)正在运行,任务 B(优先级 8)延时结束进入就绪态,调度器会立即切换到任务 B 执行。
  • 时间片调度(默认启用)

    • 同优先级任务轮流占用 CPU,每个任务的时间片长度 = 系统节拍周期(默认 1ms);
    • 例:两个优先级为 5 的任务,会交替执行,每个任务每次执行 1ms。
  • 协作式调度:

    • 抢占式调度和时间片轮转可以同时存在,当有高优先级任务就绪时,运行高优先级任务;当最高优先级的任务有好几个时,这几个任务可以以时间片轮转方式调度。
FreeRTOS中,如果一个任务在时间片没有用完的情况下变为阻塞状态,它将释放时间片剩余时间,并且调度器会利用这部分时间来执行其他任务。具体来说,以下是处理这种情况的步骤:
  • 任务阻塞:当任务执行到阻塞操作(如等待事件、信号量、队列等)时,它会停止执行并进入阻塞状态。
  • 时间片回收:一旦任务阻塞,它将不再消耗CPU时间。此时,调度器会回收该任务剩余的时间片。
  • 调度其他任务:调度器会利用回收的时间片来调度其他就绪的任务。这可能包括同一优先级的任务,也可能包括更高优先级的任务(如果存在的话)。
  • 任务就绪:当阻塞的任务等待的条件得到满足时(例如,它等待的事件或信号量被触发),它将变为就绪状态,并被放入相应的就绪列表。
  • 重新调度:调度器在下一个合适的时机(例如,当前执行的任务完成或时间片用完)会重新调度该任务,它将从上次停止执行的地方继续执行。
通过这种方式,FreeRTOS确保了系统资源的有效利用,避免了因单个任务长时间占用CPU而造成的其他任务饥饿问题。这也体现了FreeRTOS的抢占式调度特性,即任何时刻,CPU总是由当前可运行的最高优先级任务占用。如果当前任务阻塞,调度器会立即抢占CPU,给其他就绪的任务一个运行的机会。

任务调度的核心流程(调度器工作步骤)

调度器的工作可概括为 “检查 → 选择 → 切换” 三步,在 SysTick中断或任务状态变化时触发:

  • 检查任务状态
    • 遍历所有任务,更新任务状态;
    • 标记每个优先级的 “就绪任务”。
  • 选择下一个执行任务
    • 用位运算快速找到「最高优先级的就绪任务」;
    • 若该优先级只有一个就绪任务,直接选中;若多个,选中 “下一个时间片对应的任务”。
  • 任务上下文切换
    • 保存当前运行任务的上下文(寄存器值、栈指针、程序计数器 PC 等)到该任务的栈空间;
    • 从选中任务的栈空间中恢复其上下文(恢复寄存器、栈指针、PC);
    • CPU 开始执行选中任务的代码。

任务切换的触发→PendSV 中断→切换执行

任务(上下文)切换

FreeRTOS 中的上下文切换(Context Switch),是调度器实现多任务并发的核心技术 —— 本质是「保存当前运行任务的 “执行状态快照”,并恢复下一个要执行任务的 “快照”」的过程。通过快速切换,CPU 能在多个任务间切换执行,宏观上呈现 “多任务同时运行” 的效果。

简单说:上下文切换 = 保存当前任务状态 + 恢复目标任务状态,整个过程由 FreeRTOS 内核(移植层)自动完成,用户无需手动干预。

当切换触发时,CPU 首先禁用 CPU 中断(避免切换过程被打断,确保原子性)执行以下操作(汇编实现,速度极快);

数据类型 具体内容 存储位置
寄存器状态 CPU 通用寄存器(R0~R15)、程序计数器(PC,记录下一条要执行的指令地址)、状态寄存器(PSR,记录 CPU 状态)等 任务专属栈空间
任务控制信息 栈指针(SP,指向任务栈当前位置)、任务优先级、任务状态(就绪 / 阻塞等) 任务控制块(TCB)

栈空间是任务上下文的 “存储容器”,TCB 是任务状态的 “管理中枢”,汇编代码负责高效操作 CPU 寄存器,确保切换速度。

任务切换的触发
 系统节拍中断触发(被动切换)
  • 触发源:SysTick 定时器(系统节拍)周期性中断(默认 1ms / 次);
  • 触发场景:
    • 同优先级任务时间片到期(需切换到下一个同优先级任务);
    • 高优先级任务延时结束、等待的资源就绪(从阻塞态→就绪态,需抢占当前低优先级任务);
  • 特点:由硬件中断触发,属于 “被动切换”,是最常见的切换时机。
 任务主动触发(主动切换)
  • 触发源:任务调用特定 API 主动放弃 CPU 执行权;
  • 触发场景:
    • 任务调用 vTaskDelay() 进入阻塞态;
    • 任务调用 xQueueReceive()/xSemaphoreTake() 等待资源(队列 / 信号量为空),进入阻塞态;
    • 任务调用 taskYIELD() 主动请求切换(让同优先级其他任务执行);
  • 特点:由任务主动发起,属于 “主动切换”,触发后立即执行切换逻辑。
PendSV 中断

FreeRTOS 中任务(上下文)切换的核心执行逻辑,正是在 PendSV 中断的服务函数(PendSV_Handler)中实现的——PendSV 中断被 FreeRTOS 专门设计为 “任务切换中断”,其核心价值是「确保任务切换在合适的时机(低优先级中断上下文)执行,不影响高优先级中断响应」。

为什么选择 PendSV 中断做任务切换?

PendSV(Pendable Service Call,可挂起的系统调用)是 Cortex-M 架构内核自带的低优先级系统中断,FreeRTOS 选中它做任务切换的核心原因的是其独特的 “可挂起 + 低优先级” 特性,完美匹配任务切换的需求。

PendSV 是任务切换的 “最终执行者”
  • 调度器的核心是 “决定切换到哪个任务”(vTaskSwitchContext()),PendSV 中断的核心是 “执行切换动作”(保存 / 恢复上下文);
  • PendSV 的低优先级特性确保任务切换不会打断高优先级中断,是 FreeRTOS 实时性的关键保障;
  • PendSV_Handler 是汇编实现的,直接操作 CPU 寄存器和栈指针,确保切换效率(几微秒级);
  • 所有任务切换请求(无论主动还是被动),最终都会通过触发 PendSV 中断,在 PendSV_Handler 中完成实际切换。
任务切换的触发→PendSV 中断→切换执行完整关联流程
1.触发任务切换请求(置位 PendSV)

当需要任务切换触发后,调度器会调用 vPortYield()(或 portYIELD_FROM_ISR()),通过软件置位 PENDSVSET 位,发起 PendSV 中断请求。

2.PendSV 中断响应(进入 PendSV_Handler

当没有更高优先级中断执行时,CPU 响应 PendSV 中断,进入 PendSV_Handler 服务函数 —— 这是任务切换的 “执行入口”。

关键细节:

  • PendSV_Handler 是汇编函数(位于移植层 portasm.s),而非 C 函数,原因是需要直接操作 CPU 寄存器(保存 / 恢复上下文),汇编执行效率更高;
  • 进入中断后,CPU 会自动压栈部分寄存器(如 R0~R3、R12、PC、PSR),但 FreeRTOS 会在中断服务函数中补充保存剩余寄存器,确保上下文完整。
3.在 PendSV_Handler 中完成上下文切换

PendSV_Handler 的核心工作就是「完整保存当前任务上下文 + 恢复目标任务上下文」

具体汇编逻辑(以 Cortex-M3 为例):

PendSV_Handler:
    ; 1. 保存当前任务的上下文(补充保存 CPU 未自动压栈的寄存器)
    MRS     R0, PSP                 ; 读取当前任务的栈指针(PSP,任务模式栈指针)
    STMFD   R0!, {R4-R11}           ; 将 R4~R11 寄存器压入当前任务栈(CPU 未自动压栈)
    LDR     R1, =pxCurrentTCB       ; 读取当前任务的 TCB 指针
    STR     R0, [R1]                ; 更新 TCB 中的栈指针(保存当前栈位置)

    ; 2. 调用调度器函数 vTaskSwitchContext(),选择下一个要执行的任务(C 函数)
    PUSH    {LR}                    ; 保存返回地址(LR)
    BL      vTaskSwitchContext      ; 调度器选择目标任务,更新 pxCurrentTCB 为目标任务 TCB
    POP     {LR}                    ; 恢复 LR

    ; 3. 恢复目标任务的上下文
    LDR     R0, =pxCurrentTCB       ; 读取目标任务的 TCB 指针
    LDR     R0, [R0]                ; 读取目标任务的栈指针
    LDMFD   R0!, {R4-R11}           ; 从目标任务栈中弹出 R4~R11 寄存器
    MSR     PSP, R0                 ; 恢复目标任务的栈指针(PSP)

    ; 4. 退出 PendSV 中断,恢复 CPU 自动压栈的寄存器,执行目标任务
    BX      LR                      ; 退出中断,PC 指向目标任务的下一条指令

结语:

无论你是初学者还是有经验的开发者,我希望我的博客能对你的学习之路有所帮助。如果你觉得这篇文章有用,不妨点击收藏,或者留下你的评论分享你的见解和经验,也欢迎你对我博客的内容提出建议和问题。每一次的点赞、评论、分享和关注都是对我的最大支持,也是对我持续分享和创作的动力

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐