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)到第一个为 1 的 bit,其间 0 的个数。然后用31减去前导0个数,就得到处于就绪态的最高优先级了。