自学内容网 自学内容网

FreeRTOS - 调度器

指导任务的创建和删除后,那么多个任务之间是如何切换的呢。这些功能都是由调度器完成的。接下来就研究下调度器的原理。

vTaskStartScheduler

创建完任务之后,就开启调度器。

创建一个idle task,因为如果所有的任务都没有运行,总要有一个任务要运行,所以需要一个idle task。优先级最低。

xReturn = prvCreateIdleTasks();

中断在这里是关闭的,以确保在调用xPortStartScheduler ( 之前或期间不会出现勾选:创建的任务的堆栈包含一个状态字,中断处于打开状态,因此当第一个任务开始运行时,中断将自动重新启用。

portDISABLE_INTERRUPTS();

 任务还没正常开始调度,所以delay设置为最大。调度器的运行状态设置成true。tickCout从0开始。

xNextTaskUnblockTime = portMAX_DELAY;
xSchedulerRunning = pdTRUE;
xTickCount = ( TickType_t ) configINITIAL_TICK_COUNT;

xPortStartScheduler

然后就开始启动调度器,这个函数不会返回,除非遇到错误。这个函数和具体的硬件有关系。

我们以ARM CortexM3为例。

设置PendSV和SysTick为最低优先级。为什么优先级最低和为什么是这两个中断后面再分析。 

    /* Make PendSV and SysTick the lowest priority interrupts. */
    portNVIC_SHPR3_REG |= portNVIC_PENDSV_PRI;
    portNVIC_SHPR3_REG |= portNVIC_SYSTICK_PRI;

我们只需要知道这里设置了两个中断。

SysTick中断设置完优先级后,需要配置timer,例如周期。

__attribute__( ( 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_CONFIG | portNVIC_SYSTICK_INT_BIT | portNVIC_SYSTICK_ENABLE_BIT );
}

先停止SysTick,count清0。然后更具用户配置来设置SysTick中断的周期。也就是task调度的周期。我们这里不用管具体的硬件是如何配置的,只需要知道原理。需要一个定时的中断函数类触发任务切换。

设置完systick后就可以开始第一个任务了。

/* Start the first task. */
static void prvPortStartFirstTask( void )
{
    __asm volatile (
        " ldr r0, =0xE000ED08   \n" /* Use the NVIC offset register to locate the stack. */
        " ldr r0, [r0]          \n"
        " ldr r0, [r0]          \n"
        " msr msp, r0           \n" /* Set the msp back to the start of the stack. */
        " cpsie i               \n" /* Globally enable interrupts. */
        " cpsie f               \n"
        " dsb                   \n"
        " isb                   \n"
        " svc 0                 \n" /* System call to start first task. */
        " nop                   \n"
        " .ltorg                \n"
        );
}
  • 从0xE000ED08(硬件寄存器)处获取中断向量表起始地址,默认是0x80000000。然后从中断向量表的起始地址获取主栈指针MSP的初始值,所以汇编的前三行就是为了获取MSP的初始值。
  • 打开中断。
  • 调用svc指针触发svc中断。也就是PendSV中断。

SVC中断异常函数如下:

void vPortSVCHandler( void )
{
    __asm volatile (
        "   ldr r3, pxCurrentTCBConst2      \n" /* Restore the context. */
        "   ldr r1, [r3]                    \n" /* Use pxCurrentTCBConst to get the pxCurrentTCB address. */
        "   ldr r0, [r1]                    \n" /* The first item in pxCurrentTCB is the task top of stack. */
        "   ldmia r0!, {r4-r11}             \n" /* Pop the registers that are not automatically saved on exception entry and the critical nesting count. */
        "   msr psp, r0                     \n" /* Restore the task stack pointer. */
        "   isb                             \n"
        "   mov r0, #0                      \n"
        "   msr basepri, r0                 \n"
        "   orr r14, #0xd                   \n"
        "   bx r14                          \n"
        "                                   \n"
        "   .align 4                        \n"
        "pxCurrentTCBConst2: .word pxCurrentTCB             \n"
        );
}

获取当前要运行的任务,然后回去栈顶指针。从栈顶指针里恢复寄存器的值。所以这就是为什么pxTopOfStack必须是tcb的第一个元素。


原文地址:https://blog.csdn.net/u012701675/article/details/136056239

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!