嵌入式硬件笔记(2):FreeRTOS的任务管理

嵌入式硬件笔记(2):FreeRTOS的任务管理 前言在 FreeRTOS 中任务是系统运行的基本单位而任务管理则是整个 RTOS 内核最核心的功能。理解这一部分本质上就是理解系统是如何从“单线程执行”演变为“多任务协同运行”的。1. 基本概念1.1. 什么是任务在形式上一个任务通常表现为一个包含while(1)的函数但从系统角度来看它远不只是一个普通函数。每个任务都拥有独立的运行环境包括自己的栈空间、寄存器上下文以及状态信息因此可以在任意时刻被“暂停”和“恢复”。任务本质上是一个“可被调度的执行单元”。不同任务之间并不是同时运行而是在调度器的控制下快速切换执行从而在宏观上呈现出“并发运行”的效果。因此可以把任务理解为由操作系统管理的长期运行函数而不是一次性调用的代码块。对于整个单片机程序我们称之为 application/应用程序。 使用 FreeRTOS时我们可以在application中创建多个任务(task)有些文档把任务也称为线程(thread)。1.2. 任务管理任务管理指的是操作系统对所有任务的统一调度与控制过程包括任务的创建、删除、状态切换以及优先级管理等。FreeRTOS 内核通过维护一系列数据结构如任务控制块 TCB、就绪列表等来记录每个任务的状态并根据一定的调度策略决定当前应该执行哪个任务。任务管理解决的不是“任务怎么写”而是“任务什么时候执行、执行多久、是否被打断”。开发者只需要定义好任务逻辑而任务的调度顺序和运行时机则由内核自动管理。这种分离使得系统结构更加清晰也大大降低了复杂系统的开发难度。1.3. 为什么需要任务管理在裸机开发中所有功能通常写在一个while(1)循环中程序按照固定顺序执行这种方式在简单系统中尚可接受但一旦涉及多个功能模块如通信、控制、传感器采集等就会出现明显问题。例如某个模块阻塞时会影响整个系统或者关键任务无法优先执行导致实时性下降。任务管理的引入本质上是为了解决“多功能并行需求”与“单CPU执行能力”之间的矛盾。通过将系统拆分为多个任务并结合优先级与调度机制FreeRTOS 可以让关键任务优先执行同时保证其他任务也能按需运行从而在有限资源下实现高效、有序的系统行为。2.任务创建与删除在 FreeRTOS 中任务并不是自动存在的而是需要开发者显式创建。当任务不再需要时也可以由系统删除并释放其占用资源。2.1. 从代码层面看什么是任务从代码形式上看FreeRTOS 的任务本质上就是一个特定格式的函数。这个函数通常不会执行一次就结束而是会在内部通过while(1)循环持续运行由调度器决定它何时执行、何时挂起。FreeRTOS 规定了任务函数的基本原型任务函数必须带一个void *类型的参数并且通常不返回值。任务函数原型为void vTaskFunction(void *pvParameters);这里的几个关键点要注意返回值必须是void参数必须是void *用于接收外部传入的数据任务函数内部通常要写成无限循环否则任务执行完就会直接退出示例代码#include FreeRTOS.h #include task.h void vLedTask(void *pvParameters) { while (1) { // 这里写任务逻辑 vTaskDelay(pdMS_TO_TICKS(500)); } }这段代码中vLedTask就是一个标准的任务函数。虽然它看起来只是一个普通函数但一旦被xTaskCreate()创建并交给调度器管理它就不再是“手动调用”的函数而是系统中的一个独立任务。2.2. 任务的创建任务创建指的是由 FreeRTOS 为某个任务函数分配所需资源并将其加入调度系统中。创建任务时系统通常需要完成两件事一是在堆中分配任务控制块TCB和任务栈二是记录该任务的优先级、名称、入口函数等基本信息。创建成功后任务并不一定立刻执行而是先进入就绪态等待调度器分配 CPU。FreeRTOS 中最常用的任务创建函数是xTaskCreate()它用于创建动态任务也就是任务所需的内存由系统自动从 heap 中分配。创建任务函数原型为BaseType_t xTaskCreate( TaskFunction_t pxTaskCode, const char * const pcName, const configSTACK_DEPTH_TYPE uxStackDepth, void * const pvParameters, UBaseType_t uxPriority, TaskHandle_t * const pxCreatedTask );这个函数的几个核心参数可以这样理解pxTaskCode任务函数入口pcName任务名称便于调试uxStackDepth任务栈大小pvParameters传给任务的参数uxPriority任务优先级pxCreatedTask任务句柄用于后续操作该任务返回值成功pdPASS失败errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY(失败原因只有内存不足)示例代码#include FreeRTOS.h #include task.h void vLedTask(void *pvParameters) { while (1) { // LED任务逻辑 vTaskDelay(pdMS_TO_TICKS(500)); } } int main(void) { BaseType_t xReturn; xReturn xTaskCreate( vLedTask, // 任务函数 LED_Task, // 任务名称 128, // 栈大小 NULL, // 传入参数 1, // 优先级 NULL // 不保存任务句柄 ); if (xReturn pdPASS) { // 任务创建成功 } vTaskStartScheduler(); while (1) { } }这段代码展示了 FreeRTOS 中最典型的使用方式先定义任务函数再通过xTaskCreate()把它注册到系统中最后启动调度器。此时系统的执行重心就不再是main()而是各个被创建出来的任务。2.3.任务的删除任务删除指的是将某个已经存在的任务从调度系统中移除并释放它所占用的资源。删除操作通常用于某些临时性任务例如只执行一次初始化、处理特定事件后退出或者不再需要的后台任务。任务删除后将不再参与调度。FreeRTOS 中删除任务使用的是vTaskDelete()。它可以删除指定任务也可以让任务删除自己。删除任务函数原型void vTaskDelete(TaskHandle_t xTaskToDelete);参数说明很简单如果传入某个任务句柄则删除对应任务如果传入NULL表示删除当前正在运行的任务自身示例代码#include FreeRTOS.h #include task.h TaskHandle_t xMyTaskHandle NULL; void vMyTask(void *pvParameters) { while (1) { // 执行任务逻辑 vTaskDelay(pdMS_TO_TICKS(1000)); } } void vControlTask(void *pvParameters) { vTaskDelay(pdMS_TO_TICKS(5000)); // 删除 vMyTask 任务 vTaskDelete(xMyTaskHandle); while (1) { vTaskDelay(pdMS_TO_TICKS(1000)); } } int main(void) { xTaskCreate(vMyTask, MyTask, 128, NULL, 1, xMyTaskHandle); xTaskCreate(vControlTask, CtrlTask, 128, NULL, 2, NULL); vTaskStartScheduler(); while (1) { } }在这个例子中vMyTask被先创建出来并保存了句柄而vControlTask在延时 5 秒后通过vTaskDelete(xMyTaskHandle)将其删除。这种写法说明任务不仅可以被创建和调度也可以在运行过程中被其他任务动态控制。如果想让任务自己删除自己也可以这样写void vOnceTask(void *pvParameters) { // 执行一次性任务逻辑 vTaskDelete(NULL); // 删除当前任务 }这种写法通常用于一次性任务例如某些初始化任务在完成后自动退出。3. 任务的tick和任务优先级3.1. Tick系统时基在 FreeRTOS 中很多与时间相关的操作都离不开Tick。可以把它理解为系统内部统一使用的一种“时间刻度”类似于 RTOS 的时钟脉搏。每当硬件定时器周期性地产生一次中断FreeRTOS 内部的 Tick 计数就会加一这个不断递增的计数值就构成了系统的基本时间基准。任务延时、超时等待、软件定时器等功能本质上都是围绕 Tick 展开的。需要特别注意的是FreeRTOS 并不直接以“毫秒”作为内部时间单位而是以tick为单位来管理时间。例如系统若配置为 1ms 触发一次 Tick 中断那么 100 个 tick 就对应大约 100ms如果配置为 10ms 一次 Tick那么 100 个 tick 就对应 1s。因此Tick 和“真实时间”的换算关系取决于系统的节拍配置。从编程角度来看Tick 最常见的体现就是vTaskDelay()。这个函数并不是简单地“让 CPU 原地空转等待”而是让当前任务进入阻塞态并告诉调度器“等到指定的 Tick 数到达后再让我重新进入就绪态。”也正因为如此FreeRTOS 的延时与裸机里的delay_ms()有本质区别前者是“让出 CPU”后者通常是“占住 CPU”。3.1.1. Tick相关的核心函数与原型在 FreeRTOS 中Tick 并不是一个可以直接操作的变量而是通过一组 API 来使用。下面是最常见、也是最重要的几个函数。1. vTaskDelay基于Tick的延时void vTaskDelay(const TickType_t xTicksToDelay);作用让当前任务进入阻塞态延时指定的 Tick 数。使用方式为vTaskDelay(pdMS_TO_TICKS(500)); // 延时500ms2. vTaskDelayUntil周期性延时 void vTaskDelayUntil( TickType_t * const pxPreviousWakeTime, const TickType_t xTimeIncrement );作用实现固定周期执行任务比 vTaskDelay 更精确。使用方式为TickType_t xLastWakeTime xTaskGetTickCount(); while (1) { // 周期任务逻辑 vTaskDelayUntil(xLastWakeTime, pdMS_TO_TICKS(1000)); }3. xTaskGetTickCount获取当前TickTickType_t xTaskGetTickCount(void);作用获取当前系统运行的 Tick 数时间基准使用方式TickType_t currentTick; currentTick xTaskGetTickCount();3.2. 任务优先级在 FreeRTOS 中多个任务并不是“平均地轮流执行”而是要由调度器根据一定规则决定谁先运行而这个规则中最重要的一项就是任务优先级。可以说优先级决定了任务在系统中的“紧急程度”也决定了它获得 CPU 的先后顺序。3.2.1.什么是任务优先级任务优先级可以理解为任务的重要等级。在 FreeRTOS 中每个任务在创建时都会被赋予一个优先级数值越大表示优先级越高数值越小表示优先级越低。当调度器在多个就绪任务中进行选择时会优先让优先级更高的任务运行而低优先级任务只有在高优先级任务阻塞、挂起或让出 CPU 时才有机会执行。3.2.2. 任务优先级的范围与配置FreeRTOS 中的任务优先级并不是无限的而是由配置文件FreeRTOSConfig.h中的宏configMAX_PRIORITIES决定。这个宏表示系统一共支持多少个优先级等级优先级编号通常从0开始到configMAX_PRIORITIES - 1结束其中0是最低优先级。相关配置宏为#define configMAX_PRIORITIES 5如果系统中这样配置就表示任务优先级范围是0-4。这里需要注意configMAX_PRIORITIES表示的是“优先级个数”不是“最大优先级值”。例如配置为 5并不意味着最高优先级是 5而是最高只能到 4。3.2.3.任务创建时如何设置优先级任务优先级是在调用xTaskCreate()时设置的对应的参数是uxPriority。这个参数会直接决定任务创建后进入就绪列表时所处的优先级层级。函数原型只看优先级参数即可BaseType_t xTaskCreate( TaskFunction_t pxTaskCode, const char * const pcName, const configSTACK_DEPTH_TYPE uxStackDepth, void * const pvParameters, UBaseType_t uxPriority, TaskHandle_t * const pxCreatedTask );pxTaskCode任务函数入口指向任务函数函数指针决定任务执行的代码内容pcName任务名称用于调试和查看任务信息对功能无影响uxStackDepth任务栈大小指定任务使用的栈空间大小不是字节过小会栈溢出pvParameters传给任务的参数任务启动时传入的数据在任务函数中通过强制类型转换使用uxPriority任务优先级数值越大优先级越高决定任务获得 CPU 的先后顺序pxCreatedTask任务句柄用于保存任务的句柄后续可以用来删除/挂起/恢复该任务返回值任务创建结果成功pdPASS失败errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY通常是堆空间不足示例代码#include FreeRTOS.h #include task.h void vTask1(void *pvParameters) { while (1) { // 任务1逻辑 } } void vTask2(void *pvParameters) { while (1) { // 任务2逻辑 } } int main(void) { xTaskCreate(vTask1, Task1, 128, NULL, 1, NULL); // 低优先级 xTaskCreate(vTask2, Task2, 128, NULL, 3, NULL); // 高优先级 vTaskStartScheduler(); while (1); }在这个例子中vTask2的优先级高于vTask1。因此只要vTask2一直处于就绪态它就会优先运行而vTask1很可能长期得不到执行机会。3.2.4. 优先级如何影响任务调度任务优先级最直接的影响就是决定“谁先运行”。在 FreeRTOS 中如果系统中有多个任务同时处于就绪态那么调度器会优先选择优先级最高的那个任务执行。只有当高优先级任务进入阻塞态例如调用vTaskDelay()、被挂起或者主动让出 CPU 时低优先级任务才有机会执行。如果一个高优先级任务一直不阻塞、不延时也不释放 CPU那么低优先级任务可能会一直得不到运行机会。void vHighTask(void *pvParameters) { while (1) { // 高优先级任务一直执行 } } void vLowTask(void *pvParameters) { while (1) { // 低优先级任务几乎得不到执行机会 } }如果vHighTask的优先级更高且它内部没有任何阻塞或延时那么vLowTask基本不会运行。这也说明一个问题优先级设计必须合理不能单纯地把所有任务都设得很高。3.3. 任务状态在 FreeRTOS 中任务在运行过程中并不是一直占用 CPU而是会在不同状态之间不断切换。调度器正是通过这些状态来决定当前应该运行哪个任务。3.3.1.FreeRTOS中的几种任务状态FreeRTOS 中最常见的任务状态主要有四种运行态Running、就绪态Ready、阻塞态Blocked和挂起态Suspended。它们不是四种“不同类型的任务”而是同一个任务在不同时刻可能处于的不同运行阶段。1. Running运行态含义任务当前正在占用 CPU 执行特点在单核 MCU 中同一时刻只能有一个任务处于 Running 状态说明这是任务真正执行代码的状态2. Ready就绪态)含义任务已经具备运行条件但暂时还没有获得 CPU特点任务处于待运行状态一旦调度器选择到它就会进入 Running说明通常是因为当前有更高优先级任务正在运行或者同优先级任务还没轮到它3. Blocked阻塞态含义任务正在等待某个条件成立因此暂时不能运行常见原因等待时间到、等待队列数据、等待信号量、等待事件等说明只要等待条件未满足任务就不会参与调度4. Suspended挂起态含义任务被人为暂停特点与 Blocked 不同挂起态不是在等时间或事件而是被显式停止说明只有调用恢复函数后任务才会重新进入 Ready 状态3.3. 2. 任务之间的状态切换任务状态并不是固定不变的而是在调度过程中不断切换。最常见的切换路径其实就是围绕 Running、Ready、Blocked 和 Suspended 之间展开的。1. Running → Blocked当一个正在运行的任务调用vTaskDelay()或者等待队列、信号量时它就会主动放弃 CPU进入 Blocked 状态。此时调度器会立刻去选择其他 Ready 任务运行。2. Blocked → Ready当阻塞条件满足时任务就会离开 Blocked重新进入 Ready。例如延时时间到了或者等待的数据已经到来任务就会重新变成“可运行”。此时任务只是回到 Ready还不一定马上运行仍然要看有没有更高优先级任务在占用 CPU。3. Ready → Running调度器会从所有 Ready 任务中选出优先级最高的一个进入 Running 状态。如果当前正在运行的任务优先级较低而这时有一个更高优先级任务变成 Ready那么系统会立即发生切换。4. Running → Suspended / Suspended → Ready如果某个任务被vTaskSuspend()挂起它会从当前状态脱离调度系统进入 Suspended。只有后续调用vTaskResume()或xTaskResumeFromISR()它才会回到 Ready重新参与调度。3.3.3. 与任务状态相关的常用函数原型① 进入阻塞态常见函数void vTaskDelay(const TickType_t xTicksToDelay);作用让当前任务延时指定 Tick 数在此期间进入 Blocked 状态。周期性阻塞可以写为void vTaskDelayUntil( TickType_t * const pxPreviousWakeTime, const TickType_t xTimeIncrement );作用让任务按固定周期阻塞适合周期性任务。② 进入挂起态 / 恢复挂起态常见函数void vTaskSuspend(TaskHandle_t xTaskToSuspend);作用挂起指定任务。void vTaskResume(TaskHandle_t xTaskToResume);作用恢复指定任务。BaseType_t xTaskResumeFromISR(TaskHandle_t xTaskToResume);作用在中断服务函数中恢复任务。③ 获取任务状态的函数如果想查看某个任务当前处于什么状态可以使用eTaskState eTaskGetState(TaskHandle_t xTask);它的返回值是一个枚举类型常见包括eRunningeReadyeBlockedeSuspendedeDeleted这个函数在调试和状态分析时很有帮助。3.3.4.示例代码下面用一个简单例子来展示任务状态的变化。我们创建两个任务其中一个任务周期延时另一个任务负责在合适时机挂起和恢复它。这样就能直观看到任务在 Running、Blocked 和 Suspended 之间切换。#include FreeRTOS.h #include task.h TaskHandle_t xTask1Handle NULL; /* 任务1周期运行每次运行后延时 */ void vTask1(void *pvParameters) { while (1) { // Task1 正在运行Running vTaskDelay(pdMS_TO_TICKS(1000)); // 调用后Task1 进入 Blocked 状态 } } /* 任务2控制 Task1 的挂起与恢复 */ void vTask2(void *pvParameters) { while (1) { vTaskDelay(pdMS_TO_TICKS(3000)); // 挂起 Task1Task1 进入 Suspended 状态 vTaskSuspend(xTask1Handle); vTaskDelay(pdMS_TO_TICKS(3000)); // 恢复 Task1Task1 回到 Ready 状态 vTaskResume(xTask1Handle); } } int main(void) { xTaskCreate(vTask1, Task1, 128, NULL, 1, xTask1Handle); xTaskCreate(vTask2, Task2, 128, NULL, 2, NULL); vTaskStartScheduler(); while (1) { } }这段代码中vTask1平时会不断执行并周期性进入 Blocked而vTask2会在固定时间后把它挂起再恢复。这样你在调试器里就能观察到Task1正常执行时Running / Blocked 来回切换被挂起后进入 Suspended被恢复后先回到 Ready再等待调度进入 Running3.4. 空闲任务Idle Task与 Delay 函数在 FreeRTOS 中CPU 并不是一直被“用户任务”占用的。当系统中没有任何就绪任务时FreeRTOS 仍然需要一个任务来运行这个任务就是空闲任务Idle Task。同时任务是否“让出 CPU”很大程度上取决于 Delay 类函数的使用。3.4.1.空闲任务IdleFreeRTOS 在启动调度器时会自动创建一个 Idle 任务。这个任务始终存在不需要用户手动创建也不能删除。它的优先级是系统中最低的只有当没有其他任务处于 Ready 状态时Idle 才会运行。空闲任务是系统自动创建的最低优先级任务优先级0。Idle任务的作用保证系统始终有任务在运行避免CPU“空转无管理”回收被删除任务的资源重要可用于执行低优先级后台逻辑当你调用vTaskDelete()删除任务时其内存并不是立即释放的而是由 Idle 任务在后续运行中完成回收3.4.2. Delay函数任务让出CPU的关键在 FreeRTOS 中如果任务一直运行而不主动让出 CPU就会导致其他低优先级任务无法执行。因此Delay 类函数的作用非常重要。① vTaskDelay延时并让出CPUvoid vTaskDelay(const TickType_t xTicksToDelay);作用让当前任务进入 Blocked 状态在指定 Tick 后重新进入 Ready示例while (1) { // 执行任务逻辑 vTaskDelay(pdMS_TO_TICKS(1000)); // 延时1秒 }② vTaskDelayUntil周期任务 void vTaskDelayUntil( TickType_t * const pxPreviousWakeTime, const TickType_t xTimeIncrement );作用用于实现严格周期任务不会累计误差示例代码TickType_t xLastWakeTime xTaskGetTickCount(); while (1) { // 周期任务 vTaskDelayUntil(xLastWakeTime, pdMS_TO_TICKS(1000)); }保证任务“每1秒执行一次”而不是“延时1秒再执行”3.5. 任务调度3.5.1. 什么是任务调度任务调度是 FreeRTOS 内核的核心功能它的作用是在多个任务之间分配 CPU 的执行权。在单核系统中同一时刻只能有一个任务运行因此调度器需要不断在各个任务之间切换从而实现“看起来同时运行”的效果。调度器的基本原则可以概括为一句话从所有 Ready 状态的任务中选择优先级最高的任务运行。也就是说任务能不能运行取决于两个条件一是是否处于 Ready 状态二是优先级是否足够高。3.5.2. 调度方式FreeRTOS 的调度方式主要由两个机制决定抢占式调度和时间片调度它们共同决定了任务切换的行为。首先是抢占式调度。当一个高优先级任务进入 Ready 状态时系统会立即中断当前正在运行的低优先级任务并切换到高优先级任务执行。这保证了关键任务能够被优先响应是实时系统最重要的特性。其次是时间片调度。当多个任务具有相同优先级时系统不会一直运行某一个任务而是按照时间片轮流执行。例如任务A运行一段时间后会切换到任务B再切回A从而保证同优先级任务之间的公平性。这两种机制分别由配置宏控制#define configUSE_PREEMPTION 1 // 是否开启抢占 #define configUSE_TIME_SLICING 1 // 是否开启时间片3.5.3. 实战代码下面通过一个简单例子来观察调度行为。我们创建两个任务一个高优先级一个低优先级并让高优先级任务周期性延时。#include FreeRTOS.h #include task.h void vHighTask(void *pvParameters) { while (1) { // 高优先级任务执行 vTaskDelay(pdMS_TO_TICKS(1000)); // 进入Blocked } } void vLowTask(void *pvParameters) { while (1) { // 低优先级任务执行 } } int main(void) { xTaskCreate(vLowTask, LowTask, 128, NULL, 1, NULL); // 低优先级 xTaskCreate(vHighTask, HighTask, 128, NULL, 3, NULL); // 高优先级 vTaskStartScheduler(); while (1); }这段代码的运行逻辑如下系统启动后高优先级任务优先运行当vHighTask调用vTaskDelay()时进入 Blocked 状态此时 CPU 空出来低优先级任务开始运行当延时结束高优先级任务重新进入 Ready并立即抢占 CPU通过这个例子可以清楚看到调度并不是平均分配 CPU而是由优先级和任务状态共同决定的动态过程。