在 FreeRTOS 中,任务(Task)是系统调度和管理的基本单位。
上一篇文章我们介绍了:
- 什么是 FreeRTOS 任务
- Running、Ready、Blocked、Suspended 四种状态
- 调度器如何管理任务
那么新的问题来了:
一个任务到底是如何创建出来的?
在 FreeRTOS 中,创建任务主要有两种方式:
- 动态创建任务
- 静态创建任务
对应两个 API:
c
xTaskCreate()
和:
c
xTaskCreateStatic()
两者最终实现的目标相同:
创建一个可以被 FreeRTOS 调度执行的任务。
但是最大的区别:
任务所需要的内存是谁提供的。
一、FreeRTOS创建任务需要哪些资源?
在理解动态和静态创建之前,需要先了解:
一个任务并不是只有一段代码。
一个完整任务至少需要两个重要部分:
1. 任务控制块 TCB
Task Control Block
简称:
text
TCB
它保存任务的信息。
例如:
- 任务名称
- 任务状态
- 优先级
- 栈指针
- 调度信息
- 链表节点
可以理解为:
FreeRTOS 管理任务的身份证。
例如:
Task A
↓
TCB
↓
记录:
优先级
状态
栈地址
运行位置
2. 任务栈 Stack
每个任务都有自己的栈空间。
用于保存:
- 局部变量
- 函数调用信息
- CPU寄存器
- 上下文切换数据
例如:
Task A
代码
↓
Stack A
Task B
代码
↓
Stack B
为什么每个任务需要独立栈?
因为任务切换时:
系统需要保存当前任务状态。
例如:
Task A运行
↓
保存A当前状态
↓
切换Task B
↓
恢复B之前状态
没有独立栈,就无法恢复任务执行现场。
二、动态创建任务是什么?
动态创建任务:
就是:
FreeRTOS 自动从 Heap(堆内存)中申请 TCB 和 Stack,然后创建任务。
对应 API:
c
xTaskCreate()
流程:
应用程序
|
↓
调用 xTaskCreate()
|
↓
FreeRTOS Heap
|
├── 分配 TCB
|
└── 分配 Task Stack
|
↓
Task Ready
也就是说:
开发者只需要告诉系统:
- 任务函数
- 任务名称
- 栈大小
- 优先级
剩下的内存管理交给 FreeRTOS。
三、动态创建任务代码示例
例如创建 LED 任务:
c
void LED_Task(void *argument)
{
while(1)
{
LED_Toggle();
vTaskDelay(
pdMS_TO_TICKS(500)
);
}
}
创建:
c
TaskHandle_t LedTaskHandle;
xTaskCreate(
LED_Task,
"LED",
256,
NULL,
2,
&LedTaskHandle
);
参数含义:
第1个参数
任务入口函数:
c
LED_Task
表示:
任务从哪里开始执行。
第2个参数
任务名字:
c
"LED"
主要用于调试。
第3个参数
任务栈大小:
c
256
单位:
通常为:
text
Stack Depth
不同移植可能对应不同单位。
第4个参数
任务参数:
c
NULL
用于传递数据。
例如:
c
xTaskCreate(
Task,
"Task",
256,
&data,
2,
NULL
);
任务中:
c
void Task(void *argument)
{
MyData *p = argument;
}
第5个参数
优先级:
c
2
数字越大:
优先级越高。
第6个参数
任务句柄:
c
&LedTaskHandle
创建成功后:
可以通过句柄控制任务。
例如:
删除:
c
vTaskDelete(LedTaskHandle);
挂起:
c
vTaskSuspend(LedTaskHandle);
四、动态创建任务的优点
1. 使用简单
开发者不用管理内存。
只需要:
c
xTaskCreate()
即可。
2. 灵活
任务数量可以运行时变化。
例如:
网络连接:
连接成功
↓
创建通信任务
断开连接
↓
删除通信任务
这种场景非常适合动态创建。
3. 开发效率高
非常适合:
- 学习阶段
- 产品原型
- 功能快速验证
五、动态创建任务的问题
虽然方便,但是也存在风险。
1. 依赖 Heap
动态创建依赖:
text
FreeRTOS Heap
例如:
heap_1.c
heap_2.c
heap_4.c
heap_5.c
如果:
Heap空间不足
创建可能失败。
例如:
c
if(
xTaskCreate(...) != pdPASS
)
{
// 创建失败
}
2. 可能产生内存碎片
假设:
Heap:
████████████
不断:
创建任务
删除任务
可能:
██ ██ ███
虽然总空间够:
但是没有连续空间。
这就是:
内存碎片。
3. 可预测性较差
对于一些高可靠系统:
例如:
- 汽车电子
- 航空
- 医疗设备
- 工业控制
系统希望:
启动后内存固定。
而动态申请:
存在不确定性。
六、什么是静态创建任务?
静态创建任务:
就是:
任务需要的 TCB 和 Stack,由用户提前定义好,然后交给 FreeRTOS 使用。
对应 API:
c
xTaskCreateStatic()
流程:
动态:
FreeRTOS
↓
Heap申请
↓
TCB
Stack
静态:
用户
↓
提前定义
TCB
Stack
↓
xTaskCreateStatic()
↓
FreeRTOS使用
七、静态创建任务代码示例
首先定义:
TCB空间
c
StaticTask_t LED_TCB;
Stack空间
c
StackType_t LED_Stack[256];
然后:
c
TaskHandle_t LedTaskHandle;
LedTaskHandle =
xTaskCreateStatic(
LED_Task,
"LED",
256,
NULL,
2,
LED_Stack,
&LED_TCB
);
这里:
TCB:
c
LED_TCB
由用户提供。
Stack:
c
LED_Stack
由用户提供。
FreeRTOS:
不再申请 Heap。
八、静态创建任务的优点
1. 内存完全可控
系统启动以后:
内存布局固定。
例如:
RAM
↓
Task1 Stack
Task2 Stack
Task3 Stack
TCB
不会变化。
2. 不依赖 Heap
即使关闭:
动态内存:
c
configSUPPORT_DYNAMIC_ALLOCATION = 0
仍然可以运行。
3. 更适合高可靠系统
例如:
汽车:
启动
↓
分配固定资源
↓
长期运行
工业设备:
连续运行几年
更喜欢这种方式。
九、静态创建任务的问题
1. 编写复杂
需要自己定义:
c
StaticTask_t
StackType_t
例如:
c
static StaticTask_t TaskTCB;
static StackType_t TaskStack[512];
管理成本增加。
2. 需要提前规划内存
例如:
任务需要多少 Stack?
太小:
Stack Overflow
太大:
浪费 RAM。
3. 不适合任务数量动态变化
例如:
服务器连接:
用户1连接
创建任务
用户2连接
创建任务
这种场景:
动态创建更方便。
十、动态创建和静态创建对比
| 项目 | 动态创建 | 静态创建 |
|---|---|---|
| API | xTaskCreate | xTaskCreateStatic |
| 内存来源 | FreeRTOS Heap | 用户提供 |
| TCB | 系统分配 | 用户分配 |
| Stack | 系统分配 | 用户分配 |
| 灵活性 | 高 | 低 |
| 确定性 | 较低 | 高 |
| 是否依赖Heap | 是 | 否 |
| 内存碎片风险 | 有 | 无 |
| 代码复杂度 | 简单 | 复杂 |
| 适合场景 | 普通项目 | 高可靠项目 |
十一、实际项目如何选择?
选择动态创建:
适合:
- 学习 FreeRTOS
- STM32普通项目
- 产品快速开发
- 任务数量变化
例如:
智能家居
机器人
物联网设备
选择静态创建:
适合:
- 内存严格控制
- 高可靠系统
- 长时间运行设备
例如:
汽车ECU
工业控制器
医疗设备
通信设备
十二、工程中常见做法:混合使用
实际项目并不是二选一。
很多产品:
核心任务:
静态创建。
例如:
系统任务
通信任务
安全任务
使用:
c
xTaskCreateStatic()
临时任务:
动态创建。
例如:
升级任务
测试任务
日志任务
使用:
c
xTaskCreate()
十三、总结
FreeRTOS 创建任务,本质上就是:
创建两个东西:
TCB
+
Stack
动态创建:
FreeRTOS帮你申请内存
优点:
简单灵活
缺点:
依赖Heap
静态创建:
用户提前准备内存
优点:
稳定可预测
缺点:
需要提前规划
一句话总结:
动态创建任务解决"开发方便",静态创建任务解决"系统可靠"。
在学习阶段:
推荐:
c
xTaskCreate()
先理解任务机制。
在进入工业级项目后:
再根据系统要求选择:
c
xTaskCreateStatic()
