AQUA RTOS Echtzeit-Betriebssystem für MK AVR in der BASCOM AVR-Umgebung

Beim Schreiben für MK-Code, der komplizierter ist als das „Blinken eines Lichts“, ist der Entwickler mit den Einschränkungen konfrontiert, die der linearen Programmierung im Stil von „Supercycle plus Interrupts“ inhärent sind. Die Verarbeitung von Interrupts erfordert Geschwindigkeit und Prägnanz, was dazu führt, dass dem Code Flags hinzugefügt werden und der Projektstil zum „Superzyklus mit Interrupts und Flags“ wird.

Wenn die Komplexität des Systems zunimmt, wächst die Anzahl der voneinander abhängigen Flags exponentiell und das Projekt wird schnell zu einem schlecht lesbaren und verwaltbaren „Pasta-Code“.

Die Verwendung von Echtzeitbetriebssystemen hilft, den "Pasta-Code" loszuwerden und Flexibilität und Verwaltbarkeit für ein komplexes MK-Projekt wiederherzustellen.
Es wurden mehrere kooperative Echtzeitbetriebssysteme entwickelt, die für AVR-Mikrocontroller sehr beliebt sind. Sie sind jedoch alle in C oder Assembler geschrieben und nicht für diejenigen geeignet, die MK in der BASCOM AVR-Umgebung programmieren, wodurch ihnen ein so nützliches Werkzeug zum Schreiben seriöser Anwendungen entzogen wird.

Um dieses Manko zu beheben, habe ich ein einfaches RTOS für die BASCOM AVR-Programmierumgebung entwickelt, auf das ich die Leser aufmerksam mache.

Bild

Für viele ist der bekannte MK-Programmierstil der sogenannte Superzyklus . In diesem Fall besteht der Code aus einer Reihe von Funktionen, Prozeduren und Deskriptoren (Konstanten, Variablen), möglicherweise Bibliotheksfunktionen, die allgemein als "Hintergrundcode" bezeichnet werden, sowie einer großen Endlosschleife, die in einem Do-Loop- Konstrukt eingeschlossen ist. Beim Start wird zuerst die Ausrüstung des MK selbst und externer Geräte initialisiert, die Konstanten und Anfangswerte der Variablen werden gesetzt, und dann wird die Steuerung auf diesen unendlichen Superzyklus übertragen.
Die Einfachheit des Superzyklus ist offensichtlich. Die meisten Aufgaben werden von MK ausgeführt, weil auf die eine oder andere Weise zyklisch. Die Nachteile sind auch offensichtlich: Wenn ein Gerät oder Signal eine sofortige Reaktion erfordert, wird MK es bereitstellen, sobald sich der Zyklus dreht. Wenn die Signaldauer kürzer als die Zyklusdauer ist, wird ein solches Signal übersprungen.

Im folgenden Beispiel möchten wir überprüfen, ob die Taste gedrückt wird :

do ' -  if button = 1 then '     '  -  loop 

Wenn "irgendein Code" lange genug funktioniert, bemerkt MK möglicherweise keinen kurzen Knopfdruck.

Glücklicherweise ist MK mit einem Interrupt-System ausgestattet, das dieses Problem lösen kann: Alle kritischen Signale können an Interrupts „aufgehängt“ werden, und für jeden kann ein Handler geschrieben werden. So erscheint das nächste Level: ein Superzyklus mit Unterbrechungen .
Das folgende Beispiel zeigt die Struktur des Programms mit einem Superzyklus und einem Interrupt, der einen Schaltflächenklick verarbeitet:

 on button button_isr '    enable interrupts ' ***  *** do ' -  loop end '   button_isr: '  -    return 

Die Verwendung von Interrupts stellt jedoch ein neues Problem dar: Der Interrupt-Handler-Code sollte so schnell und kürz wie möglich sein. Innerhalb von Unterbrechungen ist die MK-Funktionalität eingeschränkt. Da AVR-MKs kein hierarchisches Interrupt-System haben, kann innerhalb eines Interrupts kein weiterer Interrupt auftreten - sie sind zu diesem Zeitpunkt durch die Hardware deaktiviert. Daher sollte der Interrupt so schnell wie möglich ausgeführt werden, da sonst andere (und möglicherweise wichtigere) Interrupts übersprungen und nicht verarbeitet werden.

Speicher unterbrechen
Tatsächlich kann MK innerhalb des Interrupts die Tatsache eines anderen Interrupts in einem speziellen Register notieren, wodurch es später verarbeitet werden kann. Dieser Interrupt kann jedoch nicht sofort verarbeitet werden.

Daher können wir nichts Kompliziertes in den Interrupt-Handler schreiben, insbesondere wenn dieser Code Verzögerungen aufweisen sollte - denn bis die Verzögerung funktioniert, kehrt der MK nicht zum Hauptprogramm (Superzyklus) zurück und ist für andere Interrupts taub.

Aus diesem Grund müssen Sie im Interrupt-Handler häufig nur die Tatsache des Ereignisses mit einem Flag kennzeichnen - Ihrem eigenen für jedes Ereignis - und dann die Flags innerhalb des Superzyklus überprüfen und verarbeiten. Dies verlängert natürlich die Reaktionszeit auf das Ereignis, aber zumindest verpassen wir nichts Wichtiges.

Somit entsteht die nächste Komplexitätsstufe - ein Superzyklus mit Interrupts und Flags .

Der folgende Code wird angezeigt:

 on button button_isr '    enable interrupts ' ***  *** do ' -  if button_flag = 1 then '     button_flag = 0 '     end if '  -  loop end ' ***    *** button_isr: button_flag = 1 return 

Eine beträchtliche Anzahl von Programmen für MK ist dadurch begrenzt. Solche Programme sind jedoch meist noch mehr oder weniger einfach. Wenn Sie etwas Komplizierteres schreiben, wächst die Anzahl der Flaggen wie ein Schneeball, und der Code wird immer verwirrender und unlesbarer. Außerdem wurde im obigen Beispiel das Problem mit Verzögerungen nicht gelöst. Natürlich können Sie einen separaten Interrupt an den Timer "hängen" und darin ... auch verschiedene Flags steuern. Aber das macht das Programm völlig hässlich, die Anzahl der voneinander abhängigen Flags wächst exponentiell, und selbst der Entwickler selbst kann einen solchen „Pasta-Code“ kaum bald herausfinden. Der Versuch, einen Fehler zu finden oder den Code zu ändern, wird bei der Entwicklung eines neuen Projekts häufig gleichgestellt.

Wie kann man das Problem des "Pasta-Codes" lösen und es lesbarer und handlicher machen? Das Betriebssystem (OS) hilft. Darin ist die Funktionalität, die MK implementieren sollte, in Aufgaben unterteilt, deren Betrieb vom Betriebssystem gesteuert wird.

Arten von Betriebssystemen für MK


Betriebssysteme für MK können in zwei große Klassen unterteilt werden: Betriebssystem mit Verdrängung und kooperatives Betriebssystem. In jedem dieser Betriebssysteme werden Aufgaben durch ein spezielles Verfahren gesteuert, das als Dispatcher bezeichnet wird . In einem Betriebssystem mit Verdrängung schaltet der Dispatcher die Ausführung jederzeit unabhängig von einer Aufgabe zu einer anderen um und weist jedem eine bestimmte Anzahl von Taktzyklen zu (möglicherweise unterschiedlich, abhängig von der Priorität der Aufgabe). Dieser Ansatz funktioniert insgesamt hervorragend, sodass Sie den Inhalt von Aufgaben überhaupt nicht betrachten können: Sie können mindestens den Aufgabencode schreiben

 1: goto 1 

- und trotzdem werden die restlichen Aufgaben (einschließlich dieser) ausgeführt. Präemptive Betriebssysteme erfordern jedoch viele Ressourcen (Speicher- und Prozessorzyklen), da jeder Switch den Kontext der deaktivierten Aufgabe vollständig speichern und den Kontext der erneuerbaren Aufgabe laden sollte. Der Kontext bezieht sich hier auf den Inhalt der Maschinenregister und des Stapels (BASCOM verwendet zwei Stapel - den Hardware-Stapel für die Rücksprungadressen von Unterprogrammen und den Software-Stapel für die Übergabe von Argumenten). Ein solches Laden erfordert nicht nur viele Prozessorzyklen, sondern der Kontext jeder Aufgabe muss auch eine Weile irgendwo gespeichert werden, bis sie funktioniert. In den "großen" Prozessoren, die ursprünglich auf Multitasking ausgerichtet waren, werden diese Funktionen häufig in der Hardware unterstützt und verfügen über viel mehr Ressourcen. In AVR MK gibt es keine Hardwareunterstützung für Multitasking (alles muss „manuell“ erfolgen), und der verfügbare Speicher ist klein. Daher sind das Verschieben von Betriebssystemen, obwohl sie existieren, für einfache MKs nicht allzu geeignet.

Eine andere Sache ist kooperatives Betriebssystem . Hier steuert die Aufgabe selbst, an welchem ​​Punkt die Kontrolle an den Dispatcher übertragen werden soll, sodass er andere Aufgaben starten kann. Darüber hinaus sind hierfür die Aufgaben erforderlich, da sonst die Codeausführung ins Stocken gerät. Einerseits scheint dieser Ansatz die allgemeine Zuverlässigkeit zu verringern: Wenn eine Aufgabe hängt, wird der Dispatcher niemals angerufen, und das gesamte System reagiert nicht mehr. Andererseits ist ein linearer Code oder ein Superzyklus in dieser Hinsicht nicht besser - weil sie mit genau dem gleichen Risiko einfrieren können.

Ein kooperatives Betriebssystem hat jedoch einen wichtigen Vorteil. Da der Programmierer hier den Moment des Wechsels selbst festlegt, kann dies beispielsweise nicht plötzlich geschehen, während die Aufgabe mit einer Ressource arbeitet oder gerade einen arithmetischen Ausdruck berechnet. Daher können Sie in einem kooperativen Betriebssystem in den meisten Fällen auf die Pflege des Kontexts verzichten. Dies spart Prozessorzeit und Speicher erheblich und scheint daher für die Implementierung auf MK AVR viel besser geeignet zu sein.

Taskwechsel in BASCOM AVR


Um die Taskumschaltung in der BASCOM AVR-Umgebung zu implementieren, muss der Taskcode, der jeweils als normale Prozedur implementiert ist, an einer Stelle den Dispatcher aufrufen - ebenfalls als normale Prozedur implementiert.
Stellen Sie sich vor, wir haben zwei Aufgaben, von denen jede an einer Stelle ihres Codes vom Dispatcher aufgerufen wird.

 sub task1() do '  1 '  loop end sub ' ---------------------------------- sub task2() do '  2 '  loop end sub 

Angenommen, Aufgabe 1 wurde ausgeführt. Mal sehen, was auf dem Stapel passiert, wenn ein "Dispatcher-Aufruf" ausgeführt wird:

Rücksprungadresse an den Hauptcode (2 Bytes)
Stapelanfang -> Adresse an Task 1 zurückgeben, die den Dispatcher aufgerufen hat (2 Bytes)

Die Oberseite des Stapels zeigt auf die Adresse der Anweisung in Aufgabe 1, die auf den Dispatcher-Aufruf folgt (die Schleifenanweisung in unserem Beispiel).

Das Ziel des Dispatchers besteht im einfachsten Fall darin, die Kontrolle auf Aufgabe 2 zu übertragen. Die Frage ist, wie dies zu tun ist. (Angenommen, der Dispatcher kennt die Adresse von Task 2 bereits).

Zu diesem Zweck sollte der Dispatcher die Adresse der Rückkehr zu Task 1 vom Stapel abrufen (und an einen Ort, an den er sich erinnern sollte), die Adresse von Task 2 an dieser Stelle auf dem Stapel ablegen und dann den Befehl return eingeben. Der Prozessor extrahiert die dort platzierte Adresse aus dem Stapel und fährt mit der Ausführung von Task 2 fort, anstatt zu Task 1 zurückzukehren.

Wenn Task 2 den Dispatcher aufruft, ziehen wir auch den Stapel heraus und speichern die Adresse, an der zu Task 2 zurückkehren kann, und laden die zuvor gespeicherte Adresse von Task 1 auf den Stapel. Geben Sie den Befehl return ein, und wir befinden uns am Fortsetzungspunkt von Task 1 .

Infolgedessen bekommen wir so ein Durcheinander:

Aufgabe 1 -> Dispatcher -> Aufgabe 2 -> Dispatcher -> Aufgabe 1 ....

Nicht schlecht! Und es funktioniert. Für ein praktisch verwendbares Betriebssystem reicht dies natürlich nicht aus. Schließlich sollten nicht immer und nicht alle Aufgaben funktionieren - zum Beispiel können sie etwas erwarten (Ablauf der Verzögerungszeit, Auftreten eines Signals usw.). Die Aufgaben sollten also einen Status haben (ARBEITEN, BEREIT, ERWARTET usw.). Außerdem wäre es schön, wenn Aufgaben Priorität zugewiesen würden. Wenn dann mehr als eine Aufgabe zur Ausführung bereit ist, setzt der Dispatcher die Aufgabe mit der höchsten Priorität fort.

AQUA RTOS


Um die beschriebene Idee umzusetzen, wurde das kooperative OS AQUA RTOS entwickelt, das die notwendigen Dienste für Aufgaben bereitstellt und die Implementierung von kooperativem Multitasking in der BASCOM AVR-Umgebung ermöglicht.

Wichtiger Hinweis zum Verfahrensmodus in BASCOM AVR
Bevor Sie mit der Beschreibung von AUQA RTOS beginnen, sollten Sie beachten, dass die BASCOM AVR-Umgebung zwei Arten von Adressierungsverfahren unterstützt. Dies wird durch den config submode = new | geregelt alt.
Wenn Sie die alte Option angeben, kompiliert der Compiler zum einen den gesamten Code linear, unabhängig davon, ob er irgendwo verwendet wird oder nicht, und zum anderen werden Prozeduren ohne Argumente, die im Stil von Subname / End-Sub entworfen wurden, als Prozeduren wahrgenommen , im Stil des Namens gestaltet: / return. Auf diese Weise können wir die Adresse der Prozedur als Label als Argument an eine andere Prozedur übergeben, indem wir den Modifikator bylabel verwenden. Dies gilt auch für Prozeduren, die im Stil des Subnamens / End-Substils entworfen wurden (Sie müssen den Namen der Prozedur als Bezeichnung übergeben).
Gleichzeitig unterwirft der Modus submode = old einige Einschränkungen: Taskprozeduren dürfen keine Argumente enthalten; Der Code der über $ include verbundenen Dateien ist linear im allgemeinen Projekt enthalten. Daher sollte in den verbundenen Dateien eine Umgehung angegeben werden. Gehen Sie von Anfang bis Ende mit goto und einem Label.
Daher muss der Benutzer in AQUA RTOS entweder nur die alte Prozedurnotation im Stil von task_name: / return für Aufgaben verwenden oder den allgemeineren Subnamen / end sub verwenden, indem er den Modifikator submode = old am Anfang seines Codes hinzufügt und die enthaltenen Dateien umgeht gehe zu label / include file code / label :.

AQUA RTOS-Taskstatus


Die folgenden Status sind für Aufgaben in AQUA RTOS definiert:

 OSTS_UNDEFINE OSTS_READY OSTS_RUN OSTS_DELAY OSTS_STOP OSTS_WAIT OSTS_PAUSE OSTS_RESTART 

Wenn die Aufgabe noch nicht initialisiert wurde, erhält sie den Status OSTS_UNDEFINE .
Nach der Initialisierung hat die Task den Status OSTS_STOP .
Wenn die Aufgabe zur Ausführung bereit ist , wird ihr der Status OSTS_READY zugewiesen.
Die aktuell ausgeführte Task hat den Status OSTS_RUN .
Von dort aus kann sie zu den Status OSTS_STOP, OSTS_READY, OSTS_DELAY, OSTS_WAIT, OSTS_PAUSE wechseln .
Der Status OSTS_DELAY hat eine Aufgabe, die eine Verzögerung erfüllt.
Der Status OSTS_WAIT wird Aufgaben zugewiesen, die auf ein Semaphor, ein Ereignis oder eine Nachricht warten (mehr dazu weiter unten).

Was ist der Unterschied zwischen den Status OSTS_STOP und OSTS_PAUSED ?
Wenn die Aufgabe aus irgendeinem Grund den Status von OSTS_STOP erhält, wird die nachfolgende Wiederaufnahme der Aufgabe (nach Erhalt des Status von OSTS_READY ) von ihrem Einstiegspunkt aus ausgeführt, d. H. von Anfang an. Ab dem Status von OSTS_PAUSE funktioniert die Aufgabe an dem Ort weiter, an dem sie angehalten wurde.

Aufgabenstatusverwaltung


Sowohl das Betriebssystem selbst kann die Aufgaben als auch den Benutzer automatisch verwalten, indem die Betriebssystemdienste aufgerufen werden. Es gibt mehrere Aufgabenverwaltungsdienste (die Namen aller Betriebssystemdienste beginnen mit dem Präfix OS_ ):

 OS_InitTask(task_label, task_prio) OS_Stop() OS_StopTask(task_label) OS_Pause() OS_PauseTask(task_label) OS_Resume() OS_ResumeTask(task_label) OS_Restart() 

Jeder von ihnen hat zwei Optionen: OS_service und OS_serviceTask (mit Ausnahme des OS_InitTask- Dienstes, der nur eine Option hat; der OS_Init- Dienst initialisiert das Betriebssystem selbst).

Was ist der Unterschied zwischen OS_service und OS_serviceTask ? Die erste Methode wirkt auf die Aufgabe selbst, die sie verursacht hat. Mit der zweiten Option können Sie einen Zeiger auf eine andere Aufgabe als Argument festlegen und somit eine andere Aufgabe von einer Aufgabe aus verwalten.

Über OS_Resume
Alle Task-Management-Services außer OS_Resume und OS_ResumeTask rufen den Task-Manager nach der Verarbeitung automatisch auf. Im Gegensatz dazu setzen OS_Resume * -Dienste den Taskstatus nur auf OSTS_READY. Dieser Status wird nur verarbeitet, wenn der Dispatcher explizit aufgerufen wird.

Prioritäts- und Aufgabenwarteschlange


Wie oben erwähnt, können in einem realen System einige Aufgaben wichtiger sein, während andere sekundär sein können. Daher ist eine nützliche Funktion des Betriebssystems die Möglichkeit, Aufgaben Priorität zuzuweisen. In diesem Fall wählt das Betriebssystem zuerst die Aufgabe mit der höchsten Priorität aus , wenn mehrere vorgefertigte Aufgaben gleichzeitig vorhanden sind. Wenn alle vorgefertigten Aufgaben die gleiche Priorität haben, werden sie vom Betriebssystem in einem Kreis in einer Reihenfolge ausgeführt, die als "Karussell" oder "Round-Robin" bezeichnet wird.

In AQUA RTOS wird einer Aufgabe Priorität zugewiesen, wenn sie durch einen Aufruf des Dienstes OS_InitTask initialisiert wird , der die Adresse der Aufgabe als erstes Argument und eine Zahl von 1 bis 15 als zweites Argument empfängt . Eine niedrigere Zahl bedeutet eine höhere Priorität . Während des Betriebs des Betriebssystems wird keine Änderung der der Aufgabe zugewiesenen Priorität bereitgestellt.

Verzögerungen


In jeder Aufgabe wird die Verzögerung unabhängig von anderen Aufgaben verarbeitet.
Während das Betriebssystem die Verzögerung einer Aufgabe berechnet, können andere ausgeführt werden.
Zur Organisation von Verzögerungen erbrachte Dienstleistungen OS_Delay | OS_DelayTask . Das Argument ist die Anzahl der Millisekunden, um die sich die Aufgabe verzögert . Da die Dimension des Arguments dword ist, beträgt die maximale Verzögerung 4294967295 ms oder etwa 120 Stunden, was für die meisten Anwendungen ausreichend zu sein scheint. Nach dem Aufruf des Verzögerungsdienstes wird automatisch der Dispatcher angerufen, der die Kontrolle für die Dauer der Verzögerung auf andere Aufgaben überträgt.

Semaphoren


Semaphoren in AQUA RTOS sind so etwas wie Flags und Variablen, die für Aufgaben verfügbar sind. Es gibt zwei Arten - binär und zählbar. Die ersten haben nur zwei Zustände: frei und geschlossen. Der zweite ist ein Bytezähler (der Dienst zum Zählen von Semaphoren in der aktuellen Version von AQUA RTOS ist nicht implementiert (ich bin ein fauler Arsch), daher gilt alles, was unten gesagt wird, nur für binäre Semaphoren).

Der Unterschied zwischen einem Semaphor und einem einfachen Flag besteht darin, dass die Aufgabe ausgeführt werden kann, auf die Freigabe des angegebenen Semaphors zu warten . In gewisser Weise ähnelt die Verwendung von Semaphoren wirklich einer Eisenbahn: Beim Erreichen des Semaphors überprüft die Komposition (Aufgabe) das Semaphor, und wenn es nicht geöffnet ist, wartet es darauf, dass das Aktivierungssignal weiter erscheint. Zu diesem Zeitpunkt können andere Züge (Aufgaben) weiterfahren (fahren).

In diesem Fall wird die gesamte schwarze Arbeit dem Dispatcher zugewiesen. Sobald die Aufgabe angewiesen wird, auf das Semaphor zu warten, wird die Steuerung automatisch an den Dispatcher übertragen und er kann andere Aufgaben starten - genau bis das angegebene Semaphor freigegeben ist. Sobald sich der Status des Semaphors in " Frei" ändert, weist der Dispatcher allen Aufgaben, die auf dieses Semaphor gewartet haben, den Status " Bereit" ( OSTS_READY ) zu und sie werden in der Reihenfolge Priorität und Priorität ausgeführt.
Insgesamt bietet AQUA RTOS 16 binäre Semaphoren (diese Anzahl kann im Prinzip durch Ändern der Dimension der Variablen in der Task-Steuereinheit erhöht werden, da sie im Inneren als Bit-Flags implementiert sind).
Binäre Semaphore arbeiten mit den folgenden Diensten:

 hBSem OS_CreateBSemaphore() OS_WaitBSemaphore(hBSem) OS_WaitBSemaphoreTask(task_label, hBSem) OS_BusyBSemaphore(hBSem) OS_FreeBSemaphore(hBSem) 

Vor der Verwendung muss ein Semaphor erstellt werden . Dies erfolgt durch Aufrufen des OS_CreateBSemaphore- Dienstes, der die eindeutige Bytekennung (Handle) des erstellten hBSem- Semaphors zurückgibt , oder durch den benutzerdefinierten Handler, der einen OSERR_BSEM_MAX_REACHED- Fehler generiert, der angibt, dass die maximal mögliche Anzahl von binären Semaphoren erreicht wurde.

Sie können mit der empfangenen Kennung arbeiten, indem Sie sie als Argument an andere Semaphor-Dienste übergeben.

Dienst OS_WaitBSemaphore | OS_WaitBSemaphoreTask versetzt die (aktuell | angegebene) Aufgabe in einen Zustand, in dem auf die Freigabe des hBSem- Semaphors gewartet wird, wenn dieses Semaphor ausgelastet ist, und überträgt dann die Steuerung an den Dispatcher, damit dieser andere Aufgaben starten kann. Wenn das Semaphor frei ist, findet keine Steuerübertragung statt und die Aufgabe wird einfach fortgesetzt.

Die Dienste OS_BusyBSemaphore und OS_FreeBSemaphore setzen das hBSem- Semaphor auf beschäftigt bzw. frei .

Die Zerstörung von Semaphoren zur Vereinfachung des Betriebssystems und zur Reduzierung der Codemenge ist nicht vorgesehen. Somit sind alle erstellten Semaphoren statisch.

Ereignisse


Zusätzlich zu Semaphoren können Aufgaben ereignisgesteuert sein. Eine Aufgabe kann angewiesen werden, ein bestimmtes Ereignis zu erwarten , und eine andere Aufgabe (sowie der Hintergrundcode) kann dieses Ereignis signalisieren . Gleichzeitig erhalten alle Aufgaben, die auf dieses Ereignis gewartet haben, den Status zur Ausführung bereit ( OSTS_READY ) und werden vom Dispatcher zur Ausführung in der Reihenfolge Priorität und Priorität festgelegt.

Auf welche Ereignisse kann die Aufgabe reagieren? Nun, zum Beispiel:
  • Unterbrechung;
  • Auftreten eines Fehlers;
  • Freigabe der Ressource (manchmal ist es bequemer, dafür ein Semaphor zu verwenden);
  • Ändern des Status der E / A-Leitung oder Drücken einer Taste auf der Tastatur;
  • Empfangen oder Senden eines Zeichens über RS-232;
  • Informationsübertragung von einem Teil der Anwendung zu einem anderen (siehe auch Nachrichten).

Das Ereignissystem wird durch die folgenden Dienste implementiert:

 hEvent OS_CreateEvent() OS_WaitEvent(hEvent) OS_WaitEventTask(task_label, hEvent) OS_WaitEventTO(hEvent, dwTimeout) OS_SignalEvent(hEvent) 

Bevor Sie ein Ereignis verwenden können, müssen Sie es erstellen . Dies erfolgt durch Aufrufen der Funktion OS_CreateEvent , die eine eindeutige Byte- ID (Handle) für das Ereignis hEvent zurückgibt , oder durch den benutzerdefinierten Handler einen OSERR_EVENT_MAX_REACHED- Fehler ausgibt , der angibt, dass die maximale Anzahl von Ereignissen erreicht wurde, die im Betriebssystem generiert werden können (maximal 255 verschiedene Ereignisse).

Um eine Aufgabe auf ein hEvent- Ereignis warten zu lassen , rufen Sie OS_WaitEvent in seinem Code auf und übergeben Sie das Ereignishandle als Argument. Nach dem Aufrufen dieses Dienstes wird die Kontrolle automatisch an den Dispatcher übertragen.

Im Gegensatz zum Semaphor-Dienst bietet der Ereignisdienst die Option, auf ein Ereignis mit einer Zeitüberschreitung zu warten. Verwenden Sie dazu den Dienst OS_WaitEventTO . Mit dem zweiten Argument können Sie die Anzahl der Millisekunden angeben, die die Task für das Ereignis erwarten kann. Wenn die angegebene Zeit abgelaufen ist, erhält die Task den Status zur Ausführung bereit, als ob das Ereignis eingetreten wäre, und wird vom Dispatcher so eingestellt, dass die Ausführung in der Reihenfolge ihrer Priorität und Priorität fortgesetzt wird. Die Aufgabe kann feststellen, dass es sich nicht um ein Ereignis, sondern um eine Zeitüberschreitung handelt, die von der Aufgabe überprüft werden kann, indem das globale Flag OS_TIMEOUT überprüft wird.

Der Task- oder Hintergrundcode kann das Auftreten eines bestimmten Ereignisses signalisieren , indem der Dienst OS_SignalEvent aufgerufen wird , der das Ereignishandle als Argument empfängt . In diesem Fall setzt das Betriebssystem bei allen auf dieses Ereignis wartenden Aufgaben den Status zur Ausführung bereit , damit sie in der Reihenfolge ihrer Priorität und Priorität weiter ausgeführt werden können.

Nachrichten


Das Nachrichtensystem funktioniert im Allgemeinen ähnlich wie das Ereignissystem, bietet Aufgaben jedoch mehr Optionen und Flexibilität: Es bietet nicht nur die Erwartung einer Nachricht zu einem bestimmten Thema, sondern auch die Art und Weise, wie die Nachricht selbst von einer Aufgabe zu einer anderen übertragen wird - einer Zahl oder einer Zeichenfolge.
Dies wird durch die folgenden Dienste implementiert:

 hTopic OS_CreateMessage() OS_WaitMessage(hTopic) OS_WaitMessageTask(task_label, hTopic) OS_WaitMessageTO(hTopic, dwTimeout) OS_SendMessage(hTopic, wMessage) word_ptr OS_GetMessage(hTopic) word_ptr OS_PeekMessage(hTopic) string OS_GetMessageString(hTopic) string OS_PeekMessageString(hTopic) 

Um den Nachrichtendienst nutzen zu können, müssen Sie zuerst einen Nachrichtenthema erstellen . Dies erfolgt über den Dienst OS_CreateMessage , der das Byte-Handle des hTopic- Themas zurückgibt , oder über den benutzerdefinierten Handler, der einen Fehler OSERR_TOPIC_MAX_REACHED auslöst , der angibt, dass die maximal mögliche Anzahl von Nachrichtenthemen erreicht wurde und nicht mehr erstellt werden kann.

Um die Aufgabe anzuweisen , auf eine Nachricht zum Thema hTopic zu warten , rufen Sie OS_WaitMessage in ihrem Code auf und übergeben Sie das Handle des Themas als Argument. Nach dem Aufrufen dieses Dienstes wird die Steuerung automatisch an den Task-Manager übertragen. Somit versetzt dieser Dienst die aktuelle Aufgabe in einen ZustandWarten Sie auf eine hTopic-Nachricht .

Der Wartedienst mit dem Timeout OS_WaitMessageTO funktioniert ähnlich wie der Dienst OS_WaitEventTO des Ereignissystems .

Zum Senden von Nachrichten wird der Dienst OS_SendMessage bereitgestellt . Das erste Argument ist der Griff der Themen , auf das die Nachricht übertragen werden, und die zweite - das Argument Dimension Wort . Dies kann entweder eine unabhängige Zahl oder ein Zeiger auf eine Zeichenfolge sein , die wiederum bereits eine Nachricht ist.

Um einen Zeilenzeiger zu erhalten, verwenden Sie einfach die in BASCOM integrierte varptr-Funktion wie folgt :

 strMessage = "Hello, world!" OS_SendMessage hTopic, varptr (strMessage) 

Wiederaufnahme der Arbeit nach dem Aufruf von OS_WaitMessage , dh wenn die erwartete Nachricht empfangen wird, kann die Task die Nachricht entweder mit ihrer nachfolgenden automatischen Zerstörung empfangen oder nur die Nachricht anzeigen - in diesem Fall wird sie nicht zerstört. Verwenden Sie dazu die letzten vier Dienste in der Liste. Die ersten beiden Rück die Anzahl der Dimensionen des Wortes , die entweder eine separate Nachricht sein kann, oder dienen als Zeiger auf eine Zeichenkette, die die Nachricht enthält. In diesem Fall löscht OS_GetMessage die Nachricht automatisch und OS_PeekMessage verlässt sie.

Wenn die Aufgabe sofort eine Zeichenfolge und keinen Zeiger benötigt, können Sie die Dienste OS_GetMessageString oder OS_PeekMessageString verwenden , die ähnlich wie die beiden vorherigen funktionieren, aber einen String zurückgeben, keinen Zeiger darauf.

Interner Timer-Service


Um die Arbeit mit den Verzögerungen und Timing AQUA RTOS verwendet einen eingebauten IC Hardware - Timer TIMER0 . Daher sollte externer Code (Hintergrund und Aufgaben) diesen Timer nicht verwenden. Aber normalerweise ist dies nicht erforderlich, weil Das Betriebssystem liefert Aufgaben mit allen notwendigen Werkzeugen, um mit Zeitintervallen zu arbeiten. Die Timer-Auflösung beträgt 1 ms.

Beispiele für die Arbeit mit AQUA RTOS


Grundeinstellungen


Ganz am Anfang des Benutzercodes müssen Sie festlegen, ob der Code im integrierten Simulator oder auf realer Hardware ausgeführt wird. Definieren Sie die Konstante OS_SIM = TRUE | FALSE , das den Simulationsmodus einstellt.

Bearbeiten Sie außerdem im Betriebssystemcode die Konstante OS_MAX_TASK , die die maximale Anzahl der vom Betriebssystem unterstützten Aufgaben bestimmt. Je niedriger diese Zahl, desto schneller arbeitet das Betriebssystem (weniger Overhead) und desto weniger Speicher verbraucht es. Daher sollten Sie dort nicht mehr Aufgaben angeben, als Sie benötigen. Vergessen Sie nicht, diese Konstante zu ändern, wenn sich die Anzahl der Aufgaben geändert hat.

Betriebssysteminitialisierung


Vor dem Start muss AQUA RTOS initialisiert werden. Rufen Sie dazu den Dienst OS_Init auf . Dieser Dienst konfiguriert die anfänglichen Betriebssystemeinstellungen. Noch wichtiger ist, dass es ein Argument gibt - die Adresse der benutzerdefinierten Fehlerbehandlungsroutine. Sie hat wiederum ein Argument - einen Fehlercode.

Dieser Handler muss sich im Benutzercode befinden (zumindest in Form eines Stubs) - das Betriebssystem sendet Fehlercodes an ihn, und der Benutzer hat keine andere Möglichkeit, sie abzufangen und entsprechend zu verarbeiten. Ich empfehle dringend, zumindest in der Entwicklungsphase keinen Stub zu setzen, sondern die Ausgabe von Fehlerinformationen in dieses Verfahren einzubeziehen.

Der erste Schritt bei der Arbeit mit AQUA RTOS besteht darin, den Betriebssysteminitialisierungscode und die Fehlerbehandlungsprozedur zum Benutzerprogramm hinzuzufügen:

 OS_Init my_err_trap '... '... '... sub my_err_trap(err_code as byte) print err_code end sub 

Aufgabeninitialisierung


Der zweite Schritt besteht darin, die Aufgaben durch Angabe ihres Namens und ihrer Priorität zu initialisieren:

 OS_InitTask task_1, 1 OS_InitTask task_2 , 1 '... OS_InitTask task_N , 1 

Testaufgaben


LED blinkt


Erstellen wir also eine Testanwendung, die auf eine Standard-Arduino Nano V3-Karte heruntergeladen werden kann. Erstellen Sie einen Ordner in dem Ordner mit der Betriebssystemdatei (z. B. Test) und erstellen Sie dort die folgende Basisdatei:

 '    config submode = old $include "..\aquaRTOS_1.05.bas" $regfile = "m328pdef.dat" $crystal = 16000000 $hwstack = 48 $swstack = 48 $framesize = 64 '   declare sub my_err_trap (byval err_code as byte) declare sub task_1() declare sub task_2() '       led_1 alias portd.4 led_2 alias portd.5 config portd.4 = output config portd.5 = output ' ***    *** '   OS_Init my_err_trap '   OS_InitTask task_1, 1 OS_InitTask task_2 , 1 '      «» (OSTS_STOP) '    ,     ' «  » (OSTS_READY)   OS_ResumeTask OS_ResumeTask task_1 OS_ResumeTask task_2 '      OS_Sheduler end ' ***  *** sub task_1 () do toogle led_1 '   1 OS_delay 1000 '   1000  loop end sub sub task_2 () do toogle led_2 '   2 OS_delay 333 '   333  loop end sub ' **************************************************** '   sub my_err_trap(err_code as byte) print "OS Error: "; err_code end sub 

Verbinden Sie die Anoden der LEDs mit den D4- und D5- Pins der Arduino-Karte (oder mit anderen Pins, indem Sie die entsprechenden Definitionszeilen im Code ändern). Verbinden Sie die Kathoden über 100 ... 500 Ohm Abschlusswiderstände mit dem GND- Bus . Kompilieren Sie die Firmware und laden Sie sie auf das Board hoch. Die LEDs beginnen mit einer Zeitspanne von 2 und 0,66 s asynchron zu schalten.

Schauen wir uns den Code an. Also initialisieren wir zuerst die Ausrüstung (wir setzen die Compileroptionen, den Portmodus und weisen Aliase zu), dann das Betriebssystem selbst und schließlich die Aufgaben.

Da sich die gerade erstellten Aufgaben im Status "Gestoppt" befinden, müssen Sie ihnen den Status "Bereit zur Ausführung" geben (möglicherweise nicht für alle Aufgaben in einer realen Anwendung - schließlich können einige von ihnen, wie vom Entwickler konzipiert, zunächst gestoppt und ausgeführt werden Ausführung nur von anderen Tasks und nicht sofort zu Beginn des Systems (in diesem Beispiel sollten jedoch beide Tasks sofort funktionieren). Daher rufen wir für jede Aufgabe den Dienst OS_ResumeTask auf .

Jetzt sind die Aufgaben zur Ausführung bereit, aber noch nicht abgeschlossen. Wer wird sie starten? Natürlich der Dispatcher! Dazu müssen wir es beim ersten Start des Systems aufrufen. Wenn nun alles richtig geschrieben ist, führt der Dispatcher unsere Aufgaben einzeln aus, und wir können den Hauptteil des Programms mit der end- Anweisung beenden.

Schauen wir uns die Aufgaben an. Es ist sofort ersichtlich, dass jeder von ihnen als endlose Do-Loop gerahmt ist . Die zweite wichtige Eigenschaft - innerhalb eines solchen Zyklus muss mindestens ein Aufruf an den Dispatcher oder den Betriebssystemdienst erfolgen, der den Dispatcher automatisch nach sich selbst aufruft - andernfalls gibt eine solche Aufgabe niemals die Kontrolle auf und andere Aufgaben können nicht ausgeführt werden. In unserem Fall ist dies der OS_Delay- Verzögerungsdienst . Als Argument haben wir ihm die Anzahl der Millisekunden angegeben, für die jede Aufgabe angehalten werden soll.

Wenn Sie am Anfang des Codes die Konstante OS_SIM = TRUE setzen und den Code nicht auf dem realen Chip, sondern im Simulator ausführen, können Sie die Funktionsweise des Betriebssystems verfolgen.

, , , « », . , « », .

, (, task_1 ), ( end ) task_1 , , return , – , task_1 ( do in task_1 code ).

Die Task task_1 schaltet ihre LED um und ruft den OS_delay- Dienst auf , der nach Abschluss der erforderlichen Aktionen an den Dispatcher geht.

Der Dispatcher speichert die auf dem Stapel befindliche Adresse in der Tasksteuereinheit task_1 (zeigt auf die Anweisung nach dem Aufruf von OS_delay , dh auf die Schleifenanweisung ), und stellt dann beim Drehen des Karussells fest, dass task_2 jetzt abgeschlossen sein muss . Es drückt die Taskadresse task_2 (zeigt derzeit auf die Anweisung do im Taskcode task_2 ) und führt den Befehl ausreturn , wodurch der MK die Rücksprungadresse vom Stapel abruft und dorthin wechselt - dh Task_2 ausführt .

Task task_2 schaltet seine LED um und ruft den OS_delay- Dienst auf , der nach Ausführung der erforderlichen Aktionen zum Dispatcher geht.

Der Dispatcher speichert die auf dem Stapel befindliche Adresse in der Tasksteuereinheit task_1 (zeigt auf die Anweisung nach dem Aufruf von OS_delay , dh auf die Schleifenanweisung ), und stellt dann beim Drehen des Karussells fest, dass task_2 jetzt abgeschlossen sein muss . Der Unterschied zum Ausgangszustand besteht darin, dass er sich jetzt im Tasksteuerblock task_1 befindetEs wird nicht die Startadresse der Aufgabe gespeichert, sondern die Adresse des Punktes, von dem aus der Übergang zum Dispatcher stattgefunden hat. Dort (zur Schleifenanweisung im Taskcode task_1 ) wird die Steuerung übertragen.

Task task_1 führt die Schleifenanweisung aus , und dann wird der gesamte Zyklus "Task 1 - Dispatcher - Task 2 - Dispatcher" endlos wiederholt.

Nachrichten senden


Versuchen wir nun, Nachrichten von einer Aufgabe an eine andere zu senden.

 '    config submode = old $include "..\aquaRTOS_1.05.bas" $regfile = "m328pdef.dat" $crystal = 16000000 $hwstack = 48 $swstack = 48 $framesize = 64 '   declare sub my_err_trap (byval err_code as byte) declare sub task_1() declare sub task_2() const OS_SIM = TURE '       '   dim hTopic as byte '     dim task_1_cnt as byte '    1 dim strMessage as string * 16 '  ' ***    *** OS_CreateMessage hTopic OS_Init my_err_trap OS_InitTask task_1 , 1 OS_InitTask task_2 , 1 OS_ResumeTask task_1 OS_ResumeTask task_2 OS_Sheduler end ' ***  *** sub task_1() do print "task 1" OS_Sheduler incr task_1_cnt '    1 if task_1_cnt > 3 then print "task 1 is sending message to task 2" strMessage = "Hello, task 2!" '    2 OS_SendMessage hTopic , varptr(strMessage) task_1_cnt = 0 end if loop end sub sub task_2() do print "task 2 is waiting messages..." '      1 OS_WaitMessage hTopic print "message recieved: " ; OS_GetMessageString (hTopic) loop end sub ' **************************************************** '   sub my_err_trap(err_code as byte) print "OS Error: "; err_code end sub 

Das Ergebnis der Ausführung des Programms im Simulator ist die folgende Ausgabe im Terminalfenster:

task 1
task 2 is waiting messages…
task 1
task 1
task 1
task 1 is sending message to task 2
task 1
message recieved: Hello, task 2!
task 2 is waiting messages…
task 1
task 1
...

Achten Sie auf die Reihenfolge, in der die Arbeit und der Aufgabenwechsel stattfinden. Sobald Aufgabe 1 Aufgabe 1 druckt , wird die Kontrolle an den Dispatcher übertragen, damit dieser die zweite Aufgabe starten kann. Task 2 druckt Task 2 wartet auf Nachrichten ... , ruft dann den Nachrichtenwartedienst für das Thema hTopic auf und die Steuerung wird automatisch an den Dispatcher übertragen, der erneut Task 1 aufruft. Er druckt erneut Task 1 und gibt dem Dispatcher die Kontrolle. Da der Dispatcher jedoch feststellt, dass Task 2 jetzt auf Nachrichten wartet, gibt er in der Anweisung incr nach dem Dispatcher-Aufruf die Kontrolle an Task 1 zurück .
Wenn der Zähler task_1_cntIn Task 1 wird der angegebene Wert überschritten. Die Task sendet eine Nachricht, wird jedoch weiterhin ausgeführt. Führt die Schleifenanweisung aus und druckt Task 1 erneut . Danach ruft sie den Dispatcher an, der nun feststellt, dass eine Nachricht für Aufgabe 2 vorliegt, und überträgt die Kontrolle an sie. Als nächstes wird der Prozess zyklisch ausgeführt.

Ereignisbehandlung


Der folgende Code fragt zwei Tasten ab und schaltet die LEDs um, wenn Sie die entsprechende Taste drücken:

 '    config submode = old $include "..\aquaRTOS_1.05.bas" $regfile = "m328pdef.dat" $crystal = 16000000 $hwstack = 48 $swstack = 48 $framesize = 64 '   declare sub my_err_trap (byval err_code as byte) declare sub task_scankeys() declare sub task_led_1() declare sub task_led_2() '        led_1 alias portd.4 led_2 alias portd.5 config portd.4 = output config portd.5 = output button_1 alias pind.6 button_2 alias pind.7 config portd.6 = input config portd.7 = input '   dim eventButton_1 as byte dim eventButton_2 as byte ' ***    *** eventButton_1 = OS_CreateEvent '       eventButton_2 = OS_CreateEvent OS_Init my_err_trap OS_InitTask task_scankeys , 1 OS_InitTask task_led_1 , 1 OS_InitTask task_led_2 , 1 OS_ResumeTask task_scankeys OS_ResumeTask task_led_1 OS_ResumeTask task_led_2 OS_Sheduler end ' ***  *** sub task_scankeys() do debounce button_1 , 0 , btn_1_click , sub debounce button_2 , 0 , btn_2_click , sub OS_Sheduler loop btn_1_click: OS_SignalEvent eventButton_1 return btn_2_click: OS_SignalEvent eventButton_2 return end sub sub task_led_1() do OS_WaitEvent eventButton_1 toggle led_1 loop end sub sub task_led_2() do OS_WaitEvent eventButton_2 toggle led_2 loop end sub ' **************************************************** '   sub my_err_trap(err_code as byte) print "OS Error: "; err_code end sub 

Ein echtes Anwendungsbeispiel unter AQUA RTOS



Versuchen wir uns vorzustellen, wie ein Kaffeeautomatenprogramm aussehen könnte. Das Gerät sollte das Vorhandensein von Kaffeeoptionen und die Auswahl der LEDs in den Tasten anzeigen. Signale vom Münzempfänger empfangen, das bestellte Getränk zubereiten, Wechselgeld ausgeben. Darüber hinaus muss die Maschine die internen Geräte steuern: Halten Sie beispielsweise die Temperatur des Warmwasserbereiters bei 95 ... 97 ° C; Übertragen Sie Daten zu Gerätestörungen und Bestandteilen von Zutaten und empfangen Sie Befehle über Fernzugriff (z. B. über ein GSM-Modem) sowie Signalvandalismus.

Ereignisgesteuerter Ansatz


Zunächst kann es für einen Entwickler schwierig sein, vom üblichen Schema „Supercycle + Flags + Interrupts“ zu einem auf Aufgaben und Ereignissen basierenden Ansatz zu wechseln. Dazu müssen die grundlegenden Aufgaben hervorgehoben werden, die das Gerät ausführen soll.
Versuchen wir, solche Aufgaben für unsere Maschine zu skizzieren:

  • Heizungssteuerung und -verwaltung - ControlHeater ()
  • Angabe der Verfügbarkeit und Auswahl der Getränke - ShowGoods ()
  • Annahme von Münzen / Scheinen und deren Summierung - AcceptMoney ()
  • Abfragetasten - ScanKeys ()
  • Lieferung ändern - MakeChange ()
  • Getränkeurlaub - ReleaseCoffee ()
  • Vandalismusschutz - Alarm ()

Lassen Sie uns den Grad der Wichtigkeit der Aufgaben und die Häufigkeit ihres Anrufs abschätzen.
ControlHeater () ist offensichtlich wichtig, weil wir immer kochendes Wasser brauchen, um Kaffee zu machen. Es sollte jedoch nicht zu oft durchgeführt werden, da die Heizung eine hohe Trägheit aufweist und das Wasser langsam abkühlt. Es reicht aus, die Temperatur einmal pro Minute zu überprüfen.
Geben Sie dieser Aufgabe Priorität 5. ShowGoods () ist nicht zu wichtig. Das Angebot kann sich erst nach Freigabe der Ware ändern, wenn der Vorrat an Zutaten erschöpft ist. Daher geben wir dieser Aufgabe eine Priorität von 8 und lassen sie ausführen, wenn die Maschine startet und jedes Mal, wenn die Waren freigegeben werden.
ScanKeys ()muss eine ausreichend hohe Priorität haben, damit die Maschine schnell auf Tastendrücke reagieren kann. Geben Sie dieser Aufgabe Priorität 3, und wir werden sie alle 40 Millisekunden ausführen.
AcceptMoney () ist auch Teil der Benutzeroberfläche. Wir geben ihm die gleiche Priorität wie ScanKeys () und führen ihn alle 20 Millisekunden aus.
MakeChange () wird erst ausgeführt, nachdem die Ware freigegeben wurde. Wir werden es mit ReleaseCoffee () verknüpfen und Priorität 10 geben.
ReleaseCoffee () wird nur benötigt, wenn der entsprechende Geldbetrag akzeptiert wurde und die Getränkeauswahltaste gedrückt wird. Für eine schnelle Reaktion geben wir ihm Priorität 2.
Da Vandalismusresistenz eine ziemlich wichtige Funktion der Maschine ist, ist die Aufgabe Alarm ()Sie können die höchste Priorität auf 1 setzen und einmal pro Sekunde aktivieren, um die Neigungs- oder Manipulationssensoren zu überprüfen.

Wir brauchen also sieben Aufgaben mit unterschiedlichen Prioritäten. Wenn das Programm nach dem Start die Einstellungen aus dem EEPROM liest und das Gerät initialisiert, ist es Zeit, das Betriebssystem zu initialisieren und die Aufgaben zu starten.

 '    declare sub ControlHeater() declare sub ShowGoods() declare sub AcceptMoney() declare sub ScanKeys() declare sub MakeChange () declare sub ReleaseCoffee() declare sub Alarm() 

Um als Teil von RTOS zu arbeiten, muss jede Aufgabe eine bestimmte Struktur haben: Sie muss mindestens einen Aufruf an den Dispatcher haben (oder einen Betriebssystemdienst, der die Kontrolle automatisch an den Dispatcher überträgt) - dies ist die einzige Möglichkeit, kooperatives Multitasking sicherzustellen.

Zum Beispiel könnte ReleaseCoffee () ungefähr so aussehen:

 sub ReleaseCoffee() do OS_WaitMessage bCoffeeSelection wItem = OS_GetMessage(bCoffeeSelection) Release wItem loop end sub 

Die ReleaseCoffee- Aufgabe in einer Endlosschleife erwartet eine Nachricht zum Thema bCoffeeSelection und unternimmt nichts, bis sie eintrifft (die Steuerung wird automatisch an den Dispatcher zurückgegeben, damit er andere Aufgaben starten kann). Sobald die Nachricht gesendet wird, ReleaseCoffee () wird bereit zu laufen, und wenn das geschieht, wird die Aufgabe , den Inhalt der Nachricht (der Code des ausgewählten Getränks) wItem durch den Dienst OS_GetMessage und gibt die Ware an den Kunden. Da ReleaseCoffee () das Nachrichtensubsystem verwendet, muss vor dem Starten von Multitasking eine Nachricht erstellt werden:

 dim bCoffeeSelection as byte bCoffeeSelection = OS_CreateMessage() 

Wie oben erwähnt, muss ShowGoods () einmal beim Start und jedes Mal ausgeführt werden, wenn Waren freigegeben werden. Um es mit dem Release-Verfahren ReleaseCoffee () zu verknüpfen , verwenden wir den Event-Service. Erstellen Sie dazu ein Ereignis, bevor Sie mit dem Multitasking beginnen:

 dim bGoodsReliased as byte bGoodsReliased = OS_CreateEvent() 

Und in der ReleaseCoffee () -Prozedur fügen wir nach der Zeile Release wItem einen Alarm über das bGoodsReliased- Ereignis hinzu :

 OS_SignalEvent bGoodsReliased 

Betriebssysteminitialisierung


Um das Betriebssystem für die Arbeit vorzubereiten, müssen wir es initialisieren und die Adresse des Fehlerbehandlers angeben, die sich im Benutzercode befindet. Wir tun dies mit dem OS_Init- Dienst :

 OS_Init Mailfuncion 

Im Benutzercode müssen Sie einen Handler hinzufügen - eine Prozedur, deren Byteargument der Fehlercode ist:

 sub Mailfuncion (bCoffeeErr) print "Mailfunction! Error #: "; bCoffeeErr if isErrCritical (bCoffeeErr) = 1 then CallService(bCoffeeErr) end if end sub 

Bei diesem Verfahren wird ein Fehlercode gedruckt (oder auf andere Weise angezeigt: auf dem Bildschirm, über ein GSM-Modem usw.). Wenn der Fehler kritisch ist, wird die Serviceabteilung angerufen.

Aufgabenstart


Wir erinnern uns bereits daran, dass Ereignisse, Semaphoren usw. muss vor der Verwendung initialisiert werden. Darüber hinaus müssen die Aufgaben selbst vor dem Start mit dem Dienst OS_InitTask initialisiert werden :

 OS_InitTask ControlHeater , 5 OS_InitTask ShowGoods , 8 OS_InitTask AcceptMoney , 3 OS_InitTask ScanKeys , 3 OS_InitTask MakeChange, 10 OS_InitTask ReleaseCoffee , 2 OS_InitTask Alarm , 1 

Da der Multitasking-Modus noch nicht begonnen hat, ist die Reihenfolge, in der die Aufgaben beginnen, unerheblich und hängt in keinem Fall von ihren Prioritäten ab. Zu diesem Zeitpunkt befinden sich alle Aufgaben noch im gestoppten Zustand. Um sie für die Ausführung vorzubereiten, müssen wir den Dienst OS_ResumeTask verwenden, um ihnen den Status "Ausführungsbereit" zu setzen:

 OS_ResumeTask ControlHeater OS_ResumeTask ShowGoods OS_ResumeTask AcceptMoney OS_ResumeTask ScanKeys OS_ResumeTask MakeChange OS_ResumeTask ReleaseCoffee OS_ResumeTask Alarm 

Wie bereits erwähnt, müssen nicht alle Aufgaben unbedingt gestartet werden, wenn Multitasking gestartet wird. Einige von ihnen können jederzeit in einem „gestoppten“ Zustand bleiben und nur unter bestimmten Bedingungen bereit sein. Der OS_ResumeTask- Dienst kann jederzeit von einer beliebigen Stelle im Code (Hintergrund oder Aufgabe) aufgerufen werden, wenn bereits Multitasking ausgeführt wird. Die Hauptsache ist, dass die Aufgabe, auf die es sich bezieht, vorinitialisiert ist.

Multitasking-Start


Jetzt ist alles bereit, um Multitasking zu starten. Dazu rufen wir den Dispatcher an:

 OS_Sheduler 

Danach können wir den Programmcode sicher beenden - das Betriebssystem kümmert sich nun um die weitere Ausführung des Codes.

Schauen wir uns den gesamten Code an:

 '    config submode = old $include "..\aquaRTOS_1.05.bas" $include "coffee_hardware.bas" '      '       Coffee_ $regfile = "m328pdef.dat" ' Arduino Nano v3 $crystal = 16000000 $hwstack = 48 $swstack = 48 $framesize = 64 Coffee_InitHardware '    '   declare sub Mailfuncion (byval bCoffeeErr as byte) '   declare sub ControlHeater () '   declare sub ShowGoods () '    declare sub AcceptMoney () '   declare sub ScanKeys () '   declare sub MakeChange () '      declare sub ReleaseCoffee () '   declare sub Alarm () '    '     Coffee_InitHardware () '   dim wMoney as long '    dim wGoods as long '   ' ***    *** '   OS_Init Mailfuncion '       dim bCoffeeSelection as byte bCoffeeSelection = OS_CreateMessage() '     dim bGoodsReliased as byte bGoodsReliased = OS_CreateEvent() '   OS_InitTask ControlHeater , 5 OS_InitTask ShowGoods , 8 OS_InitTask AcceptMoney , 3 OS_InitTask ScanKeys , 3 OS_InitTask MakeChange, 10 OS_InitTask ReleaseCoffee , 2 OS_InitTask Alarm , 1 '     OS_ResumeTask ControlHeater OS_ResumeTask ShowGoods OS_ResumeTask AcceptMoney OS_ResumeTask ScanKeys OS_ResumeTask MakeChange OS_ResumeTask ReleaseCoffee OS_ResumeTask Alarm '   OS_Sheduler end ' ***   *** ' ----------------------------------- sub ControlHeater() do select case GetWaterTemp() case is > 97 Coffee_HeaterOff '   case is < 95 Coffee_HeaterOn '   case is < 5 CallServce (WARNING_WATER_FROZEN) '   end select OS_Delay 60000 '  1  loop end sub ' ----------------------------------- sub ShowGoods() do LEDS = Coffee_GetDrinkSupplies() '    D, '         '   LEDS OS_WaitEvent bGoodsReliased '   " " loop end sub ' ----------------------------------- sub AcceptMoney() do wMoney = wMoney + ReadMoneyAcceptor() OS_Delay 20 loop end sub ' ----------------------------------- sub ScanKeys() do wGoods = ButtonPressed() if wMoney >= GostOf(wGoods) then OS_SendMessage bCoffeeSelection, wGoods '     bCoffeeSelection,  '     end if OS_Delay 40 loop end sub ' ----------------------------------- sub MakeChange() do OS_WaitEvent bGoodsReliased '   " " Refund wMoney loop end sub ' ----------------------------------- sub ReleaseCoffee() do OS_WaitMessage bCoffeeSelection '  bCoffeeSelection wItem = OS_GetMessage(bCoffeeSelection) '   Release wItem '    wMoney = wMoney – CostOf (wItem) '     OS_SignalEvent bGoodsReliased '     '  ,       : ' MakeChange  ShowGoods '  ,  ,     loop end sub ' ----------------------------------- sub Alarm() do OS_Delay 1000 if Hijack() = 1 then CallPolice() end if loop end sub ' ----------------------------------- ' ***    *** sub Mailfuncion (bCoffeeErr) print "Mailfunction! Error #: "; bCoffeeErr if isErrCritical (bCoffeeErr) = 1 then CallService() end if end sub 

Natürlich wäre ein Ansatz korrekter, nicht bei periodischen Abfragen von Tasten und Geldsensoren, sondern bei Verwendung von Interrupts. In den Handlern dieser Interrupts könnten wir das Senden von Nachrichten mit dem Dienst OS_SendMessage () verwenden, deren Inhalt der Nummer der gedrückten Taste oder dem Wert der eingegebenen Münze / Rechnung entspricht. Ich lade den Leser ein, das Programm selbst zu ändern. Dank des aufgabenorientierten Ansatzes und des vom Betriebssystem bereitgestellten Dienstes sind hierfür nur minimale Codeänderungen erforderlich.

AQUA RTOS Quellcode


Der Quellcode der Version 1.05 steht hier zum Download bereit

Nachtrag


F: Warum AQUA?
A: Nun, ich habe den Aquarium-Controller gemacht, es ist wie ein "Smart Home", nicht nur für Menschen, sondern auch für Fische. Voll mit Sensoren aller Art, einer Echtzeituhr, Relais- und Analogausgängen, einem Bildschirmmenü, einem flexiblen "Ereignisprogramm" und sogar einem WiFi-Modul. Die Intervalle sollten gezählt werden, die Tasten sollten abgefragt werden, die Sensoren sollten verarbeitet werden, das Ereignisprogramm sollte aus dem EEPROM gelesen und ausgeführt werden, der Bildschirm sollte aktualisiert werden, das WLAN sollte reagieren. Darüber hinaus muss der Controller für Einstellungen und Programmierung in ein mehrstufiges Menü gehen. Bei Flags und Interrupts muss nur der „Pasta-Code“ abgerufen werden, der weder verstanden noch geändert wird. Deshalb habe ich beschlossen, dass ich ein Betriebssystem brauche. Hier ist sie AQUA.

F: Sicher ist der Code voller logischer Fehler und Pannen?
A: Sicher. Ich habe, so gut ich konnte, eine Reihe von Tests entwickelt und das Betriebssystem für eine Vielzahl von Aufgaben eingesetzt und sogar eine bemerkenswerte Anzahl von Fehlern zugeschlagen, aber dies bedeutet nicht, dass alles vollständig ist. Ich bin mir mehr als sicher, dass es noch viele davon in den Seitenstraßen des Codes gibt. Daher bin ich Ihnen sehr dankbar, wenn Sie, anstatt mich in die Käfer ins Gesicht zu stechen, höflich und taktvoll auf sie zeigen und mir besser sagen, wie Sie denken, dass es besser ist, sie zu beheben. Es wird auch großartig sein, wenn das Projekt als Produkt kollektiver Kreativität weiterentwickelt wird. Zum Beispiel wird jemand einen Dienst zum Zählen von Semaphoren hinzufügen (nicht vergessen? - Ich bin ein fauler Arsch) und andere Verbesserungen anbieten. Auf jeden Fall werde ich für den konstruktiven Beitrag sehr dankbar sein.

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


All Articles