FreeRTOS任务管理详解中: FreeRTOS任务创建与删除实战教程(动态方法)
📖 前言
大家好!今天给大家带来一篇FreeRTOS的入门实战教程,主要讲解如何使用动态方法创建和删除任务。这篇文章适合刚接触RTOS的小伙伴,我会尽量用通俗易懂的语言来解释每个概念。
什么是动态创建任务?
简单来说,动态创建任务就是在程序运行时,从系统的"内存池"(堆内存)中申请一块空间来创建任务。这就像你去餐厅点餐,厨房现做现卖,用多少拿多少。
💡 小贴士 :与之对应的是静态创建任务,需要我们提前定义好任务所需的内存空间,就像提前准备好食材一样。
关键概念解释
🔹 任务栈(Task Stack)
每个任务都有自己的"工作台",用来存放局部变量、函数调用信息等。栈的大小单位是字(Word),在32位MCU中,1个字=4字节。
比如定义 TASK_STACK_SIZE 128,实际占用内存是 128 × 4 = 512字节。
🔹 任务优先级(Priority)
数字越大,优先级越高。高优先级的任务会优先执行。比如优先级4的task3会比优先级2的task1先运行。
🔹 任务句柄(Task Handle)
可以理解为任务的"身份证号",通过它我们可以对任务进行操作(比如删除、挂起等)。
🔹 临界区(Critical Section)
被保护的代码段,在这段代码执行时不会被打断。就像上厕所要锁门一样,避免被打扰。
🎯 实验目标
本次实验我们要实现以下功能:
- start_task(启动任务):负责创建其他三个任务,创建完成后自动删除自己
- task1:让LED1每500ms闪烁一次
- task2:让LED2每500ms闪烁一次
- task3:检测按键KEY1,按下后删除task1
🛠️ 实现步骤
步骤1:配置使能宏
打开 FreeRTOSConfig.h 文件,确保以下宏已开启(动态方法默认已开启):
c
#define configSUPPORT_DYNAMIC_ALLOCATION 1 // 使能动态内存分配
步骤2:创建文件
新建 freertos_demo.c 和 freertos_demo.h 文件,用于存放FreeRTOS任务相关代码。

步骤3:编写任务代码
📄 freertos_demo.h
c
#ifndef __FREERTOS_DEMO_H__
#define __FREERTOS_DEMO_H__
// 启动FreeRTOS的入口函数
void freertos_start(void);
#endif
📄 freertos_demo.c
c
#include "freertos_demo.h"
/* FreeRTOS核心头文件 */
#include "FreeRTOS.h"
#include "task.h"
/* 硬件驱动头文件 */
#include "led_app.h"
#include "uart_app.h"
#include "key_app.h"
/* ========== 启动任务配置 ========== */
#define START_TASK_STACK_SIZE 1024 // 栈大小:1024字 = 4096字节
#define START_TASK_PRIORITY 1 // 优先级:1(最低)
TaskHandle_t start_task_handle; // 任务句柄
void start_task(void *pvParameters); // 任务函数声明
/* ========== 任务1配置 ========== */
#define TASK1_STACK_SIZE 1024
#define TASK1_PRIORITY 2
TaskHandle_t task1_handle;
void task1(void *pvParameters);
/* ========== 任务2配置 ========== */
#define TASK2_STACK_SIZE 1024
#define TASK2_PRIORITY 3
TaskHandle_t task2_handle;
void task2(void *pvParameters);
/* ========== 任务3配置 ========== */
#define TASK3_STACK_SIZE 1024
#define TASK3_PRIORITY 4 // 最高优先级
TaskHandle_t task3_handle;
void task3(void *pvParameters);
/**
* @brief 启动FreeRTOS系统
* @note 这个函数在main函数中调用
*/
void freertos_start(void)
{
/* 1. 创建启动任务 */
xTaskCreate(
(TaskFunction_t)start_task, // 任务函数指针
(char*)"start_task", // 任务名称(用于调试)
(configSTACK_DEPTH_TYPE)START_TASK_STACK_SIZE, // 栈大小
(void*)NULL, // 传递给任务的参数(这里不需要)
(UBaseType_t)START_TASK_PRIORITY, // 任务优先级
(TaskHandle_t*)&start_task_handle // 任务句柄(用于后续操作)
);
/* 2. 启动任务调度器(调度器启动后,代码不会返回) */
vTaskStartScheduler();
}
/**
* @brief 启动任务:专门用来创建其他任务
* @param pvParameters 任务参数(未使用)
* @note 创建完其他任务后,这个任务就完成使命了,会删除自己
*/
void start_task(void *pvParameters)
{
/* 进入临界区:防止任务创建过程被打断 */
taskENTER_CRITICAL();
/* 创建任务1 */
xTaskCreate(
(TaskFunction_t)task1,
(char *)"task1",
(configSTACK_DEPTH_TYPE)TASK1_STACK_SIZE,
(void *)NULL,
(UBaseType_t)TASK1_PRIORITY,
(TaskHandle_t *)&task1_handle
);
/* 创建任务2 */
xTaskCreate(
(TaskFunction_t)task2,
(char *)"task2",
(configSTACK_DEPTH_TYPE)TASK2_STACK_SIZE,
(void *)NULL,
(UBaseType_t)TASK2_PRIORITY,
(TaskHandle_t *)&task2_handle
);
/* 创建任务3 */
xTaskCreate(
(TaskFunction_t)task3,
(char *)"task3",
(configSTACK_DEPTH_TYPE)TASK3_STACK_SIZE,
(void *)NULL,
(UBaseType_t)TASK3_PRIORITY,
(TaskHandle_t *)&task3_handle
);
/* 删除启动任务自己(传NULL表示删除当前任务) */
vTaskDelete(NULL);
/* 退出临界区 */
taskEXIT_CRITICAL();
}
/**
* @brief 任务1:控制LED1每500ms闪烁
* @param pvParameters 任务参数
*/
void task1(void *pvParameters)
{
while (1) // 任务必须是死循环
{
Uart_Printf(DEBUG_UART, "task1 is running...\r\n");
led_buf[0] ^= 1; // LED1状态翻转(0变1,1变0)
Led_Task(); // 更新LED显示
vTaskDelay(500); // 延时500个时钟节拍(通常500ms)
// 注意:这里会让出CPU,让其他任务运行
}
}
/**
* @brief 任务2:控制LED2每500ms闪烁
* @param pvParameters 任务参数
*/
void task2(void *pvParameters)
{
while (1)
{
Uart_Printf(DEBUG_UART, "task2 is running...\r\n");
led_buf[1] ^= 1; // LED2状态翻转
Led_Task();
vTaskDelay(500);
}
}
/* 按键检测相关变量 */
uint8_t key_val = 0; // 当前按键状态
uint8_t key_old = 0; // 上一次按键状态
uint8_t key_down = 0; // 按下沿检测
uint8_t key_up = 0; // 释放沿检测
/**
* @brief 任务3:检测按键,按下KEY1删除task1
* @param pvParameters 任务参数
*/
void task3(void *pvParameters)
{
while (1)
{
Uart_Printf(DEBUG_UART, "task3 is running...\r\n");
/* 读取按键状态 */
key_val = key_read();
/* 边沿检测:只有按键状态变化时才触发 */
key_down = key_val & (key_old ^ key_val); // 检测按下沿
key_up = ~key_val & (key_old ^ key_val); // 检测释放沿
key_old = key_val; // 更新历史状态
/* 处理按键事件 */
switch (key_down)
{
case 1: // KEY1被按下
/* 防止重复删除(句柄不为空才删除) */
if (task1_handle != NULL)
{
Uart_Printf(DEBUG_UART, "执行删除Task1\r\n");
vTaskDelete(task1_handle); // 删除任务1
task1_handle = NULL; // ⚠️ 重要:删除后手动置空句柄
}
break;
}
vTaskDelay(20); // 延时20ms,降低CPU占用率
// 注意:不要用HAL_Delay()!它不会让任务进入阻塞态,会一直占用CPU
}
}
步骤4:修改main.c
c
#include "main.h"
#include "freertos_demo.h" // 包含FreeRTOS任务头文件
int main(void)
{
/* 硬件初始化 */
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_DMA_Init();
MX_USART1_UART_Init();
/* 系统初始化 */
System_Init();
/* 启动FreeRTOS(调度器启动后不会返回) */
freertos_start();
/* ⚠️ 注意:下面的代码永远不会执行 */
while (1)
{
// 这里的代码不会被执行
}
}
🎬 实验结果
程序运行后,串口输出如下:

- task1和task2每500ms执行一次(LED闪烁)
- task3每20ms执行一次(检测按键)
- 按下KEY1后,task1被删除,LED1停止闪烁
⚠️ 重要注意事项
问题1:任务栈溢出导致系统卡死
现象:task2运行一次后系统就卡死,进入HardFault(硬故障)。
原因分析:
- 任务栈大小设置为128字(512字节)
Uart_Printf函数内部使用了512字节的缓冲区- 栈空间不够,导致栈溢出
解决方法:
c
// 修改前
#define TASK1_STACK_SIZE 128 // ❌ 太小了
// 修改后
#define TASK1_STACK_SIZE 1024 // ✅ 增大到1024字(4096字节)
如何确定合适的栈大小?
FreeRTOS提供了一个非常有用的函数:
c
UBaseType_t uxTaskGetStackHighWaterMark(TaskHandle_t xTask);
返回值:任务栈的历史最小剩余空间(单位:字)
使用示例:
c
// 在任务中添加调试代码
void task1(void *pvParameters)
{
while (1)
{
// 执行任务...
// 查看栈使用情况
UBaseType_t stack_left = uxTaskGetStackHighWaterMark(NULL);
printf("Task1 栈剩余: %d 字\r\n", stack_left);
vTaskDelay(500);
}
}
💡 经验值:保留至少20%的栈余量,以应对意外情况。
问题2:为什么删除任务后要手动置空句柄?
c
vTaskDelete(task1_handle); // 删除任务
task1_handle = NULL; // ⚠️ 必须手动置空
原因 :vTaskDelete() 只是释放了任务占用的内存,但不会自动将句柄变量置为NULL。如果不手动置空,可能会出现:
- 重复删除已删除的任务(野指针)
- 条件判断失效
问题3:vTaskDelay() vs HAL_Delay()
| 函数 | 是否让出CPU | 其他任务能否运行 | 推荐使用场景 |
|---|---|---|---|
vTaskDelay() |
✅ 是 | ✅ 能 | FreeRTOS任务中 |
HAL_Delay() |
❌ 否 | ❌ 不能 | 裸机程序或中断中 |
结论 :在FreeRTOS任务中,务必使用 vTaskDelay(),否则会导致低优先级任务饿死。
📚 知识拓展
1. 任务的生命周期
c
创建 → 就绪 → 运行 → 阻塞/挂起 → 删除
- 就绪态:等待CPU资源
- 运行态:正在占用CPU
- 阻塞态:等待事件(如延时、信号量)
- 挂起态:被暂停执行
2. 空闲任务(Idle Task)
调用 vTaskStartScheduler() 后,系统会自动创建一个优先级为0的空闲任务。它的作用是:
- 回收被删除任务的内存
- 系统空闲时运行(其他任务都阻塞时)
3. 任务调度策略
FreeRTOS默认使用抢占式调度:
- 高优先级任务可以打断低优先级任务
- 相同优先级任务采用时间片轮转
🎓 总结
本文通过一个简单的LED+按键实验,带大家入门了FreeRTOS的动态任务创建和删除。核心要点:
- ✅ 使用
xTaskCreate()动态创建任务 - ✅ 使用
vTaskDelete()删除任务,删除后记得置空句柄 - ✅ 任务栈大小要合理设置,避免溢出
- ✅ 任务中使用
vTaskDelay()而非HAL_Delay() - ✅ 利用
uxTaskGetStackHighWaterMark()优化栈大小
希望这篇文章能帮助到刚接触RTOS的你!有任何问题欢迎在评论区留言交流~
📌 下期预告:FreeRTOS静态创建任务的使用
觉得有用的话,别忘了点赞👍 + 收藏⭐ + 关注哦!