Main函数中创建完任务后,当前的任务指针pxCurrentTCB就指向了第一个任务,但是此时系统并未启动,还需要启动任务调度器,任务调度器才是整个系统的核心,在启动器中创建了一个空闲任务,最主要的是启动软件定时器和初始化了一些中断、启动第一个任务。
//...符号是删除了一些不是很重要的源码
void vTaskStartScheduler( void )
{
BaseType_t xReturn;
#if( configSUPPORT_STATIC_ALLOCATION == 1 ) //宏定义申请空间方式,FreeRTOS中如果要静态申请空间,需要打开configSUPPORT_STATIC_ALLOCATION
{
...//静态创建空闲任务
}
#else
{
...//动态创建空闲任务
}
#endif
#if ( configUSE_TIMERS == 1 ) //使能软件定时器功能
{
if( xReturn == pdPASS )
{
xReturn = xTimerCreateTimerTask(); //创建定时器服务,在这里面完成一些定时器列表项初始化工作
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_TIMERS */
...//省略了一些预编译
portDISABLE_INTERRUPTS(); //关闭中断
...
if( xPortStartScheduler() != pdFALSE )
{
//如果调度器启动成功的话就不会运行到这里,函数不会有返回值
}
else
{
//调用函数xTaskEndScheduler()才会运行到这里
}
}
else
{
//当系统中没有足够的内存创建空闲任务和定时器任务时,说明内核没有启动成功
configASSERT( xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY );
}
...
}
该函数关注空闲任务的创建、xTimerCreateTimerTask()、xPortStartScheduler(),空闲任务创建过程与普通任务的创建过程是一样的,只不过这里规定空闲的任务优先级为0,也就是最低的优先级,xTimerCreateTimerTask()函数中创建了定时器列表,并进行了相关的初始化。调度器真正启动是在xPortStartScheduler()函数中。
//源码删减后
BaseType_t xPortStartScheduler( void )
{
#if( configASSERT_DEFINED == 1 )
{
volatile uint32_t ulOriginalPriority;
//中断优先级寄存器0:IPR0
volatile uint8_t * const pucFirstUserPriorityRegister = ( uint8_t * ) ( portNVIC_IP_REGISTERS_OFFSET_16 + portFIRST_USER_INTERRUPT_NUMBER );
volatile uint8_t ucMaxPriorityValue;
//保存中断优先级值,因为下面要覆写这个寄存器
ulOriginalPriority = *pucFirstUserPriorityRegister;
//确定有效的优先级个数,先向每个位写入1,再读出来,无效位读出来永远为0,所以有多少个1就有多少个有效位
*pucFirstUserPriorityRegister = portMAX_8_BIT_VALUE;
ucMaxPriorityValue = *pucFirstUserPriorityRegister;
configASSERT( ucMaxPriorityValue == ( configKERNEL_INTERRUPT_PRIORITY & ucMaxPriorityValue ) );
/* 冗余代码,再次设置FreeRTOS可管理的优先级,此段代码用来确保开发者不正确的设置RTOS可屏蔽中断优先级值*/
ucMaxSysCallPriority = configMAX_SYSCALL_INTERRUPT_PRIORITY & ucMaxPriorityValue;
//计算最大优先级组值
ulMaxPRIGROUPValue = portMAX_PRIGROUP_BITS;
while( ( ucMaxPriorityValue & portTOP_BIT_OF_BYTE ) == portTOP_BIT_OF_BYTE )
{
ulMaxPRIGROUPValue--;
ucMaxPriorityValue <<= ( uint8_t ) 0x01;
}
...//省略一些预编译代码
ulMaxPRIGROUPValue <<= portPRIGROUP_SHIFT;
ulMaxPRIGROUPValue &= portPRIORITY_GROUP_MASK;
*pucFirstUserPriorityRegister = ulOriginalPriority; //将原先寄存器的值写回
}
#endif
/* Make PendSV and SysTick the lowest priority interrupts. */
portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI; //设置PendSV的中断优先级,为最低优先级
portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI; //设置滴答定时器的中断优先级,为最低优先
//设置滴答定时器时钟周期、使能中断
vPortSetupTimerInterrupt();
//初始化临界区嵌套计数器
uxCriticalNesting = 0;
//开始第一个任务
prvStartFirstTask();
//开始第一个任务后不会到达这里
return 0;
}
在该函数中,配置了一些中断、滴答定时器的时钟周期、初始化临界区嵌套计数器(其实是一个全局变量)之后通过调用由汇编编写成的函数prvStartFirstTask()
__asm void prvStartFirstTask( void )
{
PRESERVE8
/*cortex-M3中 0xE000ED08地址处为VTOR(向量表偏移量)寄存器,存储向量表起始地址*/
ldr r0, =0xE000ED08
/*取出向量表内容*/
ldr r0, [r0]
/* 取出向量表中的第一项,向量表第一项存储主堆栈指针MSP的初始值*/
ldr r0, [r0]
/*将堆栈地址存入主堆栈指针MSP*/
msr msp, r0 /*将r0中的值放到主堆栈指针msp中*/
/* Globally enable interrupts. */
/*使能全局中断*/
cpsie i
cpsie f
dsb
isb
/* Call SVC to start the first task. */
/*调用SVC启动第一个任务*/
svc 0
nop
nop
}
从中断向量表中取出MSP的初始值,前三步指令就是为了得到MSP初始值,然后将MSP初始值放入msp中,复位msp,至此,MSP指针归FreeRTOS系统接管,接着使能全局中断,调用SVC启动第一个任务。
关于SVC,有一篇博文讲得很好https://blog.csdn.net/minsophia/article/details/53068097
再看看调用SVC后,系统响应SVC后进行的动作
__asm void vPortSVCHandler( void )
{
PRESERVE8
/*获取当前的第一个任务,即最高优先级任务*/
ldr r3, =pxCurrentTCB
/*获取任务TCB的地址*/
ldr r1, [r3]
/*获取任务TCB的第一个成员,即TCB堆栈的栈顶*/
ldr r0, [r1]
/*寄存器r4~r11出栈*/
ldmia r0!, {r4-r11}
/*最新的堆栈指针赋给任务堆栈指针psp*/
msr psp, r0
isb
/*这里使能了中断*/
mov r0, #0
msr basepri, r0
/* 这里0x0d表示:返回后进入线程模式,从进程堆栈中做出栈操作,返回Thumb状态*/
orr r14, #0xd
bx r14
}
任务创建完成后,每个任务都会被初始化,完成之后会返回最高优先级的任务控制块,即运行的第一个任务pxCurrentTCB 永远指向当前运行的任务,因为任务对应的值放在寄存器中,而在切换时,又通过寄存器存放在栈中,所以将R3中的值放在R1中,是为了去获得栈顶,把寄存器值出栈。接着将R4~R11寄存器出栈,此时栈顶指针指在参数R0处,从中断中退出或恢复后,任务栈指针psp从R0开始处恢复其它寄存器值。
由于Cortex-M3和M4内核具有双堆栈指针,MSP主堆栈指针和PSP进程堆栈指针,或者叫PSP任务堆栈指针也是可以的。在FreeRTOS操作系统中,主堆栈指针MSP是给系统栈空间使用的,进程堆栈指针PSP是给任务栈使用的。也就是说,在FreeRTOS任务中,所有栈空间的使用都是通过PSP指针进行指向的。一旦进入了中断函数以及可能发生的中断嵌套都是用的MSP指针。这个知识点要记住它,当前可以不知道这是为什么,但是一定要记住。中断函数和中断嵌套会使用到系统栈空间
注意:一旦进入vTaskStartScheduler()函数是不会有返回的,一旦产生返回值,就说明系统没有足够的heap空间去创建空闲任务过着定时器任务。