单任务系统的编程方式,即裸机的编程方式,这种编程方式的框架一般都是在main()函数中使用一个大循环,在循环中顺序的执行相应的函数以处理相应的事务,这个大循环的部分可以视为应用程序的后台,而应用程序的前台,则是各种中断的中断服务函数。因此单任务系统也叫做前后台系统,前后台系统的运行示意图如下:
从上图中可以看到,前后台系统的实时性很差,因为大循环中函数处理的事务没有优先级之分,必须是顺序地执行处理,不论待处理事务的紧急程度有多高,没轮到就只能等着,虽然中断可以处理一些紧急的事务,但是在大型嵌入式系统中,这样的单任务系统就会显得力不从心。
多任务系统在处理事务的实时性比单任务系统要好得多,从宏观上来看,多任务系统的多个任务是可以“同时”运行的,因此紧急事务就可以无需等待CPU处理其他事务,再被处理。
要注意的是,多任务系统的多个任务可以“同时”运行,是从宏观的角度而言的,对于单核CPU而言,CPU在同一时刻只能处理一个任务,但是多任务系统的任务调度器可以根据相关的任务调度算法,将CPU的使用权分给任务,在任务获得CPU使用权之后的极短时间(宏观角度)后,任务调度器又会将CPU的使用权分配给其他任务,如此往复,在宏观的角度看来,就像是多个任务同时运行一样。
多任务系统的运行示意图,如下:
从上图中可以看出,相较于单任务系统而言,多任务系统的任务也是具有优先级的,高优先级的任务可以像中断一样抢占,抢占低优先级任务的CPU使用权;优先级相同的任务则各自轮流运行一段极短时间,从而产生“同时”运行的错觉。以上就是抢占式调度和时间片轮转调度的基本原理。
在任务有了优先级的多任务系统中,用户就可以将紧急的事务放在优先级高的任务中进行处理,那么整个系统的实时性就会大大提高。
对于整个单片机程序,我们称之为application,应用程序。
使用FreeRTOS时,我们可以在application中创建多个任务(task),有些文档把任务也叫做线程(thread)。
以日常生活为例子,比如这个母亲要同时做两件事:
喂饭:这是一个任务
回信息:这是另一个任务
这可以引入很多概念
任务状态(state)
当前正在喂饭,它是running状态;另一个“回信息”就是not running状态
not running状态又可以细分为:
ready:就绪状态,随时可以运行
blocked:阻塞状态,卡住了,母亲在等待同时回复消息
suspended:挂起状态,同时废话太多,不管他了
优先级(priority)
我工作生活兼顾:喂饭,回信息优先级一样,轮流做
我忙里偷闲,还有空闲任务,休息一下
厨房着火,什么都别说,先灭火:优先级更高
栈(stack)
喂小孩,我要记得上一口喂了米饭,这口要喂青菜了
回信息时,我要记得刚才聊的是啥
做不同的任务,这些细节不一样
对于人来说,当然是记在脑子里
对于程序来说,是记在栈里
每个任务有自己的栈
事件驱动
孩子吃饭太慢,先休息一会,等他咽下去了,等他提醒我了,再喂下一口
协助式调度(co-operative scheduling)
你给同时回信息:
同事说:好了,你下去给小孩喂一口饭把,你才能离开
同事不放你走,即使孩子哭了你也不能走
你好不容易可以给孩子喂饭了
孩子说:好了,妈妈你去处理一下工作吧,你才能离开
孩子不放你走,即使同事连发信息你也不能走
FreeRTOS 中任务存在四种任务状态,分别为运行态、就绪态、阻塞态和挂起态。FreeRTOS 运行时,任务的状态一定是这四种状态中的一种,下面就分别来介绍一下这四种任务状态。
运行态
如果一个任务得到CPU的使用权,即任务被实际执行时,那么这个任务处于运行态。如果 运行 RTOS 的 MCU 只有一个处理器核心,那么在任务时刻,都只能有一个任务处于运行态 运行
就绪态
如果一个任务已经能够被执行(不处于阻塞态和挂起态),但当前还未被执行(具有相同优 先级或更高优先级的任务正持有 CPU 使用权),那么这个任务就处于就绪态。
阻塞态
如果一个任务因延时一段时间或等待外部事件发生,那么这个任务就处理阻塞态。例如任 务调用了函数 vTaskDelay(),进行一段时间的延时,那么在延时超时之前,这个任务就处理阻塞 态。任务也可以处于阻塞态以等待队列、信号量、事件组、通知或信号量等外部事件。通常情 况下,处于阻塞态的任务都有一个阻塞的超时时间,在任务阻塞达到或超过这个超时时间后, 即使任务等待的外部事件还没有发生,任务的阻塞态也会被解除。
要注意的是,处于阻塞态的任务是无法被运行的。
在日常生活的例子中,母亲在电脑前跟同事沟通时,如果同事一直没回复,那么母亲的工作就被卡住了、被堵住了、处于阻塞状态(Blocked)。重点在于:母亲在等待。
在实际产品中,我们不会让一个任务一直运行,而是使用"事件驱动"的方法让它运行:
任务要等待某个事件,事件发生后它才能运行
在等待事件过程中,它不消耗CPU资源
在等待事件的过程中,这个任务就处于阻塞状态(Blocked)
在阻塞态的任务可以等待两种类型的事件:
时间相关的事件
可以等待一段时间:我等2分钟
也可以一直等待,直到某个绝对时间:我等到下午3点
同步事件:这事件由别的任务,或者是中断程序产生
例子1:任务A等待任务B给他发送数据
例子2:任务A等待用户按下按键
同步事件的来源有很多
队列(queue)
二进制信号量(binary semaphores)
计数信号量(counting semaphores)
互斥量(mutexes)
递归互斥量(recursive mutexes)
事件组(event groups)
任务通知(task notifcations)
挂起态
任务一般通过函数 vTaskSuspend()和函数 vTaskResums()进入和退出挂起态,与阻塞态一样, 处于挂起态的任务也无法被运行。
任务优先级是决定任务调度器如何分配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是优先级最高的任务。
这里和STM32中断的优先级是刚好相反的,中断是优先级越高,对应的优先级数值越小。
FreeRTOS一共支持三种调度方式,分别为抢占式调度,时间片轮转调度和协程式调度。其中协程式调度是用于一些资源非常少的设备上的,但是现在已经很少用到了,虽然FreeRTOS源码保留了协程式的代码,但是官方已经不更新该部分代码了。
抢占式调度主要是针对优先级不同的任务,每个任务都有一个优先级,优先级高的任务可以抢占低优先级的任务,只有当高优先级的任务主动放弃CPU(阻塞,挂起),低优先级任务才可以运行。
时间片调度是主要针对优先级相同的任务,当多个任务的优先级相同时,任务调度器会在每次系统时钟节拍到的时候切换任务,也就是说CPU轮流运行优先级相同的任务,每个任务运行的时间就是一个系统节拍。
FreeRTOS中的每个已创建的任务都包含一个任务控制块,任务控制块是一个结构体变量,FreeRTOS用任务控制块结构体存储任务的属性(名字,栈,入口函数)等,可以理解为任务控制块就是任务的“身份证”。
任务控制块的代码如下所示:
typedef struct tskTaskControlBlock /* The old naming convention is used to prevent breaking kernel aware debuggers. */
{
volatile StackType_t * pxTopOfStack; /*< 指向任务栈栈顶的指针*/
#if ( portUSING_MPU_WRAPPERS == 1 )
xMPU_SETTINGS xMPUSettings; /*< MPU相关设置 */
#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; /*< Points to the highest valid address for the stack. */
#endif
#if ( portCRITICAL_NESTING_IN_TCB == 1 )
/*记录任务独自的临界区嵌套次数*/
UBaseType_t uxCriticalNesting; /*< Holds the critical section nesting depth for ports that do not maintain their own count in the port layer. */
#endif
#if ( configUSE_TRACE_FACILITY == 1 )
UBaseType_t uxTCBNumber; /*< 由系统分配,每创建一个任务,值增加1,分配任务的值都不同,用于调试 */
UBaseType_t uxTaskNumber; /*< 由函数vTaskSetTaskNumber()设置,用于调试*/
#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; /*< Stores the amount of time the task has spent in the Running state. */
#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
/* See the comments in FreeRTOS.h with the definition of
* tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE. */
#if ( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 ) /*lint !e731 !e9029 Macro has been consolidated for readability reasons. */
uint8_t ucStaticallyAllocated; /*< 任务静态创建标志 */
#endif
#if ( INCLUDE_xTaskAbortDelay == 1 )
/*任务被中断延时标志*/
uint8_t ucDelayAborted;
#endif
#if ( configUSE_POSIX_ERRNO == 1 )
/*用于POSIX*/
int iTaskErrno;
#endif
} tskTCB;
从上面的代码中可以看到,FreeRTOS的任务控制块结构体中包含了很多成员变量,但是大部分成员变量都是可以通过FreeRTOSConfig.h配置文件中的配置文件中的配置项宏定义进行裁剪的。
不论是裸机编程还是RTOS编程,栈空间的使用都非常重要。函数中的局部变量、函数调用关系时的现场保护和函数的返回地址等都是存放在栈空间的。
对于FreeRTOS,当使用静态方式创建任务时,需要用户自行分配一块内存,作为任务的栈空间,静态创建任务的函数原型如下:
TaskHandle_t xTaskCreateStatic( TaskFunction_t pxTaskCode,
const char * const pcName, /*lint !e971 Unqualified char types are allowed for strings and single characters only. */
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字。