【STM32】FreeRTOS任务基础
00. 目录
文章目录
01. 概述
我们以前使用51、AVR、STM32单片机裸机的时候一般都是在main函数里面用while(1)做一个大循环来完成所有的处理,即应用程序是一个无限的循环,循环中调用相应的函数完成所需的处理。有时候我们也需要中断中完成一些处理。相对于多任务系统而言,这个就是单任务系统,也称作前后台系统,中断服务函数作为前台程序,大循环while(1)作为后台程序。
前后台系统的实时性差,前后台系统各个任务都是排队等着轮流执行,不管你这个程序现在有多么紧急,没轮到你就只能等着。相当于所有的任务的优先级都是一样的。但是前后台系统简单,资源消耗也少,稍微大一点的嵌入式应用中前后台系统就明显力不从心了,此时就需要多任务系统。
多任务系统就是把一个大应用分而治之,把大问题划分成很多小问题,逐步的把小问题解决掉,大问题也就随之解决了,这些小问题可以单独的作为一个小任务来处理。这些小任务是并发处理的,并不是说同一时刻执行很多任务,而是由于每个任务执行的时间很短,导致看起来像是同一时刻执行了很多个任务一样。RTOS系统中有一个任务调度器。不同的系统其任务调度器的实现方法也不相同,比如FreeRTOS是一个抢占式的实时多任务系统,那么其任务调度器也是抢占式的。
高优先级的任务可以打断低优先任务的运行而取得CPU的使用权,这样就保证了那些紧急任务的运行。我们就可以为那些对实时性要求高的任务设置一个很高的优先级。高优先级的任务执行完成以后重新将CPU的使用权归还给低优先级的任务,这个就是抢占式多任务系统的基本原理。
02. FreeRTOS任务
FreeRTOS中应用既可以使用任务,也可以使用协程,或者两者混合使用。但是任务和协程使用不同的API函数,因此不能通过队列(或者信号量)将数据从任务发送给协程,反之亦然。协程是为那些资源很少的MCU准备的,其开销很小,但是FreeRTOS官方已经不打算在更新协程了。
2.1 任务的特性
简而言之:使用RTOS的实时应用程序可认为是一系列独立任务的集合。每个任务在自己的环境中运行,不依赖于系统中的其它任务或者RTOS调度器。在任何时刻,只有一个任务得到运行,RTOS调度器决定运行哪个任务。调度器会不断的启动、停止每一个任务,宏观看上去就像整个应用程序都在执行。作为任务,不需要对调度器的活动有所了解,在任务切入切出时保存上下文环境(寄存器值、堆栈内容)是调度器主要的职责。为了实现这点,每个任务都需要有自己的堆栈。当任务切出时,它的执行环境会被保存在该任务的堆栈中,这样当再次运行时,就能从堆栈中正确的恢复上次的运行环境。
任务特性:
- 简单
- 没有使用限制
- 支持完全抢占
- 支持优先级
- 每个任务都有自己的堆栈,消耗RAM较多
- 如果使用抢占,必须小心的考虑可重入问题
03. FreeRTOS协程
协程是为了那些资源很少的MCU而出现的,但是随着MCU的飞速发展,性能越来越强大,现在协程几乎很少用到了,但是FreeRTOS目前还没有将协程移除,但是FreeRTOS是绝对不会再更新和维护协程了,因此协程大家了解一下就行了。在概念上协程和任务是相似的,但是有如下根本上的不同。
- 堆栈使用
所有的协程使用同一个堆栈,这样就比使用任务消耗更少的RAM。
- 调度器和优先级
协程使用合作式的调度器,但是可以再使用抢占式的调度器中使用协程。
- 宏实现
协程是通过宏定义来实现的。
- 使用限制
为了降低对RAM的消耗做了很多的限制。
04. 任务状态
一个任务可为下面中的一个:
-
运行:如果一个任务正在执行,那么说这个任务处于运行状态。此时它占用处理器。
-
就绪:就绪的任务已经具备执行的能力(不同于阻塞和挂起),但是因为有一个同优先级或者更高优先级的任务处于运行状态而还没有真正执行。
-
阻塞:如果任务当前正在等待某个时序或外部中断,我们就说这个任务处于阻塞状态。比如一个任务调用vTaskDelay()后会阻塞到延时周期到为止。任务也可能阻塞在队列或信号量事件上。进入阻塞状态的任务通常有一个“超时”周期,当事件超时后解除阻塞。
-
挂起:处于挂起状态的任务同样对调度器无效。仅当明确的分别调用vTaskSuspend() 和xTaskResume() API函数后,任务才会进入或退出挂起状态。不可以指定超时周期事件(不可以通过设定超时事件而退出挂起状态)
任务状态之间的转换如下图所示:
05. 任务优先级
每个任务都要被指定一个优先级,从0~configMAX_PRIORITIES,configMAX_PRIORITIES定义在FreeRTOSConfig.h中。
如果某架构硬件支持CLZ(或类似)指令(计算前导零的数目,Cortex-M3是支持该指令的,从ARMv6T2才支持这个指令),并且打算在移植层使用这个特性来优化任务调度机制,需要有一些步骤,首先将FreeRTOSConfig.h中configUSE_PORT_OPTIMISED_TASK_SELECTION设置为1,并且最大优先级数目configMAX_PRIORITIES不能大于32。除此之外,configMAX_PRIORITIES可以设置为任意值,但是考虑到configMAX_PRIORITIES设置越大,RAM消耗也越大,一般设置为满足使用的最小值。
低优先级数值代表低优先级。空闲任务(idle task)的优先级为0(tskIDLE_PRIORITY)。
FreeRTOS调度器确保处于最高优先级的就绪或运行态任务获取处理器,换句话说,处于运行状态的任务,只有其中的最高优先级任务才会运行。
任何数量的任务可以共享同一个优先级。如果宏configUSE_TIME_SLICING未定义或着宏configUSE_TIME_SLICING定义为1,处于就绪态的多个相同优先级任务将会以时间片切换的方式共享处理器。
06. 任务实现
一个任务具有以下结构:
void vATaskFunction( void *pvParameters )
{
for( ;; )
{
/*-- 应用程序代码放在这里. --*/
}
/* 任务不可以从这个函数返回或退出。在较新的FreeRTOS移植包中,如果
试图从一个任务中返回,将会调用configASSERT()(如果定义的话)。
如果一个任务确实要退出函数,那么这个任务应调用vTaskDelete(NULL)
函数,以便处理一些清理工作。*/
vTaskDelete( NULL );
}
任务函数返回为void,参数只有一个void类型指针。所有的任务函数都应该是这样。void类型指针可以向任务传递任意类型信息。
任务函数决不应该返回,因此通常任务函数都是一个死循环。
任务由xTaskCreate()函数创建,由vTaskDelete()函数删除。
空闲任务和空闲任务钩子(idle task和Idle Task hook)
空闲任务
空闲任务是启动RTOS调度器时由内核自动创建的任务,这样可以确保至少有一个任务在运行。空闲任务具有最低任务优先级,这样如果有其它更高优先级的任务进入就绪态就可以立刻让出CPU。
删除任务后,空闲任务用来释放RTOS分配给被删除任务的内存。因此,在应用中使用vTaskDelete()函数后确保空闲任务能获得处理器时间就很重要了。除此之外,空闲任务没有其它有效功能,所以可以被合理的剥夺处理器时间,并且它的优先级也是最低的。
应用程序任务共享空闲任务优先级(tskIDLE_PRIORITY)也是可能的。这种情况如何配置可以参考configIDLE_SHOULE_YIELD配置参数类获取更多信息。
空闲任务钩子
空闲任务钩子是一个函数,每一个空闲任务周期被调用一次。如果你想将任务程序功能运行在空闲优先级上,可以有两种选择:
-
在一个空闲任务钩子中实现这个功能:因为FreeRTOS必须至少有一个任务处于就绪或运行状态,因此钩子函数不可以调用可能引起空闲任务阻塞的API函数(比如vTaskDelay()或者带有超时事件的队列或信号量函数)。
-
创建一个具有空闲优先级的任务去实现这个功能:这是个更灵活的解决方案,但是会带来更多RAM开销。
创建一个空闲钩子步骤如下:
- 在FreeRTOSConfig.h头文件中设置configUSE_IDLE_HOOK为1;
- 定义一个函数,名字和参数原型如下所示:
void vApplicationIdleHook( void );
通常,使用这个空闲钩子函数设置CPU进入低功耗模式。
07. 任务控制块
FreeRTOS中的每个任务都有一些属性需要存储,FreeRTOS将这些属性集合到一起用一个结构体来表示,这个结构体叫做任务控制块:TCP_t
,在使用函数xTaskCreate()创建任务的时候就会自动的给每个任务分配一个任务控制块。此结构体在文件tasks.c中有如下声明:
/*
* Task control block. A task control block (TCB) is allocated for each task,
* and stores task state information, including a pointer to the task's context
* (the task's run time environment, including register values)
*/
typedef struct tskTaskControlBlock /* The old naming convention is used to prevent breaking kernel aware debuggers. */
{ //任务堆栈指针
volatile StackType_t * pxTopOfStack; /*< Points to the location of the last item placed on the tasks stack. THIS MUST BE THE FIRST MEMBER OF THE TCB STRUCT. */
#if ( portUSING_MPU_WRAPPERS == 1 )
//MPU相关设置
xMPU_SETTINGS xMPUSettings; /*< The MPU settings are defined as part of the port layer. THIS MUST BE THE SECOND MEMBER OF THE TCB STRUCT. */
#endif
//状态列表项
ListItem_t xStateListItem; /*< The list that the state list item of a task is reference from denotes the state of that task (Ready, Blocked, Suspended ). */
//事件列表项
ListItem_t xEventListItem; /*< Used to reference a task from an event list. */
//任务优先级
UBaseType_t uxPriority; /*< The priority of the task. 0 is the lowest priority. */
//任务堆栈起始地址
StackType_t * pxStack; /*< Points to the start of the stack. */
//任务的名字
char pcTaskName[ configMAX_TASK_NAME_LEN ]; /*< Descriptive name given to the task when created. Facilitates debugging only. */ /*lint !e971 Unqualified char types are allowed for strings and single characters only. */
#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 )
//trace或者debug的时候用得到
UBaseType_t uxTCBNumber; /*< Stores a number that increments each time a TCB is created. It allows debuggers to determine when a task has been deleted and then recreated. */
UBaseType_t uxTaskNumber; /*< Stores a number specifically for use by third party trace code. */
#endif
#if ( configUSE_MUTEXES == 1 )
//任务基础优先级,优先级翻转的使用用到
UBaseType_t uxBasePriority; /*< The priority last assigned to the task - used by the priority inheritance mechanism. */
//任务获取到的互斥信号量个数
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 )
//用来记录任务运行总时间
uint32_t ulRunTimeCounter; /*< Stores the amount of time the task has spent in the Running state. */
#endif
#if ( configUSE_NEWLIB_REENTRANT == 1 )
/* Allocate a Newlib reent structure that is specific to this task.
* Note Newlib support has been included by popular demand, but is not
* used by the FreeRTOS maintainers themselves. FreeRTOS is not
* responsible for resulting newlib operation. User must be familiar with
* newlib and must provide system-wide implementations of the necessary
* stubs. Be warned that (at the time of writing) the current newlib design
* implements a system-wide malloc() that must be provided with locks.
*
* See the third party link http://www.nadler.com/embedded/newlibAndFreeRTOS.html
* for additional information. */
//定义一个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; /*< Set to pdTRUE if the task is a statically allocated to ensure no attempt is made to free the memory. */
#endif
#if ( INCLUDE_xTaskAbortDelay == 1 )
uint8_t ucDelayAborted;
#endif
#if ( configUSE_POSIX_ERRNO == 1 )
int iTaskErrno;
#endif
} tskTCB;
/* The old tskTCB name is maintained above then typedefed to the new TCB_t name
* below to enable the use of older kernel aware debuggers. */
typedef tskTCB TCB_t;
08. 任务堆栈
FreeRTOS之所以能正确的恢复一个任务的运行就是因为有任务堆栈在保驾护航,任务调度器在进行任务切换的时候会将当前任务的现场保存到在此任务的任务堆栈中,等到此任务下次运行的时候就会先用堆栈中保存的值来恢复现场,恢复现场以后任务就会接着从上次中断的地方开始运行。
创建任务的时候需要给任务指定堆栈,如果使用的函数xTaskCreate()创建任务(动态方法)的话那么任务堆栈就会有函数xTaskCreate()自动创建,后面分析xTaskCreate()的时候会讲解。如果使用函数xTaskCreateStatic()创建任务(静态方法)的话就需要程序员自行定义任务堆栈,然后堆栈首地址作为函数的参数puxStackBuffer传递给函数。
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 ) PRIVILEGED_FUNCTION;
09. 附录
9.1 【STM32】STM32系列教程汇总
10. 参考
《FreeRTOS Reference Manual》
《Using the FreeRTOS Real Time Kernel -A Practical Guide》
《The Definitive Guide to ARM Cortex-M3 and Cortex-M4 Processors,3rd Edition》
上一篇: lwip编程接口