任务的创建和删除

API函数 描述
xTaskCreate 动态方式创建任务
xTaskCreateStatic 静态方式创建任务
xTaskDelete 删除任务

创建任务有两种创建方法,分别是动态创建任务(xTaskCreate)和静态创建任务(xTaskCreateStatic)

  • 动态创建任务:xTaskCreate创建任务时,FreeRTOS会申请两块内存:一块用于任务控制块TCB,一块用于任务栈
  • 静态创建任务:xTaskCreateStatic创建任务时,任务的任务控制块以及任务的栈空间所需的内存,需用户定义分配提供

特点:

  • 动态创建的特点是“方便”,优点是:只需要提供任务函数、名字、优先级、栈大小系统即可帮你分配内存,代码操作简单,扩展方便;缺点是:依赖堆内存管理,系统运行久了可能出现内存碎片、内存不足,排查问题也会更麻烦
  • 静态创建的特点是“可控”,优点是在编译期久准备好任务需要的TCB和栈,这样任务占多少RAM、在哪块内存里都是确定的;缺点是写起来麻烦,而且后面任务变多、栈大小要调整,维护成本高

    动态创建

  • start_task任务:这里是单独创建一个start_task启动任务来创建其他任务,最后启用调度器来决定当前应该跑哪个任务,这样是使得代码结构更加清晰,系统级初始化和任务创建裸机都放进启动任务里,而且有些初始化动作必须在调度器启动后做或者希望在RTOS环境下做,比如说还没介绍到的创建队列、信号量、时间组等等,创建完之后就删除自己,节省资源,启动任务只负责拉起系统,拉起后自己退出,这样不会长期占着CPU和任务栈
  • xTaskCreate参数介绍:

  • pxTaskCode:任务函数入口地址,一般与任务函数名字同名
  • pcName:任务名字,主要用于调试、查看任务状态、串口输出、调试器观察
  • uxStackDepth任务栈大小,单位是栈单元数(StackType_t通常为4字节),而不是字节数,最小栈深度在配置文件可以配置
    • 任务栈是占用芯片的SRAM,需要根据对应芯片的SRAM大小在配置文件中分配FreeRTOS堆大小
    • 要根据不同的任务来给定不同的栈大小,要根据局部变量多少和嵌递归函数的调用,比如大数组的局部变量uint8_t rxBuf[256],这个就占了256个字节,还有print、sprintf、snprintf也会明显增加栈占用
  • pvParameters:传给任务入口函数的参数,没有参数就为NULL
    • 如果要传入参数就是:
    • int led_id = 1;
      xTaskCreate(LedTask, "led_task", 128, &led_id, 2, NULL);
      
      void LedTask(void *pvParameters)
      {
          int led = *(int *)pvParameters;
          ...
      }
      
  • uxPriority:任务优先级,数值越大优先级越高,最大优先级也需要在配置文件设置
  • pxCreatedTask任务句柄输出参数,可以通过这个句柄去操作任务,后续还要挂起、恢复、删除、查询这个任务,就传&task_handle,比如删除任务vTaskDelete(task1_handle);
#include "FreeRTOS_demo.h"
#include "FreeRTOS.h"//这两个头文件是必须要引入的,FreeRTOS.h文件必须要放在前面
#include "task.h"

//启动任务的配置
#define START_TASK_STACK 128
#define START_TASK_PRIORITY 1
TaskHandle_t start_task_handle;
void start_task(void * pvParameters);//启动任务

//任务1的配置
#define TASK1_STACK 128
#define TASK1_PRIORITY 2
TaskHandle_t task1_handle;
void task1(void * pvParameters);//启动任务

//任务2的配置
#define TASK2_STACK 128
#define TASK2_PRIORITY 3
TaskHandle_t task2_handle;
void task2(void * pvParameters);//启动任务

//任务3的配置
#define TASK3_STACK 128
#define TASK3_PRIORITY 4
TaskHandle_t task3_handle;
void task3(void * pvParameters);//启动任务

void freertos_start(void)//启动FreeRTOS
{
    //1.创建一个启动任务
    xTaskCreate((TaskFunction_t) start_task,                //任务函数地址,前面是类型强转
                (char*) "start_task",                       //取任务名
                (configSTACK_DEPTH_TYPE) START_TASK_STACK,  //任务栈大小,最小是128字节
                (void *) NULL,                              //不需要形参就给一个NULL
                (UBaseType_t) START_TASK_PRIORITY,          //任务优先级,这里的优先级默认为5,有5个优先级
                (TaskHandle_t *) &start_task_handle);        //任务句柄
                //任务的相关信息会保存到一个tcb地址,然后这个地址会赋值给这个任务句柄,所以这只用创建一个任务句柄即可
    //2.启动调度器
    vTaskStartScheduler();//这是必须要启动的,会自动创建空闲函数
}
void start_task(void * pvParameters)//启动任务,用来创建其他task
{
    //进入临界区,临界区里面的代码不会被打断
    taskENTER_CRITICAL();
    
    //创建3个任务
    xTaskCreate((TaskFunction_t) task1,                
                (char*) "task1",                       
                (configSTACK_DEPTH_TYPE) TASK1_STACK,  
                (void *) NULL,                            
                (UBaseType_t) TASK1_PRIORITY,          
                (TaskHandle_t *) &task1_handle);
    xTaskCreate((TaskFunction_t) task2,                
                (char*) "task2",                       
                (configSTACK_DEPTH_TYPE) TASK2_STACK,  
                (void *) NULL,                            
                (UBaseType_t) TASK2_PRIORITY,          
                (TaskHandle_t *) &task2_handle);
    xTaskCreate((TaskFunction_t) task3,                
                (char*) "task3",                       
                (configSTACK_DEPTH_TYPE) TASK3_STACK,  
                (void *) NULL,                            
                (UBaseType_t) TASK3_PRIORITY,          
                (TaskHandle_t *) &task3_handle);
 
    //先退出临界区
    taskEXIT_CRITICAL();

    //启动任务只需要执行一次,用完就删除自己
    vTaskDelete(NULL);
    
}

void task1(void * pvParameters)
{
   while(1)
   {
      ...     
   }
}
void task2(void * pvParameters)
{
   while(1)
   {
      ...
   }
}
void task3(void * pvParameters)//任务3:判断按键是否按下,按下则删除task1
{
   
   while(1)
   {
      ...
   }
}

    静态创建

静态创建任务应该在配置文件定义一些东西来规范代码

开启软件定时器功能会额外创建一个定时器服务任务,这个任务本身也有栈和TCB,但是不是静态创建任务必须的,软件定时器适合实现定期、延时触发的任务,而不是一直循环跑的大任务

//开启静态分配
#define configSUPPORT_STATIC_ALLOCATION  1

//软件定时器相关的定义
#define configUSE_TIMERS_API             1                                //1:使能软件定时器
#define configTIMER_TASK_PRIORITY        ( configMAX_PRIORITIES - 1 )     //定义软件定时器任务的优先级
#define configTIMER_QUEUE_LENGTH         5                                //定义软件定时器任务队列长度                                  
#define configTIMER_TASK_STACK_DEPTH     ( configMINIMAL_STACK_SIZE * 2 ) //定义软件定时器任务堆栈空间大小

需要对每个任务都定义栈大小来分配内存

#include "FreeRTOS_demo.h"
#include "FreeRTOS.h"
#include "task.h"

//启动任务的配置
#define START_TASK_STACK 128
#define START_TASK_PRIORITY 1
TaskHandle_t start_task_handle;

StackType_t start_task_stack[START_TASK_STACK];//静态类型的任务栈,以数组形式存储
StaticTask_t start_task_TCB;//静态任务的TCB结构体类型

void start_task(void * pvParameters);//启动任务


//任务1的配置
#define TASK1_STACK 128
#define TASK1_PRIORITY 2
TaskHandle_t task1_handle;

StackType_t task1_stack[TASK1_STACK];
StaticTask_t task1_TCB;

void task1(void * pvParameters);//启动任务

//任务2的配置
#define TASK2_STACK 128
#define TASK2_PRIORITY 3
TaskHandle_t task2_handle;

StackType_t task2_stack[TASK2_STACK];
StaticTask_t task2_TCB;

void task2(void * pvParameters);//启动任务

//任务3的配置
#define TASK3_STACK 128
#define TASK3_PRIORITY 4
TaskHandle_t task3_handle;

StackType_t task3_stack[TASK3_STACK];
StaticTask_t task3_TCB;

void task3(void * pvParameters);//启动任务


/****************************************静态创建方式需要手动指定两个特殊任务的资源****************************************/
//空闲任务的配置
StackType_t idle_task_stack[configMINIMAL_STACK_SIZE];//[]是最小大小为128
StaticTask_t idle_task_TCB;
//软件定时器任务的配置
StackType_t timer_task_stack[configTIMER_TASK_STACK_DEPTH];//[]是最小大小为128
StaticTask_t timer_task_TCB;



//分配空闲任务的资源
void vApplicationGetIdleTaskMemory( StaticTask_t ** ppxIdleTaskTCBBuffer,
                                        StackType_t ** ppxIdleTaskStackBuffer,
                                        uint32_t * pulIdleTaskStackSize )
{
    *ppxIdleTaskTCBBuffer = &idle_task_TCB;
    *ppxIdleTaskStackBuffer = idle_task_stack;
    *pulIdleTaskStackSize = configMINIMAL_STACK_SIZE;
}


//分配软件定时器任务的资源
void vApplicationGetTimerTaskMemory( StaticTask_t ** ppxTimerTaskTCBBuffer,
                                         StackType_t ** ppxTimerTaskStackBuffer,
                                         uint32_t * pulTimerTaskStackSize )
{
    *ppxTimerTaskTCBBuffer = &timer_task_TCB;
    *ppxTimerTaskStackBuffer = timer_task_stack;
    *pulTimerTaskStackSize = configTIMER_TASK_STACK_DEPTH;
}
void freertos_start(void)//启动FreeRTOS
{
    //创建启动任务(静态)
    start_task_handle = xTaskCreateStatic(
                    (TaskFunction_t) start_task,              //任务函数地址,前面是类型强转
                    (char*) "start_task",                     //取任务名
                    (uint32_t) START_TASK_STACK,              //任务栈大小,最小是128字节,静态的任务栈大小为uin32_t类型
                    (void *) NULL,                            //不需要形参就给一个NULL
                    (UBaseType_t) START_TASK_PRIORITY,        //任务优先级,这里的优先级默认为5,有5个优先级
                    (StackType_t*) start_task_stack,          //任务栈的地址,以数组的形式存储
                    (StaticTask_t*) &start_task_TCB           //tcb的地址
    );

    //2.启动调度器
    vTaskStartScheduler();//这是必须要启动的,会自动创建空闲函数和软件定时器,静态创建的方式需要去实现2个分配资源的接口函数
}
void start_task(void * pvParameters)//启动任务,用来创建其他task
{
    //进入临界区,临界区里面的代码不会被打断
    taskENTER_CRITICAL();
    
    //静态创建3个任务
    task1_handle = xTaskCreateStatic(
                    (TaskFunction_t) task1,                
                    (char*) "task1",                       
                    (configSTACK_DEPTH_TYPE) TASK1_STACK,  
                    (void *) NULL,                            
                    (UBaseType_t) TASK1_PRIORITY,        
                    (StackType_t*) task1_stack,         
                    (StaticTask_t*) &task1_TCB           
    );
    task2_handle = xTaskCreateStatic(
                    (TaskFunction_t) task2,                
                    (char*) "task2",                       
                    (configSTACK_DEPTH_TYPE) TASK2_STACK,  
                    (void *) NULL,                            
                    (UBaseType_t) TASK2_PRIORITY,        
                    (StackType_t*) task2_stack,         
                    (StaticTask_t*) &task2_TCB           
    );
    task3_handle = xTaskCreateStatic(
                    (TaskFunction_t) task3,                
                    (char*) "task3",                       
                    (configSTACK_DEPTH_TYPE) TASK3_STACK,  
                    (void *) NULL,                            
                    (UBaseType_t) TASK3_PRIORITY,        
                    (StackType_t*) task3_stack,         
                    (StaticTask_t*) &task3_TCB           
    );
    //退出临界区
    taskEXIT_CRITICAL();

    //启动任务只需要执行一次,用完就删除自己
    vTaskDelete(NULL);   
}

void task1(void * pvParameters)
{
   while(1)
   {
      ...     
   }
}
void task2(void * pvParameters)
{
   while(1)
   {
      ...
   }
}
void task3(void * pvParameters)//任务3:判断按键是否按下,按下则删除task1
{
   
   while(1)
   {
      ...
   }
}

    任务的删除

    API函数 描述
    vTaskDelete 删除任务
    void vTaskDelete( TaskHandle_t xTaskToDelete )

    • 参数说明:xTaskToDelete待删除任务的任务句柄,当传入的参数为NULL,则代表删除任务自身(当前正在运行的任务)
    • 该函数用于删除已被创建的任务,被删除的任务将从就绪态任务列表、阻塞态任务列表、挂起态任务列表和事件列表中移除,但此时任务不是真正被删除结束了,要在空闲任务中释放空间后才完整删除
    • 需要注意的是,空闲任务会负责释放被删除任务中由系统分配的内存,但是如果是静态创建任务由用户在任务删除前自行申请的内存,则需要由用户在任务被删除前提前释放(就是比如申请内存malloc,要在删除前手动释放内存),否则将导致内存泄露

    任务的挂起和恢复

    API函数 描述

    vTaskSuspend()

    挂起任务,相当于暂停,可以恢复

    vTaskResume()

    恢复被挂起的任务

    xTaskResumeFromISR()

    在中断服务函数中恢复被挂起的任务

    vTaskSuspendAll()

    挂起任务调度器,调度器不会进行任务切换

    xTaskResumeAll()

    恢复任务调度器,调度器继续任务切换

    void vTaskSuspend(TaskHandle_t xTaskToSuspend)
    void vTaskResume(TaskHandle_t xTaskToResume)
    BaseType_t xTaskResumeFromISR(TaskHandle_t xTaskToResume)
    • xTaskToSuspend:待挂起任务的任务句柄,为NULL表示挂起任务自身,使用任务挂起和恢复函数需要将宏INCLUDE_vTaskSuspend配置为 1

    • xTaskToResume:不论任务被挂起函数挂起多少次只需要调用一次任务恢复函数就可以让其继续执行,被恢复的任务会重新进入就绪状态

    • vTaskSuspendAll():挂起任务调度器,调度器不会进行任务切换,当前任务一直运行,就算正在运行的任务里有延时会阻塞但是也不会进行任务切换

    • xTaskResumeAll():恢复任务调度器,调度器继续任务切换

    查询任务状态

    #define configUSE_TRACE_FACILITY 1
    #define configUSE_STATS_FORMATTING_FUNCTIONS 1
    void vTaskList( char * pcWriteBuffer )//上面的宏要打开才能调用
    • vTaskList:定义一个缓冲区字符数组,然后在想要查询的任务中调用该函数传入该数组即可查询该任务任务的各种信息

    名称                         状态       优先级       堆栈使用  任务编号

    • 'X'(运行)、 'B'(阻塞)、'R'(就绪)、'S'(暂停)、'D'(删除)(这只是被标记为删除但还没有真正释放资源)

    ESP32的任务创建和删除

        任务创建和删除

    由于ESP32是双核驱动,所以在ESP-IDF中为了支持双核 SMP,API 和内核行为都做了调整,也有xTaskCreate()、xTaskCreateStatic()动态创建和静态创建,还有新增了

    • xTaskCreatePinnedToCore()
    • xTaskCreateStaticPinnedToCore()

    这个也是动态创建和静态创建但是可以选择这个任务与哪个核绑定在哪个核上运行,新增的一个参数,0则是固定在CPU0,1则是固定在CPU1,tskNO_AFFINITY则不固定,可由两个核调度,前面两种创建本质上会转到对应的PinnedToCore版本并使用tskNO_AFFINITY,允许调度器决定他在哪个核上跑,还有就是ESP-IDF的栈单位是字节,

    ESP32FreeRTOS的删除任务也与标准FreeRTOS删除任务类似,但是在双核运行下可能情况会相对复杂,所以不建议跨核删除正在运行的任务

    • 如果是删除另一个和上正在运行的任务,内存总是由那个核的idle task来释放
    • 如果删除是当前正在另一个核上运行的任务,会触发另一个核的yield(当前任务主动放弃CPU让调度器去运行其他就绪任务 )然后由某一个idle task来清理
    • 如果被删任务当前不在任何核上运行(运行是指处于运行态、就绪态、阻塞态、挂起态之一),或者某些条件满足时,释放可能立即完成

        任务挂起和恢复

    在ESP32中vTaskSuspendAll() 只会暂停该核上的调度,另一个核仍可以继续跑任务,无法跨多个核同时真正挂起整个调度器,两个核之间的调度器是单独分开的

    Logo

    openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

    更多推荐