FreeRTOS多任务系统

简介: FreeRTOS多任务系统

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 字。


目录
相关文章
|
6月前
|
消息中间件 存储 算法
【软件设计师备考 专题 】操作系统的内核(中断控制)、进程、线程概念
【软件设计师备考 专题 】操作系统的内核(中断控制)、进程、线程概念
193 0
|
3月前
|
存储 算法 调度
深入理解操作系统:进程调度的算法与实现
【8月更文挑战第31天】在操作系统的核心,进程调度扮演着关键角色,它决定了哪个进程将获得CPU的使用权。本文不仅剖析了进程调度的重要性和基本概念,还通过实际代码示例,展示了如何实现一个简单的调度算法。我们将从理论到实践,一步步构建起对进程调度的理解,让读者能够把握操作系统中这一复杂而精妙的部分。
|
4月前
|
人工智能 分布式计算 物联网
操作系统的演变:从单任务到多任务和多线程
在数字时代的浪潮中,操作系统作为计算机硬件与软件之间的桥梁,经历了从简单到复杂的演进过程。初始的操作系统仅能执行单一任务,随着技术的进步,它们逐渐发展为能够同时处理多个任务和线程的系统。这一变化不仅提升了计算机的效率,也极大地促进了现代计算技术的发展。本文将深入探讨操作系统的关键发展阶段,分析其对现代计算技术的影响,并展望未来可能的发展趋势。
|
6月前
|
算法 API 调度
【FreeRTOS】多任务创建
【FreeRTOS】多任务创建
|
6月前
|
Linux 测试技术 调度
进程调度预备开发
进程调度预备开发
55 0
|
存储 算法 搜索推荐
操作系统实验四:进程调度
操作系统实验四:进程调度
145 0
|
消息中间件 算法 安全
RTOS实时操作系统中RT-Thread、FreeRTOS和uCOS 选择哪一个学习比较好?
RTOS实时操作系统中RT-Thread、FreeRTOS和uCOS 选择哪一个学习比较好?
|
算法 Linux 人机交互
第三章 处理机调度和死锁【操作系统】1
第三章 处理机调度和死锁【操作系统】1
106 0
|
算法 安全 Go
第三章 处理机调度和死锁【操作系统】2
第三章 处理机调度和死锁【操作系统】2
190 0
|
算法 调度
《操作系统》第二章 2.2处理机调度
《操作系统》第二章 2.2处理机调度