一、任务的创建

栈是内存中一块特殊的区域,主要用于:

任务上下文保存:当任务被切换时,CPU 寄存器(如 PC、SP、通用寄存器等)的值会被压入栈中,以便任务恢复时能够还原现场。
局部变量存储:任务函数中定义的局部变量在栈上分配内存。
函数调用链管理:保存函数调用时的返回地址、参数传递等信息。

和堆的对比:

特性 栈(Stack) 堆(Heap)
管理方式 自动(由系统或编译器管理) 手动(需调用 malloc/free 等函数)
数据顺序 LIFO(后进先出) 无特定顺序
内存增长方向 多数系统从高地址向低地址增长(递减栈) 从低地址向高地址增长
效率 快(仅需移动栈指针) 慢(涉及内存分配算法)
碎片问题 无碎片 可能产生碎片
空间大小 通常较小(由系统或开发者限制) 较大(受限于可用内存总量)

1.静态分配栈

静态创建任务时直接指定一块预先定义的数组作为栈空间

特点:

    栈空间在编译时确定,不依赖动态内存分配(无需heap支持)
    适合对内存安全要求高的场景

// 静态定义任务栈数组和任务句柄
static StackType_t xTaskStack[ configMINIMAL_STACK_SIZE ];
static StaticTask_t xTaskBuffer;

// 创建静态任务
TaskHandle_t xTaskCreateStatic(
    TaskFunction_t pxTaskCode,       // 任务函数
    const char * const pcName,       // 任务名称
    uint32_t ulStackDepth,           // 栈深度(单位:字,如32位系统为4字节/字)
    void * const pvParameters,       // 任务参数
    UBaseType_t uxPriority,          // 任务优先级
    StackType_t * const puxStackBuffer, // 栈数组
    StaticTask_t * const pxTaskBuffer // 任务控制块
);

2.动态分配栈

通过动态创建任务xTaskCreate()自动从 FreeRTOS 中分配栈空间

// 创建动态任务
BaseType_t xTaskCreate(
    TaskFunction_t pxTaskCode,
    const char * const pcName,
    configSTACK_DEPTH_TYPE usStackDepth, // 栈深度(单位:字)
    void * const pvParameters,
    UBaseType_t uxPriority,
    TaskHandle_t * const pxCreatedTask
);

堆大小配置
通过FreeRTOSConfig.h中的configTOTAL_HEAP_SIZE定义堆总大小

#define configTOTAL_HEAP_SIZE (16 * 1024)  // 16KB堆空间

堆内存管理方案
FreeRTOS 提供 5 种堆分配方案(位于heap_1.c~heap_5.c),需在FreeRTOSConfig.h中通过configUSE_MALLOC_FAILED_HOOK选择:
方案 1(heap_1.c
特点:仅支持内存分配(无释放),速度最快,无内存碎片。
适用场景:任务数量固定且无需删除的简单系统。
方案 2(heap_2.c
特点:支持分配和释放,使用最佳匹配算法,但可能导致碎片化。
适用场景:任务创建和删除频繁,但栈大小相近的系统。
方案 3(heap_3.c
特点:封装标准 C 库的malloc()和free(),线程安全。
适用场景:依赖标准库内存管理的系统。
方案 4(heap_4.c
特点:支持合并相邻空闲块,减少碎片化,使用首次匹配算法。
适用场景:任务栈大小差异较大的复杂系统。
方案 5(heap_5.c
特点:支持在不连续的内存区域(如 SRAM 和外部 RAM)分配堆。
适用场景:内存分布分散的系统

TCB(任务控制块)

任务控制块(Task Control Block, TCB)是管理任务的核心数据结构,存储了任务的状态、上下文和属性信息

任务控制块中包含了管理任务所需的关键信息,具体如下:首先是任务状态,用于表明任务当前处于运行、就绪、阻塞或挂起等状态;任务优先级,决定了任务在调度时的顺序;任务栈指针,指向任务栈的顶部,用于保存任务上下文;任务句柄,作为任务的唯一标识;还有任务名称,方便调试和识别。此外,TCB 中还可能包含任务的阻塞时间等待事件的相关信息,以及指向链表节点的指针,用于将任务组织到不同优先级的就绪链表、阻塞链表或挂起链表中,从而实现操作系统对任务的高效管理和调度。

1.动态分配

通过xTaskCreate()自动从 FreeRTOS 堆分配内存

xTaskCreate()函数会:

从 FreeRTOS 堆中分配两块内存:
   1.任务控制块(TCB):存储任务状态、优先级、栈指针等信息。
   2.任务栈:用于保存任务上下文和局部变量。
初始化 TCB 和栈空间,并将栈指针指向栈顶。
将任务添加到就绪列表,等待调度器执行

2.静态分配

静态创建任务时用户需要手动分配任务控制块(TCB)和任务栈的内存,这种方式不依赖动态内存分配(堆),适合对内存安全要求高的嵌入式系统

#include "FreeRTOS.h"
#include "task.h"

// 1. 静态分配任务控制块(TCB)
static StaticTask_t xTaskTCB;  // 用户定义的TCB变量

// 2. 静态分配任务栈
static StackType_t xTaskStack[256];  // 256字的任务栈(假设32位系统=1024字节)

// 任务函数
void vTaskFunction(void *pvParameters) {
    // 任务逻辑
    for (;;) {
        // ...
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

// 3. 创建静态任务
void vCreateStaticTask(void) {
    TaskHandle_t xTaskHandle;
    
    xTaskHandle = xTaskCreateStatic(
        vTaskFunction,      // 任务函数
        "StaticTask",       // 任务名称
        256,                // 栈深度(单位:字)
        NULL,               // 任务参数
        1,                  // 任务优先级
        xTaskStack,         // 预分配的任务栈
        &xTaskTCB           // 预分配的TCB
    );
    
    // 任务创建后,xTaskTCB将被FreeRTOS初始化和管理
}

使用静态任务创建时,需在FreeRTOSConfig.h中启用以下配置

// 启用静态内存分配支持
#define configSUPPORT_STATIC_ALLOCATION 1

// 实现空闲任务的静态内存分配钩子
void vApplicationGetIdleTaskMemory(StaticTask_t **ppxIdleTaskTCBBuffer,
                                   StackType_t **ppxIdleTaskStackBuffer,
                                   uint32_t *pulIdleTaskStackSize) {
    static StaticTask_t xIdleTaskTCB;
    static StackType_t uxIdleTaskStack[configMINIMAL_STACK_SIZE];
    
    *ppxIdleTaskTCBBuffer = &xIdleTaskTCB;
    *ppxIdleTaskStackBuffer = uxIdleTaskStack;
    *pulIdleTaskStackSize = configMINIMAL_STACK_SIZE;
}

// 如果使用了定时器服务,还需实现:
void vApplicationGetTimerTaskMemory(StaticTask_t **ppxTimerTaskTCBBuffer,
                                    StackType_t **ppxTimerTaskStackBuffer,
                                    uint32_t *pulTimerTaskStackSize) {
    // 为定时器任务提供静态内存
    static StaticTask_t xTimerTaskTCB;
    static StackType_t uxTimerTaskStack[configTIMER_TASK_STACK_DEPTH];
    
    *ppxTimerTaskTCBBuffer = &xTimerTaskTCB;
    *ppxTimerTaskStackBuffer = uxTimerTaskStack;
    *pulTimerTaskStackSize = configTIMER_TASK_STACK_DEPTH;
}

二、任务的调度机制

调度器是任务调度的「大脑」,而遍历链表是其实现调度算法的「具体动作」。无论是优先级抢占、时间片轮转还是任务阻塞后的调度,调度器都会根据当前系统状态和调度策略,有针对性地遍历相应任务链表,确保 CPU 资源高效分配给符合条件的任务。这一过程结合了数据结构优化(如优先级位图、高效链表操作),以保证调度效率和实时性。

FreeRTOS 的任务调度机制核心依赖于任务链表(Task Lists),这是一组用于组织和管理任务状态的数据结构。链表通过嵌入在 TCB 中的节点(ListItem_t)来管理任务。

FreeRTOS 主要维护以下链表:

就绪列表(Ready Lists):按优先级分组的链表数组,每个优先级对应一个链表,存储所有就绪态任务。
阻塞列表(Blocked Lists):存储所有因等待事件(如延时、信号量)而阻塞的任务,按延时时间排序。
挂起列表(Suspended List):存储被挂起的任务(调用 vTaskSuspend())

举例

在 FreeRTOS 中,当任务调度器决定要执行任务 A 时,会通过一系列对任务 A 的 TCB(任务控制块)的操作来完成任务切换和运行控制,具体流程如下:

一、调度器选择任务 A 的前提:TCB 中的关键信息

调度器首先需要通过任务 A 的 TCB 确认其具备运行条件,主要依赖 TCB 中的以下信息:

任务状态(eState):任务 A 的 TCB 必须处于 “就绪状态”(eReady),且已被挂载到对应优先级的就绪链表中。
任务优先级(uxPriority):任务 A 的优先级必须是当前所有就绪任务中最高的(基于 FreeRTOS 的优先级抢占调度机制)。
调度器状态:系统调度器未被挂起(vTaskSuspendAll()未调用或已通过xTaskResumeAll()恢复)。

二、调度器控制 TCB 执行任务 A 的核心步骤

1. 定位任务 A 的 TCB
调度器通过遍历就绪任务链表(pxReadyTasksLists),从最高优先级链表开始查找,找到处于就绪状态的任务 A 的 TCB。
就绪链表中每个节点本质是任务 TCB 中的xStateListItem(状态链表项),通过链表指针可直接访问完整的 TCB。
2. 保存当前运行任务的上下文到其 TCB
若当前已有运行的任务(如任务 B),调度器会触发一次上下文切换:
借助 CPU 的堆栈指针,将当前任务 B 的 CPU 寄存器(如 PC 程序计数器、LR 链接寄存器、通用寄存器等)压入任务 B 的堆栈。
更新任务 B 的 TCB 中的pxTopOfStack(栈顶指针),记录当前堆栈位置(以便后续恢复)。
将任务 B 的 TCB 状态从 “运行”(eRunning)改回 “就绪”(eReady),并保持在就绪链表中(若仍有运行资格)。
3. 更新任务 A 的 TCB 状态
调度器将任务 A 的 TCB 状态从 “就绪”(eReady)改为 “运行”(eRunning),标记其为当前活跃任务。
(可选)若系统启用了任务运行时间统计,会重置任务 A 的 TCB 中ulRunTimeCounter(运行时间计数器)。
4. 从任务 A 的 TCB 恢复上下文并运行
调度器从任务 A 的 TCB 中获取pxTopOfStack(栈顶指针),该指针指向任务 A 上次被切换出去时保存的堆栈位置。
将堆栈中保存的 CPU 寄存器值恢复到实际寄存器中(如恢复 PC 指针到任务 A 上次中断的指令位置)。
此时 CPU 的程序计数器指向任务 A 的代码段,任务 A 开始执行。

三、任务 A 运行过程中调度器对 TCB 的动态控制

任务 A 运行时,调度器会通过监控其 TCB 来决定是否需要再次调度:

主动放弃 CPU:若任务 A 调用vTaskDelay()(延时)或xQueueReceive()(等待事件),调度器会:
修改任务 A 的 TCB 状态为 “阻塞”(eBlocked),并设置xTicksToWait(阻塞超时时间)。
将其 TCB 从就绪链表移到阻塞链表(xDelayedTaskList或事件相关链表)。
重新从就绪链表选择下一个最高优先级任务执行。
被更高优先级任务抢占:若有更高优先级任务(任务 C)进入就绪状态,调度器会:
立即暂停任务 A,将其上下文保存到 TCB 的堆栈中(更新pxTopOfStack)。
任务 A 的 TCB 状态改回 “就绪”,回到就绪链表。
切换到任务 C 的 TCB,恢复其上下文并运行。
任务 A 执行完毕:若任务 A 是一次性任务(如通过vTaskDelete(NULL)删除自身),调度器会:
将其 TCB 从所有链表中移除,标记为 “删除” 状态(eDeleted)。
释放 TCB 和任务堆栈占用的内存。
从就绪链表选择下一个任务执行。

四、核心依赖:TCB 中的堆栈指针与链表节点

pxTopOfStack:是任务切换的 “锚点”,调度器通过它保存 / 恢复任务上下文,确保任务 A 能从上次中断的位置继续执行。
链表节点(xStateListItem):调度器通过操作这些节点,将任务 A 的 TCB 在不同状态链表(就绪、阻塞、挂起)之间迁移,实现对任务生命周期的管理。

1.高优先级任务直接抢占

比如:

初始状态:
- 优先级3链表:[任务A](运行中)
- 优先级5链表:[任务B](就绪)
- uxTopReadyPriority = 5(最高就绪优先级)

抢占发生时:
1. 调度器发现当前运行任务优先级3 < uxTopReadyPriority 5
2. 将任务A从优先级3链表头部移除(保持链表顺序)
3. 从优先级5链表头部取出任务B
4. 任务B开始执行,任务A被插入到优先级3链表头部(等待下次调度)

2.多个同优先级任务调度

时间片轮转(Round Robin)是 FreeRTOS 处理同优先级任务的调度策略,其核心逻辑是:当多个任务处于同一优先级的就绪状态时,每个任务运行固定时间(时间片)后,调度器强制切换到下一个任务,确保同优先级任务公平获取 CPU 资源。

当调度器触发时间片轮转时:

1. 定位当前任务优先级P:通过TCB获取当前任务优先级
2. 访问优先级P对应的就绪链表:pxReadyTasksLists[P]
3. 检查该优先级下的任务数:uxCurrentNumberOfTasks[P]
   - 若任务数=1:无需轮转,当前任务继续执行
   - 若任务数>1:执行时间片轮转
4. 时间片计数器递减:
   - 每个任务运行时,优先级P的时间片计数器(xSchedulerRunning等相关变量)递减
   - 当计数器减为0时,触发轮转
5. 链表遍历与任务切换:
   - 将当前任务从链表头部移除,插入到链表尾部
   - 取出链表新头部的任务作为下一个运行任务
   - 执行上下文切换(保存当前任务寄存器,恢复新任务状态)

3.任务阻塞

任务阻塞是指任务因等待某个事件(如延时、信号量、队列消息)而暂时无法执行的状态。当任务阻塞时,系统会将其从就绪链表移至阻塞链表,并在事件满足后再移回调就绪链表

三、互斥的引入

多任务并发时,若直接访问共享资源(如硬件寄存器、全局变量),会因「任务切换 / 中断抢占」导致数据读写混乱(比如任务 A 写一半,任务 B 打断并修改,最终数据错误)。

1.队列

队列是一种任务间通信(IPC)机制,用于在多个任务或中断服务程序(ISR)之间传递数据。

队列的三大核心:

(1)关中断:
队列操作(如入队、出队)需保证原子性(避免任务 / 中断并发冲突),通过「关中断」或更轻量的「临界区保护」,暂停中断响应,确保队列状态读写不被打断。
(2)环形缓冲区:
队列常用的存储结构,用一段连续内存模拟循环队列,解决普通数组队列「假溢出」问题,头尾指针循环移动,高效复用空间,支持快速读写。
(3)链表:
队列可基于链表节点动态管理消息,无需预先分配固定大小缓冲区,适合消息长度 / 数量不确定场景,入队出队通过修改链表指针实现,灵活但稍增内存开销。

队列结构体源码分析:

读队列:

任务发起读队列请求后,先关中断保障操作原子性,检查队列是否有数据:有数据则拷贝到任务缓冲区、更新读指针,开中断后任务继续执行;无数据时,若阻塞时间为 0 直接返回错误、开中断继续执行,若阻塞时间 > 0 则任务进入阻塞态,从 ReadyList 移至队列的 xTasksWaitingToReceive 列表,开中断后调度器切换任务。当队列写入新数据,唤醒 xTasksWaitingToReceive 中最高优先级任务,移回 ReadyList,重新执行读队列流程 。

写队列:

写队列也是这个流程,下面是他们的区别:

读写队列流程对比

操作 队列状态判断 阻塞列表 唤醒条件
读队列 队列是否有数据 xTasksWaitingToReceive 队列被写入新数据
写队列 队列是否已满 xTasksWaitingToSend 队列被读取且有空间

带超时的资源请求


当你调用 xQueueReceive(xQueue, &data, xTicksToWait) 时:

若 xTicksToWait = portMAX_DELAY:任务会一直等待,直到队列有数据,仅依赖资源唤醒。
若 xTicksToWait > 0:任务会同时进入两种等待状态:
加入队列的 xTasksWaitingToReceive(等待数据);
加入 xDelayedTaskList(等待时间到期)。

2.信号量

FreeRTOS 中,信号量复用了队列(Queue)的底层结构(通过 QueueDefinition 的联合体实现)。用来表示资源的数量,可以看成特殊的队列

TaskHandle_t xMutexHolder:
记录持有互斥锁的任务句柄。当信号量作为互斥锁(Mutex)时,标记当前哪个任务占据了锁,防止其他任务误释放,也用于实现「优先级继承」(高优先级任务等待时,临时提升该任务优先级)。
UBaseType_t uxRecursiveCallCount:
递归互斥锁的重入计数(同一线程(任务)重复获取锁的次数)。若信号量是递归互斥锁(Recursive Mutex),同一任务可多次「获取锁」,每次获取计数 +1,释放时计数 -1 ;只有计数归 0,锁才真正被释放,避免简单嵌套导致的死锁。

信号量两种类型:

      二进制:计数器只有 0/1,适合互斥锁(Mutex)场景。
      计数型:计数器支持多值,适合资源池(如 5 个串口资源)场景。

实现核心:

1. 关中断(或临界区)
为避免任务 / 中断并发修改信号量状态(如计数器、等待队列),操作时会关中断(或进入临界区),确保原子性:

申请信号量(xSemaphoreTake):关中断 → 检查计数器 → 按需阻塞任务 → 开中断;
释放信号量(xSemaphoreGive):关中断 → 增加计数器 → 唤醒等待任务 → 开中断。
2. 等待队列
信号量内部维护等待任务链表(类似队列的 xTasksWaitingTo...):

若信号量不可用(计数器为 0),申请任务会被移入该链表,进入阻塞态;
当信号量被释放,从链表中唤醒最高优先级任务,重新竞争资源。

信号量获取:

信号量释放:

和队列的区别:

对比项 队列(Queue) 信号量(Semaphore)
数据传输 传递具体数据(如结构体、变量) 不传递数据,仅传递「事件 / 权限」
核心逻辑 数据的 FIFO 缓冲 计数器 + 等待队列
典型场景 任务间大量数据通信 同步事件、资源互斥

3.互斥量

互斥量也可以看作是特殊的信号量

互斥量和二进制信号量的区别:

1. 持有权与释放规则
互斥量:
有 “持有权” 概念,只有获取锁的任务能释放,其他任务释放无效,避免误操作。
二进制信号量:
无严格持有权,任意任务 / 中断都可释放,灵活性高但需人工保证逻辑正确。
2. 优先级继承(互斥量特有)

什么是优先级反转?
当高优先级任务等待低优先级任务持有的资源时,低优先级任务被中间优先级任务抢占,导致高优先级任务被迫长时间等待,形成 “优先级颠倒” 的现象。
互斥量:
支持优先级继承,若高优先级任务等待低优先级任务持有的互斥量,会临时提升低优先级任务优先级,避免 “优先级反转”,保障实时性
二进制信号量:
无此机制,若用于互斥场景,高优先级任务可能因等待低优先级任务释放信号量,出现长时间阻塞。

优先级继承代码:

如何实现提升优先级:

前面我们知道不同优先级有不同的ReadyList,那么直接把这个任务放在高优先级的就绪链表里面就实现了对优先级的提升,之后在放回原来的就绪链表

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐