FreeRTOS学习记录 05--任务调度器开启和切换

0 前言

@ Author         :Dargon
@ Record Date    :2021/07/13
@ Reference Book : `FreeRTOS源码详解与应用开发`,`ARM Cortex-M3与Cortex-M4权威指南`,`B站正点原子FreeRTOS讲解视频`
@ Purpose        :学习正点原子的miniFly,该飞控基于FreeRTOS系统开发的,所以学习一下记录下关于RTOS系统的一些基本操作,大概了解系统的工作原理,如何创建,运行,切换任务等等基本操作流程。在此进行学习的记录。

1 任务调度器的开启

  • 这里所提到的应该是FreeRTOS最核心的东西了,首先在创建了一个start的任务的时候,最后就会调用vTaskStartScheduler()来开启任务调度器。

1.1 如何启动第一个任务的

  • vTaskStartScheduler() 任务源码分析

    void vTaskStartScheduler( void )
    {
          
          
    BaseType_t xReturn;
    
        /* Add the idle task at the lowest priority. */
        #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 ) // --xReturn 针对返回值 创建通过
        {
          
          
            /* 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; // --初始化下一个阻塞解放时刻 为32位最大值
            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(); // --就是一个macro 来定义是否使用时间计数器的问题 不用管它
    
            /* Setting up the timer tick is hardware specific and thus in the
            portable interface. */
            // --进入这个内核硬件初始化函数 就不会在回来了 // --初始化systick 和PendSV中断
            if( xPortStartScheduler() != pdFALSE ) 
            {
          
           
                /* Should not reach here as if the scheduler is running the
                function will not return. */
            }
            else
            {
          
          
                /* Should only reach here if a task calls xTaskEndScheduler(). */
            }
        }
        else // --创建失败 一个断言 就是打印一些错误信息
        {
          
          
            /* This line will only be reached if the kernel could not be started,
            because there was not enough FreeRTOS heap to create the idle task
            or the timer task. */
            configASSERT( xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY );
        }
    
        /* Prevent compiler warnings if INCLUDE_xTaskGetIdleTaskHandle is set to 0,
        meaning xIdleTaskHandle is not used anywhere else. */
        ( void ) xIdleTaskHandle;
    }
    
    1. 调用xTaskCreate() 创建一个空闲任务
    2. 若是需要 软件定时器的话,进行初始化
    3. portDISABLE_INTERRUPTS() 关中断,在SVC中断服务函数里面,会在重新打开中断(关中断的方法,就是直接操作basepri寄存器,具体操作见 FreeRTOS学习记录 01--中断管理 2.1小节
    4. 接下来一些相应的全局变量的初始化
          // --相应的参数初始化 一些静态全局变量
          xNextTaskUnblockTime = portMAX_DELAY; // --初始化下一个阻塞解放时刻 为32位最大值
          xSchedulerRunning = pdTRUE;
          xTickCount = ( TickType_t ) 0U;
      
    5. 进行一个if条件的判断xPortStartScheduler() 这里就是开启了任务的调度,正常的话,系统会开始运行第一个任务,就不会在回来了
    • xPortStartScheduler() 内核相关的硬件初始化函数

      • 处理系统时钟Systick 和PendSV
      // --被任务开始调度器 调用,内核相关的硬件初始化函数 
      BaseType_t xPortStartScheduler( void )
      {
              
              
          /* Make PendSV and SysTick the lowest priority interrupts. */
          // --配置PendSV 和 SysTick滴答定时器
          // --portNVIC_SYSPRI2_REG =对应配置内存地址 0xE000ED20 - 0xE000ED23
          // --进行配置的内存 最后2 Byte 对应着0xE000ED22 和0xE000ED23 2Byte
          // --portNVIC_PENDSV_PRI 值的设置 : 需要左移 16位 前面2字节 不是我们目标寄存器
          // --portNVIC_SYSTICK_PRI 值的设置 : 需要左移 24位 前面3字节 不是我们目标寄存器
          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(); // --设置 Systick 定时器中断 进行Systick 定时器的寄存器的一些操作
      
          /* Initialise the critical nesting count ready for the first task. */
          uxCriticalNesting = 0; // 临界中断嵌套 
      
          /* Ensure the VFP is enabled - it should be anyway. */
          prvEnableVFP(); // --使能FPU 操作 F4和F7 都有 F1是没有的 FPU 针对浮点进行的计算
      
          /* Lazy save always. */
          *( portFPCCR ) |= portASPEN_AND_LSPEN_BITS; // --惰性压栈 看的晕晕乎乎的 和FPU相关的操作 我是膨胀了吗
      
          /* Start the first task. */
          prvStartFirstTask(); // --开启第一个任务
      
          /* Should not get here! */
          return 0;  
      }
      
      1. 前面首先是一些断言assert,防止程序出错,接下来一堆的条件编译,所以就直接看核心内容
      2. 设定PendSV 中断和 Systick中断的优先级为最低,具体为什么这样设置,操作见 FreeRTOS学习记录 01--中断管理 1.3小节
      3. 调用函数vPortSetupTimerInterrupt() 配置Systick的中断寄存器,下面关于配置滴答定时器的定时周期,和CTRL寄存器的设置
          /* Configure SysTick to interrupt at the requested rate. */ 
          // --180MHz/1000 出来的数字就相当于1ms的计数量
          portNVIC_SYSTICK_LOAD_REG = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL;
          // --对应的将Systick 定时器的 CTRL寄存器 前三位 置1
          portNVIC_SYSTICK_CTRL_REG = ( portNVIC_SYSTICK_CLK_BIT | portNVIC_SYSTICK_INT_BIT | portNVIC_SYSTICK_ENABLE_BIT );
      
      1. uxCriticalNesting = 0; 临界嵌套数初始化为0,后面在任务的临界嵌套中使用

      2. 调用函数prvEnableVFP();使能FPU,关于浮点的计算

        1. 根据**FreeRTOS学习记录 01--中断管理 1.1小节**的结构体SCB_Type 找到该寄存器的绝对内存地址0X E000 ED88
        2. 若使能FPU,需要将CPACR寄存器的bit20~bit23置1,
        3. 下面是CPACR寄存器的具体配置

        在这里插入图片描述

      3. 操作portFPCCR寄存器,是关于惰性压栈的,晕乎乎的,没整明白

      4. 调用函数prvStartFirstTask()真正的启动第一个任务

      • __asm void prvStartFirstTask( void ) 汇编开始 启动第一个任务

        __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
        }
        
        1. ldr r0, =0xE000ED08 将0xE000ED08 保存在R0寄存器内,

        2. 后面两步 ldr r0, [r0],将地址为0xE000ED08 的内存里面的值取出来 保存在R0内,在以这个值为地址的内存里面的内容取出来,存放到R0里面,这个值就是MSP的值,后面进行初始化

        3. 为什么是开始地址为0xE000ED08,下面有正点原子书上提供: 一般来说,向量表应该是从起始地址(0X 0000 0000)开始的存储的,但是有一个重定义的向量表,Cortex-M就提供了一个名为向量表偏移寄存器(VTOR),且该寄存器的地址就是0xE000ED08,该寄存器里面的内容值就是向量表开始的地址0x 0800 0000,

          
          #define FLASH_BASE            ((uint32_t)0x08000000) /*!< FLASH(up to 1 MB) base address in the alias region 
          #define VECT_TAB_OFFSET  0x00 /*!< Vector Table base offset field. 
          
          #else // --重新设置 中断向量表
          SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal FLASH */
          

          刚好对应的也就是,keil程序下载的时候所对应的地址

          在这里插入图片描述

        4. 向量表的起始地址0x 0800 0000里面存储的就是MSP的初始值,所以前面的1、2、3步就是把为了获取MSP的初始值,存放到R0里面

        5. msr msp, r0将MSP 复位

        6. svc 0 开始触发SVC 中断,进入SVC中断服务函数

        • __asm void vPortSVCHandler( void ) SVC中断服务函数
          __asm void vPortSVCHandler( void )
          {
                      
                      
              PRESERVE8
          
              /* Get the location of the current TCB. */
              ldr	r3, =pxCurrentTCB
              ldr r1, [r3]
              ldr r0, [r1]
              /* Pop the core registers. */
              ldmia r0!, {
                      
                      r4-r11, r14} // --运用指令 恢复要运行的任务的堆栈
              msr psp, r0 // --psp 进程栈指针设置为 任务的堆栈
              isb
              mov r0, #0
              msr	basepri, r0
              bx r14 // --寄存器的配置 SVC中断出来之后 就直接从PSP指针地址 开始执行
              // --至此 第一个任务启动完成 之后的任务切换就是 PendSV处理了(这个中断只用这一次!!!)
          }
          
          1. ldr r3, =pxCurrentTCBpxCurrentTCB 指向正在运行的任务,保存到R3里,pxCurrentTCB的初始化,在一开始的初始化空闲任务的时候就已经处理了,具体操作请见 FreeRTOS学习记录 02--任务篇 2.1小节
          2. ldr r1, [r3]获取当前任务块的地址
          3. ldr r0, [r1]读取任务块所在的地址,即是栈顶指针(pxTopOfStack)所指向的地址
          4. ldmia r0!, {r4-r11, r14} 接下来开始恢复该任务的任务现场,从pxTopOfStack开始出栈到 r4-r11, r149个寄存器内,同时pxTopOfStack变化从0x 2000 10040x 2000 1028,对应着0x24,且对应的9个寄存器为 4X9 =36 bit =0x24,在关于任务函数的初始化中,任务堆栈的初始化中,提到对这些区域的赋值,一个个的按照顺序压入栈内的,出来时候也要按照顺序出来,具体操作请见 FreeRTOS学习记录 02--任务篇 2.1小节
          5. msr basepri, r0 设置BASEPRI寄存器为0,开始打开中断。
          6. 至此 第一个任务就正式的开始运行了

2 任务的切换

2.1 PendSV 异常

  • 任务的切换场合是在 通过某个事件引起PendSV异常,然后进入PendSV相应的中断服务函数里面
  • 都是将ICSR 寄存器的bit 28 位置1.

2.2 两个事件引起PendSV 异常

  • 执行系统调用

    1. 对于带有 YIELD函数,将会引起任务的切换
    2. 最终调用的是portYIELD()函数
    // --对应的macro define
    #define portNVIC_INT_CTRL_REG		( * ( ( volatile uint32_t * ) 0xe000ed04 ) )
    #define portNVIC_PENDSVSET_BIT		( 1UL << 28UL )
    
    /* Scheduler utilities. */
    #define portYIELD()																\
    {
            
            																				\
        /* Set a PendSV to request a context switch. */								\
        portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;								\
                                                                                    \
        /* Barriers are normally not required but do ensure the code is completely	\
        within the specified behaviour for the architecture. */						\
        __dsb( portSY_FULL_READ_WRITE );											\
        __isb( portSY_FULL_READ_WRITE );											\
    }
    
    1. portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;所对应的就是将 ICSR 寄存器的bit 28 位置1.
  • 系统滴答定时器中断

    /systick中断服务函数,使用OS时用到
    void SysTick_Handler(void)
    {
          
          	
        if(xTaskGetSchedulerState()!=taskSCHEDULER_NOT_STARTED)//系统已经运行
        {
          
          
            xPortSysTickHandler();	// --系统运行 滴答定时器服务函数 滴答滴答的进行+1操作~~
        }
    }
    
    1. 调用xPortSysTickHandler();
    • xPortSysTickHandler()
      // --滴答定时器中断调用函数
      void xPortSysTickHandler( void )
      {
              
              
          /* The SysTick runs at the lowest interrupt priority, so when this interrupt
          executes all interrupts must be unmasked.  There is therefore no need to
          save and then restore the interrupt mask value as its value is already
          known - therefore the slightly faster vPortRaiseBASEPRI() function is used
          in place of portSET_INTERRUPT_MASK_FROM_ISR(). */
          vPortRaiseBASEPRI(); // --disenable 中断
          {
              
              
              /* Increment the RTOS tick. */
              if( xTaskIncrementTick() != pdFALSE )
              {
              
              
                  /* A context switch is required.  Context switching is performed in
                  the PendSV interrupt.  Pend the PendSV interrupt. */
                  portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
              }
          }
          vPortClearBASEPRIFromISR(); // --恢复中断
      }
      
      1. 可以看出portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;所对应的就是将 ICSR 寄存器的bit 28 位置1.出发PendSV的中断。

2.3 PendSV 的中断服务函数 在这里面进行任务切换

  • 任务切换的核心要点

    __asm void xPortPendSVHandler( void )
    {
          
          
        extern uxCriticalNesting;
        extern pxCurrentTCB;
        extern vTaskSwitchContext;
    
        PRESERVE8
    
        mrs r0, psp
        isb
        /* Get the location of the current TCB. */
        ldr	r3, =pxCurrentTCB
        ldr	r2, [r3]
    
        /* Is the task using the FPU context?  If so, push high vfp registers. */
        tst r14, #0x10
        it eq
        vstmdbeq r0!, {
          
          s16-s31}
    
        /* Save the core registers. */
        stmdb r0!, {
          
          r4-r11, r14}
    
        /* Save the new top of stack into the first member of the TCB. */
        str r0, [r2]
    
        stmdb sp!, {
          
          r3}
        mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
        msr basepri, r0
        dsb
        isb
        bl vTaskSwitchContext
        mov r0, #0
        msr basepri, r0
        ldmia sp!, {
          
          r3}
    
        /* The first item in pxCurrentTCB is the task top of stack. */
        ldr r1, [r3]
        ldr r0, [r1]
    
        /* Pop the core registers. */
        ldmia r0!, {
          
          r4-r11, r14}
    
        /* Is the task using the FPU context?  If so, pop the high vfp registers
        too. */
        tst r14, #0x10
        it eq
        vldmiaeq r0!, {
          
          s16-s31}
    
        msr psp, r0
        isb
        #ifdef WORKAROUND_PMU_CM001 /* XMC4000 specific errata */
            #if WORKAROUND_PMU_CM001 == 1
                push {
          
           r14 }
                pop {
          
           pc }
                nop
            #endif
        #endif
    
        bx r14
    }
    
    1. mrs r0, psp获取进程栈指针,存放到r0寄存器里面,是指向当前运行任务的堆栈区域
    2. ldr r3, =pxCurrentTCB ;ldr r2, [r3] 获取当前运行的任务块的地址
    3. 保存FPU 相关的
    4. stmdb r0!, {r4-r11, r14} 让这几个需要手动入栈的寄存器,进行入栈
    5. str r0, [r2] 把新的栈顶指针,给任务块的栈顶指针pxTopOfStack
    6. stmdb sp!, {r3} R3 指针指向当前任务 ,就是pxCurrentTCB,我们切换后,需要将pxCurrentTCB 指向我们要切换的任务
    7. configMAX_SYSCALL_INTERRUPT_PRIORITY 关闭中断
    8. 上面相当于已经把 当前运行的任务现场已经保存好了,压入到了该任务的堆栈中了,下面最最核心的是,找下一个要切换的任务。整个系统都是在围绕着这个运行的
    9. 调用函数vTaskSwitchContext()
    10. 后续
    • vTaskSwitchContext()
    void vTaskSwitchContext( void )
    {
          
          
        if( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE )
        {
          
          
            /* The scheduler is currently suspended - do not allow a context
            switch. */
            xYieldPending = pdTRUE;
        }
        else
        {
          
          
            xYieldPending = pdFALSE;
            traceTASK_SWITCHED_OUT();
    
            /* Check for stack overflow, if configured. */
            taskCHECK_FOR_STACK_OVERFLOW();
    
            /* Select a new task to run using either the generic C or port
            optimised asm code. */
            taskSELECT_HIGHEST_PRIORITY_TASK();
            traceTASK_SWITCHED_IN();
    
            #if ( configUSE_NEWLIB_REENTRANT == 1 )
            {
          
          
                /* Switch Newlib's _impure_ptr variable to point to the _reent
                structure specific to this task. */
                _impure_ptr = &( pxCurrentTCB->xNewLib_reent );
            }
            #endif /* configUSE_NEWLIB_REENTRANT */
        }
    }
    
    • taskSELECT_HIGHEST_PRIORITY_TASK();
      #define taskSELECT_HIGHEST_PRIORITY_TASK()														\
          {
                
                																								\
          UBaseType_t uxTopPriority;																		\
                                                                                                          \
              /* Find the highest priority list that contains ready tasks. */								\
              portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority );								\
              configASSERT( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ uxTopPriority ] ) ) > 0 );		\
              listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) );		\
          } /* taskSELECT_HIGHEST_PRIORITY_TASK() */
      
      1. 通过最高优先级来获得下一个要运行的任务
      2. 在每一个就绪列表中的任务,他们的优先级,会在32bit uxReadyPriorities寄存器这里进行挂个号,在硬件上运用 计算前导0 的个数,可以更快的获得相应的优先级值
      #define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities ) uxTopPriority = ( 31UL - ( uint32_t ) __clz( ( uxReadyPriorities ) ) )
      
      1. 通过2 步的优先级值,来获得处于就绪态列表中的相应的任务,并且更新该最高优先级任务到pxCurrentTCB
      2. pxCurrentTCB指向我们目前要运行的任务,下面就将该任务的任务现场恢复到相应的R寄存器里面。
    1. ldmia sp!, {r3} 由于pxCurrentTCB 已更新所以,此时R3的值已经发生改变了
    2. ldr r1, [r3]; ldr r0, [r1] 取出当前任务的 栈顶指针
    3. ldmia r0!, {r4-r11, r14} 从任务堆栈中出栈 到r4-r11, r14寄存器
    4. 关于FPU的操作
    5. msr psp, r0 更新PSP 进程栈指针,这是一个公用的,每次那个任务用过之后,更新着PSP
    6. bx r14 执行之后,硬件自动恢复R0~R3、 R12、LR、PC和xPSR寄存器。由于R14 =0xffff fffd,所以这里结束之后会进入进程模式,并且使用进程栈指针(PSP),PC值恢复为即将运行的任务函数,新的任务开始运行,任务切换成功。
    7. 关于R14 =0xffff fffd
      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eYeKYriV-1626227883736)(Photo\R14.png)]

生改变了
11. ldr r1, [r3]; ldr r0, [r1] 取出当前任务的 栈顶指针
12. ldmia r0!, {r4-r11, r14} 从任务堆栈中出栈 到r4-r11, r14寄存器
13. 关于FPU的操作
14. msr psp, r0 更新PSP 进程栈指针,这是一个公用的,每次那个任务用过之后,更新着PSP
15. bx r14 执行之后,硬件自动恢复R0~R3、 R12、LR、PC和xPSR寄存器。由于R14 =0xffff fffd,所以这里结束之后会进入进程模式,并且使用进程栈指针(PSP),PC值恢复为即将运行的任务函数,新的任务开始运行,任务切换成功。
16. 关于R14 =0xffff fffd
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/Dallas01/article/details/118720839