Task definition and switching principle analysis of freertos kernel
- main program
- Task control block
- Task creation function
- Task stack initialization
- ready list
- Scheduler
- Summary task switching
main program
The purpose of this program is to use freertos to constantly switch between two tasks. Look at the change of variables (waveform) in the two tasks.
The following figure shows the result of delay(100) in the task function.
The following figure shows the result of delay(2) in the task function
In a multitasking system, the CPU seems to be doing two things at the same time, that is, the best expectation is that the waveforms of the two variables should be exactly the same.
In this experiment, the delay is reduced, and the middle distance between their two variable waveforms is still not reduced, which shows that this experiment is only an introduction and is far from reaching the efficiency of RTOS.
This experimental feature is that it has the ability of active task switching. How to realize this is worth studying.
The following two figures visually show the active switching of the program. By observing the parameter CurrentTCB, you can find that it changes all the time.
Why does it change? It can be found by gradually debug ging because a SwitchContext function is called.
So let's take a look at what's in main:
It can be seen from the following that there are task stacks, task control blocks, task functions and tasks to be created. There are ready lists and schedulers.
Task stack:
#define TASK1_STACK_SIZE 20 StackType_t Task1Stack[TASK1_STACK_SIZE]; #define TASK2_STACK_SIZE 20 StackType_t Task2Stack[TASK2_STACK_SIZE];
Task function (task entry):
void Task1_Entry( void *p_arg ) { for( ;; ) { flag1 = 1; delay( 100 ); flag1 = 0; delay( 100 ); /* Task switching, here is manual switching */ taskYIELD(); } } void Task2_Entry( void *p_arg ) { for( ;; ) { flag2 = 1; delay( 100 ); flag2 = 0; delay( 100 ); /* Task switching, here is manual switching */ taskYIELD(); } }
Task control block:
TCB_t Task1TCB; TCB_t Task2TCB;
Ready list initialization:
prvInitialiseTaskLists();
Create task:
typedef void * TaskHandle_t; TaskHandle_t Task1_Handle;
Task1_Handle = xTaskCreateStatic( (TaskFunction_t)Task1_Entry, /* Task entry */ (char *)"Task1", /* Task name, in string form */ (uint32_t)TASK1_STACK_SIZE , /* Task stack size, in words */ (void *) NULL, /* Task parameters */ (StackType_t *)Task1Stack, /* Start address of task stack */ (TCB_t *)&Task1TCB ); /* Task control block */
To add a task to the ready list:
vListInsertEnd( &( pxReadyTasksLists[1] ), &( ((TCB_t *)(&Task1TCB))->xStateListItem ) );
Start scheduler:
vTaskStartScheduler();
Task control block
Multi task system, task execution is scheduled by the system. There is a lot of information about the task, so the task control block is used to represent the task, which is convenient for system scheduling.
The task control block type contains all the information of the task, such as the stack top pointer pxTopOfStack, the task node xStateListItem, the starting address of the task stack pxStack, and the task name pcTaskName.
typedef struct tskTaskControlBlock { volatile StackType_t *pxTopOfStack; /* Stack top */ ListItem_t xStateListItem; /* Task node */ StackType_t *pxStack; /* Start address of task stack */ /* Task name, in string form */ char pcTaskName[ configMAX_TASK_NAME_LEN ]; } tskTCB; typedef tskTCB TCB_t;
Task creation function
In main, xtask createstatic is called to create a task. It can be seen from the observation that this function actually changes the Task1TCB task control block. At the beginning of the birth of this task control block, it has not been initialized. The purpose of calling the task creation function is to initialize the task control block.
Task1_Handle = xTaskCreateStatic( (TaskFunction_t)Task1_Entry, /* Task entry */ (char *)"Task1", /* Task name, in string form */ (uint32_t)TASK1_STACK_SIZE , /* Task stack size, in words */ (void *) NULL, /* Task parameters */ (StackType_t *)Task1Stack, /* Start address of task stack */ (TCB_t *)&Task1TCB ); /* Task control block */
Visually express the internal of this function:
Task node in the task control block: the following code is the initialization process, which is actually the initialization of common nodes in the linked list.
/* Initialize xStateListItem node in TCB */ vListInitialiseItem( &( pxNewTCB->xStateListItem ) ); /* Sets the owner of the xStateListItem node */ listSET_LIST_ITEM_OWNER( &( pxNewTCB->xStateListItem ), pxNewTCB );
Where is this task entry reflected? In fact, it is reflected in the task stack. In main The initialization task stack in C only opens up a section of memory space, and there is no specific description of what is put in it. After calling the task creation function, the task stack is also initialized (put things in it), and the task entry is placed in this stack. When the task stack is initialized, the task control block is successfully initialized.
Therefore, the task stack initialization function must be called in the task creation function.
Task stack initialization
The function code for initializing the task stack is as follows:
StackType_t *pxPortInitialiseStack( StackType_t *pxTopOfStack, TaskFunction_t pxCode, void *pvParameters ) { /* When an exception occurs, it is automatically loaded into the CPU register */ pxTopOfStack--; *pxTopOfStack = portINITIAL_XPSR; /* xPSR bit24 must be set to 1 */ pxTopOfStack--; *pxTopOfStack = ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK; /* PC,Task entry function */ pxTopOfStack--; *pxTopOfStack = ( StackType_t ) prvTaskExitError; /* LR,Function return address */ pxTopOfStack -= 5; /* R12, R3, R2 and R1 Default initialization is 0 */ *pxTopOfStack = ( StackType_t ) pvParameters; /* R0,Task parameters */ /* When an exception occurs, manually load the contents of the CPU register */ pxTopOfStack -= 8; /* R11, R10, R9, R8, R7, R6, R5 and R4 Default initialization is 0 */ /* Returns the pointer to the top of the stack. At this time, pxTopOfStack points to the idle stack */ return pxTopOfStack; }
static void prvTaskExitError( void ) { /* The function stops here */ for(;;); }
The pointer at the top of the stack is pxTopOfStack. pxStack is a pointer pointing to the starting address of the task stack, and ulsackdepth is the size of the task stack. The following is the code to get the stack top pointer. Stack is last in first out, first in first out. In fact, the advanced stack is pressed to the bottom (the subscript is the last). Therefore, if there is nothing in the stack, the position of the top of the stack must be at the back (that is, the position with the highest address).
/* Get stack top address */ pxTopOfStack = pxNewTCB->pxStack + ( ulStackDepth - ( uint32_t ) 1 );
The following two figures express the same meaning, but the one on the right may be easier to understand (the one on the advanced stack is pressed to the bottom).
After the function of initializing the task stack runs, the stack changes, and there are contents in it, as shown in the figure below. You can see that the task entry address is saved and the task parameters are also saved.
#define portINITIAL_XPSR ( 0x01000000 )
So far, through the task creation function, the task control block has been successfully initialized, and the task stack has been filled. The task stack contacts the task entry address (the function entity of the task). There is a stack top pointer in the member variable of the task control block, which contacts the task stack. Then, the task stack, the function entity of the task and the control block of the task are connected through the task creation function.
Insert a sentence: one element of the task stack takes up four bytes! In the above figure, if the r0 address is 0x40, the pxTopOfStack address is 0x20 (because 0x40-0x20=32), 32 ÷ 4 = 8, that is, eight elements.
#define portSTACK_TYPE uint32_t typedef portSTACK_TYPE StackType_t; StackType_t Task1Stack[TASK1_STACK_SIZE]; uint32_t u: representative unsigned That is, unsigned, that is, the defined variable cannot be negative; int: The representative type is int Plastic surgery; 32: Represents four bytes, i.e int Type; _t: Representative use typedef Defined; Overall representative: use typedef Defined unsigned int Type macro definition; position(bit): Each bit has only two states 0 or 1. The smallest unit of data that a computer can represent. byte(Byte): 8 Bit binary number is one byte. The contents of the basic storage unit of the computer are expressed in bytes.
ready list
The following is the definition and initialization of the ready list in main. Add tasks to the ready list.
First, the definition of thread list. In short, the ready list is a List_t-type array (in fact, each element in the array is equivalent to the root node), and the array subscript corresponds to the priority of the task.
#define configMAX_PRIORITIES /* Task ready list */ List_t pxReadyTasksLists[ configMAX_PRIORITIES ]; /* Initialize task related lists, such as the ready list */ prvInitialiseTaskLists(); /* Add task to ready list */ vListInsertEnd( &( pxReadyTasksLists[1] ), &( ((TCB_t *)(&Task1TCB))->xStateListItem ) ); /* Add task to ready list */ vListInsertEnd( &( pxReadyTasksLists[2] ), &( ((TCB_t *)(&Task2TCB))->xStateListItem ) );
The ready list initialization function is as follows. In short, it is to initialize the list_ Each element in an array of type T is initialized (root node initialization).
/* Initialize task related lists */ void prvInitialiseTaskLists( void ) { UBaseType_t uxPriority; for( uxPriority = ( UBaseType_t ) 0U; uxPriority < ( UBaseType_t ) configMAX_PRIORITIES; uxPriority++ ) { vListInitialise( &( pxReadyTasksLists[ uxPriority ] ) ); } }
The function of adding tasks to the ready list is vListInsertEnd. As mentioned in the two-way circular linked list before, it is actually inserting ordinary nodes into the root node.
The ready list establishes a connection between different tasks, as shown below.
Scheduler
Start the scheduler with an SVC interrupt.
As can be seen from the following code, pxCurrentTCB refers to the address of task 1tcb (task control block).
typedef struct tskTaskControlBlock { volatile StackType_t *pxTopOfStack; /* Stack top */ ListItem_t xStateListItem; /* Task node */ StackType_t *pxStack; /* Start address of task stack */ /* Task name, in string form */ char pcTaskName[ configMAX_TASK_NAME_LEN ]; } tskTCB; typedef tskTCB TCB_t; //Void vtaskstartscheduler (void) function pxCurrentTCB = &Task1TCB;
In the following interrupt function of svc, the first step is to give the top pointer of the task stack to the r0 register.
It can be considered that R0 = pxtopofstack (the address of the top pointer of the task stack).
//__ ASM void vportsvchandler (void) function ldr r3, =pxCurrentTCB //Load the address of pxCurrentTCB to r3 ldr r1, [r3] //Give r1 the content pointed to by r3. The content is the address of task 1tcb ldr r0, [r1] //Give r0 the content pointed to by r1. The content is the first content in the address of Task1TCB, that is, pxTopOfStack
Next: take R0 (the address of the top pointer of the task stack) as the base address, and load the upward 8-byte content in the task stack into the CPU registers r4-r11.
ldmia r0!, {r4-r11}
Then save r0 to psp.
msr psp, r0
The following code is to change exc_ The return value is 0xFFFFFFD, so the interrupt returns to the thread mode and uses the thread stack (sp=psp).
orr r14, #0xd
Look at the figure below. When the exception returns, the PSP pointer is used to exit the stack. The PSP pointer takes out all the remaining contents in the task stack (the contents not read in the register) (automatically loads the remaining contents in the stack into the cpu register). Then the address of the task function is given to the PC, and the program jumps to the place of the task function and continues to run.
Figure 1 is as follows: note that psp moves and pxTopOfStack does not move.
The following is an experiment to prove the correctness of the above description of psp pointer motion:
r0 starts with the value of pxTopOfStack (the address of the top pointer of the task stack)
Next, give the moved r0 to the PSP, and the PSP position at this time is at the place in figure 1psp2.
In the following figure, the psp address is still 0x40.
After the program runs bx r14, it runs to the task function. At this time, psp=0x60, and the position is psp3 in Figure 1.
Now the program runs to the task function. The taskYIELD() function is called in the task function to trigger the PendSV interrupt (the lowest priority. It will respond only when there are no other interrupts running). The following figure shows the register group status before entering the PendSV interrupt service function.
The following figure shows the register group status when entering the PendSV interrupt service function. It can be observed that the psp has changed from 0x60 to 0x40.
Now you can know the location of psp, as shown in the figure below. This is because after entering the xPortPendSVHandler function, the running environment of the previous task will be automatically stored in the task stack, and psp will be updated automatically.
The following code stores the value of psp in r0.
//__ ASM void xportpendsvhandler (void) function mrs r0, psp
//Void vtaskstartscheduler (void) function pxCurrentTCB = &Task1TCB;/*pxCurrentTCB There is an address. The content in this address is the address of the current task*/ /*The first content of the current task address is the stack top pointer of the current task*/ //__ ASM void xportpendsvhandler (void) function ldr r3, =pxCurrentTCB /* Load the address of pxCurrentTCB to r3 */ ldr r2, [r3] /* Give r2 the content pointed to by r3, and the content is the address of task 1tcb (current task)*/ /*[r2]Is the top pointer of the current task stack*/
stmdb r0!, {r4-r11} /* Store the values of CPU registers r4~r11 to the address pointed to by r0 */ str r0, [r2] /* Give the address of r0 to the top pointer of the current task stack */
After the above code, the position of r0 is as follows. psp does not change in the above process, only r0.
Compare with the figure below to make it clearer. r2 stores the address of the current task. r0 stores the address of the stack top pointer.
The following describes r3: r3=0x2000000C. The first content stored in this address is the address 0x20000068 of the current task block, as shown in the figure below.
The following describes the address of the current task block: the first content stored in the address 0x20000068 of the current task block is the address of the pointer at the top of the stack.
The following describes the address of the stack top pointer: the content in the stack top pointer address is just the task stack of the current task.
You can compare the following figure to observe the content in the current task stack. At the same time, the content also corresponds to the address. The address can be derived from the above figure. For example, 0x20000060 is 0x10000000 stored in the address.
The following code: the purpose is to temporarily press r3 and r14 into the main stack (the stack pointed to by MSP), because the task switching function needs to be called next. When calling the function, the return address is automatically saved in r14. The content of r3 is the address of the current task block (ldr r3, =pxCurrentTCB). After calling the function, pxCurrentTCB will be updated.
stmdb sp!, {r3, r14}
Before executing the code, MSP points to the address 0x20000058.
After executing the code, the address pointed to by MSP is 8 bytes less, while r3 and r14 are stored in the address pointed to by MSP.
In fact, the specific information in the stack pointed to by msp can be deduced, as shown in green words:
The following code: basepri is the interrupt mask register. In the following setting, interrupts with priority greater than or equal to 11 will be masked. It is equivalent to turning off the interrupt and entering the critical section.
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY msr basepri, r0 /* #define configMAX_SYSCALL_INTERRUPT_PRIORITY 191 /* The upper four bits are valid, which is equal to 0xb0 or 11 */ 191 When converted to binary, it is 11000000, and the upper four digits are 1100 */
The following code: the vTaskSwitchContext function is called to select the task with the highest priority and update pxCurrentTCB. At present, manual switching is used here.
bl vTaskSwitchContext
void vTaskSwitchContext( void ) { /* The two tasks switch in turn */ if( pxCurrentTCB == &Task1TCB ) { pxCurrentTCB = &Task2TCB; } else { pxCurrentTCB = &Task1TCB; } }
Now let's explain the consequences of calling this function:
As can be seen from the figure below, r3=0x2000000C, and the content in this address is the address of the current task block.
Proceed to the following step, the address of the current task block has changed, and at the same time, the content in the 0x2000000C address has also changed. In other words, after stepping out of the calling function, you can find the new task address after the change through r3.
Then it suddenly becomes clear why r3 should be put on the stack before calling the function. Look at the assembly code at the top of the middle of the figure below. The assembly code behind the C language calls registers r0 and r1 to store some intermediate variables. In order to prevent intermediate variables from being stored in the r3 register when running the function, r3 is put on the stack for protection. Think about it. If the intermediate variable is stored in the r3 register, the 0x2000000C address will not be stored in the r3 register, and the new task address after the change can not be found through r3.
The following code: interrupts with priority higher than 0 are masked, which is equivalent to opening interrupts and exiting the critical section.
mov r0, #0 / * exit critical section*/ msr basepri, r0
The following code restores r3 and r14
ldmia sp!, {r3, r14} /* Restore r3 and r14 */
As shown in the following figure, r3 and r14 are restored, and the MSP is changed from 0x20000550 to 0x20000558.
There is a detail here. After the MSP changes, the number in front of the stack pointed by the MSP (stored r3 and r14) is left. This makes people wonder what the stack really means. Isn't it just moving the MSP pointer here.
At this time, by observing the contents of the psp address, you can find that it is still the previous task stack. After looking at the issue of stack and the issue of entities in c language (after the issue of stack in c language, the content is not in the stack), the issue of stack moves the pointer, and the content is still in the stack.
After the following code is completed, r0 stores the address of the top pointer of the current task stack.
ldr r1, [r3] ldr r0, [r1] /* The first item of the currently active task TCB saves the stack top of the task stack, and now the stack top value is stored in R0*/
The following is the content of the current task stack.
ldmia r0!, {r4-r11} /* Out of stack */
At this time, the r0 position changes to 0x200000c0.
Then I gave r0 to psp. Remember, in the past, psp pointed to 0x20000040, that is, the task stack of the previous task, which was switched to the task stack of another task. That is, psp points to 0x200000c0.
msr psp, r0
After running the following code, the effect is shown in the figure below.
bx r14
Carefully observe that when the exception exits, psp will be used as the base address to automatically load the rest of the task stack into the CPU register. Then the PC pointer gets the address of the task function in the task stack, and then jumps to the task function. At this point, the switching is completed.
Finally, take a look at psp: from the following two figures, you can see what psp out of the stack means.
The following is the direction of psp after returning Thread Mode (entering the task function).
The following figure shows the direction of psp when it does not return to Thread Mode.
Summary task switching
Summarize the core ideas:
1. First of all, in the task function, it is in the Thread Mode state (why? Because bx r14 instruction, the value of r14 in it is set to 0xFFFFFFFD), and then through the taskYIELD() function in the task function, it enters the Handler Mode state, in which the task switching operation is carried out, that is, The task stack pointed to by psp is switched (so the task function pointed to by pc is also changed later). Then when the exception ends, psp comes out of the stack. Now pc points to the address of the switched task function, so it jumps to another task function.
2. Understand the principle of switching to the task function
When creating a task before, the task function has been saved in the task stack.
If you exit the stack, the rest of the stack pointed to by psp will be loaded into the register, as shown in the figure below: then the address of the task function will be given to the pc pointer. After the exception returns, the program will jump to the place of the task function to continue running, and then it will be switched to the task function.