2021-07-16 22:23:32 Friday |
1, Bare metal system and multitasking system
Bare metal system
- polling systems
During bare metal programming, first initialize the relevant hardware, and then let the main program cycle continuously in a dead cycle to do all kinds of things sequentially, with poor real-time performance. - Front and rear console system
Interrupt is added to the polling system. The response of external events is completed in the interrupt, and the event processing is still completed in the polling system. If the event to be processed is very short, it can be processed in the interrupt service program.
The interrupt is called the foreground, and the infinite loop in the main function is called the background.
The event response and processing are separated, but the event processing is executed sequentially in the background. Compared with the polling system, the front and rear systems ensure that the events will not be lost. In addition, the interrupt has the function of nesting, which greatly improves the real-time response ability of the program.
Multitasking system
Compared with the front and back systems, the event response of multi task system is also completed in interrupt, but the event processing is completed in task. In a multitasking system, tasks, like interrupts, also have priority, and tasks with high priority will be executed first.
When an emergency event is marked with an interrupt, if the task priority corresponding to the event is high enough, it will be responded immediately. Compared with the front and rear systems, the real-time performance of multi task system is improved.
What is a mission?
Compared with the program subjects executed in the background in the front and back system, in the multi task system, the program subjects are divided into independent, infinite loop and non returnable small programs according to the function of the program. This small program is called task.
Tasks are scheduled and managed by the operating system.
Differences among polling, front and back stations and multitasking systems
2021-07-20 01:01:16 Tuesday |
2, Task definition and implementation of task switching
Learn how to create tasks and how to switch tasks (task switching is completed by assembly code)
1. Create task
1.1. Define task stack
When the system is running, how are global variables, local variables when sub functions are called, function return addresses when interrupts occur and other environmental parameters stored?
In the bare metal system, it is placed in the stack (the stack is a continuous memory space in the MCU RAM). The size of the stack is configured by the code in the startup file, and finally by the C library function_ main to initialize.
When the bare metal system needs to use the stack, you can find a free space in the stack, but the multitasking system can't.
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. (stack space is usually a predefined global array) the largest stack that can be used is Stack_Size decision.
In a multitasking system, the task stack is to allocate independent rooms in a unified stack space, and each task can only use its own room.
The task stack is actually a predefined global data, and the data type is CPU_STK
static CPU_STK Task1Stk[TASK1_STK_SIZE]; static CPU_STK Task2Stk[TASK2_STK_SIZE];
Supplement:
volatile keyword
Volatile is meant to be "Volatile". Because accessing registers is much faster than accessing memory units, the compiler will generally optimize to reduce memory access, but it may read dirty data. When a variable value is required to be declared using volatile, the system always reads data from its memory again, even if its previous instruction has just read data from there. Specifically, when a variable declared by this keyword is encountered, the compiler will no longer optimize the code accessing the variable, so as to provide stable access to special addresses; If valatile is not used, the compiler optimizes the declared statement. (to put it succinctly, volatile keyword affects the compiler compilation results. The variable declared by volatile indicates that the variable may change at any time. For the operations related to the variable, do not compile and optimize to avoid errors)
1.2. Define task function
Task is an independent function. The function body loops indefinitely and cannot return.
1.3. Define task control block TCB
As mentioned earlier, the execution of tasks in a multitasking system is scheduled by the system. In order to schedule tasks smoothly, the system defines an additional Task control block (TCB) for each Task, which is equivalent to the ID card of the Task. It contains all the information of the Task, such as Task stack, Task name, Task parameters, etc.
All system operations on tasks can be realized through this TCB, which is a new data type.
/* Task control block data type declaration */ struct os_tcb { CPU_STK *StkPtr;//Stack pointer CPU_STK_SIZE StkSize;//Stack size };
//Task TCB * * definition** static OS_TCB Task1TCB; static OS_TCB Task2TCB;
1.4. Implement task creation function
The task stack, the function entity of the task, and the TCB of the task finally need to be linked to be uniformly scheduled by the system.
This connection is implemented by the task creation function OSTaskCreate.
void OSTaskCreate ( OS_TCB *p_tcb,//Task control block pointer OS_TASK_PTR p_task,//Task function name void *p_arg,//Task parameters, used to pass task parameters CPU_STK *p_stk_base, //Point to the starting address of the task stack CPU_STK_SIZE stk_size, //Indicates the size of the task stack OS_ERR *p_err) //Used to store error codes. { CPU_STK *p_sp; //Task stack initialization function. When the task runs for the first time, the parameters loaded into the CPU register are placed in the task stack. When the task is created, the stack is initialized in advance. p_sp = OSTaskStkInit (p_task,//Task name, indicating the entry address of the task. Load to PC register R15 during task switching p_arg,//Formal parameters of the task. Load to register R0 during task switching p_stk_base,//Start address of task stack stk_size);//Size of task stack p_tcb->StkPtr = p_sp; p_tcb->StkSize = stk_size; *p_err = OS_ERR_NONE; }
After the task is created, it should be added to the ready list, indicating that the task is ready and the system can schedule it at any time.
typedef struct os_rdy_list OS_RDY_LIST; struct os_rdy_list { OS_TCB *HeadPtr; OS_TCB *TailPtr; }; OS_EXT OS_RDY_LIST OSRdyList[OS_CFG_PRIO_MAX]; /* Add task to ready list */ OSRdyList[0].HeadPtr = &Task1TCB; OSRdyList[1].HeadPtr = &Task2TCB;
2. OS system initialization
OS system initialization is generally done after hardware initialization, as long as the defined global variables are initialized.
void OSInit (OS_ERR *p_err) { OSRunning = OS_STATE_OS_STOPPED;//Operation status of the system OSTCBCurPtr = (OS_TCB *)0;//The system pointer to the currently running TCB. OSTCBHighRdyPtr = (OS_TCB *)0;//Point to the TCB of the task with the highest priority in the ready task OS_RdyListInit();//Initialize the global variable OSRdyList [], that is, initialize the sequence table. *p_err = OS_ERR_NONE;//Running the code here means there are no errors. }
void OS_RdyListInit(void) { OS_PRIO i; OS_RDY_LIST *p_rdy_list; for ( i=0u; i<OS_CFG_PRIO_MAX; i++ ) { p_rdy_list = &OSRdyList[i]; p_rdy_list->HeadPtr = (OS_TCB *)0; p_rdy_list->TailPtr = (OS_TCB *)0; } }
3. Start the system
After the task is created and the system is initialized, start the system.
void OSStart (OS_ERR *p_err) { if ( OSRunning == OS_STATE_OS_STOPPED ) { /* Manually configure task 1 to run first */ OSTCBHighRdyPtr = OSRdyList[0].HeadPtr; /* Start task switching and will not return. It is written in assembly language*/ OSStartHighRdy(); /* It will not run here. Running here indicates that a fatal error has occurred */ *p_err = OS_ERR_FATAL_RETURN; } else { *p_err = OS_STATE_OS_RUNNING; } }
4. Task switching
After calling the OSStartHighRdy() function to trigger the PendSV exception, you need to write the PendSV exception service function, and then switch tasks in it.
PendSV exception service mainly completes two tasks: one is to save the above, that is, to save the environment parameters of the currently running task; The second is to switch the following, that is, load the environment parameters of the next task to be run from the task stack to the CPU register, so as to realize the task switching.
5. main() function
int main(void) { OS_ERR err; /* Initialize related global variables */ OSInit(&err); /* Create task */ OSTaskCreate ((OS_TCB*) &Task1TCB, (OS_TASK_PTR ) Task1, (void *) 0, (CPU_STK*) &Task1Stk[0], (CPU_STK_SIZE) TASK1_STK_SIZE, (OS_ERR *) &err); OSTaskCreate ((OS_TCB*) &Task2TCB, (OS_TASK_PTR ) Task2, (void *) 0, (CPU_STK*) &Task2Stk[0], (CPU_STK_SIZE) TASK2_STK_SIZE, (OS_ERR *) &err); /* Add task to ready list */ OSRdyList[0].HeadPtr = &Task1TCB; OSRdyList[1].HeadPtr = &Task2TCB; /* Start the OS and will not return */ OSStart(&err); }
Task functions Task1 and Task2 do not really switch automatically. Instead, the OSSched() function is added to their respective function bodies to realize manual switching.
/* Task switching actually triggers a PendSV exception, and then performs context switching in the PendSV exception */ void OSSched (void) { if ( OSTCBCurPtr == OSRdyList[0].HeadPtr ) { OSTCBHighRdyPtr = OSRdyList[1].HeadPtr; } else { OSTCBHighRdyPtr = OSRdyList[0].HeadPtr; } OS_TASK_SW(); }
The scheduling algorithm of the OSSched() function is very simple, that is, if the current task is task 1, the next task is task 2. If the current task is task 2, the next task is task 1.
Then call the OS_TASK_SW() function triggers PendSV exception, and then implements task switching in PendSV exception.
Tuesday, July 20, 2021, 19:56:48 |
3, Task time slice run
On the basis of the previous code example, SysTick interrupt is added to switch tasks in the SysTick interrupt service function, so as to realize the time slice operation of dual tasks, that is, the running time of each task is the same.
1. What is SysTick?
RTOS needs a time base to drive, and the frequency of system task scheduling is equal to that of the time base. Usually, the time base is provided by a timer and can also be obtained from other periodic signal sources.
There is a system timer SysTick in Cortex-M kernel, which is embedded in NVIC. It is a 24 bit decreasing counter. The time of each count is 1/SYSCLK.
When the value of the reload value register decreases to 0, the system timer generates an interrupt to cycle back and forth.
SysTick is the most suitable timer to provide time base for the operating system and maintain the system heartbeat.
2. How to use SysTick?
Only one initialization function is required.
void OS_CPU_SysTickInit (CPU_INT32U ms) { /* Sets the value of the reload register */ SysTick->LOAD = ms * SystemCoreClock / 1000 - 1; /* Configure interrupt priority as the lowest */ NVIC_SetPriority (SysTick_IRQn, (1<<__NVIC_PRIO_BITS) - 1); /* Resets the value of the current counter */ SysTick->VAL = 0; /* Select clock source, enable interrupt, enable counter */ SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk | SysTick_CTRL_TICKINT_Msk | SysTick_CTRL_ENABLE_Msk; }
3. Write SysTick interrupt service function
void SysTick_Handler(void) { OSTimeTick(); }
void OSTimeTick (void) { /* Task scheduling, like the previous code example, does not need to be modified */ OSSched(); }
4. main() function
Compared with the previous main(), only SysTick related content is added.
int main(void) { OS_ERR err; /* Close interrupt */ CPU_IntDis();//(1) Why turn off interrupts? /* Configure systick to interrupt once every 10ms */ OS_CPU_SysTickInit (10);//(2) The task scheduling is completed in the interrupt service function of SysTick /* Initialize related global variables */ OSInit(&err); /* Create task */ OSTaskCreate ((OS_TCB*) &Task1TCB, (OS_TASK_PTR ) Task1, (void *) 0, (CPU_STK*) &Task1Stk[0], (CPU_STK_SIZE) TASK1_STK_SIZE, (OS_ERR *) &err); OSTaskCreate ((OS_TCB*) &Task2TCB, (OS_TASK_PTR ) Task2, (void *) 0, (CPU_STK*) &Task2Stk[0], (CPU_STK_SIZE) TASK2_STK_SIZE, (OS_ERR *) &err); /* Add task to ready list */ OSRdyList[0].HeadPtr = &Task1TCB; OSRdyList[1].HeadPtr = &Task2TCB; /* Start the OS and will not return */ OSStart(&err); }
(1) Before OS system initialization, SysTick timer is enabled to generate 10ms interrupt, which triggers task scheduling,
If we don't turn off the interrupt at the beginning, we will enter SysTick interrupt before the OS is started, and then task scheduling will occur,
Since the OS has not been started, scheduling is not allowed, so close the interrupt first.
After the system starts, the interrupt is restarted by OSStartHighRdy() in the OSStart() function.
In this code example, the task scheduling will no longer be implemented in their respective tasks, but will be put into the SysTick interrupt service function,
Thus, each task can run the same time slice, occupy the CPU in turn and enjoy the CPU equally.
In the previous code, the two tasks also occupy the CPU in turn and enjoy the same time slice, which is the time of a single task run.
The difference is that in this code, the time slice of the task is equal to the time base of the SysTick timer, which is the synthesis of the single running time of many tasks,
That is, the task runs many times in this time slice.
Wednesday, July 21, 2021, 10:17:05 |
4, Blocking delay and idle tasks
In the previous example, the software delay is used for the delay in the task body, that is, the CPU is left idle to achieve the delay effect. In order to drain the performance of the CPU and try not to let it idle, blocking delay is introduced here.
What is blocking delay?
When the task needs to be delayed, the task will give up the right to use the CPU, and the CPU can do other things. When the task delay time expires, the CPU right will be regained, and the task will continue to run, making full use of the CPU resources.
When the task needs to be delayed and enters the blocking state, if the CPU has no other tasks to run, the system will create an idle task for the CPU. At this time, the CPU will run the idle task.
What are idle tasks?
Idle task is the task with the lowest priority created by the system during initialization. The main body of idle task is very simple, just counting a global variable.
In practical application, when the system enters the idle task, the single chip microcomputer can enter sleep or low power consumption in the idle task.
1. Implement idle tasks
1.1. Define idle task stack
CPU_STK OSCfg_IdleTaskStk[OS_CFG_IDLE_TASK_STK_SIZE];
/* Starting address of idle task stack */ CPU_STK * const OSCfg_IdleTaskStkBasePtr = (CPU_STK *)&OSCfg_IdleTaskStk[0]; /* Idle task stack size */ CPU_STK_SIZE const OSCfg_IdleTaskStkSize = (CPU_STK_SIZE)OS_CFG_IDLE_TASK_STK_SIZE; //The starting address and size of the stack of idle tasks are defined as a constant and cannot be modified.
1.2. Define idle task TCB
/* Idle task TCB */ OS_EXT OS_TCB OSIdleTaskTCB;
1.3. Define idle task function
/* Idle task */ void OS_IdleTask (void *p_arg) { p_arg = p_arg; /* Idle tasks do nothing but operate on the global variable OSIdleTaskCtr + + */ for (;;) { OSIdleTaskCtr++;//Idle task count variable } }
1.4. Idle task initialization
Idle tasks are completed during system initialization, which means that idle tasks have been created before the system is started.
void OSInit (OS_ERR *p_err) { /* Configure the initial status of the OS to stop */ OSRunning = OS_STATE_OS_STOPPED; /* Initialize two global TCB S, which are used for task switching */ OSTCBCurPtr = (OS_TCB *)0; OSTCBHighRdyPtr = (OS_TCB *)0; /* Initialize ready list */ OS_RdyListInit(); /* Initialize idle tasks */ OS_IdleTaskInit(p_err);(1) if (*p_err != OS_ERR_NONE) { return; } } /* Idle task initialization */ void OS_IdleTaskInit(OS_ERR *p_err) { /* Initialize idle task counters */ OSIdleTaskCtr = (OS_IDLE_CTR)0;(2) /* Create idle task */ OSTaskCreate( (OS_TCB *)&OSIdleTaskTCB,(3) (OS_TASK_PTR )OS_IdleTask, (void *)0, (CPU_STK *)OSCfg_IdleTaskStkBasePtr, (CPU_STK_SIZE)OSCfg_IdleTaskStkSize, (OS_ERR *)p_err ); }
2. Implement blocking delay
Blocking of blocking delay means that after the task calls the delay function, the task will be stripped of CPU usage, and then enter the blocking state. The task can not continue to run until the delay ends and the task regains CPU usage. During the period when the task is blocked, the CPU can execute other tasks. If other tasks are also in the delayed state, the CPU will run idle tasks.
/* Blocking delay */ void OSTimeDly(OS_TICK dly) { /* Set delay time */ OSTCBCurPtr->TaskDelayTicks = dly;//TaskDelayTicks is a member of the task control block. It is used to record the time that the task needs to be delayed. The unit is the interrupt cycle of SysTick. /* Task scheduling */ OSSched(); }
struct os_tcb { CPU_STK *StkPtr; CPU_STK_SIZE StkSize; /* Number of task delay cycles */ OS_TICK TaskDelayTicks; };
void OSSched(void) { /* If the current task is an idle task, try to execute task 1 or task 2, See if their delay time ends. If the delay time of the task does not expire, Then go back and continue the idle task */ if ( OSTCBCurPtr == &OSIdleTaskTCB ) { if (OSRdyList[0].HeadPtr->TaskDelayTicks == 0) { OSTCBHighRdyPtr = OSRdyList[0].HeadPtr; } else if (OSRdyList[1].HeadPtr->TaskDelayTicks == 0) { OSTCBHighRdyPtr = OSRdyList[1].HeadPtr; } else { /* If the task delay has not expired, return to continue to execute the idle task */ return; } } else { /*If it is task1 or task2, check another task, If another task is not in delay, switch to the task Otherwise, judge whether the current task should enter the delay state, If so, switch to idle tasks. Otherwise, no switching will be performed */ if (OSTCBCurPtr == OSRdyList[0].HeadPtr) { if (OSRdyList[1].HeadPtr->TaskDelayTicks == 0) { OSTCBHighRdyPtr = OSRdyList[1].HeadPtr; } else if (OSTCBCurPtr->TaskDelayTicks != 0) { OSTCBHighRdyPtr = &OSIdleTaskTCB; } else { /* Return and do not switch because both tasks are in delay */ return; } } else if (OSTCBCurPtr == OSRdyList[1].HeadPtr) { if (OSRdyList[0].HeadPtr->TaskDelayTicks == 0) { OSTCBHighRdyPtr = OSRdyList[0].HeadPtr; } else if (OSTCBCurPtr->TaskDelayTicks != 0) { OSTCBHighRdyPtr = &OSIdleTaskTCB; } else { /* Return and do not switch because both tasks are in delay */ return; } } } /* Task switching triggers PendSV exception.*/ OS_TASK_SW(); }
3. main() function
There is little change from the previous experimental code.
- The initialization function of the idle task is called in OSInint, and the idle task is created before the system starts.
- The delay function in the task is replaced by blocking delay. The delay time is two SysTick interrupt cycles, i.e. 20ms.