1.多任务系统
????????在没有RTOS的“裸机”系统中,一般是在main函数中用while(1)做一个大循环来进行处理。这种方式是典型的单任务系统,也称作前后台系统,中断服务作为前台程序,大循环作为后台程序。
????????前后台系统的弊端很明显,就是各个后台任务都是排队等待轮流执行,不管该任务有多紧急,没有轮到,就只能等着。
??????? 多任务系统会并发处理很多任务,注意,并不是说同一时刻一起执行很多任务,而是由于每个任务执行的时间很短,导致看起来像是同一时刻执行了很多个任务一样。每个任务在分配时会被分配到不同的优先级,在RTOS运行时通过任务调度器来进行任务切换。FreeRTOS采用抢占式任务调度方式。
????????抢占式多任务系统的基本原理为,高优先级的任务可以打断低优先级任务的运行而取得CPU的使用权,从而保证紧急任务的执行。高优先级的任务执行完成后重新把CPU的使用权归还给低优先级的任务。
?2.任务(Task)的特性
????????在使用RTOS的时候,一个实时应用可以作为一个独立的任务。每个任务都有自己的运行环境,不依赖于系统中其他的任务或者RTOS调度器。任何一个时间点只能有一个任务运行,具体运行哪个任务是有RTOS调度器来决定的,RTOS调度器因此就会重复的开启、关闭每个任务。任务不需要了解RTOS调度器的具体行为,RTOS调度器的职责是确保当一个任务开始执行的时候其上下文环境(寄存器值,堆栈内容等)和任务上一次退出的时候相同。为了做到这一点,每个任务都必须有个堆栈,当任务切换的时候将上下文环境保存在堆栈中,这样当任务再次执行的时候就可以从堆栈中取出上下文环境,任务恢复运行。
特性
- 没有使用限制
- 支持抢占
- 支持优先级
- 每个任务都拥有堆栈导致RAM使用量增大
3.任务状态
FreeRTOS中的任务永远处于下面几个状态中的某一个:
????????运行态
????????当一个任务在运行时,那么就说这个任务处于运行态,处于运行态的任务就是当前正在使用处理器的任务。如果使用的是单核处理器,那不管在任何时刻,永远都只有一个任务处于运行态。
??????? 就绪态
??????? 处于就绪态的任务时那些已经准备就绪(这些任务没有被阻塞或者挂起),可以运行的任务,但是处于就绪态的任务还没有运行,因为有一个通优先级或者更高优先级的任务正在运行。
??????? 阻塞态
??????? 如果一个任务当前正在等待某个外部事件的话,就说它处于阻塞态,比如如果某个任务调用了函数vTaskDelay()的话就会进入阻塞态,直到延时周期完成。任务在等待队列、信号量、事件组、通知或互斥信号量的时候也会进入阻塞态。任务进入阻塞态会有一个超时时间,超时后,任务就会退出阻塞态进入就绪态。
??????? 挂起态
??????? 任务进入挂起态以后不能被调度器调用进入运行态。没有超时时间。任务进入和退出挂起态通过调用函数vTaskSuspend()和xTaskResume()。
?4.任务优先级
??????? 每个任务都可以分配一个从0~(configMAX_PRIORITIES-1)的优先级,一般每个使用FreeRTOS的厂家都会自己定义好的。可以在响应的头文件中找到。这里我使用的是MTK7682。优先级如下:
???????????????
???????? 可以看到优先级最高可以设置到19。这个每个厂家都不同,理论上最高不超过31。而且可以看到,厂家已经规定好了用户可以使用的优先级大小,从TASK_PRIORITY_LOW到TASK_PRIORITY_HARD_REALTIME。
????????优先级数字越低,表示优先级越低,0的优先级最低,configMAX_PRIORITIES-1的优先级最高。空闲任务的优先级最低,为0。当任务的优先级相同时,此时处于就绪态的优先级相同的任务就会使用时间片轮转调度器获取运行时间。
5.任务控制块
??????? FreeRTOS的每个任务都有一些属性需要存储,FreeRTOS把这些属性集合到一起用一个结构体来表示,这个结构体叫做任务控制块。在使用函数xTaskCreate()创建任务的时候就会自动的给每个任务分配一个任务控制块。定义如下:
/*
* 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 )
xMPU_SETTINGS xMPUSettings; /*< MPU相关设置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. */
struct _reent xNewLib_reent;
#endif
#if( configUSE_TASK_NOTIFICATIONS == 1 ) //任务通知相关变量
volatile uint32_t ulNotifiedValue; //任务通知值
volatile uint8_t ucNotifyState; //任务通知状态
#endif
/* See the comments above 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; /*< 标记任务是动态创建还是静态创建,如果是静态创建为pfTURE,如果是动态创建的就为pdFALSE,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;
?6.任务堆栈
???????? 任务调度器在进行任务切换的时候会将当前任务的现场(寄存器值等)保存在此任务的任务堆栈中,等到此任务下次运行的时候就会先用堆栈中保存的值来恢复现场,恢复现场以后任务就会接着从上次中断的地方开始运行。
??????? 创建任务的时候需要给任务指定堆栈,如果使用xTaskCreate()创建任务(动态方法),那么任务堆栈就会由该函数自动创建。如果使用xTaskCreateStatic()创建任务(静态方法),就需要自行定义任务堆栈,然后堆栈首地址作为函数
7.堆栈大小
????????不管是使用xTaskCreate()还是xTaskCreateStatic()创建任务,都需要制定任务堆栈大小。任务堆栈的数据类型为StackType_t,本质上为uint32_t.
查看xTaskCreate函数中的源码,如下:
????????从代码中可以看到申请的堆栈大小为usStackDepth*sizeof(StackType_t)。StackType_t变量的大小为4个字节,那么任务的实际堆栈大小就是我们定义的4倍。
8.任务切换
????????要了解任务切换的过程,就必须先了解pendSV的概念。pendSV(可挂起的系统调用)异常,其优先级可以通过编程来设置。可以通过将中断控制和状态寄存器ICSR的bit28(pendSV的挂起位置1)来触发pendSV中断。与SVC异常不同,pendSV是不精确的,因此它的挂起状态可在更高优先级异常处理内设置,且会在高优先级处理完成后执行。
????????利用该特性,若将PendSV设置为最低的异常优先级,可以让PendSV异常处理在所有其他中断处理完成后执行,这对上下文切换非常有用。
上下文切换内触发的场合可以是:
- 1.执行一个系统调用
- 2.系统滴答定时器(systick)中断
????????PendSV异常将上下文切换请求延迟到所有其他IRQ处理都已经完成后,此时需要将PendSV设置为最低优先级。若OS需要执行上下文切换,会设置PendSV的挂起状态,并在PendSV异常内执行上下文切换。
????????上述说到执行上下文切换场合可以是1.执行一个系统调用,2系统滴答定时器(systick)中断。
系统调用通常是调用taskYIELD()函数来强制执行任务切换。
#define taskYIELD() portYIELD()
#define portYIELD() \
{ \
/* Set a PendSV to request a context switch. */ \
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; //通过向中断控制和状态寄存器ICSR的bit28写入1来挂起PendSV中断,这样就可以在PendSV中断服务函数中进行任务切换。
/* Barriers are normally not required but do ensure the code is completely \
within the specified behaviour for the architecture. */ \
__dsb( portSY_FULL_READ_WRITE ); \
__isb( portSY_FULL_READ_WRITE ); \
}
系统滴答中断中执行任务切换
ATTR_TEXT_IN_RAM void xPortSysTickHandler( void )
{
/* The SysTick runs at the lowest interrupt priority, so when this interrupt
executes all interrupts must be unmasked. There is therefore no need to
save and then restore the interrupt mask value as its value is already
known. */
//( void ) portSET_INTERRUPT_MASK_FROM_ISR();
UBaseType_t uxSavedInterruptStatus;
uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();
{
/* must suspend flash before fetch code from flash */
extern void Flash_ReturnReady(void);
Flash_ReturnReady();
#ifdef MTK_SWLA_ENABLE
//SLA_CustomLogging("tik",SA_LABEL);
#endif /* MTK_SWLA_ENABLE */
/* Increment the RTOS tick. */
if( xTaskIncrementTick() != pdFALSE ) //增加时钟计数器xTickCount的值
{
/* A context switch is required. Context switching is performed in
the PendSV interrupt. Pend the PendSV interrupt. */
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
}
}
portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus );
//portCLEAR_INTERRUPT_MASK_FROM_ISR( 0 );
}
????????可以看到同样是通过portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;指令来挂起PendSV中断。在PendSV中断中执行任务切换。
9.时间片调度
??????? 在FreeRTOS中允许一个任务运行一个时间片(一个时钟节拍的长度)后让出CPU使用权,让拥有同优先级的下一个任务运行。这种调度方法就是时间片调度。一个时钟节拍的长度可以通过查看系统配置configTICK_RATE_HZ来确认,如果为1000,那么一个时间片长度就是1ms。
?
Task1、Task2、Task3优先级都为N。Task3为运行态,Task1、Task2为就绪态
- task3运行,在时间片到达后,进入systick中断,挂起PendSV中断切换任务到task1.
- Task1运行到时间片结束,进入systick中断,挂起PendSV中断切换任务到task2.
- Task2运行到时间片结束,进入systick中断,挂起PendSV中断切换任务到task3.
- Task3运行完成,调用任务切换函数portYIELD()强行进行任务切换,放弃剩余的时间片,从而使优先级N下的下一个就绪的任务运行。
- Task1运行到时间片结束,进入systick中断,挂起PendSV中断切换任务到task2.
- Task2执行完剩余时间片
|