FreeRTOS learning record 05 -- task scheduler startup and switching

Posted by MrTL on Mon, 17 Jan 2022 04:16:28 +0100

0 Preface

@ Author         : Dargon
@ Record Date    : 2021/07/13
@ Reference Book : `FreeRTOS Detailed source code and application development`,`ARM Cortex-M3 And Cortex-M4 Authoritative guide`,`B Standing point atom FreeRTOS Explanation video`
@ Purpose        : Learning about punctual atoms miniFly,The flight control is based on FreeRTOS System development, so learn to record about RTOS Some basic operations of the system, about the working principle of the system, how to create, run, switch tasks and other basic operation processes. Record of learning here.

1. Enable task scheduler

  • What is mentioned here should be the core of FreeRTOS. First, when a start task is created, vTaskStartScheduler() will be called to start the task scheduler.

1.1 how to start the first task

  • vTaskStartScheduler() task source code analysis

    void vTaskStartScheduler( void )
    {
    BaseType_t xReturn;
    
        /* Add the idle task at the lowest priority. */
        #else / / -- create tasks using dynamic methods
        {
            /* 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 (configure_timers = = 1) / / -- judge whether to create a software timer
        {
            if( xReturn == pdPASS )
            {
                xReturn = xTimerCreateTimerTask();
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }
        }
        #endif /* configUSE_TIMERS */
    
        if( xReturn == pdPASS ) // --xReturn is created for the return value
        {
            /* 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(); // --Close interrupt first
    
            #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 */
    
            // --Initialize some static global variables with corresponding parameters
            xNextTaskUnblockTime = portMAX_DELAY; // --Initialize the next block release time to a 32-bit maximum
            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(); // --It's a macro to define whether to use the time counter. Don't worry about it
    
            /* Setting up the timer tick is hardware specific and thus in the
            portable interface. */
            // --After entering the kernel, the hardware initialization function will not come back / /-- initialize systick and PendSV interrupt
            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 // --If the creation fails, an assertion is to print some error messages
        {
            /* 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. Call xTaskCreate() to create an idle task
    2. If a software timer is required, initialize it
    3. portDISABLE_INTERRUPTS() turns off the interrupt. In the SVC interrupt service function, the interrupt will be turned on again (the method of turning off the interrupt is to directly operate the basepri register. See section 01 - interrupt management 2.1 of FreeRTOS learning record for specific operations)
    4. Next, some corresponding global variables are initialized
          // --Initialize some static global variables with corresponding parameters
          xNextTaskUnblockTime = portMAX_DELAY; // --Initialize the next block release time to a 32-bit maximum
          xSchedulerRunning = pdTRUE;
          xTickCount = ( TickType_t ) 0U;
      
    5. Make an if condition judgment. xPortStartScheduler() here is the task scheduling enabled. Normally, the system will start running the first task and will not come back
    • xPortStartScheduler() kernel related hardware initialization function

      • Processing system clock Systick and PendSV
      // --Called by the task start scheduler, the kernel related hardware initialization function 
      BaseType_t xPortStartScheduler( void )
      {
          /* Make PendSV and SysTick the lowest priority interrupts. */
          // --Configure PendSV and SysTick tick timer
          // --portNVIC_SYSPRI2_REG = corresponding configuration memory address 0xE000ED20 - 0xE000ED23
          // --The last 2 bytes of memory configured correspond to 0xE000ED22 and 0xe000ed23 2bytes
          // --portNVIC_PENDSV_PRI value setting: it needs to shift 16 bits left. The first 2 bytes are not our target register
          // --portNVIC_SYSTICK_PRI value setting: it needs to be shifted by 24 bits to the left. The first 3 bytes are not our target register
          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(); // --Set the Systick timer to interrupt some operations of the register of the Systick timer
      
          /* Initialise the critical nesting count ready for the first task. */
          uxCriticalNesting = 0; // Critical interrupt nesting 
      
          /* Ensure the VFP is enabled - it should be anyway. */
          prvEnableVFP(); // --Enable FPU operations F4 and F7. F1 is not available. FPU performs floating-point calculations
      
          /* Lazy save always. */
          *( portFPCCR ) |= portASPEN_AND_LSPEN_BITS; // --The inert stack looks dizzy. Have I expanded the FPU related operations
      
          /* Start the first task. */
          prvStartFirstTask(); // --Start the first task
      
          /* Should not get here! */
          return 0;  
      }
      
      1. First, some assertions assert to prevent program errors. Next, a pile of conditional compilation, so let's look directly at the core content
      2. Set the priority of PendSV interrupt and Systick interrupt to the lowest. See Section 1.3 of FreeRTOS learning record 01 - interrupt management for specific reasons
      3. Call the function vportsetuptimerinthrupt() to configure the interrupt register of Systick. The following is about configuring the timing cycle of tick timer and the setting of CTRL register
          /* Configure SysTick to interrupt at the requested rate. */ 
          // --The number from 180MHz/1000 is equivalent to the number of meters in 1ms
          portNVIC_SYSTICK_LOAD_REG = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL;
          // --Corresponding to the first three positions 1 of CTRL register of Systick timer
          portNVIC_SYSTICK_CTRL_REG = ( portNVIC_SYSTICK_CLK_BIT | portNVIC_SYSTICK_INT_BIT | portNVIC_SYSTICK_ENABLE_BIT );
      
      1. uxCriticalNesting = 0; The critical nesting number is initialized to 0, which will be used later in the critical nesting of the task

      2. Call the function prvnablevfp(); Enable FPU, floating point calculation

        1. SCB structure according to * * FreeRTOS learning record 01 -- interrupt management subsection 1.1 * *_ Type find the absolute memory address 0X E000 ED88 of the register
        2. To enable FPU, set bit20~bit23 of CPACR register to 1,
        3. The following is the specific configuration of CPACR register

      3. The operation of portFPCCR register is about inert stack pressing. I'm dizzy and don't understand

      4. Call the function prvStartFirstTask() to actually start the first task

      • __ ASM void prvstartfirsttask (void) assembly starts the first task

        __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 save 0xE000ED08 in R0 register,

        2. In the next two steps, ldr r0, [r0], take out the value in the memory with address 0xE000ED08 and save it in R0. Take out the content in the memory with this value as the address and store it in R0. This value is the value of MSP, which will be initialized later

        3. Why is the starting address 0xE000ED08? It is provided in the punctual atomic book below: Generally speaking, the vector table should be stored from the starting address (0x 0000 0000 0000), but there is a redefined vector table. Cortex-M provides a vector table offset register (VTOR), and the address of this register is 0xE000ED08, The content value in this register is the address 0x 0800 0000 at the beginning of the vector table,

          #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 // --Reset interrupt vector table
          SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal FLASH */
          

          Exactly corresponding, that is, the corresponding address when the keil program is downloaded

        4. The initial value of MSP is stored in the starting address 0x 0800 0000 of the vector table, so steps 1, 2 and 3 above are to store the initial value of MSP in R0

        5. msr msp, r0 reset MSP

        6. svc 0 starts to trigger SVC interrupt and enters SVC interrupt service function

        • __ ASM void vportsvchandler (void) SVC interrupt service function
          __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} // --Use the command to recover the stack of tasks to run
              msr psp, r0 // --The psp process stack pointer is set to the stack of the task
              isb
              mov r0, #0
              msr	basepri, r0
              bx r14 // --After SVC interrupts the register configuration, it will be executed directly from the PSP pointer address
              // --So far, the task switching after the first task is started is handled by PendSV (this interrupt is only used once!!!)
          }
          
          1. ldr r3, =pxCurrentTCBpxCurrentTCB points to the running task and saves it in R3. The initialization of pxCurrentTCB has been handled when initializing idle tasks at the beginning. For specific operations, see Section 2.1 of FreeRTOS learning record 02 - tasks
          2. ldr r1, [r3] get the address of the current task block
          3. ldr r0, [r1] read the address where the task block is located, that is, the address pointed to by the stack top pointer (pxTopOfStack)
          4. ldmia r0!, {r4-r11, r14} next, start to restore the task site of the task, and start from pxTopOfStack to r4-r11, r149 registers. At the same time, pxTopOfStack changes from 0x 2000 1004 to 0x 2000 1028, corresponding to 0x24, and the corresponding 9 registers are 4X9 =36 bit =0x24. In the initialization of task function and the initialization of task stack, the assignment of these areas is mentioned, Those that are pushed into the stack one by one should also come out in order. For specific operations, please refer to Section 2.1 of FreeRTOS learning record 02 - task chapter
          5. msr basepri, r0 set the BASEPRI register to 0 and start to open the interrupt.
          6. At this point, the first task officially starts running

2 task switching

2.1 PendSV exception

  • The task switching occasion is to cause PendSV exception through an event, and then enter the corresponding interrupt service function of PendSV
  • bit 28 of ICSR register is set to position 1

2.2 two events cause PendSV exception

  • Execute system call

    1. For functions with YIELD, task switching will be caused
    2. Finally, the portYIELD() function is called
    // --Corresponding 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; The corresponding is to set bit 28 of ICSR register to position 1
  • System tick timer interrupt

    /systick Interrupt service function,use OS When used
    void SysTick_Handler(void)
    {	
        if(xTaskGetSchedulerState()!=taskSCHEDULER_NOT_STARTED)//The system is already running
        {
            xPortSysTickHandler();	// --The system runs the ticking timer service function to perform + 1 operation~~
        }
    }
    
    1. Call xPortSysTickHandler();
    • xPortSysTickHandler()
      // --Tick timer interrupt call function
      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(); // --Disable interrupt
          {
              /* 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(); // --Recovery interrupt
      }
      
      1. It can be seen that portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; The corresponding is to set bit 28 of ICSR register to position 1 Departure PendSV interrupt.

2.3 the interrupt service function of pendsv performs task switching here

  • Key points of task switching

    __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 and PSP obtain the process stack pointer and store it in the r0 register, which points to the stack area of the current running task
    2. ldr r3, =pxCurrentTCB ;ldr r2, [r3] get the address of the currently running task block
    3. Save FPU related
    4. stmdb r0!, {r4-r11, r14} stack these registers that need to be manually stacked
    5. str r0, [r2] give the new stack top pointer to the stack top pointer pxTopOfStack of the task block
    6. stmdb sp!, {r3} R3 pointer points to the current task, which is pxCurrentTCB. After switching, we need to point pxCurrentTCB to the task we want to switch
    7. configMAX_SYSCALL_INTERRUPT_PRIORITY shutdown interrupt
    8. The above is equivalent to that the currently running task site has been saved and pressed into the stack of the task. The most important thing below is to find the next task to switch. The whole system is running around this.
    9. Call the function vTaskSwitchContext()
    10. follow-up
    • 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. Get the next task to run with the highest priority
      2. The priority of each task in the ready list will be marked in the 32bit uxReadyPriorities register. The corresponding priority value can be obtained faster by calculating the number of leading zeros on the hardware
      #define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities ) uxTopPriority = ( 31UL - ( uint32_t ) __clz( ( uxReadyPriorities ) ) )
      
      1. Obtain the corresponding task in the ready status list through the priority value in step 2, and update the highest priority task to pxCurrentTCB
      2. pxCurrentTCB points to the task we want to run at present. Next, restore the task field of the task to the corresponding R register.
    1. ldmia sp!, {r3} since pxCurrentTCB has been updated, the value of R3 has changed at this time
    2. ldr r1, [r3]; ldr r0, [r1] fetch the stack top pointer of the current task
    3. ldmia r0!, {r4-r11, r14} out of the task stack to the r4-r11, r14 register
    4. Operation of FPU
    5. msr psp, r0 updates the PSP process stack pointer, which is a public. Every time that task is used, the PSP is updated
    6. After bx r14 is executed, the hardware automatically restores R0~R3, R12, LR, PC and xPSR registers. Since R14 =0xffff fffd, it will enter the process mode after the end of this process, and use the process stack pointer (PSP), the PC value will be restored to the task function to be run, the new task will start running, and the task switching is successful.
    7. About R14 =0xffff fffd
      [the external chain picture transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the picture and upload it directly (img-eYeKYriV-1626227883736)(Photo\R14.png)]

Life has changed
11. ldr r1, [r3]; ldr r0, [r1] fetch the stack top pointer of the current task
12. ldmia r0!, {r4-r11, r14} out of the task stack to the r4-r11, r14 register
13. Operation of FPU
14. msr psp, r0 updates the PSP process stack pointer, which is a public. Every time that task is used, the PSP is updated
15. After BX R14 is executed, the hardware automatically restores R0~R3, R12, LR, PC and xPSR registers. Since R14 =0xffff fffd, it will enter the process mode after the end of this process, and use the process stack pointer (PSP), the PC value will be restored to the task function to be run, the new task will start running, and the task switching is successful.
16. About R14 =0xffff fffd

Topics: FreeRTOS