FreeRTOS学习记录----任务切换


首先,先上结构图,请对照代码理解。

(一)什么是任务切换?

  任务切换就是在就绪列表里面寻找优先级最高的就绪任务,然后执行该任务。

(二)任务什么时候切换?

  1)、当执行系统调用的时候,进行任务切换。

  2)、当发生滴答定时器(systick)中断的时候,进行任务切换。

情况1:执行系统调用时

  所谓的系统调用就是指执行taskYIELD()函数。taskYIELD()是一个宏。
   

#define taskYIELD()    portYIELD()

  接着往下看 portYIELD()函数,它也是一个宏。

#define portYIELD() 
{ 
    portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; 

    __dsb( portSY_FULL_READ_WRITE ); 
    __isb( portSY_FULL_READ_WRITE ); 
}

  通过向中断控制和状态寄存器ICSR的bit28写入1挂起PendSV,来启动PendSV中断。portNVIC_PENDSVSET_BIT 的值为如下宏定义。

#define portNVIC_PENDSVSET_BIT        ( 1UL << 28UL )

  

情况2:发生滴答定时器(systick)中断的时候

  OK,第一种情况很简单,来看看第二种情况,

void SysTick_Handler(void)
{
    if(xTaskGetSchedulerState()!=taskSCHEDULER_NOT_STARTED)//系统已经运行
    {
      xPortSysTickHandler();
    }
}

  定时器中断中,调用了 xPortSysTickHandler()函数。此函数具体如下:

void xPortSysTickHandler( void )
{
    vPortRaiseBASEPRI();  //关闭中断
    {
        if( xTaskIncrementTick() != pdFALSE ) //增加时钟计数器 xTickCount 的值
        {
        portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;  //置为ICSR寄存器bit28来挂起PendSV异常。
        }
    }
    vPortClearBASEPRIFromISR(); //打开中断
}

 可以看出 xPortSysTickHandler()函数,和portYIELD()函数是通过一样的方法来启动任务切换的。

(三)如何进行任务切换?

  PendSV中断中的xPortPendSVHandler()是真正实现任务切换的地方,我们来看看源码:

__asm void xPortPendSVHandler( void )
{
    extern uxCriticalNesting;
    extern pxCurrentTCB;
    extern vTaskSwitchContext;

    PRESERVE8        //栈的8字节对齐

    mrs r0, psp          //读取当前psp进程指针,存入r0
    isb
    /* 获取当前任务控制块 */
    ldr    r3, =pxCurrentTCB  //把pxCurrentTCB的地址给R3,(注意pxCurrrentTCB本身是指针变量),所以r3是地址的地址。
    ldr    r2, [r3]        //把r3地址中的值给r2,r2中就存储当前的任务控制块
    stmdb r0!, {r4-r11, r14}    // 以R0为基地址,依次向下递减,将寄存器r4-r11存储到任务栈。             

    /* 保存最新的栈顶指针到当前任务控制块的第一字段*/
    str r0, [r2]          //把r0的值存入r2的地址,相当于*r2 = r0

    stmdb sp!, {r3}      //将寄存器R3的值临时压栈,寄存器r3中仍然保存着当前任务的任务控制块,
                         //而接下来要调用函数vTaskSwitchContext,防止r3的值被改写,故临时压栈
    
    mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
    msr basepri, r0         //关中断,进入临界区
    
    dsb
    isb
    bl vTaskSwitchContext  //调用函数vTaskSwitchContext,此函数用来获取下一个要运行的任务,并将pxCurrentTCB更新为要运行的这个任务
    
    mov r0, #0
    msr basepri, r0      //开中断,退出临界区
    
    ldmia sp!, {r3}   //刚刚保存的寄存器R3的值出栈,恢复寄存器R3的值。注意,经过调用函数vTaskSwitchContext,此时
                      //pxCurrentTCB的值已经改变了,所以读取R3所保存的地址处的数据就会发现其值改变了,成
                      //为了下一个要运行的任务的任务控制块。


    ldr r1, [r3]
    ldr r0, [r1]     //获取新的运行任务的栈顶,并存到r0中去

    /* 出栈内核寄存器中的值 */
    ldmia r0!, {r4-r11, r14}  //含义::依次出栈 ,将任务栈的值依次出栈赋值给r4-r11。地址向上递增。  

    msr psp, r0 //更新进程栈指针PSP的值
    isb
    bx r14    //执行此行代码以后硬件自动恢复寄存器R0~R3、R12、LR、PC和xPSR的值,
          确定异常返回以后应该进入处理器模式还是进程模式,使用主栈指针(MSP)还是进程栈指针(PSP)。         
          很明显这里会进入进程模式,并且使用进程栈指针(PSP), 寄存器PC值会被恢复为即将运行的任务的任务函数,新的任务开始运行!
         
}

  ok。这段PendSV中断服务函数还是比较难以理解的,不懂的可以跳过。接下来看看调用的vTaskSwitchContext()来获取下一个要运行的任务是怎么操作的。

void vTaskSwitchContext( void )
{
    if( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE ) //如果任务调度器挂起,那么不进行任务切换
    {
        xYieldPending = pdTRUE;       
    }
    else
    {
        xYieldPending = pdFALSE;
        traceTASK_SWITCHED_OUT();
     taskCHECK_FOR_STACK_OVERFLOW();
     taskSELECT_HIGHEST_PRIORITY_TASK(); //调用函数 taskSELECT_HIGHEST_PRIORITY_TASK()获取下一个要运行的任务。
     traceTASK_SWITCHED_IN();
  } 
} 

  taskSELECT_HIGHEST_PRIORITY_TASK()本质上是一个宏,在 tasks.c 中有定义。
  FreeRTOS 中查找下一个要运行的任务有两种方法:一个是通用的方法,另外一个就是使用
硬件的方法,这个在我们讲解 FreeRTOSCofnig.h 文件的时候就提到过了,至于选择哪种方法通
过宏 configUSE_PORT_OPTIMISED_TASK_SELECTION 来决定的。当这个宏为 1 的时候就使
用硬件的方法,否则的话就是使用通用的方法。
 

1、通用方法:

#define taskSELECT_HIGHEST_PRIORITY_TASK() 
{ 
    UBaseType_t uxTopPriority = uxTopReadyPriority; 
    while( listLIST_IS_EMPTY( &( pxReadyTasksLists[ uxTopPriority ] ) ) ) 
    {
    configASSERT( uxTopPriority ); 
    --uxTopPriority; 
    } 
  listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) ); 
  uxTopReadyPriority
= uxTopPriority; }

  pxReadyTasksLists[]为就绪任务列表数组,每一个优先级都有一个就绪列表。通用方法就是通过while循环,从最高优先级 uxTopReadyPriority开始,

循环判断就绪列表中,哪个不为空。然后再将 uxTopPriority递减,并且记录有就绪任务的优先级。

  找到了有就绪任务的优先级之后,接下来调用 listGET_OWNER_OF_NEXT_ENTRY()来获得列表中的一个列表项,然后将获得列表项所获取到的任务控制块给pxCurrentTCB,这样就找到了下一个要运行的任务。
      这种方法对于任务的数量没有限制,效率不高。

2、硬件方法:

#define taskSELECT_HIGHEST_PRIORITY_TASK() 
{ 
    UBaseType_t uxTopPriority; 
portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority); //获取处于就绪态的最高优先级;
configASSERT( listCURRENT_LIST_LENGTH(
& ( pxReadyTasksLists[ uxTopPriority ] ) )> 0 );
listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB,
&( pxReadyTasksLists[ uxTopPriority ] ) ); //这一步与通用方法一样;获得列表中的列表项,
//然后获取相应的任务控制块给pxCurrrentTCB; }

  portGET_HIGHEST_PRIORITY 本质上是个宏,定义如下

#define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities )  uxTopPriority = ( 31UL- ( uint32_t ) __clz( ( uxReadyPriorities ) ) )

   注意使用硬件方法的时候,uxTopReadyPriority 就不代表处于就绪态的最高优先级了,而是每个bit 代表一个优先级,bit0 代表优先级0,当某个优先级有任务的话,就将相应的bit置为1。

  __clz(uxReadyPriorities)就是计算 uxReadyPriorities 的前导零个数,前导零个数就是指从最高位开始(bit31)到第一个为 的 bit,其间 的个数。然后用31减去前导0个数,就得到处于就绪态的最高优先级了。