FreeRTOS系列教程(二):如何进行任务管理?

目录

一. 前言

二. 任务管理原理

2.1 任务调度机制

2.2 任务状态

2.2.1 任务状态的定义

2.2.2 任务状态的迁移

三. 任务管理API函数

3.1 任务创建函数:xTaskCreate

3.2 任务删除函数:vTaskDelete

3.3 任务相对延时函数:vTaskDelay

3.4 任务绝对延时函数:vTaskDelayUntil

3.5 任务挂起函数:vTaskSuspend

3.6 任务恢复函数:vTaskResume

3.7 其他任务管理API函数

四. 任务管理相关实验

4.1 任务创建与删除实验

4.2 任务挂起与恢复实验

五. 小结


一. 前言

大家好,我是旭辉君,一个致力于智能硬件干货分享的技术博主。

在上篇文章中,我们理解了FreeRTOS内核的一些关键概念,链接如下:

FreeRTOS系列教程(一):内核关键概念-CSDN博客文章浏览阅读712次,点赞7次,收藏17次。本文介绍了FreeRTOS内核的一些关键概念,包括:链表与节点,任务,堆与栈,临界段,阻塞延时,优先级,时间片等。这些概念的理解有助于我们后续在自己的项目中使用FreeRTOS的时候,能更得心应手。这些概念在初次理解的时候可能有些晦涩难懂,我尽量用一些形象的语言给大家展示出来。https://blog.csdn.net/weixin_42434952/article/details/137438517?spm=1001.2014.3001.5501接下来本文将带大家一起进入如何把FreeRTOS应用好的篇章。从系统的角度看,任务是竞争系统资源的最小运行单元。 FreeRTOS 是一个支持多任务的操作系统,每个任务在自己的环境中运行。在任何时刻,只有一个任务得到运行, FreeRTOS 调度器决定运行哪个任务。

本文将重点介绍FreeRTOS中任务管理的知识,看完本文,我们将会知道:

  • 如何创建一个或多个任务?
  • 任务都有哪些状态?
  • FreeRTOS 如何选择在任何给定时间执行哪个任务?
  • 任务的优先级对系统运行有什么影响?

带着这些问题,我们进入FreeRTOS的任务之旅。

二. 任务管理原理

2.1 任务调度机制

得益于FreeRTOS操作系统的任务调度机制的优势,调度器可以在每次系统滴答中断后进行任务调度,然后根据任务的优先级和状态,决定下一个要执行的任务。当一个任务正在执行时,如果有更高优先级的任务变为就绪状态,调度器将保存当前任务的状态,然后加载并执行新的任务。当新的任务完成或被阻塞时,调度器再次保存其状态,然后加载并恢复之前的任务。通过这种方式,虽然单片机在任何时刻都只在执行一个任务,但由于任务切换的速度非常快,它可以看起来像是在同时执行多个任务。为了能够选择要运行的下一个任务,调度程序本身必须在每个时间片,也就是滴答中断的末尾执行,滴答中断频率由应用程序在 FreeRTOSConfig.h 中定义的 configTICK_RATE_HZ 配置。例如,如果 configTICK_RATE_HZ 设置为 100(Hz),则时间片将为 10 毫秒。

下图显示了调度程序是如何执行的:在Task1时间片的末尾进入滴答中断,然后从滴答中断返回到另一个任务。

下图更具体的演示了使用“基于时间片的固定优先级抢占式调度”算法抢占调度任务的调度过程,大部分FreeRTOS操作系统都使用这种任务调度策略。

这里面,task1是最高优先级的任务,task2是中等优先级的任务,task3是低优先级的任务,Idle task是空闲任务,空闲任务一般具有最低优先级。

系统启动后,task1的时间还没到,运行task2,等task2进入阻塞态后运行空闲任务,当task3的事件到达就会抢占空闲任务,task3运行期间,如果task2的周期到了,因为task2优先级高就会抢占task3,task2运行期间,由于task1优先级高,一旦task1等待的事件到了就会抢占task2,如此往复。

不同于单片机的中断的优先级,在FreeRTOS中,数字越大,优先级越高。可用优先级的最大数量在 FreeRTOSConfig.h 中定义的 configMAX_PRIORITIES 配置。 优先级 0 是最低优先级,分配给空闲任务使用,一般不建议用户来使用这个优先级。因此,可用优先级的范围是 0 到(configMAX_PRIORITIES - 1)。在同一个优先级下,也可以放任意数量的任务,确保最大的设计灵活性。

2.2 任务状态

2.2.1 任务状态的定义

FreeRTOS 系统中的任务有如下四种状态定义。系统初始化完成后,创建的任务就可
以根据自己所处的不同状态,在系统由内核进行调度:

  • 就绪( Ready):该任务在就绪列表中, 就绪的任务已经具备执行的能力,只等待调度器进行调度,新创建的任务会初始化为就绪态。
  • 运行(Running):该状态表明任务正在执行, 此时它占用处理器, FreeRTOS 调度器选择运行的永远是处于最高优先级的就绪态任务,当任务被运行的一刻,它的任务状态就变成了运行态。
  • 阻塞(Blocked): 如果任务当前正在等待某个时序或外部中断,我们就说这个任务处于阻塞状态,此时任务不在就绪列表中。任务被挂起、 任务被延时、任务正在等待信号量、读写队列或者等待读写事件等都属于阻塞态。
  • 挂起态(Suspended): 处于挂起态的任务对调度器而言是不可见的, 让一个任务进入挂起状态的唯一办法就是调用 vTaskSuspend()函数;而 把 一 个 挂 起 状态 的任 务 恢复的 唯 一 途径 就 是 调 用 vTaskResume() 或 vTaskResumeFromISR()函数,我们可以这么理解挂起态与阻塞态的区别,当任务有较长的时间不允许运行的时候,我们可以挂起任务,这样子调度器就不会管这个任务的任何信息,直到我们调用恢复任务的 API 函数;而任务处于阻塞态的时候,系统还需要判断阻塞态的任务是否超时,是否可以解除阻塞,比处于挂起态费时一些。大多数应用程序不使用挂起状态。
2.2.2 任务状态的迁移

FreeRTOS 系统中的每一个任务多种状态之间的转换关系是怎么样的呢?从运行态任务变成阻塞态,或者从阻塞态变成就绪态,这些任务状态是如何进行迁移?下面就让我们一起了解。

如下图是一个完整的状态机迁移图,包含如下九种状态迁移方式:

  1. 创建任务→就绪态:任务创建完成后进入就绪态,表明任务已准备就绪,随时可以运行,只等待调度器进行调度。
  2. 就绪态→运行态:发生任务切换时,就绪列表中最高优先级的任务被执行,从而进入运态。
  3. 运行态→就绪态:有更高优先级任务创建或者恢复后,在滴答中断会发生任务调度,此刻最高优先级任务变为运行态,那么原先运行的任务由运行态变为就绪态,依然在就绪列表中,等待最高优先级的任务运行完毕继续运行原来的任务(此处可以看做是 CPU 使用权被更高优先级的任务抢占了)。
  4. 运行态→阻塞态:正在运行的任务发生阻塞(挂起、延时、读信号量等待)时,该任务会从就绪列表中删除,任务状态由运行态变成阻塞态,然后发生任务切换,运行就绪列表中当前最高优先级任务。
  5. 阻塞态→就绪态:阻塞的任务被恢复后(任务恢复、延时时间超时、读信号量超时或读到信号量等),此时被恢复的任务会被加入就绪列表,从而由阻塞态变成就绪态;如果此时被恢复任务的优先级高于正在运行任务的优先级,则会发生任务切换,将该任务将再次转换任务状态,由就绪态变成运行态。
  6. 就绪态→挂起态:任务可以通过调用 vTaskSuspend() 函数可以将处于就绪态的任务挂起,被挂起的任务得不到CPU 的使用权,也不会参与调度,除非它从挂起态中解除。
  7. 阻塞态→挂起态:同样,任务可以通过调用 vTaskSuspend() 函数将处于阻塞态的任务挂起。
  8. 运行态→挂起态:同样,任务可以通过调用 vTaskSuspend() 函数将处于运行态的任务挂起。总之,不管当前任务处于何种状态,调用vTaskSuspend()后都会将任务挂起。
  9. 挂起态→就绪态: 把 一 个 挂 起 状态 的 任 务 恢复的 唯 一 途 径 就 是调 用 vTaskResume() 或 vTaskResumeFromISR()  函数,如果此时被恢复任务的优先级高
    于正在运行任务的优先级,则会发生任务切换,将该任务将再次转换任务状态,由就绪态
    变成运行态。

三. 任务管理API函数

下面我们对任务管理常用的函数进行讲解,重点在于如何正确使用其接口函数。

3.1 任务创建函数:xTaskCreate

BaseType_t xTaskCreate(    TaskFunction_t pvTaskCode,
                            const char * const pcName,
                            const configSTACK_DEPTH_TYPE uxStackDepth,
                            void *pvParameters,
                            UBaseType_t uxPriority,
                            TaskHandle_t *pxCreatedTask
                          );

其中,

pvTaskCode

指向实现任务的函数的指针(即要创建的任务函数的名称)。记得要创建的任务必须是无限循环的吗,没有返回值。

pcName

任务的描述性名称。FreeRTOS 不会以任何方式使用它,它纯粹是作为调试辅助工具而包含。

usStackDepth

每个任务都有自己的唯一堆栈,该堆栈在创建任务时由内核分配给任务。 

该值指定堆栈可以容纳的字数,而不是字节数。 例如,如果堆栈是 32 位宽并且 usStackDepth 作为 100 传入,则将分配 400 字节的堆栈空间(100 * 4 字节)。

空闲任务使用的堆栈大小由应用程序定义的常量 configMINIMAL_STACK_SIZE定义。

没有简单的方法来确定任务所需的堆栈空间。一般分配一个我们认为合理的值,然后可以使用FreeRTOS 提供的任务的堆栈使用量API函数 uxTaskGetSystemState()查看,来确保分配的空间确实足够,并且RAM 不会被浪费。 

pvParameters分配给 pvParameters 的值是传递给任务的值,如果没有可以设置为 NULL

uxPriority

定义任务执行的优先级。可以将任务优先级设置为从 0(最低优先级)到(configMAX_PRIORITIES - 1)。

pxCreatedTask

pxCreatedTask 可用于传递正在创建的任务的句柄。然后,此句柄可用于引用 API 调用中的任务,例如,更改任务优先级或删除任务的时候。如果我们的应用程序没有使用任务句柄,那么 pxCreatedTask 可以设置为 NULL

返回值

有两种返回值:

pdPASS:这表明该任务已成功创建。

pdFAIL:这表明该任务尚未创建,可能的原因是 FreeRTOS 可用的堆内存不足,无法分配足够的 RAM 来保存任务数据结构和堆栈。

 3.2 任务删除函数:vTaskDelete

void vTaskDelete( TaskHandle_t xTask );

其中,

xTask 待删除的任务的句柄。传递 NULL 将删除正在执行的任务本身。删除任务后,将从所有就绪,阻塞,挂起和事件列表中删除。

  3.3 任务相对延时函数:vTaskDelay

void vTaskDelay( const TickType_t xTicksToDelay );

其中,

xTicksToDelay 调用任务应阻塞的 tick 周期数。

vTaskDelay()延时是相对性的延时,它指定的延时时间是从调用 vTaskDelay()结束后开
始计算的, 经过指定的时间后延时结束。比如 vTaskDelay(100), 从调用 vTaskDelay()结束
后,任务进入阻塞状态,经过 100 个系统时钟节拍周期后,任务解除阻塞。此外,该延迟时间不是绝对准确的,其它任务和中断活动, 会影响到 vTaskDelay()的使用(比如调用前高优先级任务抢占了当前任务
进而影响到任务的下一次执行的时间。

3.4 任务绝对延时函数:vTaskDelayUntil

void vTaskDelayUntil( TickType_t *pxPreviousWakeTime,
                      const TickType_t xTimeIncrement );

其中,

pxPreviousWakeTime 指向一个变量的指针,该变量用于保存任务最后一次解除阻塞的时间。 该变量在第一次使用前必须用当前时间进行初始化,即得到当前时间, 在这之后,该变量会在vTaskDelayUntil() 中自动更新。
xTimeIncrement 任务执行的周期。 该任务将在 (*pxPreviousWakeTime + xTimeIncrement)时间解除阻塞。 配合相同的 xTimeIncrement 参数值 调用 vTaskDelayUntil 将导致任务以固定的间隔期执行。

不同于vTaskDelay()的相对延时,vTaskDelayUntil()的延时是绝对的,适用于周期性执行的任务。 当指定唤醒时间(*pxPreviousWakeTime +xTimeIncrement)到达后, vTaskDelayUntil()函数立刻返回,如果任务是最高优先级的,那么任务会立马解除阻塞所以说 vTaskDelayUntil()函数的延时是绝对性的。

3.5 任务挂起函数:vTaskSuspend

void vTaskSuspend( TaskHandle_t xTaskToSuspend );

其中,

xTaskToSuspend 被挂起的任务句柄。传递空句柄NULL将暂停任务自身。

任务可以通过调用 vTaskSuspend()将处于任何状态的任务挂起,被挂起的任务得不到 CPU 的使用权,也不会参与调度,它相对于调度器而言是不可见的,除非它从挂起态中解除。比如我们想暂停某个任务一段时间,但后面某个时候又想让这个任务继续执行吗,那么不能用任务删除函数vTaskDelete(),因为删除后任务就完全被系统删除了,这时候vTaskSuspend()是一个比较好的选择,想要任务暂停的时候直接将任务挂起,其内部的资源都会保留下来,同时也不会参与系统中任务的调度。

3.6 任务恢复函数:vTaskResume

void vTaskResume( TaskHandle_t xTaskToResume );

其中,

xTaskToResume要恢复的任务句柄。注意这里不能传递空句柄NULL。

当任务挂起之后,如果想要恢复,就可以调用vTaskResume()函数让任务从挂起前的状态恢复到就绪状态,无论任务在挂起时候调用过多少次vTaskSuspend()函数,也只需调用一次vTaskResume ()函数即可将任务恢复运行,同样,无论调用多少次的 vTaskResume()函数,也只在任务是挂起态的时候才进行恢复。这里注意,要恢复的任务不能在任务自身中恢复,而应该在其他运行的任务中恢复,因为这时候挂起的任务已经不运行了,调度器都看不到它。

3.7 其他任务管理API函数

上面几个任务管理API函数已经能适用大部分的工况,当然,FreeRTOS官方提供的任务管理函数不止于此,想要了解更多的话,我们可以参考官方网址,里面对每个API函数都有详细的说明:

Links to the FreeRTOS task control API function vTaskDelay, vTaskDelayUntil, uxTaskPriorityGet, vTaskPrioritySet, vTaskSuspend, vTaskResume, xTaskResumeFromISR, vTaskSetApplicationTaskTag, xTaskCallApplicationTaskHookThis page contains links to the FreeRTOS task control API function descriptions, vTaskDelay, vTaskDelayUntil, uxTaskPriorityGet, vTaskPrioritySet, vTaskSuspend, vTaskResume, xTaskResumeFromISR, vTaskSetApplicationTaskTag, xTaskCallApplicationTaskHook. FreeRTOS is a portable, open source, mini Real Time kernel.A free RTOS for small embedded systemsicon-default.png?t=N7T8https://www.freertos.org/zh-cn-cmn-s/a00112.html

四. 任务管理相关实验

4.1 任务创建与删除实验

在这个实验中,我创建了两个任务task_example_1和task_example_2,task_example_1优先级高于task_example_2,task_example_1每间隔1s即100个tick进入阻塞状态,软件中已设置tick周期为10ms;stask_example_2每间隔500ms进入阻塞状态。在task_example_1的循环中对cnt进行累加,累加到5也就是5s后,删除任务task_example_2,用来模拟删除任务现象。主体代码如下:

TaskHandle_t  task_example_1_handle = NULL;
TaskHandle_t  task_example_2_handle = NULL;

static int32_t cnt;
TickType_t xWakeTime_task1;
TickType_t xWakeTime_task2;

static void task_example_1(void* arg)
{
    
    while (1)
    {
        xWakeTime_task1 = xTaskGetTickCount();    
        printf("task_example_1 is Running at %d\r\n",xWakeTime_task1); 

        cnt ++;
        printf("cnt: %d\n", cnt);
        vTaskDelay(1000 / portTICK_RATE_MS);   /* 延时1s */ 
    }
 
}

static void task_example_2(void* arg)
{
    
    while (1)
    {
        xWakeTime_task2 = xTaskGetTickCount();     
        printf("task_example_2 is Running at %d\r\n",xWakeTime_task2);
        
        if(cnt == 5)
        {
             printf("task_example_2 has deleted\r\n");
             vTaskDelete(task_example_2_handle);
             
        }

        vTaskDelay(500 / portTICK_RATE_MS); 
    }
 
}



void app_main(void)
{
    BaseType_t xReturn;

    xWakeTime_task1 = xTaskGetTickCount(); 
    printf("app_main is Running at %d\r\n",xWakeTime_task1);

    //create task
   xReturn = xTaskCreate(task_example_1, "task_example_1", 2048, NULL, 2, &task_example_1_handle);
   if(xReturn == pdPASS)      printf("创建 task_example_1 任务成功!\r\n");
   else                       printf("创建 task_example_1 任务失败!\r\n");

   xReturn = xTaskCreate(task_example_2, "task_example_2", 2048, NULL, 1, &task_example_2_handle);
   if(xReturn == pdPASS)      printf("创建 task_example_2 任务成功!\r\n");
   else                       printf("创建 task_example_2 任务失败!\r\n");

  

    printf("Minimum free heapconfigMINIMAL_STACK_SIZE size: %d bytes\n", esp_get_minimum_free_heap_size());
   
}

下图为运行代码后的串口输出,可以看到: 任务创建成功,每间隔100个tick,task_example_1运行一次,之后进入阻塞状态,优先级更低的task_example_2得以运行,每50个tick运行一次。在cnt计数达到5之后,task_example_2被删除,之后串口就只有task_example_1运行显示。

 4.2 任务挂起与恢复实验

在这个实验中,同样创建了两个任务task_example_1和task_example_2,task_example_1优先级高于task_example_2,task_example_1每间隔1s即100个tick进入阻塞状态;stask_example_2每间隔500ms进入阻塞状态。在task_example_1的循环中对cnt进行累加,累加3s后,挂起任务task_example_2,累加6s后,恢复任务task_example_2,主体代码如下:

TaskHandle_t  task_example_1_handle = NULL;
TaskHandle_t  task_example_2_handle = NULL;

static int32_t cnt;
TickType_t xWakeTime_task1;
TickType_t xWakeTime_task2;

static void task_example_1(void* arg)
{
    
    while (1)
    {
        xWakeTime_task1 = xTaskGetTickCount();    
        printf("task_example_1 is Running at %d\r\n",xWakeTime_task1); 

        cnt ++;
        printf("cnt: %d\n", cnt);
        if(cnt == 3)
        {
             printf("task_example_2 has suspended\r\n");
             vTaskSuspend(task_example_2_handle);
             
        }

        if(cnt == 6)
        {
             printf("task_example_2 has resumed\r\n");
             vTaskResume(task_example_2_handle);
             
        }
        vTaskDelay(1000 / portTICK_RATE_MS);   /* 延时1s */ 

    }
 
}

static void task_example_2(void* arg)
{
    
    while (1)
    {
        xWakeTime_task2 = xTaskGetTickCount();     
        printf("task_example_2 is Running at %d\r\n",xWakeTime_task2);
        
        vTaskDelay(500 / portTICK_RATE_MS); 

    }
 
}


void app_main(void)
{
    BaseType_t xReturn;

    xWakeTime_task1 = xTaskGetTickCount(); 
    printf("app_main is Running at %d\r\n",xWakeTime_task1);

   xReturn = xTaskCreate(task_example_1, "task_example_1", 2048, NULL, 2, &task_example_1_handle);
   if(xReturn == pdPASS)      printf("创建 task_example_1 任务成功!\r\n");
   else                       printf("创建 task_example_1 任务失败!\r\n");

   xReturn = xTaskCreate(task_example_2, "task_example_2", 2048, NULL, 1, &task_example_2_handle);
   if(xReturn == pdPASS)      printf("创建 task_example_2 任务成功!\r\n");
   else                       printf("创建 task_example_2 任务失败!\r\n");

  

    printf("Minimum free heapconfigMINIMAL_STACK_SIZE size: %d bytes\n", esp_get_minimum_free_heap_size());

}

下图为运行代码后的串口输出,可以看到: 任务创建成功,在第3s的时候,task_example_2被挂起,之后串口就只有task_example_1运行显示;在第6s的时候,task_example_2被恢复,就得以继续运行。

五. 小结

本文主要探索了任务管理的原理及其使用方法,相信通过本文,大家对第一节的几个问题有了自己的答案,同时也对如何使用任务管理API函数有了更直观的认识。有某些地方不太明白的同学,欢迎留言交流。原创不易,大家的点赞和关注是对于持续更新莫大的鼓励,谢谢!

想要完整源码的同学,可以扫码,或者微信搜索:硬件电子与嵌入式小栈 ,关注我的微信公众号,留言即可获取,公众号里面也会有丰富的干货文章。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值