FreeRTOS调度器与任务

一.调度器

1.调度器开启

        任务调度器,就是函数vTaskStartScheduler(),函数的源代码比较长,大致了解原理就可以,工作流程大致如下:

  1. 创建空闲任务,空闲任务的优先级为最低。如果使用软件定时器则再创建定时器服务任务。
  2. 关闭中断。
  3. 变量 xSchedulerRunning 设置为 pdTRUE,调度器开始运行。
  4. 调用函数 xPortStartScheduler()来初始化跟调度器启动有关的硬件,比如滴答定时器FPU 单元和 PendSV 中断等等。

注:这个PendSV是什么呢,先卖个关子,我们后面会讲?

2.调度器硬件初始化

        硬件初始化由函数xPortStartScheduler()来完成,我们都知道FreeRTOS的系统时钟由滴答定时器,任务切换则会借助PendSV中断,所以FreeRTOS内核在运行时这部分硬件必须先完成初始化,所以xPortStartScheduler()函数中做的实际上就是这些,我们可以看一下函数源码:

BaseType_t xPortStartScheduler( void )
{


    /* Make PendSV and SysTick the lowest priority interrupts. */
    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. */
    vPortSetupTimerInterrupt();

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

    /* Ensure the VFP is enabled - it should be anyway. */
    prvEnableVFP();

    /* Lazy save always. */
    *( portFPCCR ) |= portASPEN_AND_LSPEN_BITS;

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

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

这个源码前面很长一截都是条件编译相关的,这里为了看着方便就删掉了,可以去port.c中该函数源码处查看

这个函数的执行内容大致如下:

  1. 设置PendSV和滴答定时器的中断优先级,都为最低等级
  2. 调用函数vPortSetupTimerInterrupt()设置滴答定时器周期并使能
  3. 初始化临界区嵌套计数器
  4. 点用函数prvEnableVFP()使能FPU
  5. FPCCR寄存器设置,这部分我们就不深究了,这里设置的意义是保证寄存器可以保存和恢复异常入口和退出时的状态
  6. 调用函数prvStartFirstTask()启动第一个任务,切换到第一个任务的上下文。

3.空闲任务

在前面讲了调度器开启会创建一个空闲任务,那这个空闲任务的作用是什么呢?其实空闲任务就是系统空闲时运行的任务,当其他任务处于某种状态不能运行是,系统需要一个任务保证系统正常运行能去做上下文切换,就会运行空闲任务。

空闲任务由系统自动创建,不需要用户手动创建,任务调度器启动后必须有一个任务在运行所以创建了空闲任务,但空闲任务还有一些其他功能如下:

以下是一些空闲任务的主要功能和特点:

  1. CPU 资源管理: 当没有其他任务需要运行时,空闲任务会执行,从而确保 CPU 资源不会被浪费。
  2. 电源管理: 空闲任务可以用来实现低功耗模式。例如,在空闲任务中可以调用低功耗操作,将处理器置于低功耗状态,减少功耗。
  3. 内存管理: 空闲任务有时还用于动态内存管理,例如,释放被删除任务所占用的堆内存。
  4. 系统监控: 可以在空闲任务中执行一些监控操作,比如监控系统的运行状态、检查系统健康等。

        用户可以通过定义 vApplicationIdleHook() 函数来扩展空闲任务的功能。这个钩子函数会在空闲任务中被调用,用户可以在其中添加自己的代码,例如处理后台任务、调试输出等,但是需要注意的是,由于空闲任务的优先级最低,因此在空闲任务中执行的操作应尽可能简单且快速,以避免阻塞其他高优先级任务的执行。

比如,我们可以将状态灯闪烁放在这个函数中去做:

voidvApplicationIdleHook(void) 
{    
    // 用户自定义的空闲任务代码
    // 可以放置低功耗操作、后台处理等
    // 例如:LED 指示灯闪烁    
    toggleLED();
}

2.任务切换

        我们整个FreeRTOS的核心就是进行任务管理而任务管理的核心就是任务切换。任务切换不仅决定了任务执行的顺序,同时任务切换的效率也决定了系统的整体性能。

1.PendSV

        PendSV全称为 “Pended System Service Call”,中文翻译过来就是可挂起的系统调用。它是 Cortex-M 微控制器中一种用于实现任务调度和上下文切换的关键机制,是 Cortex-M 微控制器(由 ARM 架构设计)的一个异常向量,是一种低优先级的系统异常。保证两个事情:

        它对于操作系统而言非常重要,它的优先级可以通过编程设置,并且它的执行会在更高优先级异常处理完成后才会执行。在我们的RTOS中,当需要切换任务时,会触发 PendSV 异常,并将PendSV设置为挂起状态,在异常内完成上下文切换。其涉及思想如下:

这种设计使得上下文切换过程不会干扰其他更高优先级的中断服务例程,从而保证系统的实时性和稳定性。

PendSV 的主要特点包括:

1.低优先级

        PendSV 异常的优先级比其他硬件中断和异常低,这意味着它通常是在所有更高优先级的中断都处理完后才会被处理。

2.触发机制

        操作系统在需要上下文切换时,可以通过设置特定的寄存器来触发 PendSV 异常。

3.上下文切换

PendSV 异常处理程序通常负责保存当前任务的上下文(例如寄存器值),并恢复即将运行的任务的上下文。那么我们的PendSV的中断服务程序是怎么找到下一个要运行的任务的呢?实际上它内部会调用vTaskSwitchContext()来获取下一个要运行的任务。该函数接口内部调用taskSELECT_HIGHEST_PRIORITY_TASK();

至于PendSV的中断服务函数xPortPendSVHandler()的详细实现我们就不去深究了,里面都是汇编代码,我们只需要知道它的工作流程如下:

  1. 保存当前任务状态:将当前任务的寄存器状态保存到 TCB 中,包括程序计数器(PC)、堆栈指针(SP)等。
  2. 选择下一个任务:调用调度器选择下一个要运行的任务。
  3. 恢复下一个任务状态:从新任务的 TCB 中恢复寄存器状态,使其从之前中断的位置继续运行。

2.任务切换场合

在FreeRTOS中,上下文切换共有两种场合会被触发,分别为可以执行系统调用和SysTick中断:

1.可以执行一个系统调用

        执行系统调用其实就是执行FreeRTOS提供的相关任务API,如执行任务切换的函数taskYIELD() (实际上是个宏,本质是portYIELD() ),此外,FreeRTOS有些API内部也会调用taskYIELD()函数,这些API和t        askYIELD()函数都被称为系统调用。

2.SysTick中断

        FreeRTOS中滴答定时器的中断服务函数也会进行任务切换,其函数原型如下

void SysTick_Handler(void)
{    
    if(xTaskGetSchedulerState()!=taskSCHEDULER_NOT_STARTED)//ϵͳÒѾ­ÔËÐÐ
    {
        xPortSysTickHandler();    
    }
}

可以看到,滴答定时器里面调用了FreeRTOS的函数接口xPortSysTickHandler(),我们可以去看下这个函数做了什么,其函数原型如下:

void xPortSysTickHandler( void )
{
  
    vPortRaiseBASEPRI();
    {

        if( xTaskIncrementTick() != pdFALSE )
        {

            portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
        }
    }

    vPortClearBASEPRIFromISR();
}

可以看出来,里面除了开关中断以外,就是启动了PendSV中断,这样就可以进行任务切换了

3.时间片调度

在前面我们其实已经讲了调度的问题,FreeRTOS是支持多个任务用同一个优先级的。在 FreeRTOS 中允许一个任务运行一个时间片(一个时钟节拍的长度)后让出 CPU 的使用权,让拥有同优先级的下一个任务运行,FreeRTOs 中的这种调度方法就是时间片调度。

同一优先级三个就绪的任务运行状态如下图所示:

如果要使用时间片调度,configUSE_PREEMPTIONconfigUSE_TIME_SLICING必须置为1,时间片长度由宏configTICK_RATE_HZ来确定,一个时间片的长度就是滴答定时器的中断周期。在上面滴答定时器里,可以看到它调用了xTaskIncrementTick()函数并且判断了它的返回值,那这个函数里做了什么呢,我们可以去看下函数源码如下,这个代码巨长,为了方便同学们查看我在这里做了删减,代码如下:

BaseType_t xTaskIncrementTick( void )
{

        #if ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) )
        {
            if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ pxCurrentTCB->uxPriority ] ) ) > ( UBaseType_t ) 1 )
            {
                xSwitchRequired = pdTRUE;
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }
        }
        #endif 
}

可以看到当这个两个宏为1才会编译下面的部分,并且在这里判断对应优先级下还有没有其他任务,如果有才会返回pdTRUE。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值