Ein bisschen über Multitasking in Mikrocontrollern

Ein bisschen über Multitasking


Jeder, der Tag für Tag oder von Fall zu Fall mit der Programmierung von Mikrocontrollern beschäftigt ist, wird früher oder später vor die Frage gestellt: Soll ich ein Multitask-Betriebssystem verwenden? Es gibt ziemlich viele von ihnen im Netzwerk, und viele von ihnen sind kostenlos (oder fast kostenlos). Einfach wählen.


Ähnliche Zweifel treten auf, wenn Sie auf ein Projekt stoßen, bei dem der Mikrocontroller gleichzeitig mehrere verschiedene Aktionen ausführen muss. Einige von ihnen sind nicht mit anderen verbunden, während der Rest im Gegenteil nicht ohne einander auskommen kann. Außerdem kann es zu viele von beiden geben. Was „zu viel“ ist, hängt davon ab, wer die Entwicklung bewertet oder wer sie durchführt. Nun, wenn es die gleiche Person ist.


Es handelt sich vielmehr nicht um eine Quantitätsfrage, sondern um einen qualitativen Unterschied der Aufgaben in Bezug auf die Ausführungsgeschwindigkeit oder einige andere Anforderungen. Solche Gedanken können zum Beispiel entstehen, wenn das Projekt die Versorgungsspannung regelmäßig überwachen muss (fehlt sie?), Sehr oft die Werte der Eingangsgrößen lesen und speichern (sie geben keine Ruhe), gelegentlich die Temperatur überwachen und den Lüfter steuern (es gibt nichts zu atmen), überprüfen Sie Ihre Beobachten Sie mit jemandem, dem Sie vertrauen (es ist gut, wenn Sie es befehlen), bleiben Sie mit dem Bediener in Kontakt (versuchen Sie, ihn nicht zu irritieren), überprüfen Sie die Prüfsumme des permanenten Speichers des Programms auf Demenz (wenn eingeschaltet, einmal pro Woche oder morgens).


Solche heterogenen Aufgaben können sehr sinnvoll und erfolgreich programmiert werden, wobei eine einzige Hintergrundaufgabe und Timer-Interrupts erforderlich sind. Im Handler dieser Interrupts wird jedes Mal eines der „Teile“ der nächsten Aufgabe ausgeführt. Abhängig von der Wichtigkeit, Dringlichkeit oder ähnlichen Überlegungen werden diese Herausforderungen häufig für einige Aufgaben wiederholt, für andere jedoch selten. Und doch müssen wir sicherstellen, dass jede Aufgabe einen kurzen Teil der Arbeit erledigt, sich dann auf den nächsten kleinen Teil der Arbeit vorbereitet und so weiter. Wenn Sie sich daran gewöhnen, erscheint dieser Ansatz nicht allzu kompliziert. Unannehmlichkeiten treten auf, wenn Sie ein Projekt erstellen möchten. Oder zum Beispiel plötzlich auf einen anderen übertragen. Es sollte beachtet werden, dass die zweite oft schwieriger und ohne Pseudo-Viele-Aufgabe ist.


Was aber, wenn Sie ein vorgefertigtes Betriebssystem für Mikrocontroller verwenden? Sicher, viele machen es. Dies ist eine gute Option. Aber der Autor dieser Zeilen war und ist bis jetzt von der Idee gestoppt worden, dass es notwendig sein wird, dies zu verstehen, nachdem er viel Zeit verbracht hat, aus dem zu wählen, was wir bekommen haben, und nur das zu verwenden, was wirklich benötigt wird. Und tun Sie das alles, wohlgemerkt, und tauchen Sie in den Code eines anderen ein! Und es gibt keine Gewissheit, dass dies in sechs Monaten nicht wiederholt werden muss, denn es wird vergessen.


Mit anderen Worten, warum benötigen Sie eine vollständige Garage mit Werkzeugen und Vorrichtungen, wenn dort ein Fahrrad gelagert und verwendet wird?


Daher bestand der Wunsch, einfache „Wechsel“ -Aufgaben nur für Cortex-M4 durchzuführen (naja, vielleicht sogar für M3 und M7). Aber der alte, gute Wunsch, nicht viel zu belasten, verschwand nicht.


Also machen wir das einfachste. Eine kleine Anzahl von Aufgaben teilt sich die Ausführungszeit zu gleichen Teilen. Wie in Abbildung 1 unten tun dies vier Aufgaben. Lassen Sie das Haupt Null sein, da es schwierig ist, sich ein anderes vorzustellen.



Auf diese Weise erhalten sie garantiert ihren Slot oder ihre Zeitspanne (Tick) und müssen nicht über das Vorhandensein anderer Aufgaben informiert sein. Jede Aufgabe genau nach 3 Ticks erhält wieder die Möglichkeit, etwas zu tun.


Wenn jedoch eine der Aufgaben erforderlich ist, um auf ein externes Ereignis zu warten, z. B. durch Drücken einer Taste, wird die kostbare Zeit unseres Mikrocontrollers dumm verbracht. Dem können wir nicht zustimmen. Und unsere Kröte (Gewissen) auch. Es muss etwas getan werden.


Und lassen Sie die Aufgabe, wenn sie bisher nichts zu tun hat, die von der Zecke verbleibende Zeit ihren Kameraden geben, die höchstwahrscheinlich mit aller Kraft pflügen.


Mit anderen Worten, das Teilen ist notwendig. Lassen Sie Aufgabe 2 genau das tun, wie in Abbildung 2 dargestellt.



Und warum sollte unsere Hintergrundaufgabe nicht den Rest der Zeit aufgeben dürfen, wenn Sie noch warten müssen? Lassen wir es zu. Wie in Abbildung 3 gezeigt.



Und wenn Sie wissen, dass einige der Aufgaben nicht bald erfordern, dass Sie etwas erneut überprüfen oder einfach nur arbeiten? Und sie konnte sich ein wenig Schlaf gönnen und verschwendete stattdessen Zeit und ging unter ihre Füße. Keine Bestellung, sie muss repariert werden. Lassen Sie Aufgabe 3 ein Stück ihrer Zeit (oder tausend) verpassen. Wie in Abbildung 4 gezeigt.



Nun, wie wir sehen, haben wir eine faire Koexistenz von Aufgaben oder Ähnlichem skizziert. Wir müssen jetzt dafür sorgen, dass sich unsere individuellen Aufgaben wie vorgeschrieben verhalten. Und wenn wir versuchen, die Zeit zu schätzen, lohnt es sich, sich an eine Sprache auf niedriger Ebene zu erinnern (ich habe keine Angst vor dem Wortassembler) und dem Compiler aus keiner Sprache auf hoher oder sehr hoher Ebene vollständig zu vertrauen. In der Tat sind wir tief in unserem Herzen entschieden gegen jede Abhängigkeit. Darüber hinaus vereinfacht die Tatsache, dass wir keinen Assembler benötigen, sondern nur von Cortex-M4, unser Leben.


Für den Stapel wählen wir einen gemeinsamen RAM-Bereich aus, der sich füllt, dh in Richtung abnehmender Speicheradressen. Warum? Nur weil es nicht anders funktioniert. Wir werden diesen wichtigen Bereich mental in gleiche Abschnitte unterteilen, entsprechend der Anzahl der angegebenen maximalen Anzahl unserer Aufgaben. Abbildung 5 zeigt dies für vier Aufgaben.



Als nächstes wählen wir den Ort aus, an dem Kopien der Stapelzeiger für jede Aufgabe gespeichert werden. Durch Unterbrechen des Timers, den wir als System-Timer verwenden, speichern wir alle Register der aktuellen Task in ihrem Stapelbereich (das SP-Register zeigt jetzt dorthin). Dann speichern wir seinen Stapelzeiger an einer speziellen Stelle (wir speichern seinen Wert) und erhalten den Stapelzeiger der nächsten Aufgabe ( Schreiben Sie von unserem speziellen Ort aus einen neuen Wert in das Register SP) und stellen Sie alle Register wieder her. Ihre Kopien werden jetzt im SP-Register unserer nächsten Aufgabe angezeigt. Nun, wir verlassen natürlich die Unterbrechung. Darüber hinaus wird der gesamte Kontext der nächsten Aufgabe in der Liste in den Registern angezeigt.


Wahrscheinlich ist es überflüssig zu sagen, dass die nächste nach task3 in der Warteschlange main ist. Und natürlich ist es nicht überflüssig, sich daran zu erinnern, dass der Cortex-M4 bereits über einen SysTick-Timer und einen speziellen Interrupt verfügt, und viele Hersteller von Mikrocontrollern wissen davon. Wir werden es und diese Unterbrechung wie vorgesehen verwenden.


Um diesen System-Timer zu starten und alle erforderlichen Vorbereitungen und Überprüfungen vorzunehmen, müssen Sie das dafür vorgesehene Verfahren anwenden.


U8 main_start_task_switcher(void); 

Diese Routine gibt 0 zurück, wenn alle Prüfungen bestanden wurden, oder einen Fehlercode, wenn ein Fehler aufgetreten ist. Grundsätzlich wird geprüft, ob der Stapel korrekt ausgerichtet ist und ob genügend Platz dafür vorhanden ist, und auch alle unsere speziellen Stellen sind mit Anfangswerten gefüllt. Kurz gesagt, Langeweile.


Wenn sich jemand den Text des Programms ansehen möchte, kann er dies am Ende der Erzählung problemlos tun, beispielsweise per E-Mail.


Ja, ich habe völlig vergessen, wenn wir die Register der nächsten Aufgabe zum ersten Mal in ihrem Leben aus dem Speicher nehmen. Es ist notwendig, dass sie aussagekräftige Originalwerte erhalten. Und da sie sie von ihrem Abschnitt des Stapels aufnimmt, müssen Sie sie im Voraus dort ablegen und ihren Stapelzeiger bewegen, damit es bequem ist, sie zu nehmen. Dafür brauchen wir ein Verfahren


  U8 task_run_and_return_task_number(U32 taskAddress); 

An diese Unterroutine geben wir die 32-Bit-Adresse des Beginns unserer Aufgabe an, die wir ausführen möchten. Und sie (die Unterroutine) gibt uns die Nummer der Aufgabe an, die sich in einer speziellen allgemeinen Tabelle herausstellte, oder 0, wenn in der Tabelle kein Platz vorhanden war. Dann können wir eine andere Aufgabe ausführen, dann eine andere und so weiter, obwohl alle drei zusätzlich zu unserer nie endenden Hauptaufgabe sind. Sie wird niemandem ihre Nullnummer geben.


Ein paar Worte zu den Prioritäten. Die Hauptpriorität war und ist es, den Leser nicht mit unnötigen Details zu überladen.


Aber im Ernst, wir müssen uns daran erinnern, dass es schließlich Unterbrechungen von seriellen Schnittstellen, von mehreren SPI-Verbindungen, von einem Analog-Digital-Wandler, von einem anderen Timer gibt. Und was passiert, wenn wir zu einer anderen Aufgabe (Kontext wechseln) wechseln, wenn wir uns im Handler einer Art Interrupt befinden? Dies ist schließlich keine legitime Aufgabe, sondern eine vorübergehende Trübung des Programms. Und wir werden diesen seltsamen Kontext als eine Art Aufgabe behalten. Es wird eine Verwirrung geben: Der Kragen lässt sich nicht befestigen, die Kappe passt nicht. Hör auf, nein, das ist aus einer anderen Geschichte.


In unserem Fall ist dies einfach nicht zulässig. Wir dürfen nicht zulassen, dass wir während der Verarbeitung eines ungeplanten Interrupts den Kontext wechseln. Hier sind die Prioritäten dafür. Wir müssen nur ein bisschen warten und erst dann, wenn diese beispiellose Kühnheit endet, ruhig zu einer anderen Aufgabe wechseln. Kurz gesagt, die Priorität des Interrupts unseres Taskwechsels sollte schwächer sein als die Priorität aller anderen verwendeten Interrupts. Dies geschieht übrigens auch in unserem Startvorgang, und dort wird es installiert, die höchstmögliche Priorität.


Ich wollte nicht reden, aber ich musste. Unser Prozessor verfügt über zwei Betriebsmodi: privilegiert und nicht privilegiert. Und auch zwei Register für den Stapelzeiger:
Haupt-SP und SP-Prozess. Wir werden also nicht gegen Kleinigkeiten tauschen, sondern nur den privilegierten Modus und nur den Hauptstapelzeiger verwenden. Darüber hinaus wurde dies alles bereits zu Beginn der Steuerung angegeben. Wir werden unser Leben also nicht komplizieren.


Es bleibt zu erinnern, dass jede Aufgabe sicher in der Lage sein möchte, alles in die Hölle zu werfen und sich zu entspannen. Und dies kann jederzeit während des Arbeitstages geschehen, dh während unserer Zecke. Cortex-M4 bietet für solche Fälle einen speziellen Assembler-Befehl SVC, den wir an unsere Situation anpassen werden. Es führt zu einer Unterbrechung, die uns zum Ziel führt. Und wir werden zulassen, dass die Aufgabe nicht nur nach dem Mittagessen den Arbeitsplatz verlässt, sondern auch morgen nicht kommt. Warum, lass es nach den Ferien kommen. Und wenn nötig, lassen Sie es kommen, wenn die Reparatur abgeschlossen ist oder überhaupt nicht kommt. Zu diesem Zweck gibt es eine Prozedur, die die Aufgabe selbst verursachen kann.


  void release_me_and_set_sleep_period(U32 ticks); 

Diese Routine muss nur angeben, wie viele Zecken zum Ausruhen geplant sind. Wenn 0, können Sie nur den Rest des aktuellen Ticks ausruhen. Wenn 0xFFFFFFFF, wird die Aufgabe "schlafen", bis jemand aufwacht. Alle anderen Zahlen geben die Anzahl der Ticks an, während der sich die Aufgabe im Ruhezustand befindet.


Damit jemand anderes von der Seite aufwachen oder ihn schlafen lassen konnte, musste ich solche Verfahren hinzufügen.


  void task_wake_up_action(U8 taskNumber); void set_task_sleep_period(U8 taskNumber, U32 ticks); 

Und für alle Fälle sogar eine solche Unterroutine.


  void task_remove_action(U8 taskNumber); 

Grob gesagt streicht sie eine Aufgabe aus der Liste der Mitarbeiter. Ehrlich gesagt weiß ich noch nicht, warum ich es geschrieben habe. Plötzlich nützlich?


Es ist Zeit zu zeigen, wie der Ort aussieht, an dem eine Aufgabe durch eine andere ersetzt wird, dh der Schalter selbst.


Für alle Fälle erinnern wir uns, dass einige der Register beim Eingeben des Interrupts ohne unsere Teilnahme automatisch auf dem Stapel gespeichert werden (wie es in Cortex-M4 üblich ist). Deshalb müssen wir nur den Rest retten. Dies ist unten zu sehen. Lassen Sie sich nicht von dem, was Sie sehen, beunruhigen. Dies sind Cortex-M4-Assembler-Anweisungen (M3, M7), wie in der IAR Embedded Workbench beschrieben.


Diejenigen, die noch keine Montageanleitung erhalten haben, glauben Sie mir einfach, sie sehen wirklich so aus. Dies sind die Moleküle, aus denen jedes Programm unter dem ARM Cortex-M4 besteht.


 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 

Die Behandlung des von der Aufgabe selbst geordneten Interrupts, wenn der Rest des Ticks zurückgegeben wird, sieht ähnlich aus. Der einzige Unterschied besteht darin, dass Sie sich später noch darum kümmern müssen, ein wenig zu schlafen (oder gründlich einzuschlafen). Es gibt eine Subtilität. Es müssen zwei Aktionen ausgeführt werden: Schreiben Sie die gewünschte Nummer in den Sleep-Timer und lassen Sie den SVC unterbrechen. Die Tatsache, dass diese beiden Aktionen nicht atomar ablaufen (dh nicht beide gleichzeitig), beunruhigt mich ein wenig. Stellen Sie sich für eine Millisekunde vor, wir hätten gerade den Timer gespannt und zu diesem Zeitpunkt war es Zeit, an einer anderen Aufgabe zu arbeiten. Die andere begann, ihre Zecke auszugeben, während unsere Aufgabe darin besteht, die nächsten Zecken wie erwartet zu schlafen (weil ihr Timer nicht Null ist). Wenn es dann soweit ist, erhält unsere Aufgabe ihr Häkchen und gibt es sofort an, um die SVC zu unterbrechen, da diese beiden Aktionen noch ausgeführt werden müssen. Meiner Meinung nach wird nichts Schreckliches passieren, aber das Sediment wird bleiben. Deshalb werden wir das tun. Der zukünftige Schlaf-Timer wird an einen vorläufigen Ort gestellt. Es wird von dort von der Interruptroutine selbst von SVC übernommen. Atomizität wird sozusagen erreicht. Dies ist unten gezeigt.


 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 

Es sei daran erinnert, dass sich alle diese Unterprogramme und Interrupt-Handler auf einen bestimmten Datenbereich beziehen, der vom Autor wie in Abbildung 7 dargestellt ausgeführt wird.


  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 ;******************************************************************************* 

Um sicherzustellen, dass all das der gesunde Menschenverstand ist, musste der Autor ein kleines Projekt unter der IAR Embedded Workbench schreiben, in dem es ihm gelang, alles im Detail zu untersuchen und zu berühren. Alles wurde auf dem STM32F303VCT6-Controller (ARM Cortex-M4) getestet. Oder besser gesagt, mit der STM32F3DISCOVERY-Karte. Es gibt genügend LEDs, um jede Aufgabe mit einer eigenen LED separat zu blinken.


Es gibt noch ein paar weitere Funktionen, die ich nützlich fand. Zum Beispiel eine Unterroutine, die in jedem Stapelbereich die Anzahl der nicht betroffenen Wörter zählt, dh gleich Null bleibt. Dies kann beim Debuggen hilfreich sein, wenn Sie überprüfen müssen, ob das Füllen des Stapels mit der einen oder anderen Aufgabe zu nahe am Grenzwert liegt.


  U32 get_task_stack_empty_space(U8 taskNum); 

Ich möchte noch eine Funktion erwähnen. Dies ist eine Gelegenheit für die Aufgabe selbst, Ihre Nummer in der Liste herauszufinden. Sie können es später jemandem erzählen.


 ;******************************************************************************* ; Example: U8 get_my_number(void); ;     (). ..    . ;******************************************************************************* get_my_number LDR R0,=timersTable ;    (currentTaskNumber) LDR R0,[R0] ;  BX LR ;============================================================== 

Das ist wahrscheinlich alles für den Moment.

Source: https://habr.com/ru/post/de454874/


All Articles