系列一:微型操作系统内核源码详解系列一:rtos内核源码概论篇(以freertos为例)-CSDN博客
系列二:微型操作系统内核源码详解系列二:数据结构和对象篇(以freertos为例)-CSDN博客
系列三:微型操作系统内核源码详解系列三(0):空间存储及内存管理篇(前置篇)-CSDN博客
微型操作系统内核源码详解系列三(1):任务及切换篇(任务函数定义)-CSDN博客
微型操作系统内核源码详解系列三(2):任务及切换篇(任务函数定义)-CSDN博客
微型操作系统内核源码详解系列三(3):任务及切换篇(任务函数定义)-CSDN博客
微型操作系统内核源码详解系列三(4):arm架构篇-CSDN博客
微型操作系统内核源码详解系列三(5):进程与线程-CSDN博客
系列四:
微型操作系统内核源码详解系列四(1):操作系统调度算法(linux0.11版本内核)-CSDN博客
微型操作系统内核源码详解系列四(2):操作系统调度算法(rt-thread内核)-CSDN博客
微型操作系统内核源码详解系列四(3):操作系统调度算法(FreeRTOS内核篇上)-CSDN博客
微型操作系统内核源码详解系列四(4):操作系统调度算法(FreeRTOS内核篇下)-CSDN博客
系列五:
微型操作系统内核源码详解系列五(1):arm cortex m3架构-CSDN博客
微型操作系统内核源码详解系列五(2):cm3下栈的初始化-CSDN博客
微型操作系统内核源码详解系列五(3):cm3下调度的开启-CSDN博客
上一篇文章,我们已经完成了栈的初始化,接下来笔者将会讲解调度器是如何配置并且开启第一个任务的。
在此之前,让我们重新看看官方的指导:
PSP和MSP分别是cm3下的两个堆栈指针,任何情况下只能使用其中一个。
笔者在系列五的博客截取了很多张cm3手册中的信息,这些都是至关重要的的信息,是指导如何写出一个月FreeRTOS的蓝图,请读者一定要把这些信息认真看完。
如果读者阅读到某一篇博客时感觉一点都看不懂了,那么笔者推荐把所在系列从头到尾认认真真看一遍,或者把全系列看一遍,因为笔者描述思路时比较零散,你可能要看到后面才能看懂前面的东西。如果发现实在是看不懂,大可不必纠结,可以选择搁置一段时间再来重新阅读。
通过上文,大概可以知道:在cm3下的双堆栈中,特权级别的代码会使用MSP,用户级别的代码会使用PSP。
这就是任务的轮转调度中断部分,任务的切换主要是由systick中断和PendSV中断完成。
有读者可能好奇,既然调度器的职责是完成上一个认为与下一个任务的切换,也就是说我们只需要在PendSV中断里面保存上一个任务的状态,再加载下一个任务的状态就行了,那么第一个任务从哪里来呢?
为了开启第一个任务,我们使用SVC调用,SVC调用是特权级的中断,毫无疑问更安全。
SVC系统调用主要职责就是开启os下的第一个任务。
但是调用SVC中断前需要进行配置:
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;
}
/* 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 */
/* Make PendSV and SysTick the lowest priority interrupts. */
portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
portNVIC_SYSPRI2_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;
/* Ensure the VFP is enabled - it should be anyway. */
prvEnableVFP();
/* Lazy save always. */
*( portFPCCR ) |= portASPEN_AND_LSPEN_BITS;
/* Start the first task. */
prvStartFirstTask();
/* Should not get here! */
return 0;
}
上面的函数,宏定义里面的configASSERT_DEFINED是断言,用来调试,让我们关注这些关键部分:
解释一下,这段代码的作用是:配置pendsv和systck中断的优先级为最低(防止打断其他中断任务),然后设置systck定时器产生ISR(中断服务程序),就是设置SysTick定时器以生成系统tick中断。uxCriticalNesting用来记录进入临界区的次数,prvEnableVFP作用是支持浮点数运算,下一行代码的作用是设置浮点上下文寄存器以启用惰性保存机制,之后就是开启第一个任务的入口函数。
顺便一提,不能使用systick中断进行任务上下文切换的原因是:systick响应会抢占中断,但是OS不允许中断过程中执行上下文切换。
下面这个带firsttask命名的函数的作用还不是开启第一个任务,它的作用是产生SVC调用,SVC调用里面的函数才会把第一个任务的状态加载到CPU,这才是开启第一个任务。
ldr这三行代码的作用是--找到主栈的栈顶指针:
关于它是如何找到栈顶指针的,可以参考下图:
简单来说就是,先找到向量表偏移量寄存器,再找到向量表,向量表记录了msp的初始值:
svc下是特权模式运行,所以使用msp作为堆栈指针。这段代码将栈顶指针的值存储到msp,现在msp就指向栈顶指针了:
之后的四行代码就是开启全局中断和异常:
毫无疑问,这里就是开启svc中断响应了,nop指令表示延时,在计算机体系架构中,很多指令并不能持续执行,可能需要延时,如果读者学过流水线这些可能会知道nop的重要性:
到这里,SVC调用将会被响应,我们将会迎来arm架构下最精彩的部分:任务的开启与切换。