1.多任务系统
RTOS 系统本质上的目的就是为了解决任务管理的问题,FreeRTOS也是如此。我们使用FreeRTOS本质上也是为了处理多任务,初步上手 RTOS 系统首先必须掌握的也是任务的创建、删除、挂起和恢复等操作,由此可见任务管理的重要性。本章是后面学习的基础,所以一定要掌握本章关于 FreeRTOs 任务管理的基础知识。
在前面的学习中,我们的开发都是裸机开发,把所有要做的事情都放在一个while(1)循环中,也在中断当中处理一些事情,这样简单的系统实际上 就是单任务系统,也叫做前后台系统,其中中断作为前台程序,大循环while(1)作为后台程序。
前后台系统最大的问题在于它的实时性差,无论前台程序还是后台程序实际上都是在排着队执行,中断程序也只用中断的优先级管理,无论多紧急的任务都只能等着。这种系统结构简单,成本低,只适合简易的任务简单的场景下,再任务量大复杂的场景下就显得捉襟见肘了。
多任务系统实际上就是把每个应用任务划分为很多的小任务,并发地处理这些小任务,每个任务执行的时间很短(占用CPU时间短),看起来像是同一时刻执行了很多个任务一样。
单多个任务带来了一个新的问题,即任务优先级问题,为了解决这个问题RTOS设计了任务调度器,不同的系统其任务调度器的实现方法也不同, FreeRTOS 是一个抢占式的实时多任务系统,他的任务调度器也是抢占式的。
其任务调度流程如下:
高优先级的任务可以打断低优先级任务的运行而取得 CPU 的使用权,这样就保证了那些紧急任务的运行。这样我们就可以为那些对实时性要求高的任务设置一个很高的优先级,比如自动驾驶中的障碍物检测任务等。高优先级的任务执行完成以后重新把 CPU 的使用权归还给低优先级的任务,这个就是抢占式多任务系统的基本原理。
2.任务
1.什么是任务
在使用 RTOS 的时候一个实时应用可以作为一个独立的任务。每个任务都有自己的运行环境,不依赖于系统中其他的任务或者 RTOS 调度器。任何一个时间点只能有一个任务运行,具体运行哪个任务是由 RTOS 调度器来决定的,RTOS 调度器因此就会重复的开启、关闭每个任务。任务不需要了解 RTOS 调度器的具体行为,RTOS 调度器的职责是确保当一个任务开始执行的时候其上下文环境(寄存器值,堆栈内容等)和任务上一次退出的时候相同。为了做到这一点,每个任务都必须有个堆栈,当任务切换的时候将上下文环境保存在堆栈中,这样当任务再次执行的时候就可以从堆栈中取出上下文环境,任务恢复运行。
在FreeRTOS下还有种特殊的任务叫协程,这里我们就不详细介绍了。
任务(Task)作为操作系统的基本执行单位,其本身具有以下特点:
- 独立性:每个任务在 FreeRTOS 中是独立的执行实体,拥有自己的程序计数器、堆栈空间和寄存器上下文。这种独立性使得任务可以独立执行,互不干扰。
- 任务函数:任务是由任务函数定义的,这个函数是一个无限循环的结构。任务函数在循环中执行任务逻辑,通常包含等待或阻塞机制,以便让出 CPU 资源。
- 优先级分配:每个任务在创建时都可以分配一个优先级,优先级决定了任务的执行顺序。高优先级的任务在调度中优先级更高,可以抢占低优先级的任务。
- 可变堆栈大小:每个任务在创建时可以分配不同大小的堆栈。这允许根据任务的复杂度和需要灵活分配内存资源。堆栈大小的选择应平衡内存使用和避免堆栈溢出。
- 状态转换:任务可以处于多种状态:运行(Running)、就绪(Ready)、阻塞(Blocked)、挂起(Suspended)和删除(Deleted)。任务状态由调度器和系统事件决定,并根据这些状态进行任务切换。
- 阻塞和延时:任务可以通过使用 FreeRTOS 提供的 API(如 vTaskDelay 和 xQueueReceive 等)进入阻塞状态。阻塞状态下的任务不会消耗 CPU 资源,直到满足解除阻塞的条件为止。
- 任务通知和事件:任务可以使用 FreeRTOS 的任务通知机制,这是一种轻量级的信号机制,可以用于任务间的简单通信和同步。此外,任务还可以等待队列、信号量和事件组等同步原语。
- 任务删除:任务可以在执行过程中被删除。删除任务时,其分配的堆栈和其他资源会被释放。这可以通过调用 vTaskDelete 函数来实现。
- 挂起和恢复:任务可以被挂起,暂停其执行,直到被恢复。挂起的任务不会被调度器选中执行。
- 协作式调度:除了抢占式调度,FreeRTOS 也支持协作式调度,在这种模式下,任务主动让出 CPU 控制权。这适用于资源受限或任务执行周期已知的场合。
2.任务的状态转换
FreeRTOS任务的状态转换关系如图所示,可以看出来和我们之前所学的进程的状态转换比较相似
FreeRToS 中的任务永远处于下面几个状态中的某一个:
- 运行态:当一个任务正在运行时,那么就说这个任务处于运行态,处于运行态的任务就是当前正在使用处理器的任务。如果使用的是单核处理器的话那么不管在任何时刻永远都只有一个任务处于运行态
- 就绪态:处于就绪态的任务是那些已经准备就绪(这些任务没有被阻塞或者挂起),可以运行的任务但是处于就绪态的任务还没有运行,因为有一个同优先级或者更高优先级的任务正在运行
- 阻塞态:如果一个任务当前正在等待某个外部事件的话就说它处于阻塞态,比如说如果某个任务调用了函数 vTaskDelay(的话就会进入阻塞态,直到延时周期完成。任务在等待队列、信号量、事件组、通知或互斥信号量的时候也会进入阻塞态。任务进入阻塞态会有一个超时时间,当超过这个超时时间任务就会退出阻塞态,即使所等待的事件还没有来临
- 挂起态:像阻塞态一样,任务进入挂起态以后也不能被调度器调用进入运行态,但是进入挂起态的任务没有超时时间。任务进入和退出挂起态通过调用函数vTaskSuspend()和 xIaskResume()。
3.任务的优先级
这部分内容同学们都较为熟悉了,FreeRTOS每个人物都会分配一个0至configMAX_PRIORITIES-1之间的优先级,只有当如果所使用的硬件平台支持类似计算前导零这样的指令(可以通过该指令选择下一个要运行的任务,Cortex-M处理器是支持该指令的),并且宏COnfgUSE_PORT_OPTIMISED_TASK_SELECTION也设置为了1,那么宏configMAX_PRIORITIES不能超过 32,也就是优先级不能超过 32 级。其他情况下宏configMAX PRIORITIES 可以为任意值,但是考虑到 RAM 的消耗,一般来说宏值会尽量取一个小值
优先级数字越低表示任务的优先级越低,0的优先级最低,confgMAX_PRIORITIES-1的优先级最高。空闲任务的优先级最低,为0。
FreeRTOS 调度器确保处于就绪态或运行态的高优先级的任务获取处理器使用权,换句话说就是处于就绪态的最高优先级的任务才会运行。当宏 confgUSE_TIME_SLICING 定义为1的时候多个任务可以共用一个优先级,数量不限。默认情况下该宏在文件FreeRTOs.h中已经定义为1。此时处于就绪态的优先级相同的任务就会使用时间片轮转调度器获取运行时间。
4.任务的实现
无论是使用哪种方式创建任务,他们的第一个参数都是pxTaskCode,即任务函数,说白了就是这个任务要完成什么功能。
官方的任务函数模板如下:
void vATaskFunction(void *pvParameters)
{
for(;;)
{
//任务程序
vTAskDelay(); //挂起
}
vTaskDelete(NULL)
}
需要注意的是,不能从任务函数中返回或者退出,从任务函数中返回或者退出都会调用configASSERT(),但是可以在任务函数中调用vTaskDelete(NULL)来删除任务
5.任务控制块
FreeRTOS 的每个任务都有一些属性需要存储,FreeRTOS 把这些属性集合到一起用一个结构体来表示,这个结构体叫做任务控制块:TCB_t,在使用函数 xTaskCreate()创建任务的时候就会自动的给每个任务分配一个任务控制块。此结构体在文件 tasks.c中有定义,任务控制块的内容是比较多的,但大部分内容与裁剪有关,不使用这部分功能的时候可以减小它的大小,具体实现如下:
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; /*任务栈栈底*/
#endif
#if ( portCRITICAL_NESTING_IN_TCB == 1 )
UBaseType_t uxCriticalNesting; /*临界区嵌套深度 */
#endif
#if ( configUSE_TRACE_FACILITY == 1 )
UBaseType_t uxTCBNumber;
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; /*< Stores the amount of time the task has spent in the Running state. */
#endif
#if ( ( configUSE_NEWLIB_REENTRANT == 1 ) || ( configUSE_C_RUNTIME_TLS_SUPPORT == 1 ) )
configTLS_BLOCK_TYPE xTLSBlock; /*< Memory block used as Thread Local Storage (TLS) Block for the task. */
#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 ) /*标志任务的动静态,如果是静态的变量为pdTURE*/
uint8_t ucStaticallyAllocated;
#endif
#if ( INCLUDE_xTaskAbortDelay == 1 )
uint8_t ucDelayAborted;
#endif
#if ( configUSE_POSIX_ERRNO == 1 )
int iTaskErrno;
#endif
} tskTCB;
6.任务堆栈
FreeRTOS 之所以能正确的恢复一个任务的运行就是因为有任务堆栈在保驾护航,任务调度器在进行任务切换的时候会将当前任务的现场(CPU 寄存器值等)保存在此任务的任务堆栈中等到此任务下次运行的时候就会先用堆栈中保存的值来恢复现场,恢复现场以后任务就会接着从上次中断的地方开始运行。
创建任务的时候需要给任务指定堆栈,如果使用的函数 xTaskCreate()创建任务(动态方法)的话那么任务堆栈就会由函数 xTaskCreate()自动创建。如果使用函数 xTaskCreateStatic()创建任务(静态方法)的话就需要程序员自行定义任务堆栈,然后堆栈首地址作为函数的参数 puxStackBufer 传递给函数,其实现如下:
TaskHandle_t xTaskCreateStatic( TaskFunction_t pxTaskCode,
const char *pcName,
uint32_t ulStackDepth,
void *pvParameters,
UBaseType_t uxPriority,
StackType_t *puxStackBuffer,
StaticTask_t *pxTaskBuffer );
其中puxStackBuffer就是任务堆栈,静态创建时由用户定义将首地址传递给这个参数。
3.任务相关API使用
这次学习涉及的API接口共有6个。
任务的创建
任务创建是FreeRTOS最基础的功能之一,在前面的章节中我们强调过,任务的创建分为动态和静态两种。
1.动态创建xTaskCreate()
BaseType_t xTaskCreate( TaskFunction_t pvTaskCode,
const char * const pcName,
const configSTACK_DEPTH_TYPE uxStackDepth,
void *pvParameters,
UBaseType_t uxPriority,
TaskHandle_t *pxCreatedTask);
参数:
pvTaskCode 任务函数的指针
pcName 任务名。一般用于调试和追踪。
任务名称的最大长度由FreeRTOSConfig.h 中的 configMAX_TASK_NAME_LEN 定义。
uxStackDepth 任务堆栈大小。一般实际申请到的堆栈大小是该值的四倍
pvParameters 传递给任务函数的参数。
uxPriority 创建任务执行的优先级 。
pxCreatedTask 任务句柄,传递至由 xTaskCreate() 函数创建的任务 。 pxCreatedTask 是可选的,可设置为 NULL。
返回值:
pdPASS 成功
errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY 任务创建失败,内存不足
2.xTaskCreateStatic
如果 configSUPPORT_STATIC_ALLOCATION 未定义,那么它默认为 0。
如果 configSUPPORT_STATIC_ALLOCATION 设置为 1,那么应用程序编写者还必须提供两个回调函数: vApplicationGetIdleTaskMemory() 提供内存供 RTOS 空闲任务使用,并且(如果 configUSE_TIMERS 设置为 1),vApplicationGetTimerTaskMemory()用于提供 RTOS 守护/定时器服务任务使用的内存。
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 任务堆栈
pxTaskBuffer 任务控制块
返回值:
NULL 创建失败,一般情况下由puxStackBuffer或pxTaskBuffer为空导致
其他值 创建成功,返回任务句柄
3.vTaskDelete
被删除了的任务不再存在,也就是说再也不会进入运行态。任务被删除以后就不能再使用此任务的句柄,如果此任务是使用动态方法创建的,也就是使用函数 xTaskCreate()创建的,那么在此任务被删除以后此任务之前申请的堆栈和控制块内存会在空闲任务中被释放掉,因此当调用函数删除任务以后必须给空闲任务一定的运行时间。
只有那些由内核分配给任务的内存才会在任务被删除以后自动的释放掉,用户分配给任务的内存需要用户自行释放掉,比如某个任务中用户调用函数 pvPortMalloc()分配了 500 字节的内存,那么在此任务被删除以后用户也必须调用函数 vPortFree()将这 500 字节的内存释放掉,否则会导致内存泄露。此函数原型如下:
void vTaskDelete( TaskHandle_t xTask );
TIPS: INCLUDE_vTaskDelete 必须定义为 1 才能使用此函数
参数 xtask 要删除任务的句柄
返回值 无
4.vTaskSuspend
有时候我们需要暂停某个任务的运行,过一段时间以后在重新运行。FreeRTOS 给我们提供任务挂起和恢复,当某个任务要停止运行一段时间的话就将这个任务挂起,当要重新运行这个任务的话就恢复这个任务的运行。
此函数用于将某个任务设置为挂起态,进入挂起态的任务永远都不会进入运行态。退出挂起态的唯一方法就是调用任务恢复函数。函数原型如下:
void vTaskSuspend( TaskHandle_t xTaskToSuspend );
TIPS:必须将 INCLUDE_vTaskSuspend 定义为 1 才能使用此函数。
5.vTaskResume
将一个任务从挂起状态恢复到就绪态
TIPS:必须将 INCLUDE_vTaskSuspend 定义为 1 才能使用此函数。
6.xTaskResumeFromISR
用于中断中恢复一个任务
TIPS:必须将 include_vTaskSuspend 和 INCLUDE_xTaskResumeFromISR 定义为 1 才能使用此函数。