FreeRTOS xTaskCreate 任务创建链路解析¶
本文按本工程源码来解释任务创建过程,重点讲清楚三件事:
xTaskCreate()到底做了什么。- TCB 是什么,为什么任务离不开 TCB。
- 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 能调度的“任务对象”。
这个“任务对象”主要由两块内存组成:
- TCB:任务控制块,记录任务名字、优先级、栈顶、链表节点等管理信息。
- 任务栈:任务真正运行时使用的栈,里面预先放好一份“像是被中断打断过”的寄存器现场。
等任务创建完成后,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 中,首次任务栈大概会被摆成这样:

这一步的意义非常大:
PC被写成任务函数入口,所以任务第一次恢复时会跳进任务函数。R0被写成pvParameters,所以任务函数能收到参数。LR被写成prvTaskExitError,如果任务函数错误返回,就会进入错误处理。- 返回的新栈顶会保存进
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()。
它的逻辑可以直白理解为:
- 从当前记录的最高优先级开始找。
- 找到最高的、非空的就绪链表。
- 从这条链表里取下一个 TCB。
- 把
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 | pxDelayedTaskList 或 pxOverflowDelayedTaskList |
| 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 任务,那么调度器启动后:
- Idle 任务优先级是 0。
start_task优先级是 1。- 调度器会先选优先级 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 返回成功,任务却没运行?¶
常见原因:
- 没有调用
vTaskStartScheduler()。 - 任务函数创建了,但
main()还在裸机while(1)里跑。 - 堆不够,
xTaskCreate()实际返回了errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY。 - 任务函数没有写死循环,执行完返回了。
- 中断优先级配置不符合 FreeRTOS 要求,导致调度异常。
14.2 栈大小 128 是 128 字节吗?¶
不是。
xTaskCreate() 的 usStackDepth 单位是 StackType_t。在 Cortex-M3 上通常 StackType_t 是 32 位,也就是 4 字节。
所以:
128 个 StackType_t = 128 * 4 = 512 字节
14.3 TCB 里为什么有两个链表节点?¶
因为一个任务可能同时需要表达两件事:
- 它现在处于什么状态,比如 Ready、Blocked、Suspended。
- 它正在等哪个事件,比如队列、信号量、事件组。
所以:
| 节点 | 负责 |
|---|---|
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[任务函数运行]
最重要的理解点:
- TCB 是任务的管理档案,FreeRTOS 通过 TCB 管任务。
- 任务栈保存 CPU 运行现场,切换任务就是保存和恢复不同任务的栈现场。
- 任务链表保存任务状态,任务在哪里,基本就说明它是什么状态。
- Ready 链表按优先级分组,调度器总是优先找最高优先级的非空 Ready 链表。
pxCurrentTCB是当前任务指针,上下文切换最终就是换掉它,再恢复新任务的栈。