1 单任务和多任务系统
1.1 单任务系统
单任务系统的编程方式,即裸机的编程方式,这种编程方式的框架一般都是在 main()函数中使用一个大循环,在循环中顺序地调用相应的函数以处理相应的事务,这个大循环的部分可以视为应用程序的后台, 而应用程序的前台,则是各种中断的中断服务函数。因此单任务系统也叫做前后台系统,前后台系统的运行示意图, 如下图所示:
从上图可以看出,前后台系统的实时性很差,因为大循环中函数处理的事务没有优先级之分, 必须是顺序地被执行处理的,不论待处理事务的紧急程度有多高,没轮到只能等着,虽然中断能够处理一些紧急的事务,但是在一些大型的嵌入式应用中,这样的单任务系统就会显得力不从心。
1.2 多任务系统
多任务系统在处理事务的实时性上比单任务系统要好得多,从宏观上来看,多任务系统的多个任务是可以“同时” 运行的,因此紧急的事务就可以无需等待 CPU 处理完其他事务,在被处理。
要注意的是多任务系统的多个任务可以“同时” 运行,是从宏观的角度而言的,对于单核的 CPU 而言, CPU 在同一时刻只能够处理一个任务,但是多任务系统的任务调度器会根据相关的任务调度算法,将 CPU 的使用权分配给任务,在任务获取 CPU 使用权之后的极短时间(宏观角度)后,任务调度器又会将 CPU 的使用权分配给其他任务,如此往复,在宏观的角度看来,就像是多个任务同时运行了一样。
多任务系统的运行示意图,如下图所示:
从上图可以看出, 相较于单任务系统而言,多任务系统的任务也是具有优先级的,高优先级的任务可以像中断的抢占一样,抢占低优先级任务的 CPU 使用权;优先级相同的任务则各自轮流运行一段极短的时间(宏观角度),从而产生“同时”运行的错觉。 以上就是抢占式调度和时间片调度的基本原理。
在任务有了优先级的多任务系统中,用户就可以将紧急的事务放在优先级高的任务中进行处理,那么整个系统的实时性就会大大地提高。
2 FreeRTOS 任务状态
FreeRTOS 中任务存在四种任务状态,分别为运行态、就绪态、阻塞态和挂起态。 FreeRTOS运行时,任务的状态一定是这四种状态中的一种,下面就分别来介绍一下这四种任务状态。
1.运行态
如果一个任务得到CPU 的使用权,即任务被实际执行时,那么这个任务处于运行态。如果
运行 RTOS 的 MCU 只有一个处理器核心,那么在任务时刻,都只能有一个任务处理运行态。
2. 就绪态
如果一个任务已经能够被执行(不处于阻塞态后挂起态),但当前还未被执行(具有相同优
先级或更高优先级的任务正持有 CPU 使用权),那么这个任务就处于就绪态。
3. 阻塞态
如果一个任务因延时一段时间或等待外部事件发生,那么这个任务就处理阻塞态。例如任
务调用了函数vTaskDelay(),进行一段时间的延时,那么在延时超时之前,这个任务就处理阻塞
态。任务也可以处于阻塞态以等待队列、信号量、事件组、通知或信号量等外部事件。通常情
况下,处于阻塞态的任务都有一个阻塞的超时时间,在任务阻塞达到或超过这个超时时间后,
即使任务等待的外部事件还没有发生,任务的阻塞态也会被解除。
要注意的是,处于阻塞态的任务是无法被运行的。
4. 挂起态
任务一般通过函数 vTaskSuspend()和函数 vTaskResums()进入和退出挂起态与阻塞态一样,
处于挂起态的任务也无法被运行。
四种任务状态之间的转换图如下图所示:
3 FreeRTOS 任务优先级
任务优先级是决定任务调度器如何分配 CPU 使用权的因素之一。 每一个任务都被分配一个0~(configMAX_PRIORITIES-1)的任务优先级,宏 configMAX_PRIORITIES 在 FreeRTOSConfig.h文件中定义
如果在 FreeRTOSConfig.h文件中,将宏 configUSE_PORT_OPTIMISED_TASK_SELECTION定义为 1,那么 FreeRTOS 则会使用特殊方法计算下一个要运行的任务,这种特殊方法一般是使用硬件计算前导零指令,对于 STM32 而言,硬件计算前导零的指令,最大支持 32 位的数,因此宏 configMAX_PRIORITIES 的值不能超过 32。当然,系统支持的优先级数量越多,系统消耗的资源也就越多,因此读者在实际的工程开发当中,应当合理地将宏 configMAX_PRIORITIES定义为满足应用需求的最小值。
FreeRTOS 的任务优先级高低与其对应的优先级数值,是成正比的,也就是说任务优先级数值为 0 的任务优先级是最低的任务优先级,任务优先级数值为(configMAX_PRIORITIES-1)的任务优先级是最高的任务优先级。 FreeRTOS 的任务优先级高低与其对应数值的逻辑关系正好与STM32 的中断优先级高低与其对应数值的逻辑关系相反,如下图所示,因此作为刚入门FreeRTOS 的读者,特别注意
4 Free RTOS 任务调度方式
FreeRTOS 一共支持三种任务调度方式,分别为抢占式调度、时间片调度和协程式调度。在 FreeRTOS 官方的在线文档中, FreeRTOS 官方对协程式调度做了特殊说明,说明如下图所示:
FreeRTOS 官方对协程式调度的特殊说明,翻译过来就是“协程式调度是用于一些资源非常少的设备上的,但是现在已经很少用到了。虽然协程式调度的相关代码还没有被删除,但是今后也不打算继续开发协程式调度。”
可以看出, FreeRTOS 官方已经不再开发协程式调度了,因此笔者并不推荐读者在开发中使用协程式调度。协程式调度是专门为资源十分紧缺的设备开发的,因此使用协程式调度也会有受到很多的限制,但是现在 MCU 的资源都已经十分富裕了,因此也就没有必要再使用和学习协程式调度了,本开发文档也就不再提供协程式调度的相关教程。
4.1 抢占式调度
抢占式调度主要时针对优先级不同的任务,每个任务都有一个优先级,优先级高的任务可以抢占优先级低的任务,只有当优先级高的任务发生阻塞或者被挂起,低优先级的任务才可以运行。
4.2 时间片调度
时间片调度主要针对优先级相同的任务,当多个任务的优先级相同时, 任务调度器会在每一次系统时钟节拍到的时候切换任务,也就是说 CPU 轮流运行优先级相同的任务,每个任务运行的时间就是一个系统时钟节拍。 有关系统时钟节拍的相关内容,在下文讲解 FreeRTOS 系统时钟节拍的时候会具体分析
5 FreeRTOS 任务控制块
FreeRTOS 中的每一个已创建任务都包含一个任务控制块,任务控制块是一个结构体变量,FreeRTOS 用任务控制块结构体存储任务的属性。
任务控制块的定义如以下代码所示:
typedef struct tskTaskControlBlock { /* 指向任务栈栈顶的指针 */ volatile StackType_t * pxTopOfStack; #if ( portUSING_MPU_WRAPPERS == 1 ) /* MPU 相关设置 */ xMPU_SETTINGS xMPUSettings; #endif /* 任务状态列表项 */ ListItem_t xStateListItem; /* 任务等待事件列表项 */ ListItem_t xEventListItem; /* 任务的任务优先级 */ UBaseType_t uxPriority; /* 任务栈的起始地址 */ StackType_t * pxStack; /* 任务的任务名 */ char pcTaskName[ configMAX_TASK_NAME_LEN ]; #if ( ( portSTACK_GROWTH > 0 ) || ( configRECORD_STACK_HIGH_ADDRESS == 1 ) ) /* 指向任务栈栈底的指针 */ StackType_t * pxEndOfStack; #endif #if ( portCRITICAL_NESTING_IN_TCB == 1 ) /* 记录任务独自的临界区嵌套次数 */ UBaseType_t uxCriticalNesting; #endif #if ( configUSE_TRACE_FACILITY == 1 ) /* 由系统分配(每创建一个任务,值增加一),分配任务的值都不同,用于调试 */ UBaseType_t uxTCBNumber; /* 由函数 vTaskSetTaskNumber()设置,用于调试 */ UBaseType_t uxTaskNumber; #endif #if ( configUSE_MUTEXES == 1 ) /* 保存任务原始优先级,用于互斥信号量的优先级翻转 */ UBaseType_t uxBasePriority; /* 记录任务获取的互斥信号量数量 */ UBaseType_t uxMutexesHeld; #endif #if ( configUSE_APPLICATION_TASK_TAG == 1 ) /* 用户可自定义任务的钩子函数用于调试 */ TaskHookFunction_t pxTaskTag; #endif #if ( configNUM_THREAD_LOCAL_STORAGE_POINTERS > 0 ) /* 保存任务独有的数据 */ void *pvThreadLocalStoragePointers[configNUM_THREAD_LOCAL_STORAGE_POINTERS]; #endif #if ( configGENERATE_RUN_TIME_STATS == 1 ) /* 记录任务处于运行态的时间 */ configRUN_TIME_COUNTER_TYPE ulRunTimeCounter; #endif #if ( configUSE_NEWLIB_REENTRANT == 1 ) /* 用于 Newlib */ struct _reent xNewLib_reent; #endif #if ( configUSE_TASK_NOTIFICATIONS == 1 ) /* 任务通知值 */ volatile uint32_t ulNotifiedValue[ configTASK_NOTIFICATION_ARRAY_ENTRIES ]; /* 任务通知状态 */ volatile uint8_t ucNotifyState[ configTASK_NOTIFICATION_ARRAY_ENTRIES ]; #endif #if ( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 ) /* 任务静态创建标志 */ uint8_t ucStaticallyAllocated; #endif #if ( INCLUDE_xTaskAbortDelay == 1 ) /* 任务被中断延时标志 */ uint8_t ucDelayAborted; #endif #if ( configUSE_POSIX_ERRNO == 1 ) /* 用于 POSIX */ int iTaskErrno; #endif } tskTCB; typedef struct tskTaskControlBlock * TaskHandle_t;
从上面的代码可以看出, FreeRTOS 的任务控制块结构体中包含了很多成员变量,但是,大部分的成员变量都是可以通过 FreeRTOSConfig.h 配置文件中的配置项宏定义进行裁剪的。
6 FreeRTOS 任务栈
不论是裸机编程还是 RTOS 编程,栈空间的使用的非常重要。函数中的局部变量、函数调用时的现场保护和函数的返回地址等都是存放在栈空间中的。
对于 FreeRTOS,当使用静态方式创建任务时,需要用户自行分配一块内存,作为任务的栈空间, 静态方式创建任务的函数原型如下所示:
TaskHandle_t xTaskCreateStatic(TaskFunction_t pxTaskCode, const char * const pcName, const uint32_t ulStackDepth, void * const pvParameters, UBaseType_t uxPriority, StackType_t * const puxStackBuffer, StaticTask_t * const pxTaskBuffer)
其中函数的参数 ulStackDepth,为任务栈的大小;参数 puxStackBuffer,为任务的栈的内存空间。 FreeRTOS 会根据这两个参数,为任务设置好任务的栈。
而使用动态方式创建任务时,系统则会自动从系统堆中分配一块内存,作为任务的栈空间,动态方式创建任务的函数原型如下所示:
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode, const char * const pcName, const configSTACK_DEPTH_TYPE usStackDepth, void * const pvParameters, UBaseType_t uxPriority, TaskHandle_t * const pxCreatedTask)
其中函数的参数usStackDepth,即为任务栈的大小。FreeRTOS会根据栈的大小,从FreeRTOS的系统堆中分配一块内存,作为任务的栈空间。
值得一提的是, 参数 usStackDepth 表示的任务栈大小,实际上是以字为单位的,并非以字节为单位。对于静态方式创建任务的函数 xTaskCreateStatic(), 参数 usStackDepth 表示的是作为任务栈且其数据类型为 StackType_t 的数组 puxStackBuffer 中元素的个数;而对于动态方式创建任务的函数 xTaskCreate(),参数 usStackDepth 将被用于申请作为任务栈的内存空间,其内存申请相关代码,如下所示:
pxStack = pvPortMallocStack((((size_t)usStackDepth) * sizeof(StackType_t)));
可以看出, 静态和动态创建任务时,任务栈的大小都与数据类型 StackType_t 有关, 对于STM32 而言,该数据类型的相关定义,如下所示:
#define portSTACK_TYPE uint32_t typedef portSTACK_TYPE StackType_t;
因此, 不论是使用静态方式创建任务还是使用动态方式创建任务, 任务的任务栈大小都应该为 ulStackDepth*sizeof(uint32_t)字节,即 ulStackDepth 字。