登录后台

页面导航

本文编写于 256 天前,最后修改于 256 天前,其中某些信息可能已经过时。

任务管理

任务函数

任务是使用C函数实现的,唯一特别的就是它的格式,它返回一个空,传递一个空指针参数,下面是任务函数的原型:

void aTaskFunction(void *pvParameters)

每一个任务都是有单独权限的程序。有一个进入点,会在一个内循环中一直运行,不会退出。下面是一个任务函数的基本结构

void aTaskFunction(void *pvParameters){
    /* 每个使用此样板函数的任务实现都会有其本身的变量复制。除非变量被申明为静态的,如果申明为静态变量内存中只会有一个此变量,所有的任务会共享此变量。变量名字前面的前缀在第1章第五节有描述,数据格式和编码指南有介绍*/
    int32_t lVariableExample = 0;

    /* 一个任务通常在其内链循环中实现 */
    for(;;){
        /* 任务的具体实现代码放在这里 */
    }
    /* 如果任务因为break退出上面的循环,在到达这个函数末尾前删除当前任务。传递给vTaskDelete()函数一个NULL参数,表示删除当前任务。这种约定的介绍位于第0节。对于V9.0.0以前的Freertos项目,必须要一个heap_n.c才能编译。对于V9.0.0以后版本只有在将configSUPPORT_DYNAMIC_ALLOCATION设置为1,或configSUPPORT_DYNAMIC_ALLOCATION没有在FreeRTOSconfig.h中配置时,才需要heap_n.c文件。第二章堆内存数据格式和编码规范有介绍。*/
    vTaskDelete(NULL);
}

Freertos的任务实现函数不允许任何形式的返回,也就是说不能包含任何return语句,也不能运行到程序的末尾。如果一个程序不再有用,它应该被明确的删除,这个上面的注释中也有提到。
可以用一个单独的任务来创建多个任务。每一个创建的任务成为执行实例的一部分。每一个创建任务都拥有独立的堆栈和局部变量。

顶层任务状态

一个程序由多个任务组成。如果处理器是单核的,每次就只能有一个任务在运行。意味着任务有两种状态,运行和不运行。这种简单的模型非常容易实现,但它却太简单了。下面将不运行又分成几个状态。
当一个任务是运行状态,处理器就执行这个任务。一个任务不在执行状态,它就是休眠的。它的状态被保存为就绪,即任务调度器决定下次恢复它执行将它的状态改变为执行。当一个任务恢复执行,它首先会恢复上次终止前的状态。
一个任务从不运行转换成运行被称作’switched in’或’swaped in’。相反从运行切换到不运行又叫’switched out’或’swaped out’。Freertos任务调度器是唯一可以进行这种切换的函数。

创建任务

xTaskCreate函数

// xTaskCreate函数原型
BaseType_t xTaskCreate(
                    TaskFunction_T pvTaskCode,
                    const char * pcName,
                    uint16_t usStackDepth,
                    void * pvParameters,
                    BaseType_t uxPriority,
                    TaskHandle_t * pxCreatedTask
);
  • pvTaskCode:任务只是永不退出的C函数,因此用内链循环实现,pvTaskCode变量只是一个实现这个任务函数的简单指针。其实只是这个函数的名字
  • pcname:任务的名字描述,Freertos没有以任何方式使用它。只是作为一个调试提示。对于人类用一个名字识别一个任务远比用他的句柄容易。configMAX_TASK_NAME_LEN限制了名字的最长字符数,它是包含结尾的空字符的。字符串太长会被截断。
  • usStackDepth:每个任务都有唯一的栈,他是任务创建时每个分配给任务的,usStackDepth决定每个给任务分配多大的栈。
    这个值是以字为单位的,不是字节。比如栈是32位宽,usStackDepth传递100,会分配400个字节的栈空间(4 * 100),栈空间由栈宽度决定。空闲任务的栈空间由常量configMINIMAL_STACK_SIZE确定,这个常量定义在Freeetos的实例程序中,是推荐的最小任务空间。如果任务会用很多的栈空间,你应该分配一个大的空间。
    没有简单的方法确定任务的栈空间。它可以计算,但大部分程序员只是随便写一个可接受值。然后用freertos提供的功能确定它是否合适的,内存也没有被大量浪费。
  • pvParameters:任务函数接受一个void *格式的参数,这个变量就是传给任务函数的变量。
  • uxPriority:任务优先级。最小的任务等级是0,最大的是configMAX_PRIORITIES。configMAX_PRIORITIES是用户定义的。
  • pxCreatedTask:当前创建任务句柄。这个句柄可以用于后面传递给任务管理的用户接口函数,比如删除任务或改变任务优先级函数。
  • Returned Value:有两个可能的返回值,pdass和pdFalse。pdPass表示任务创建成功。pdFalse表示任务创建失败。可能是因为没有足够的空间分配给任务数据结构和栈。

实例1创建任务

void task1(void *pvParameters){
    const char *pcTaskName = "Task1 is running\r\n";
    volatile uint32_t ul;   /*  volatile 防止编译器优化掉,不进行循环*/

    /* 大多数任务,都以这样的内联循环运行*/
    for(;;){
        /* 打印任务名字 */
        vPrintString( pcTaskName );

        /* 延迟 */
        for(ul = 0; ul < mainDELAY_LOOP_COUNT; ul++)
        {
            /* 这个循环只是延迟实现,啥都不干。后面的例子会用delay或sleep函数替代*/
        }
    }
}

void task2(void *pvParameters){
    const char *pcTaskName = "Task2 is running\r\n";
    volatile uint32_t ul;   /*  volatile 防止编译器优化掉,不进行循环*/

    /* 大多数任务,都以这样的内联循环运行*/
    for(;;){
        /* 打印任务名字 */
        vPrintString( pcTaskName );

        /* 延迟 */
        for(ul = 0; ul < mainDELAY_LOOP_COUNT; ul++)
        {
            /* 这个循环只是延迟实现,啥都不干。后面的例子会用delay或sleep函数替代*/
        }
    }
}
// 主函数开始调度器之前创建任务

int main(void){
    /* 创建2个任务。真正的程序应当检测xTaskCreate()函数的返回值,确保函数成功执行。*/
    xTaskCreate(task1, "Task1", 1000, NULL, 1, NULL);
    xTaskCreate(task2, "Task2", 1000, NULL, 1, NULL);

    /* 开启调度器 */
    vTaskStartScheduler();

    /* 如果一切顺利,主函数就不会运行到接下来的循环。如果主函数运行到这个循环,就说明没有足够的堆用于创建空闲任务。第二章提供了更加详细的堆管理信息。*/
    for(;;);
}
Task1 is running
Task2 is running
Task1 is running
Task2 is running
Task1 is running
Task2 is running
Task1 is running
Task2 is running
...

尽管程序感觉像是在处理器中同时运行,但这不是实事。实际上两个任务交替进入和退出运行状态,两个任务都以同等优先级运行。它们会共享处理器的时间。最后的执行结果就像上面那样。

# 两个任务的实际执行情况

Task1  |---|   |---|   |---|   |
Task2  |   |---|   |---|   |---|

time   T1  T2  T3  T4 ...

实例2使用参数

// 单独任务函数用来创建2个任务
void vTaskFunction(void *pvParameters){
    char *pcTaskName;
    volatile uint32_t ul; // volatile 关键字防止编译器将它优化掉,不进行延迟循环*/

    /* 打印出的字符串是通过这个参数传递的,注意转化为char *格式*/
    pcTaskName = (char *)pvParameters;

    /* 和大多数任务一样的内联循环*/
    for(;;){
        // 打印任务名字
        vPrintString(pcTaskName);
        // 延迟循环
        for(ul = 0; ul < mainDELAY_LOOP_COUNT; ul++) ;
    }
}
// 例2的main()函数
/* 首先定义将要传递给任务函数的字符串。它们被定义为常量,而不是栈上的变量,是为了保证任务执行中也可以访问到*/
static const char *pcTextForTask1 = "Task1 is running\r\n";
static const char *pcTextForTask2 = "Task2 is running\r\n";

int main(void){
    // 创建第一个任务
    xTaskCreate(vTaskFunction,"Task1", 1000, pcTextForTask1, 1, NULL);
    /* 同样的方式创建另外一个任务。注意这次的任务创建使用的是相同的任务实现(vTaskFunction)。只有传递的参数不一样。两个相同实现的任务就创建成功了。*/
    xTaskCreate(vTaskFunction,"Task2", 1000, pcTextForTask2, 1, NULL);

    /* 开启调度器以使任务开始执行 */
    vTaskStartScheduler();

    /* 如果一切正常,main()函数不会执行到这里,因为调度器应该正在执行任务。如果main()函数运行到了这里,那么应该是没有足够的空间用于创建空闲任务了。可以查看第二章获取更多关于堆管理的细节*/
    for(;;);
}

任务优先级

xTaskCreate函数的uxPriority参数是任务创建时的初始优先级。这个优先级可以使用vTaskPrioritySet函数在调度器运行之后改变。
最大的可用优先级在FreeRTOSConfig.h文件中配置,具体一点就是configMAX_PRIORITIES宏。低优先级的数字代表低优先级任务,0是最低的优先级。尽管优先级是从0到configMAX_PRIORITIES - 1,有些任务可以使用相同的优先级。确保最大的设计灵活性。
FreeRTOS系统可以在2个中选一个方法决定哪个任务运行。configMAX_PRIORITIES可以被设置为最大多大取决于使用那种方法:

  1. 通常做法
    通常做法是用C实现的,所有的FreeRTOS架构都可以使用。
    当使用通常方法时,FreeRTOS系统不会限制configMAX_PRIORITIES可以设置的最大值。尽管一般都建议让configMAX_PRIORITIES尽量小,因为越大就需要更大的内存空间,也会导致更多不必要的浪费。每次任务调度都会查找这个优先级队列。
    FreeRTOSConfig.h中的configUSE_PORT_OPTIMISED_TASK_SELECTION设置为0或没有被设置时就是使用通常的方法。或者当前FreeRTOS只支持通常做法也就只能用通常做法了。
  2. 平台优化法
    平台优化法使用了少量的汇编代码,所以它比通常做法快。configMAX_PRIORITIES不会对运行时间有坏影响。
    如果使用平台优化法,configMAX_PRIORITIES不能大于32。就像通常方法中建议configMAX_PRIORITIES尽量小,因为更大的值导致更多的内存浪费。FreeRTOSCofig.h文件中的configUSE_PORT_OPTIMISED_TASK_SELECTION设置为1时使用平台优化法。但注意不是所有的平台都支持这种方法。

FreeRTOS会始终保持高优先级任务处于执行状态。多个同等优先级任务可以允许的,调度器会轮流执行这些任务。

时间度量和tick中断

所有的任务都有优先级,都需要运行。因此每个任务运行一个时间片,时间片开始时进入运行状态,时间片结束退出运行状态。上面的T1,T2之间的时间就是一个时间片。为了选出下一个要运行的任务,每个时间片结束时必须执行调度器。tick中断是一个周期性中断,就是用来启动调度器。tick中断频率就是用来设置每个时间片的长度。它被定义在FreeRTOSConfig.h中的configTICK_RATE_HZ宏上。比如,configTICK_RATE_HZ设置为100Hz哪个每个时间片就是10ms。两个tick中断间隔叫tick周期,它等于时间片长度。上面的Task1,Task2的运行时序,同样演示了调度器的运行规矩。下面就展示了带调度器的运行时序图。从一般任务到tick中断和从tick中断到一般任务就完成了一个调度器的调用。

# tick中断的执行顺序
Kernel(tick) |   -|   -|   -|   -|   -|
Task1        |--- |    |--- |    |--- |
Task2        |    |--- |    |--- |    |

FreeRTOS中常用各种时钟周期来指代时间,它们经常简单的用’tick’来表示。pdMS_TO_TICKS宏就是将ms时间转换为具体的tick。它的频率依赖于具体的tick频率,pdMS_TO_TICKS在tick频率超过1000Hz的时候不能使用(如果configTICK_TATE_HZ大于1000)。

/* pdMS_TO_TICKS()接受一个以毫秒为单位的时间值,将它转化为tick的周期数。下面例子中xTimeTicks被设置为tick周期数,这些周期数刚好花费200ms*/
TickType_t xTimeTicks = pdMS_TO_TICKS(200);

不推荐在程序中直接用tick数指定时间,而是使用pdMS_TO_TICKS宏方式的ms指定时间,这样做可以确保即使tick周期改变,程序指定的时间也不会改变。
tick计数是从调度器运行以来总共发生的tick中断的总数,假设tick数量没有溢出。用户程序指定延迟周期数时不用考虑溢出问题,因为FreeRTOS会自动给我们管理好。

优先级的使用

//创建两个不同优先级任务

/* 定义2个要传递给任务函数的字符串。它们定义为静态常量,以至于可以在任务函数中访问*/
static const *pcTextForTask1 = "Task1 is running\r\n";
static const *pcTextForTask2 = "Task2 is running\r\n";

int main(void){
    /* 创建一个优先级为1的任务*/
    xTaskCreate(vTaskFunction, "Task1", 1000, (void *)pcTextForTask1, 1, NULL);
    /* 创建一个优先级为2的任务*/
    xTaskCreate(vTaskFunction, "Task2", 1000, (void *)pcTextForTask2, 2, NULL);

    /* 开启调度器 */
    vTaskStartScheduler();

    /* 不应该运行到这里 */
    return 0;
}

调度器会一直选择高优先级任务运行。任务2的优先级高于任务1,因此任务2会一直处于运行状态。因为任务1一直没有进入运行态,它不会打印字符串。任务1相对于任务2就处于"starved"–饥饿状态。

# 不同优先级任务运行时序

FreeRTOS(Tick)    |   -|   -|   -|   -|
Task1             |    |    |    |    |
Task2             |--- |--- |--- |--- |
                  t1  t2   t3   t4   t5

拓展非运行状态

目前为止,所有的任务都有事情做,它们不用等待一些事件或者条件。总是可以进入运行状态。这种连续处理任务作用有限。因为它们只能在最低优先级下运行。因为如果它们运行在其他优先级,它们会完全阻碍更低优先级任务运行。
为了让任务模型有用,必须用事件驱动方式重写。一个事件驱动的任务,只有在时间发生后才会执行,事件发生前是不能执行的。调度器选择最高优先级任务执行。高优先级任务不能执行,就是调度器不会选择执行它们,而是选择一个低优先级可以执行的任务。因此,事件驱动模型意味着,任务可以以不同优先级创建,而且高优先级任务不会使低优先级任务处于"饥饿"状态。

阻塞状态

任务在等待事件发生就叫做阻塞态(blocked),它是非运行态的子状态。
任务可以在阻塞态等待两种事件:

  • 时间关键事件–事件可以是延迟周期期满,也可以是一个绝对时间的到来。比如等待10ms
  • 同步事件–事件源自其他任务或中断。比如一个任务可能在等待一个队列接受到数据。同步事件包含很多种事件类型。
    FreeRTOS队列,二进制信号量,普通信号量,互斥锁,继承互斥锁,事件组和任务通知都可以用来创建同步事件。对于一个任务,完全可能阻塞在一个同步事件上,而且没有超时时间。或者同时阻塞在各种同步事件上。比如一个任务可以选择等待10ms或等待队列接收到数据。任务即会在10ms内接收到数据也会在10ms到时间停止阻塞。

暂停状态

暂停状态是非运行态的子状态。调度器不会执行暂停状态的任务。唯一将任务设置为暂停状态的方法是调用vTaskSuspend()接口函数,退出暂停的方法是调用vTaskResume()或vTaskResumeISR()接口函数。大部分程序不会使用暂停状态。

就绪状态

程序没有在运行也没有在阻塞,同时没有在暂停状态就被叫做就绪状态。它们可以运行,因此是准备运行,但当前没有在运行。

状态转换

            ----------------------
        |非运行              |
        |        >暂停态<    |
        |      /  ^   |   \  |
        |     /   |  恢复  \ |
        |    /   暂停 |     \| vTaskSuspend
        |   /     |   v      |\----
        |  /      就绪态-----|-----> 运行态
        | |       ooooo <----|------ ooooooo
        |  \        ^        |/-----
        | vTask     |       /| Blocking API
        | Suspend  事件    / |
        |     \     |     /  |
        |      \    |    /   |
        |        阻塞态<     |
        |                    | 
        ----------------------

使用阻塞状态创建一个延迟

之前所有创建的任务都是周期性的——他们都周期性延迟并打印自己任务名称。然后再次延迟,如此循环。以上的任务都是使用null循环做粗糙的延迟——任务持续增加一个值,直到这个值到一个固定值。例3抛弃了这种拙劣的做法。高优先级任务在执行空循环的时候依然保持执行状态,会让比他低优先级的任务一直处于"饥饿"状态。
有很多拙劣的轮训方式,它们都很低效。任务不会通过轮训做什么具体任务,但它依然占用处理器时间,并且浪费处理器时钟。下面实例中用vTaskDelay()接口函数代替了空循环这种低效轮训行为。它的原型就在下面展示。新的任务定义也在下面。注意只有INCLUDE_vTaskDelay在FreeRTOS.h文件中定义为1时可以使用。
vTaskDelay()将调用它的任务状态改为阻塞状态,一段固定数量的tick中断后恢复。阻塞状态中任务不会使用处理器时间,因此任务只有在真正工作时才占用处理器。

// 原型
void vTaskDelay(TickType_t xTicksToDelay);
// 参数: xTicksToDelay 在任务返回就绪状态前将会保持阻塞状态的tick中断数。
//      例: 当前tick计数是10000,一个任务调用了vTaskDelay(100),这个任务会进入阻塞态,保持阻塞状态直到tick计数到10100。
//      pdMS_TO_TICKS可以用来和vTaskDelay共同使用,用于阻塞固定的时间。例:调用vTaskDelay(pdMS_TO_TICKS(100))将会保持阻塞状态100ms。
// 用vTaskDelay()代替空循环延迟函数

void vTaskFunction(void *pvParameters){
    char *pcTaskName;
    const TickType_t xDelay250ms = pdMS_TO_TICKS(250);

    /* 通过这个变量传给字符串,注意强制转换为char * */
    pcTaskName = (char *)pvParameters;

    /* 同大多数任务一样的内联循环 */
    for(;;){
        /* 打印任务名字 */
        vPrintString(pcTaskName);
        /* 延迟一个周期,这次调用vTaskDelay(),它将当前任务状态改为阻塞状态,直到延迟周期到达。它的参数是以tick数为单位的,所以使用pdMS_TO_TICKS宏实现和时间时间的转换,本例中延迟250ms */
        vTaskDelay(xDelay250ms);
    }
}
Task2 is running
Task1 is running
Task2 is running
Task1 is running
Task2 is running
Task1 is running
Task2 is running
Task1 is running
Task2 is running
Task1 is running
Task1    | -  |    |    |    |    |    | -  |    |
Task2    |-   |    |    |    |    |    |-   |    |
Idle     |  --|----|----|----|----|----|  --|----|
        t1   t2   t3   t4     time    tn

start -> Ready -> Running --Blocking API-> Blocked --Event-> Ready -> Running ....

只是改变了任务的实现,而没有改变任务的功能。对比用空循环和vTaskDelay延迟的代码可以得到结论,这个功能以更有效的方式实现。
用null循环延迟时运行模式是任务一直都在运行。它用掉了100个处理器时钟片。本例的运行模式是,当任务进入阻塞态,进入延迟周期后。只有有事情处理的任务才会使用处理器时钟(本例中就是打印任务名字)。因此导致只会使用很少量的处理器资源。
这里假设,每次任务离开阻塞状态只会使用一小部分tick周期,之后就再次进入阻塞态。大部分时间没有任务在运行(没有任务处于就绪状态),因此没有任务是运行状态。当这种事情发生时,空闲任务就会运行。空闲任务分配的处理器时间是判断系统备用能力的重要指标。使用一个事件驱动的实时系统,可以通过程序实现事件驱动来增加备用处理器能力。

vTaskDelayUntil函数

vTaskDelayUntil和vTaskDelay相似。vTaskDelay的参数指定了2次阻塞状态之间应该发生的tick中断数。也就是说阻塞状态保持时间就是vTaskDelay()的参数。但任务离开阻塞状态的时间和vTaskDelay()调用时间有关。
vTaskDelayUntil特点。这个外部tick数是任务从阻塞态变为就绪态的tick数。vTaskDelayUntil应用于运行周期固定的任务(任务以固定频率运行)。特定时间绝对无阻塞调用任务。而不是和什么时候调用vTaskDelay()相关。

// vTaskDelayUntil()实现任务函数实例
void vTaskFunction(void *pvParameters){
    char *pcTaskName;
    TickType_t xLastWakeTime;

    /* 待打印字符串捅咕这个参数传递,注意强制转换为char * */
    pcTaskName = (char *)pvParameters;

    /* xLastWakeTime需要使用当前tick数初始化。注意这是唯一一次这个变量明确写入的地方。从这以后xLastWakeTime都是vTaskDelayUntil()函数自动管理*/
    xLastWakeTime = xTaskGetTickCount();

    /* 和其他任务一样的内联循环 */
    for(;;){
        /* 打印任务名称 */
        vPrintString(pcTaskName);
        /* 这个任务每250ms执行一次,和之前的vTaskDelay()一样,需要使用pdMS_TO_TICKS(250)将时间转换成相应的tick数。xLastWakeTime是vTaskDelayUntil()函数自动更新的,而不是由任务明确更新*/
        vTaskDelayUntil(&xLastWakeTime, pdMS_TO_TICKS(250));
    }
}

空闲任务

当任务在阻塞状态,不能运行,不能被调度器选中。最少要有一个任务处于运行状态。为了满足这一点,调度器会自动创建一个任务,在调用vTaskStartScheduler()的时候。空闲任务比一个循环都小,就像开始的初始化例子一样,它总是可以运行的。
空闲任务优先级最低——0,确保不会阻碍更高优先级任务进入运行状态。因此空闲任务优先级也是可以共享的。FreeRTOSConfig.h文件中的configIDLE_SHOULD_YIELD常量可以用于阻止空闲任务抢占其他任务需要的处理器宝贵时间。
注意:如果一个任务使用vTaskDelete()函数,空闲任务不会使其他任务饥饿。这是因为空闲任务的职责只是在删除任务后清理内核资源。

可以通过空闲任务勾子给空闲任务增加特定函数(空闲回调)——每个空闲任务周期自动调用的函数。
空闲任务勾子常用情形:

  • 执行低优先级,背景的,连续处理函数。
  • 度量程序备用能力空间(空闲任务只有在其他高优先级任务无事做时才运行,因此可以用于测量程序备份空间,以判断系统能力)
  • 处理器进省电模式,提供一个简单自动的省电方式,没有程序执行就进省电模式。

空闲任务勾子函数必须遵守以下规定:

  • 不试图阻塞或暂停。阻塞空闲任务可能导致没有任务可以进入运行状态。
  • 如果任务调用vTaskDelete函数,空闲勾子函数必须返回给调用者一个可用的时间周期。这是因为空闲任务的职责就是删除任务后清理系统资源。如果空闲任务永久处于空闲任务勾子函数中,清理动作就不会发生。

以下是空闲任务勾子函数原型:

// 一个简单的空闲任务勾子函数
/* 一个会被空闲任务勾子函数增加的变量 */
ul uint32_t ulIdelCycleCount = 0UL;

/* 空闲任务勾子函数函数名必须是vApplicationIdleHook(),没有参数,返回void */
void vApplicationIdleHook(){
    /* 只增加循环次数 */
    ulIdelCycleCount++;
}
void vTaskFunction(void *pvParameters){
    char *pcTaskName;
    const TickType_t xDelay250ms = pdMS_TO_TICKS(250);

    /* 打印字符串 通过这个变量传递,注意强制类型转换char * */
    pcTaskName = (char *)pvParameters;

    /* 如常内联*/
    for(;;){
        /* 打印任务名字和循环次数 */
        vPrintStringAndNumber(pcTaskName, ulIdelCycleCount);
        /* 延迟250ms */
        vTaskDelay(xDelay250ms);
    }
}
# 空闲任务勾子函数实例输出
Task2 is running
ulIdleCycleCount = 0
Task1 is running
ulIdleCycleCount = 0
Task2 is running
ulIdleCycleCount = 3869504
Task1 is running
ulIdleCycleCount = 3869504
Task2 is running
ulIdleCycleCount = 8564623
Task1 is running
ulIdleCycleCount = 8564623
Task2 is running
ulIdleCycleCount = 13181489
Task1 is running
ulIdleCycleCount = 13181489
Task2 is running
ulIdleCycleCount = 17838406
Task1 is running
ulIdleCycleCount = 17838406

显示每个任务循环之间的时间,空闲任务勾子函数被调用了400多万次。空闲任务勾子函数调用次数取决于硬件速度。

改变任务优先级

vTaskPrioritySet()接口函数可以用来在调度器启动后改变已有任务的优先级。值得注意的是vTaskPrioritySet()接口函数只有在FreeRTOSConfig.h中的INCLUDE_vTaskPrioritySet设置为1时才可以使用。

// vTaskPrioritySet()原型和参数

void vTaskPrioritySet( TaskHandle_t pxTask, UBaseType_t uxNewPriority );

// 参数:
// pxTask: 要改变优先级的任务句柄——可以查看前面章节的xTaskCreate()接口函数的pxCreatedTask参数说明了解更多相关信息。
// uxNewPriority: 任务要将被设置成的优先级。会自动检查是否小于configMAX_PRIORITIES,最大就是configMAX_PRIORITIES-1。configMAX_PRIORITIES是FreeRTOSConfig.h中的一个配置选项。

uxTaskPriorityGet()接口函数可以用来获取任务优先级。注意只有在FreeRTOSConfig.h中的INCLUDE_uxTaskPriorityGet被设置为1时,uxTaskPriorityGet()接口函数才能使用。

// uxTaskPriorityGet原型
UBaseType_t uxTaskPriorityGet( TaskHandle_t pxTask );
// 参数 - pxTask:结合vTaskPrioritySet()接口函数看,就一目了然了。就是它的第一个参数。—可以查看前面章节的xTaskCeate()接口函数的pxCreatedTask参数说明了解更多相关信息。可以通过传递NULL参数获取当前任务的优先级。
// 返回值:相关任务优先级

实例

  1. Task1以一个最高优先级创建,因此会首先执行。他会一直打印出它的任务名称,直到任务2优先级持续增加到可以降低Task1优先级。
  2. Task2在处于最高优先级的时候停止运行,因为只有一个任务处于运行状态,Task2运行,Task1就处于就绪态。
  3. Task2打印一个消息,然后降低自己的优先级,并低于Task1优先级。
  4. Task2降低自身优先级后,Task1再次成为优先级最高的任务,Task1再次进入运行态,强制Task2进入就绪态。
// Task1源码
void vTask1(void *pvParameters){
    UBaseType_t uxPriority;

    /* 在Task2优先级更高前这个任务会一直运行,Task1和Task2都不会阻塞,所有都处于运行或就绪态*/
    /*获取当前任务优先级*/
    uxPriority = uxTaskPriorityGet(NULL);

    for(;;){
        // 打印
        vPrintString("Task1 is running\r\n");
        // 增加Task2优先级,直到Task2优先级不小于Task1优先级,Task2开始执行。注意vTaskPrioritySet()中xTask2Handle句柄的使用。创建任务时会初始化Task2任务句柄*/
        vPrintString("About to raise the Task 2 priority/r/n");
        vTaskPrioritySet(xTask2Handle, (uxPriority + 1));

        /* Task1只有在优先级比Task2任务优先级高时才会运行。因此对于这个任务,为了达到这个目标,Task2必须已经执行,并且Task2任务优先级降低且低于Task1任务优先级*/
    }
}

//Task2源码
void vTask2(void *pvParamerters){
    UBaseType_t uxPriority;

    /*Task1会在这个任务之前一直运行,行为Task1以更高的优先级创建。Task1和Task2都不会进入阻塞态,所以不是处于运行就是处于就绪态*/
    /*通过给uxTaskPriorityGet()接口函数传递NULL参数获取当前任务优先级*/
    uxPriority = uxTaskPriorityGet(NULL);

    for(;;){
        vPrintString("Task2 is running\r\n");
        /*降低当前任务优先级。传递NULL意味着改变当前任务优先级。当当前任务优先级低于Task1优先级时,Task1立即开始运行,就会抢占当前任务*/
        vPrintString("About to lower the Task2 priority/r/n");
        vTaskPrioritySet(NULL, uxPriority - 2);
    }
}
//例8 main()函数实现
/* 声名一个句柄变量用来保存Task2句柄 */
TaskHandle_t xTask2Handle = NULL;

int main(void){
    /* 以优先级2创建第一个任务。任务函数参数这里没有使用传递NULL,任务句柄参数设置为NULL不使用。*/
    xTaskCreate(Task1,"Task1",1000,NULL,2,NULL);
    /* Task1优先级为2 */

    /* 以优先级1创建第二个任务,它的优先级比Task1优先级更低。任务参数也没有使用,设置为NULL。但任务句柄有使用到,因此传递任务句柄地址给xTaskCreate()函数使用,此参数是最后一个参数*/
    xTaskCreate(Task2, "Task2", 1000, NULL, 1, &xTask2Handle);

    /* 开启调度器 */
    vTaskStartScheduler();

    /* 如果一切顺利,main()函数就不会运行到这里,因为调度器现在执行任务。如果main()函数运行到这里可能是因为没有足够的堆给idle任务使用。更加详细的堆管理可以查看第二章查看详情*/
    for(;;);
}
-------------------            --------------------------
|Task1优先级最高先|             |Task1优先级任务比Task2任  |
|运行             |  ----------|务优先级高时再次运行,     |
------------------- /          |Task2优先级增加比Task1高  |
           \       /           |时Task2又运行,如此往复    |
            \     /            --------------------------
             \   /
              v v
Task1        |- - -|            --------------------------
Task2        | - - |            |空闲任务从未运行,因为两  |
Idle         | ^   |            |个任务总是在运行且优先级  |
            t1 |  t2  time      |都比空闲任务优先级高      |
               |                --------------------------
               |
        --------------------
        |Task2每运行1次,   |
        |Task1设置Task2优先 |
        |级为最高           |
        --------------------

About to raise the Task2 priority
Task2 is running
About to lower the Task2 priority
Task1 is running 
About to raise the Task2 priority
Task2 is running
About to lower the Task2 priority
Task1 is running 
About to raise the Task2 priority
Task2 is running
About to lower the Task2 priority
Task1 is running 
About to raise the Task2 priority
Task2 is running
About to lower the Task2 priority
Task1 is running 
About to raise the Task2 priority
Task2 is running
About to lower the Task2 priority
Task1 is running

删除任务

  1. main()函数中以1的优先级创建一个任务。当它运行时以优先级2创建任务2。任务2现在是最高优先级任务,因此立即开始执行。
  2. 任务2除了删除自己之外啥都不做。它可以通过传递给vTaskDelete()函数NULL参数而不是自身任务句柄完成删除操作。
  3. 任务2已经被删除后,任务1再次成为最高优先级任务,因此继续开始执行,从它调用vTaskDelay()进入一个短暂的阻塞态哪里。
  4. 任务1进入阻塞态时空闲任务执行,释放分配给已经删除的任务2的内存。
  5. 任务1离开阻塞态,它再次成为最高优先级任务,进入就绪态抢占空闲任务。因此进入运行状态,再次创建任务2。如此循环。
// 例9 任务2函数实现
void Task2(void *pvParameters){
    /* 任务2除了删除自己什么都不做。为了删除自己需要调用vTaskDelete(),使用参数NULL作为参数,而不需要传递传递自身的句柄*/
    vPrintString("Task2 is running and about to delete itself\r\n");
    vTaskDelete(NULL);
}

// 例9 任务1函数实现

TaskHandle_t xTask2Handle = NULL;

void Task1(void *pvarameters){
    const TickType_t xDelay100ms = pdMS_TO_TICKS(100);

    for(;;){
        /* 打印任务名字 */
        vPrintString("Task1 is running\r\n");
        /* 以更高的优先级创建任务2。任务函数参数不使用传NULL,任务句柄要使用,传递xTask2Handle变量的地址作为最后一个参数*/
        xTaskCreate(Task2, "Task2", 1000, NULL, 2, &xTask2Handle);

        /* 任务2优先级更高,因此对于任务1到这里时,任务2已经执行并删除了自己,延迟100ms*/
        vTaskDelay(xDelay100ms);
    }
}

// 例9 main()实现
int main(){
    /* 以优先级1创建一个任务1,任务参数不用传递NULL,任务句柄也不使用传NULL*/
    xTaskCreate(Task1, "Task1", 1000, NULL, 1, NULL);
    // 开启调度器
    vTaskStartScheduler();

    // main()不应该执行到这里,因为调度器应该在执行任务 */
    for(;;);
}

调度器算法

任务状态和事件

当前运行的任务处于运行态。单核处理器中一次只能有一个任务处于运行态。
当前没有运行,也不是阻塞和暂停态的任务,就是就绪态。它们可以被调度器选中进入运行态。调度器总是选择优先级最高的任务运行。
任务可以在等待一个事件处于阻塞态,当任务发生时它们会自动被移动到就绪态。暂停任务出现在特定的时间,比如,一个阻塞时间到期,而它通常用来实现周期任务或超时任务。同步任务发生在一个任务或中断服务用任务通知,队列,事件组或各种信号量发送信息。它们都通常用作同步信号,比如接收到一个硬件信息。

设置调度程序

调度器程序是用来决定选择哪个就绪任务执行的。调度器程序是可以使用FreeRTOS.h文件中的configUSE_PREEMPTION和configUSE_TIME_SLICING常量配置的。第三个配置常量configUSE_TICKLESS_IDLE也会影响调度器程序,它可以用来关闭tick中断很长一段时间。configUSE_TICKLESS_IDLE是一个最新提供的特殊选项,可以用电量最小化。假设它设置为0,如果配置文件没有定义,默认值也是0。FreeRTOS调度器所有可能的配置会确保任务共享一个优先级时会依次执行。依次执行是指循环调度。循环调度算法并不保证同等优先级任务执行时间相等。只有处于就绪态的相同优先级任务会被执行。

优先级任务抢占时间片

将configUSE_PREEMPTION和configUSE_TIME_SLICING都设置为1时,FreeRTOS调度器称作带时间片固定优先级抢占任务调度器,它是最多小RTOS程序所使用的调度器程序,之前所有的例子都用这种调度器。

  • 固定优先级:固定优先级调度器不会更改调度器调用的任务优先级,但也不会阻碍自身或其他任务更改优先级
  • 抢占:对于抢占调度器,一个比执行中任务优先级更高优先级任务进入就绪态,调度算法会立即发生抢占行为。被抢占意味着非自愿切出运行态,不会阻塞和方式运行而是变成就绪态,允许不同任务进入运行态
  • 时间片:时间片用于相同优先级任务共享处理器时间,即使任务没有主动放弃或者进入阻塞态。调度器会在每个时间片结束时选择一个新任务执行,如果有相同优先级任务处于就绪状态。一个时间片等于两个tick中断之间的时间
博主已关闭本页面的评论功能