FreeRTOS任务调度
多任务运行机制
任务是实现某种功能的函数,里面通常有死循环
任何任务都不应该自己退出,不能有return
两种方法结束任务:
任务中跳出死循环,使用vtaskdelete删除自己,也就是在函数返回前,杀死自身
其他任务中调用vtaskdelete
多任务:
优先级数字越小,优先级越低
RTOS将CPU时间分为基本时间片systick定时器的定时周期
任务轮询时,交出CPU使用权的任务会把CPU当前任务的场景压到自己的栈空间
获得使用权的一方会把自己栈空间保存的数据恢复至CPU
任务状态
就绪状态 (与运行状态之间的转换叫做切入和切出)
任务被创建之后自动进入
运行状态
占有cpu并运行的状态,
让出cpu的方法
vtasksuspend()
执行阻塞式函数进入阻塞状态
阻塞状态
任务暂时让出cpu使用权,处于等待状态
时间延迟类函数:以 vTaskDelay()或 vTaskDelayUntil()为代表。运行状态任务调用后进入阻塞,延迟指定时长后转为就绪状态,参与调度后有机会再次运行。
事件请求类函数(进程间通信场景):以请求信号量的 xSemaphoreTake()为例。运行状态任务调用后进入阻塞,若其他任务释放信号量或等待超时,任务从阻塞状态转为就绪状态
运行状态的任务调用 vTaskSuspend(),可将处于阻塞状态的任务转入挂起状态。
挂起状态
(通常指运行、就绪、阻塞状态),可通过函数 vTaskSuspend()进入挂起状态。
挂起状态无法自动解除,需由其他任务调用 vTaskResume()函数,才能让挂起的任务重新转为就绪状态,进而参与调度器的调度。
优先级
每个任务必须设置优先级,优先级总数由 FreeRTOSConfig.h中宏 configMAX_PRIORITIES定义,默认值为 56。
优先级数值越小,优先级越低(最低优先级为 0,最高优先级为 configMAX_PRIORITIES - 1)。
任务创建时需设置初始优先级,运行过程中可修改优先级;多个任务允许拥有相同优先级。
若开发板处理器为 STM32F407,FreeRTOS 接口通常配置为 CMSIS - RTOS V2,此时在 CubeMX 中 USE_PORT_OPTIMISED_TASK_SELECTION参数为不可修改状态,默认始终为 Disabled0(即采用通用方法)。
空闲任务
空闲任务优先级是0
FreeRTOS要求“任何时刻必须有任务处于运行状态”。若用户创建的所有任务都因阻塞(如等待信号量、延时等)无法运行时,空闲任务会自动占用CPU,维持系统的“运行态”需求。
FreeRTOS基础时钟与嘀嗒信号解析
其定时频率由内核配置参数 configTICK_RATE_HZ设定,默认值为 1000,即 SysTick 每 1ms 触发一次中断
函数 xTaskGetTickCount()用于读取 xTickCount的当前值,可获取系统运行的“滴答计数”,反映时间流逝。
SysTick 定时器的中断不仅用于生成嘀嗒信号,还承担任务切换申请的功能
任务调度
方法概述
抢占式(时间片可选)和合作式

采用时间片的抢占式调度方法
FreeRTOS的上下文切换(任务切换)是通过PendSV中断实现的,而SysTick中断负责触发调度请求(通过置位PendSV标志)。在pendsv中断里处理上下文切换
FreeRTOS的任务切换优先级总是低于系统中断优先级
在FreeRTOS的抢占式调度(configUSE_PREEMPTION = 1,默认开启)机制下,不需要等待当前时间片结束——高优先级任务一旦进入就绪态,会立即抢占低优先级任务的CPU使用权,直接切换运行。
高优先级会插队,同优先级会用时间片
不使用时间片的抢占式调度方法
就绪态也会有顺序,对于两个就绪状态。采用先进先出的调度方法
合作式调度任务
FreeRTOS 不主动发起上下文切换,
运行状态的任务进入阻塞状态,运行状态的任务主动调用 taskYIELD()函数。
两种情况下进行一次上下文切换
taskYIELD()`函数的作用:
该函数是任务主动申请上下文切换的“信号”——调用后,调度器会立即执行一次上下文切换,将CPU使用权转移给其他就绪任务
函数介绍
FreeRTOS 中任务管理涵盖任务生命周期操作(创建、删除、挂起、恢复)、调度器控制(启动、挂起、恢复)、延时与时间同步(延迟函数、获取系统节拍)等核心能力。
任务生命周期操作(创建、删除、挂起、恢复)
创建任务
xTaskCreate()/ xTaskCreateStatic():分别以动态分配内存、静态预分配内存方式创建任务;
xTaskCreate() 返回值:BaseType_t类型,若返回 pdPASS表示任务创建成功。
xTaskCreateStatic()
- 额外参数(对比动态):
puxStackBuffer:需提前定义的栈空间数组(用于存储任务运行时的局部变量等);pxTaskBuffer:需提前定义的任务控制块存储空间(用于管理任务元信息)。 - 返回值:直接返回任务的句柄(
TaskHandle_t类型)。
任务句柄(TaskHandle_t)是 FreeRTOS 中任务的唯一身份标识,本质是一个指向任务控制块(Task Control Block, TCB)的指针(typedef void* TaskHandle_t)。它的核心作用是让内核或用户代码能精准定位并操作特定任务
这里参数中字是CPU的字长,32是32位,也就是4字节
删除任务
vTaskDelete(句柄)
挂起任务
vTaskSuspend(句柄)
恢复任务
vTaskResume(句柄)被挂起的任务只能在其他函数中让他恢复
启动调度器
vTaskStartScheduler():启动任务调度器(开启多任务调度)
延时函数
vTaskDelay()是FreeRTOS 中用于任务延时的核心函数,需结合“节拍(Tick)”机制理解,
pdMS_TO_TICKS()将毫秒转化为节拍,并传入vTaskDelay
绝对延时函数
能够实现绝对时间相等的循环
vTaskDelayUntil(&previousWakeTime, pdMS_TO_TICKS(1000)):
其中previousWakeTime记录着唤醒时间,并且自动更新
举例说明:
- 系统启动后,
xTaskGetTickCount()返回0(初始节拍)。 previousWakeTime初始化为0(TickType_t previousWakeTime = xTaskGetTickCount())。
第一次循环:
- 执行任务逻辑(比如采集传感器),假设耗时 100ms(系统节拍变为
0+100=100)。 - 调用
vTaskDelayUntil(&previousWakeTime, pdMS_TO_TICKS(1000)):函数内部计算下次唤醒时间:0 + 1000 = 1000(节拍)。任务阻塞,直到系统节拍达到1000才唤醒。唤醒后,函数自动把previousWakeTime更新为1000(本次唤醒时刻)。
第二次循环:
- 任务从唤醒点继续执行,再次执行任务逻辑,假设耗时 200ms(系统节拍变为
1000+200=1200)。 - 再次调用
vTaskDelayUntil(&previousWakeTime, pdMS_TO_TICKS(1000)):函数内部计算下次唤醒时间:1000 + 1000 = 2000(节拍)。任务阻塞到节拍2000唤醒。函数自动把previousWakeTime更新为2000。
结果:
无论任务逻辑耗时多久,previousWakeTime始终保存着上次实际唤醒的时刻。下一次调用 vTaskDelayUntil()时,函数会基于这个最新时刻计算下次唤醒时间,确保周期稳定在 1000ms。
代码示例



代码中还是可以用hal_delay,但是hal_delay不能使任务进入阻塞状态
而是一直处于连续运行状态
vTaskDelayUntil很香,一张图直观表示
这是用vTaskDelay

这是用vTaskDelayUntil

对任务进行操作:

获取任务句柄
FreeRTOS 中获取任务句柄的3个函数
xTaskGetCurrentTaskHandle():获取当前任务的句柄。
xTaskGetIdleTaskHandle():获取空闲任务的句柄。
xTaskGetHandle():通过任务名称获取句柄
注意最后一个函数的传参“任务名称字符串”函数运行时间长,不建议大量使用,而且需要把参数INCLUDE_xTaskGetHandle设置为1
单个任务操作
获取当前任务优先级

这是cubemx中对应的优先级数字枚举值
获取当前任务的句柄 TaskHandle_t taskHandle = xTaskGetCurrentTaskHandle();
设置优先级:将枚举 osPriorityAboveNormal 转换为 UBaseType_t 后传入* vTaskPrioritySet(taskHandle, (UBaseType_t)osPriorityAboveNormal);
UBaseType_t是 FreeRTOS 内核定义的基础无符号整数类型
把枚举值转换后再传入
vTaskGetInfo获取函数信息
vTaskGetInfo()是 FreeRTOS 提供的任务信息查询函数,用于获取指定任务的详细运行状态、优先级、栈使用等关键信息,是任务调试与管理的核心工具。
要使用 vTaskGetInfo(),需在 FreeRTOSConfig.h中开启配置项:
#define configUSE_TRACE_FACILITY 1 // 默认值为 1,可在 CubeMX 中配置
1 | typedef struct xTASK_STATUS { |
栈空间高水位:
每个任务(Task)都有独立的栈空间,用于存储局部变量、函数调用上下文(如返回地址、寄存器值)、任务切换时的临时数据等。任务运行过程中,栈会动态变化(函数调用时“压栈”增长,函数返回时“弹栈”收缩)。
任务在运行过程中,栈的使用量会波动(比如函数嵌套越深,栈占用越多)。当栈占用最多时,剩余的空间最少——这个“最少的剩余空间”就是高水位值。
栈溢出是实时系统中常见的致命问题(若栈越界写入相邻内存,可能导致程序崩溃、数据 corruption 等)
这个栈和数据结构中的栈的区别
数据结构的栈用于算法层面的逻辑封装
FreeRTOS 任务栈
用于硬件层面的任务切换。任务运行时,CPU 寄存器、局部变量、返回地址等执行上下文会被临时保存到栈中;当任务被抢占或唤醒时,OS 从栈中恢复这些上下文,确保任务能“断点续跑”。
返回任务名称pcTaskGetName() char *pcTaskGetName( TaskHandle_t xTaskToQuery )
查询自己的时候传参为NULL
高水位值uxTaskGetStackHighWaterMark() UBaseType_t uxTaskGetStackHighWaterMark( TaskHandle_t xTask );
需要配置
必须将宏 INCLUDE_uxTaskGetStackHighWaterMark设为 1(默认已开启
返回任务的当前运行状态 eTaskGetState()
eTaskState eTaskGetState( TaskHandle_t xTask );
需要配置
INCLUDE_eTaskGetState或 configUSE_TRACE_FACILITY设为 1(两者默认已开启
内核信息统计
uxTaskGetNumberOfTasks() 返回 FreeRTOS 内核当前管理的任务总数
UBaseType_t uxTaskGetNumberOfTasks( void )
vTaskList()
返回内核中所有任务的字符串列表信息 包含任务名称、状态、优先级、栈空间高水位位、任务编号等。
void vTaskList( char * pcWriteBuffer );pcWriteBuffer:预先创建的字符数组指针,用于存储函数返回的字符串信息。需确保数组足够大(FreeRTOS 不会自动检查数组大小)
vTaskList()内部调用 sprintf()格式化字符串,会导致编译后程序体积明显增大,仅建议在调试阶段使用,发布版本需禁用。

需要设置
configUSE_TRACE_FACILITY:默认值 1,用于使能跟踪功能
configUSE_STATS_FORMATTING_FUNCTION:默认值 0,需设为 1以使能任务统计格式化功能。
configSUPPORT_DYNAMIC_ALLOCATION:默认值 1,控制动态内存分配支持(CubeMX 中不可单独修改,需结合其他配置)。
删除任务(两步走):
FreeRTOS 中,任务删除(通过 vTaskDelete()实现)并非“一键销毁”,而是分为逻辑删除和物理内存释放两个阶段:
逻辑删除:调用 vTaskDelete()后,内核会立即将任务从调度器的可调度队列(就绪、阻塞、挂起等链表)中移除,并把任务状态标记为 eDeleted(已删除)。此时任务不再参与调度,也无法被唤醒/运行。
物理内存释放:任务占用的内存(包括任务控制块 TCB、栈内存等)并不会被“立即释放”,而是由 空闲任务(Idle Task) 异步完成释放。
空闲任务是 FreeRTOS 内核自动创建的优先级最低的任务,其核心职责之一是 清理被删除任务的内存(避免内存泄漏)。所以空闲任务是有功能有作用的
UBaseType_t与uint8_t
UBaseType_t是 FreeRTOS 内核定义的基础无符号整数类型
若 configUSE_16_BIT_TICKS = 1(仅适用于 16 位 MCU,如 Cortex-M0 部分场景):UBaseType_t等价于 uint16_t(2 字节,16 位);
若 configUSE_16_BIT_TICKS = 0(绝大多数 32 位 MCU,如 Cortex-M3/M4/M7,默认配置):UBaseType_t等价于 uint32_t(4 字节,32 位)。
UBaseType_t不是固定 8 位,其宽度随架构配置变化;而 uint8_t是固定 8 位(1 字节)的无符号整数(属于 C 标准库类型)。
UBaseType_t不能表示浮点数,也不能涉及负数 它是 FreeRTOS 专为任务管理中的非负整数数据设计的类型,用来表示任务相关的结构体或者其他变量任务管理用 UBaseType_t,通用计算用 C 标准类型
uxTaskGetSystemState()
1 | UBaseType_t uxTaskGetSystemState( TaskStatus_t * const pxTaskStatusArray, |
用于获取系统中所有任务的状态信息,并为每个任务填充 TaskStatus_t结构体(该结构体在 vTaskGetInfo()相关内容中介绍),最终通过参数返回这些任务状态数据
也就是

也需要进行相关配置
vTaskGetRunTimeStats()用于统计系统中每个任务的运行时间
需要配置
void vTaskGetRunTimeStats(char *pcWriteBuffer);
参数 pcWriteBuffer:指向用于存储统计结果的字符数组(函数会将数据以“文字表格”形式写入该数组,格式与 vTaskList()的返回结果类似)。
注意事项:
中断影响:函数执行时会禁止所有中断,因此仅能在程序调试阶段使用,不可在正式发布(运行)阶段调用,否则会严重影响系统实时性。
同时代码体积会急剧增加
xTaskGetSchedulerState用于获取 FreeRTOS 任务调度器的当前状态
BaseType_t xTaskGetSchedulerState( void );
代码应用举例
实际代码中的栈空间大小
不是随意给出,是在程序运行过程中通过统计栈空间的高水位值,给出的。
调试阶段测试一下
osKernelStart接管程序,不进入while 大循环
一些函数的配置在cubemx中无法设置

这里不直接修改而重新定义宏,防止在对应沙箱段内

临界代码段
taskENTER_CRITICAL()时,暂停任务调度
taskEXIT_CRITICAL()时,恢复任务调度
临界段通过“暂停调度”保证这段代码原子性执行(不被其他任务打断),进而保护全局变量的正确性。尤其是在LCD显示函数中, 或者屏幕数据的发送函数中,尽量不要出现发一半被打断的现象,DMA是外设直接访问内存, FreeRTOS 任务调度是否会打断 DMA 传输
需在函数返回前调用 vTaskDelete(NULL)自我删除

