一、任务创建与删除(核心生命周期管理)
这是使用 FreeRTOS 的起点,文档介绍了两种创建方式和一种删除方式。
1. 动态创建:xTaskCreate() ------ 最常用
- 内存来源 :任务控制块(TCB)和任务堆栈所需内存自动 从 FreeRTOS 堆(
ucHeap)中分配。 - 前提条件 :
- 必须提供内存管理文件(如
heap_4.c)。 - 宏
configSUPPORT_DYNAMIC_ALLOCATION必须为1(默认就是 1)。
- 必须提供内存管理文件(如
- 关键参数注意 :
usStackDepth:堆栈大小,单位是字(word),不是字节。在 32 位 MCU(如 STM32)上,实际字节数要乘以 4。pxCreatedTask:用于返回任务句柄,后续挂起、删除、改变优先级等操作都需要它。
- 返回值 :成功返回
pdPASS,失败返回errCOULD_NOT_ALLOCATE_REQUESTED_MEMORY(通常是堆空间不足)。
2. 静态创建:xTaskCreateStatic() ------ 更可控,但稍复杂
- 内存来源 :任务堆栈和控制块的内存完全由用户提供(通常定义为全局数组)。
- 使用前提 :宏
configSUPPORT_STATIC_ALLOCATION必须设为1。 - 额外要求 :当启用静态分配后,用户必须实现两个钩子函数 ,为空闲任务和定时器服务任务提供静态内存:
vApplicationGetIdleTaskMemory()vApplicationGetTimerTaskMemory()
- 适用场景:需要将任务内存放到特定区域(如内部 SRAM、外部 SDRAM),或者对动态分配的不确定性(碎片、失败)有顾虑时使用。在资源极其紧张或安全关键的系统中,静态方法更受青睐。
3. 受限任务创建:xTaskCreateRestricted() ------ 用于带 MPU 的芯片
- 仅在 MCU 带有**内存保护单元(MPU)**时使用,可以为任务设置内存访问权限,提高系统安全性(如隔离用户任务和系统任务)。
4. 删除任务:vTaskDelete()
- 参数为待删除任务的句柄。传入
NULL表示删除任务自身。 - 关键注意事项(高频踩坑点) :
- 若任务是动态创建的,删除后其占用的 TCB 和堆栈内存会在空闲任务中释放 。因此,删除任务后必须给空闲任务运行机会 (比如调用
vTaskDelay(1)或taskYIELD()),否则内存无法被回收,造成泄漏。 - 用户自行分配的内存,必须自行释放 。如果任务内部用
pvPortMalloc()申请了内存,删除前必须调用vPortFree(),否则会导致内存泄漏。
- 若任务是动态创建的,删除后其占用的 TCB 和堆栈内存会在空闲任务中释放 。因此,删除任务后必须给空闲任务运行机会 (比如调用
二、任务挂起与恢复(暂停/继续,不丢数据)
当你需要暂停某个任务,并在一段时间后从暂停点继续执行时,使用挂起和恢复机制,而非删除+重建(后者会丢失任务内部的变量状态)。
1. 挂起:vTaskSuspend()
- 将任务置于挂起态(Suspended),该任务将永远不会被调度,直到被恢复。
- 参数为任务句柄,传入
NULL表示挂起自身。
2. 恢复(任务级):vTaskResume()
- 在任务上下文中恢复一个被挂起的任务。
- 只能恢复通过
vTaskSuspend()挂起的任务。
3. 恢复(中断级):xTaskResumeFromISR()
- 在**中断服务函数(ISR)**中恢复任务。
- 特殊返回值 :
- 返回
pdTRUE:表示被恢复的任务优先级等于或高于 当前被打断的任务,这意味着退出 ISR 后必须进行上下文切换 (通常调用portYIELD_FROM_ISR())。 - 返回
pdFALSE:不需要立即切换。
- 返回
- 与
vTaskResume()的区别:此函数专用于中断环境,且会返回是否需要切换的指示,这是中断安全的必要设计。
三、实验例程深度解析
通过两个完整实验,清晰地展示了上述 API 的标准用法模式。
1. 标准工程结构模式(推荐学习)
- 步骤 1 :在
main()中完成硬件初始化。 - 步骤 2 :在调度器启动前 ,使用
xTaskCreate()创建一个"起始任务"(start_task)。 - 步骤 3 :调用
vTaskStartScheduler()启动调度器。 - 步骤 4 :在
start_task的任务函数中,创建所有其他应用任务、队列、信号量等内核对象。 - 步骤 5 :创建完成后,调用
vTaskDelete(NULL)删除start_task自身,释放其占用的内存。
这种模式的好处是:将系统初始化与业务任务分离,结构清晰,且所有资源创建都在一个任务上下文中完成,便于管理。
2. 动态创建实验(4.2 节)
- 核心逻辑 :
- 任务 2 的优先级(3)高于任务 1(2),因此一开始任务 2 会优先执行。
- 任务 1 每运行一次,计数加 1。当运行满 5 次后,调用
vTaskDelete(Task2Task_Handler)删除任务 2。 - 删除后,任务 2 不再运行,LED1 停止闪烁,串口也不再打印任务 2 的信息。
- 关键观察点 :删除任务后,空闲任务会回收任务 2 的内存(前提是给了空闲任务运行时间,例程中通过
vTaskDelay让出了 CPU)。
3. 静态创建实验(4.3 节)
- 与动态实验功能完全相同,但所有任务的堆栈(
StackType_t Task1TaskStack[128])和控制块(StaticTask_t Task1TaskTCB)都由用户显式定义。 - 需注意 :必须实现
vApplicationGetIdleTaskMemory和vApplicationGetTimerTaskMemory,否则编译会报链接错误(文档图 4.3.1 所示)。
4. 挂起/恢复实验(4.5 节)
-
任务设计:
key_task:轮询按键状态。task1_task和task2_task:执行周期任务并显示运行次数。
-
按键功能:
KEY2:挂起任务 2(vTaskSuspend(Task2Task_Handler))。KEY1:恢复任务 1(vTaskResume(Task1Task_Handler))。WKUP:挂起任务 1。KEY0(中断方式):在外部中断 ISR 中调用xTaskResumeFromISR()恢复任务 2。
-
中断中的关键代码模式:
cBaseType_t xHigherPriorityTaskWoken = pdFALSE; xHigherPriorityTaskWoken = xTaskResumeFromISR(Task2Task_Handler); portYIELD_FROM_ISR(xHigherPriorityTaskWoken); // 根据返回值决定是否切换这是所有在 ISR 中使用 FreeRTOS API 的标准范式,务必牢记。
-
实验结论 :挂起任务后,其运行计数值保持不变;恢复后继续累加,证明了挂起不会丢失任务内部状态。
四、综合对比与选择建议
| 功能 | API 函数 | 适用场景 | 注意事项 |
|---|---|---|---|
| 动态创建 | xTaskCreate() |
绝大多数常规应用,简单快捷 | 需确保堆空间足够;内存释放依赖空闲任务 |
| 静态创建 | xTaskCreateStatic() |
内存敏感、安全关键、需固定内存地址的场景 | 需用户提供堆栈和TCB;需实现两个系统钩子函数 |
| 删除任务 | vTaskDelete() |
任务不再需要时,回收资源 | 动态任务的内存由空闲任务回收;用户内存需手动释放 |
| 挂起任务 | vTaskSuspend() |
临时停止任务,保留上下文和数据 | 挂起后任务不再被调度,直到被恢复 |
| 恢复(任务级) | vTaskResume() |
在任务代码中恢复另一个任务 | 仅用于任务上下文 |
| 恢复(中断级) | xTaskResumeFromISR() |
在ISR中恢复任务,实现中断触发任务运行 | 必须检查返回值并决定是否切换上下文 |
五、总结与最佳实践
- 任务创建模式:强烈推荐"起始任务 + 动态创建"的模式,兼顾了代码清晰度和灵活性。除非有特殊内存要求,否则无需使用静态创建。
- 内存管理 :务必理解动态创建的任务删除后,内存不会立即释放,而是交给空闲任务。因此,频繁创建删除任务的系统,要保证空闲任务有足够机会运行,并注意堆碎片问题(使用
heap_4.c可缓解)。 - 中断安全 :任何时候在 ISR 中调用 FreeRTOS API(如
xTaskResumeFromISR、xQueueSendFromISR),都要遵循"检查返回值 → 调用portYIELD_FROM_ISR"的模式,确保调度器能及时响应高优先级任务。 - 调试手段 :实验代码中大量使用
printf和 LCD 显示,这是学习 RTOS 行为的有力工具。在实际产品中,可用更轻量的日志方式代替。
掌握这些任务 API,就掌握了 FreeRTOS 任务管理的核心。后续的信号量、队列、事件组等,都是在此基础上构建的任务间通信与同步机制。
如果你对某个特定点(比如中断中的上下文切换细节,或 heap_4.c 的内存管理算法)还想深入了解,可以随时提出。