FreeRTOS任务切换

来源:互联网 发布:linux hexo github 编辑:程序博客网 时间:2024/06/07 04:53

本文是《ALIENTEK STM32F429 FreeRTOS 开发教程》第九章学习笔记
第一章笔记–FreeRTOS简介与源码下载
第二章笔记–FreeRTOS在STM32F4上移植
第三章笔记-FreeRTOS系统配置
第四章笔记-FreeRTOS中断分析
第四章笔记补充-FreeRTOS临界段代码
第五章笔记-FreeRTOS任务基础
第六章笔记-FreeRTOS任务API函数的使用
第七章笔记-FreeRTOS列表和列表项
第八章笔记-1-FreeRTOS任务创建
第八章笔记-2-FreeRTOS任务调度器开启

一、FreeRTOS任务切换场合

  • 可以执行的一个系统调用
  • 系统能够滴答定时器(SysTick)中断

1.1 执行系统调用

执行系统调用即执行FreeRTOS系统提供的相关API函数,比如任务切换函数taskYIELD(),或是有些调用了taskYIELD()函数的API函数。

taskYIELD()源码:

#define taskYIELD()     portYIELD()#define portYIELD()                                 {    portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;    __dsb( portSY_FULL_READ_WRITE );                __isb( portSY_FULL_READ_WRITE );            }

portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT:

#define portNVIC_INT_CTRL_REG       ( * ( ( volatile uint32_t * ) 0xe000ed04 ) )#define portNVIC_PENDSVSET_BIT      ( 1UL << 28UL )

中断控制及状态寄存器ICSR(地址:0xE000_ED04),向ICSR的第28位写入1悬起PendSV(启动PendSV中断)

__dsb( portSY_FULL_READ_WRITE );__isb( portSY_FULL_READ_WRITE ): dsb和isb 完成数据同步隔离和指令同步隔离 完成作用是保证之前存储器访问操作和指令都执行完(个人认为是要确保开启了PendSV)

1.2 系统滴答定时器(SysTick)中断

FreeRTOS中滴答定时器(SysTick)中断服务函数中也会进行任务切换

滴答定时器中断服务函数:

void SysTick_Handler(void){      if(xTaskGetSchedulerState()!=taskSCHEDULER_NOT_STARTED)    {        xPortSysTickHandler();      }    HAL_IncTick();}void xPortSysTickHandler( void ){    vPortRaiseBASEPRI();    {        if( xTaskIncrementTick() != pdFALSE )        {            portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;        }    }    vPortClearBASEPRIFromISR();}

if(xTaskGetSchedulerState()!=taskSCHEDULER_NOT_STARTED):判断系统是否运行

vPortRaiseBASEPRI():关闭中断

if( xTaskIncrementTick() != pdFALSE ):增加时钟计数器xTickCount的值,如果不出错则期待能够PendSV中断

vPortClearBASEPRIFromISR():开启中断

二、PendSV中断服务函数

#define xPortPendSVHandler PendSV_Handler__asm void xPortPendSVHandler( void ){    extern uxCriticalNesting;    extern pxCurrentTCB;    extern vTaskSwitchContext;    PRESERVE8    mrs r0, psp    ldr r3, =pxCurrentTCB           /* Get the location of the current TCB. */    ldr r2, [r3]    tst r14, #0x10                  /* Is the task using the FPU context?  If so, push high vfp registers. */    it eq    vstmdbeq r0!, {s16-s31}    mrs r1, control    stmdb r0!, {r1, r4-r11, r14}    /* Save the remaining registers. */    str r0, [r2]                    /* Save the new top of stack into the first member of the TCB. */    stmdb sp!, {r3}    mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY    msr basepri, r0    dsb    isb    bl vTaskSwitchContext    mov r0, #0    msr basepri, r0    ldmia sp!, {r3}                                    /* Restore the context. */    ldr r1, [r3]    ldr r0, [r1]                    /* The first item in the TCB is the task top of stack. */    add r1, r1, #4                  /* Move onto the second item in the TCB... */    ldr r2, =0xe000ed9c             /* Region Base Address register. */    ldmia r1!, {r4-r11}             /* Read 4 sets of MPU registers. */    stmia r2!, {r4-r11}             /* Write 4 sets of MPU registers. */    ldmia r0!, {r3-r11, r14}        /* Pop the registers that are not automatically saved on exception entry. */    msr control, r3    tst r14, #0x10                  /* Is the task using the FPU context?  If so, pop the high vfp registers too. */    it eq    vldmiaeq r0!, {s16-s31}    msr psp, r0    bx r14    nop}

PRESERVE8: 8字节对齐

mrs r0,psp: 读取psp(R13)(进程栈指针)到r0

isb:指令隔离,确保之前指令确实执行

ldr r3,=pxCurrentTCB:获取当前任务控制块地址,加载到r3

ldr r2,[r3]: 将r3内内容加载到r2

tst r14,#0x10:判断任务是否使用FPU,当处理器进入异常处理或中断服务程序(ISR)时,链接寄存器(LR)的数值会被更新为EXC_RETURN数值
这里写图片描述
可以看出为什么要这么判断是否使用FPU

it eq:如果使用了则需将FPU寄存器s16~s31手动保存到任务堆栈中

vstmdbeq r0!,{s16-s31}:保存s16~s31这16个寄存器(注意写回符号!)

stmdb r0!,{r4-r11,r14}:保存r4~r11和r14折几个寄存器的值

str r0,[r2]:将r0的值写入r2内保存的地址里,r2内存放着任务控制块的首地址,r0中存放着当前最新的堆栈栈顶指针,即把当前栈顶指针写入当前任务控制块的第一个字段

stmdb sp!,{r3}:将r3的值入栈,stmdb sp!,rx 相当于 push rx;r3内是当前任务控制块的地址,为了防止接下来调用函数vTaskSwitchContext()有可能改写r3的值,所以将r3的值压栈

mov r0,#configMAX_SYSCALL_INTERRUPT_PRIORITY & msr basepri,r0:用来关闭中断,进入临界区。向basepri写入什么数即屏蔽多少优先级的中断

dsb & isb:完成数据同步隔离和指令同步隔离,即等待之前操作全部完成

bl vTaskSwitchContext:调用函数vTaskSwitchContext(),函数用来获取下一个要运行任务,并将pxCurrentTCB更新为这个要运行的任务

vTaskSwitchContext()源码:

void vTaskSwitchContext( void ){    if( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE )    {        /* The scheduler is currently suspended - do not allow a context        switch. */        xYieldPending = pdTRUE;    }    else    {        xYieldPending = pdFALSE;        traceTASK_SWITCHED_OUT();        #if ( configGENERATE_RUN_TIME_STATS == 1 )        {                #ifdef portALT_GET_RUN_TIME_COUNTER_VALUE                    portALT_GET_RUN_TIME_COUNTER_VALUE( ulTotalRunTime );                #else                    ulTotalRunTime = portGET_RUN_TIME_COUNTER_VALUE();                #endif                /* Add the amount of time the task has been running to the                accumulated time so far.  The time the task started running was                stored in ulTaskSwitchedInTime.  Note that there is no overflow                protection here so count values are only valid until the timer                overflows.  The guard against negative values is to protect                against suspect run time stat counter implementations - which                are provided by the application, not the kernel. */                if( ulTotalRunTime > ulTaskSwitchedInTime )                {                    pxCurrentTCB->ulRunTimeCounter += ( ulTotalRunTime - ulTaskSwitchedInTime );                }                else                {                    mtCOVERAGE_TEST_MARKER();                }                ulTaskSwitchedInTime = ulTotalRunTime;        }        #endif /* configGENERATE_RUN_TIME_STATS */        /* Check for stack overflow, if configured. */        taskCHECK_FOR_STACK_OVERFLOW();        /* Select a new task to run using either the generic C or port        optimised asm code. */        taskSELECT_HIGHEST_PRIORITY_TASK();        traceTASK_SWITCHED_IN();        #if ( configUSE_NEWLIB_REENTRANT == 1 )        {            /* Switch Newlib's _impure_ptr variable to point to the _reent            structure specific to this task. */            _impure_ptr = &( pxCurrentTCB->xNewLib_reent );        }        #endif /* configUSE_NEWLIB_REENTRANT */    }}
  • if( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE ):如果调度器挂起则不能进行任务切换

mov r0,#0 & msr basepri,r0: 打开中断,退出临界区。

ldmia sp!,{r3}:将刚才保存的r3的值出栈,恢复r3的值,但是此时pxCurrentTCB已经变成下一个要运行任务的任务控制块,所以此时r3保存的地址处数据改变成下一任务的任务控制块

ldr r1,[r3] & ldr r0,[r1]:将r3内值指向数据加载到r1,即将pxCurrentTCB的值加载到r1;将r1内值指向数据加载到r0,即将新的要运行的任务堆栈栈顶保存在r0中

ldmia r0!,{r4-r11,r14}:r4~r11,r14出栈,即要运行的任务的现场

tst r14,#0x10……{s16-s31}:跟前面一样判断是否使用FPU,如果使用了则需手动恢复FPU的s16~s31寄存器

msr psp,r0 & isb:更新进程栈指针

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

三、查找下一个要运行的任务

在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();        traceTASK_SWITCHED_IN();    }}

if( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE ){……}:判断调度器是否挂起,若调度器挂起则不能任务切换,需要将xYieldPending赋值pdTRUE说明需要任务切换

xYieldPending = pdFALSE:如果调度器没有被挂起,则开始执行接下来的调度工作,所以把xYieldPending赋值为pdFALSE说明已经开始任务切换

traceTASK_SWITCHED_OUT():
源代码中只宏定义了这个函数的名字,工程中没有找到实现代码,说明了功能是“前一个任务已经选定的运行要求。pxcurrenttcb持有指针
转到正在切换的任务的任务控制块”

#ifndef traceTASK_SWITCHED_OUT    /* Called before a task has been selected to run.  pxCurrentTCB holds a pointer    to the task control block of the task being switched out. */    #define traceTASK_SWITCHED_OUT()#endif

taskCHECK_FOR_STACK_OVERFLOW():检查堆栈溢出,源代码中如果定义了未使用堆栈的宏则则定义这个函数出现,函数具体执行代码,根据configcheck_for_stack_overflow和堆栈增长方向portSTACK_GROWTH变量作为条件编译条件决定

#ifndef taskCHECK_FOR_STACK_OVERFLOW    #define taskCHECK_FOR_STACK_OVERFLOW()#endif

taskSELECT_HIGHEST_PRIORITY_TASK():获取下一个要运行的任务

taskSELECT_HIGHEST_PRIORITY_TASK()是一个宏定义,有两种方法去实现查找下一要运行的任务,通过宏configUSE_PORT_OPTIMISED_TASK_SELECTION来决定使用哪种方法,若宏为1则使用硬件的方法,否则使用通用方法

(1)通用方法(即所有处理器都可以使用的方法)

#if ( configUSE_PORT_OPTIMISED_TASK_SELECTION == 0 )#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;         }

pxReadyTasksList[]为就绪任务列表数组,一个优先级一个列表,同优先级的就绪任务挂到相对应的列表。

uxTopPriority代表处于就绪态的最高优先值,每次创建人物时会判断新任务的优先级是否大于uxTopPriority,若大于,则将新任务的优先级赋值给uxTopPriority。函数prvAddTaskToReadyList()也会修改uxTopPriority的值,即某任务添加到就绪列表中会用uxTopPriority记录就需列表中的最高优先级

while( listLIST_IS_EMPTY( &( pxReadyTasksLists[ uxTopPriority ] ) ) ):从最高优先级开始判断,当哪个列表不为空时即证明哪个优先级有就绪任务。

listLIST_IS_EMPTY():判断某个列表是否为空

uxTopPriority(记录有就绪任务的优先级)

listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) ):查找到就绪任务的优先级后,使用函数listGET_OWNER_OF_NEXT_ENTRY()来获取列表中下一个列表项,再将获取到的列表项对用的任务控制块赋值给pxCurrentTCB,即确定下一个要运行的任务。

#define listGET_OWNER_OF_NEXT_ENTRY( pxTCB, pxList )                                    {    List_t * const pxConstList = ( pxList );    ( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext;    if( ( void * ) ( pxConstList )->pxIndex == ( void * ) &( ( pxConstList )->xListEnd ) )  \    {        ( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext;        }    ( pxTCB ) = ( pxConstList )->pxIndex->pvOwner;}

可以看到listGET_OWNER_OF_NEXT_ENTRY()宏定义具体先把从当前列表项指向下一列表项,再把新指向的列表项的pvOwner(任务控制块)赋值给传入的任务控制块

(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 ] ) );       \} 

portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority ):获取处于就绪态的最高优先级,
实际是一个宏具体实现为:

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

CLZ指令是计算前导0的个数

使用硬件方法时,uxTopReadyPriority使用每个bit代表一个优先级,当某优先级有就绪任务则将对应bit位置1

uxTopPriority = ( 31UL - ( uint32_t )__clz( ( uxReadyPriorities ))):31减去获得的前导-个数即得到处于就绪态的最高优先级

listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) ):跟上通用方法一样,获取列表中下一列表项,把下一列表项对应的任务控制块赋值给pxCurrentTCB

使用硬件方法,最多只能有32个优先级,32个优先级已经完全够用,因为FreeRTOS支持时间片,每个优先级可以支持无限多个任务

四、FreeRTOSS时间片调度

FreeRTOS中允许一个任务运行一个时间片(一个时钟节拍长度)后让出CPU使用权,让拥有同优先级的下一个任务运行

使用时间片调度,宏configUSE_PREEMPTION(1,使用抢占式内核,0使用协程)和configUSE_TIMERS(1,启动软件定时器)都必须为1

时间片的长度由宏configTICK_RATE_HZ决定,设定x则时间片长度为 1/x(秒)

原创粉丝点击