FreeRTOS的源代码个人分析(基于KEIL下STM32F103的Demo) 四

开始任务的实现分析:xPortStartScheduler()函数

FreeRTOS里开始任务是在main里调用vTaskStartScheduler函数来开始任务的,在调用这个函数后,系统会先自动的创建一个优先级最低(也就是0优先级)的空闲任务IdleTask,这个任务的作用是在所有用户的任务都被挂起,也就是当前没有用户所建立的任务在运行时,系统就会运行这个IdleTask。(但如果有用户任务的优先级也是0的话,那么用户任务会和IdleTask任务分时运行,所以一般设置任务优先级至少为1)。然后如果在config里设置了定时器,那么也会建立一个定时器的任务,定时器任务的优先级默认是最高优先级。
在完成这些准备工作之后,FreeRTOS就会调用xPortStartScheduler()函数来开启任务运行。

    #else
    {
        /* The Idle task is being created using dynamically allocated RAM. */
        xReturn = xTaskCreate(  prvIdleTask,
                                "IDLE", configMINIMAL_STACK_SIZE,
                                ( void * ) NULL,
                                ( tskIDLE_PRIORITY | portPRIVILEGE_BIT ),
                                &xIdleTaskHandle ); /*lint !e961 MISRA exception, justified as it is not a redundant explicit cast to all supported compilers. */
    }
    #endif /* configSUPPORT_STATIC_ALLOCATION */

    #if ( configUSE_TIMERS == 1 )
    {
        if( xReturn == pdPASS )
        {
            xReturn = xTimerCreateTimerTask();
        }
        else
        {
            mtCOVERAGE_TEST_MARKER();
        }
    }
    #endif /* configUSE_TIMERS */

    if( xReturn == pdPASS )
    {
        /* 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. */
        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. */
            _impure_ptr = &( pxCurrentTCB->xNewLib_reent );
        }
        #endif /* configUSE_NEWLIB_REENTRANT */

        xNextTaskUnblockTime = portMAX_DELAY;
        xSchedulerRunning = pdTRUE;
        xTickCount = ( TickType_t ) 0U;

        /* 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. */
        portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();

        /* Setting up the timer tick is hardware specific and thus in the
        portable interface. */
        if( xPortStartScheduler() != pdFALSE )
        {
            /* Should not reach here as if the scheduler is running the
            function will not return. */
        }

在调用xPortStartScheduler之前,初始化了三个变量的值,分别是xNextTaskUnblockTime,xSchedulerRunning和xTickCount。xNextTaskUnblockTime是下一个任务解挂的时间(系统时钟sysTick的Tick值),设置为0xFFFFFFFF,是滴答计时器不会计数到的一个值,xSchedulerRunning看名字就知道是系统运行标志位,xTickCount是当前sysTick时钟的Tick值。初始化这三个值后就在if括号里调用了xPortStartScheduler。注意在初始化三个变量之前还调用了portDISABLE_INTERRUPTS(),这个实现使用了内联汇编,主要内容就是给basepri寄存器写入0xbf,也就是屏蔽11级以下低优先级的中断(包括任务调度用的PendSV中断和滴答计时器中断)。

#define portDISABLE_INTERRUPTS()                vPortRaiseBASEPRI()
#define portENABLE_INTERRUPTS()                 vPortSetBASEPRI( 0 )

static portFORCE_INLINE void vPortRaiseBASEPRI( void )
{
uint32_t ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;

    __asm
    {
        /* Set BASEPRI to the max syscall priority to effect a critical
        section. */
        msr basepri, ulNewBASEPRI
        dsb
        isb
    }
}

因为configASSERT_DEFINED默认设置的为0,因此#if( configASSERT_DEFINED == 1 )下面的语句块无效,去除这一块后贴出精简后的的代码。

BaseType_t xPortStartScheduler( void )
{
    /* Make PendSV and SysTick the lowest priority interrupts. */
    portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
    portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;

    /* Start the timer that generates the tick ISR.  Interrupts are disabled
    here already. */
    vPortSetupTimerInterrupt();

    /* Initialise the critical nesting count ready for the first task. */
    uxCriticalNesting = 0;

    /* Start the first task. */
    prvStartFirstTask();

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

这个函数内容其实不多,主要是设置CM3内核里,PENDSV和SYSTICK这两个系统中断的优先级,均设置为最低的15级,这样一来,当调用vPortEnterCritical(portENTER_CRITICAL)时(其设置了内核里BASEPRI寄存器的值为11,屏蔽了11以上更低优先级的中断),这两个中断会被屏蔽。然后调用vPortSetupTimerInterrupt,这个函数的作用就是设置滴答计时器的周期值和是能中断。

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 */

        /* 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 );
    }

可以看到portNVIC_SYSTICK_LOAD_REG = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL; 这里的两个值见FreeRTOSConfig.h这个系统配置文件里的定义

#define configCPU_CLOCK_HZ          ( ( unsigned long ) 72000000 )  
#define configTICK_RATE_HZ          ( ( TickType_t ) 1000 )
#define configSYSTICK_CLOCK_HZ configCPU_CLOCK_HZ

可见默认的系统滴答频率是1000HZ,也就是1ms一个滴答,经过(72000000 / 1000)-1这个计算后装载给滴答计时器,这样滴答计时器就会没1ms溢出一次,计数值从0到71999(也就是说对于大于等于72000的计数值,是达不到的)。装载完计数值后就开启了滴答的中断,并使能滴答计时器。
在配置了滴答计时器后,下一行是初始化uxCriticalNesting这个变量为0,这个变量是标志当前进入临界区的嵌套层数,每当调用vPortEnterCritical(portENTER_CRITICAL)来屏蔽内核优先级11以下低优先级的中断(包括任务切换用的PendSV和滴答计时器中断)时,会让这个变量加1。调用vPortExitCritical这个函数来离开临界区时会减1,当这个变量为0时才会再度开启中断。
最后就是调用prvStartFirstTask()这个函数来开启第一个任务,让系统跑起来了:

__asm void prvStartFirstTask( void )
{
    PRESERVE8

    /* Use the NVIC offset register to locate the stack. */
    ldr r0, =0xE000ED08
    ldr r0, [r0]
    ldr r0, [r0]

    /* Set the msp back to the start of the stack. */
    msr msp, r0
    /* Globally enable interrupts. */
    cpsie i
    cpsie f
    dsb
    isb
    /* Call SVC to start the first task. */
    svc 0
    nop
    nop
}

这个函数是用内联汇编写的,0xE000ED08这个地址是向量表偏移量寄存器(VTOR)(见CM3权威指南中文版113页),保存的是向量表的地址。
FreeRTOS的源代码个人分析(基于KEIL下STM32F103的Demo) 四_第1张图片
一般情况向量表都是放在ROM里的,而且一般都是从0x00000000这个首地址开始存放。在第二篇里,降到main()里的prvSetupHardware函数里,有设置这个VTOR寄存器的一行代码,设置的就是0x00000000地址,因此现在通过ldr r0 [r0]读取这个寄存器的值得到的就是r0 = 0x00000000,通过Debug可以在左边栏里查看内核的各个寄存器的情况,也可以在代码上面一栏里看到编译后汇编代码与其对应的代码地址:
FreeRTOS的源代码个人分析(基于KEIL下STM32F103的Demo) 四_第2张图片
再次读取 ldr r0 [r0],得到的就是向量表第一个值也就是申请的主堆栈指针MSP的地址,然后将这个地址赋值给MSP寄存器。之后的cpsie i 和cpsie f这两个指令是开启全部的中断,然后调用svc 0,引发SVC中断,进入SVC中断服务—vPortSVCHandler里:

__asm void vPortSVCHandler( void )
{
    PRESERVE8

    ldr r3, =pxCurrentTCB   /* Restore the context. */
    ldr r1, [r3]            /* Use pxCurrentTCBConst to get the pxCurrentTCB address. */
    ldr r0, [r1]            /* The first item in pxCurrentTCB is the task top of stack. */
    ldmia r0!, {r4-r11}     /* Pop the registers that are not automatically saved on exception entry and the critical nesting count. */
    msr psp, r0             /* Restore the task stack pointer. */
    isb
    mov r0, #0
    msr basepri, r0
    orr r14, #0xd
    bx r14
}

这个函数也是用内联汇编写的,这个函数其实是xPortPendSVHandler服务的一部分,区别是不用保存当前任务下的各个寄存器数据和栈信息等,而是直接读取pxCurrentTCB然后跳转到这个任务去。首先是把re赋值为pxCurrentTCB这个变量地址,读取r3 ldr r1,[r3]就会得到这个指针指向的当前任务地址,也就是系统启动时第一个任务的地址。这个指针在调用xTaskCreate来创建任务时会被赋值,这个在后面分析xTaskCreate这个函数会说到。得到第一个任务的指针地址(存储在r1里)后,就可以跳转到这个任务地址去执行任务代码了,这个过程在第三篇分析 FreeRTOS进行任务切换的过程 的原理一样,不再重复叙述,不过要注意的是倒数第二行有个 orr r14, #0xd(按位或)。为什么要这么做可以见CM3权威指南中文版139页关于异常返回值,EXC_RETURN位段详解。
FreeRTOS的源代码个人分析(基于KEIL下STM32F103的Demo) 四_第3张图片
由这张图可见,这个按位或的作用是把R14寄存器的低4位设为D,在这个异常返回后进入线程模式,使用线程堆栈PSP,因为任务运行时要确保使用的是线程模式,只有发生中断或异常时,才让系统进入Handle模式并使用MSP。在xPortPendSVHandler里之所以没有这一行,是因为在进入这个异常前,系统正在跑任务,使用的就是线程模式和PSP,进入异常后变成Handle模式和MSP,但在异常返回时会自动回到这个异常发生前的模式也就是线程模式与PSP。在vPortSVCHandler这个函数被调用之前,系统一直是处于Handle模式并使用MSP的。(因为复位后就是Handle模式,因此大部分没用上系统的STM32的工程,都是让STM32处于Handle这个最高模式下运行的。)
在最后一行的bx r14被执行后,系统就会跳转到第一个任务进行执行了,然后系统就开始跑起来了!

你可能感兴趣的:(嵌入式,FreeRTOS,STM32)