调度器是FreeRTOS操作系统的核心,主要负责任务切换,即在就绪任务中找出最高优先级的任务,并使之获得CPU运行权。调度器并非自动运行的,需要人为启动它。
API函数vTaskStartScheduler()用于启动调度器,它会创建一个空闲任务、初始化一些静态变量,最主要的,它会初始化系统节拍定时器并把SVC中断和Pendsv中断设置为最低优先级,在prvStartFirstTask函数里开全局中断及触发SVC异常,接下来进入SVC中断服务函数vPortSVCHandler,这个函数只要是把pxCurrentTCB 这个结构体指向的任务栈信息加载到CPU的寄存器中,{r4-r11,r14}需要手动加载,其他的栈的信息回自动加载到cpu的其他寄存器中,在这里第一个要运行的任务已经准备好了,此时会进入systick中断函数判断是否需要切换任务,如果需要则触发Pendsv异常,接着进入Pendsv中断服务函数,如果只创建了一个任务,在Pendsv服务程序就是执行这个任务,如果创建了多个任务则会Pendsv服务程序里选择在启动第就绪态最高优先级的任务来执行。本文以Cortex-M4架构为例。
先来看启动调度器的API函数vTaskStartScheduler(),源码精简后如下所示:
void vTaskStartScheduler( void )
{
BaseType_t xReturn;
StaticTask_t *pxIdleTaskTCBBuffer= NULL;
StackType_t *pxIdleTaskStackBuffer= NULL;
uint16_t usIdleTaskStackSize =tskIDLE_STACK_SIZE;
/*如果使用静态内存分配任务堆栈和任务TCB,则需要为空闲任务预先定义好任务内存和任务TCB空间*/
#if(configSUPPORT_STATIC_ALLOCATION == 1 )
{
vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer, &pxIdleTaskStackBuffer, &usIdleTaskStackSize);
}
#endif /*configSUPPORT_STATIC_ALLOCATION */
/* 创建空闲任务,使用最低优先级*/
xReturn =xTaskGenericCreate( prvIdleTask, "IDLE",usIdleTaskStackSize, ( void * ) NULL, ( tskIDLE_PRIORITY | portPRIVILEGE_BIT), &xIdleTaskHandle,pxIdleTaskStackBuffer,pxIdleTaskTCBBuffer, NULL );
if( xReturn == pdPASS )
{
/* 先关闭中断,确保节拍定时器中断不会在调用xPortStartScheduler()时或之前发生.当第一个任务启动时,会重新启动中断*/
portDISABLE_INTERRUPTS();
/* 初始化静态变量 */
xNextTaskUnblockTime = portMAX_DELAY;
xSchedulerRunning = pdTRUE;
xTickCount = ( TickType_t ) 0U;
/* 如果宏configGENERATE_RUN_TIME_STATS被定义,表示使用运行时间统计功能,则下面这个宏必须被定义,用于初始化一个基础定时器/计数器.*/
portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();
/* 设置系统节拍定时器,这与硬件特性相关,因此被放在了移植层.*/
if(xPortStartScheduler() != pdFALSE )
{
/* 如果调度器正确运行,则不会执行到这里,函数也不会返回*/
}
else
{
/* 仅当任务调用API函数xTaskEndScheduler()后,会执行到这里.*/
}
}
else
{
/* 执行到这里表示内核没有启动,可能因为堆栈空间不够 */
configASSERT( xReturn );
}
/* 预防编译器警告*/
( void ) xIdleTaskHandle;
}
这个API函数首先创建一个空闲任务,空闲任务使用最低优先级(0级),空闲任务的任务句柄存放在静态变量xIdleTaskHandle中,可以调用API函数xTaskGetIdleTaskHandle()获得空闲任务句柄。
如果任务创建成功,则关闭中断portDISABLE_INTERRUPTS();(调度器启动结束时会再次使能中断的),初始化一些静态变量,然后调用函数xPortStartScheduler()来启动系统节拍定时器并启动第一个任务。
注意:
portDISABLE_INTERRUPTS();这个函数在不同的架构中需要修改,主要就是关闭中断,具体要关闭那些中断可以根据项目的具体需求,我这里是关闭了除nmi和复位的其他所有中断。
因为设置系统节拍定时器涉及到硬件特性,因此函数xPortStartScheduler()由移植层提供,不同的硬件架构,这个函数的代码也不相同。但是大体的思路是一样的。
对于Cortex-M4架构,函数xPortStartScheduler()的实现如下所示:
BaseType_t xPortStartScheduler( void )
{
/* configMAX_SYSCALL_INTERRUPT_PRIORITY must not be set to 0.
See http://www.FreeRTOS.org/RTOS-Cortex-M3-M4.html */
configASSERT( configMAX_SYSCALL_INTERRUPT_PRIORITY );
/* This port can be used on all revisions of the Cortex-M7 core other than
the r0p1 parts. r0p1 parts should use the port from the
/source/portable/GCC/ARM_CM7/r0p1 directory. */
configASSERT( portCPUID != portCORTEX_M7_r0p1_ID );
configASSERT( portCPUID != portCORTEX_M7_r0p0_ID );
#if( configASSERT_DEFINED == 1 )
{
volatile uint32_t ulOriginalPriority;
volatile uint8_t * const pucFirstUserPriorityRegister = ( volatile uint8_t * const ) ( 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;
/* 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 /* conifgASSERT_DEFINED */
/* 将PendSV和SysTick中断设置为最低优先级*/
portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;
/* 启动系统节拍定时器,即SysTick定时器,初始化中断周期并使能定时器*/
vPortSetupTimerInterrupt();
/* Initialise the critical nesting count ready for the first task. */
uxCriticalNesting = 0;
/* Ensure the VFP is enabled - it should be anyway. */
vPortEnableVFP();
/* Lazy save always. */
*( portFPCCR ) |= portASPEN_AND_LSPEN_BITS;
/* Start the first task. */
prvPortStartFirstTask();
/* Should never get here as the tasks will now be executing! Call the task
exit error function to prevent compiler warnings about a static function
not being called in the case that the application writer overrides this
functionality by defining configTASK_RETURN_ADDRESS. Call
vTaskSwitchContext() so link time optimisation does not remove the
symbol. */
vTaskSwitchContext();
prvTaskExitError();
/* Should not get here! */
return 0;
}
在Cortex-M4架构中,FreeRTOS为了任务启动和任务切换使用了三个异常:SVC、PendSV和SysTick。SVC(系统服务调用)用于任务启动(即第一个任务的准备工作),有些操作系统不允许应用程序直接访问硬件,而是通过提供一些系统服务函数,通过SVC来调用;PendSV(可挂起系统调用)用于完成任务切换,它的最大特性是如果当前有优先级比它高的中断在运行,PendSV会推迟执行,直到高优先级中断执行完毕;SysTick用于产生系统节拍时钟(中断的时间要控制好),提供一个时间片,如果多个任务共享同一个优先级,则每次SysTick中断,下一个任务将获得一个时间片。关于详细的SVC、PendSV异常描述,推荐《Cortex-M3权威指南》一书的“异常”部分。
这里将PendSV和SysTick异常优先级设置为最低,这样任务切换不会打断某个中断服务程序,中断服务程序也不会被延迟,这样简化了设计,有利于系统稳定。
接下来调用函数vPortSetupTimerInterrupt()设置SysTick定时器中断周期并使能定时器运行这个函数比较简单,就是设置SysTick硬件的相应寄存器。不同的架构有所不同。
接下来有一个关键的函数是prvStartFirstTask(),这个函数用来启动第一个任务。我们先看一下源码:我这是在ubuntu上用GCC 编译的,在keil上编译的话编写格式不一样。
static void prvPortStartFirstTask( void )
{
/* Start the first task. This also clears the bit that indicates the FPU is
in use in case the FPU was used before the scheduler was started - which
would otherwise result in the unnecessary leaving of space in the SVC stack
for lazy saving of FPU registers. */
__asm volatile(
/* Cortext-M4硬件中,0xE000ED08地址处为VTOR(向量表偏移量)寄存器,存储向量表起始地址*/
" ldr r0, =0xE000ED08 \n" /* Use the NVIC offset register to locate the stack. */
" ldr r0, [r0] \n"
" ldr r0, [r0] \n"
" msr msp, r0 \n" /* Set the msp back to the start of the stack. */
/* 将堆栈地址存入主堆栈指针 */
" mov r0, #0 \n" /* Clear the bit that indicates the FPU is in use, see comment above. */
" msr control, r0 \n"
/* 使能全局中断*/
" cpsie i \n" /* Globally enable interrupts. */
" cpsie f \n"
" dsb \n"
" isb \n"
/* 调用SVC启动第一个任务 */
" svc 0 \n" /* System call to start first task. */
" nop \n"
);
}
/*-----------------------------------------------------------*/
首先复位主堆栈指针MSP的值,表示从此以后MSP指针被FreeRTOS接管,需要注意的是,Cortex-M3硬件的中断也使用MSP指针。之后使能中断,使用汇编指令svc 0触发SVC中断,完成启动第一个任务的工作。我们看一下SVC中断服务函数:
void vPortSVCHandler( void )
{
__asm volatile (
" ldr r3, =pxCurrentTCB \n" /* (1) Restore the context. */
" ldr r1, [r3] \n" /* (2)Use pxCurrentTCBConst to get the pxCurrentTCB address. */
" ldr r0, [r1] \n" /* (3)The first item in pxCurrentTCB is the task top of stack. */
" ldmia r0!, {r4-r11, r14} \n" /* (4) Pop the registers that are not automatically saved on exception entry and the critical nesting count. */
" msr psp, r0 \n" /* (5)Restore the task stack pointer. */
" isb \n" // (5)
" mov r0, #0 \n" // (6)
" msr basepri, r0 \n" // (7)
" bx r14 \n" // (8)
" \n"
" .align 4 \n" // (9)
);
}
(1) pxCurrentTCB是一个全局变量指针,用来指向当前正在运行或者即将运行任务的任务控制块。 加载pxCurrentTCB任务控制块的地址到R3中;注意这个pxCurrentTCB的地址永远不会变,但是在在任务切换的时候会改变里的值,用来指向不同的任务块。
(2) 载pxCurrentTCB到R1
(3) 加载pxCurrentTCB指向的任务控制块到R0中,因为任务控制块中的第一个成员变量就是任务的栈顶指针: pxTopOfStack。
(4) 以r0为基地址(指针先加后操作)将栈中的8个字的数据加载到CPU中的R4-R11中。
(5) 将操作5后新的栈顶指针R0更新到PSP中(注意:在执行异常的时候,SP以MSP为栈指针,在执行任务时SP以PSP为栈指针);
(6) 执行7之前清流水线,确保操作6指令执行完毕。清除R0;
(7) 设置BASEPR寄存器为0 即打开所有中断;
(8) 两个操作具体解释如下
当r14为0xFFFFFFFX,执行是中断返回指令,
cortext-m3的做法,X的bit0为1表示返回thumb状态,bit1和bit2分别表示返回后sp用msp还是psp、以及返回到特权模式还是用户模式;
异常返回,这个时候栈中的剩下内容将会自动加载到CPU寄存器:xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务的形参)同时PSP的值也将更新,即指向任务栈的栈顶 。
0x0D 表示返回后为thumb状态,且指定SP出栈指针使用PSP作为出栈指针,这里的栈指针指向的准备运行的栈,所有在执行BX LR后,在恢复现场时会以当前的PSP为基地址,把栈中的剩下内容将会自动加载到CPU寄存器: xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务的形参)中,又因为跳转是BX 无链接,所以跳转后的R15 PC会被从新的PC从堆栈中恢复到R15(PC)寄存器中,所以程序会从该新任务入口函数开始执行。
(9) 4个字节对齐
在这里第一个任务的准备工作已经准备完了,接下来就是等待systick中断的到来触发Pendsv异常,在Pendsv里进行任务切换。
xPortSysTickHandler函数的源码如下:
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. */
portDISABLE_INTERRUPTS();//关闭中断。不同架构关闭中断的方式不一样,在移植在RSIC-V的架构中这里个函数是需要改变的
{
/* 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; //触发pendsv异常
}
}
portENABLE_INTERRUPTS();//打开中断
}
从上面可以看到xPortSysTickHandler函数主要就是关中断,判断是否有任务需要切换,有就触发Pendsv异常,最后打开i中断。
任务切换
在FreeROTS任务的切换实际由xPortPendSVHandler函数完成,主要完成上下文切换即保存上文,切换下文两个主要目的操作;
切换前我们需要了解上文需要保存什么,下文切换的又是什么;
上文中保存的主要内容是:
(1).寄存器中的R4-R11数据(其余的进入异常前CPU自动保存);在cortex-m4在还包括R14寄存器
(2).当前任务的栈顶指针;
(3).全局TCB地址;
下文切换需要切换的内容主要是:
(1).最新任务的TCB;
(2).最新任务中栈中的r4-r11数据到CPU R4-R11;在cortex-m4在还包括R14寄存器
(3)新任务栈顶存入PSP,用来出栈根据PSP调用新任务;
主要流程如下图所示:在cortex-m4在还包括R14寄存器
![在这里插入图片描述](https://img-blog.csdnimg.cn/20190110142056787.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzE1MTAwMzc5,size_16,color_FFFFFF,t_70)
xPortPendSVHandler函数的代码如下:
void xPortPendSVHandler( void )
{
/* This is a naked function. */
__asm volatile
(
" mrs r0, psp \n" //(1)
" isb \n" //(2)
" \n"
" ldr r3, pxCurrentTCBConst \n" //(3) /* Get the location of the current TCB. */
" ldr r2, [r3] \n" //(4)
" \n"
//如果你的Cortex-M4架构支持FPU 就是打开FPU的编译选项
//" tst r14, #0x10 \n" /* Is the task using the FPU context? If so, push high vfp registers. */
//" it eq \n"
//" vstmdbeq r0!, {s16-s31} \n"
" \n"
" stmdb r0!, {r4-r11, r14} \n" //(5) /* Save the core registers. */
" str r0, [r2] \n" //(6) /* Save the new top of stack into the first member of the TCB. */
" \n"
" stmdb sp!, {r0, r3} \n" //(7)
" mov r0, %0 \n" //(8)
" msr basepri, r0 \n" //(9)
" dsb \n" //(10)
" isb \n" //(11)
" bl vTaskSwitchContext \n" //(12)
" mov r0, #0 \n" //(13)
" msr basepri, r0 \n" //(14)
" ldmia sp!, {r0, r3} \n" //(15)
" \n"
" ldr r1, [r3] \n" //(16)/* The first item in pxCurrentTCB is the task top of stack. */
" ldr r0, [r1] \n" //(17)
" \n" //(18)
" ldmia r0!, {r4-r11, r14} \n" //(18)/* Pop the core registers. */
" \n"
//如果你的Cortex-M4架构支持FPU 就是打开FPU的编译选项
//" tst r14, #0x10 \n" /* Is the task using the FPU context? If so, pop the high vfp registers too. */
//" it eq \n"
//" vldmiaeq r0!, {s16-s31} \n"
" \n"
" msr psp, r0 \n" //(19)
" isb \n" //(20)
" \n"
#ifdef WORKAROUND_PMU_CM001 /* XMC4000 specific errata workaround. */
#if WORKAROUND_PMU_CM001 == 1
" push { r14 } \n"
" pop { pc } \n"
#endif
#endif
" \n"
" bx r14 \n" //(21)
" \n"
" .align 4 \n" //(22)
"pxCurrentTCBConst: .word pxCurrentTCB \n"
::"i"(configMAX_SYSCALL_INTERRUPT_PRIORITY)
);
}
说明:
上文的保存,指的是自动入栈的CPU寄存器
注意:在进入该函数前,系统(CPU)会自动的将上一个任务的运行的环境即: xPSR,PC(任务入口地址),R12,R3,R2,R1,R0(任务的形参)这些寄存器的值会存储到任务的栈中,是自动完成的。
(1) 进入到xPortPendSVHandler函数中第一步要做的是将剩下的 R4~R11,R14手动保存入栈,同时PSP 会自动更新(在更新之前 PSP 指向任务栈的栈顶)。
(3)将pxCurrentTCB的地址加载到R3中;pxCurrentTCB的地址的值存放的是任务块的指针,这个值是会改变的,通常任务切换到其他的任务就是改这个值,指向需要切换的任务块的指针。
(4)将R3中指向的内容加载到R2中R2 =pxCurrentTCB ;
(5)先将R0指向地址递减在以新R0为及地址将CPU中的R4-R11,R14手动入栈,即保存到当前任务栈中,把R14(LR)寄存器入栈保存,是因为调用vTaskSwitchContext函数返回时,CPU会把返回地址自动保存到LR寄存器中,R14(LR)寄存器的值被覆盖故需要进入入栈保护处理。
(6)将R0的值存储到R2指向的内容,因为R2等于pxCurrentTCB,而pxCurrentTCB的第一个成员为pxTopOfStack,所以就是将R0存储到的上一个任务的pxTopOfStack任务栈顶指针中。完成了上文的保存;
(7)把R3一并入栈保存是因为下面要调用vTaskSwitchContext函数来切换任务控制块(实际就是把需要切换的任务块的结构体指针存放在pxCurrentTCB的地址里),可能会用到R3,把R3的值覆盖,而我们还需要把原来的R3来当作新的pxCurrentTCB,所以保险起见一并把R3也保存了起来;至于为什么要把R0入栈,还不知到原因。后面出栈的时候会被覆盖
上下文切换;
(8)把0值存到R0中,用来屏蔽BASEPRI的值;
(9)关闭部分高优先级中断;
(10)(11)清流水线
(12)调用vTaskSwitchContext函数用来更新下一个需要运行任务的任务控制块pxCurrentTCB;
(13)把0值存到R0中,用来屏蔽BASEPRI的值;
(14)退出临界段,开中断,直接往 BASEPRI写 0;
(15)从堆栈中恢复R3,R0的值,
(16)将将要运行任务的任务控制块pxCurrentTCB的地址指向的内容即任务控制块本身加载到R1中;
(17)加载R1中指向的内容即将要运行的任务的栈顶指针到R0中;
(18)把R0为基地址,将下一个将要运行任务的栈加载到R4-R11,R14寄存器中,此时的R0的指向已经变化;
(19)将R0的值更新到PSP中,用来退出异常后以PSP为及地址,将新任务栈中剩下的内容加载到CPU寄存器中,然后清流水线,保证此操作完成后在执行下一条指令。
(20)清流水线
(21) 异常发生时,R14中保存异常返回标志,包括返回后进入线程模式还是处理器模式、使用PSP堆栈指针还是MSP堆栈指针,当调用 bx r14指令后,硬件会知道要从异常返回, 然后出栈,这个时候堆栈指针PSP已经指向了新任务堆栈的正确位置,当新任务的运行地址及参数r0,r1,r2,r3,r12,r15,xpsr自动加载到R0,R1,R2,R3,R12,R15(PC)以及xPSR寄存器后,CPU根据被出栈到PC寄存器的值,执行新的任务指令。
(22) 4字节对齐
到此任务切换就已经准备完毕,等这个函数返回时,CPU已经指向新的任务了,
仅供个人笔记和学习参考使用
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)