跳转至

FreeRTOS xTaskCreate 任务创建链路解析

本文按本工程源码来解释任务创建过程,重点讲清楚三件事:

  1. xTaskCreate() 到底做了什么。
  2. TCB 是什么,为什么任务离不开 TCB。
  3. FreeRTOS 为什么用任务链表管理所有任务。

本工程关键信息:

项目 本工程取值
FreeRTOS 版本 V10.5.1 / V202212.01
MCU 移植层 FreeRTOS/portable/RVDS/ARM_CM3
堆管理 FreeRTOS/portable/MemMang/heap_4.c
任务优先级数量 configMAX_PRIORITIES = 5,优先级范围是 0 ~ 4
系统节拍 configTICK_RATE_HZ = 1000,也就是 1ms 一次 tick
总堆大小 configTOTAL_HEAP_SIZE = 17 * 1024 字节
栈增长方向 portSTACK_GROWTH = -1,Cortex-M3 栈从高地址向低地址增长

源码入口参考:

源码 作用
Demo/demo.c 示例里调用 xTaskCreate()
FreeRTOS/source/tasks.c 任务创建、任务链表挂接、调度选择
FreeRTOS/include/list.h 链表结构和链表宏
FreeRTOS/source/list.c 链表插入、删除、初始化
FreeRTOS/portable/RVDS/ARM_CM3/port.c Cortex-M3 栈初始化和上下文切换
Core/Inc/FreeRTOSConfig.h FreeRTOS 配置

1. 一句话看懂 xTaskCreate

xTaskCreate() 不是直接让任务运行,它只是把“一个普通 C 函数”包装成 FreeRTOS 能调度的“任务对象”。

这个“任务对象”主要由两块内存组成:

  1. TCB:任务控制块,记录任务名字、优先级、栈顶、链表节点等管理信息。
  2. 任务栈:任务真正运行时使用的栈,里面预先放好一份“像是被中断打断过”的寄存器现场。

等任务创建完成后,FreeRTOS 会把 TCB 里的 xStateListItem 挂到对应优先级的就绪链表里。调度器以后不是直接找函数,而是从就绪链表里找 TCB,再通过 TCB 找到任务栈,从任务栈恢复现场,让任务函数开始跑。

flowchart TD
    A[普通 C 函数 start_task] --> B[xTaskCreate]
    B --> C[申请 TCB 内存]
    B --> D[申请任务栈内存]
    C --> E[初始化 TCB]
    D --> E
    E --> F[初始化任务栈现场]
    F --> G[把 TCB 的 xStateListItem 挂到就绪链表]
    G --> H[调度器以后从链表里选任务]
    H --> I[通过 pxCurrentTCB 指向当前运行任务]

2. 本工程示例入口

Demo/demo.c 里有一个任务创建示例:

#define START_TASK_STACK_SIZE 128
#define START_TASK_PRIORITY 1

TaskHandle_t start_task_handle;

void start_task(void *pvParameters);

void demo_start(void)
{
    xTaskCreate((TaskFunction_t)start_task,
                (char*)"start_task",
                (configSTACK_DEPTH_TYPE)START_TASK_STACK_SIZE,
                (void*)NULL,
                (UBaseType_t)START_TASK_PRIORITY,
                NULL);
}

这次调用的含义很直接:

参数 本例传入 含义
pxTaskCode start_task 任务函数入口
pcName "start_task" 任务名字,方便调试
usStackDepth 128 栈深度,单位是 StackType_t,不是字节
pvParameters NULL 传给任务函数的参数
uxPriority 1 任务优先级
pxCreatedTask NULL 不接收任务句柄

注意:xTaskCreate() 只负责创建任务。真正开始调度,还需要后面调用 vTaskStartScheduler()


3. xTaskCreate 总链路

下面这张图是任务创建的主链路。

sequenceDiagram
    participant App as 应用层 demo_start
    participant API as xTaskCreate
    participant Heap as heap_4 pvPortMalloc
    participant Init as prvInitialiseNewTask
    participant List as prvAddNewTaskToReadyList
    participant Ready as pxReadyTasksLists

    App->>API: xTaskCreate(start_task, name, stack, param, priority, handle)
    API->>Heap: 申请任务栈 pxStack
    Heap-->>API: 返回栈内存地址
    API->>Heap: 申请 TCB pxNewTCB
    Heap-->>API: 返回 TCB 内存地址
    API->>Init: 填充 TCB 字段
    Init->>Init: 计算栈顶 pxTopOfStack
    Init->>Init: 设置任务名、优先级、链表节点 owner
    Init->>Init: pxPortInitialiseStack 伪造首次运行现场
    Init-->>API: TCB 初始化完成
    API->>List: prvAddNewTaskToReadyList(pxNewTCB)
    List->>Ready: 插入 pxReadyTasksLists[priority]
    List-->>API: 任务进入 Ready 状态
    API-->>App: 返回 pdPASS 或内存不足错误

源码里对应的调用关系是:

xTaskCreate()
  ├─ pvPortMallocStack() / pvPortMalloc()   申请任务栈和 TCB
  ├─ prvInitialiseNewTask()                 初始化 TCB 和任务栈
  │   ├─ vListInitialiseItem()              初始化 TCB 内部的链表节点
  │   ├─ listSET_LIST_ITEM_OWNER()          让链表节点能反查 TCB
  │   └─ pxPortInitialiseStack()            构造任务首次运行的栈现场
  └─ prvAddNewTaskToReadyList()             把任务挂到就绪链表
      └─ prvAddTaskToReadyList()
          └─ listINSERT_END()

4. 第一步:申请两块核心内存

本工程是 Cortex-M3,portSTACK_GROWTH = -1,也就是栈向低地址增长。xTaskCreate() 在这种配置下会先申请任务栈,再申请 TCB。

flowchart LR
    A[xTaskCreate] --> B[pvPortMallocStack<br/>申请任务栈]
    B --> C{栈申请成功?}
    C -- 否 --> D[pxNewTCB = NULL<br/>返回内存不足]
    C -- 是 --> E[pvPortMalloc<br/>申请 TCB]
    E --> F{TCB 申请成功?}
    F -- 否 --> G[释放刚才申请的任务栈<br/>返回内存不足]
    F -- 是 --> H[memset 清零 TCB]
    H --> I[pxNewTCB->pxStack = pxStack]

为什么要两块内存?

内存 谁用 里面放什么
TCB FreeRTOS 内核用 任务名字、优先级、栈顶指针、链表节点、通知值等
任务栈 CPU 运行任务时用 局部变量、函数调用现场、寄存器保存现场

可以把 TCB 理解成“任务身份证 + 管理档案”,把任务栈理解成“任务真正干活时的工作空间”。


5. TCB 是 FreeRTOS 认识任务的核心

TCB 的源码类型是 TCB_t,真实结构体是 tskTaskControlBlock。关键字段如下:

classDiagram
    class TCB_t {
        pxTopOfStack
        xStateListItem
        xEventListItem
        uxPriority
        pxStack
        pcTaskName[]
        pxEndOfStack
        ulNotifiedValue[]
        ucNotifyState[]
    }

    class ListItem_t {
        xItemValue
        pxNext
        pxPrevious
        pvOwner
        pxContainer
    }

    class List_t {
        uxNumberOfItems
        pxIndex
        xListEnd
    }

    TCB_t *-- ListItem_t : 内含两个链表节点
    List_t o-- ListItem_t : 链表挂节点

关键字段解释:

字段 直白解释
pxTopOfStack 当前任务栈顶。上下文切换时,CPU 寄存器保存/恢复都靠它。它必须是 TCB 第一个成员。
xStateListItem 状态链表节点。任务在 Ready、Blocked、Suspended 等状态之间移动,主要移动这个节点。
xEventListItem 事件链表节点。任务等队列、信号量、事件组时,用这个节点挂到事件等待链表。
uxPriority 任务优先级。值越大,优先级越高。
pxStack 任务栈起始地址,用于释放任务或检查栈。
pcTaskName[] 任务名字,主要方便调试。
pxEndOfStack 栈边界,做栈检查时有用。

重点:FreeRTOS 调度任务时,调度的是 TCB,不是直接调度函数名。

flowchart TD
    A[任务函数 start_task] --> B[pxPortInitialiseStack 把函数入口写进任务栈]
    C[TCB_t] --> D[pxTopOfStack 指向任务栈现场]
    C --> E[uxPriority 决定放到哪个就绪链表]
    C --> F[xStateListItem 挂到状态链表]
    F --> G[pxReadyTasksLists priority]
    G --> H[调度器选中该 TCB]
    H --> I[CPU 从 TCB->pxTopOfStack 恢复现场]
    I --> A

6. 第二步:prvInitialiseNewTask 初始化 TCB

prvInitialiseNewTask() 的任务是把刚申请到的空 TCB 填完整。

flowchart TD
    A[prvInitialiseNewTask] --> B[可选: 用固定字节填充任务栈<br/>方便检查栈使用量]
    B --> C[根据 portSTACK_GROWTH 计算 pxTopOfStack]
    C --> D[拷贝任务名字 pcTaskName]
    D --> E[检查并修正 uxPriority]
    E --> F[写入 pxNewTCB->uxPriority]
    F --> G[初始化 xStateListItem 和 xEventListItem]
    G --> H[设置链表节点的 pvOwner = pxNewTCB]
    H --> I[设置 xEventListItem 的排序值]
    I --> J[pxPortInitialiseStack 构造首次运行现场]
    J --> K[如果用户要句柄<br/>*pxCreatedTask = pxNewTCB]

6.1 计算任务栈顶

本工程栈向低地址增长,所以源码逻辑是:

pxTopOfStack = &( pxNewTCB->pxStack[ ulStackDepth - 1 ] );
pxTopOfStack = 对齐后的高地址;

也就是说,任务栈内存虽然从低地址开始申请,但真正第一次压栈会从高地址开始往低地址走。

flowchart BT
    A[低地址<br/>pxStack 起始位置] --> B[任务栈空间]
    B --> C[高地址<br/>pxTopOfStack 初始位置]
    C --> D[CPU 压栈方向<br/>向低地址增长]

6.2 初始化两个链表节点

TCB 里有两个非常重要的链表节点:

TCB_t
  ├─ xStateListItem  状态链表节点
  └─ xEventListItem  事件链表节点

prvInitialiseNewTask() 会做这几步:

vListInitialiseItem( &( pxNewTCB->xStateListItem ) );
vListInitialiseItem( &( pxNewTCB->xEventListItem ) );

listSET_LIST_ITEM_OWNER( &( pxNewTCB->xStateListItem ), pxNewTCB );
listSET_LIST_ITEM_VALUE( &( pxNewTCB->xEventListItem ),
                         configMAX_PRIORITIES - uxPriority );
listSET_LIST_ITEM_OWNER( &( pxNewTCB->xEventListItem ), pxNewTCB );

pvOwner 很关键,它让链表节点能反向找到自己的 TCB:

flowchart LR
    A[pxReadyTasksLists 里的 ListItem_t] -->|pvOwner| B[TCB_t]
    B -->|xStateListItem| A

这就是 FreeRTOS 链表设计的核心:链表里挂的是节点 ListItem_t,但节点里有 pvOwner,所以内核可以通过节点找到真正的任务 TCB。

6.3 pxPortInitialiseStack:让任务看起来像“已经准备好被恢复”

任务刚创建时从来没有真正运行过。那调度器第一次切到它时,怎么让 CPU 跳进 start_task()

答案是:pxPortInitialiseStack() 提前在任务栈里摆好一份寄存器现场。

在 Cortex-M3 中,首次任务栈大概会被摆成这样:

image-20260424221441475

这一步的意义非常大:

  1. PC 被写成任务函数入口,所以任务第一次恢复时会跳进任务函数。
  2. R0 被写成 pvParameters,所以任务函数能收到参数。
  3. LR 被写成 prvTaskExitError,如果任务函数错误返回,就会进入错误处理。
  4. 返回的新栈顶会保存进 pxNewTCB->pxTopOfStack

7. 第三步:prvAddNewTaskToReadyList 把任务放进就绪链表

TCB 初始化完成后,任务还不能被调度器找到。必须把它挂进就绪链表。

sequenceDiagram
    participant API as xTaskCreate
    participant Add as prvAddNewTaskToReadyList
    participant Core as FreeRTOS 全局状态
    participant Ready as pxReadyTasksLists

    API->>Add: prvAddNewTaskToReadyList(pxNewTCB)
    Add->>Add: taskENTER_CRITICAL 进入临界区
    Add->>Core: uxCurrentNumberOfTasks++
    alt pxCurrentTCB == NULL
        Add->>Core: pxCurrentTCB = pxNewTCB
        alt 这是第一个任务
            Add->>Core: prvInitialiseTaskLists 初始化所有任务链表
        end
    else 调度器还没运行
        Add->>Core: 如果新任务优先级更高<br/>pxCurrentTCB = pxNewTCB
    end
    Add->>Ready: prvAddTaskToReadyList(pxNewTCB)
    Ready->>Ready: listINSERT_END(&pxReadyTasksLists[uxPriority], &xStateListItem)
    Add->>Add: taskEXIT_CRITICAL 退出临界区
    alt 调度器已经运行且新任务优先级更高
        Add->>Core: taskYIELD_IF_USING_PREEMPTION 触发切换
    end

这里有两个核心点:

7.1 第一个任务创建时,会初始化所有任务链表

prvInitialiseTaskLists() 会初始化:

链表 作用
pxReadyTasksLists[0] ~ pxReadyTasksLists[4] 就绪链表,每个优先级一个
xDelayedTaskList1 延时链表
xDelayedTaskList2 tick 溢出时使用的延时链表
xPendingReadyList 调度器挂起时,临时保存已经就绪的任务
xTasksWaitingTermination 等待空闲任务清理的已删除任务
xSuspendedTaskList 挂起任务链表
flowchart TD
    A[prvInitialiseTaskLists] --> B[初始化 5 个就绪链表]
    A --> C[初始化两个延时链表]
    A --> D[初始化 pending ready 链表]
    A --> E[初始化删除等待链表]
    A --> F[初始化挂起链表]
    A --> G[pxDelayedTaskList = xDelayedTaskList1]
    A --> H[pxOverflowDelayedTaskList = xDelayedTaskList2]

7.2 就绪链表是按优先级分组的数组

本工程 configMAX_PRIORITIES = 5,所以有 5 条就绪链表:

flowchart TD
    R[pxReadyTasksLists 数组] --> P0[优先级 0 就绪链表]
    R --> P1[优先级 1 就绪链表]
    R --> P2[优先级 2 就绪链表]
    R --> P3[优先级 3 就绪链表]
    R --> P4[优先级 4 就绪链表]

    P1 --> T1[start_task 的 xStateListItem]
    T1 --> O1[pvOwner 指回 start_task 的 TCB]

START_TASK_PRIORITY = 1,所以 start_task 会被插入:

pxReadyTasksLists[1]

实际插入动作来自这个宏:

listINSERT_END( &( pxReadyTasksLists[ pxTCB->uxPriority ] ),
                &( pxTCB->xStateListItem ) );

意思是:把这个任务 TCB 里的 xStateListItem 插到对应优先级链表的尾部。


8. 任务链表 List_t / ListItem_t 怎么工作

FreeRTOS 的链表是双向循环链表。

List_t 代表一条链表,里面有:

字段 作用
uxNumberOfItems 当前链表里有几个真实节点
pxIndex 当前遍历到哪里了,用于同优先级任务轮流运行
xListEnd 结束标记节点,不是任务,只是链表尾标记

ListItem_t 代表一个节点,里面有:

字段 作用
xItemValue 排序值。延时链表里常用作唤醒时间,事件链表里常用作优先级排序值
pxNext 下一个节点
pxPrevious 上一个节点
pvOwner 拥有这个节点的对象,任务链表里一般就是 TCB
pxContainer 当前节点属于哪条链表

空链表长这样:

flowchart LR
    L[List_t] --> End[xListEnd 结束标记]
    End -->|pxNext| End
    End -->|pxPrevious| End
    L --> N[uxNumberOfItems = 0]
    L --> I[pxIndex = xListEnd]

插入一个任务后:

flowchart LR
    L[List_t: pxReadyTasksLists 1] --> End[xListEnd]
    End -->|pxNext| A[start_task.xStateListItem]
    A -->|pxNext| End
    End -->|pxPrevious| A
    A -->|pxPrevious| End
    A -->|pvOwner| TCB[start_task TCB]
    A -->|pxContainer| L

插入两个同优先级任务后:

flowchart LR
    End[xListEnd] --> A[TaskA.xStateListItem]
    A --> B[TaskB.xStateListItem]
    B --> End
    End -.pxPrevious.-> B
    B -.pxPrevious.-> A
    A -.pxPrevious.-> End

同一个优先级有多个任务时,调度器通过 pxIndex 轮流取下一个任务,这样同优先级任务可以时间片轮转。


9. 调度器如何从链表选任务

创建任务只是把任务放到 Ready 状态。真正运行谁,由调度器决定。

核心宏是 taskSELECT_HIGHEST_PRIORITY_TASK()

它的逻辑可以直白理解为:

  1. 从当前记录的最高优先级开始找。
  2. 找到最高的、非空的就绪链表。
  3. 从这条链表里取下一个 TCB。
  4. pxCurrentTCB 指向这个 TCB。
flowchart TD
    A[需要切换任务] --> B[从最高优先级开始查 pxReadyTasksLists]
    B --> C{该优先级链表为空?}
    C -- 是 --> D[优先级减 1 继续找]
    D --> C
    C -- 否 --> E[listGET_OWNER_OF_NEXT_ENTRY]
    E --> F[通过 ListItem_t.pvOwner 找到 TCB]
    F --> G[pxCurrentTCB = 选中的 TCB]
    G --> H[PendSV 从 pxCurrentTCB->pxTopOfStack 恢复任务现场]

核心关系是:

pxReadyTasksLists[优先级]
  -> ListItem_t
      -> pvOwner
          -> TCB_t
              -> pxTopOfStack
                  -> CPU 恢复寄存器

这条链路就是 FreeRTOS 任务调度的主干。


10. 创建完成后,到第一次运行之间发生什么

完整过程可以这样看:

sequenceDiagram
    participant App as 应用代码
    participant Task as tasks.c
    participant Ready as 就绪链表
    participant Port as Cortex-M3 port.c
    participant CPU as CPU

    App->>Task: xTaskCreate 创建任务
    Task->>Ready: 把 TCB->xStateListItem 插入就绪链表
    App->>Task: vTaskStartScheduler
    Task->>Task: 创建 Idle 任务
    Task->>Port: xPortStartScheduler
    Port->>CPU: 配置 SysTick / PendSV / SVC
    Port->>CPU: SVC 启动第一个任务
    CPU->>Task: 根据 pxCurrentTCB 找到任务栈
    CPU->>CPU: 恢复寄存器现场
    CPU->>App: 跳转到任务函数 start_task

更底层一点,首次启动任务时,Cortex-M3 的移植层会通过 SVC 异常恢复任务现场。之后任务切换主要靠 PendSV

flowchart TD
    A[当前任务运行] --> B[SysTick 或主动 yield]
    B --> C[触发 PendSV]
    C --> D[保存当前任务 R4-R11 到当前任务栈]
    D --> E[把新的栈顶写入 pxCurrentTCB->pxTopOfStack]
    E --> F[vTaskSwitchContext 选择下一个 TCB]
    F --> G[从新的 pxCurrentTCB->pxTopOfStack 恢复 R4-R11]
    G --> H[异常返回后继续运行新任务]

为什么 TCB 的第一个成员必须是 pxTopOfStack

因为 port.c 的汇编切换代码会直接认为:

TCB 地址的第一个字段 = 任务栈顶指针

所以 pxTopOfStack 放在 TCB 第一个成员,是上下文切换能正确工作的硬要求。


11. TCB、任务栈、任务链表三者的关系

这张图是整篇文档最核心的图。

flowchart TD
    subgraph Heap[FreeRTOS 堆 heap_4]
        Stack[任务栈内存<br/>保存寄存器现场和局部变量]
        TCB[TCB_t<br/>任务管理信息]
    end

    subgraph TCBFields[TCB 内部关键字段]
        Top[pxTopOfStack<br/>当前栈顶]
        Base[pxStack<br/>栈起始地址]
        Prio[uxPriority<br/>优先级]
        Name[pcTaskName<br/>任务名]
        StateItem[xStateListItem<br/>状态链表节点]
        EventItem[xEventListItem<br/>事件链表节点]
    end

    subgraph Lists[FreeRTOS 内核链表]
        Ready[pxReadyTasksLists 优先级就绪链表]
        Delay[pxDelayedTaskList 延时链表]
        Suspend[xSuspendedTaskList 挂起链表]
        Event[队列/信号量/事件等待链表]
    end

    TCB --> Top
    TCB --> Base
    TCB --> Prio
    TCB --> Name
    TCB --> StateItem
    TCB --> EventItem

    Top --> Stack
    Base --> Stack
    StateItem --> Ready
    StateItem --> Delay
    StateItem --> Suspend
    EventItem --> Event
    StateItem -->|pvOwner| TCB
    EventItem -->|pvOwner| TCB

可以这样记:

概念 一句话
任务函数 你写的业务代码入口
任务栈 CPU 跑这个任务时用的私有栈
TCB FreeRTOS 管这个任务时用的档案
xStateListItem TCB 在状态链表里的挂钩
xEventListItem TCB 在事件等待链表里的挂钩
就绪链表 调度器挑任务的候选池
pxCurrentTCB 当前正在运行,或者即将运行的任务 TCB

12. FreeRTOS 核心思想:任务状态就是链表位置

FreeRTOS 判断任务状态,很多时候不是靠一个简单的 state 枚举字段,而是看 TCB 的链表节点在哪里。

stateDiagram-v2
    [*] --> Ready: xTaskCreate 后插入就绪链表
    Ready --> Running: 调度器选中 pxCurrentTCB
    Running --> Ready: 时间片到 / 被更高优先级任务抢占
    Running --> Blocked: vTaskDelay / 等队列 / 等信号量
    Blocked --> Ready: 超时到 / 事件发生
    Running --> Suspended: vTaskSuspend
    Suspended --> Ready: vTaskResume
    Running --> Deleted: vTaskDelete
    Deleted --> [*]: Idle 任务清理 TCB 和栈

对应到链表就是:

任务状态 TCB 的 xStateListItem 通常在哪里
Ready pxReadyTasksLists[uxPriority]
Blocked pxDelayedTaskListpxOverflowDelayedTaskList
Suspended xSuspendedTaskList
Deleted 等待清理 xTasksWaitingTermination

事件等待稍微特殊:任务等队列、信号量时,xStateListItem 会进入延时链表,xEventListItem 会进入对象自己的事件等待链表。

flowchart LR
    T[某个正在等待队列的 TCB] --> S[xStateListItem]
    T --> E[xEventListItem]
    S --> D[延时链表<br/>负责超时唤醒]
    E --> Q[队列事件链表<br/>负责事件唤醒]

这就是为什么 TCB 里要有两个链表节点:一个管“时间和状态”,一个管“事件等待”。


13. 用 start_task 举一个完整例子

假设 demo_start() 调用了:

xTaskCreate(start_task, "start_task", 128, NULL, 1, NULL);

创建完成后,内核数据结构大概是:

flowchart TD
    A[start_task 函数] --> B[任务栈]
    B --> C[栈里 PC = start_task<br/>R0 = NULL]
    D[start_task 的 TCB] --> E[pcTaskName = start_task]
    D --> F[uxPriority = 1]
    D --> G[pxStack 指向任务栈起始地址]
    D --> H[pxTopOfStack 指向伪造现场后的栈顶]
    D --> I[xStateListItem]
    I --> J[pxReadyTasksLists 1]
    I -->|pvOwner| D

如果现在 Ready 链表只有这一个用户任务和 Idle 任务,那么调度器启动后:

  1. Idle 任务优先级是 0。
  2. start_task 优先级是 1。
  3. 调度器会先选优先级 1 的 start_task
flowchart TD
    P4[优先级 4 空] --> P3[优先级 3 空]
    P3 --> P2[优先级 2 空]
    P2 --> P1[优先级 1 有 start_task]
    P1 --> Run[选择 start_task 运行]
    P0[优先级 0 有 Idle] --> Idle[只有高优先级没任务时才运行]

14. 常见问题

14.1 为什么 xTaskCreate 返回成功,任务却没运行?

常见原因:

  1. 没有调用 vTaskStartScheduler()
  2. 任务函数创建了,但 main() 还在裸机 while(1) 里跑。
  3. 堆不够,xTaskCreate() 实际返回了 errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY
  4. 任务函数没有写死循环,执行完返回了。
  5. 中断优先级配置不符合 FreeRTOS 要求,导致调度异常。

14.2 栈大小 128 是 128 字节吗?

不是。

xTaskCreate()usStackDepth 单位是 StackType_t。在 Cortex-M3 上通常 StackType_t 是 32 位,也就是 4 字节。

所以:

128 个 StackType_t = 128 * 4 = 512 字节

14.3 TCB 里为什么有两个链表节点?

因为一个任务可能同时需要表达两件事:

  1. 它现在处于什么状态,比如 Ready、Blocked、Suspended。
  2. 它正在等哪个事件,比如队列、信号量、事件组。

所以:

节点 负责
xStateListItem 任务状态
xEventListItem 事件等待

14.4 为什么同优先级任务可以轮流运行?

同优先级任务挂在同一条 Ready 链表里。调度器用 listGET_OWNER_OF_NEXT_ENTRY() 每次取下一个节点,并更新链表的 pxIndex

所以同优先级任务不是永远取第一个,而是沿着链表往后走。

14.5 为什么任务函数不能直接 return?

任务函数不是普通函数调用。它是被调度器通过伪造的栈现场“恢复”出来的,没有正常的调用者。

所以 FreeRTOS 在任务初始栈里把 LR 放成 prvTaskExitError。如果任务函数 return,就会进入错误处理。

正确写法通常是:

void start_task(void *pvParameters)
{
    for (;;)
    {
        /* 任务循环 */
    }
}

如果任务真的要结束,应调用:

vTaskDelete(NULL);

15. 最后总结

xTaskCreate() 的本质是:

申请内存 -> 填 TCB -> 伪造任务栈 -> 挂入 Ready 链表 -> 等调度器选择

FreeRTOS 任务系统的核心可以压缩成这张图:

flowchart LR
    A[xTaskCreate] --> B[TCB]
    B --> C[任务栈]
    B --> D[xStateListItem]
    D --> E[Ready / Blocked / Suspended 链表]
    E --> F[调度器选择 TCB]
    F --> G[pxCurrentTCB]
    G --> H[从 pxTopOfStack 恢复 CPU 现场]
    H --> I[任务函数运行]

最重要的理解点:

  1. TCB 是任务的管理档案,FreeRTOS 通过 TCB 管任务。
  2. 任务栈保存 CPU 运行现场,切换任务就是保存和恢复不同任务的栈现场。
  3. 任务链表保存任务状态,任务在哪里,基本就说明它是什么状态。
  4. Ready 链表按优先级分组,调度器总是优先找最高优先级的非空 Ready 链表。
  5. pxCurrentTCB 是当前任务指针,上下文切换最终就是换掉它,再恢复新任务的栈。