其他分享
首页 > 其他分享> > 【freertos】005-启动调度器分析

【freertos】005-启动调度器分析

作者:互联网

前言

本节主要讲解启动调度器。

这些都是与硬件相关,所以会分两条线走:posix和cortex m3。

原文:李柱明博客:https://www.cnblogs.com/lizhuming/p/16076476.html

5.1 调度器的基本概念

5.1.1 调度器

调度器就是使用相关的调度算法来决定当前需要执行的任务。

调度器特点:

  1. 调度器可以区分就绪态任务和挂起任务。
  2. 调度器可以选择就绪态中的一个任务,然后激活它。
  3. 不同调度器之间最大的区别就是如何分配就绪态任务间的完成时间。

嵌入式实时操作系统的核心就是调度器和任务切换:

5.1.2 抢占式调度

抢占式调度:

5.1.3 时间片调度

最常用的的时间片调度算法就是Round-robin调度算法,这种调度算法可以用于抢占式或者合作式的多任务中。

实现Round-robin调度算法需要给同优先级的任务分配一个专门的列表,用于记录当前就绪的任务,并为每个任务分配一个时间片。

当任务就绪链表中最高优先级中存在两个以上的任务时,当前运行的任务耗尽时间片后,当前链表的下一个任务到运行态,把当前任务重新插入到当前优先级就绪链表尾部。

使用时间片调度需要在FreeRTOSConfig.h文件中使能宏定义:#defineconfigUSE_TIME_SLICING 1

需要注意的是,freertos时间片不能随意的设置时间为多少个tick,只能默认一个tick。

5.2 cortex m3架构的三个异常

在Cortex-M3架构中,FreeRTOS为了任务启动和任务切换使用了三个异常:SVC、PendSV和SysTick。

对应三个异常回调:

#define xPortPendSVHandler PendSV_Handler
#define xPortSysTickHandler SysTick_Handler
#define vPortSVCHandler SVC_Handler

注意:Cortex-M的优先级数值越大其优先级越低。

5.2.1 SVC

SVC(系统服务调用,亦简称系统调用)用于任务启动。所以只被调用一次。

有些操作系统不允许应用程序直接访问硬件,而是通过提供一些系统服务函数,用户程序使用SVC发出对系统服务函数的呼叫请求,以这种方法调用它们来间接访问硬件,它就会产生一个SVC异常。

在该异常回调里启动第一个任务。

5.2.2 PendSV

PendSV(可挂起系统调用)用于完成任务切换。

该异常可以像普通的中断一样被挂起的,它的最大特性是如果当前有优先级比它高的中断在运行,PendSV会延迟执行,直到高优先级中断执行完毕,这样子产生的PendSV中断就不会打断其他中断的运行。

在该异常的回调函数里执行任务切换。

5.2.3 SysTick

SysTick用于产生系统节拍时钟。

每次systick异常产生都会检查是否需要任务调度,如果需要,则出发PendSV异常即可。

5.3 启动调度器

5.3.1 启动调度器描述

启动调度器使用API函数vTaskStartScheduler()

该函数会:

启动调度器,硬件相关是调用xPortStartScheduler()

5.3.2 创建空闲任务

空闲任务时在启动调度器时创建的,该任务不能阻塞,创建空闲任务是为了不让系统退出,因为系统一旦启动就必须占有任务。

空闲任务主体主要是做一些系统内存的清理工作、进入休眠或者低功耗操作等操作。

创建空闲任务,也分两种方式,取决于是否开启静态内存分配宏configSUPPORT_STATIC_ALLOCATION

5.3.2.1 静态内存创建

参考前面任务基础相关的文章便可知,静态内存创建任务需要用户提供任务控制块和任务栈空间。

由于空闲任务是内核API创建的,所以用户需要通过指定的函数vApplicationGetIdleTaskMemory()提供这些信息。

实现代码如下:

/* 如果开启了静态内存功能,创建空闲任务就按静态内存创建 */
#if ( configSUPPORT_STATIC_ALLOCATION == 1 )
    {
        StaticTask_t * pxIdleTaskTCBBuffer = NULL;
        StackType_t * pxIdleTaskStackBuffer = NULL;
        uint32_t ulIdleTaskStackSize;

        /* 获取空闲任务的任务控制块地址、任务栈地址、任务栈大小这三个参数。
        	这个API是有用户实现 */
        vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer, &pxIdleTaskStackBuffer, &ulIdleTaskStackSize );
        /* 创建空闲任务,使用最低优先级*/
        xIdleTaskHandle = xTaskCreateStatic( prvIdleTask,
                                             configIDLE_TASK_NAME,
                                             ulIdleTaskStackSize,
                                             ( void * ) NULL,
                                             portPRIVILEGE_BIT,
                                             pxIdleTaskStackBuffer,
                                             pxIdleTaskTCBBuffer );

        if( xIdleTaskHandle != NULL )
        {
            xReturn = pdPASS;
        }
        else
        {
            xReturn = pdFAIL;
        }
    }
#endif /* if ( configSUPPORT_STATIC_ALLOCATION == 1 ) */

5.3.2.2 动态内存创建

动态内存创建空闲任务,直接使用xTaskCreate()实现即可。

#if ( configSUPPORT_STATIC_ALLOCATION != 1 )
{
    /* 动态内存方式创建空闲任务 */
    xReturn = xTaskCreate( prvIdleTask,
                           configIDLE_TASK_NAME,
                           configMINIMAL_STACK_SIZE,
                           ( void * ) NULL,
                           portPRIVILEGE_BIT,
                           &xIdleTaskHandle );
}
#endif /* configSUPPORT_STATIC_ALLOCATION */

5.3.3 创建软件定时器任务

软件定时器组件功能,后面会详细分析,这里只做简单说明

和创建空闲任务一个道理。

前提条件时需要配置configUSE_TIMERS开启软件定时器功能。

创建软件定时器内容集成在xTimerCreateTimerTask()API内部了,其实现和创建空闲任务一样的。

通过宏configSUPPORT_STATIC_ALLOCATION区分静态和动态内存创建。

5.3.3.1 初始化软件定时器组件内容

调用prvCheckForValidListAndQueue()API初始化定时链表和创建定时器通信服务队列。

5.3.3.2 静态内存创建

通过用户实现的vApplicationGetTimerTaskMemory()API获取软件定时器任务控制块和任务栈信息。

#if ( configSUPPORT_STATIC_ALLOCATION == 1 )
{
    StaticTask_t * pxTimerTaskTCBBuffer = NULL;
    StackType_t * pxTimerTaskStackBuffer = NULL;
    uint32_t ulTimerTaskStackSize;

    /* 获取软件定时器任务的任务控制块地址、任务栈地址、任务栈大小这三个参数。
        	这个API是有用户实现 */
    vApplicationGetTimerTaskMemory( &pxTimerTaskTCBBuffer, &pxTimerTaskStackBuffer, &ulTimerTaskStackSize );
    /* 创建软件定时器任务 */
    xTimerTaskHandle = xTaskCreateStatic( prvTimerTask,
                                          configTIMER_SERVICE_TASK_NAME,
                                          ulTimerTaskStackSize,
                                          NULL,
                                          ( ( UBaseType_t ) configTIMER_TASK_PRIORITY ) | portPRIVILEGE_BIT,
                                          pxTimerTaskStackBuffer,
                                          pxTimerTaskTCBBuffer );

    if( xTimerTaskHandle != NULL )
    {
        xReturn = pdPASS;
    }
}
#endif

5.3.3.3 动态内存创建

动态内存创建软件定时器任务,直接使用xTaskCreate()实现即可。

#if ( configSUPPORT_STATIC_ALLOCATION != 1 )
{
    /* 动态内存方式创建软件定时器任务 */
    xReturn = xTaskCreate( prvTimerTask,
                           configTIMER_SERVICE_TASK_NAME,
                           configTIMER_TASK_STACK_DEPTH,
                           NULL,
                           ( ( UBaseType_t ) configTIMER_TASK_PRIORITY ) | portPRIVILEGE_BIT,
                           &xTimerTaskHandle );
}
#endif /* configSUPPORT_STATIC_ALLOCATION */

5.3.4 调度器中的用户函数

在启动调度器时,内核运行用户插入一个函数调用,一般用于启动调度器标识处理。

指定函数:freertos_tasks_c_additions_init()

使能宏:FREERTOS_TASKS_C_ADDITIONS_INIT

/* freertos_tasks_c_additions_init 函数由用户定义,用于启动调度器时调用一次 */
#ifdef FREERTOS_TASKS_C_ADDITIONS_INIT
{
    freertos_tasks_c_additions_init();
}
#endif

5.3.5 CPU利用率统计配置

如果用户配置了portCONFIGURE_TIMER_FOR_RUN_TIME_STATS()宏函数,在调度器启动时需要调用。

该函数一般是重置定时器起始值,搭配portGET_RUN_TIME_COUNTER_VALUE()宏函数实现运行时间统计功能。

可以参考李柱明博客:cpu利用率统计后面可能会有独立章节描述该功能的实现

在启动调度器中的代码:

/* 如果宏configGENERATE_RUN_TIME_STATS被定义,表示使用运行时间统计功能,则下面这个宏必须被定义,用于初始化一个基础定时器/计数器.*/
portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();

5.3.6 posix启动调度器分析

源码分析:

5.3.6.1 启动调度器

在接口层,启动调度调用xPortStartScheduler()

portBASE_TYPE xPortStartScheduler( void )
{
    int iSignal;
    sigset_t xSignals;

    /* 获取当前线程ID */
    hMainThread = pthread_self();

    /* 设置系统计时器以按要求的频率生成滴答中断 */
    prvSetupTimerInterrupt();

    /* 开启第一个任务. */
    vPortStartFirstTask();

    /* 等待用户调用关闭调度器 vPortEndScheduler() 这个API发出的信号 */
    sigemptyset( &xSignals );
    sigaddset( &xSignals, SIG_RESUME );

    /* 等待关闭调度器的信号 */
    while ( !xSchedulerEnd )
    {
        sigwait( &xSignals, &iSignal ); 
    }

    /* 删除Idle任务并释放其资源 */
#if ( INCLUDE_xTaskGetIdleTaskHandle == 1 )
    vPortCancelThread( xTaskGetIdleTaskHandle() );
#endif

#if ( configUSE_TIMERS == 1 )
    /* 删除软件定时器任务并释放其资源 */
    vPortCancelThread( xTimerGetTimerDaemonTaskHandle() );
#endif /* configUSE_TIMERS */

    /* 恢复原始信号掩模 */
    (void)pthread_sigmask( SIG_SETMASK, &xSchedulerOriginalSignalMask,  NULL );

    return 0;

5.3.6.2 实现滴答时钟

利用进程实时定时器实现系统滴答:prvSetupTimerInterrupt()

采用posix标准下的getitimer()setitimer()API去实现。

在进程里使用ITIMER_REAL计数器实现系统滴答时钟。

void prvSetupTimerInterrupt( void )
{
    struct itimerval itimer;
    int iRet;

    /* 用当前的定时器信息初始化结构 */
    iRet = getitimer( ITIMER_REAL, &itimer );
    if ( iRet )
    {
        prvFatalError( "getitimer", errno );
    }

    /* 设置定时器事件之间的时间间隔. */
    itimer.it_interval.tv_sec = 0;
    itimer.it_interval.tv_usec = portTICK_RATE_MICROSECONDS;

    /* 设计初始值 */
    itimer.it_value.tv_sec = 0;
    itimer.it_value.tv_usec = portTICK_RATE_MICROSECONDS;

    /* 重置定时器. */
    iRet = setitimer( ITIMER_REAL, &itimer, NULL );
    if ( iRet )
    {
        prvFatalError( "setitimer", errno );
    }

    /* 获取纳秒值 */
    prvStartTimeNs = prvGetTimeNs();
}

5.3.6.3 启动第一个任务

利用线程通信实现启动第一个任务:vPortStartFirstTask()

原理在前面posix模拟器设计说过。

利用线程型号实现线程的启停从而实现任务切换。

先获取线程句柄:

void vPortStartFirstTask( void )
{
    /* 获取当前任务的线程句柄 */
    Thread_t *pxFirstThread = prvGetThreadFromTask( xTaskGetCurrentTaskHandle() );
    /* 启动第一个任务. */
    prvResumeThread( pxFirstThread );
}

发信号给下一个需要跑的线程,让其启动,这样就进入了freertos世界嘞:

static void prvResumeThread( Thread_t *xThreadId )
{
    /* 如果当前线程不是接下来要跑的线程 */
    if ( pthread_self() != xThreadId->pthread )
    {
        /* 发送事件启动新的线程 */
        event_signal(xThreadId->ev);
    }
}

void event_signal( struct event * ev )
{
    pthread_mutex_lock( &ev->mutex );
    ev->event_triggered = true; // 解除阻塞的标记
    pthread_cond_signal( &ev->cond ); // 发送信号给需要启动的线程,让其解除阻塞
    pthread_mutex_unlock( &ev->mutex );
}

那还需要停止当前线程嘞,完成这些时后,回进入等待结束调度器事件而阻塞:(代码在xPortStartScheduler()中)

/* 等待用户调用关闭调度器 vPortEndScheduler() 这个API发出的信号 */
sigemptyset( &xSignals );
sigaddset( &xSignals, SIG_RESUME );

/* 等待关闭调度器的信号 */
while ( !xSchedulerEnd )
{
    sigwait( &xSignals, &iSignal ); 
}

5.3.7 cortex m3启动调度器分析

启动调度器:xPortStartScheduler()

SVC异常启动第一个任务:vPortSVCHandler()

5.3.7.1 基本知识

  1. cortex m的双堆栈指针MSP和PSP的切换。

  2. 硬件出入栈和软件出入栈。

    1. 硬件出入栈:异常时,硬件会完成部分必要寄存器的出入栈。
    2. 软件出入栈:由于硬件压栈信息对保护上下文不够,需要软件出入栈完成其它CPU寄存器的出入栈。

5.3.7.2 cortex m3的启动调度器的基本内容

  1. 把PendSV和SysTick设置为最低优先级的中断。

  2. 启动滴答定时器。

  3. 启动第一个任务。通过SVC异常方式。

    1. 重置MSP堆栈指针。

    2. 使能全局中断。

    3. 触发SVC异常。进入SVC异常。

      1. 获取pxCurrentTCB值,即是当前需要跑的任务句柄。
      2. 通过任务句柄获取任务控制块,通过任务控制块获取任务栈顶。
      3. 软件出栈。
      4. 更新栈顶指针到PSP。
      5. 修改R14寄存器,使异常退出时,进入线程模式,使用PSP栈指针。
      6. 退出异常。硬件自动使用PSP出栈。

至此,系统已经启动,进入freertos世界。

5.3.7.3 FromISR中断保护配置

在freertos中会看到FromISR后缀的API,这些API执行环境不一样,一般用于中断回调中使用,要求不能阻塞,快进快出。

这些API不能在中断保护外的中断回调中使用,取决于宏configMAX_SYSCALL_INTERRUPT_PRIORITY

所以需要配置进出临界能屏蔽中断的优先级级别,优先级等于或低于 configMAX_SYSCALL_INTERRUPT_PRIORITY 的中断能被临界API屏蔽,可调用FromISR后缀的API。

先了解下几个宏:(数值越小,中断优先级越高)

#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;

    /* 确定可以调用ISR安全FreeRTOS API函数的最大优先级。
        ISR安全函数是以“FromISR”结尾的。
        FreeRTOS维护独立的线程和ISR API函数,以确保进入中断尽可能快和简单。
        保存将要被破坏的中断优先级值。 */
    ulOriginalPriority = *pucFirstUserPriorityRegister;

    /* 确定可用的优先级位数。首先写所有可能的位。 */
    *pucFirstUserPriorityRegister = portMAX_8_BIT_VALUE;
    /* 把值读回来看看,因为无效的优先级位读出位0,读出有多少个1就知道有多少位优先级。 */
    ucMaxPriorityValue = *pucFirstUserPriorityRegister;

    /* 内核中断优先级应该设置为最低优先级。 */
    configASSERT( ucMaxPriorityValue == ( configKERNEL_INTERRUPT_PRIORITY & ucMaxPriorityValue ) );

    /* 对最大系统调用优先级使用相同的掩码。 */
    ucMaxSysCallPriority = configMAX_SYSCALL_INTERRUPT_PRIORITY & ucMaxPriorityValue;

    /* 为回读的位数计算可接受的最大优先级组值。 */
    ulMaxPRIGROUPValue = portMAX_PRIGROUP_BITS;

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

    #ifdef __NVIC_PRIO_BITS
        {
            /* 检查定义优先级位数的CMSIS配置,该配置与实际从硬件查询的优先级位数相匹配。 */
            configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == __NVIC_PRIO_BITS );
        }
    #endif

    #ifdef configPRIO_BITS
        {
            /* 检查定义优先级位数的FreeRTOS配置,该配置与从硬件实际查询的优先级位数匹配。 */
            configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == configPRIO_BITS );
        }
    #endif

    /* 将优先级组的值移回它在AIRCR寄存器中的位置 */
    ulMaxPRIGROUPValue <<= portPRIGROUP_SHIFT;
    ulMaxPRIGROUPValue &= portPRIORITY_GROUP_MASK;

    /* 将中断的中断优先级寄存器恢复到原来的值 */
    *pucFirstUserPriorityRegister = ulOriginalPriority;
}
#endif /* configASSERT_DEFINED */

5.3.7.4 配置PendSV和SysTick中断优先级

PendSV用于切换任务;

SysTick用于系统节拍。

这两个都配置为最低优先级。

这样任务切换不会打断某个中断服务程序,中断服务程序也不会被延迟,有利于系统稳定。

而且SysTick是硬件定时器,响应可能会延迟,都是系统事件不会有偏差。

 /* 将PendSV和SysTick设置为最低优先级的中断 */
portNVIC_SHPR3_REG |= portNVIC_PENDSV_PRI;
portNVIC_SHPR3_REG |= portNVIC_SYSTICK_PRI;

5.3.7.5 启动滴答定时器

调用vPortSetupTimerInterrupt()实现。

5.3.7.6 启动第一个任务

调用prvStartFirstTask()实现。

启动第一个任务:

__asm void prvStartFirstTask( void )
{
    PRESERVE8 /* 当前栈需按照 8 字节对齐 */
    /* 在 Cortex-M 中,0xE000ED08 是 SCB_VTOR 寄存器的地址,里面存放的是向量表的起始地址,即 MSP 的地址 */
    ldr r0, =0xE000ED08 /* 将 0xE000ED08 这个立即数加载到寄存器 R0 */
    ldr r0, [ r0 ] /* 将 0xE000ED08 地址中的值,也就是向量表的实际地址加载到 R0 */
    ldr r0, [ r0 ] /* 根据向量表实际存储地址,取出向量表中的第一项,向量表第一项存储主堆栈指针 MSP 的初始值 */

    /* 将msp设置回堆栈的开始 */
    msr msp, r0
    /* 使能全局中断 */
    cpsie i
    cpsie f
    dsb
    isb
    /* 触发SVC异常开启动第一个任务. */
    svc 0
    nop
    nop
/* *INDENT-ON* */
}

SVC回调:

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

    ldr r3, = pxCurrentTCB   /* 加载 pxCurrentTCB 的地址到 r3. */
    ldr r1, [ r3 ] /* 加载 pxCurrentTCB 到 r3. 而任务控制块的第一个成员就是任务栈顶指针。 */
    ldr r0, [ r1 ]           /* 任务控制块的第一个成员就是栈顶指针,所以此时 r0 等于栈顶指针 */
    ldmia r0 !, { r4 - r11 } /* 软件出栈部分,r4-r11寄存器出栈 */
    msr psp, r0 /* 将新的栈顶指针 r0 更新到 psp,任务执行的时候使用的堆栈指针是psp. */
    isb
    mov r0, # 0 /* 将寄存器 r0 清 0 */
    msr basepri, r0 /* 设置 basepri 寄存器的值为 0,即打开所有中断。basepri 是一个中断屏蔽寄存器,大于等于此寄存器值的中断都将被屏蔽。Cortex-M的优先级数值越大其优先级越低。 */
    orr r14, # 0xd /* 向 r14 寄存器最后 4 位按位或上0x0D。退出异常时使用进程堆栈指针 PSP 完成出栈操作并返回后进入任务模式、返回 Thumb 状态 */
    bx r14 /* 异常返回,这个时候出栈使用的是 PSP 指针,自动将栈中的剩下内容加载到 CPU 寄存器: xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0。PSP 的值也将更新,即指向任务栈的栈顶 */
/* *INDENT-ON* */
}

5.3.7.7 启动第一个任务后的任务栈情况

该图片源自野火

附件

vTaskStartScheduler()

void vTaskStartScheduler( void )
{
    BaseType_t xReturn;

    /* 如果开启了静态内存功能,创建空闲任务就按静态内存创建 */
    #if ( configSUPPORT_STATIC_ALLOCATION == 1 )
        {
            StaticTask_t * pxIdleTaskTCBBuffer = NULL;
            StackType_t * pxIdleTaskStackBuffer = NULL;
            uint32_t ulIdleTaskStackSize;

            /* 获取空闲任务的任务控制块地址、任务栈地址、任务栈大小这三个参数。
            	这个API是有用户实现 */
            vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer, &pxIdleTaskStackBuffer, &ulIdleTaskStackSize );
            /* 创建空闲任务,使用最低优先级*/
            xIdleTaskHandle = xTaskCreateStatic( prvIdleTask,
                                                 configIDLE_TASK_NAME,
                                                 ulIdleTaskStackSize,
                                                 ( void * ) NULL,
                                                 portPRIVILEGE_BIT,
                                                 pxIdleTaskStackBuffer,
                                                 pxIdleTaskTCBBuffer );

            if( xIdleTaskHandle != NULL )
            {
                xReturn = pdPASS;
            }
            else
            {
                xReturn = pdFAIL;
            }
        }
    #else /* if ( configSUPPORT_STATIC_ALLOCATION == 1 ) */
        {
            /* 动态内存方式创建空闲任务 */
            xReturn = xTaskCreate( prvIdleTask,
                                   configIDLE_TASK_NAME,
                                   configMINIMAL_STACK_SIZE,
                                   ( void * ) NULL,
                                   portPRIVILEGE_BIT,
                                   &xIdleTaskHandle );
        }
    #endif /* configSUPPORT_STATIC_ALLOCATION */

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

    if( xReturn == pdPASS )
    {
        /* freertos_tasks_c_additions_init 函数由用户定义,用于启动调度器时调用一次 */
        #ifdef FREERTOS_TASKS_C_ADDITIONS_INIT
            {
                freertos_tasks_c_additions_init();
            }
        #endif

        /* 先关闭中断,确保节拍定时器中断不会在调用xPortStartScheduler()时或之前发生。当第一个任务启动时,会重新启动中断*/
        portDISABLE_INTERRUPTS();

        #if ( configUSE_NEWLIB_REENTRANT == 1 )
            {
                /* 略 */
                _impure_ptr = &( pxCurrentTCB->xNewLib_reent );
            }
        #endif /* configUSE_NEWLIB_REENTRANT */

        /* 初始化静态变量 */
        xNextTaskUnblockTime = portMAX_DELAY;
        xSchedulerRunning = pdTRUE;
        xTickCount = ( TickType_t ) configINITIAL_TICK_COUNT;

        /* 如果宏configGENERATE_RUN_TIME_STATS被定义,表示使用运行时间统计功能,则下面这个宏必须被定义,用于初始化一个基础定时器/计数器.*/
        portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();

        traceTASK_SWITCHED_IN();

        /* 设置系统节拍定时器,这与硬件特性相关,因此被放在了移植层.*/
        if( xPortStartScheduler() != pdFALSE )
        {
            /* 如果调度器正确运行,则不会执行到这里,函数也不会返回*/
        }
        else
        {
            /* 仅当任务调用API函数xTaskEndScheduler()后,会执行到这里.*/
        }
    }
    else
    {
        /* 执行到这里表示内核没有启动,可能因为堆栈空间不够 */
        configASSERT( xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY );
    }

    /* 预防编译器警告*/
    ( void ) xIdleTaskHandle;
    ( void ) uxTopUsedPriority;
}

posix:xPortStartScheduler()

portBASE_TYPE xPortStartScheduler( void )
{
    int iSignal;
    sigset_t xSignals;

    /* 获取当前线程ID */
    hMainThread = pthread_self();

    /* 设置系统计时器以按要求的频率生成滴答中断 */
    prvSetupTimerInterrupt();

    /* 开启第一个任务. */
    vPortStartFirstTask();

    /* 等待用户调用关闭调度器 vPortEndScheduler() 这个API发出的信号 */
    sigemptyset( &xSignals );
    sigaddset( &xSignals, SIG_RESUME );

    /* 等待关闭调度器的信号 */
    while ( !xSchedulerEnd )
    {
        sigwait( &xSignals, &iSignal ); 
    }

    /* Cancel the Idle task and free its resources */
#if ( INCLUDE_xTaskGetIdleTaskHandle == 1 )
    vPortCancelThread( xTaskGetIdleTaskHandle() );
#endif

#if ( configUSE_TIMERS == 1 )
    /* Cancel the Timer task and free its resources */
    vPortCancelThread( xTimerGetTimerDaemonTaskHandle() );
#endif /* configUSE_TIMERS */

    /* Restore original signal mask. */
    (void)pthread_sigmask( SIG_SETMASK, &xSchedulerOriginalSignalMask,  NULL );

    return 0;
}

posix:prvSetupTimerInterrupt()

void prvSetupTimerInterrupt( void )
{
    struct itimerval itimer;
    int iRet;

    /* 用当前的定时器信息初始化结构 */
    iRet = getitimer( ITIMER_REAL, &itimer );
    if ( iRet )
    {
        prvFatalError( "getitimer", errno );
    }

    /* 设置定时器事件之间的时间间隔. */
    itimer.it_interval.tv_sec = 0;
    itimer.it_interval.tv_usec = portTICK_RATE_MICROSECONDS;

    /* 设计初始值 */
    itimer.it_value.tv_sec = 0;
    itimer.it_value.tv_usec = portTICK_RATE_MICROSECONDS;

    /* 重置定时器. */
    iRet = setitimer( ITIMER_REAL, &itimer, NULL );
    if ( iRet )
    {
        prvFatalError( "setitimer", errno );
    }

    /* 获取纳秒值 */
    prvStartTimeNs = prvGetTimeNs();
}

posix:vPortStartFirstTask()

void vPortStartFirstTask( void )
{
    /* 获取当前任务的线程句柄 */
    Thread_t *pxFirstThread = prvGetThreadFromTask( xTaskGetCurrentTaskHandle() );

    /* 启动第一个任务. */
    prvResumeThread( pxFirstThread );
}

static void prvResumeThread( Thread_t *xThreadId )
{
    /* 如果当前线程不是接下来要跑的线程 */
    if ( pthread_self() != xThreadId->pthread )
    {
        /* 发送事件启动新的线程 */
        event_signal(xThreadId->ev);
    }
}

void event_signal( struct event * ev )
{
    pthread_mutex_lock( &ev->mutex );
    ev->event_triggered = true; // 解除阻塞的标记
    pthread_cond_signal( &ev->cond ); // 发送信号给需要启动的线程,让其解除阻塞
    pthread_mutex_unlock( &ev->mutex );
}

cortex m3:xPortStartScheduler()

BaseType_t xPortStartScheduler( void )
{
    #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;

            /* 确定可以调用ISR安全FreeRTOS API函数的最大优先级。
                ISR安全函数是以“FromISR”结尾的。
                FreeRTOS维护独立的线程和ISR API函数,以确保进入中断尽可能快和简单。
                保存将要被破坏的中断优先级值。 */
            ulOriginalPriority = *pucFirstUserPriorityRegister;

            /* 确定可用的优先级位数。首先写所有可能的位。 */
            *pucFirstUserPriorityRegister = portMAX_8_BIT_VALUE;
            /* 把值读回来看看,因为无效的优先级位读出位0,读出有多少个1就知道有多少位优先级。 */
            ucMaxPriorityValue = *pucFirstUserPriorityRegister;

            /* 内核中断优先级应该设置为最低优先级。 */
            configASSERT( ucMaxPriorityValue == ( configKERNEL_INTERRUPT_PRIORITY & ucMaxPriorityValue ) );

            /* 对最大系统调用优先级使用相同的掩码。 */
            ucMaxSysCallPriority = configMAX_SYSCALL_INTERRUPT_PRIORITY & ucMaxPriorityValue;

            /* 为回读的位数计算可接受的最大优先级组值。 */
            ulMaxPRIGROUPValue = portMAX_PRIGROUP_BITS;

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

            #ifdef __NVIC_PRIO_BITS
                {
                    /* 检查定义优先级位数的CMSIS配置,该配置与实际从硬件查询的优先级位数相匹配。 */
                    configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == __NVIC_PRIO_BITS );
                }
            #endif

            #ifdef configPRIO_BITS
                {
                    /* 检查定义优先级位数的FreeRTOS配置,该配置与从硬件实际查询的优先级位数匹配。 */
                    configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == configPRIO_BITS );
                }
            #endif

            /* 将优先级组的值移回它在AIRCR寄存器中的位置 */
            ulMaxPRIGROUPValue <<= portPRIGROUP_SHIFT;
            ulMaxPRIGROUPValue &= portPRIORITY_GROUP_MASK;

            /* 将中断的中断优先级寄存器恢复到原来的值 */
            *pucFirstUserPriorityRegister = ulOriginalPriority;
        }
    #endif /* configASSERT_DEFINED */

    /* 将PendSV和SysTick设置为最低优先级的中断 */
    portNVIC_SHPR3_REG |= portNVIC_PENDSV_PRI;
    portNVIC_SHPR3_REG |= portNVIC_SYSTICK_PRI;

    /* 启动滴答定时器。注意,当前全局中断是关闭的,在启动第一个任务时会开启。 */
    vPortSetupTimerInterrupt();

    /* 初始化为第一个任务准备的关键嵌套计数。 */
    uxCriticalNesting = 0;

    /* 启动第一个任务。 */
    prvStartFirstTask();

    /* 启动调度器后时不会跑到这里的 */
    return 0;
}

cortex m3:prvStartFirstTask()

__asm void prvStartFirstTask( void )
{
/* *INDENT-OFF* */
    PRESERVE8 /* 当前栈需按照 8 字节对齐 */

    /* 在 Cortex-M 中,0xE000ED08 是 SCB_VTOR 寄存器的地址,里面存放的是向量表的起始地址,即 MSP 的地址 */
    ldr r0, =0xE000ED08 /* 将 0xE000ED08 这个立即数加载到寄存器 R0 */
    ldr r0, [ r0 ] /* 将 0xE000ED08 地址中的值,也就是向量表的实际地址加载到 R0 */
    ldr r0, [ r0 ] /* 根据向量表实际存储地址,取出向量表中的第一项,向量表第一项存储主堆栈指针 MSP 的初始值 */

    /* 将msp设置回堆栈的开始 */
    msr msp, r0
    /* 使能全局中断 */
    cpsie i
    cpsie f
    dsb
    isb
    /* 触发SVC异常开启动第一个任务. */
    svc 0
    nop
    nop
/* *INDENT-ON* */
}

cortex m3:vPortSVCHandler()

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

    ldr r3, = pxCurrentTCB   /* 加载 pxCurrentTCB 的地址到 r3. */
    ldr r1, [ r3 ] /* 加载 pxCurrentTCB 到 r3. 而任务控制块的第一个成员就是任务栈顶指针。 */
    ldr r0, [ r1 ]           /* 任务控制块的第一个成员就是栈顶指针,所以此时 r0 等于栈顶指针 */
    ldmia r0 !, { r4 - r11 } /* 软件出栈部分,r4-r11寄存器出栈 */
    msr psp, r0 /* 将新的栈顶指针 r0 更新到 psp,任务执行的时候使用的堆栈指针是psp. */
    isb
    mov r0, # 0 /* 将寄存器 r0 清 0 */
    msr basepri, r0 /* 设置 basepri 寄存器的值为 0,即打开所有中断。basepri 是一个中断屏蔽寄存器,大于等于此寄存器值的中断都将被屏蔽。Cortex-M的优先级数值越大其优先级越低。 */
    orr r14, # 0xd /* 向 r14 寄存器最后 4 位按位或上0x0D。退出异常时使用进程堆栈指针 PSP 完成出栈操作并返回后进入任务模式、返回 Thumb 状态 */
    bx r14 /* 异常返回,这个时候出栈使用的是 PSP 指针,自动将栈中的剩下内容加载到 CPU 寄存器: xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0。PSP 的值也将更新,即指向任务栈的栈顶 */
/* *INDENT-ON* */
}

cortex m3:xPortPendSVHandler()

__asm void xPortPendSVHandler(void)
{
    extern uxCriticalNesting;
    extern pxCurrentTCB; /* 指向当前激活的任务 */
    extern vTaskSwitchContext;

    PRESERVE8

    mrs r0, psp     /* PSP内容存入R0 */
    isb /* 指令同步隔离,清流水线 */

    ldr r3, = pxCurrentTCB /* 当前激活的任务TCB指针存入R2 */
    ldr r2,[r3]

    stmdb r0 !,{r4 - r11} /* 保存剩余的寄存器,异常处理程序执行前,硬件自动将xPSR、PC、LR、R12、R0-R3入栈 */
    str r0,[r2] /* 将新的栈顶保存到任务TCB的第一个成员中 */

    stmdb sp !,{r3, r14} /* 将R3和R14临时压入堆栈,因为即将调用函数vTaskSwitchContext,调用函数时,返回地址自动保存到R14中,所以一旦调用发生,R14的值会被覆盖,因此需要入栈保护; R3保存的当前激活的任务TCB指针(pxCurrentTCB)地址,函数调用后会用到,因此也要入栈保护*/
    mov r0,#configMAX_SYSCALL_INTERRUPT_PRIORITY /* 进入临界区 */
    msr basepri,r0
    dsb /* 数据和指令同步隔离 */
    isb
    bl vTaskSwitchContext /* 调用函数,寻找新的任务运行,通过使变量pxCurrentTCB指向新的任务来实现任务切换 */
    mov r0,#0 /* 退出临界区*/
    msr basepri,r0
    ldmia sp !,
    {r3, r14} /* 恢复R3和R14*/

    ldr r1,[r3] 
    ldr r0, [r1] /* 当前激活的任务TCB第一项保存了任务堆栈的栈顶,现在栈顶值存入R0*/
    ldmia r0 !,{r4 - r11} /* 出栈*/
    msr psp,r0
    isb
    bx r14 /* 异常发生时,R14中保存异常返回标志,包括返回后进入线程模式还是处理器模式、使用PSP堆栈指针还是MSP堆栈指针,当调用 bx r14指令后,硬件会知道要从异常返回,然后出栈,这个时候堆栈指针PSP已经指向了新任务堆栈的正确位置,当新任务的运行地址被出栈到PC寄存器后,新的任务也会被执行。*/
    nop
}

标签:定时器,优先级,freertos,void,调度,任务,005,r0
来源: https://www.cnblogs.com/lizhuming/p/16076476.html