05. FreeRTOS的任务调度

一、开启任务调度

   vTaskStartScheduler() 函数用于启动任务调度器,任务调度器启动后,FreeRTOS 便会开始进行任务调度,除非调用函数 xTaskEndScheduler() 停止任务调度器,否则不会再返回。函数 vTaskStartScheduler() 的代码如下所示:

void vTaskStartScheduler( void )
{
    BaseType_t xReturn;

    traceENTER_vTaskStartScheduler();

    #if ( configUSE_CORE_AFFINITY == 1 ) && ( configNUMBER_OF_CORES > 1 )
    {
        configASSERT( ( sizeof( UBaseType_t ) * taskBITS_PER_BYTE ) >= configNUMBER_OF_CORES );
    }
    #endif /* #if ( configUSE_CORE_AFFINITY == 1 ) && ( configNUMBER_OF_CORES > 1 ) */

    xReturn = prvCreateIdleTasks();                                             // 创建空闲任务

    #if ( configUSE_TIMERS == 1 )                                               // 如果使能软件定时器
    {
        if( xReturn == pdPASS )                                                 // 程序执行到这,说明空闲任务创建成功
        {
            xReturn = xTimerCreateTimerTask();                                  // 创建软件定时器任务
        }
        else
        {
            mtCOVERAGE_TEST_MARKER();
        }
    }
    #endif /* configUSE_TIMERS */

    if( xReturn == pdPASS )                                                     // 程序执行到这,说明软件定时器任务执行成功
    {
        #ifdef FREERTOS_TASKS_C_ADDITIONS_INIT
        {
            freertos_tasks_c_additions_init();                                  // 此函数用于添加一些附加初始化
        }
        #endif

        portDISABLE_INTERRUPTS();                                               // 关闭中断

        #if ( configUSE_C_RUNTIME_TLS_SUPPORT == 1 )
        {
            configSET_TLS_BLOCK( pxCurrentTCB->xTLSBlock );
        }
        #endif

        xNextTaskUnblockTime = portMAX_DELAY;                                   // 下一个任务的阻塞超时时间
        xSchedulerRunning = pdTRUE;                                             // 调度器的运行状态
        xTickCount = ( TickType_t ) configINITIAL_TICK_COUNT;                   // 系统节拍

        portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();                               // 统计任务的运行时间

        traceTASK_SWITCHED_IN();                                                // 调试功能

        // 该函数用于完成启动任务调度器中与硬件架构相关的配置部分,以及启动第一个任务
        ( void ) xPortStartScheduler();
    }
    else
    {
        // 动态方式创建空闲任务和定时器服务任务(如果有)时,因分配给 FreeRTOS 的堆空间不足,导致任务无法成功创建
        configASSERT( xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY );
    }

    ( void ) xIdleTaskHandles;                                                  // 防止编译器警告
    ( void ) uxTopUsedPriority;                                                 // 调试使用

    traceRETURN_vTaskStartScheduler();
}

  从上面的代码可以看出,函数 vTaskStartScheduler() 主要做了六件事情:

  1. 创建空闲任务,根据是否支持静态内存管理,使用静态方式或动态方式创建空闲任务。
  2. 创建定时器服务任务,创建定时器服务任务需要配置启用软件定时器,创建定时器服务任务,同样是根据是否配置支持静态内存管理,使用静态或动态方式创建定时器服务任务。
  3. 关闭中断,使用 portDISABLE_INTERRUPT() 关闭中断,这种方式只关闭受 FreeRTOS 管理的中断。关闭中断主要是为了防止 SysTick 中断在任务调度器开启之前或过程中,产生中断。FreeRTOS 会在开始运行第一个任务时,重新打开中断。
  4. 初始化一些全局变量,并将任务调度器的运行标志设置为已运行。
  5. 初始化任务运行时间统计功能的时基定时器,任务运行时间统计功能需要一个硬件定时器提供高精度的计数,这个硬件定时器就在这里进行配置,如果配置不启用任务运行时间统计功能的,就无需进行这项硬件定时器的配置。
  6. 最后就是调用函数 xPortStartScheduler()。

  xPortStartScheduler() 完成启动任务调度器中与硬件架构相关的配置部分,以及启动第一个任务,具体的代码如下所示:

BaseType_t xPortStartScheduler( void )
{
    configASSERT( portCPUID != portCORTEX_M7_r0p1_ID );
    configASSERT( portCPUID != portCORTEX_M7_r0p0_ID );

    #if ( configCHECK_HANDLER_INSTALLATION == 1 )
    {
        const portISR_t * const pxVectorTable = portSCB_VTOR_REG;

        configASSERT( pxVectorTable[ portVECTOR_INDEX_SVC ] == vPortSVCHandler );
        configASSERT( pxVectorTable[ portVECTOR_INDEX_PENDSV ] == xPortPendSVHandler );
    }
    #endif /* configCHECK_HANDLER_INSTALLATION */

    // 检测用户在 FreeRTOSConfig.h 文件中对中断相关部分的配置是否有误
    #if ( configASSERT_DEFINED == 1 )
    {
        volatile uint8_t ucOriginalPriority;
        volatile uint32_t ulImplementedPrioBits = 0;
        volatile uint8_t * const pucFirstUserPriorityRegister = ( volatile uint8_t * const ) ( portNVIC_IP_REGISTERS_OFFSET_16 + portFIRST_USER_INTERRUPT_NUMBER );
        volatile uint8_t ucMaxPriorityValue;

        ucOriginalPriority = *pucFirstUserPriorityRegister;
        *pucFirstUserPriorityRegister = portMAX_8_BIT_VALUE;
        ucMaxPriorityValue = *pucFirstUserPriorityRegister;
        ucMaxSysCallPriority = configMAX_SYSCALL_INTERRUPT_PRIORITY & ucMaxPriorityValue;

        configASSERT( ucMaxSysCallPriority );

        configASSERT( ( configMAX_SYSCALL_INTERRUPT_PRIORITY & ( ~ucMaxPriorityValue ) ) == 0U );

        while( ( ucMaxPriorityValue & portTOP_BIT_OF_BYTE ) == portTOP_BIT_OF_BYTE )
        {
            ulImplementedPrioBits++;
            ucMaxPriorityValue <<= ( uint8_t ) 0x01;
        }

        if( ulImplementedPrioBits == 8 )
        {
            configASSERT( ( configMAX_SYSCALL_INTERRUPT_PRIORITY & 0x1U ) == 0U );
            ulMaxPRIGROUPValue = 0;
        }
        else
        {
            ulMaxPRIGROUPValue = portMAX_PRIGROUP_BITS - ulImplementedPrioBits;
        }

        ulMaxPRIGROUPValue <<= portPRIGROUP_SHIFT;
        ulMaxPRIGROUPValue &= portPRIORITY_GROUP_MASK;

        *pucFirstUserPriorityRegister = ucOriginalPriority;
    }
    #endif /* configASSERT_DEFINED */

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

    // 配置 SysTick,清空 SysTick 的计数值,根据 configTICK_RATE_HZ 配置 SysTick 的重装载值,开启 SysTick 计数和中断
    vPortSetupTimerInterrupt();

    uxCriticalNesting = 0;                                                      // 初始化临界区嵌套次数计数器为 0

    // 使能 FPU,仅 ARM Cortex-M4/M7 内核 MCU 才有此行代码,ARM Cortex-M3 内核 MCU 无 FPU
    vPortEnableVFP();

    // 在进出异常时,自动保存和恢复 FPU 相关寄存器,仅 ARM Cortex-M4/M7 内核 MCU 才有此行代码, 仅 ARM Cortex-M4/M7 内核 MCU 才有此行代码
    *( portFPCCR ) |= portASPEN_AND_LSPEN_BITS;

    prvPortStartFirstTask();                                                    // 启动第一个任务

    vTaskSwitchContext();
    prvTaskExitError();

    // 正常情况下,程序不会指定到这里
    return 0;
}

  xPortStartScheduler() 函数的解析如下所示:

  1. 在启用断言的情况下,函数 xPortStartScheduler() 会检测用户在 FreeRTOSConfig.h 文件中对中断的相关配置是否有误。
  2. 配置 PendSV 和 SysTick 的中断优先级为最低优先级。
  3. 调用函数 vPortSetupTimerInterrupt() 配置 SysTick,函数 vPortSetupTimerInterrupt() 首先会将 SysTick 当前计数值清空,并根据 FreeRTOSConfig.h 文件中配置的 configSYSTICK_CLOCK_HZ(SysTick 时钟源频率)和 configTICK_RATE_HZ(系统时钟节拍频率)计算并设置 SysTick 的重装载值,然后启动 SysTick 计数和中断。
  4. 初始化临界区嵌套计数器为 0。
  5. 调用函数 prvEnableVFP() 使能 FPU,因为 ARM Cortex-M3 内核 MCU 无 FPU,此函数仅在 ARM Cortex-M4/M7 内核 MCU 平台上被调用,执行改函数后 FPU 被开启。
  6. 接下来将 FPCCR 寄存器的 [31:30] 置 1,这样在进出异常时,FPU 的相关寄存器就会自动地保存和恢复,同样地,因为 ARM Cortex-M3 内核 MCU 无 FPU,此当代码仅在 ARM Cortex M4/M7 内核 MCU 平台上被调用。
  7. 调用函数 prvStartFirstTask() 启动第一个任务。

二、启动第一个任务

  prvStartFirstTask() 函数用于初始化启动第一个任务前的环境,主要是重新设置 MSP 指针,并使能全局中断,具体的代码如下所示:

static void prvPortStartFirstTask( void )
{
    __asm volatile (
        " ldr r0, =0xE000ED08   \n"                                             //  0xE000ED08 为 VTOR 地址
        " ldr r0, [r0]          \n"                                             // 获取 VTOR 的值
        " ldr r0, [r0]          \n"                                             // 获取 VTOR 的值
        " msr msp, r0           \n"                                             // 初始化 MSP
        " mov r0, #0            \n" 
        " msr control, r0       \n"
        " cpsie i               \n"                                             // 使能全局中断
        " cpsie f               \n"
        " dsb                   \n"
        " isb                   \n"
        " svc 0                 \n"                                             // 调用 SVC 启动第一个任务
        " nop                   \n"
        " .ltorg                \n"
        );
}

  程序在运行过程中需要一定的栈空间来保存局部变量等一些信息。当有信息保存到栈中时,MCU 会自动更新 SP 指针,使 SP 指针指向最后一个入栈的元素,那么程序就可以根据 SP 指针来从栈中存取信息。ARM Cortex-M 提供了两个栈空间,这两个栈空间的堆栈指针分别是 MSP(主堆栈指针)和 PSP(进程堆栈指针)。在 FreeRTOS 中 MSP 是给系统栈空间使用的,而 PSP 是给任务栈使用的,也就是说,FreeRTOS 任务的栈空间是通过 PSP 指向的,而在进入中断服务函数时,则是使用 MSP 指针。当使用不同的堆栈指针时,SP 会等于当前使用的堆栈指针。

  0xE000ED08 是 VTOR(向量表偏移寄存器)的地址,VTOR 中保存了向量表的偏移地址。一般来说向量表是从其实地址 0x00000000 开始的,但是在有情况下,可能需要修改或重定向向一般来说向量表是从其实地址 0x00000000 开始的,但是在有情况下,可能需要修改或重定向向量表的首地址,因此 ARM Corten-M 提供了 VTOR 对向量表进行从定向。而向量表是用来保存中断异常的入口函数地址,即栈顶地址的,并且向量表中的第一个字保存的就是栈底的地址。

  在获取了栈顶指针后,将 MSP 指针重新赋值为栈底指针。这个操作相当于丢弃了程序之前保存在栈中的数据,因为 FreeRTOS 从开启任务调度器到启动第一个任务都是不会返回的,因此将栈中的数据丢弃,也不会有影响。

  重新赋值 MSP 后,接下来就重新使能全局中断,因为之前在函数 vTaskStartScheduler() 中关闭了受 FreeRTOS 的中断。

  最后使用 SVC 指令,并传入系统调用号 0,触发 SVC 中断。

中断产生时,硬件自动将 xPSR,PC(R15),LR(R14),R12,R3-R0 出/入栈; 而 R4~R11 需要手动出/入栈。

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

  当使能了全局中断,并且手动触发 SVC 中断后,就会进入到 SVC 的中断服务函数中。SVC 的中断服务函数为 vPortSVCHandler(),该函数在 port.c 文件中有定义,具体的代码如下所示:

void vPortSVCHandler( void )
{
    __asm volatile (
        // 获取任务栈地址
        "   ldr r3, pxCurrentTCBConst2      \n"                                 // r3 指向优先级最高的就绪态任务的任务控制块
        "   ldr r1, [r3]                    \n"                                 // r1 为任务控制块地址
        "   ldr r0, [r1]                    \n"                                 // r0 为任务控制块的第一个元素(栈顶)

        // 模拟出栈,并设置 PSP
        "   ldmia r0!, {r4-r11, r14}        \n"                                 // 任务栈弹出到 CPU 寄存器
        "   msr psp, r0                     \n"                                 //  设置 PSP 为任务栈指针
        "   isb                             \n"

        "   mov r0, #0                      \n"                                 // 使能所有中断
        "   msr basepri, r0                 \n"
        "   bx r14                          \n"                                 // 使用 PSP 指针,并跳转到任务函数
        "                                   \n"
        "   .align 4                        \n"
        "pxCurrentTCBConst2: .word pxCurrentTCB             \n"
        );
}

  从上面代码中可以看出,函数 vPortSVCHandler() 就是用来跳转到第一个任务函数中去的,该函数的具体解析如下:

  首先通过 pxCurrentTCB 获取优先级最高的就绪态任务的任务栈地址,优先级最高的就绪态任务就是系统将要运行的任务。pxCurrentTCB 是一个全局变量,用于指向系统中优先级最高的就绪态任务的任务控制块,在前面创建 start_task 任务、空闲任务、定时器处理任务时自动根据任务的优先级高低进行赋值的。

  定时器处理任务的任务优先级为 31,是系统中优先级最高的任务,因此当进入 SVC 中断时,pxCurrentTCB 就是指向了定时器处理任务的任务控制块。接着通过获取任务控制块中的第一个元素,得到该任务的栈顶指针。

  接下来通过任务的栈顶指针,将任务栈中的内容出栈到 CPU 寄存器中,任务栈中的内容在调用任务创建函数的时候,已经初始化了。然后再设置 PSP 指针,那么,这么一来,任务的运行环境就准备好了。

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

  最后通过汇编指令,使 CPU 跳转到任务的函数中去执行。

SVC中断只在启动第一次任务时会调用一次,以后均不调用。

三、任务切换

  任务切换的本质就是 CPU 寄存器的切换。假设当由任务 A 切换到任务 B 时,主要分为两步:

  1. 需暂停任务 A 的执行,并将此时任务 A 的寄存器保存到任务堆栈,这个过程叫做 保存现场
  2. 将任务 B 的各个寄存器值(被存于任务堆栈中)恢复到 CPU 寄存器中,这个过程叫做 恢复现场

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

任务切换

  我们通过向中断控制和状态寄存器 ICSR 的 bit28 写入 1 挂起 PendSV 来启动 PendSV 中断。PendSV 中断可以通过如下方式触发:

  • 滴答定时器中断调用。
  • 执行 FreeRTOS 提供的相关 API 函数:portYIELD()

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

四、PendSV中断服务函数

  FreeRTOS 在 PendSV 的中断中,完成任务切换,PendSV 的中断服务函数由 FreeRTOS 编写,将 PendSV 的中断服务函数定义成函数 xPortPendSVHandler()

  针对 ARM Cortex-M3 和针对 ARM Cortex-M4 和 ARM Cortex-M7 内核的函数 xPortPendSVHandler() 稍有不同,其主要原因在于 ARM Cortex-M4 和 ARM Cortex-M7 内核具有浮点单元,因此在进行任务切换的时候,还需考虑是否保护和恢复浮点寄存器的值。

  针对 ARM Cortex-M4 内核的函数 xPortPendSVHandler() ,具体的代码如下所示:

void xPortPendSVHandler( void )
{
    /* This is a naked function. */

    __asm volatile
    (
        "   mrs r0, psp                         \n"                             //  R0 为 PSP,即当前运行任务的任务栈指针
        "   isb                                 \n"
        "                                       \n"
        "   ldr r3, pxCurrentTCBConst           \n"                             //  R3 为 pxCurrentTCB 的地址值,即指向当前运行任务控制块的指针
        "   ldr r2, [r3]                        \n"                             //  R2 为 pxCurrentTCB 的值,即当前运行任务控制块的首地址
        "                                       \n"
        // 取 R14 寄存器的值,因为处于中断,此时 R14 为 EXC_RETURN,
        // 通过判断 EXC_RETURN 的 bit4 是否为 0,判断在进入 PendSV 中断前运行的任务是否使用的浮点单元,
        // 若使用了浮点单元,需要在切换任务时,保存浮点寄存器的值
        "   tst r14, #0x10                      \n" /* Is the task using the FPU context?  If so, push high vfp registers. */
        "   it eq                               \n"
        "   vstmdbeq r0!, {s16-s31}             \n"
        "                                       \n"
        //  将 R4~R11 和 R14 寄存器入栈到当前运行任务的任务栈中,此时的 R14 为 EXC_RETURN,主要用于指示任务是否使用了浮点单元
        "   stmdb r0!, {r4-r11, r14}            \n" 
        "   str r0, [r2]                        \n"                             //  R2 指向的地址为此时的任务栈指针
        "                                       \n"
        "   stmdb sp!, {r0, r3}                 \n"                             // 将 R0、R3 入栈到 MSP 指向的栈中
        "   mov r0, %0                          \n"                             //  屏蔽受 FreeRTOS 管理的所有中断
        "   msr basepri, r0                     \n"
        "   dsb                                 \n"
        "   isb                                 \n"
        //  跳转到函数 vTaskSeitchContext,主要用于更新 pxCurrentTCB,使其指向最高优先级的就绪态任务
        "   bl vTaskSwitchContext               \n"
        "   mov r0, #0                          \n"                             // 使能所有中断
        "   msr basepri, r0                     \n"
        "   ldmia sp!, {r0, r3}                 \n"                             // 将 R0、R3 重新从 MSP 指向的栈中出栈
        "                                       \n"
        // R3 为 pxCurrentTCB 的地址值,
        // pxCurrentTCB 已经在函数 vTaskSwitchContext 中更新为最高优先级的就绪态任务
        // 因此 R1 为 pxCurrentTCB 的值,即当前最高优先级就绪态任务控制块的首地址
        "   ldr r1, [r3]                        \n" 
        "   ldr r0, [r1]                        \n"                             // R0 为最高优先级就绪态任务的任务栈指针
        "                                       \n"
        // 从最高优先级就绪态任务的任务栈中出栈 R4~R11 和 R14,这里出栈的 R14 为 EXC_RETURN,其保存了任务是否使用浮点单元的信息
        "   ldmia r0!, {r4-r11, r14}            \n"
        "                                       \n"
        //  此时 R14 为 EXC_RETURN,通过判断 EXC_RETURN 的 bit4 是否为 0,判断任务是否使用的浮点单元,若使用了浮点单元,则需要从任务的任务栈中恢复出浮点寄存器的值
        "   tst r14, #0x10                      \n" 
        "   it eq                               \n"
        "   vldmiaeq r0!, {s16-s31}             \n"
        "                                       \n"
        "   msr psp, r0                         \n"                             // 更新 PSP 为任务切换后的任务栈指针
        "   isb                                 \n"
        "                                       \n"
        #ifdef WORKAROUND_PMU_CM001 /* XMC4000 specific errata workaround. */
            #if WORKAROUND_PMU_CM001 == 1
                "           push { r14 }                \n"
                "           pop { pc }                  \n"
            #endif
        #endif
        "                                       \n"
        // 跳转到切换后的任务运行, 执行此指令,CPU 会自动从 PSP 指向的任务栈中,
        // 出栈 R0、R1、R2、R3、R12、LR、PC、xPSR 寄存器,接着 CPU 就跳转到 PC 指向的代码位置运行,也就是任务上次切换时运行到的位置
        "   bx r14                              \n"
        "                                       \n"
        "   .align 4                            \n"
        "pxCurrentTCBConst: .word pxCurrentTCB  \n"
        ::"i" ( configMAX_SYSCALL_INTERRUPT_PRIORITY )
    );
}

  从上面的代码可以看出,FreeRTOS 在进行任务切换的时候,会将 CPU 的运行状态,在当前任务在进行任务切换前,进行保存,保存到任务的任务栈中,然后从切换后运行任务的任务栈中恢复切换后运行任务在上一次被切换时保存的 CPU 信息。

  但是从 PendSV 的中断回调函数代码中,只看到程序保存和恢复的 CPU 信息中的部分寄存器信息(R4 寄存器~R11 寄存器),这是因为硬件会自动出栈和入栈其他 CPU 寄存器的信息。

  在任务运行的时候,CPU 使用 PSP 作为栈空间使用,也就是使用运行任务的任务栈。当 SysTick 中断(SysTick 的中断服务函数会判断是否需要进行任务切换)发生时,在跳转到 SysTick 中断服务函数运行前,硬件会自动将除 R4 ~ R11 寄存器的其他 CPU 寄存器入栈,因此就将任务切换前 CPU 的部分信息保存到对应任务的任务栈中。当退出 PendSV 时,会自动从栈空间中恢复这部分 CPU 信息,以共任务正常运行。

  因此在 PendSV 中断服务函数中,主要要做的事情就是,保存硬件不会自动入栈的 CPU 信息,已经确定写一个要运行的任务,并将 pxCurrentTCB 指向该任务的任务控制块,然后更新 PSP 指针为该任务的任务堆栈指针。

五、FreeRTOS确定下一个要运行的任务

  在 PendSV 的中断服务函数中,调用了函数 vTaskSwitchContext() 来确定写一个要运行的任务。函数 vTaskSwitchContext() 在 task.c 文件中有定义,具体的代码如下所示:

void vTaskSwitchContext( void )
{
    traceENTER_vTaskSwitchContext();

    if( uxSchedulerSuspended != ( UBaseType_t ) 0U )                            // 判断任务调度器是否运行
    {
        // 此全局变量用于在系统运行的任意时刻标记需要进行任务切换, 会在 SysTick 的中断服务函数中统一处理
        //  任务任务调度器没有运行,不允许任务切换, 
        // 因此将 xYieldPending 设置为 pdTRUE,那么系统会在 SysTick 的中断服务函数中持续发起任务切换, 直到任务调度器运行
        xYieldPendings[ 0 ] = pdTRUE;
    }
    else
    {
        xYieldPendings[ 0 ] = pdFALSE;                                          // 可以执行任务切换,因此将 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[ 0 ] );
            #else
                ulTotalRunTime[ 0 ] = portGET_RUN_TIME_COUNTER_VALUE();
            #endif

            if( ulTotalRunTime[ 0 ] > ulTaskSwitchedInTime[ 0 ] )
            {
                pxCurrentTCB->ulRunTimeCounter += ( ulTotalRunTime[ 0 ] - ulTaskSwitchedInTime[ 0 ] );
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }

            ulTaskSwitchedInTime[ 0 ] = ulTotalRunTime[ 0 ];
        }
        #endif /* configGENERATE_RUN_TIME_STATS */

        taskCHECK_FOR_STACK_OVERFLOW();                                         // 检查任务栈是否溢出

        #if ( configUSE_POSIX_ERRNO == 1 )                                      // 此宏为 POSIX 相关配置
        {
            pxCurrentTCB->iTaskErrno = FreeRTOS_errno;
        }
        #endif

        // 将 pxCurrentTCB 指向优先级最高的就绪态任务,两种方法,由 FreeRTOSConfig.h 文件配置决定
        taskSELECT_HIGHEST_PRIORITY_TASK();                                   
        traceTASK_SWITCHED_IN();                                                // 用于调试

        portTASK_SWITCH_HOOK( pxCurrentTCB );

        #if ( configUSE_POSIX_ERRNO == 1 )                                      // 此宏为 POSIX 相关配置
        {
            FreeRTOS_errno = pxCurrentTCB->iTaskErrno;
        }
        #endif

        #if ( configUSE_C_RUNTIME_TLS_SUPPORT == 1 )
        {
            /* Switch C-Runtime's TLS Block to point to the TLS
                * Block specific to this task. */
            configSET_TLS_BLOCK( pxCurrentTCB->xTLSBlock );
        }
        #endif
    }

    traceRETURN_vTaskSwitchContext();
}

  函数 vTaskSwitchContext() 调用了函数 taskSELECT_HIGHEST_PRIORITY_TASK() ,来将 pxCurrentTCB 设置为指向优先级最高的就绪态任务。

  函数 taskSELECT_HIGHEST_PRIORITY_TASK() 用于将 pcCurrentTCB 设置为优先级最高的就绪态任务,因此该函数会使用位图的方式在任务优先级记录中查找优先级最高任务优先等级,然后根据这个优先等级,到对应的就绪态任务列表在中取任务。

  FreeRTOS 提供了两种从任务优先级记录中查找优先级最高任务优先等级的方式,一种是由纯 C 代码实现的,这种方式适用于所有运行 FreeRTOS 的 MCU;另外一种方式则是使用了硬件计算前导零的指令,因此这种方式并不适用于所有运行 FreeRTOS 的 MCU,而仅适用于具有有相应硬件指令的 MCU。

  软件方式实现的函数 taskSELECT_HIGHEST_PRIORITY_TASK() 是一个宏定义,在 task.c文件中由定义,具体的代码如下所示:

#define taskSELECT_HIGHEST_PRIORITY_TASK()                                                  \
do {                                                                                        \
    /* 全局变量 uxTopReadyPriority 以位图方式记录了系统中存在任务的优先级                       \ 
       将遍历的起始优先级设置为这个全局变量,而无需从系统支持优先级的最大值开始遍历 */             \ 
    UBaseType_t uxTopPriority = uxTopReadyPriority;                                         \
                                                                                            \
    /* 按照优先级从高到低,判断对应的就绪态任务列表中是否由任务,                                \
       找到存在任务的最高优先级就绪态任务列表后,退出遍历,*/                                    \
    while( listLIST_IS_EMPTY( &( pxReadyTasksLists[ uxTopPriority ] ) ) != pdFALSE )        \
    {                                                                                       \
        configASSERT( uxTopPriority );                                                      \
        --uxTopPriority;                                                                    \
    }                                                                                       \
                                                                                            \
    /* 从找到了就绪态任务列表中取下一个任务, 让 pxCurrentTCB 指向这个任务的任务控制块 */         \
    listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) );   \
    /*  更新任务优先级记录 */                                                                 \
    uxTopReadyPriority = uxTopPriority;                                                     \
} while( 0 ) /* taskSELECT_HIGHEST_PRIORITY_TASK */

  依靠特定硬件指令实现的函数 taskSELECT_HIGHEST_PRIORITY_TASK() 是一个宏定义,在 task.c 文件中有定义,具体的代码如下所示:

#define taskSELECT_HIGHEST_PRIORITY_TASK()                                                  \
do {                                                                                        \
    /* 全局变量 uxTopReadyPriority 以位图方式记录了系统中存在任务的优先级                       \  
       将遍历的起始优先级设置为这个全局变量,而无需从系统支持优先级的最大值开始遍历 */             \                                                                              \
    UBaseType_t uxTopPriority;                                                              \
                                                                                            \
    /* 使用硬件方式从任务优先级记录中获取最高的任务优先等级,                                    \
      找到存在任务的最高优先级就绪态任务列表后,退出遍历, */                                    \
    portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority );                          \
    configASSERT( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ uxTopPriority ] ) ) > 0 ); \
    /* 从找到了就绪态任务列表中取下一个任务, 让 pxCurrentTCB 指向这个任务的任务控制块 */         \
    listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) );   \
} while( 0 )

  在使用硬件方式实现的函数 taskSELECT_HIGHEST_PRIORITY_TASK() 中调用了函数 portGET_HIGHEST_PRIORITY() 来计算任务优先级记录中的最高任务优先级,函数 portGET_HIGHEST_PRIORITY() 实际上是一个宏定义,在 portmacro.h 文件中有定义,具体的代码如下所示:

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

  可以看到,宏 portGET_HIGHEST_PRIORITY() 使用了 __clz 这个硬件指定来计算 uxReadyPriorities 的前导零,然后使用 31(变量 uxReadyPriorities 的最大比特位)减去得到的前导零,那么就得到了变量 uxReadyPriorities 中,最高位 1 的比特位。使用此方法就限制了系统最大的优先级数量不能超多 32,即最高优先等级位 31,不过对于绝大多数的应用场合,32 个任务优先级等级已经足够使用了。

六、任务调度

  调度器就是使用相关的调度算法来决定当前需要执行哪个任务,FreeRTOS 支持以下任务调度方式:抢占式调度时间片调度

  • 抢占式调度:主要针对优先级不同的任务,每个任务都有一个优先级,优先级高的任务可以抢占优先级低的任务。
  • 时间片调度:主要针对优先级相同的任务,当多个任务的优先级相同且就绪时,任务调度器会根据用户所设置的时间片轮流的运行这些任务。

6.1、抢占式调度

  在 FreeRTOS 中,高优先级任务,优先执行,并且高优先级任务不停止,低优先级任务无法执行。被抢占的任务将会进入就绪态。

抢占式调度

  首先,这里创建三个任务:Task1、Task2、Task3。Task1、Task2、Task3 的优先级分别为 1、2、3;在 FreeRTOS 中任务设置的数值越大,优先级越高,所以 Task3 的优先级最高。

  该程序在运行时,首先 Task1 在运行中,在这个过程中 Task2 就绪了,在抢占式调度器的作用下 Task2 会抢占 Task1 的运行。然后,在 Task2 运行过程中,Task3 就绪了,在抢占式调度器的作用下 Task3 会抢占 Task2 的运行。接着,Task3 运行过程中,Task3 阻塞了(系统延时或等待信号量等),此时就绪态中,优先级最高的任务 Task2 执行。随后,Task3 阻塞解除了(延时到了或者接收到信号量),此时 Task3 恢复到就绪态中,抢占 TasK2 的运行。

6.2、时间片调度

  同等优先级任务轮流地享有相同的 CPU 时间(可设置), 叫时间片,在 FreeRTOS 中,一个时间片就等于 SysTick 中断周期。没有用完的时间片不会再使用,下次任务得到执行还是按照一个时间片的时钟节拍运行。

时间片调度

  首先,这里创建三个任务:Task1、Task2、Task3。Task1、Task2、Task3 的优先级均为 1,即 3 个任务同等优先级。

  该程序在运行时,首先 Task1 运行完 1 个时间片后,切换至 Task2 运行。然后,Task2 运行完 1 个时间片后,切换至 Task3 运行。如果在 Task3 运行过程中(还不到 1 个时间片),Task3 阻塞了(系统延时或等待信号量等),此时直接切换到下一个任务 Task1。接着,Task1 运行完 1 个时间片后,切换至 Task2 运行,依次循环运行下去。

同等优先级任务,轮流执行,时间片流转。

一个时间片大小,取决为滴答定时器中断频率。

注意没有用完的时间片不会再使用,下次任务 Task3 得到执行还是按照 1 个时间片的时钟节拍运行。

使用时间片调度需把宏 configUSE_TIME_SLICINGconfigUSE_PREEMPTION 置 1。

posted @   星光映梦  阅读(236)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
历史上的今天:
2023-03-07 12. 字符串
点击右上角即可分享
微信分享提示