A little bit about multitasking
Anyone who day after day, or occasionally, is engaged in programming microcontrollers, will sooner or later be confronted with the question: should a multi-tasking operating system be used? They are offered quite a lot in the network, with a lot of them - free (or almost free). Just choose.
Such doubts arise when a project comes across in which the microcontroller must simultaneously perform several different actions. Some of them are not related to others, and the rest, by contrast, can not be without each other. In addition, those and others may be too much. What is “too much” depends on who will evaluate or on who will carry out the development. Well, if it is the same person.
Rather, it is not a question of quantity, but a question of the qualitative difference in tasks with respect to the speed of execution, or some other requirements. Such thoughts may arise, for example, when the project requires regular monitoring of the supply voltage (is it missing?), Quite often read and save the values ​​of the input values ​​(they do not give rest), occasionally monitor the temperature and control the fan (there is nothing to breathe), check their watch with someone you trust (you are good at commanding there), keep in touch with the operator (try not to irritate him), check the checksum of the program's permanent memory for dementia (when turned on, or once a week, or in the morning).
Such heterogeneous tasks can be quite intelligently and successfully programmed, relying on one background task and on timer interrupts. In the handler of these interrupts, each time one of the “pieces” of the next task is executed. Depending on the importance, urgency or similar considerations, these challenges for some tasks are repeated often, and for others - rarely. And yet, we must take care that each task does a short part of the work, then prepare for the next small portion of work, and so on. Such an approach, if used, does not seem too complicated. Disadvantages arise when a project needs to be extended. Or, for example, suddenly pass to another. It should be noted that the second is often more difficult and without any pseudo-many-problem.
And what if you use a ready-made operating system for microcontrollers? Undoubtedly, many do. This is a good option. But the author of these lines, so far, has stopped and continues to stop the idea that it will be necessary to understand this, having spent a lot of time, to choose from what has been obtained and to use only what is really required from it. And do all this, mind you, digging in someone else's code! And there is no certainty that in six months it will not have to be repeated, for it will be forgotten.
In other words, why do you need a full garage of tools and accessories if a bicycle is stored and used there?
Therefore, there was a desire to make a simple “switch” of tasks only for Cortex-M4 (well, maybe even for M3 and M7). But the good old desire not to strain too much is not lost.
So, we do the simplest. A small number of tasks divide the execution time equally. As in Figure 1 below, four tasks do this. Let the zero of them be main, since it is difficult to imagine something else.
By working in this way, they are guaranteed to receive their slot or period of time (tick) and are not at all obliged to know about the existence of other tasks. Each task after exactly 3 ticks will again be able to do something.
But, on the other hand, if some of the tasks need to wait for an external event, for example, pressing a button, then it will stupidly waste precious time of our microcontroller. We can not agree with that. And our toad (conscience) - too. Need to do something.
And let the task, if it has nothing to do yet, give the time left from the tick to its comrades, who, for sure, are plowing with all their might.
In other words, sharing is necessary. Let task 2 do just that, as in Figure 2.
And why wouldn't our main task, too, main, be allowed to give the remaining time if we still have to wait? Let's allow. As shown in Figure 3.
And if it is known that some of the tasks will not soon need to check something again or just work? And she could allow herself some sleep, and instead would be aimlessly wasting time and getting under her feet. Not order, it must be corrected. Let task 3 skip one piece of its time (or a thousand). As shown in Figure 4.
Well, as we see, we have a fair coexistence of tasks or something like that. We must now force our individual tasks to behave as they are prescribed. And if we try to value time, then it is worth remembering about a low-level language (I’m not afraid of this word - assembler) and not to trust the compiler completely from any language, high level or very high one. We are, deep down, resolutely against any kind of dependence. Besides, life is simplified by the fact that we need not any assembler, but only from Cortex-M4.
For the stack, select one common area of ​​RAM that will be filled down, that is, in the direction of decreasing memory addresses. Why? Just because it does not work differently. This important area is divided mentally into equal sections by the number of the stated maximum number of our tasks. Figure 5 shows this for four tasks.
Next, select the place where we will store copies of the stack pointers for each task. Now, by the interrupt from the timer, which we take for the system one, we save all the registers of the current task in its stack area (the SP register is pointing there now), then we save its stack pointer in a special place (we save its value), we pull out the stack pointer of the next task ( write the new value to the register SP) from our special place and restore all its registers. Their copies are now indicated by the SP register of our next task. Well, and we quit interruption, of course. In this case, the entire context of the next task in the registers appears.
It would probably be superfluous to say that the next one after task3 will be main. But not superfluous, of course, it will be recalled that the SysTick timer and a special interruption from it are already provided for in the Cortex-M4, and many microcontroller manufacturers know about it. We will use it and this interruption as intended.
In order to start this system timer, as well as to make all the necessary preparations and checks, you must use the procedure designed for this.
U8 main_start_task_switcher(void);
This routine returns 0 if all checks passed or an error code if something went wrong. It is basically checked whether the stack is correctly aligned and whether there is enough space for it, and all our special places are filled with initial values. In short, boredom.
If someone wants to look at the text of the program, then at the end of the story he can easily do it, for example, through personal mail.
Yes, I completely forgot, when we take the registers of the next task from storage for the first time in her life, it is necessary that they get meaningful original values. And since, she will pick them up from her section of the stack, we need to put them in advance and move her stack pointer so that it is convenient to take. For this we need a procedure
U8 task_run_and_return_task_number(U32 taskAddress);
We give this subroutine the 32-bit address of the beginning of our task, which we want to run. And she (the subroutine) tells us the task number, which turned out in a special general table, or 0, if there was no place in the table. Then we can start another task, then another, and so on, even though all three are in addition to our never-shut down main task. She will never give up her zero number to anyone.
A few words about priorities. The main priority was and remains not to load the reader with unnecessary details.
But seriously, you need to remember that there are interrupts from serial ports, from several SPI connections, from an analog-to-digital converter, from another timer, after all. And what will happen if we are going to switch to another task (switch context) when we are in an interrupt handler. After all, it will not be a legitimate task, but a temporary clouding of the program. And we will keep this strange context as a task. The embarrassment will happen: the collar does not fasten, the cap does not fit. Stop, no, this is from another story.
In our case, this simply cannot be allowed. We cannot allow us to switch the context during the processing of a non-scheduled interrupt. Here are the priorities. We just have to wait a bit, and only then, when this unprecedented impudence is over, calmly switch to another task. In short, the interrupt priority of our task switch should be lower than the priority of any of the remaining interrupts used. This, by the way, is also done in our launch procedure, it is there that it is established, the most non-priority of all.
I did not want to talk, but I have to. Our processor has two modes of operation: privileged and non-privileged. And also two registers for the stack pointer:
main SP and SP process. So, we will not be wasted on trifles, we will use only the privileged mode and only the main stack pointer. Moreover, all this has already been given at the start of the controller. So, just do not complicate your life.
It remains to remember that each task, for sure, would like to be able to throw everything to hell and how to relax. And this can happen at any time during the working day, that is, during our tick. The Cortex-M4 for such cases provides a special assembly command SVC, which we will adapt to your situation. It leads to an interruption that will lead us to the goal. And we will allow the task not only to leave the workplace after lunch, but also not to come tomorrow. But what is really there, let him come after the holidays. And if necessary, then let him come when he finishes repairs or does not come at all. For this there is a procedure that the task itself can call.
void release_me_and_set_sleep_period(U32 ticks);
This subroutine only needs to specify how many ticks are planned to rest. If 0, then you can rest only the rest of the current tick. If 0xFFFFFFFF, then the task will be “sleep” until someone wakes up. All other numbers mean the number of ticks during which the task will be in a state of sleep.
So that someone else could wake up the task from the side or make him sleep, I had to add such procedures.
void task_wake_up_action(U8 taskNumber); void set_task_sleep_period(U8 taskNumber, U32 ticks);
And, just in case, even such a subroutine.
void task_remove_action(U8 taskNumber);
She, roughly speaking, deletes the task from the list of workers. Honestly, I don’t know why I wrote it. Suddenly useful?
It's time to show how the place looks like where one task is replaced by another, that is, the switch itself.
Just in case, let's remember that some of the registers, when entering an interrupt, are saved on the stack without our participation, automatically (as is customary in Cortex-M4). Therefore, we only need to save the rest. Below it can be seen. Do not be afraid of what you see, this is the assembly instructions from the Cortex-M4 (M3, M7), as presented by the IAR Embedded Workbench.
Those who have not yet come across assembler instructions, just believe, they really look that way. These are the molecules that make up any program under ARM Cortex-M4.
SysTick_Handler STMDB SP!,{R4-R11} ; LDR R0,=timersTable ; LDR R1,=stacksTable ; LDR R2,[R0] ;R2 () STR SP,[R1,R2,LSL #2] ; SP (R2 * 4) __st_next_check ADD R2,R2,#1 ; CMP R2,#TASKS_LIMIT ;R2-TASKS_LIMIT BLO __st_no_border_yet ; MOV R2,#0 ; (main) LDR R3,[R1] ; main SP MOV SP,R3 B __st_timer_ok __st_no_border_yet ;; LDR SP,[R1,R2,LSL #2] ; (errata Cortex M4) ;; CMP SP,#0 ; LDR R3,[R1,R2,LSL #2] ; SP CMP R3,#0 ; =0 BEQ __st_next_check MOV SP,R3 LDR R3,[R0,R2,LSL #2] ; suspend timer CBZ R3,__st_timer_ok ; 0 , ; CMP R3,#0xFFFFFFFF ; , BEQ __st_next_check SUB R3,R3,#1 ; 1 STR R3,[R0,R2,LSL #2] ; suspend timer B __st_next_check __st_timer_ok STR R2,[R0] ; LDMIA SP!,{R4-R11} ; R4-R11 BX LR
Handling an interrupt ordered by the task itself when it gives up the remainder of a tick looks similar. With the only difference that we still have to try to get some sleep (or sleep thoroughly). There is one subtlety. We need to do two actions, write the desired number into the sleep timer and call the SVC interrupt. The fact that these two actions are not atomic (that is, not both at the same time) bothers me a bit. Imagine for a millisecond that we just cocked the timer and at that time it was time to work on another task. The other began to spend her tick, while our task will be to sleep her next tics, as it should be (after all, her timer is not zero). Then, when her time comes, our task will receive its tick and immediately give it back to interrupt the SVC, because of the two actions, this one is not done. Nothing terrible, in my opinion, will happen, but the sediment will remain. Therefore, we will do so. The future sleep timer is put in a preliminary place. It is taken from there by the SVC interrupt handling procedure itself. Atomicity, as it were, achieved. This is shown below.
SVC_Handler LDR R0,__sysTickAddr ; SysTick MOV R1,#6 ; CSR , STR R1,[R0] ;Stop SysTimer MOV R1,#7 ; , STR R1,[R0] ;Start SysTimer ; STMDB SP!,{R4-R11} ; LDR R0,=timersTable ; LDR R1,=stacksTable ; LDR R2,[R0] ;R2 () STR SP,[R1,R2,LSL #2] ; SP (R2 * 4) LDR R3,=tmpTimersTable ; tmpTimers LDR R3,[R3,R2,LSL #2] ;tmpTimer STR R3,[R0,R2,LSL #2] ; timer __svc_next_check ADD R2,R2,#1 ; CMP R2,#TASKS_LIMIT ;R2-TASKS_LIMIT BLO __svc_no_border_yet ; MOV R2,#0 ; (main) LDR R3,[R1] ; main SP MOV SP,R3 B __svc_timer_ok __svc_no_border_yet ;; LDR SP,[R1,R2,LSL #2] ;Restore SP does not work (errata Cortex M4) ;; CMP SP,#0 ; LDR R3,[R1,R2,LSL #2] ; SP CMP R3,#0 ; =0 BEQ __svc_next_check MOV SP,R3 LDR R3,[R0,R2,LSL #2] ; suspend timer CBZ R3,__svc_timer_ok ; 0 , B __svc_next_check __svc_timer_ok STR R2,[R0] ; LDMIA SP!,{R4-R11} ; R4-R11 BX LR
It should be recalled that all these routines and interrupt handlers refer to a certain data area that looks like the author’s performance as shown in Figure 7.
DATA SECTION .taskSwitcher:CODE:ROOT(2) __topStack DCD sfe(CSTACK) __botStack DCD sfb(CSTACK) __dimStack DCD sizeof(CSTACK) __sysAIRCRaddr DCD 0xE000ED0C __sysTickAddr DCD 0xE000E010 __sysSHPRaddr DCD 0xE000ED18 __sysTickReload DCD RELOAD ;******************************************************************************* ; Task table for concurrent tasks (main is number 0). ;******************************************************************************* SECTION TABLE:DATA:ROOT(2) DS32 1 ;stack shift due to FPU mainCopyCONTROL DS32 1 ;Needed to determine if FPU is used mainPSRvalue DS32 1 ;Copy from main ;*******************************************************************************
To make sure that there is common sense in all of the above, the author had to write a small project under the IAR Embedded Workbench, where everything was able to be examined and touched in detail. Everything was tested on the STM32F303VCT6 controller (ARM Cortex-M4). Or rather, using the STM32F3DISCOVERY board. There are enough light-emitting diodes to give each task plenty of blinking with its own LED.
There are a few more features that seemed useful to me. For example, a subroutine that counts in each stack area the number of unaffected words, that is, remaining zero. This can be useful when debugging, when you need to check whether the stack is not too close to the limit level for a particular task.
U32 get_task_stack_empty_space(U8 taskNum);
Another feature I would like to mention. This is an opportunity for the task itself to find out its number in the list. You can then tell someone.
;******************************************************************************* ; Example: U8 get_my_number(void); ; (). .. . ;******************************************************************************* get_my_number LDR R0,=timersTable ; (currentTaskNumber) LDR R0,[R0] ; BX LR ;==============================================================
Here, perhaps, that's all.
Source: https://habr.com/ru/post/454874/
All Articles