Objectives of this section
This section is the foundation of the foundation. You must learn to create tasks and focus on how to switch tasks. Because the task switching in FreeRTOS is completed by assembly code, the code looks difficult to understand. Let's look at it patiently.
In this section, we will create two tasks and make them switch constantly. The main body of the task is to make a variable flip according to a certain frequency, and observe the waveform change of the variable in the logic analyzer through KEIL's software simulation function.
In a multitasking system, the effect diagram of two tasks switching constantly should have the same waveform of the two variables, just like the CPU doing two things at the same time. This is the meaning of multitasking.
This section is just the beginning. Let's first master how to switch tasks. In the following chapters, we will gradually improve the function code and add system scheduling to realize real multitasking.
What is a mission
In the bare metal system, the main body of the system is the infinite loop executed in sequence in the main function. In this infinite loop, the CPU completes all kinds of things in order. In multi task system, we divide the whole system into independent and unreturnable functions according to different functions. This function is called task.
void task_entry (void *parg) { /* Task body, infinite loop and cannot return */ for (;;) { /* Task subject code */ } }
Create task
Define task stack
In a multitasking system, each task is independent and does not interfere with each other, so an independent stack space should be allocated for each task. This stack space is usually a predefined global array or a dynamically allocated memory space, but they all exist in RAM.
In this section, we want to realize the rotation of two variables according to a certain frequency. Each variable corresponds to a task, so we need to define two task stacks. In a multitasking system, you need to define as many task stacks as there are tasks.
#define TASK1_STACK_SIZE 128 StackType_t Task1Stack[TASK1_STACK_SIZE]; #define TASK2_STACK_SIZE 128 StackType_t Task2Stack[TASK2_STACK_SIZE];
Define task function
Task is an independent function. The function body loops indefinitely and cannot return. In this section, we are in main Two tasks defined in C.
/* Software delay */ void delay (uint32_t count) { for(; count!=0; count--); } /* Task 1 */ void Task1_Entry( void *p_arg ) { for( ;; ) { flag1 = 1; delay( 100 ); flag1 = 0; delay( 100 ); } } /* Task 2 */ void Task2_Entry( void *p_arg ) { for( ;; ) { flag2 = 1; delay( 100 ); flag2 = 0; delay( 100 ); } }
Define task control block
In a multitasking system, the execution of tasks is scheduled by the system. In order to schedule tasks smoothly, the system defines an additional task control block for each task. This task control block is equivalent to the ID card of the task, which contains all the information of the task, such as the stack pointer of the task, the task name, the formal parameters of the task, etc. With this task control block, all future system operations on tasks can be realized through this task control block. Defining a task control block requires a new data type, which is in task C this is declared in the C header file. (in order to use the data type tskTCB in other places, I put the declaration of the task control block in the header file FreeRTOS.h)
typedef struct tskTaskControlBlock { volatile StackType_t *pxTopOfStack; /* Stack top */ ListItem_t xStateListItem; /* List items for tasks */ StackType_t *pxStack; /* Start address of task stack */ char pcTaskName[ configMAX_TASK_NAME_LEN ]; /* Task name, in string form */ } tskTCB; typedef tskTCB TCB_t;
In freertosconfig H defines the macro configMAX_TASK_NAME_LEN to control the length of the task name, in the form of string. The default is 16.
#define configMAX_TASK_NAME_LEN ( 16 )
In this experiment, we are in main Task control block defined for two tasks in C file
/* Define task control block */ TCB_t Task1TCB; TCB_t Task2TCB;
Implement task creation function
The stack of tasks, the function entities of tasks and the control blocks of tasks finally need to be connected before they can be uniformly scheduled by the system. Then the work of this connection is realized by the task creation function xtask createstatic(), which is in task C (task.c needs to be created in the folder freertos and added to the freertos/source group of the project for the first time) H declares that all functions related to the task are defined in this file.
Xtask createstatic() function
#if( configSUPPORT_STATIC_ALLOCATION == 1 )//① TaskHandle_t xTaskCreateStatic( TaskFunction_t pxTaskCode, /* ②Task entry */ const char * const pcName, /* Task name, in string form */ const uint32_t ulStackDepth, /* Task stack size, in words */ void * const pvParameters, /* Task parameters */ StackType_t * const puxStackBuffer, /* Start address of task stack */ TCB_t * const pxTaskBuffer) /* Task control block pointer */ { TaskHandle_t xReturn; //③ The task handle is used to point to the TCB of the task TCB_t *pxNewTCB; //Initialize xNewTCB to TCB structure //The start address of the task stack and the parameters of the task control block are passed into NewTCB if( (pxTaskBuffer != NULL ) && ( puxStackBuffer != NULL )) { pxNewTCB = (TCB_t * ) pxTaskBuffer; pxNewTCB->pxStack = ( StackType_t * ) puxStackBuffer; /* ④Create a new task */ prvInitialiseNewTask( pxTaskCode, /* Task entry */ pcName, /* Task name, in string form */ ulStackDepth, /* Task stack size, in words */ pvParameters, /* Task parameters */ &xReturn, /* task handle */ pxNewTCB); } else { xReturn = NULL; } /* Returns the task handle. If the task is created successfully, xReturn should point to the task control block */ return xReturn; } #endif /* configSUPPORT_STATIC_ALLOCATION */
① In FreeRTOS, there are two methods to create tasks, one is to use dynamic creation, and the other is to use static creation. During dynamic creation, the memory of task control block and stack is dynamically allocated during task creation. When a task is deleted, the memory can be released. During static creation, the memory of task control block and stack needs to be defined in advance. It is static memory. When a task is deleted, the memory cannot be released. At present, we take static creation as an example, configSUP-PORT_STATIC_ALLOCATION is in freertosconfig Defined in H, we configure it as 1.
#define configSUPPORT_STATIC_ALLOCATION 1
② Task entry, that is, the function name of the task. TaskFunction_t is in projdefs H (projdefs.h needs to create a new data type under the include folder for the first time, and then add it to the project freertos/source group file). In fact, it is a null pointer.
#ifndef PROJDEFS_H #define PROJDEFS_H typedef void (*TaskFunction_t)( void * ); #endif
③ Define a task handle xReturn, which is used to point to the TCB of the task. The data type of the task handle is TaskHandle_t. In task H is actually a null pointer.
typedef void * TaskHandle_t;
④ Call the prvInitialiseNewTask() function to create a new task. The function is in task C implementation
prvInitialiseNewTask() function
static void prvInitialiseNewTask( TaskFunction_t pxTaskCode, /* Task entry */ const char * const pcName, /* Task name, in string form */ const uint32_t ulStackDepth, /* Task stack size, in words */ void * const pvParameters, /* Task parameters */ TaskHandle_t * const pxCreatedTask, /* task handle */ TCB_t *pxNewTCB ) /* Task control block pointer */ { StackType_t *pxTopOfStack;//Define task stack top data type UBaseType_t x; //Define a ubasetype_ Tdata auxiliary value x, which is used in the length of the task name /* Get stack top address */ pxTopOfStack = pxNewTCB->pxStack + ( ulStackDepth - ( uint32_t ) 1 ); /* ①Align 8 bytes down */ pxTopOfStack = ( StackType_t * )( ( ( uint32_t ) pxTopOfStack ) &(~( ( uint32_t ) 0x0007 ) ) ); /* Store the name of the task in TCB */ for( x = ( UBaseType_t ) 0; x < (UBaseType_t) configMAX_TASK_NAME_LEN; x++ ) { pxNewTCB->pcTaskName[ x ] = pcName[ x ]; if( pcName[ x ] == 0x00 ) { break; } } /* The length of the task name cannot exceed configMAX_TASK_NAME_LEN */ pxNewTCB->pcTaskName[ configMAX_TASK_NAME_LEN - 1 ] ='\0'; /* Initialize xStateListItem list item in TCB */ vListInitialiseItem( &(pxNewTCB->xStateListItem )); /* Sets the owner of the xStateListItem list item */ listSET_LIST_ITEM_OWNER( &( pxNewTCB->xStateListItem ), pxNewTCB ); /* ②Initialize task stack */ pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxTaskCode, pvParameters ); /* Make the task handle point to the task control block */ if( ( void * ) pxCreatedTask != NULL ) { *pxCreatedTask = ( TaskHandle_t ) pxNewTCB; } }
① Align the stack top pointer down to 8 bytes. In the single chip microcomputer with Cortex-M3 (Cortex-M4 or Cortex-M7) core, because the bus width is 32 bits, usually as long as the stack keeps 4 bytes aligned, why 8 bytes? Are there any operations that are 64 bit? Indeed, it is floating-point operation, so 8-byte alignment is required (but we haven't involved floating-point operation at present, just for the consideration of subsequent compatible floating-point operation). If the pointer at the top of the stack is 8-byte aligned, the pointer will not move during the downward 8-byte alignment. If it is not 8-byte aligned, a few bytes will be left out during the downward 8-byte alignment and will not be used. For example, when pxTopOfStack is 33, it obviously cannot divide 8, and the downward 8-byte alignment is 32, Then one byte will be left unused.
② Call pxportinitializestack() function to initialize the task stack and update the pointer at the top of the stack. The environment parameters of the first run of the task are stored in the task stack. This function is in port C (for the first use of port.c, you need to create a new one under the freertoportablervdsarm_cm3 (ARM_CM4 or ARM_CM7) folder, and then add it to the project freertos/source group file).
Pxportinitializestack() function
#define portINITIAL_XPSR ( 0x01000000 ) #define portSTART_ADDRESS_MASK ( ( StackType_t ) 0xfffffffeUL ) static void prvTaskExitError( void ) { /* The function stops here */ for(;;); } 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; }
① When an exception occurs, the CPU automatically loads the contents of the CPU register from the stack. It includes 8 registers, which are bit 24 of R0, R1, R2, R3, R12, R14, R15 and xPSR, and the order cannot be changed.
② The return address of the task. Usually, the task will not return. If it returns, it will jump to prvTaskExitError. This function is an infinite loop.
③ Return the pointer to the top of the stack. At this time, pxTopOfStack points to the specific figure above. When the task runs for the first time, it starts from this stack pointer to manually load the contents of 8 words into the CPU registers: R4, R5, R6, R7, R8, R9, R10 and R11. When the exception exits, the contents of the remaining 8 words in the stack will be automatically loaded into bit 24 of the CPU registers: R0, R1, R2, R3, R12, R14, R15 and xPSR. At this point, the PC pointer points to the task entry address,
To successfully jump to the first task.
Implementation ready list
Define ready list
After the task is created, we need to add the task to the ready list to indicate that the task is ready and the system can schedule it at any time. The ready list is in task C.
/* Task ready list */ List_t pxReadyTasksLists[ configMAX_PRIORITIES ];
The ready list is actually a list_ The size of the array is determined by the macro configmax that determines the maximum task priority_ Priorities decision, configMAX_PRIORITIES in FreeRTOSCon-
The default definition in fig.h is 5, and 256 priorities are supported at most.
#define configMAX_PRIORITIES ( 5 )
The subscript of the array corresponds to the priority of the task, and the tasks with the same priority are uniformly inserted into the same linked list of the ready list. An empty ready list is shown in the following figure:
Ready list initialization
The ready list needs to be initialized before use. The initialization of the ready list is in task C function prvInitialiseTaskLists().
prvInitialiseTaskLists()
void prvInitialiseTaskLists( void ) { UBaseType_t uxPriority; for( uxPriority = ( UBaseType_t ) 0U; uxPriority < ( UBaseType_t ) configMAX_PRIORITIES; uxPriority++) { vListInitialise( &( pxReadyTasksLists[ uxPriority ] ) ); } }
Insert task into ready list
There is an xStateListItem member in the task control block, and the data type is ListItem_t. We insert the task into the ready list by inserting the xStateListItem of the task control block into the ready list. If the ready list is compared to a clothes hanger and the task is clothes, xStateListItem is the hook on the clothes hanger. Each task brings its own clothes hanger hook to hang itself in various lists.
In this experiment, after the task is created, we insert the task into the ready list in main C.
TaskHandle_t Task1_Handle; TaskHandle_t Task2_Handle; int main(void) { /* Initialize task related lists, such as the ready list */ prvInitialiseTaskLists(); /* Create task */ Task1_Handle = xTaskCreateStatic( ( TaskFunction_t )Task1_Entry, (char *)"Task1", (uint32_t)TASK1_STACK_SIZE, (void *)NULL, (StackType_t *)Task1Stack, (TCB_t *)&Task1TCB); /* Add task to ready list */ vListInsertEnd( &( pxReadyTasksLists[1]), &( ((TCB_t *)(&Task1TCB))->xStateListItem ) ); Task2_Handle = xTaskCreateStatic( ( TaskFunction_t )Task2_Entry, (char *)"Task2", (uint32_t)TASK2_STACK_SIZE, (void *)NULL, (StackType_t *)Task2Stack, (TCB_t *)&Task2TCB); /* Add task to ready list */ vListInsertEnd( &( pxReadyTasksLists[2]), &( ((TCB_t *)(&Task2TCB))->xStateListItem ) ); for(;;) { /*Don't do anything*/ } }
The subscript of the ready list corresponds to the priority of the task, but our task does not support priority at present. We will talk about the knowledge points about supporting multiple priorities later, so when inserting Task1 and Task2 tasks into the ready list, you can choose the insertion position at will. We choose to insert Task1 task into the list with 1 in the ready list, and Task2 task into the list with 2 in the ready list.
Implementation scheduler
The scheduler is the core of the operating system. Its main function is to realize task switching, that is, find the task with the highest priority from the ready list, and then execute the task. From the perspective of code, the scheduler is nothing more than composed of several global variables and some functions that can realize task switching, all of which are in task C file.
Start scheduler
The start of the scheduler is completed by the vTaskStartScheduler() function, which is in task C.
vTaskStartScheduler() function
/* The task control block pointer of the currently running task. The default initialization is NULL */ TCB_t * volatile pxCurrentTCB = NULL; extern TCB_t Task1TCB; void vTaskStartScheduler( void ) { /* ①Manually specify the first task to run */ pxCurrentTCB = &Task1TCB; /* ②Start scheduler */ if( xPortStartScheduler() != pdFALSE ) { } }
① pxCurrentTCB is a task The global pointer defined by C is used to point to the task control block of the task currently running or about to run. At present, we do not support priority, so manually specify the first task to run.
② Call the function xPortStartScheduler() to start the scheduler. If the scheduler starts successfully, it will not return. This function is in port C. In projdefs h. Define the macro pdFALSE to indicate the status.
#define pdFALSE ( ( BaseType_t ) 0 ) #define pdTRUE ( ( BaseType_t ) 1 ) #define pdPASS ( pdTRUE ) #define pdFAIL ( pdFALSE )
xPortStartScheduler() function
/* * Refer to 4.4.3 of STM32F10xxx Cortex-M3 programming manual. Baidu can find this document by searching "PM0056" * In Cortex-M, the SHPR3 register in the SCB of the kernel peripheral is used to set the exception priority of SysTick and PendSV * System handler priority register 3 (SCB_SHPR3) SCB_SHPR3: 0xE000 ED20 * Bits 31:24 PRI_15[7:0]: Priority of system handler 15, SysTick exception * Bits 23:16 PRI_14[7:0]: Priority of system handler 14, PendSV */ #define portNVIC_SYSPRI2_REG ( * ( ( volatile uint32_t * ) 0xe000ed20 ) ) #define portNVIC_PENDSV_PRI ( ( ( uint32_t ) configKERNEL_INTERRUPT_PRIORITY ) << 16UL ) #define portNVIC_SYSTICK_PRI ( ( ( uint32_t ) configKERNEL_INTERRUPT_PRIORITY ) << 24UL ) BaseType_t xPortStartScheduler( void ) { /* ①Configure PendSV and SysTick to have the lowest interrupt priority */ portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI; portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI; /* ②Start the first task and don't return */ prvStartFirstTask(); /* It shouldn't run here */ return 0; }
① Configure PendSV and SysTick to have the lowest interrupt priority. Both SysTick and PendSV involve system scheduling. The priority of system scheduling is lower than that of other hardware interrupts of the system, that is, the external hardware interrupts in the corresponding system are given priority. Therefore, the interrupt priority of SysTick and PendSV is configured as the lowest.
② Call the function prvStartFirstTask() to start the first task. If it is started successfully, it will not return. This function is written by the assembler in port C implementation.
Macro configKERNEL_INTERRUPT_PRIORITY needs to be in freertosconfig Defined in H.
#define configKERNEL_INTERRUPT_PRIORITY 255 / * the upper four digits are valid, which is equal to 0xff or 15*/
prvStartFirstTask() function
prvStartFirstTask() function is used to start the first task. It mainly does two actions: one is to update the MSP value, the other is to generate SVC system call, and then go to the interrupt service function of SVC to really switch to the first task.
/* * Refer to 4.4.3 of STM32F10xxx Cortex-M3 programming manual. Baidu can find this document by searching "PM0056" * In Cortex-M, the address range of SCB of kernel peripheral is 0xE000ED00-0xE000ED3F * 0xE000ED008 SCB in SCB peripheral_ The address of vtor register, which stores the starting address of the vector table, that is, the address of MSP */ __asm void prvStartFirstTask( void ) { PRESERVE8//① /* In Cortex-M, 0xE000ED08 is SCB_ Address of vtor register, ② Inside is the starting address of the vector table, that is, the address of MSP */ ldr r0, =0xE000ED08//③ ldr r0, [r0] //④ ldr r0, [r0] //⑤ /* ⑥Sets the value of the main stack pointer msp */ msr msp, r0 /* ⑦Enable global interrupt */ cpsie i cpsie f dsb isb /* ⑧Call SVC to start the first task */ svc 0 nop nop }
① The current stack needs to be aligned according to 8 bytes. If all operations are 32-bit, then 4 bytes can be aligned. In Cortex-M, floating-point operations are 8 bytes.
② In Cortex-M, 0xE000ED08 is SCB_ The address of vtor register, which stores the starting address of vector table, that is, the address of MSP. The vector table is usually stored from the starting address of internal FLASH, so it can be seen that the value of MSP is stored in memory: 0x00000000. This can be verified by checking the value of memory during simulation, as shown in the following figure
③ Load the immediate 0xE000ED08 into register R0.
④ Load the contents pointed to by the address 0xE000ED08 into the register R0. At this time, R0 is equal to SCB_ The value of vtor register is equal to 0x00000000, that is, the starting address of memory.
⑤ Load the content pointed to by the address 0x00000000 into R0. At this time, R0 is equal to 0x200008DB.
⑥ Store the value of R0 in MSP. At this time, MSP is equal to 0x200008DB, which is the stack top pointer of the main stack. The initial operation is a little redundant, because when the system starts, reset is executed_ Handler, the vector table has been initialized, and the MSP value has been updated to the starting value of the vector table, that is, the pointer to the top of the main stack.
⑦ Use the CPS instruction to turn on the global interrupt. In order to quickly switch interrupts, Cortex-M kernel has specially set up a CPS instruction, which has four uses:
CPSID I ;PRIMASK=1 ; // Off interrupt CPSIE I ;PRIMASK=0 ; // Open interrupt CPSID F ;FAULTMASK=1 ;// Guan anomaly CPSIE F ;FAULTMASK=0 ;// Abnormal opening
PRIMASK and faultmaster are two of the three interrupt mask registers in Cortex-M kernel, and the other is BASEPRI. See the following table for the detailed usage of these three registers.
name | Function description |
---|---|
PRIMASK | This is a single bit register. After it is set to 1, all maskable exceptions are turned off, leaving only NMI and hard FAULT to respond. Its default value is 0, indicating that there is no shutdown interrupt. |
FAULTMASK | This is a one bit register. When it is set to 1, only NMI can respond, and all other exceptions, even hard FAULT, are shut up. Its default value is also 0, indicating that there is no off exception. |
BASEPRI | This register has a maximum of 9 bits (determined by the number of bits expressing priority). It defines the threshold of the masked priority. When it is set to a certain value, all interrupts with priority number greater than or equal to this value are turned off (the higher the priority number, the lower the priority). However, if it is set to 0, no interrupt will be turned off, and 0 is also the default value. |
⑧ Generate a system call. The service number 0 indicates SVC interrupt. Next, the SVC interrupt service function will be executed. The SVC interrupt service function is in port C.
vPortSVCHandler() function
To successfully respond to SVC interrupt, its function name must be consistent with the name registered in the vector table. In the vector table of the startup file, the registered name of SVC interrupt service function is SVC_Handler, so the name of SVC interrupt service function should be written as SVC_Handler, but in FreeRTOS, the official version is vPortSVCHandler(). In order to respond to SVC interrupts smoothly, we have two options: change the registered function name of SVC in the interrupt vector table or change the interrupt service name of SVC in FreeRTOS. Here, we take the second method, that is, in freertosconfig Add the method of macro definition in H to modify. By the way, change the interrupt service function names of PendSV and SysTick to be consistent with the vector table, and configmax_ SYSCALL_ INTERRUPT_ The macros of priority are also defined together.
#define configMAX_SYSCALL_INTERRUPT_PRIORITY 191 / * the upper four bits are valid, i.e. equal to 0xb0 or 11*/ #define xPortPendSVHandler PendSV_Handler #define xPortSysTickHandler SysTick_Handler #define vPortSVCHandler SVC_Handler
The vPortSVCHandler() function starts the first task and does not return.
__asm void vPortSVCHandler( void ) { extern pxCurrentTCB;//① PRESERVE8 ldr r3, =pxCurrentTCB /* ②Load the address of pxCurrentTCB to r3 */ ldr r1, [r3] /* ③Load tcrx1 to tcrxb */ ldr r0, [r1] /* ④Load the value pointed to by pxCurrentTCB to r0. At present, the value of r0 is equal to the top of the first task stack */ ldmia r0!, {r4-r11} /* ⑤Take r0 as the base address, load the contents of the stack into r4~r11 registers, and r0 will be incremented at the same time */ msr psp, r0 /* ⑥Update the value of r0, that is, the stack pointer of the task, to psp */ isb mov r0, #0 / * ⑦ set the value of r0 to 0*/ msr basepri, r0 /* ⑧Set the value of basepri register to 0, that is, all interrupts are not masked */ orr r14, #0xd / * ⑨ before exiting from SVC interrupt service, press bit or up 0x0D to the last 4 bits of r14 register, Causes the hardware to use the process stack pointer when exiting PSP After completing the stack operation and returning, enter the thread mode and return Thumb state */ bx r14 /* ⑩When the exception returns, the rest of the stack will be automatically loaded into the CPU register: xPSR,PC(Task entry address), R14, R12, R3, R2, R1, R0 (formal parameters of the task) At the same time, the value of PSP will also be updated, that is, point to the top of the task stack */ }
① Declare the external variable pxCurrentTCB, which is a The global pointer defined in C is used to point to the task control block of the task currently running or about to run.
② Load the address of pxCurrentTCB to r3.
③ Load pxCurrentTCB to r3.
④ Load the task control block pointed to by pxCurrentTCB to r0. The first member of the task control block is the stack top pointer, so r0 is equal to the stack top pointer at this time. The stack space distribution of a task that has just been created and has not been run is shown in the following figure, that is, r0 is equal to pxTopOfStack in the figure.
⑤ Take r0 as the base address, load the contents of 8 words increased upward in the stack into the CPU registers r4~r11, and r0 will increase automatically at the same time.
⑥ Update the new stack top pointer r0 to psp. The stack pointer used during task execution is psp. At this time, the specific direction of psp is shown in.
⑦ Clear register r0 to 0.
⑧ Set the value of basepri register to 0, that is, open all interrupts. Basepri is an interrupt mask register. Interrupts greater than or equal to the value of this register will be masked.
⑨ When the task is completed by pressing the 0xr4 bit on the stack, the process returns to the front of the stack and returns to the PSC state by pressing the 0xr4 bit on the stack after the interrupt operation is completed. In SVC interrupt service, MSP stack pointer is used, which is in ARM state.
⑩ The exception returns. At this time, the PSP pointer is used to automatically load the rest of the stack into the CPU register: xPSR, PC (task entry address), R14, R12, R3, R2, R1, R0 (task formal parameters). At the same time, the value of PSP will also be updated, that is, point to the top of the task stack, as shown in the figure below.
Task switching
Task switching is to find the highest priority ready task in the ready list, and then execute the task. However, at present, we do not support priority, and only realize the switching of two tasks in turn.
taskYIELD()
/* In task Defined in H */ #define taskYIELD() portYIELD() /* In portmacro Defined in H */ /* Interrupt control status register: 0xe000ed04 * Bit 28 PENDSVSET: PendSV Suspension position */ #define portNVIC_INT_CTRL_REG (*(( volatile uint32_t *)␣ ,→ 0xe000ed04)) #define portNVIC_PENDSVSET_BIT ( 1UL << 28UL ) #define portSY_FULL_READ_WRITE ( 15 ) #define portYIELD() { /* Trigger PendSV to generate context switching */ portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;//① __dsb( portSY_FULL_READ_WRITE ); __isb( portSY_FULL_READ_WRITE ); }
① The implementation of portYIELD is very simple. In fact, it is to suspend the PendSV at position 1. When there are no other interrupts, respond to the PendSV interrupt and execute the PendSV interrupt service function we have written to realize task switching.
xPortPendSVHandler() function
PendSV interrupt service function is the real place to realize task switching
__asm void xPortPendSVHandler( void ) { extern pxCurrentTCB;//① extern vTaskSwitchContext;//② PRESERVE8//③ /* When entering PendSVC Handler, the environment in which the last task was run is: xPSR,PC(Task entry address), R14, R12, R3, R2, R1, R0 (formal parameters of the task) The values of these CPU registers will be automatically saved to the task stack, and the rest r4~r11 need to be saved manually */ /* Get the task stack pointer to r0 */ mrs r0, psp//④ isb ldr r3, =pxCurrentTCB /* ⑤Load the address of pxCurrentTCB to r3 */ ldr r2, [r3] /* ⑥Load pxCurrentTCB to r2 */ stmdb r0!, {r4-r11} /* ⑦Store the values of CPU registers r4~r11 to the address pointed to by r0 */ str r0, [r2] /* ⑧Store the new stack top pointer of the task stack to the first member of the current task TCB, that is, the stack top pointer */ stmdb sp!, {r3, r14} /* ⑨Temporarily push R3 and R14 onto the stack because the function vTaskSwitchContext will be called, When calling the function, the return address is automatically saved in R14, so once the call occurs, the value of R14 will be overwritten, so it needs to be protected on the stack; R3 The saved TCB pointer (pxCurrentTCB) address of the currently active task will be used after the function call, so it should also be protected on the stack */ mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY / * ⑩ enter critical section*/ msr basepri, r0//(11) dsb isb bl vTaskSwitchContext /* (12)Call the function vTaskSwitchContext to find a new task to run, and realize task switching by pointing the variable pxCurrentTCB to the new task */ mov r0, #0 / * (13) exit critical section*/ msr basepri, r0 ldmia sp!, {r3, r14} /* (14)Restore r3 and r14 */ ldr r1, [r3]//(15) ldr r0, [r1] /* (16)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*/ ldmia r0!, {r4-r11} /* (17)Out of stack */ msr psp, r0//(18) isb bx r14 /* (19)When an exception occurs, R14 stores the exception return flag, including whether to enter thread mode or processor mode after return Whether PSP stack pointer or MSP stack pointer is used, when the bx r14 instruction is called, the hardware will know to return from the exception, And then out of the stack. At this time, the stack pointer PSP has pointed to the correct position of the new task stack, When the running address of the new task is out of the stack to the PC register, the new task will also be executed.*/ nop }
① Declare the external variable pxCurrentTCB, which is a The global pointer defined in C is used to point to the task control block of the task currently running or about to run.
② Declare the external function vTaskSwitchContext, which will be used later.
③ The current stack needs to be aligned according to 8 bytes. If all operations are 32-bit, then 4 bytes can be aligned. In Cortex-M, floating-point operations are 8 bytes.
④ Store the value of PSP in R0. When entering the PendSVC Handler, the environment in which the last task was running, i.e. xPSR, PC (task entry address), R14, R12, R3, R2, R1, R0 (formal parameters of the task), the values of these CPU registers will be automatically stored in the task stack, the remaining r4~r11 need to be saved manually, and the PSP will be automatically updated (the PSP points to the top of the task stack before updating), At this time, the specific direction of PSP is shown in the figure
⑤ Load the address of pxCurrentTCB to r3.
⑥ Load the content pointed to by r3 to r2, that is, r2 is equal to pxCurrentTCB.
⑦ Take r0 as the base address (the pointer decreases first and then operates. The DB of STMDB represents DecreaseBefor), store the value of CPU registers r4~r11 in the task stack, and update the value of r0. At this time, the direction of r0 is as shown in the figure.
⑧ Store the value of r0 in the content pointed to by r2, which is equal to pxCurrentTCB. Specifically, store the value of r0 into the stack top pointer pxTopOfStack of the previous task. The specific direction is the same as that of r0 in Figure 5 ‑ 11. At this point, the above saving in context switching is completed.
⑨ Temporarily push R3 and R14 onto the stack (in the whole system, the interrupt uses the main stack and the stack pointer uses MSP), because the function vTaskSwitchContext is called next. When calling the function, the return address is automatically saved in R14, so once the call occurs, The value of R14 will be overwritten (after the PendSV interrupt service function is executed, when returning, it needs to decide whether to return to processor mode or task mode according to the value of R14, and whether to use PSP or MSP when leaving the stack). Therefore, it needs to be protected in the stack. R3 stores the address of the TCB pointer (pxCurrentTCB) of the currently running task (precisely, because it is about to switch to a new task). After the function call, the value of pxCurrentTCB will be updated. Later, we need to operate pxCurrentTCB through R3, However, when running the function vTaskSwitchContext, it is uncertain whether R3 register will be used as the intermediate variable, so R3 is also protected on the stack for the sake of insurance.
⑩ Set configmax_ SYSCALL_ INTERRUPT_ The value of priority is stored in r0, which is in freertosconfig H, which is used to configure the value of the interrupt mask register BASEPRI. The upper four bits are valid. The current configuration is 191. Because the upper four bits are valid, the actual value is equal to 11, that is, interrupts with priority higher than or equal to 11 will be masked. In terms of off interrupts, FreeRTOS is different from other RTOS off interrupts. Instead, FreeRTOS operates the BASEPRI register to reserve some interrupts, which is not like μ Operate PRIMASK directly like C/OS or RT thread to close all interrupts (except hard FAULT).
(11) Turn off the interrupt and enter the critical section, because the value of the global pointer pxCurrentTCB will be updated next.
(12) Call the function vTaskSwitchContext. This function is in task C, there is only one role. Select the task with the highest priority, and then update pxCurrentTCB. At present, we do not support priority, so we can manually switch between task 1 and task 2. The specific implementation of this function is in task c.
vTaskSwitchContext() function
void vTaskSwitchContext( void ) { /* The two tasks switch in turn */ if( pxCurrentTCB == &Task1TCB ) { pxCurrentTCB = &Task2TCB; } else { pxCurrentTCB = &Task1TCB; } }
(13) Exit the critical section, interrupt and write 0 directly to BASEPRI.
(14) Recover the values of registers r3 and r14 from the main stack. At this time, sp uses MSP.
(15) Load the content pointed to by r3 to r1. r3 stores the address of pxCurrentTCB, that is, make r1 equal to pxCurrentTCB. pxCurrentTCB is updated in the vTaskSwitchContext function above, pointing to the TCB of the next task to be run.
(16) Load the content pointed to by r1 to r0, that is, the stack top pointer of the next task to be run.
(17) Take r0 as the base address (take the value first and then increment the pointer. IA of LDMIA represents IncreaseAfter), and load the contents of the task stack of the next task to be run into the CPU registers r4~r11.
(18) Update the value of psp. In case of abnormal exit, psp will be used as the base address to automatically load the rest of the task stack into the CPU register.
(19) When an exception occurs, R14 stores the exception return flag, including whether to enter task mode or processor mode after return, and whether to use PSP stack pointer or MSP stack pointer. R14 at this time is equal to 0xfffffd, which most indicates that the exception returns and enters the task mode. SP takes PSP as the stack pointer to exit the stack. After exiting the stack, PSP points to the top of the task stack. After calling the bxr14 instruction, the system takes PSP as the SP pointer out of the stack and loads the rest of the task stack of the new task to be run next into the CPU registers: R0 (task parameter), R1, R2, R3, R12, R14 (LR), R15 (PC) and xPSR, so as to switch to the new task.
main function
The creation of the task, the implementation of the ready list and the implementation of the scheduler have been completed. Now we put all the test code into main C inside.
/* ************************************************************************* * Included header file ************************************************************************* */ #include "FreeRTOS.h" #include "task.h" /* ************************************************************************* * global variable ************************************************************************* */ portCHAR flag1; portCHAR flag2; extern List_t pxReadyTasksLists[ configMAX_PRIORITIES ]; /* ************************************************************************* * Task control block & Stack ************************************************************************* */ TaskHandle_t Task1_Handle; #define TASK1_STACK_SIZE 20 StackType_t Task1Stack[TASK1_STACK_SIZE]; TCB_t Task1TCB; TaskHandle_t Task2_Handle; #define TASK2_STACK_SIZE 20 StackType_t Task2Stack[TASK2_STACK_SIZE]; TCB_t Task2TCB; /* ************************************************************************* * Function declaration ************************************************************************* */ void delay (uint32_t count); void Task1_Entry( void *p_arg ); void Task2_Entry( void *p_arg ); /* ************************************************************************ * main function ************************************************************************ */ int main(void) { /* Initialize task related lists, such as the ready list */ prvInitialiseTaskLists(); /* Create task */ Task1_Handle = xTaskCreateStatic( ( TaskFunction_t )Task1_Entry, (char *)"Task1", (uint32_t)TASK1_STACK_SIZE, (void *)NULL, (StackType_t *)Task1Stack, (TCB_t *)&Task1TCB); /* Add task to ready list */ vListInsertEnd( &( pxReadyTasksLists[1]), &( ((TCB_t *)(&Task1TCB))->xStateListItem ) ); Task2_Handle = xTaskCreateStatic( ( TaskFunction_t )Task2_Entry, (char *)"Task2", (uint32_t)TASK2_STACK_SIZE, (void *)NULL, (StackType_t *)Task2Stack, (TCB_t *)&Task2TCB); /* Add task to ready list */ vListInsertEnd( &( pxReadyTasksLists[2]), &( ((TCB_t *)(&Task2TCB))->xStateListItem ) ); /* Start the scheduler and start multi task scheduling. If it is started successfully, it will not be returned */ vTaskStartScheduler(); for(;;) { /*Don't do anything*/ } } /* ************************************************************************* * Function implementation ************************************************************************* */ /* Software delay */ void delay (uint32_t count) { for(; count!=0; count--); } /* Task 1 */ void Task1_Entry( void *p_arg ) { for( ;; ) { flag1 = 1; delay( 100 ); flag1 = 0; delay( 100 ); /* Task switching, here is manual switching */ taskYIELD(); } } /* Task 2 */ void Task2_Entry( void *p_arg ) { for( ;; ) { flag2 = 1; delay( 100 ); flag2 = 0; delay( 100 ); /* Task switching, here is manual switching */ taskYIELD(); } }
Because priority is not supported at present, the task switching function taskYIELD() is actively called after each task is executed to realize task switching.
Experimental phenomenon
Software debugging and simulation, specific process:
- Press the Debug button to enter the debugging interface
- The logic analyzer button calls up the logic analyzer
- The variable to be observed is added to the logic analyzer, and the variable is set to Bit mode. The default is Analog
- Click the full speed operation button to see the waveform, and the In Out All in the Zoom column can Zoom in and out the waveform
It is not enough to read the contents of this section and then simulate to see the waveform. It should be to add the current task control block pointer pxCurrentTCB, ready list pxreadytask lists, control block of each task and task stack to the observation window, and then execute the program step by step to see how these variables change. In particular, how do the CPU register, task stack and PSP change during task switching? Let the machine execute the code in its own mind.
Explanation of assembly instructions involved in this section
Some functions in this section are written in assembly. Refer to the following table for the ARM assembly instructions involved
Instruction name function | effect |
---|---|
EQU | Take a symbolic name for the numeric constant, which is equivalent to define in C language |
AREA | Assemble a new code segment or data segment |
SPACE | Allocate memory space |
PRESERVE8 | The current file stack needs to be aligned according to 8 bytes |
EXPORT | Declare that a label has global attributes and can be used by external files |
DCD | Allocating memory in words requires 4-byte alignment and requires initialization of these memories |
PROC | Define the subroutine, which is used in pairs with end to indicate the end of the subroutine |
WEAK | Weak definition: if the external file declares a label, the label defined by the external file will be used first. If the external file is not defined, there will be no error. It should be noted that this is not an ARM instruction, but a compiler instruction. It is put together here for convenience. |
IMPORT | The declaration label comes from an external file, which is similar to the EX-TERN keyword in C language |
B | Jump to a label |
ALIGN | When the compiler aligns the storage address of instructions or data, it usually needs to be aligned with an immediate number, which means 4-byte alignment by default. It should be noted that this is not an ARM instruction, but a compiler instruction. It is put together here for convenience. |
END | End of file reached |
IF,ELSE,ENDIF | Assemble conditional branch statements, similar to if else in C language |
MRS | Load the value of the special function register into the general register |
MSR | Store the value of the general register to the special function register |
CBZ | Compare and transfer if the result is 0 |
CBNZ | Comparison, if the result is not 0, it will be transferred |
LDR | Load words from memory into a register |
LDR [pseudo instruction] plus | An immediate value or an address value to a register |
LDRH | Load a half word from memory into a register |
LDRB | Load bytes from memory into a register |
STR | Store a register in memory by word |
STRH | Store the lower half word of a register in memory |
STRB | Store the low byte of a register in memory |
ORR | Bitwise OR |
BX | Jump directly to the address given by the register |
BL | Jump to the address corresponding to the label, and save the address of the next instruction before jump to LR |
BLX | Jump to the address given by the register REG, switch the processor state according to the LSB of REG, and save the address of the next instruction before transfer to LR. ARM(LSB=0),Thumb(LSB=1). If CM3 only runs in thumb, you must ensure that the LSB of REG = 1, otherwise a fault will be called |
Reference: Reference: FreeRTOS Kernel Implementation and application development practice - based on RT1052