多任务运行机制

任务是实现某种功能的函数,里面通常有死循环

任何任务都不应该自己退出,不能有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 定时器的中断不仅用于生成嘀嗒信号,还承担任务切换申请的功能

任务调度

方法概述

抢占式(时间片可选)和合作式

image-20251013054137743

采用时间片的抢占式调度方法

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初始化为 0TickType_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

代码示例

image-20251013182319286

image-20251013182400806

image-20251013182841129

代码中还是可以用hal_delay,但是hal_delay不能使任务进入阻塞状态

而是一直处于连续运行状态

vTaskDelayUntil很香,一张图直观表示

这是用vTaskDelay

image-20251013184727647

这是用vTaskDelayUntil

image-20251013184706492

对任务进行操作:

image-20251013185049773

获取任务句柄

FreeRTOS 中获取任务句柄的3个函数

xTaskGetCurrentTaskHandle():获取当前任务的句柄。

xTaskGetIdleTaskHandle():获取空闲任务的句柄。

xTaskGetHandle():通过任务名称获取句柄

注意最后一个函数的传参“任务名称字符串”函数运行时间长,不建议大量使用,而且需要把参数INCLUDE_xTaskGetHandle设置为1

单个任务操作

获取当前任务优先级

image-20251013185713545

这是cubemx中对应的优先级数字枚举值

获取当前任务的句柄 TaskHandle_t taskHandle = xTaskGetCurrentTaskHandle();

设置优先级:将枚举 osPriorityAboveNormal 转换为 UBaseType_t 后传入* vTaskPrioritySet(taskHandle, (UBaseType_t)osPriorityAboveNormal);

UBaseType_tFreeRTOS 内核定义的基础无符号整数类型

把枚举值转换后再传入

vTaskGetInfo获取函数信息

vTaskGetInfo()是 FreeRTOS 提供的任务信息查询函数,用于获取指定任务的详细运行状态、优先级、栈使用等关键信息,是任务调试与管理的核心工具。

要使用 vTaskGetInfo(),需在 FreeRTOSConfig.h中开启配置项:

#define configUSE_TRACE_FACILITY 1 // 默认值为 1,可在 CubeMX 中配置

1
2
3
4
5
6
7
8
9
10
11
typedef struct xTASK_STATUS {
TaskHandle_t xHandle; // 任务句柄(与传入的 xTask 对应)
const char *pcTaskName; // 任务名称(创建任务时指定的字符串)
UBaseType_t xTaskNumber; // 任务唯一编号(用于调试标识)
eTaskState eCurrentState; // 当前任务状态(枚举类型)
UBaseType_t uxCurrentPriority;// 当前优先级(可能因“优先级继承”动态变化)
UBaseType_t uxBasePriority; // 基础优先级(仅“互斥量防优先级反转”场景有效,需 configUSE_MUTEXES=1)
uint32_t ulRunTimeCounter; // 任务累计运行时间(需使能运行时间统计)
StackType_t *pxStackBase; // 栈空间基地址(低地址,任务创建时分配)
uint16_t usStackHighWaterMark; // 栈高水位(单位:字,反映栈剩余最小空间)
} TaskStatus_t;

栈空间高水位:

每个任务(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()格式化字符串,会导致编译后程序体积明显增大,仅建议在调试阶段使用,发布版本需禁用

image-20251013192452498

需要设置

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_tFreeRTOS 内核定义的基础无符号整数类型

configUSE_16_BIT_TICKS = 1(仅适用于 16 位 MCU,如 Cortex-M0 部分场景):UBaseType_t等价于 uint16_t2 字节,16 位);

configUSE_16_BIT_TICKS = 0(绝大多数 32 位 MCU,如 Cortex-M3/M4/M7,默认配置):UBaseType_t等价于 uint32_t4 字节,32 位)。

UBaseType_t不是固定 8 位,其宽度随架构配置变化;而 uint8_t固定 8 位(1 字节)的无符号整数(属于 C 标准库类型)。

UBaseType_t不能表示浮点数,也不能涉及负数 它是 FreeRTOS 专为任务管理中的非负整数数据设计的类型,用来表示任务相关的结构体或者其他变量任务管理用 UBaseType_t,通用计算用 C 标准类型

uxTaskGetSystemState()

1
2
3
UBaseType_t uxTaskGetSystemState( TaskStatus_t * const pxTaskStatusArray, 
const UBaseType_t uxArraySize,
uint32_t * const pulTotalRunTime );

用于获取系统中所有任务的状态信息,并为每个任务填充 TaskStatus_t结构体(该结构体在 vTaskGetInfo()相关内容中介绍),最终通过参数返回这些任务状态数据

也就是

image-20251013193717381

也需要进行相关配置

vTaskGetRunTimeStats()用于统计系统中每个任务的运行时间

需要配置

void vTaskGetRunTimeStats(char *pcWriteBuffer);

参数 pcWriteBuffer:指向用于存储统计结果的字符数组(函数会将数据以“文字表格”形式写入该数组,格式与 vTaskList()的返回结果类似)。

注意事项:

中断影响:函数执行时会禁止所有中断,因此仅能在程序调试阶段使用,不可在正式发布(运行)阶段调用,否则会严重影响系统实时性。

同时代码体积会急剧增加

xTaskGetSchedulerState用于获取 FreeRTOS 任务调度器的当前状态

BaseType_t xTaskGetSchedulerState( void );

代码应用举例

实际代码中的栈空间大小

不是随意给出,是在程序运行过程中通过统计栈空间的高水位值,给出的。

调试阶段测试一下

osKernelStart接管程序,不进入while 大循环

一些函数的配置在cubemx中无法设置

image-20251013195300414

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

image-20251013195359612

临界代码段

taskENTER_CRITICAL()时,暂停任务调度

taskEXIT_CRITICAL()时,恢复任务调度

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

需在函数返回前调用 vTaskDelete(NULL)自我删除