阅读引言: 此文将会从一个工程文件, 一步一步的分析RTOS的任务调度实现, 这里选用FreeRTOS分析, 别的也差不多的, 可能在细节上有少许不一样。
目录
1, 常见嵌入式实时操作系统
2, 任务调度的本质
3, 任务调度分析开始
1, 常见嵌入式实时操作系统
操作系统名称 | 特点 | 许可类型 | 适用领域 |
---|---|---|---|
FreeRTOS | 轻量级、易于配置、广泛使用 | 开源(MIT许可) | 各种资源受限的嵌入式系统 |
RT-Thread | 中文支持良好、组件丰富、高度可扩展 | 开源(Apache许可) | 智能家居、穿戴设备等 |
VxWorks | 高度可靠、网络功能强大、广泛用于军事和航空领域 | 商业许可 | 军事、航空、工业控制 |
uC/OS | 实时性能高、源码开放、易于理解 | 商业许可 | 工业控制、消费电子 |
QNX | 安全性高、支持多核、广泛用于汽车电子 | 商业许可 | 汽车电子、医疗设备 |
Zephyr | 开源、模块化设计、适用于物联网设备 | 开源(Apache许可) | 物联网设备 |
mbed OS | 专为物联网设计、支持多种硬件平台 | 开源(Apache许可) | 物联网、智能硬件 |
CMSIS-RTOS | ARM Cortex微控制器的官方RTOS标准 | 标准 | ARM Cortex微控制器 |
INTEGRITY | 确定性性能、广泛用于汽车和航空 | 商业许可 | 汽车、航空、航天 |
ThreadX | 实时性能高、内存使用效率高、可扩展 | 商业许可 | 工业控制、消费电子 |
2, 任务调度的本质
cpu每隔一定时间执行不同函数的指令, 或者更底层一点的说法是完成不同指令段的切换, 让cpu分别执行不同指令段的内容, 并且cpu在切换不同指令段执行的时候, 之前执行的内容, 需要被保存, 不可能有从头开始把, 假设cpu在执行任务1, 现在被更高优先级的任务2抢占了, 这个时候就需要将任务1的相关内容保存到任务1自己的堆栈里面去。具体保存啥呢?
-
线程是函数吗?函数需要保存吗?函数在Flash上,不会被破坏,无需保存, 这段内存区域是只读的
-
函数执行到了哪里?需要保存吗?需要保存, 上下文信息,
-
函数里用到了全局变量,全局变量需要保存吗?全局变量在内存上,还能保存到哪里去?全局变量无需保存
-
函数里用到了局部变量,局部变量需要保存吗?局部变量在栈里,也是在内存里,只要避免栈不被破坏即可,局部变量无需保存
-
运算的中间值需要保存吗?中间值保存在哪里?在CPU寄存器里,另一个线程也要用到CPU寄存器,所以CPU寄存器需要保存
-
函数运行了哪里:它也是一个CPU寄存器,名为"PC"
-
汇总:CPU寄存器需要保存!
-
保存在哪里?保存在线程的栈里
-
怎么理解CPU寄存器、怎么理解栈?
在裸机编程中, 不同的函数使用的是同一个栈空间, 而装上操作系统之后, 不同的任务(线程)之间的栈是独立开的, 互不影响, 起始函数和线程是很类似的, 只是说进程和线程都是针对操作系统提出来的概念。
接下来, 让我们看看RTOS是如何实现上下文切换的。
3, 任务调度分析开始
接下来, 我会从FreeRTOS的一个工程, 从启动代码开始, 到创建任务, 去分析RTOS切换任务是如何实现的。
启动代码就不看了, 相信大家都是有基础的, 不会的也可以先去看看我之前的文章。前面没啥好说的, 直接去freertos_demo函数中看看
在这里呢, 创建了一个开始任务, 接着就启动了任务调度器。
那咱们先看看创建任务是如何创建的吧
#if ( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode,
const char * const pcName, /*lint !e971 Unqualified char types are allowed for strings and single characters only. */
const configSTACK_DEPTH_TYPE usStackDepth,
void * const pvParameters,
UBaseType_t uxPriority,
TaskHandle_t * const pxCreatedTask )
{
TCB_t * pxNewTCB;
BaseType_t xReturn;
/* If the stack grows down then allocate the stack then the TCB so the stack
* does not grow into the TCB. Likewise if the stack grows up then allocate
* the TCB then the stack. */
#if ( portSTACK_GROWTH > 0 )
{
/* Allocate space for the TCB. Where the memory comes from depends on
* the implementation of the port malloc function and whether or not static
* allocation is being used. */
pxNewTCB = ( TCB_t * ) pvPortMalloc( sizeof( TCB_t ) );
if( pxNewTCB != NULL )
{
/* Allocate space for the stack used by the task being created.
* The base of the stack memory stored in the TCB so the task can
* be deleted later if required. */
pxNewTCB->pxStack = ( StackType_t * ) pvPortMallocStack( ( ( ( size_t ) usStackDepth ) * sizeof( StackType_t ) ) ); /*lint !e961 MISRA exception as the casts are only redundant for some ports. */
if( pxNewTCB->pxStack == NULL )
{
/* Could not allocate the stack. Delete the allocated TCB. */
vPortFree( pxNewTCB );
pxNewTCB = NULL;
}
}
}
#else /* portSTACK_GROWTH */
{
StackType_t * pxStack;
/* Allocate space for the stack used by the task being created. */
pxStack = pvPortMallocStack( ( ( ( size_t ) usStackDepth ) * sizeof( StackType_t ) ) ); /*lint !e9079 All values returned by pvPortMalloc() have at least the alignment required by the MCU's stack and this allocation is the stack. */
if( pxStack != NULL )
{
/* Allocate space for the TCB. */
pxNewTCB = ( TCB_t * ) pvPortMalloc( sizeof( TCB_t ) ); /*lint !e9087 !e9079 All values returned by pvPortMalloc() have at least the alignment required by the MCU's stack, and the first member of TCB_t is always a pointer to the task's stack. */
if( pxNewTCB != NULL )
{
/* Store the stack location in the TCB. */
pxNewTCB->pxStack = pxStack;
}
else
{
/* The stack cannot be used as the TCB was not created. Free
* it again. */
vPortFreeStack( pxStack );
}
}
else
{
pxNewTCB = NULL;
}
}
#endif /* portSTACK_GROWTH */
if( pxNewTCB != NULL )
{
#if ( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 ) /*lint !e9029 !e731 Macro has been consolidated for readability reasons. */
{
/* Tasks can be created statically or dynamically, so note this
* task was created dynamically in case it is later deleted. */
pxNewTCB->ucStaticallyAllocated = tskDYNAMICALLY_ALLOCATED_STACK_AND_TCB;
}
#endif /* tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE */
prvInitialiseNewTask( pxTaskCode, pcName, ( uint32_t ) usStackDepth, pvParameters, uxPriority, pxCreatedTask, pxNewTCB, NULL );
prvAddNewTaskToReadyList( pxNewTCB );
xReturn = pdPASS;
}
else
{
xReturn = errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY;
}
return xReturn;
}
#endif /* configSUPPORT_DYNAMIC_ALLOCATION */
/*-----------------------------------------------------------*/
动态创建任务的函数还是非常简单的, 简单概述一下: 为任务控制块和任务的栈空间分配内存空间, 接着假设cpu执行到该任务的的入口指令的位置被打断, 将此时CPU寄存器信息保存到任务栈中, 接着将此任务挂载到就绪链表中去。
我们看一个东西, ARM架构的保护现场的内存布局图
也就是说cpu的寄存器保存到内容的时候, 布局是这样的
接下来就该说说这个函数了,函数v TaskStartScheduler 用于启动任务调度器,任务调度器启动后,FreeRTOS 便会开始进行任务调度,除非调用函数x TaskEndScheduler() 停止任务调度器,否则不会再返回。函数vTaskStartScheduler 的代码如下所示:
void vTaskStartScheduler( void )
{
BaseType_t xReturn;
/* Add the idle task at the lowest priority. */
#if ( configSUPPORT_STATIC_ALLOCATION == 1 )
{
StaticTask_t * pxIdleTaskTCBBuffer = NULL;
StackType_t * pxIdleTaskStackBuffer = NULL;
uint32_t ulIdleTaskStackSize;
/* The Idle task is created using user provided RAM - obtain the
* address of the RAM then create the idle task. */
vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer, &pxIdleTaskStackBuffer, &ulIdleTaskStackSize );
xIdleTaskHandle = xTaskCreateStatic( prvIdleTask,
configIDLE_TASK_NAME,
ulIdleTaskStackSize,
( void * ) NULL, /*lint !e961. The cast is not redundant for all compilers. */
portPRIVILEGE_BIT, /* In effect ( tskIDLE_PRIORITY | portPRIVILEGE_BIT ), but tskIDLE_PRIORITY is zero. */
pxIdleTaskStackBuffer,
pxIdleTaskTCBBuffer ); /*lint !e961 MISRA exception, justified as it is not a redundant explicit cast to all supported compilers. */
if( xIdleTaskHandle != NULL )
{
xReturn = pdPASS;
}
else
{
xReturn = pdFAIL;
}
}
#else /* if ( configSUPPORT_STATIC_ALLOCATION == 1 ) */
{
/* The Idle task is being created using dynamically allocated RAM. */
xReturn = xTaskCreate( prvIdleTask,
configIDLE_TASK_NAME,
configMINIMAL_STACK_SIZE,
( void * ) NULL,
portPRIVILEGE_BIT, /* In effect ( tskIDLE_PRIORITY | portPRIVILEGE_BIT ), but tskIDLE_PRIORITY is zero. */
&xIdleTaskHandle ); /*lint !e961 MISRA exception, justified as it is not a redundant explicit cast to all supported compilers. */
}
#endif /* configSUPPORT_STATIC_ALLOCATION */
#if ( configUSE_TIMERS == 1 )
{
if( xReturn == pdPASS )
{
xReturn = xTimerCreateTimerTask();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_TIMERS */
if( xReturn == pdPASS )
{
/* freertos_tasks_c_additions_init() should only be called if the user
* definable macro FREERTOS_TASKS_C_ADDITIONS_INIT() is defined, as that is
* the only macro called by the function. */
#ifdef FREERTOS_TASKS_C_ADDITIONS_INIT
{
freertos_tasks_c_additions_init();
}
#endif
/* Interrupts are turned off here, to ensure a tick does not occur
* before or during the call to xPortStartScheduler(). The stacks of
* the created tasks contain a status word with interrupts switched on
* so interrupts will automatically get re-enabled when the first task
* starts to run. */
portDISABLE_INTERRUPTS();
#if ( configUSE_NEWLIB_REENTRANT == 1 )
{
/* Switch Newlib's _impure_ptr variable to point to the _reent
* structure specific to the task that will run first.
* See the third party link http://www.nadler.com/embedded/newlibAndFreeRTOS.html
* for additional information. */
_impure_ptr = &( pxCurrentTCB->xNewLib_reent );
}
#endif /* configUSE_NEWLIB_REENTRANT */
xNextTaskUnblockTime = portMAX_DELAY;
xSchedulerRunning = pdTRUE;
xTickCount = ( TickType_t ) configINITIAL_TICK_COUNT;
/* If configGENERATE_RUN_TIME_STATS is defined then the following
* macro must be defined to configure the timer/counter used to generate
* the run time counter time base. NOTE: If configGENERATE_RUN_TIME_STATS
* is set to 0 and the following line fails to build then ensure you do not
* have portCONFIGURE_TIMER_FOR_RUN_TIME_STATS() defined in your
* FreeRTOSConfig.h file. */
portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();
traceTASK_SWITCHED_IN();
/* Setting up the timer tick is hardware specific and thus in the
* portable interface. */
if( xPortStartScheduler() != pdFALSE )
{
/* Should not reach here as if the scheduler is running the
* function will not return. */
}
else
{
/* Should only reach here if a task calls xTaskEndScheduler(). */
}
}
else
{
/* This line will only be reached if the kernel could not be started,
* because there was not enough FreeRTOS heap to create the idle task
* or the timer task. */
configASSERT( xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY );
}
/* Prevent compiler warnings if INCLUDE_xTaskGetIdleTaskHandle is set to 0,
* meaning xIdleTaskHandle is not used anywhere else. */
( void ) xIdleTaskHandle;
/* OpenOCD makes use of uxTopUsedPriority for thread debugging. Prevent uxTopUsedPriority
* from getting optimized out as it is no longer used by the kernel. */
( void ) uxTopUsedPriority;
}
/*-----------------------------------------------------------*/
从上面的代码可以看出,函数v TaskStartScheduler() 主要做了六件事情。
1. 创建空闲任务,根据是否支持静态内存管理,使用静态方式或动态方式创建空闲任务。
2. 创建定时器服务任务,创建定时器服务任务需要配置启用软件定时器,创建定时器服务任务,同样是根据是否配置支持静态内存管理,使用静态或动态方式创建定时器服务任务。
3. 关闭中断,使用portDISABLE_INTERRUPT() 关闭中断,这种方式只关闭受FreeRTOS 管理的中断。关闭中断主要是为了防止SysTick 中断在任务调度器开启之前或过程中,产生中断。FreeRTOS 会在开始运行第一个任务时,重新打开中断。
4. 初始化一些全局变量,并将任务调度器的运行标志设置为已运行。
5. 初始化任务运行时间统计功能的时基定时器,任务运行时间统计功能需要一个硬件定时器提供高精度的计数,这个硬件定时器就在这里进行配置,如果配置不启用任务运行时间统计功能的,就无需进行这项硬件定时器的配置。
6. 最后就是调用函数xPortStartScheduler() 。
所以, 现在需要了解的函数就是xPortStartScheduler()
走, 看看去, 函数xPortStartScheduler() 完成启动任务调度器中与硬件架构相关的配置部分,以及启动第一个任务,具体的代码如下所示:
/*
* See header file for description.
*/
BaseType_t xPortStartScheduler( void )
{
#if ( configASSERT_DEFINED == 1 )
{
volatile uint32_t ulOriginalPriority;
volatile uint8_t * const pucFirstUserPriorityRegister = ( uint8_t * ) ( portNVIC_IP_REGISTERS_OFFSET_16 + portFIRST_USER_INTERRUPT_NUMBER );
volatile uint8_t ucMaxPriorityValue;
/* Determine the maximum priority from which ISR safe FreeRTOS API
* functions can be called. ISR safe functions are those that end in
* "FromISR". FreeRTOS maintains separate thread and ISR API functions to
* ensure interrupt entry is as fast and simple as possible.
*
* Save the interrupt priority value that is about to be clobbered. */
ulOriginalPriority = *pucFirstUserPriorityRegister;
/* Determine the number of priority bits available. First write to all
* possible bits. */
*pucFirstUserPriorityRegister = portMAX_8_BIT_VALUE;
/* Read the value back to see how many bits stuck. */
ucMaxPriorityValue = *pucFirstUserPriorityRegister;
/* The kernel interrupt priority should be set to the lowest
* priority. */
configASSERT( ucMaxPriorityValue == ( configKERNEL_INTERRUPT_PRIORITY & ucMaxPriorityValue ) );
/* Use the same mask on the maximum system call priority. */
ucMaxSysCallPriority = configMAX_SYSCALL_INTERRUPT_PRIORITY & ucMaxPriorityValue;
/* Calculate the maximum acceptable priority group value for the number
* of bits read back. */
ulMaxPRIGROUPValue = portMAX_PRIGROUP_BITS;
while( ( ucMaxPriorityValue & portTOP_BIT_OF_BYTE ) == portTOP_BIT_OF_BYTE )
{
ulMaxPRIGROUPValue--;
ucMaxPriorityValue <<= ( uint8_t ) 0x01;
}
#ifdef __NVIC_PRIO_BITS
{
/* Check the CMSIS configuration that defines the number of
* priority bits matches the number of priority bits actually queried
* from the hardware. */
configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == __NVIC_PRIO_BITS );
}
#endif
#ifdef configPRIO_BITS
{
/* Check the FreeRTOS configuration that defines the number of
* priority bits matches the number of priority bits actually queried
* from the hardware. */
configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == configPRIO_BITS );
}
#endif
/* Shift the priority group value back to its position within the AIRCR
* register. */
ulMaxPRIGROUPValue <<= portPRIGROUP_SHIFT;
ulMaxPRIGROUPValue &= portPRIORITY_GROUP_MASK;
/* Restore the clobbered interrupt priority register to its original
* value. */
*pucFirstUserPriorityRegister = ulOriginalPriority;
}
#endif /* configASSERT_DEFINED */
/* Make PendSV and SysTick the lowest priority interrupts. */
portNVIC_SHPR3_REG |= portNVIC_PENDSV_PRI;
portNVIC_SHPR3_REG |= portNVIC_SYSTICK_PRI;
/* Start the timer that generates the tick ISR. Interrupts are disabled
* here already. */
vPortSetupTimerInterrupt();
/* Initialise the critical nesting count ready for the first task. */
uxCriticalNesting = 0;
/* Start the first task. */
prvStartFirstTask();
/* Should not get here! */
return 0;
}
/*-----------------------------------------------------------*/
函数xPort StartScheduler() 的解析如下所示:
1. 在启用断言的情况下,函数xPortStartScheduler 会检测用户在FreeRTOS Config.h 文件中对中断的相关配置是否有误,感兴趣的读者请自行查看这部分的相关代码。
2.配置PendSV 和SysTick 的中断优先级为最低优先级
3. 调用函数v PortSetupTimerInterrupt() 配置SysTickSysTick,函数vPortSetupTimerInterrupt 首先会将SysTick 当前计数值清空,并根据FreeRTOSConfig .h 文件中配置的configSYSTICK_CLOCK_HZ HZ(SysTick 时钟源频率)和configTICK_RATE_HZ HZ(系统时钟节拍频率)计算并设置SysTick 的重装载值,然后启动SysTick 计数和中断。
4.初始化临界区嵌套计数器为0 。
5. 调用函数prvEnableVFP 使能FPUFPU,因为A RM Cortex M3 内核MCU 无FPUFPU,此函数仅在ARM Cortex M4/M7 内核MCU 平台上被调用,执行改函数后FPU 被开启。
6. 接下来将FPCCR 寄存器的 31:30 置11,这样在进出异常时,FPU 的相关寄存器就会自动地保存和恢复,同样地,因为A RM Cortex M3 内核MCU 无FPUFPU,此当代码仅在A RM Cortex
M4/M7 内核MCU 平台上被调用。
7. 调用函数prvStartFirstTask() 启动第一个任务。
又去看看prvStartFirstTask()函数的内容], 函数prvStartFirstTask() 用于初始化启动第一个任务前的环境,主要是重新设置MSP 指针,并使能全局中断,具体的代码如下所示(这里以正点原子的STM 32 F 1 系列开发板为例,其他类型的开发板类似):
__asm void prvStartFirstTask( void )
{
/* *INDENT-OFF* */
PRESERVE8
/* Use the NVIC offset register to locate the stack. */
ldr r0, =0xE000ED08
ldr r0, [ r0 ]
ldr r0, [ r0 ]
/* Set the msp back to the start of the stack. */
msr msp, r0
/* Globally enable interrupts. */
cpsie i
cpsie f
dsb
isb
/* Call SVC to start the first task. */
svc 0
nop
nop
/* *INDENT-ON* */
}
/*-----------------------------------------------------------*/
从上面的代码可以看出,函数p rvStartFirstTask() 是一段汇编代码,解析如下所示:
1. 首先是使用了PRESERVE8,进行8 字节对齐,这是因为,栈在任何时候都是需要4 字节对齐的,而在调用入口得8 字节对齐,在进行C 编程的时候,编译器会自动完成的对齐的操作,而对于汇编,就需要开发者手动进行对齐。
2. 接下来的三行代码是为了获得MSP 指针的初始值,那么这里就能够引出两个问题:
(1) 什么是MSP 指针?
程序在运行过程中需要一定的栈空间来保存局部变量等一些信息。当有信息保存到栈中时,MCU 会自动更新SP 指针,使SP 指针指向最后一个入栈的元素,那么程序就可以根据SP 指针来从栈中存取信息。对于正点原子的STM 32 F 1 、STM 32 F 4 、STM 32 F 7 和STM 32 H 7 开发板上使用的ARM Cortex M 的MCU 内核来说,ARM Cortex M 提供了两个栈空间,这两个栈空间的堆栈指针分别是MSP(主堆栈指针)和PSP(进程堆栈指针)。在FreeRTOS 中MSP 是给系统栈空间使用的,而PSP 是给任务栈使用的,也就是说,FreeRTOS 任务的栈空间是通过PSP 指向的,而在进入中断服务函数时,则是使用MSP 指针。当使用不同的堆栈指针时,SP 会等于当前使用的堆栈指针。
(2) 为什么是0xE000ED08?
0xE000ED08 是VTORVTOR(向量表偏移寄存器)的地址,VTOR 中保存了向量表的偏移地址。一般来说向量表是从其实地址0 x00000000 开始的,但是在有情况下,可能需要修改或重定向向量表的首地址,因此ARM Corten M 提供了VTOR 对向量表进行从定向。而向量表是用来保存中断异常的入口函数地址,即栈顶地址的,并且向量表中的第一个字保存的就是栈底的地址,在start_stm32xxxxxx.s 文件中有如下定义:
以上就是向量表(只列出前几个)的部分内容,可以看到向量表的第一个元素就是栈指针的初始值,也就是栈底指针。
在了解了这两个问题之后,接下来再来看看代码。首先是获取VTOR 的地址,接着获取VTOR 的值,也就是获取向量表的首地址,最后获取向量表中第一个字的数据,也就是栈底指针了。
3. 在获取了栈顶指针后,将MSP 指针重新赋值为栈底指针。这个操作相当于丢弃了程序之前保存在栈中的数据,因为FreeRTOS 从开启任务调度器到启动第一个任务都是不会返回的,是一条不归路,因此将栈中的数据丢弃,也不会有影响。
4. 重新赋值MSP 后,接下来就重新使能全局中断,因为之前在函数vTaskStartScheduler
中关闭了受FreeRTOS 的中断。
5. 最后使用SVC 指令,并传入系统调用号00,触发SVC 中断。
在第一个任务中调用了svc指令, 必然会触发svc异常服务, 自然的就会调用到svc的服务函数
__asm void vPortSVCHandler( void )
{
/* *INDENT-OFF* */
PRESERVE8
ldr r3, = pxCurrentTCB /* Restore the context. */
ldr r1, [ r3 ] /* Use pxCurrentTCBConst to get the pxCurrentTCB address. */
ldr r0, [ r1 ] /* The first item in pxCurrentTCB is the task top of stack. */
ldmia r0 !, { r4 - r11 } /* Pop the registers that are not automatically saved on exception entry and the critical nesting count. */
msr psp, r0 /* Restore the task stack pointer. */
isb
mov r0, # 0
msr basepri, r0
orr r14, # 0xd
bx r14
/* *INDENT-ON* */
}
从上面代码中可以看出,函数v PortSVCHandler() 就是用来跳转到第一个任务函数中去的,该函数的具体解析如下:
1. 首先通过p xCurrentTCB 获取优先级最高的就绪态任务的任务栈地址,优先级最高的就绪态任务就是系统将要运行的任务。p xCurrentTCB 是一个全局变量,用于指向系统中优先级最高的就绪态任务的任务控制块,在前面创建s tart_task 任务、空闲任务、定时器处理任务时自动根据任务的优先级高低进行赋值的,具体的赋值过程在后续分析任务创建函数时,会具体分析。
这里举个例子,在《FreeRTOS 移植实验》中,start_task 任务、空闲任务、定时器处理任务的优先级如下表所示:
说了这么多,FreeRTOS 对于进入中断后r 14 为EXC_RETURN 的具体应用就是,通过判断EXC_RETURN 的bit 4 是否为00,来判断任务是否使用了浮点单元。
最后通过bx r14 指令,跳转到任务的任务函数中执行,执行此指令,CPU 会自动从PSP 指向的栈中出栈R 0 、R1 、R2 、R3 、R12 、LR 、PC 、xPSR 寄存器,并且如果EXC_RETURN 的b it4 为00(使用了浮点单元),那么CPU 还会自动恢复浮点寄存器。
好, 到此FreeRTOS的第一个任务就算是启动完成了, 那后来的任务又是如何进行切换的呢?Systick滴答定时器, 每隔一个时间片, 就会触发一次中断, 在其服务函数里面进行的任务切换, 具体请接着往下看。
PendSv异常
PendSV(Pended Service Call,可挂起服务调用),是一个对RTOS 非常重要的异常。PendSV
的中断优先级是可以编程的,用户可以根据实际的需求,对其进行配置。PendSV 的中断由将中
断控制状态寄存器(ICSR)中PENDSVSET 为置一触发(中断控制状态寄存器的有关内容,请
查看4.1.5 小节《中断控制状态寄存器》)。PendSV 与SVC 不同,PendSV 的中断是非实时的,
即PendSV 的中断可以在更高优先级的中断中触发,但是在更高优先级中断结束后才执行。
利用PendSV 的这个可挂起特性,在设计RTOS 时,可以将PendSV 的中断优先级设置为
最低的中断优先级(FreeRTOS 就是这么做的,更详细的内容,请查看4.3.1 小节《PendSV 和
SysTick 中断优先级》),这么一来,PendSV 的中断服务函数就会在其他所有中断处理完成后才
执行。任务切换时,就需要用到PendSV 的这个特性。
首先,来看一下任务切换的一些基本概念,在典型的RTOS 中,任务的处理时间被分为多
个时间片,OS 内核的执行可以有两种触发方式,一种是通过在应用任务中通过SVC 指令触发,
例如在应用任务在等待某个时间发生而需要停止的时候,那么就可以通过SVC 指令来触发OS
内核的执行,以切换到其他任务;第二种方式是,SysTick 周期性的中断,来触发OS 内核的执
行。下图演示了只有两个任务的RTOS 中,两个任务交替执行的过程:
在操作系统中,任务调度器决定是否切换任务。图中的任务及切换都是在SysTick 中
断中完成的,SysTick 的每一次中断都会切换到其他任务。如果一个中断请求(IRQ)在SysTick 中断产生之前产生,那么SysTick 就可能抢占该中断
请求,这就会导致该中断请求被延迟处理,这在实时操作系统中是不允许的,因为这将会影响
到实时操作系统的实时性,如下图所示:
并且,当SysTick 完成任务的上下文切换,准备返回任务中运行时,由于存在中断请求,
ARM Cortex-M 不允许返回线程模式,因此,将会产生用法错误异常(Usage Fault)。
在一些RTOS 的设计中,会通过判断是否存在中断请求,来决定是否进行任务切换。虽然
可以通过检查xPSR 或NVIC 中的中断活跃寄存器来判断是否存在中断请求,但是这样可能会
影响系统的性能,甚至可能出现中断源在SysTick 中断前后不断产生中断请求,导致系统无法
进行任务切换的情况。
PendSV 通过延迟执行任务切换,直到处理完所有的中断请求,以解决上述问题。为了达到
这样的效果,必须将PendSV 的中断优先级设置为最低的中断优先等级。如果操作系统决定切
换任务,那么就将PendSV 设置为挂起状态,并在PendSV 的中断服务函数中执行任务切换,如
下图所示:
1. 任务一触发SVC 中断以进行任务切换(例如,任务一正等待某个事件发生)。
2. 系统内核接收到任务切换请求,开始准备任务切换,并挂起PendSV 异常。
3. 当退出SVC 中断的时候,立刻进入PendSV 异常处理,完成任务切换。
4. 当PendSV 异常处理完成,返回线程模式,开始执行任务二。
5.中断产生,并进入中断处理函数。
6. 当运行中断处理函数的时候,SysTick 异常(用于内核时钟节拍)产生。
7. 操作系统执行必要的操作,然后挂起PendSV 异常,准备进行任务切换。
8.当SysTick 中断处理完成,返回继续处理中断。
9. 当中断处理完成,立马进入PendSV 异常处理,完成任务切换。
10. 当PendSV 异常处理完成,返回线程模式,继续执行任务一。PendSV在RTOS 的任务切换中,起着至关重要的作用,FreeRTOS 的任务切换就是在PendSV中完成的。
PendSv中断服务函数
FreeRTOS在PendSV 的中断中,完成任务切换,PendSV 的中断服务函数由FreeRTOS 编写,将PendSV 的中断服务函数定义成函数x PortPendSVHandler() 。针对ARM Cortex M3 和针对ARM Cortex M4 和ARM Cortex M7 内核的函数xPortPendSVHandler 稍有不同,其主要原因在于ARM Cortex M4 和ARM Cortex M7 内核具有浮点单元,因此在进行任务切换的时候,还需考虑是否保护和恢复浮点寄存器的值。针对ARM Cortex M3 内核的函数xPortPendSVHandler ()(),具体的代码如下所示:
__asm void xPortPendSVHandler( void )
{
extern uxCriticalNesting;
extern pxCurrentTCB;
extern vTaskSwitchContext;
/* *INDENT-OFF* */
PRESERVE8
mrs r0, psp
isb
ldr r3, =pxCurrentTCB /* Get the location of the current TCB. */
ldr r2, [ r3 ]
stmdb r0 !, { r4 - r11 } /* Save the remaining registers. */
str r0, [ r2 ] /* Save the new top of stack into the first member of the TCB. */
stmdb sp !, { r3, r14 }
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
msr basepri, r0
dsb
isb
bl vTaskSwitchContext
mov r0, #0
msr basepri, r0
ldmia sp !, { r3, r14 }
ldr r1, [ r3 ]
ldr r0, [ r1 ] /* The first item in pxCurrentTCB is the task top of stack. */
ldmia r0 !, { r4 - r11 } /* Pop the registers and the critical nesting count. */
msr psp, r0
isb
bx r14
nop
/* *INDENT-ON* */
}
/*-----------------------------------------------------------*/
从上面的代码可以看出,FreeRTOS 在进行任务切换的时候,会将CPU 的运行状态,在当前任务在进行任务切换前,进行保存,保存到任务的任务栈中,然后从切换后运行任务的任务栈中恢复切换后运行任务在上一次被切换时保存的CPU 信息。
但是从PendSV 的中断回调函数代码中,只看到程序保存和恢复的CPU 信息中的部分寄存器信息(R4 寄存器~R11 寄存器),这是因为硬件会自动出栈和入栈其他CPU 寄存器的信息。
在任务运行的时候,CPU 使用PSP 作为栈空间使用,也就是使用运行任务的任务栈。当SysTick 中断(SysTick 的中断服务函数会判断是否需要进行任务切换,相关内容在后续章节会进行讲解)发生时,在跳转到SysTick 中断服务函数运行前,硬件会自动将除R 4~ R 11 寄存器的其他CPU 寄存器入栈,因此就将任务切换前CPU 的部分信息保存到对应任务的任务栈中。当退出PendSV 时,会自动从栈空间中恢复这部分CPU 信息,以共任务正常运行。
因此在PendSV 中断服务函数中,主要要做的事情就是,保存硬件不会自动入栈的CPU 信息,已经确定写一个要运行的任务,并将pxCurrentTCB 指向该任务的任务控制块,然后更新PSP 指针为该任务的任务堆栈指针。
FreeRTOS 确定下一个要运行的任务
从上面可以看到,在PendSV 的中断服务函数中,调用了函数vTaskSwitchContext来确定写一个要运行的任务。
函数vTaskSwitchContext 调用了函数taskSELECT_HIGHEST_PRIORITY_TASK ()(),来将p xCurrentTCB 设置为指向优先级最高的就绪态任务。
函数taskSELECT_HIGHEST_PRIORITY_TASK
函数taskSELECT_HIGHEST_PRIORITY_TASK 用于将pcCurrentTCB 设置为优先级最高的就绪态任务,因此该函数会使用位图的方式在任务优先级记录中查找优先级最高任务优先等级,然后根据这个优先等级,到对应的就绪态任务列表在中取任务。
FreeRTOS提供了两种从任务优先级记录中查找优先级最高任务优先等级的方式,一种是由纯C 代码实现的,这种方式适用于所有运行FreeRTOS 的MCU;另外一种方式则是使用了硬件计算前导零的指令,因此这种方式并不适用于所有运行FreeRTOS 的MCU,而仅适用于具有有相应硬件指令的MCU 。正点原子所有板卡所使用的STM 32 MCU 都支持以上两种方式。具体使用哪种方式,用户可以在FreeRTOSConfig.h 文件中进行配置,配置方法,请查看第三章《FreeRTOS 系统配置》的相关章节。软件方式实现的函数taskSELECT_HIGHEST_PRIORITY_TASK 是一个宏定义,在task .c文件中由定义,具体的代码如下所示:
依靠特定硬件指令实现的函数taskSELECT_HIGHEST_PRIORITY_TASK 是一个宏定义,在t ask.c 文件中有定义,具体的代码如下所示:
在使用硬件方式实现的函数taskSELECT_HIGHEST_PRIORITY_TASK 中调用了函数portGET_HIGHEST_PRIORITY 来计算任务优先级记录中的最高任务优先级,函数portGET_HIGHEST_PRIORITY 实际上是一个宏定义,在p ortmacro.h 文件中有定义,具体的代码如下所示:
PendSV 异常何时触发
PendSV异常用于进行任务切换,当需要进行任务切换的时候,FreeRTOS 就会触发PendSV
异常,以进行任务切换。
FreeRTOS提供了多个用于触发任务切换的宏,如下所示:
从上面的代码中可以看到,这些后实际上最终都是调用了函数portYIELD(),函数实际上是一个宏定义,在portmacro.h 文件中有定于,具体的代码如下所示:
上面代码中宏portNVIC_INT_CTRL_REG 和宏p ortNVIC_PENDSVSET_BIT 在p ortmacro.h
文件中有定义,具体的代码如下所示:
在systick的服务函数里面判断的, 判断需要切换任务, 往中断状态控制寄存器中写入特定值, 触发PendSv中断。
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 - therefore the slightly faster vPortRaiseBASEPRI() function is used
* in place of portSET_INTERRUPT_MASK_FROM_ISR(). */
vPortRaiseBASEPRI();
{
/* Increment the RTOS tick. */
if( xTaskIncrementTick() != pdFALSE )
{
/* A context switch is required. Context switching is performed in
* the PendSV interrupt. Pend the PendSV interrupt. */
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
}
}
vPortClearBASEPRIFromISR();
}
/*-----------------------------------------------------------*/
既然FreeRTOS 的系统时钟节拍来自SysTick,那么FreeRTOS 系统时钟节拍的处理,自然就是在SysTick 的中断服务函数中完成的。在前面第2 .1.3 小节《修改SYSTEM 文件》中,修改了SysTick 的中断服务函数,SysTick 的中断服务函数定义在d elay.c 文件中,具体的代码如下所示:
从上面的代码可以看出,函数xPortSysTickHandler 调用了函数xTaskIncrementTick() 来处理系统时钟节拍。在调用函数x TaskIncrementTick() 前后分别屏蔽了受FreeRTOS 管理的中断和取消中断屏蔽,这是因为SysTick 的中断优先级设置为最低的中断优先等级,在SysTick 的中断中处理FreeRTOS 的系统时钟节拍时,并不希望收到其他中断的影响。在通过函数xTaskIncrementTick( 处理完系统时钟节拍和相关事务后,再根据函数xTaskIncrementTick 的返回值,决定是否进行任务切换,如果进行任务切换,就触发PendSV 异常,在本次SysTick 中断及其他中断处理完成后,就会进入PendSV 的中断服务函数进行任务切换,使用PendSV 进行切换任务的相关内容,请查看第九章《FreeRTOS 任务切换》。
接下来分析函数xTaskIncrementTick 是如何处理系统时钟节拍及相关事务的,函数x TaskIncrementTick() 在t ask.c 文件中有定义,具体的代码如下所示:
BaseType_t xTaskIncrementTick( void )
{
TCB_t * pxTCB;
TickType_t xItemValue;
BaseType_t xSwitchRequired = pdFALSE;
/* Called by the portable layer each time a tick interrupt occurs.
* Increments the tick then checks to see if the new tick value will cause any
* tasks to be unblocked. */
traceTASK_INCREMENT_TICK( xTickCount );
if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )
{
/* Minor optimisation. The tick count cannot change in this
* block. */
const TickType_t xConstTickCount = xTickCount + ( TickType_t ) 1;
/* Increment the RTOS tick, switching the delayed and overflowed
* delayed lists if it wraps to 0. */
xTickCount = xConstTickCount;
if( xConstTickCount == ( TickType_t ) 0U ) /*lint !e774 'if' does not always evaluate to false as it is looking for an overflow. */
{
taskSWITCH_DELAYED_LISTS();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/* See if this tick has made a timeout expire. Tasks are stored in
* the queue in the order of their wake time - meaning once one task
* has been found whose block time has not expired there is no need to
* look any further down the list. */
if( xConstTickCount >= xNextTaskUnblockTime )
{
for( ; ; )
{
if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE )
{
/* The delayed list is empty. Set xNextTaskUnblockTime
* to the maximum possible value so it is extremely
* unlikely that the
* if( xTickCount >= xNextTaskUnblockTime ) test will pass
* next time through. */
xNextTaskUnblockTime = portMAX_DELAY; /*lint !e961 MISRA exception as the casts are only redundant for some ports. */
break;
}
else
{
/* The delayed list is not empty, get the value of the
* item at the head of the delayed list. This is the time
* at which the task at the head of the delayed list must
* be removed from the Blocked state. */
pxTCB = listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList ); /*lint !e9079 void * is used as this macro is used with timers and co-routines too. Alignment is known to be fine as the type of the pointer stored and retrieved is the same. */
xItemValue = listGET_LIST_ITEM_VALUE( &( pxTCB->xStateListItem ) );
if( xConstTickCount < xItemValue )
{
/* It is not time to unblock this item yet, but the
* item value is the time at which the task at the head
* of the blocked list must be removed from the Blocked
* state - so record the item value in
* xNextTaskUnblockTime. */
xNextTaskUnblockTime = xItemValue;
break; /*lint !e9011 Code structure here is deemed easier to understand with multiple breaks. */
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/* It is time to remove the item from the Blocked state. */
listREMOVE_ITEM( &( pxTCB->xStateListItem ) );
/* Is the task waiting on an event also? If so remove
* it from the event list. */
if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL )
{
listREMOVE_ITEM( &( pxTCB->xEventListItem ) );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/* Place the unblocked task into the appropriate ready
* list. */
prvAddTaskToReadyList( pxTCB );
/* A task being unblocked cannot cause an immediate
* context switch if preemption is turned off. */
#if ( configUSE_PREEMPTION == 1 )
{
/* Preemption is on, but a context switch should
* only be performed if the unblocked task has a
* priority that is equal to or higher than the
* currently executing task. */
if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )
{
xSwitchRequired = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_PREEMPTION */
}
}
}
/* Tasks of equal priority to the currently running task will share
* processing time (time slice) if preemption is on, and the application
* writer has not explicitly turned time slicing off. */
#if ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) )
{
if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ pxCurrentTCB->uxPriority ] ) ) > ( UBaseType_t ) 1 )
{
xSwitchRequired = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) ) */
#if ( configUSE_TICK_HOOK == 1 )
{
/* Guard against the tick hook being called when the pended tick
* count is being unwound (when the scheduler is being unlocked). */
if( xPendedTicks == ( TickType_t ) 0 )
{
vApplicationTickHook();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_TICK_HOOK */
#if ( configUSE_PREEMPTION == 1 )
{
if( xYieldPending != pdFALSE )
{
xSwitchRequired = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_PREEMPTION */
}
else
{
++xPendedTicks;
/* The tick hook gets called at regular intervals, even if the
* scheduler is locked. */
#if ( configUSE_TICK_HOOK == 1 )
{
vApplicationTickHook();
}
#endif
}
return xSwitchRequired;
}
/*-----------------------------------------------------------*/
从上面的代码可以看到,函数xTaskIncrementTick 处理了系统时钟节拍、阻塞任务列表、时间片调度等。
处理系统时钟节拍,就是在每次SysTick 中断发生的时候,将全局变量xTickCount 的值加11,也就是将系统时钟节拍计数器的值加1 。
处理阻塞任务列表,就是判断阻塞态任务列表中是否有阻塞任务超时,如果有,就将阻塞时间超时的阻塞态任务移到就绪态任务列表中,准备执行。同时在系统时钟节拍计数器xTickCount 的加1 溢出后,将两个阻塞态任务列表调换,这是FreeRTOS 处理系统时钟节拍计数器溢出的一种机制。
处理时间片调度,就是在每次系统时钟节拍加1 后,切换到另外一个同等优先级的任务中运行,要注意的是,此函数只是做了需要进行任务切换的标记,在函数退出后,会统一进行任务切换,因此时间片调度导致的任务切换,也可能因为有更高优先级的阻塞任务就绪导致任务切换,而出现任务切换后运行的任务比任务切换前运行任务的优先级高,而非相等优先级。
到此, 整个RTOS的任务调度的原理大致就讲完的, 希望对有需要的人有所帮助。