任务管理

FreeRTOS 的任务管理是其核心功能之一,它负责在嵌入式系统中调度和管理多个任务(时间片轮询的方式),以便高效地利用处理器资源。任务管理的目标是确保各个任务按预期运行,执行时能及时响应事件或中断,并在不同任务间进行切换。

任务状态

任务可以存在于以下状态中:

  • 运行

    当任务实际执行时,它被称为处于运行状态。任务当前正在使用处理器。 如果运行 RTOS 的处理器只有一个内核, 那么在任何给定时间内都只能有一个任务处于运行状态。

  • 准备就绪

    准备就绪任务指那些能够执行(它们不处于阻塞或挂起状态), 但目前没有执行的任务, 因为同等或更高优先级的不同任务已经处于运行状态。

  • 阻塞

    如果任务当前正在等待时间或外部事件,则该任务被认为处于阻塞状态。 例如,如果一个任务调用 vTaskDelay(),它将被阻塞(被置于阻塞状态), 直到延迟结束——一个时间事件。 任务也可以通过阻塞来等待队列、信号量、事件组、通知或信号量 事件。处于阻塞状态的任务通常有一个”超时”期, 超时后任务将被超时,并被解除阻塞, 即使该任务所等待的事件没有发生。“阻塞”状态下的任务不使用任何处理时间,不能 被选择进入运行状态。

  • 挂起

    与“阻塞”状态下的任务一样, “挂起”状态下的任务不能被选择进入运行状态,但处于挂起状态的任务没有超时。相反,任务只有在分别通过 vTaskSuspend() 和 xTaskResume() API 调用明确命令时才会进入或退出挂起状态。

image-20241123194619522

任务优先级

每个任务均被分配了从 0 到 ( configMAX_PRIORITIES - 1 ) 的优先级,configMAX_PRIORITIES 通常定义在 FreeRTOSConfig.h。

如果正在使用的移植实现了使用“前导零计数”类指令的移植优化任务选择机制 (针对单一指令中的任务选择)而且 configUSE_PORT_OPTIMISED_TASK_SELECTION 在 FreeRTOSConfig.h 中设置为 1,则 configMAX_PRIORITIES 无法高于 32。在其他所有情况下, configMAX_PRIORITIES 可以取任何合理数值——但为了保证 RAM 的使用效率,应取 实际需要的最小值。

优先级数字小表示任务优先级低。空闲任务的优先级为零 (tskIDLE_PRIORITY)。

处于相同优先级的任务数量不限。如果 configUSE_TIME_SLICING 未经定义,或者如果 configUSE_TIME_SLICING 设置为 1,则具有相同优先级的若干就绪状态任务将通过时间切片轮询调度方案共享可用的处理时间

相同优先级谁先执行?

任务调度

单核

FreeRTOS 默认使用固定优先级的抢占式调度策略,对同等优先级的任务执行时间切片轮询调度:

  • “固定优先级”是指调度器不会永久更改任务的优先级, 但可能会因优先级继承而暂时提高任务的优先级。
  • “抢占式”是指调度器始终运行优先级最高且可运行的 RTOS 任务, 无论任务何时能够运行。例如, 如果中断服务程序 (ISR) 更改了优先级最高且可运行的任务, 调度器会停止当前正在运行的低优先级任务 并启动高优先级任务——即使这发生在同一个时间片内 。这种情况下可以说高优先级任务 “抢占”了低优先级任务。
  • “轮询调度”是指具有相同优先级的任务轮流进入运行状态。
  • “时间切片”是指调度器会在每个 tick 中断上在同等优先级任务之间进行切换, tick 中断之间的时间构成一个时间切片。(tick 中断是 RTOS 用来衡量时间的周期性中断。)

使用优先排序的抢占式调度器,避免任务饥饿

始终运行优先级最高且可运行的任务的一个后果是从未进入阻塞或挂起状态的高优先级任务将永久性剥夺所有更低优先级任务的任何执行时间。这就是通常最好创建事件驱动型任务的原因之一 。例如,如果一个高优先级任务正在等待一个事件,那么它就不应处于该事件的循环(轮询)中,因为如果处于轮询中,它会一直运行,永远不进入“阻塞”或 “挂起”状态。相反,该任务应进入“阻塞”状态等待这一事件。该事件可以 通过某个FreeRTOS任务间通信和同步原语发送至任务。 收到事件后,优先级更高的任务会自动解除“阻塞”状态。这样低优先级 任务会运行,而高优先级任务会处于“阻塞”状态。

优先级继承

假设:

  • 任务 A(高优先级)需要一个资源(例如互斥锁)。
  • 任务 B(低优先级)已经持有该资源,并且任务 A 被阻塞。
  • 任务 C(中优先级)开始执行并抢占任务 B。

没有优先级继承的情况:

  • 任务 C 会抢占任务 B 的执行,任务 B 不能及时释放资源,任务 A 会被长时间阻塞,导致优先级反转。

启用优先级继承的情况:

  • 当任务 A 因为资源被任务 B 占用而阻塞时,FreeRTOS 会自动提升任务 B 的优先级,将其临时提升到任务 A 的优先级水平。
  • 任务 B 现在有了和任务 A 相同的优先级,它会继续执行并尽快释放资源。
  • 任务 B 释放资源后,它的优先级会恢复到原来的值,任务 A 也会重新获得执行权。

AMP

使用 FreeRTOS 的非对称多处理 (AMP) 是指多核设备的每个核心都单独运行自己的 FreeRTOS 实例。这些 核心并不都需要具有相同架构, 但如果 FreeRTOS 实例之间需要进行通信,则需要共享一些内存。

每个核心都会运行自己的 FreeRTOS 实例, 因此任何给定核心上的调度算法与上文的单核系统调度算法完全相同 。您可以使用流缓冲区或消息缓冲区作为核间通信原语, 这样一来,一个核心上的任务可以进入“阻塞”状态, 以等待另一个核心发来的数据或事件。

SMP

使用 FreeRTOS 的对称多处理 (SMP) 指的是 一个 FreeRTOS 实例可以跨多个处理器核心调度 RTOS 任务。 由于只有一个 FreeRTOS 实例在运行,一次只能使用 FreeRTOS 的一个移植, 因此每个核心必须具有相同的处理器架构并共用相同的内存空间。

空闲任务

RTOS 调度器启动时,自动创建空闲任务,以确保始终存在一个能够运行的任务。它以最低优先级创建, 以确保如果有更高的优先级应用程序任务处于准备就绪状态,则不使用任何 CPU 时间(并不参与实际的应用逻辑)。

空闲任务负责释放 RTOS 分配给已删除任务的内存。因此,在使用 vTaskDelete() 函数来确保空闲任务不会匮乏处理时间的应用程序中( 空闲任务占用系统的过多处理时间), 这很重要。 空闲任务没有其他激活函数,因此可以在所有其他条件下合理地缺乏微控制器时间 。

应用程序任务可以共享空闲任务优先级 (tskIDLE_PRIORITY)。 请参阅 configIDLE_SHOULD_YIELD 配置参数, 了解如何配置该行为。

创建任务

TaskHandle

任务引用的类型(任务句柄)。例如,调用 xTaskCreate(通过指针参数) 返回 TaskHandle_t 变量,然后可以将该变量用作 vTaskDelete 的参数来删除任务。

任务创建时,FreeRTOS 会为每个任务分配一个任务控制块(TCB),并通过任务句柄提供访问该控制块的指针。任务控制块保存了任务的所有信息,包括任务的状态、优先级、堆栈指针等。

任务句柄的存储:通常,任务句柄是一个 TaskHandle_t 类型的变量,这个变量实际上是一个指向任务控制块的指针。你可以通过传递任务句柄来操作任务。例如,调用 vTaskDelete() 函数时需要传入一个任务句柄来指定删除哪个任务。

xTaskCreate

创建一项新任务并将其添加到准备运行的任务列表中;

注意:configSUPPORT_DYNAMIC_ALLOCATION(是否支持动态内存分配) 必须在 FreeRTOSConfig.h 中设置为 1,或处于未定义状态(默认为 1) 才可使用此 RTOS API 函数。

1
2
3
4
5
// 如果未定义则创建一个,但是默认启用动态内存管理,所以可以不管他
#ifndef configSUPPORT_DYNAMIC_ALLOCATION
/* Defaults to 1 for backward compatibility. */
#define configSUPPORT_DYNAMIC_ALLOCATION 1
#endif
1
2
3
4
5
6
7
8
// 用法 
BaseType_t xTaskCreate( TaskFunction_t pvTaskCode,
const char * const pcName,
const configSTACK_DEPTH_TYPE uxStackDepth,
void *pvParameters,
UBaseType_t uxPriority,
TaskHandle_t *pxCreatedTask
);
  • pvTaskCode

    指向任务入口函数的指针(即实现任务的函数名称)。

任务通常以无限循环的形式实现;实现任务的函数绝不能尝试返回或退出。但是任务可通过 vTaskDelete 删除。

  • pcName

    任务的描述性名称。此参数主要用于方便调试,但也可用于获取任务句柄(通过 xTaskGetHandle 函数)。任务名称的最大长度 由 FreeRTOSConfig.h 中的 configMAX_TASK_NAME_LEN 定义。

  • uxStackDepth

    分配用作任务堆栈的字数(不是字节数!)。

    例如,如果堆栈宽度为 16 位,uxStackDepth 为 100,则将分配 200 字节用作任务堆栈。如果堆栈宽度为 32 位,uxStackDepth 为 400, 则将分配 1600 字节用作任务堆栈。堆栈深度与堆栈宽度的乘积不得超过 size_t 类型变量所能包含的最大值。

  • pvParameters

    作为参数传递给所创建任务的值。如果 pvParameters 设置为某变量的地址,则在创建的任务执行时,该变量必须仍然存在, 因此,不能传递堆栈变量的地址。

  • uxPriority

    指定优先级。

    支持 MPU 的系统 可以通过在 uxPriority 中设置 portPRIVILEGE_BIT 位来选择以特权(系统)模式创建任务。 例如,要创建优先级为 2 的特权任务,请将 uxPriority 设置为 ( 2 | portPRIVILEGE_BIT )。应断言优先级低于 configMAX_PRIORITIES。如果 configASSERT 未定义,则优先级默认上限为 (configMAX_PRIORITIES - 1)。

  • pxCreatedTask

    用于将句柄传递至由 xTaskCreate() 函数创建的任务。pxCreatedTask 是可选参数, 可设置为 NULL。

返回:

  • 如果任务创建成功,则返回 pdPASS,
  • 否则返回 errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void vTaskCode(void* pvParameters) {
configASSERT( ( ( uint32_t ) pvParameters ) == 1 );
for( ;; ) {
/* Task code goes here. */
}
}

BaseType_t xReturned;
TaskHandle_t xHandle = NULL;

xReturned = xTaskCreate(vTaskCode, "NAME", STACK_SIZE, ( void * ) 1, tskIDLE_PRIORITY, &xHandle );

if( xReturned == pdPASS ) {
vTaskDelete( xHandle );
}

xTaskCreateStatic

创建一项新任务 并将其添加到准备运行的任务列表中。

configSUPPORT_STATIC_ALLOCATION 必须在 FreeRTOSConfig.h 中设置为 1,才可使用此 RTOS API 函数。

如果 使用 xTaskCreateStatic 创建任务,则 RAM 由应用程序编写者提供,这会产生更多的参数,但这样能够在编译时静态分配 RAM。

1
2
3
4
5
6
7
8
// 用法
TaskHandle_t xTaskCreateStatic( TaskFunction_t pxTaskCode,
const char * const pcName,
const uint32_t ulStackDepth,
void * const pvParameters,
UBaseType_t uxPriority,
StackType_t * const puxStackBuffer,
StaticTask_t * const pxTaskBuffer );
  • pxTaskCode

    指向任务入口函数的指针(即实现任务的函数名称, 请参阅如下示例)。

    任务通常以无限循环的形式实现;实现任务的函数绝不能尝试返回或退出。但是任务可通过 vTaskDelete 删除。

  • pcName

    任务的描述性名称。此参数主要用于方便调试,但也可用于获取任务句柄(通过 xTaskGetHandle 函数)。

    任务名称的最大长度由 FreeRTOSConfig.h中的 configMAX_TASK_NAME_LEN 定义。 |

  • ulStackDepth

    puxStackBuffer 参数用于将 StackType_t 变量的数组传递至 xTaskCreateStatic。

    ulStackDepth 必须设置为数组中的索引数。

  • pvParameters

    作为参数传递给所创建任务的值。

    如果 pvParameters 设置为某变量的地址,则在创建的任务执行时,该变量必须仍然存在,因此,不能传递堆栈变量的地址。 |

  • uxPriority

    指定任务优先级。

    支持 MPU 的系统 可以通过在 uxPriority 中设置 portPRIVILEGE_BIT 位来选择以特权(系统)模式创建任务。例如要创建优先级为 2 的特权任务,请将 uxPriority 设置为 ( 2 | portPRIVILEGE_BIT)。

    应断言优先级低于 configMAX_PRIORITIES。如果 configASSERT 未定义,则优先级默认上限为 (configMAX_PRIORITIES - 1)。

  • puxStackBuffer

    必须指向至少包含 ulStackDepth 个索引的 StackType_t 数组(见上述 ulStackDepth参数)。

    该数组将用作任务堆栈,因此必须持久存在 (不能在函数的堆栈上声明)。 |

  • pxTaskBuffer

    必须指向 StaticTask_t 类型的变量(指向自定义分配内存的一个句柄)。该变量将用于保存新任务的数据结构体 (TCB),因此必须持久存在(不能在函数的堆栈上声明)。

返回:

如果 puxStackBuffer 和 pxTaskBuffer 均不为 NULL,则创建任务, 并返回任务的句柄。

如果 puxStackBuffer 或 pxTaskBuffer 为 NULL,则不会创建任务, 并返回 NULL。

vTaskDelete

从 RTOS 内核管理中移除任务。要删除的任务将从所有就绪、 阻塞、挂起和事件列表中移除。

INCLUDE_vTaskDelete 必须定义为 1,才可使用此函数。

注意:空闲任务负责释放由 RTOS 内核分配给已删除任务的内存。因此,如果应用程序调用了 vTaskDelete(),请务必确保空闲任务获得足够的微控制器处理时间。

任务代码分配的内存(通过 malloc() 或其他内存分配函数分配的内存)不会自动释放, 应在任务删除之前手动释放。

1
void vTaskDelete( TaskHandle_t xTask );
  • xTask

    要删除的任务的句柄。如果传递 NULL,会删除调用任务。

示例

1
2
3
4
5
6
7
void vOtherFunction( void ) {
TaskHandle_t xHandle = NULL;
xTaskCreate( vTaskCode, "NAME", STACK_SIZE, NULL, tskIDLE_PRIORITY, &xHandle );
if( xHandle != NULL ) {
vTaskDelete( xHandle );
}
}

任务控制

按给定的滴答数延迟任务。任务保持阻塞的实际时间取决于滴答频率 。

INCLUDE_vTaskDelay 必须定义为 1,才可使用此函数。

常量 portTICK_PERIOD_MS 可用于根据滴答频率计算实际时间。

1
#define portTICK_PERIOD_MS    ( ( TickType_t ) 1000 / configTICK_RATE_HZ )

configTICK_RATE_HZ 是 每秒钟发生的滴答次数,也就是系统时钟的频率,单位是赫兹(Hz)。它定义了在每秒钟内系统时钟产生多少次滴答(tick)。

那么每个滴答时钟的时间不就是 1s / configTICK_RATE_HZ ==> 1000ms / configTICK_RATE_HZ 毫秒了么

参数:

  • xTicksToDelay

    调用任务应阻塞的 tick 周期数。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void vTaskFunction( void * pvParameters ) {
/* Block for 500ms. */
const TickType_t xDelay = 500 / portTICK_PERIOD_MS;

for( ;; ) {
/* Simply toggle the LED every 500ms, blocking between each toggle. */
vToggleLED();
vTaskDelay(xDelay);
/*
vTaskDelay(pdMS_TO_TICKS(500))
pdMS_TO_TICKS() 宏用于将给定的数字转换成对应的滴答数量。
*/
}
}

vTaskDelay 函数的调用和任务的执行时间,可能会受到其他任务、系统中断以及代码路径的影响,从而导致任务实际延迟的时间与期望的延迟时间有所偏差。

vTaskDelay

按给定的滴答数延迟任务。任务保持阻塞的实际时间取决于滴答频率 。

INCLUDE_vTaskDelay 必须定义为 1,才可使用此函数。

常量 portTICK_PERIOD_MS 可用于根据滴答频率计算实际时间。

1
#define portTICK_PERIOD_MS (( TickType_t ) 1000 / configTICK_RATE_HZ)

参数:

  • xTicksToDelay

    调用任务应阻塞的 tick 周期数。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
void vTaskFunction( void * pvParameters ) {

const TickType_t xDelay = 500 / portTICK_PERIOD_MS;

for( ;; ) {
vToggleLED();
vTaskDelay(xDelay);
/* 还可以用下面这个宏
vTaskDelay(pdMS_TO_TICKS(500))
pdMS_TO_TICKS() //宏用于将给定的数字转换成对应的滴答数量。
*/
}
}

会恢复中断 并进行任务调度?

vTaskDelayUntil

xTaskDelayUntil

uxTaskPriorityGet

uxTaskPriorityGetFromISR

uxTaskBasePriorityGet

uxTaskBasePriorityGetFromISR

vTaskPrioritySet

vTaskSuspend

vTaskResume

xTaskResumeFromISR

xTaskAbortDelay

RTOS 内核控制

vTaskStartScheduler

启动 RTOS 调度器。调用后,RTOS 内核可以控制在何时执行哪些任务。

空闲任务和定时器守护进程任务](可选)会在 RTOS 调度器启动时自动创建。

vTaskStartScheduler 仅在以下情况下返回:没有足够的 RTOS 堆可用来创建空闲或定时器守护进程任务。

示例

1
2
3
4
void vAFunction( void ){
xTaskCreate( vTaskCode, "NAME", STACK_SIZE, NULL, tskIDLE_PRIORITY, NULL );
vTaskStartScheduler();
}

xTaskResumeAll

恢复通过调用 vTaskSuspendAll() 挂起的调度器。

xTaskResumeAll() 仅恢复调度器,不会恢复之前通过调用 vTaskSuspend() 而挂起的任务。

但是注意,在该任务中,如果他检测到

临界区

问题

一、堆栈应该多大?

确定使用 RTOS 时需要多少堆栈与确定编写裸机应用程序(不使用操作系统的应用程序)时需要多少堆栈并无多大区别 。

所需的堆栈大小取决于以下应用程序特定的参数:

  • 函数调用嵌套深度
  • 函数作用域变量声明的数量和大小
  • 函数参数的数量
  • 处理器架构
  • 编译器
  • 编译器优化级别
  • 中断服务程序的堆栈要求——对于许多 RTOS 移植来说是零,因为 RTOS 在进入中断服务程序时会切换为使用专用中断堆栈。

尽管难以确定要为任务分配多少堆栈,但 RTOS 会提供功能,以便采用务实的试错方法来调整任务的堆栈大小; uxTaskGetStackHighWaterMark() API 函数可以用于查看实际的堆栈使用量,允许在分配的堆栈超出必要大小时减少堆栈大小, 而堆栈溢出检测 功能可以用于确定堆栈是否太小。

F407yihuofeimen

二、如何使用我们自己定义的 delay_ms 函数

当我们定义的延时函数依赖于 SysTick 定时器时,由于 FreeRTOS 接管了 SysTick 的控制权,该定时器只有在 vTaskStartScheduler 函数执行后才会启用。因此,如果延时函数 delay_ms() 被调用时 vTaskStartScheduler() 尚未运行,SysTick 尚未工作,可能导致延时函数进入死循环,阻塞程序运行。

三、就绪队列如何保证优先执行先执行高优先级的任务

在 FreeRTOS 中,就绪队列中的任务是通过多个链表来管理的,每个链表对应一个不同的优先级级别。虽然这些队列使用链表来存储任务,但 FreeRTOS 保证高优先级任务优先执行的机制是通过优先级队列(优先级数组)和调度策略来实现的。

就绪状态的优先级队列

1
2
3
4
5
6
7
8
9
10
11
12
#define configMAX_PRIORITIES ( 5 )

// 链表
typedef struct xLIST{
listFIRST_LIST_INTEGRITY_CHECK_VALUE
volatile UBaseType_t uxNumberOfItems;
ListItem_t * configLIST_VOLATILE pxIndex;
MiniListItem_t xListEnd;
listSECOND_LIST_INTEGRITY_CHECK_VALUE
} List_t;

PRIVILEGED_DATA static List_t pxReadyTasksLists[ configMAX_PRIORITIES ];

当调度器决定从就绪队列中选择一个任务来执行时,它会按照优先级从 高到低 的顺序检查优先级队列。由于每个优先级都有一个独立的链表,调度器总是首先检查 优先级最高的队列(即数组 pxReadyTasksLists[0] 对应的队列)。

  • 高优先级任务:如果某个任务的优先级较高(数字较小),它会出现在 pxReadyTasksLists[0] 中,调度器会优先选择该队列中的任务来执行。
  • 低优先级任务:如果最高优先级队列为空,调度器会选择下一个优先级的队列,以此类推。