【08】FreeRTOS的任务调度

目录

1.开启任务调度器

2.启动第一个任务

2.1prvStartFirstTask ()

2.1.1什么是MSP指针?

2.1.2为什么是 0xE000ED08?

2.2 vPortSVCHandler ()

2.2.1出栈/压栈汇编指令详解

3.任务切换

3.1PendSV中断如何触发?

3.1.1滴答定时器中断 

3.1.2执行FreeRTOS相关API函数

3.2PendSV中断服务函数实现步骤

3.2.1 查找最高优先级任务

3.2.2前导置0指令

 3.2.3获取最高优先级任务的任务控制块

4.总结


学习调度器之前的一些建议:

本讲的内容和Cortex-M处理器的内核架构密切联系,所以学习之前建议大家:

⭐①提前阅读《Cortex M3权威指南(中文)》和《Cortex M3与M4权威指南》

⭐②结合文档教程正点原子《FreeRTOS开发手册》第八章进行学习

1.开启任务调度器

函数vTaskStartScheduler()

作用:创建好任务后,如果不开启任务调度器,任务将不能被执行。用于启动任务调度器,任务调度器启动后, FreeRTOS 便会开始进行任务调度。

vTaskStartScheduler()函数完整代码+中文注释步骤如下: 

void vTaskStartScheduler( void )
{
    BaseType_t xReturn;

    /* 判断动态创建还是静态创建,由于默认是动态创建任务,并不会进入到下面的if语句,而是进入到else语句 */
    /* Add the idle task at the lowest priority. */
    #if ( configSUPPORT_STATIC_ALLOCATION == 1 )
        {
            StaticTask_t * pxIdleTaskTCBBuffer = NULL;
            StackType_t * pxIdleTaskStackBuffer = NULL;
            uint32_t ulIdleTaskStackSize;

            /* The Idle task is created using user provided RAM - obtain the
             * address of the RAM then create the idle task. */
            vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer, &pxIdleTaskStackBuffer, &ulIdleTaskStackSize );
            xIdleTaskHandle = xTaskCreateStatic( prvIdleTask,
                                                 configIDLE_TASK_NAME,
                                                 ulIdleTaskStackSize,
                                                 ( void * ) NULL,       /*lint !e961.  The cast is not redundant for all compilers. */
                                                 portPRIVILEGE_BIT,     /* In effect ( tskIDLE_PRIORITY | portPRIVILEGE_BIT ), but tskIDLE_PRIORITY is zero. */
                                                 pxIdleTaskStackBuffer,
                                                 pxIdleTaskTCBBuffer ); /*lint !e961 MISRA exception, justified as it is not a redundant explicit cast to all supported compilers. */

            if( xIdleTaskHandle != NULL )
            {
                xReturn = pdPASS;
            }
            else
            {
                xReturn = pdFAIL;
            }
        }
    #else /* if ( configSUPPORT_STATIC_ALLOCATION == 1 ) */
        {
            /* 1、创建空闲任务 */
            /* The Idle task is being created using dynamically allocated RAM. */
            xReturn = xTaskCreate( prvIdleTask,
                                   configIDLE_TASK_NAME,
                                   configMINIMAL_STACK_SIZE,
                                   ( void * ) NULL,
                                   portPRIVILEGE_BIT,  /* In effect ( tskIDLE_PRIORITY | portPRIVILEGE_BIT ), but tskIDLE_PRIORITY is zero. */
                                   &xIdleTaskHandle ); /*lint !e961 MISRA exception, justified as it is not a redundant explicit cast to all supported compilers. */
        }
    #endif /* configSUPPORT_STATIC_ALLOCATION */
    /* 2、如果使能软件定时器,则调用函数xTimerCreateTimerTask()创建定时器任务 */
    #if ( configUSE_TIMERS == 1 )
        {
            if( xReturn == pdPASS )
            {
                xReturn = xTimerCreateTimerTask();
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }
        }
    #endif /* configUSE_TIMERS */
    /* xReturn为创建任务返回值,创建成功则返回pdPASS */
    if( xReturn == pdPASS )
    {
        /* freertos_tasks_c_additions_init() should only be called if the user
         * definable macro FREERTOS_TASKS_C_ADDITIONS_INIT() is defined, as that is
         * the only macro called by the function. */
        #ifdef FREERTOS_TASKS_C_ADDITIONS_INIT
            {
                freertos_tasks_c_additions_init();
            }
        #endif

        /* Interrupts are turned off here, to ensure a tick does not occur
         * before or during the call to xPortStartScheduler().  The stacks of
         * the created tasks contain a status word with interrupts switched on
         * so interrupts will automatically get re-enabled when the first task
         * starts to run. */
        /* 3、关闭中断,防止调度器开启之前或过程中,受中断干扰,会在运行第一个任务时打开中断 */
        portDISABLE_INTERRUPTS();

        #if ( configUSE_NEWLIB_REENTRANT == 1 )
            {
                /* Switch Newlib's _impure_ptr variable to point to the _reent
                 * structure specific to the task that will run first.
                 * See the third party link http://www.nadler.com/embedded/newlibAndFreeRTOS.html
                 * for additional information. */
                _impure_ptr = &( pxCurrentTCB->xNewLib_reent );
            }
        #endif /* configUSE_NEWLIB_REENTRANT */
        /* 4、初始化全局变量,并将任务调度器的运行标志设置为已运行 */
        xNextTaskUnblockTime = portMAX_DELAY;                /* 下一个任务的阻塞超时时间,由于是在开启任务调度器,并没有任务,所以设置为最大阻塞时间 */
        xSchedulerRunning = pdTRUE;                          /* 调度器运行状态设置为正在运行 */
        xTickCount = ( TickType_t ) configINITIAL_TICK_COUNT;/* 系统节拍周期,滴答定时器每中断一次,变量xTickCount加1,为系统提高心跳节拍。由于此时并没有任务执行,则变量xTickCount赋值为0 */

        /* If configGENERATE_RUN_TIME_STATS is defined then the following
         * macro must be defined to configure the timer/counter used to generate
         * the run time counter time base.   NOTE:  If configGENERATE_RUN_TIME_STATS
         * is set to 0 and the following line fails to build then ensure you do not
         * have portCONFIGURE_TIMER_FOR_RUN_TIME_STATS() defined in your
         * FreeRTOSConfig.h file. */
        /* 5、初始化任务运行时间统计功能的时基定时器(统计任务运行时间,此处只有接口,需要时需自行编写) */
        portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();
        /* 调试功能,函数并未实现,只保留了函数接口 */
        traceTASK_SWITCHED_IN();
        
        /* Setting up the timer tick is hardware specific and thus in the
         * portable interface. */
        /* 6、调用函数 xPortStartScheduler() */ 
        if( xPortStartScheduler() != pdFALSE )
        {
            /* Should not reach here as if the scheduler is running the
             * function will not return. */
        }
        else
        {
            /* Should only reach here if a task calls xTaskEndScheduler(). */
        }
    }
    else
    {
        /* This line will only be reached if the kernel could not be started,
         * because there was not enough FreeRTOS heap to create the idle task
         * or the timer task. */
        configASSERT( xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY );
    }

    /* Prevent compiler warnings if INCLUDE_xTaskGetIdleTaskHandle is set to 0,
     * meaning xIdleTaskHandle is not used anywhere else. */
    ( void ) xIdleTaskHandle;

    /* OpenOCD makes use of uxTopUsedPriority for thread debugging. Prevent uxTopUsedPriority
     * from getting optimized out as it is no longer used by the kernel. */
    ( void ) uxTopUsedPriority;
}

        在函数vTaskStartScheduler()中调用了函数xPortStartScheduler(),具体作用是用于完成启动任务调度器中与硬件架构相关的配置部分(比如滴答定时器的初始化配置),以及启动第一个任务。运行完函数时xPortStartScheduler()并不会再返回,而是直接跳转第一个任务开始执行

xPortStartScheduler()函数完整代码+中文注释步骤如下: 

BaseType_t xPortStartScheduler( void )
{
    /* configMAX_SYSCALL_INTERRUPT_PRIORITY must not be set to 0.
     * See https://www.FreeRTOS.org/RTOS-Cortex-M3-M4.html */
    configASSERT( configMAX_SYSCALL_INTERRUPT_PRIORITY );

    /* This port can be used on all revisions of the Cortex-M7 core other than
     * the r0p1 parts.  r0p1 parts should use the port from the
     * /source/portable/GCC/ARM_CM7/r0p1 directory. */
    configASSERT( portCPUID != portCORTEX_M7_r0p1_ID );
    configASSERT( portCPUID != portCORTEX_M7_r0p0_ID );
    /* 1、检测用户在 FreeRTOSConfig.h 文件中对中断的相关配置是否有误  */
    #if ( configASSERT_DEFINED == 1 )
        {
            volatile uint32_t ulOriginalPriority;
            volatile uint8_t * const pucFirstUserPriorityRegister = ( uint8_t * ) ( portNVIC_IP_REGISTERS_OFFSET_16 + portFIRST_USER_INTERRUPT_NUMBER );
            volatile uint8_t ucMaxPriorityValue;

            /* Determine the maximum priority from which ISR safe FreeRTOS API
             * functions can be called.  ISR safe functions are those that end in
             * "FromISR".  FreeRTOS maintains separate thread and ISR API functions to
             * ensure interrupt entry is as fast and simple as possible.
             *
             * Save the interrupt priority value that is about to be clobbered. */
            ulOriginalPriority = *pucFirstUserPriorityRegister;

            /* Determine the number of priority bits available.  First write to all
             * possible bits. */
            *pucFirstUserPriorityRegister = portMAX_8_BIT_VALUE;

            /* Read the value back to see how many bits stuck. */
            ucMaxPriorityValue = *pucFirstUserPriorityRegister;

            /* The kernel interrupt priority should be set to the lowest
             * priority. */
            configASSERT( ucMaxPriorityValue == ( configKERNEL_INTERRUPT_PRIORITY & ucMaxPriorityValue ) );

            /* Use the same mask on the maximum system call priority. */
            ucMaxSysCallPriority = configMAX_SYSCALL_INTERRUPT_PRIORITY & ucMaxPriorityValue;

            /* Calculate the maximum acceptable priority group value for the number
             * of bits read back. */
            ulMaxPRIGROUPValue = portMAX_PRIGROUP_BITS;

            while( ( ucMaxPriorityValue & portTOP_BIT_OF_BYTE ) == portTOP_BIT_OF_BYTE )
            {
                ulMaxPRIGROUPValue--;
                ucMaxPriorityValue <<= ( uint8_t ) 0x01;
            }

            #ifdef __NVIC_PRIO_BITS
                {
                    /* Check the CMSIS configuration that defines the number of
                     * priority bits matches the number of priority bits actually queried
                     * from the hardware. */
                    configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == __NVIC_PRIO_BITS );
                }
            #endif

            #ifdef configPRIO_BITS
                {
                    /* Check the FreeRTOS configuration that defines the number of
                     * priority bits matches the number of priority bits actually queried
                     * from the hardware. */
                    configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == configPRIO_BITS );
                }
            #endif

            /* Shift the priority group value back to its position within the AIRCR
             * register. */
            ulMaxPRIGROUPValue <<= portPRIGROUP_SHIFT;
            ulMaxPRIGROUPValue &= portPRIORITY_GROUP_MASK;

            /* Restore the clobbered interrupt priority register to its original
             * value. */
            *pucFirstUserPriorityRegister = ulOriginalPriority;
        }
    #endif /* configASSERT_DEFINED */
    
    /* Make PendSV and SysTick the lowest priority interrupts. */
    /* 2、配置 PendSV 和 SysTick 的中断优先级为最低优先级 */
    portNVIC_SHPR3_REG |= portNVIC_PENDSV_PRI;
    portNVIC_SHPR3_REG |= portNVIC_SYSTICK_PRI;

    /* Start the timer that generates the tick ISR.  Interrupts are disabled
     * here already. */
    /* 3、调用函数 vPortSetupTimerInterrupt()配置 SysTick,初始化滴答定时器,设置滴答定时器的中断频率 */
    vPortSetupTimerInterrupt();
    
    /* Initialise the critical nesting count ready for the first task. */
    /* 4、初始化临界区嵌套计数器为 0 */
    uxCriticalNesting = 0;
    
    /* Ensure the VFP is enabled - it should be anyway. */
    /* 5、调用函数 prvEnableVFP()使能 FPU,ARM Cortex-M3内核无FPU,仅M4/M7才有此行代码 */
    prvEnableVFP();
    
    /* Lazy save always. */
    /* 将寄存器的30和31位置1,在进出异常时,自动保存和恢复FPU相关寄存器 */
    /* FPU寄存器有32位寄存器,S0~S31,S0~S15自动保存和恢复,S16~S31需要手动恢复和保存 */
    *( portFPCCR ) |= portASPEN_AND_LSPEN_BITS;
    
    /* Start the first task. */
    /* 6、调用函数 prvStartFirstTask()启动第一个任务 */
    prvStartFirstTask();

    /* Should not get here! */
    return 0;
}

滴答定时器配置函数vPortSetupTimerInterrupt()

portNVIC_SYSTICK_LOAD_REG是滴答定时器的重装载值,寄存器地址是0xe000e014;configSYSTICK_CLOCK_HZ 是CPU时钟,这里使用STM32F429,CPU时钟是180MHz,系统时钟也是180M;configTICK_RATE_HZ是在FreeRTOSConfig.h中设置的系统时钟节拍1000;portNVIC_SYSTICK_LOAD_REG 等于configSYSTICK_CLOCK_HZ/configTICK_RATE_HZ=180000,当数至0时,触发中断;滴答定时器时钟源通过portNVIC_SYSTICK_CTRL_REG控制寄存器设置,地址是0xe000e010,通过位2设置时钟源,如果此位为1,内核时钟180M,如果是外部时钟180M/8。portNVIC_SYSTICK_INT_BIT为1左移1位,倒数到0时产生SysTick异常请求。

程序中,滴答定时器时钟源为内部时钟180M,计数一次的时间为1/180M,滴答定时器为向下计数,计数时间为\frac{180000}{180M}=\frac{1}{1000}s,也就是滴答定时器中断时间为1ms。

__weak void vPortSetupTimerInterrupt( void )
    {
        /* Calculate the constants required to configure the tick interrupt. */
        #if ( configUSE_TICKLESS_IDLE == 1 )
            {
                ulTimerCountsForOneTick = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ );
                xMaximumPossibleSuppressedTicks = portMAX_24_BIT_NUMBER / ulTimerCountsForOneTick;
                ulStoppedTimerCompensation = portMISSED_COUNTS_FACTOR / ( configCPU_CLOCK_HZ / configSYSTICK_CLOCK_HZ );
            }
        #endif /* configUSE_TICKLESS_IDLE */

        /* Stop and clear the SysTick. */
        portNVIC_SYSTICK_CTRL_REG = 0UL;
        portNVIC_SYSTICK_CURRENT_VALUE_REG = 0UL;

        /* Configure SysTick to interrupt at the requested rate. */
        portNVIC_SYSTICK_LOAD_REG = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL;
        portNVIC_SYSTICK_CTRL_REG = ( portNVIC_SYSTICK_CLK_BIT | portNVIC_SYSTICK_INT_BIT | portNVIC_SYSTICK_ENABLE_BIT );
    }

使能FPU寄存器函数prvEnableVFP()

        函数中用到了汇编语言,在《Cortex M3权威指南(中文)》可以查到指令集的基本使用。在查找FPU寄存器使用时需要到《Cortex M3与M4权威指南》中查找,因为Cortex M3中无FPU寄存器。

__asm void prvEnableVFP( void )
{
/* *INDENT-OFF* */
    PRESERVE8

    /* The FPU enable bits are in the CPACR. */
    ldr.w r0, =0xE000ED88
    ldr r1, [ r0 ]

    /* Enable CP10 and CP11 coprocessors, then save back. */
    orr r1, r1, #( 0xf << 20 )
    str r1, [ r0 ]
    bx r14
    nop
/* *INDENT-ON* */
}

         将FPU寄存器使能操作的位是C11和C10位,如果将C11和C10都设置为11,则为完全访问,使能FPU。

 

2.启动第一个任务

        在开启任务调度器函数xPortStartScheduler()末尾,调用了启动第一个任务的函数prvStartFirstTask()。而prvStartFirstTask()函数中调用了vPortSVCHandler ()SVC中断服务函数。

OS如何启动第一个任务?

如果创建了n个任务,从中选取优先级最高的任务执行。假设我们要启动的第一个任务是任务A,那么就需要将任务A的寄存器值恢复到CPU寄存器。任务A的寄存器值,在一开始创建任务时就保存在任务堆栈里边。

注意:

1、中断产生时,硬件自动将xPSR,PC(R15),LR(R14),R12,R3-R0保存和恢复(进入中断保存到任务堆栈是入栈,出中断时寄存器从堆栈中恢复到到CPU寄存器); 而R4~R11需要手动保存和恢复。

2、进入中断后硬件会强制使用MSP指针 ,此时LR(R14)的值将会被自动被更新为特殊的值EXC_RETURN

关于以上提到的寄存器,可查看《 Cortex M3权威指南(中文) 》第37页。

2.1prvStartFirstTask ()

__asm void prvStartFirstTask( void )
{
/* *INDENT-OFF* */
    /* 首先进行8字节对齐 */
    PRESERVE8

    /* Use the NVIC offset register to locate the stack. */
    ldr r0, =0xE000ED08  /* 0xE000ED08为VTOR地址 */
    ldr r0, [ r0 ]       /* 获取VTOR的值,通过地址来找到向量表存储的地址 */
    ldr r0, [ r0 ]       /* 获取MSP的初始值,通过向量表存储的地址获取第一个元素 */
    /* Set the msp back to the start of the stack. */
    /* 初始化MSP,将r0赋值给msp */
    msr msp, r0

    /* Clear the bit that indicates the FPU is in use in case the FPU was used
     * before the scheduler was started - which would otherwise result in the
     * unnecessary leaving of space in the SVC stack for lazy saving of FPU
     * registers. */
    mov r0, #0
    msr control, r0
    /* Globally enable interrupts. */
    /* 使能全局中断,打开中断 */
    cpsie i
    cpsie f
    dsb
    isb
    /* Call SVC to start the first task. */
    /* 调用SVC启动第一个任务 */
    svc 0
    nop
    nop
/* *INDENT-ON* */
}

        函数用于初始化启动第一个任务前的环境,主要是重新设置MSP 指针,并使能全局中断(触发SVC中断)。

prvStartFirstTask()第一行代码首先进行了8字节对齐,这是因为栈在任何时候都需要4字节对齐,而在调用入口需要8字节对齐。 在进行C编程的时候,编译器会自动完成对齐操作,而对于汇编则需要开发者手动进行对齐。

后三行是为了获取MSP的初始值,0xE000ED08地址是向量表的寄存器地址。向量表在startup_stm32f429xx.s中,向量表的第一个成员是__initial_sp,也就是MSP的初始值,向量表的起始处都必须包含主堆栈指针(MSP)的初始值、复位向量、NMI、硬fault服务例程。

为什么要将向量表第一个成员赋值给MSP?

因为上电时默认使用MSP来做主堆栈指针,从上电跑到这里,经过一系列的函数调用、出/入栈,MSP不是一开始的位置,重新将初始值值赋值给MSP,MSP回到原点。之前使用的寄存器就不再需要保存,直接丢掉,因为现在不需要回去,启动了第一个任务后,后面的执行都在任务和任务之间,这是一条不归路

2.1.1什么是MSP指针?

程序在运行过程中需要一定的栈空间来保存局部变量等一些信息。当有信息保存到栈中时, MCU 会自动更新 SP 指针(也就是R13),ARM Cortex-M 内核提供了两个栈空间,:

主堆栈指针(MSP):它由 OS 内核、异常服务例程以及所有需要特权访问的应用程序代码来使用。

进程堆栈指针(PSP):用于常规的应用程序代码(不处于异常服务例程中时)。

在FreeRTOS中,中断使用MSP(主堆栈),中断以外使用PSP(进程堆栈)。在FreeRTOS中使用的是双堆栈指针,裸机无论是进程还是中断都是使用的MSP主堆栈指针

2.1.2为什么是 0xE000ED08?

        因为需从 0xE000ED08 获取向量表的偏移,为啥要获得向量表呢?因为向量表的第一个是 MSP 指针!

        取 MSP 的初始值的思路是先根据向量表的位置寄存器 VTOR (0xE000ED08) 来获取向量表存储的地址; 在根据向量表存储的地址,来访问第一个元素,也就是初始的 MSP。

        CM3 允许向量表重定位——从其它地址处开始定位各异常向量 这个就是向量表偏移量寄存器,向量表的起始地址保存的就是主栈指针MSP 的初始值

2.2 vPortSVCHandler ()

__asm void vPortSVCHandler( void )
{
/* *INDENT-OFF* */
    /* 首先进行8字节对齐 */
    PRESERVE8

    /* Get the location of the current TCB. */
    ldr r3, =pxCurrentTCB  /* 获取优先级最高的任务控制块地址 */
    ldr r1, [ r3 ]         /* 通过任务控制块的地址获取其成员的首地址 */
    ldr r0, [ r1 ]         /* 通过首成员的地址获取首成员的值 */
    /* Pop the core registers. */
    ldmia r0!, {r4-r11,r14}    /* 出栈指令,从R0地址开始,将值赋值给后面的寄存器,出栈到CPU寄存器 */
    msr psp, r0                /* 将R0的值赋值给PSP,因为退出中断时,从PSP自动恢复剩下的寄存器 */
    isb
    /* 使能所有中断 */
    mov r0, #0          /* 将r0等于0 */
    msr basepri, r0     /* 将0赋值给中断屏蔽寄存器,为0时开启所有中断 */
    bx r14              /* 跳转到任务的任务函数中执行,也就是跳转到PC指向任务函数地址去执行 */
/* *INDENT-ON* */
}

         前面提到只需要将R4-R11手动恢复,但是这里多了一个R14,这里Cortex M3系列是没用R14的出栈操作的,Cortex M3最后多了一行代码“orr r14, # oxd”,将R14或上0x0d,退出中断时使用PSP进程堆栈指针。由于M4、M7支持FPU寄存器,会有R14寄存器被赋值特殊值EXC_RETURN,该特殊值不同的值会有不同的意义。EXC_RETURN只有6个合法的值(M4、M7,M3只有3个合法值,可以在《Cortex M3与M4权威指南》中找到),如下表所示

描述使用浮点单元(M4、M7支持,M3不支持未使用浮点单元
中断返回后进入Hamdler模式,并使用MSP0xFFFFFFE10xFFFFFFF1
中断返回后进入线程模式,并使用 MSP0xFFFFFFE90xFFFFFFF9
中断返回后进入线程模式,并使用 PSP0xFFFFFFED0xFFFFFFFD

        恢复R14的值是为了判别是否使用浮点单元。注意:SVC中断只在启动第一次任务时会调用一次,以后均不调用,后面的任务切换都在PendSV中实现。

函数实现步骤:

1. 通过 pxCurrentTCB 获取优先级最高的就绪态任务的任务栈地址,优先级最高的就绪态任务是系统将要运行的任务 。

2. 通过任务的栈顶指针,将任务栈中的内容出栈到 CPU 寄存器中,任务栈中的内容在调用任务创建函数的时候,已初始化,然后设置 PSP 指针 。

3. 通过往 BASEPRI 寄存器中写 0,允许中断。

4. R14 是链接寄存器 LR,在 ISR 中(此刻我们在 SVC 的 ISR 中),它记录了异常返回值 EXC_RETURN

2.2.1出栈/压栈汇编指令详解

1、出栈(恢复现场,从内存里将地址的值恢复到CPU,CPU寄存器就会执行),方向:从下往上(低地址往高地址):假设r0地址为0x04汇编指令示例:

ldmia r0!, {r4-r6}   /* 任务栈r0地址由低到高,将r0存储地址里面的内容手动加载到 CPU寄存器r4、r5、r6 */

r0地址(0x04)内容加载到r4,此时地址r0 = r0+4  = 0x08 

r0地址(0x08)内容加载到r5,此时地址r0 = r0+4  = 0x0C

r0地址(0x0C)内容加载到r6,此时地址r0 = r0+4  = 0x10

2、压栈(保存现场,将CPU寄存器保存到内存里),方向:从上往下(高地址往低地址):假设r0地址为0x10汇编指令示例:

stmdb r0!, {r4-r6} }   /* r0的存储地址由高到低递减,将r4、r5、r6里的内容存储到r0的任务栈里面。 */

地址:r0 = r0-4  = 0x0C,将r6的内容(寄存器值)存放到r0所指向地址(0x0C)

地址:r0 = r0-4  = 0x08,将r5的内容(寄存器值)存放到r0所指向地址(0x08)

地址:r0 = r0-4  = 0x04,将r4的内容(寄存器值)存放到r0所指向地址(0x04)

3.任务切换

任务切换的本质:就是CPU寄存器的切换。

假设当由任务A切换到任务B时,主要分为两步:

第一步:需暂停任务A的执行,并将此时任务A的寄存器(CPU寄存器的值)保存到任务堆栈,这个过程叫做保存现场;

第二步:将任务B的各个寄存器值(被存于任务B的任务堆栈中)恢复到CPU寄存器中,这个过程叫做恢复现场;

对任务A保存现场,对任务B恢复现场,这个整体的过程称之为:上下文切换(可以理解为任务切换) 

任务A和任务B切换示意图

        任务A在运行时,CPU寄存器中是A的各个寄存器;任务B抢占时,第一步需要将任务A寄存器保存,将CPU中任务A的各个寄存器保存到任务A的任务堆栈中,下次恢复任务A运行时,将任务A的任务堆栈中的寄存器恢复到CPU寄存器中,接着被打断的点继续运行;第二步将任务B的寄存器值恢复到CPU寄存器中;任务切换后,CPU寄存器中就是B的各个寄存器。

注意任务切换的过程在PendSV中断服务函数里边完成

3.1PendSV中断如何触发?

1、滴答定时器中断调用(判断符合条件时,触发PendSV中断

2、执行FreeRTOS提供的相关API函数:portYIELD()(只要调用此函数就会触发PendSV中断,FreeRTOS提供了很多API函数,这些函数都是一些宏定义,主要还是调用此函数

本质:通过向中断控制和状态寄存器 ICSR 的bit28 写入 1 挂起 PendSV 来启动 PendSV 中断

上表摘取于《Cortex M3权威指南(中文)》第131页

3.1.1滴答定时器中断 

        在开始任务调度器后调用函数xPortSysTickHandler()

//systick中断服务函数,使用OS时用到
void SysTick_Handler(void)
{	
    HAL_IncTick();
	if(xTaskGetSchedulerState()!=taskSCHEDULER_NOT_STARTED)					//OS开始跑了,才执行正常的调度处理
	{
		xPortSysTickHandler();
	}
}
void xPortSysTickHandler( void )
{
    /* The SysTick runs at the lowest interrupt priority, so when this interrupt
     * executes all interrupts must be unmasked.  There is therefore no need to
     * save and then restore the interrupt mask value as its value is already
     * known - therefore the slightly faster vPortRaiseBASEPRI() function is used
     * in place of portSET_INTERRUPT_MASK_FROM_ISR(). */
    vPortRaiseBASEPRI();
    {
        /* Increment the RTOS tick. */
        if( xTaskIncrementTick() != pdFALSE )
        {
            /* A context switch is required.  Context switching is performed in
             * the PendSV interrupt.  Pend the PendSV interrupt. */
            portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
        }
    }

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

         0xe000ed04是中断控制及状态寄存器地址,将第28位写入1将挂起PendSV启动PendSV中断,当if条件满足时,触发PendSV中断。xTaskIncrementTick()函数中有两种情况,需要任务切换,一个是阻塞超时时间到了,任务从阻塞列表中解除,挂载到就绪列表中,如果挂载到就绪列表中的任务比当前正在执行的任务优先级高,则需任务切换;还有一个是时间片调度,就绪列表中存在同等优先级的任务,也需任务切换。

3.1.2执行FreeRTOS相关API函数

虽然有众多API函数都可触发PendSV中断,但是本质都是下列函数:

    #define portYIELD()                                 \
    {                                                   \
        /* Set a PendSV to request a context switch. */ \
        portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; \
                                                        \
        /* Barriers are normally not required but do ensure the code is completely \
         * within the specified behaviour for the architecture. */ \
        __dsb( portSY_FULL_READ_WRITE );                           \
        __isb( portSY_FULL_READ_WRITE );                           \
    }

3.2PendSV中断服务函数实现步骤

        首先进行8字节对齐

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

/* *INDENT-OFF* */
    PRESERVE8

        将psp任务栈赋值给r0, psp是在中断以外所使用的堆栈指针,中断服务函数以内是MSP主堆栈指针。由于是在中断服务函数PendSV中,所以使用的是MSP,外面任务运行用的是PSP。

    mrs r0, psp            /* 将psp的值赋值给r0 */
    isb
  

        通过以下两部获取了栈顶指针的地址。栈顶指针保存的是栈顶地址,底压栈时,psp会不断往下移,直至栈顶,将地址赋值给栈顶指针,寄存器恢复时只要从栈顶指针往上恢复即可。

  /* Get the location of the current TCB. */
    ldr r3, =pxCurrentTCB   /* r3等于当前正在运行任务控制块的地址 */
    ldr r2, [ r3 ]          /* 从地址取值,获取到任务控制块栈顶指针地址 */

        浮点单元有32个寄存器S0~S31,S0~S15自动保存和恢复,S16~S31手动保存和恢复。

    /* Is the task using the FPU context?  If so, push high vfp registers. */
    tst r14, #0x10    /* 判断位4是否为1,如果为1则不使用浮点数,为0使用浮点数 *、
    it eq
    vstmdbeq r0!, {s16-s31}    /* 如果使用寄存器压栈保存寄存器s16-s31 */

         从R0地址开始,将R4~R11和R14的值保存到任务栈中。

    /* Save the core registers. */
    stmdb r0!, {r4-r11, r14}

        将R0地址写入到R2所指向的内存中,R2前面已经说了,指向的是栈顶指针的地址,所以将R0写入到栈顶指针内容中去。

    /* Save the new top of stack into the first member of the TCB. */
    str r0, [ r2 ]

    stmdb sp!, {r0, r3}  /* 将R0,R3进行压栈,sp就是msp */
    mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY  /* 将宏的值5写入R0 */
    msr basepri, r0    /* 将r0写入中断屏蔽寄存器,关闭5~15中断优先级的中断 */
    dsb
    isb
    bl vTaskSwitchContext    /* 通过函数vTaskSwitchContext查找下一个要运行的任务的任务控制块 */
    mov r0, #0               /* 将r0清0 */
    msr basepri, r0          /* 将r0赋值给中断屏蔽寄存器,打开中断 */
    ldmia sp!, {r0, r3}      /* 出栈,*/

    /* The first item in pxC urrentTCB is the task top of stack. */
    ldr r1, [ r3 ]           /* 取出r3地址中的值 */
    ldr r0, [ r1 ]           /* 取出r1地址中的值,R0获取的是栈顶地址 */

    /* Pop the core registers. */
    ldmia r0!, {r4-r11, r14}    /* 将r0地址中的值出栈,恢复到r4~r11,r14 */

    /* Is the task using the FPU context?  If so, pop the high vfp registers
     * too. */
    tst r14, #0x10             /* 判断r14的bit4是否被置1,为1则不使用浮点数,为0则使用浮点数 */
    it eq
    vldmiaeq r0!, {s16-s31}    /* 如果使用则出栈操作 */

    msr psp, r0        /* 将r0的值赋值给psp,自动将剩余寄存器出栈 */
    isb
    #ifdef WORKAROUND_PMU_CM001 /* XMC4000 specific errata */
        #if WORKAROUND_PMU_CM001 == 1
            push { r14 }
            pop { pc }
            nop
        #endif
    #endif

    bx r14        /* 返回r14 */
/* *INDENT-ON* */
}

3.2.1 查找最高优先级任务

         函数vTaskSwitchContext()是通过调用函数taskSELECT_HIGHEST_PRIORITY_TASK(),函数taskSELECT_HIGHEST_PRIORITY_TASK()又调用函数portGET_HIGHEST_PRIORITY()找到的最高优先级的任务,内容如下:

#define taskSELECT_HIGHEST_PRIORITY_TASK()                                                  \
    {                                                                                           \
        UBaseType_t uxTopPriority;                                                              \
                                                                                                \
        /* Find the highest priority list that contains ready tasks. */                         \
        portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority );  /* 获取当前最高优先级任务 */                         \
        configASSERT( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ uxTopPriority ] ) ) > 0 ); \
        listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) \); /* 获取当前任务最高优先级的任务控制块 */   \ *
    } 

        在之前文章中提到过,任务优先级是0~31,对应32个就绪列表,就绪列表的序号也是0~31,和任务优先级相对应,每一个就绪列表都用一个位来表示其中有没有任务挂载其中,如果有则为1,反之为0。32位的值就保存在变量uxReadyPriorities中,函数__clz()是前导置0指令,此处是通过硬件的方式查找任务最高优先级。 

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

3.2.2前导置0指令

        左侧是一个32位的变量表示就绪列表,只要列表中挂载任务,则该优先级的就绪列表的标志位置1,前导置0指令此时为3,因为最高位前面有3个0。

        再举个例子,如下图所示,由于第30位为1,前面有一位为0,则前导置0 指令结果为1。所谓的前导置0指令,大家可以简单理解为计算一个 32位数,头部 0 的个数 。通过前导置0指令获得任务的最高优先级。

 3.2.3获取最高优先级任务的任务控制块

        参数pxList是最高优先级的就绪列表,将就绪列表赋值给pxConstList。

#define listGET_OWNER_OF_NEXT_ENTRY( pxTCB, pxList )                                           			 \
    {                                                                                         							 \
        List_t * const pxConstList = ( pxList );                                              					 

         就绪列表的pxIndex指针指向最高优先级就绪列表的pxIndex指针的下一个,一开始初始化列表时,pxIndex指针指向末尾列表项,也就是将pxIndex指针指向下图中的列表项1。

       ( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext;                         			 

         判断最高优先级就序列表pxConstListpxIndex指针是否指向末尾列表项,否则不执行if中内容。

        if( ( void * ) ( pxConstList )->pxIndex == ( void * ) &( ( pxConstList )->xListEnd ) ) 			 \
        {                                                                                      							 \
            ( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext;                       			 \
        }                                                                                      							 

         将当前最高优先级任务的所属任务控制块,赋值给pxTCB。

	       ( pxTCB ) = ( pxConstList )->pxIndex->pvOwner;                                         			 	 \ 
    }

         假如又触发了一次PendSV中断,此时又需要进行一次任务切换,此时就绪列表的pxIndex指针指向列表项1,则指向列表项1的下一个末尾列表项。此时的if条件成立,进入程序将就绪列表的pxIndex指针更换成下一个,还是指向列表项1。以上是当列表中仅有一个任务时

        当最高优先级就绪列表中有多个任务时,指针会先指向1,判断列表项1是否等于末尾列表项,否将返回任务1的任务控制块;同理指向列表项2,返回列表项2的任务控制块,;下一次将指向末尾列表项,if条件成立则更换pxIndex指针指向列表项1。最终在步骤②和③之间循环,也就是时间片调度,每触发一次PendSV中断,将同等优先级的任务轮流执行。

4.总结

 

 

 

 

 

  • 6
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

花落指尖❀

您的认可是小浪宝宝最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值