FreeRTOS任务和协程简介及实现

描述

概述

“任务”的特征

简单来说,FreeRTOS实时系统能够创建多个独立的任务,任务之间互不干扰。任务创建之后并不是一起运行的,而是通过优先级顺序进行任务的调用,和调度也没有依赖关系。所以不管什么时候程序只能执行一个任务,只有当该任务执行完成或者被打断才能执行下一个任务。具体应该执行那个任务是由调度器来进行负责,因此RTOS可以重复的启动和停止每个任务。这里RTOS调度器为了确保处理器在进行任务交换时的环境(寄存器、堆栈内容)与交换之后的任务是完全相同。

因此,为了这一点的实现,每个任务都应该有自己的堆栈空间。当任务进行切换,执行环境则保存到该任务的堆栈中,所以,当一段时间后切换回该任务,它能够精确地回复上次工作时的状态。

任务总结

简单

没有使用限制

支持全部的抢占优先级

完全优先

X每个任务都有自己的堆栈,会导致RAM的使用空间增加

若是使用优先级,应该考虑优先级的问题

协程特征

在使用协程时,应该注意协程时为了非常小的设备实现的,现在已经很少在实际中应用。

虽然这些代码并没有删除,但是官方目前也没有进一步开发的打算。因此,如果你使用了,应该注意一些。

协程其实和任务差不多,但是还是有一些区别的:

比如以下这几点:

1.堆栈

协程是没有堆栈分配的,是所有创建的协程共同使用一个堆栈空间,这相比于任务来说,减少了RAM的使用空间。

调度和优先级

协程使用协同调度,但是可以包含在使用的抢占优先级之中。

宏定义

协程例程实现是通过一组宏提供的。

条件限制

RAM使用量的减少是以在如何构建协程方面的一些严格限制为代价的。

协程总结

在协程之间共享堆栈会大大降低RAM使用量。

协程操作使再入问题变得不那么严重。

跨架构的可移植性很强。

完全优先级相对于其他协程,但如果两者混合,总是可以被任务抢占。

缺乏堆栈需要特别考虑。

API调用的位置限制。

协程操作只在协程之间进行。

1-任务

1.1 任务状态

任务可以是以下几种状态中的一种:

1.1.1 运行

当任务实际执行时,它被称为处于正在运行状态。它当前正在使用处理器。 如果运行RTOS的处理器只有一个内核,那么 在任何给定时间只能是一个处于“正在运行”状态的任务。

1.1.2 就绪(准备)

就绪任务是那些能够执行的任务(它们没有处于阻塞或挂起状态),但目前没有执行,因为一个相同或更高优先级的不同任务已经处于运行状态。

1.1.3 阻塞

如果一个任务正在等待一个临时事件或外部事件,则该任务被称为处于阻塞状态。例如,如果一个任务调用vTaskDelay(),它将阻塞(被置于阻塞状态),直到延迟时间结束(一个临时事件)。任务也可以阻塞来等待队列、信号量、事件组、通知或信号量事件。处于阻塞状态的任务通常有一个“超时”时间,过了这个时间任务就会超时并被取消阻塞,即使任务等待的事件还没有发生。

处于“阻塞”状态的任务不占用任何处理时间,并且不能被选泽进入“运行中”的状态。

1.1.4 挂起

与“阻塞”状态的任务一样,处于“挂起”状态的任务不能被选择进入“正在运行”状态,但“挂起”状态的任务没有超时时间。相反,只有分别通过vTaskSuspend()和xTaskResume() 的API调用显式地命令任务进入或退出Suspended状态时,任务才会进入或退出Suspended状态。

图1是任务状态转换图:

FreeRTOS

2- 任务优先级

每个任务分配一个从0到(configMAX_PRIORITIES - 1)的优先级,其中configMAX_PRIORITIES是在FreeRTOSConfig.h中定义的(后面的章节会说一下这个头文件)。

如果正在使用的端口实现了端口优化的任务选择机制,该机制使用'计数前导零'类型的指令(用于单个指令中的任务选择),并且configUSE_PORT_OPTIMISED_TASK_SELECTION在FreeRTOSConfig.h中设置为1,那么configMAX_PRIORITIES不能超过32。在其他情况下,configMAX_PRIORITIES可以在合理范围内取任何值(由于用到RAM空间,因此在使用时,尽可能的保持实际需要的空间大小需求)。

FreeRTOS优先级设置是

数字越大优先级越高

。空任务的优先级是0(tskIDLE_PRIORITY)(注意:不同的系统优先级不同,有的OS是数字越小优先级越高,这点要注意一下)。

FreeRTOS调度器确保处于就绪或运行状态的任务总是优先于同样处于就绪状态的低优先级任务,优先获得处理器(CPU)时间。换句话说,处于运行状态的任务始终是运行优先级最高的任务。

不管多少个任务都可以共享相同的优先级。如果没有定义configUSE_TIME_SLICING,或者configUSE_TIME_SLICING设置为1,那么具有相同优先级的就绪状态任务将使用时间切片轮询调度方式共享可用的处理时间。

3-任务调度

3.1 RTOS调度(单核)

在默认情况下,FreeRTOS使用的是固定优先级抢占方式,对相同优先级的任务进行时间切换轮询方式。

“固定优先级”意味着调度器不会永久更改任务的优先级,尽管它可能由于优先级继承而临时提高任务的优先级。

“抢占式”意味着调度程序总是运行最高优先级RTOS任务,不管这个任务是什么时间可以运行。例如,如果中断服务例程(ISR)更改了能够运行的最高优先级任务,调度器将停止当前运行的低优先级任务并启动高优先级任务——即使这发生在一个时间片内。在这种情况下,低优先级任务被高优先级任务“抢占”了。

“循环”是指具有相同优先级的任务轮流进入运行状态。

时间切片”意味着调度程序将在每个tick中断上在同等优先级的任务之间切换——tick中断之间的时间是一个时间切片(tick中断是RTOS用来测量时间的周期性中断)。

在使用抢占优先级调度程序时,应当避免任务互斥。

始终运行最高优先级任务的后果是,永远不会进入阻塞或挂起状态的高优先级任务将永久阻断所有低优先级任务的任何执行时间。这就是为什么最好创建事件驱动的任务的原因之一。例如,如果一个高优先级的任务正在等待一个事件,那么它就不应该处于该事件的循环(轮询)中,因为通过轮询,它始终在运行,因此永远不会处于阻塞或挂起状态。相反,任务应该进入阻塞状态来等待事件。可以使用众多FreeRTOS任务间通信和同步之一将事件发送给任务。接收到事件后,优先级更高的任务会自动从阻塞状态中移除。当高优先级任务处于阻塞状态时,低优先级任务将运行。

3.1.1配置RTOS调度策略

配置RTOS调度一般是在FreeRTOSConfig.h,当然你也可以在其他文件设置,但是这里不建议这么操作。

下面这些是更改默认时间调度的配置:

configUSE_PREEMPTION

如果configUSE_PREEMPTION为0,则抢占关闭,只有在运行状态任务进入阻塞或挂起状态、运行状态任务调用或中断服务例程(ISR)手动请求切换才会发生任务切换。

configUSE_TIME_SLICING

若configUSE_TIME_SLICING为0,则关闭时间切片,因此调度器不会在每个tick中断中在同等优先级的任务之间切换。

3.2 FreeRTOS AMP调度策略

使用FreeRTOS的非对称多处理(AMP)是指多核设备的每个内核运行自己独立的FreeRTOS实例。这些内核并不都需要具有相同的体系结构,但如果FreeRTOS实例需要彼此通信,则需要共享一些内存。

每个内核都运行自己的FreeRTOS实例,因此在任何给定的内核上的调度算法与上面描述的单核系统完全相同。可以使用流或消息缓冲区作为核间通信原语,以便一个核上的任务可以进入Blocked状态,以等待来自另一个核的数据或事件发送。

3.3 FreeRTOS SMP调度策略

使用FreeRTOS的对称多处理(SMP)是指FreeRTOS的一个实例跨多个处理器内核调度RTOS任务。由于FreeRTOS只有一个实例在运行,所以一次只能使用FreeRTOS的一个端口**,因此每个内核必须具有相同的处理器架构并共享相同的内存空间。**

FreeRTOS SMP调度策略使用与单核调度策略相同的算法,但与单核和AMP场景不同的是,SMP在任何给定时间会导致多个任务处于Running状态(每个内核有一个Running状态任务)。这意味着,只有在没有高优先级任务可以运行时,才会运行低优先级任务的假设不再成立。要理解其中的原因,就要考虑最初有一个高优先级任务和两个中等优先级任务都处于Ready状态时,SMP调度器将如何选择在双核微控制器上运行的任务。调度器需要选择两个任务,每个内核对应一个任务。首先,高优先级任务是能够运行的最高优先级任务,因此它将被选中用于第一个内核。这样就剩下两个中等优先级的任务作为能够运行的最高优先级的任务,因此会为第二个内核选择一个。结果是高优先级和中等优先级的任务同时运行。

3.3.1 配置SMP RTOS调度策略

下面的配置项有助于将为单核或AMP RTOS配置编写的代码移动到SMP RTOS配置,当这些代码依赖于这样一个假设:如果有一个高优先级的任务能够运行,那么低优先级的任务将不会运行。

configRUN_MULTIPLE_PRIORITIES

在FreeRTOSConfig.h文件中,如果configRUN_MULTIPLE_PRIORITIES设置为0,那么调度器将支持同时运行具有相同优先级的多个任务。这可能会修复假定一次只运行一个任务的代码,但代价是失去SMP配置的一些好处。

configUSE_CORE_AFFINITY

在FreeRTOSConfig.h文件中 configUSE_CORE_AFFINITY被设置为1 ,那么可以使用vTaskCoreAffinitySet() API函数来定义一个哪些内核任务可以运行,哪些内核任务不运行,使用这种方法,可以防止两个任务同时执行,让他们对各自的执行顺序进行判断。

4-任务实现

4.1 任务执行

一个任务应该有以下结构:

1void vATaskFunction( void *pvParameters )
 2{
 3for( ;; )
 4{
 5-- Task application code here. --
 6}
 7
 8/* Tasks must not attempt to return from their implementing
 9function or otherwise exit. In newer FreeRTOS port
10attempting to do so will result in an configASSERT() being
11called if it is defined. If it is necessary for a task to
12exit then have the task call vTaskDelete( NULL ) to ensure
13its exit is clean. */
14vTaskDelete( NULL );
15}

TaskFunction_t类型被定义为一个返回void并将void指针作为唯一形参的函数。实现一个任务的所有函数都应该是这种类型。可以使用该参数将任何类型的信息传递到任务中—这可以通过几个标准的演示应用程序任务进行演示。(具体演示代码请查看文件夹下的演示例程)

如下演示代码:

1/* main_full() is called from main() if the #define in main.c is set to create
 2the comprehensive demo, rather than simple blinky demo. */
 3int main_full( void )
 4{
 5/* Setup the microcontroller hardware for the demo. */
 6prvSetupHardware();
 7
 8/* Create the common demo application tasks, for example: */
 9vStartInterruptQueueTasks();
10vStartMessageBufferAMPTasks()
11vCreatePollQTasks();
12Etc.
13
14/* Create any tasks defined within main.c itself, or otherwise specific to the
15demo being built. */
16xTaskCreate( vCheckTask, "check", STACK_SIZE, NULL, TASK_PRIORITY, NULL );
17Etc.
18
19/* Start the RTOS scheduler, this function should not return as it causes the
20execution context to change from main() to one of the created tasks. */
21vTaskStartScheduler();
22
23/* Should never get here! */
24return 0;
25}

任务函数不应该返回,因此通常作为连续循环实现。通常最好创建事件驱动的任务,这样就不会占用低优先级任务的处理时间,如下结构:

1void vATaskFunction( void *pvParameters )
 2{
 3for( ;; )
 4{
 5/* Psudeo code showing a task waiting for an event 
 6with a block time. If the event occurs, process it. 
 7If the timeout expires before the event occurs, then 
 8the system may be in an error state, so handle the
 9error. Here the pseudo code "WaitForEvent()" could 
10replaced with xQueueReceive(), ulTaskNotifyTake(), 
11xEventGroupWaitBits(), or any of the other FreeRTOS 
12communication and synchronisation primitives. */
13if( WaitForEvent( EventObject, TimeOut ) == pdPASS )
14{
15-- Handle event here. --
16}
17else
18{
19-- Clear errors, or take actions here. --
20}
21}
22
23/* As per the first code listing above. */
24vTaskDelete( NULL );
25}

具体的请查看例程代码尝试下面的这几个函数:

通过调用xTaskCreate()或xTaskCreateStatic()创建任务,通过调用vTaskDelete()删除任务。

4.2 任务宏的创建

任务函数可以使用portTASK_FUNCTION和portTASK_FUNCTION_PROTO宏来定义。提供这些宏是为了允许编译器把特定的语法分别添加到函数定义和原型中。它们的使用不是必需的,除非你使用的端口的文档中特别说明。

上面的原型可以写成下面这样:

1void vATaskFunction( void *pvParameters );
 2Or,
 3portTASK_FUNCTION_PROTO( vATaskFunction, pvParameters );
 4Likewise the function above could equally be written as:
 5portTASK_FUNCTION( vATaskFunction, pvParameters )
 6{
 7for( ;; )
 8{
 9-- Task application code here. --
10}
11}

当然具体的任务宏定义是需要根据你实现的功能函数进行任务宏定义的。

2-协程

2.1-协同状态

协程仅适用于对RAM有限制的处理器,一般情况下32位MCU是不会使用的(在这里还是给大家说一下,基础理论知识就全点)。

2.1.1 运行

当协程实际执行时,我们称其处于运行状态。当前处理器正在工作。

2.1.2 就绪

就绪的协程是那些能够执行(它们没有被阻塞)但目前没有执行的。协程可能处于Ready状态,一是 另一个具有同等或更高优先级的协程已经处于Running状态 ;二是任务处于Running状态(只有当应用程序同时使用任务和协程时,才会出现这种情况)。

2.1.3 阻塞

如果一个协程当前正在等待一个临时或外部事件,那么它就被称为处于Blocked状态。例如,如果一个协程调用crDELAY(),它将阻塞(被置于阻塞状态),直到延迟周期超时,一个临时事件。阻塞的协程无法用于调度。

下面是协程的通信图:

FreeRTOS

2.2 协程的实现

协程的结构如下:

1void vACoRoutineFunction( CoRoutineHandle_t xHandle,
 2UBaseType_t uxIndex )
 3{
 4crSTART( xHandle );
 5
 6for( ;; )
 7{
 8-- Co-routine application code here. --
 9}
10
11crEND();
12}

类型crCOROUTINE_CODE被定义为一个返回void并将CoRoutineHandle_t和索引作为参数的函数。实现协程的所有函数都应该是这种类型的(上面的这段代码)。

协程的创建是通过xCoRoutineCreate()来进行的。

注意:

所有协程函数都必须以调用crSTART()开始。

所有协程函数都必须以对crEND()的调用结束。

协程函数不应该返回,因此通常作为连续循环实现。

可以从单个协程函数创建许多协程。

提供 uxIndex 参数是为了区分此类 协程。

2.3-协程优先级

协程的优先级从0到(configMAX_CO_ROUTINE_PRIORITIES - 1)。configMAX_CO_ROUTINE_PRIORITIES在FreeRTOSConfig.h中定义,可以在应用程序的基础上设置。优先级和任务一样数字越大优先级越高。

协程优先级只与其他协程相关。如果在同一个应用程序中混合了任务和协程,那么任务的优先级始终比协程的优先级高。

2.4- 协程调度

协程是通过重复调用vCoRoutineSchedule() 来调度的。调用 vCoRoutineSchedule() 的最佳位置是从空闲任务钩子。即使您的应用程序只使用协程,也会出现这种现象,因为空闲任务仍将在调度程序启动时自动创建。

2.5- 协程和任务的混合

在空间任务中调度协程是允许任务和协程的混合的。这种方式只有当协程优先级低于空闲任务的优先级时,这种方式下才能执行。

2.5.1-局限性和复杂性

和任务相比来说,协程的好处是降低了RAM的使用空间,但是这些也会带来一定的限制。比如局限性以及复杂性。

2.5.2堆栈共享

当协程阻塞时,携程的堆栈时不会被保存的,这样即使在堆栈上有再多的变量,也会丢失。因此为了能够解决这个问题,需要声明一个在阻塞时保持调用的变量,作为静态。例如:

1void vACoRoutineFunction( CoRoutineHandle_t xHandle,
 2UBaseType_t uxIndex )
 3{
 4static char c = 'a';
 5
 6// Co-routines must start with a call to crSTART().
 7crSTART( xHandle );
 8
 9for( ;; )
10{
11// If we set c to equal 'b' here ...
12c = 'b';
13
14// ... then make a blocking call ...
15crDELAY( xHandle, 10 );
16
17// ... c will only be guaranteed to still 
18// equal 'b' here if it is declared static
19// (as it is here).
20}
21
22// Co-routines must end with a call to crEND().
23crEND();
24}

堆栈共用的另一种结果是,可能导致协程阻塞API函数的调用只能来自于协程函数本身,而不是协程函数调用的函数。

1例如:
 2void vACoRoutineFunction( CoRoutineHandle_t xHandle, UBaseType_t uxIndex )
 3{
 4// Co-routines must start with a call to crSTART().
 5crSTART( xHandle );
 6
 7for( ;; )
 8{
 9// It is fine to make a blocking call here,
10crDELAY( xHandle, 10 );
11
12// but a blocking call cannot be made from within
13// vACalledFunction().
14vACalledFunction();
15}
16
17// Co-routines must end with a call to crEND().
18crEND();
19}
20
21void vACalledFunction( void )
22{
23// Cannot make a blocking call here!
24}

2.5.3 switch语句的应用

FreeRTOS文件中包含的默认协程实现不允许从switch语句中进行阻塞调用。

1void vACoRoutineFunction( CoRoutineHandle_t xHandle, UBaseType_t uxIndex )
 2{
 3// Co-routines must start with a call to crSTART().
 4crSTART( xHandle );
 5
 6for( ;; )
 7{
 8// It is fine to make a blocking call here,
 9crDELAY( xHandle, 10 );
10
11switch( aVariable )
12{
13case 1 : // Cannot make a blocking call here!
14break;
15default: // Or here!
16}
17}
18
19// Co-routines must end with a call to crEND().
20crEND();
21}

2.6协程例子

2.6.1 创建LED协程

1void vFlashCoRoutine( CoRoutineHandle_t xHandle,
 2UBaseType_t uxIndex )
 3{
 4// Co-routines must start with a call to crSTART().
 5crSTART( xHandle );
 6
 7for( ;; )
 8{
 9// Delay for a fixed period.
10crDELAY( xHandle, 10 );
11
12// Flash an LED.
13vParTestToggleLED( 0 );
14}
15
16// Co-routines must end with a call to crEND().
17crEND();
18}

2.6.2 协程调度

协同例程是通过重复调用 vCoRoutineSchedule()来调度的。执行此操作的最佳位置是从空闲任务中编写空闲任务挂钩。首先确保在FreeRTOSConfig.h 中将configUSE_IDLE_HOOK设置为 1。然后将空闲任务钩子编写为:

1void vApplicationIdleHook( void )
 2{
 3vCoRoutineSchedule( void );
 4}
 5或者,如果空闲任务不执行任何其他函数,从循环中调用vCoRoutineSchedule()会更有效:
 6void vApplicationIdleHook( void )
 7{
 8for( ;; )
 9{
10vCoRoutineSchedule( void );
11}
12}

2.6.3 创建协程并启动RTOS调度

1#include "task.h"
 2#include "croutine.h"
 3
 4#define PRIORITY_0 0
 5
 6void main( void )
 7{
 8// In this case the index is not used and is passed 
 9// in as 0.
10xCoRoutineCreate( vFlashCoRoutine, PRIORITY_0, 0 );
11
12// NOTE: Tasks can also be created here!
13
14// Start the RTOS scheduler.
15vTaskStartScheduler();
16}

2.6.4 扩展

现在假设我们要从同一个函数创建 8 个这样的协程。每个协程将闪烁不同的 LED 以不同的速率显示。index 参数可用于区分协程函数本身。

1#include "task.h"
 2#include "croutine.h"
 3
 4#define PRIORITY_0 0
 5#define NUM_COROUTINES 8
 6
 7void main( void )
 8{
 9int i;
10
11for( i = 0; i < NUM_COROUTINES; i++ )
12{
13// This time i is passed in as the index.
14xCoRoutineCreate( vFlashCoRoutine, PRIORITY_0, i );
15}
16
17// NOTE: Tasks can also be created here!
18
19// Start the RTOS scheduler.
20vTaskStartScheduler();
21}
22还扩展了协程功能,因此每个程序都使用不同的LED和闪光率。
23const int iFlashRates[ NUM_COROUTINES ] = { 10, 20, 30, 40, 50, 60, 70, 80 };
24const int iLEDToFlash[ NUM_COROUTINES ] = { 0, 1, 2, 3, 4, 5, 6, 7 }
25
26void vFlashCoRoutine( CoRoutineHandle_t xHandle, UBaseType_t uxIndex )
27{
28// Co-routines must start with a call to crSTART().
29crSTART( xHandle );
30
31for( ;; )
32{
33// Delay for a fixed period. uxIndex is used to index into
34// the iFlashRates. As each co-routine was created with
35// a different index value each will delay for a different
36// period.
37crDELAY( xHandle, iFlashRate[ uxIndex ] );
38
39// Flash an LED. Again uxIndex is used as an array index,
40// this time to locate the LED that should be toggled.
41vParTestToggleLED( iLEDToFlash[ uxIndex ] );
42}
43
44// Co-routines must end with a call to crEND().
45crEND();
46}
打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分