(三)FreeRTOS调度器开启过程分析

         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空间去创建空闲任务过着定时器任务。

你可能感兴趣的:(FreeRTOS)