FreeRTOS任务管理详解中: FreeRTOS任务创建与删除实战教程(动态方法)

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)

被保护的代码段,在这段代码执行时不会被打断。就像上厕所要锁门一样,避免被打扰。


🎯 实验目标

本次实验我们要实现以下功能:

  1. start_task(启动任务):负责创建其他三个任务,创建完成后自动删除自己
  2. task1:让LED1每500ms闪烁一次
  3. task2:让LED2每500ms闪烁一次
  4. task3:检测按键KEY1,按下后删除task1

🛠️ 实现步骤

步骤1:配置使能宏

打开 FreeRTOSConfig.h 文件,确保以下宏已开启(动态方法默认已开启):

c 复制代码
#define configSUPPORT_DYNAMIC_ALLOCATION  1  // 使能动态内存分配

步骤2:创建文件

新建 freertos_demo.cfreertos_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的动态任务创建和删除。核心要点:

  1. ✅ 使用 xTaskCreate() 动态创建任务
  2. ✅ 使用 vTaskDelete() 删除任务,删除后记得置空句柄
  3. ✅ 任务栈大小要合理设置,避免溢出
  4. ✅ 任务中使用 vTaskDelay() 而非 HAL_Delay()
  5. ✅ 利用 uxTaskGetStackHighWaterMark() 优化栈大小

希望这篇文章能帮助到刚接触RTOS的你!有任何问题欢迎在评论区留言交流~


📌 下期预告:FreeRTOS静态创建任务的使用

觉得有用的话,别忘了点赞👍 + 收藏⭐ + 关注哦!

相关推荐
瑶光守护者2 小时前
【学习笔记】5G RedCap:智能回落5G NR驻留的接入策略
笔记·学习·5g
你想知道什么?2 小时前
Python基础篇(上) 学习笔记
笔记·python·学习
曼巴UE52 小时前
UE5 C++ 动态多播
java·开发语言
SystickInt3 小时前
32 DMA实现ROM与RAM通信
stm32·单片机·嵌入式硬件
steins_甲乙3 小时前
C++并发编程(3)——资源竞争下的安全栈
开发语言·c++·安全
xian_wwq3 小时前
【学习笔记】可信数据空间的工程实现
笔记·学习
请一直在路上3 小时前
python文件打包成exe(虚拟环境打包,减少体积)
开发语言·python
浩瀚地学3 小时前
【Arcpy】入门学习笔记(五)-矢量数据
经验分享·笔记·python·arcgis·arcpy
luguocaoyuan3 小时前
JavaScript性能优化实战技术学习大纲
开发语言·javascript·性能优化
禁默3 小时前
“零消耗”调用优质模型:AI Ping结合Cline助我快速开发SVG工具,性能与官网无异
开发语言·php