FreeRTOS advanced part 3 -- Analysis of FreeRTOS scheduler startup process

Posted by rsasalm on Sun, 02 Jan 2022 06:09:18 +0100

Using FreeRTOS, a basic program architecture is as follows:

   
  1. int main( void)
  2. {
  3. Necessary initialization;
  4. Create task 1;
  5. Create task 2;
  6. ...
  7. vTaskStartScheduler(); /*Start scheduler*/
  8. while( 1);
  9. }

After the task is created, the static variable pointer (see< FreeRTOS advanced Chapter 2 - FreeRTOS task creation analysis >Section 7) points to the highest priority ready task. However, the task cannot run at this time, because there is another key step: start the FreeRTOS scheduler.

The scheduler is the core of FreeRTOS operating system, which is mainly responsible for task switching, that is, finding the ready task with the highest priority and making it obtain the CPU running right. The scheduler does not run automatically and needs to be started manually.

The API function vTaskStartScheduler() is used to start the scheduler. It will create an idle task and initialize some static variables. Most importantly, it will initialize the system beat timer and set the corresponding interrupt, and then start the first task. This article is used to analyze the process of starting the scheduler. Like the previous article, starting the scheduler also involves hardware features (such as system beat timer initialization). Therefore, this paper still takes Cortex-M3 architecture as an example.

The simplified source code of the API function vTaskStartScheduler() of the start scheduler is as follows:

   
  1. void vTaskStartScheduler( void )
  2. {
  3. BaseType_t xReturn;
  4. StaticTask_t *pxIdleTaskTCBBuffer= NULL;
  5. StackType_t *pxIdleTaskStackBuffer= NULL;
  6. uint16_t usIdleTaskStackSize =tskIDLE_STACK_SIZE;
  7. /*If static memory is used to allocate task stack and task TCB, task memory and task TCB space need to be pre-defined for idle tasks*/
  8. #if(configSUPPORT_STATIC_ALLOCATION == 1 )
  9. {
  10. vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer, &pxIdleTaskStackBuffer, &usIdleTaskStackSize);
  11. }
  12. #endif /*configSUPPORT_STATIC_ALLOCATION */
  13. /* Create idle tasks with the lowest priority*/
  14. xReturn =xTaskGenericCreate( prvIdleTask, "IDLE",usIdleTaskStackSize, ( void * ) NULL, ( tskIDLE_PRIORITY | portPRIVILEGE_BIT), &xIdleTaskHandle,pxIdleTaskStackBuffer,pxIdleTaskTCBBuffer, NULL );
  15. if( xReturn == pdPASS )
  16. {
  17. /* Turn off the interrupt first to ensure that the beat timer interrupt does not occur on or before the call to xPortStartScheduler() When the first task starts, the interrupt is restarted*/
  18. portDISABLE_INTERRUPTS();
  19. /* Initialize static variables */
  20. xNextTaskUnblockTime = portMAX_DELAY;
  21. xSchedulerRunning = pdTRUE;
  22. xTickCount = ( TickType_t ) 0U;
  23. /* If the macro configGENERATE_RUN_TIME_STATS is defined to indicate that the runtime statistics function is used. The following macro must be defined to initialize a basic timer / counter*/
  24. portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();
  25. /* Set the system beat timer, which is related to the hardware characteristics, so it is placed in the migration layer*/
  26. if(xPortStartScheduler() != pdFALSE )
  27. {
  28. /* If the scheduler runs correctly, it will not execute here and the function will not return*/
  29. }
  30. else
  31. {
  32. /* This will be executed only when the task calls the API function xTaskEndScheduler()*/
  33. }
  34. }
  35. else
  36. {
  37. /* Execution here indicates that the kernel is not started, possibly because there is not enough stack space */
  38. configASSERT( xReturn );
  39. }
  40. /* Prevent compiler warnings*/
  41. ( void ) xIdleTaskHandle;
  42. }

This API function first creates an idle task. The idle task uses the lowest priority (level 0). The task handle of the idle task is stored in the static variable xidletask handle. You can call the API function xTaskGetIdleTaskHandle() to obtain the idle task handle.

If the task is created successfully, close the interrupt (when the scheduler starts to terminate and then interrupt again), initialize some static variables, and then call the function xPortStartScheduler() to start the system timer and start the first task. Because setting the system beat timer involves hardware characteristics, the function xPortStartScheduler() is provided by the migration layer. The code of this function is also different for different hardware architectures.

For Cortex-M3 architecture, the implementation of the function xPortStartScheduler() is as follows:

   
  1. BaseType_t xPortStartScheduler( void )
  2. {
  3. #if(configASSERT_DEFINED == 1 )
  4. {
  5. volatile uint32_tulOriginalPriority;
  6. /* Interrupt priority register 0:IPR0 */
  7. volatile uint8_t * constpucFirstUserPriorityRegister = ( uint8_t * ) (portNVIC_IP_REGISTERS_OFFSET_16 +portFIRST_USER_INTERRUPT_NUMBER );
  8. volatile uint8_tucMaxPriorityValue;
  9. /* This large section of code is used to determine the highest ISR priority. In this ISR or lower priority ISR, API functions ending with FromISR can be safely called*/
  10. /* Save the interrupt priority value because this register (IPR0) will be overwritten below */
  11. ulOriginalPriority = *pucFirstUserPriorityRegister;
  12. /* Determines the number of valid priority bits First write 1 to all bits and then read it out. Since the invalid priority bits are read out as 0, then count the number of 1 to know how many priority bits there are*/
  13. *pucFirstUserPriorityRegister= portMAX_8_BIT_VALUE;
  14. ucMaxPriorityValue = *pucFirstUserPriorityRegister;
  15. /* Redundancy code is used to prevent the user from setting the RTOS maskable interrupt priority value incorrectly */
  16. ucMaxSysCallPriority =configMAX_SYSCALL_INTERRUPT_PRIORITY &ucMaxPriorityValue;
  17. /* Calculate maximum priority group value */
  18. ulMaxPRIGROUPValue =portMAX_PRIGROUP_BITS;
  19. while( (ucMaxPriorityValue &portTOP_BIT_OF_BYTE ) ==portTOP_BIT_OF_BYTE )
  20. {
  21. ulMaxPRIGROUPValue--;
  22. ucMaxPriorityValue <<= ( uint8_t ) 0x01;
  23. }
  24. ulMaxPRIGROUPValue <<=portPRIGROUP_SHIFT;
  25. ulMaxPRIGROUPValue &=portPRIORITY_GROUP_MASK;
  26. /* Restores the value of the IPR0 register*/
  27. *pucFirstUserPriorityRegister= ulOriginalPriority;
  28. }
  29. #endif /*conifgASSERT_DEFINED */
  30. /* Set PendSV and SysTick interrupts to the lowest priority*/
  31. portNVIC_SYSPRI2_REG |=portNVIC_PENDSV_PRI;
  32. portNVIC_SYSPRI2_REG |=portNVIC_SYSTICK_PRI;
  33. /* Start the system beat timer, namely SysTick timer, initialize the interrupt cycle and enable the timer*/
  34. vPortSetupTimerInterrupt();
  35. /* Initialize critical area nested counter */
  36. uxCriticalNesting = 0;
  37. /* Start the first task */
  38. prvStartFirstTask();
  39. /* Never come here! */
  40. return 0;
  41. }

As you can see from the source code, the first large section is redundant code. Because the interrupt priority of Cortex-M3 is somewhat counterintuitive: the higher the interrupt priority value of Cortex-M3, the lower the priority. The task priority of FreeRTOS is the opposite: the higher the priority value, the higher the priority. According to official statistics, when FreeRTOS is used on Cortex-M3 hardware, most of the problems are due to incorrect priority setting. Therefore, in order to make FreeRTOS more robust, the author of FreeRTOS deliberately added redundant code when writing the code of Cortex-M3 architecture migration layer. For detailed interrupt priority settings of Cortex-M3 architecture, refer to< FreeRTOS series Chapter 7 -- special precautions for using FreeRTOS with Cortex-M kernel >A penny.

In the Cortex-M3 architecture, FreeRTOS uses three exceptions for task startup and task switching: SVC, PendSV and SysTick. SVC (system service call) is used for task startup. Some operating systems do not allow applications to directly access hardware, but call it through SVC by providing some system service functions; PendSV (system call can be suspended) is used to complete task switching. Its biggest feature is that if an interrupt with higher priority is running, PendSV will delay the execution until the execution of the high priority interrupt is completed; SysTick is used to generate the system beat clock and provide a time slice. If multiple tasks share the same priority, the next task will be interrupted each time SysTick is interrupted Get a time slice. For detailed description of SVC and PendSV exceptions, the "exception" section of Cortex-M3 authoritative guide is recommended.

Here, the PendSV and SysTick exception priority are set to the lowest, so that the task switching will not interrupt a certain interrupt service program, and the interrupt service program will not be delayed, which simplifies the design and is conducive to system stability.

Next, call the function vportsetuptimerinthrupt() to set the interrupt cycle of SysTick timer and enable the timer to run. This function is relatively simple, that is, to set the corresponding registers of SysTick hardware.

The next key function is prvStartFirstTask(), which is used to start the first task. Let's first look at the source code:

   
  1. __ asm void prvStartFirstTask( void )
  2. {
  3. PRESERVE8
  4. /* Cortext-M3 In the hardware, the address 0xE000ED08 is the vtor (vector table offset) register, which stores the starting address of the vector table*/
  5. ldr r0, = 0xE000ED08
  6. ldr r0, [r0]
  7. /* Take out the first item in the vector table, and the first item in the vector table stores the initial value of the main stack pointer MSP*/
  8. ldr r0, [r0]
  9. /* Store stack address in main stack pointer */
  10. msr msp, r0
  11. /* Enable global interrupt*/
  12. cpsie i
  13. cpsie f
  14. dsb
  15. isb
  16. /* Call SVC to start the first task */
  17. svc 0
  18. nop
  19. nop
  20. }

The first few lines of code in the program are used to reset the value of the main stack pointer MSP, indicating that the MSP pointer has been taken over by FreeRTOS since then. It should be noted that the interrupt of Cortex-M3 hardware also uses the MSP pointer. After that, enable the interrupt, use the assembly instruction svc 0 to trigger the SVC interrupt to complete the work of starting the first task. Let's look at the SVC interrupt service function:

   
  1. __ asm void vPortSVCHandler( void )
  2. {
  3. PRESERVE8
  4. ldr r3, =pxCurrentTCB /* pxCurrentTCB Point to the highest priority ready task TCB */
  5. ldr r1, [r3] /* Get task TCB address */
  6. ldr r0, [r1] /* Get the first member of the task TCB, pxTopOfStack at the top of the current stack */
  7. ldmia r0!, {r4-r11} /* Out of stack, register r4~r11 out of stack */
  8. msr psp, r0 /* The latest stack top pointer is assigned to the thread stack pointer PSP */
  9. isb
  10. mov r0, # 0
  11. msr basepri, r0
  12. orrr14, # 0xd /* Here 0x0d means: enter the thread mode after returning, make stack operations from the process stack, and return to the Thumb state*/
  13. bx r14
  14. }

From the previous article on task creation, we have learned about the pointer pxCurrentTCB. This is defined in tasks The only global variable in C points to the ready task TCB at the highest priority. We know that the core function of FreeRTOS is to ensure that the ready task at the highest priority obtains CPU permission. Therefore, it can be said that the task pointed to by this pointer is either running or about to run (the scheduler is closed), so this variable is named pxCurrentTCB.

According to< FreeRTOS advanced Chapter 2 - FreeRTOS task creation analysis >In Section 3, we can know that when a task is created, its task stack will be initialized like a task switch, as shown in Figure 1-1. For Cortex-M3 architecture, it is required to stack, PC, LR, R12, R3~R0 and R11 ~ R4 in turn. R11 ~ R4 need to be manually stacked, and other registers are automatically stacked by hardware. The register PC is initialized to the task function pointer vTask_A. In this way, when a task is switched, task a obtains CPU control and the task function vTask_A is out of the stack to the PC register, and then the code of task a will be executed; The LR register is initialized to the function pointer prvTaskExitError, which is an error handling function provided by the migration layer.

The task TCB structure member pxTopOfStack represents the top of the current stack. It points to the last item in the stack, so in the figure it points to R4. Another TCB structure member pxStack represents the starting position of the stack, so in the figure it points to the beginning of the stack.


Figure 1-1: task stack distribution after task creation

Therefore, the SVC interrupt service function uses the global pointer pxCurrentTCB to obtain the TCB of the first task to be started from the beginning, so as to obtain the current stack top pointer of the task. First, take the artificially stacked registers R4~R11 out of the stack, assign the latest stack top pointer to the thread stack pointer PSP, and then cancel the interrupt masking. Here, as long as an interrupt occurs, it can be responded to.

The interrupt service function returns through the assembly of the following two sentences. In the Cortex-M3 architecture, the value of r14 determines the mode of returning from the exception. Here, the last four bits of r14 are bitwise or 0x0d, indicating that stack operations are made from the process stack when returning, entering the thread mode after returning, and returning to the Thumb state.

   
  1. orr r14, # 0xd
  2. bx r14

After executing bx# r14 instruction, the hardware will automatically stack registers xPSR, PC, LR, R12, R3~R0. At this time, the task function pointer vtask of task a_ A will be out of the stack into the PC pointer to start executing task a.

So far, the task vTask_A obtains the CPU Execution Authority, and the scheduler officially starts working.

 

 

 

Topics: Embedded system