Multitasking ist heutzutage wichtig. Ich frage mich, wie wir das mit Mikrocontrollern und eingebetteter Programmierung erreichen können. Ich entwerfe ein System, das auf einem PIC-Mikrocontroller basiert. Ich habe seine Firmware in MplabX IDE unter Verwendung von C entworfen und dann eine Anwendung dafür in Visual Studio unter Verwendung von C # entworfen.
Gibt es eine Möglichkeit, dasselbe in meinem Mikrocontroller-Code zu tun, seit ich mich daran gewöhnt habe, Threads in der C # -Programmierung auf dem Desktop zu verwenden, um parallele Aufgaben zu implementieren? MplabX IDE bietet, pthreads.h
aber es ist nur ein Stub ohne Implementierung. Ich weiß, dass es FreeRTOS-Unterstützung gibt, aber das macht Ihren Code komplexer. Einige Foren sagen, dass Interrupts auch als Multitasking verwendet werden können, aber ich glaube nicht, dass Interrupts Threads entsprechen.
Ich entwerfe ein System, das einige Daten an einen UART sendet und gleichzeitig Daten über (kabelgebundenes) Ethernet an eine Website senden muss. Ein Benutzer kann die Ausgabe über die Website steuern, die Ausgabe wird jedoch mit einer Verzögerung von 2-3 Sekunden ein- und ausgeschaltet. Das ist also das Problem, vor dem ich stehe. Gibt es eine Lösung für Multitasking in Mikrocontrollern?
quelle
Antworten:
Es gibt zwei Haupttypen von Multitasking-Betriebssystemen: präventive und kooperative. Mit beiden können mehrere Aufgaben im System definiert werden. Der Unterschied besteht darin, wie die Aufgabenumschaltung funktioniert. Natürlich läuft mit einem einzigen Core-Prozessor immer nur eine Task gleichzeitig.
Für beide Arten von Multitasking-Betriebssystemen ist für jede Aufgabe ein separater Stack erforderlich. Dies impliziert also zwei Dinge: Erstens, dass der Prozessor das Platzieren von Stacks an einer beliebigen Stelle im RAM zulässt und daher Anweisungen zum Bewegen des Stack-Zeigers (SP) enthält - dh, es gibt keinen speziellen Hardware-Stack wie am unteren Ende PIC's. Dadurch entfallen die PIC10, 12 und 16-Serien.
Sie können ein Betriebssystem fast vollständig in C schreiben, aber der Task-Switcher, in dem sich der SP bewegt, muss sich in der Assembly befinden. Zu verschiedenen Zeiten habe ich Task-Switches für PIC24, PIC32, 8051 und 80x86 geschrieben. Die Eingeweide sind je nach Architektur des Prozessors sehr unterschiedlich.
Die zweite Anforderung besteht darin, dass genügend RAM vorhanden ist, um mehrere Stapel bereitzustellen. Normalerweise möchte man mindestens ein paar hundert Bytes für einen Stapel; Aber selbst bei nur 128 Bytes pro Task benötigen acht Stapel 1 KB RAM - Sie müssen jedoch nicht für jeden Task die gleiche Stapelgröße zuweisen. Denken Sie daran, dass Sie genügend Stack benötigen, um die aktuelle Task und alle Aufrufe der verschachtelten Subroutinen zu verarbeiten, aber auch Speicherplatz für einen Interrupt-Aufruf, da Sie nie wissen, wann einer auftreten wird.
Es gibt ziemlich einfache Methoden, um zu bestimmen, wie viel Stapel Sie für jede Aufgabe verwenden. Sie können beispielsweise alle Stapel auf einen bestimmten Wert initialisieren, z. B. 0x55, und das System für eine Weile ausführen und dann den Speicher stoppen und untersuchen.
Sie sagen nicht, welche Art von PICs Sie verwenden möchten. Die meisten PIC24- und PIC32-Geräte bieten ausreichend Platz für die Ausführung eines Multitasking-Betriebssystems. Der PIC18 (der einzige 8-Bit-PIC mit Stapeln im RAM) hat eine maximale RAM-Größe von 4 KB. Also das ist ziemlich zweifelhaft.
Bei kooperativem Multitasking (das einfachere von beiden) wird die Taskumschaltung nur durchgeführt, wenn die Task ihre Kontrolle an das Betriebssystem "zurückgibt". Dies geschieht immer dann, wenn die Task eine OS-Routine aufrufen muss, um eine Funktion auszuführen, auf die sie warten wird, z. B. eine E / A-Anforderung oder einen Timer-Aufruf. Dies erleichtert dem Betriebssystem das Wechseln von Stapeln, da nicht alle Register und Statusinformationen gespeichert werden müssen. Der SP kann einfach auf eine andere Task umgeschaltet werden (wenn keine anderen Tasks ausgeführt werden können, handelt es sich um einen inaktiven Stack gegebene Kontrolle). Wenn die aktuelle Task keinen OS-Aufruf tätigen muss, aber schon eine Weile ausgeführt wurde, muss sie die Kontrolle freiwillig aufgeben, um das System reaktionsfähig zu halten.
Das Problem mit kooperativem Multitasking besteht darin, dass es das System überlasten kann, wenn die Aufgabe niemals die Kontrolle verliert. Nur es und alle Interrupt-Routinen, die zufällig gesteuert werden, können ausgeführt werden, sodass das Betriebssystem anscheinend abstürzt. Dies ist der "kooperative" Aspekt dieser Systeme. Wenn ein Watchdog-Timer implementiert ist, der nur zurückgesetzt wird, wenn ein Taskwechsel ausgeführt wird, können diese fehlerhaften Tasks abgefangen werden.
Windows 3.1 und frühere Versionen waren kooperative Betriebssysteme, weshalb ihre Leistung teilweise nicht so hoch war.
Präventives Multitasking ist schwieriger zu implementieren. Hier müssen Aufgaben nicht manuell die Steuerung aufgeben, sondern jeder Aufgabe kann eine maximale Ausführungszeit (z. B. 10 ms) zugewiesen werden, und dann wird zur nächsten ausführbaren Aufgabe gewechselt, falls eine vorhanden ist. Dazu muss eine Task willkürlich angehalten, alle Statusinformationen gespeichert und der SP auf eine andere Task umgeschaltet und gestartet werden. Dies macht den Task-Switcher komplizierter, erfordert mehr Stack und verlangsamt das System ein wenig.
Sowohl beim kooperativen als auch beim präemptiven Multitasking können zu jeder Zeit Interrupts auftreten, die die ausgeführte Task vorübergehend präemptieren.
Wie Supercat in einem Kommentar hervorhebt, besteht ein Vorteil von kooperativem Multitasking darin, dass es einfacher ist, Ressourcen gemeinsam zu nutzen (z. B. Hardware wie ein Mehrkanal-ADC oder Software wie das Ändern einer verknüpften Liste). Manchmal möchten zwei Aufgaben gleichzeitig auf dieselbe Ressource zugreifen. Bei der vorbeugenden Zeitplanung könnte das Betriebssystem mithilfe einer Ressource Aufgaben in der Mitte einer Aufgabe wechseln. Daher sind Sperren erforderlich, um zu verhindern, dass eine andere Task hereinkommt und auf dieselbe Ressource zugreift. Bei kooperativem Multitasking ist dies nicht erforderlich, da die Task steuert, wann sie sich selbst an das Betriebssystem zurückgibt.
quelle
void foo(void* context)
Die Controller-Logik (der Kernel) zieht jeweils ein Zeiger- und Funktionszeigerpaar aus der Warteschlange und ruft es einzeln auf. Diese Funktion verwendet den Kontext zum Speichern ihrer Variablen und dergleichen und kann dann hinzufügen, dass der Warteschlange eine Fortsetzung hinzugefügt wird. Diese Funktionen müssen schnell wieder verfügbar sein, damit andere Aufgaben in der CPU ausgeführt werden können. Dies ist eine ereignisbasierte Methode, die nur einen Stapel benötigt.Threading wird von einem Betriebssystem bereitgestellt. In der eingebetteten Welt haben wir normalerweise kein Betriebssystem ("Bare Metal"). Somit bleiben die folgenden Optionen:
Ich rate Ihnen, das einfachste der oben genannten Schemata zu verwenden, das für Ihre Anwendung geeignet ist. Nach dem, was Sie beschreiben, würde ich die Hauptschleife haben, die Pakete erzeugt und sie in kreisförmige Puffer legt. Lassen Sie dann einen UART ISR-basierten Treiber starten, der jedes Mal ausgelöst wird, wenn das vorherige Byte gesendet wurde, bis der Puffer gesendet wurde, und dann auf weiteren Pufferinhalt wartet. Ähnliches gilt für das Ethernet.
quelle
Wie bei jedem Single-Core-Prozessor ist echtes Software-Multitasking nicht möglich. Sie müssen also darauf achten, dass Sie auf eine Weise zwischen mehreren Aufgaben wechseln. Die verschiedenen RTOS kümmern sich darum. Sie verfügen über einen Scheduler und wechseln basierend auf einem System-Tick zwischen verschiedenen Aufgaben, um eine Multitasking-Funktion zu erhalten.
Die damit verbundenen Konzepte (Speichern und Wiederherstellen von Kontexten) sind recht kompliziert. Daher wird das manuelle Ausführen wahrscheinlich schwierig und macht den Code komplexer. Da Sie dies noch nie getan haben, treten darin Fehler auf. Mein Rat hier wäre, ein getestetes RTOS wie FreeRTOS zu verwenden.
Sie haben erwähnt, dass Interrupts eine Multitasking-Ebene bieten. Das ist irgendwie wahr. Der Interrupt unterbricht Ihr aktuelles Programm zu jedem Zeitpunkt und führt den Code dort aus. Dies ist vergleichbar mit einem Zwei-Task-System, bei dem Sie eine Task mit niedriger Priorität und eine andere mit hoher Priorität haben, die innerhalb einer Zeitscheibe des Schedulers endet.
Sie könnten also einen Interrupt-Handler für einen wiederkehrenden Timer schreiben, der ein paar Pakete über den UART sendet, und dann den Rest Ihres Programms für ein paar Millisekunden ausführen und die nächsten Bytes senden. Auf diese Weise erhalten Sie eine begrenzte Multitasking-Fähigkeit. Aber Sie werden auch eine ziemlich lange Unterbrechung haben, was eine schlechte Sache sein könnte.
Die einzige Möglichkeit, mehrere Aufgaben gleichzeitig auf einer Single-Core-MCU auszuführen, besteht darin, den DMA und die Peripheriegeräte zu verwenden, da sie unabhängig vom Core arbeiten (DMA und MCU teilen sich denselben Bus, arbeiten also etwas langsamer, wenn beide sind aktiv). Während der DMA die Bytes zum UART mischt, kann Ihr Core die Daten an das Ethernet senden.
quelle
In den anderen Antworten wurden bereits die am häufigsten verwendeten Optionen (Hauptschleife, ISR, RTOS) beschrieben. Hier ist eine weitere Option als Kompromiss: Protothreads . Es ist im Grunde eine sehr leichte Bibliothek für Threads, die die Hauptschleife und einige C-Makros verwendet, um ein RTOS zu "emulieren". Natürlich ist es kein vollständiges Betriebssystem, aber für "einfache" Threads kann es nützlich sein.
quelle
Mein grundlegendes Design für ein RTOS mit minimalen Zeitfenstern hat sich in mehreren Mikrofamilien nicht wesentlich geändert. Es ist im Grunde eine Timer-Unterbrechung, die eine Zustandsmaschine antreibt. Die Interrupt-Serviceroutine ist der Betriebssystemkern, während die switch-Anweisung in der Hauptschleife die Benutzertasks sind. Gerätetreiber sind Interrupt-Serviceroutinen für E / A-Interrupts.
Die Grundstruktur ist wie folgt:
Dies ist im Grunde ein kooperatives Multitasking-System. Aufgaben sind so geschrieben, dass sie niemals in eine Endlosschleife eintreten. Dies ist uns jedoch egal, da die Aufgaben in einer Ereignisschleife ausgeführt werden und die Endlosschleife daher implizit ist. Dies ist eine ähnliche Art der Programmierung wie bei ereignisorientierten / nicht blockierenden Sprachen wie Javascript oder Go.
Sie können ein Beispiel für diesen Architekturstil in meiner RC-Sendersoftware sehen (ja, ich benutze ihn tatsächlich, um RC-Flugzeuge zu fliegen, daher ist es etwas sicherheitskritisch, um zu verhindern, dass ich meine Flugzeuge abstürze und möglicherweise Menschen töte): https://github.com / slebetman / pic-txmod . Es hat im Grunde 3 Aufgaben - 2 Echtzeitaufgaben, die als Stateful Device-Treiber implementiert sind (siehe ppmio) und 1 Hintergrundaufgabe, die die Mischlogik implementiert. Im Grunde ist es Ihrem Webserver insofern ähnlich, als es 2 E / A-Threads hat.
quelle
Ich weiß zwar zu schätzen, dass in der Frage speziell die Verwendung eines eingebetteten RTOS gefragt wird, aber mir fällt die allgemeinere Frage ein, wie Multitasking auf einer eingebetteten Plattform erreicht werden kann.
Ich rate Ihnen dringend, zumindest vorerst die Verwendung eines eingebetteten RTOS zu vergessen. Ich rate dazu, weil ich denke, dass es wichtig ist, zuerst zu lernen, wie man durch extrem einfache Programmiertechniken, die aus einfachen Task-Schedulern und Zustandsautomaten bestehen, eine „Parallelität“ von Aufgaben erreicht.
Um das Konzept extrem kurz zu erklären, hat jedes zu erledigende Arbeitsmodul (dh jede "Aufgabe") eine bestimmte Funktion, die regelmäßig aufgerufen ("angekreuzt") werden muss, damit dieses Modul einige Aufgaben ausführt. Das Modul behält seinen eigenen aktuellen Zustand. Sie haben dann eine Endlosschleife (den Scheduler), die die Modulfunktionen aufruft.
Grobe Darstellung:
Eine solche Single-Thread-Programmierstruktur, bei der Sie die Funktionen der Hauptzustandsmaschine regelmäßig aus einer Hauptplanungsschleife aufrufen, ist in der eingebetteten Programmierung allgegenwärtig. Aus diesem Grund empfehle ich dem OP nachdrücklich, sich damit vertraut zu machen, bevor Sie direkt mit der Verwendung beginnen RTOS-Tasks / -Threads.
Ich arbeite an einem eingebetteten Gerät mit einer Hardware-LCD-Schnittstelle, einem internen Webserver, einem E-Mail-Client, einem DDNS-Client, VOIP und vielen anderen Funktionen. Obwohl wir ein RTOS (Keil RTX) verwenden, ist die Anzahl der verwendeten einzelnen Threads (Tasks) sehr gering und der größte Teil des "Multitasking" wird wie oben beschrieben erreicht.
Um einige Beispiele für Bibliotheken zu nennen, die dieses Konzept veranschaulichen:
Die Keil-Netzwerkbibliothek. Der gesamte TCP / IP-Stack kann Single-Threaded ausgeführt werden. Sie rufen regelmäßig main_TcpNet () auf, das den TCP / IP-Stack und alle anderen Netzwerkoptionen durchläuft, die Sie aus der Bibliothek (z. B. vom Webserver) kompiliert haben. Siehe http://www.keil.com/support/man/docs/rlarm/rlarm_main_tcpnet.htm . Zugegebenermaßen erreichen Sie in einigen Situationen (möglicherweise außerhalb des Rahmens dieser Antwort) einen Punkt, an dem die Verwendung von Threads nützlich oder erforderlich wird (insbesondere, wenn blockierende BSD-Sockets verwendet werden). (Weitere Anmerkung: Der neue V5-MDK-ARM erzeugt tatsächlich einen dedizierten Ethernet-Thread - aber ich versuche nur, eine Illustration bereitzustellen.)
Die Linphone VOIP-Bibliothek. Die Linphone-Bibliothek selbst ist Single-Threaded. Sie rufen die
iterate()
Funktion in einem ausreichenden Intervall auf. Siehe http://www.linphone.org/docs/liblinphone-javadoc/org/linphone/core/LinphoneCore.html#iterate () . (Ein schlechtes Beispiel, da ich dies auf einer eingebetteten Linux-Plattform und in den Abhängigkeitsbibliotheken von Linphone verwendet habe, aber es soll einen Punkt verdeutlichen.)Zurück zu dem vom OP beschriebenen spezifischen Problem scheint das Problem darin zu liegen, dass die UART-Kommunikation zur gleichen Zeit stattfinden muss wie ein Netzwerk (Übertragung von Paketen über TCP / IP). Ich weiß nicht, welche Netzwerkbibliothek Sie tatsächlich verwenden, aber ich würde annehmen, dass sie eine Hauptfunktion hat, die häufig aufgerufen werden muss. Sie müssten Ihren Code schreiben, der sich mit dem Senden / Empfangen von UART-Daten befasst, um auf ähnliche Weise wie eine Zustandsmaschine zu strukturieren, die durch periodische Aufrufe einer Hauptfunktion iteriert werden kann.
quelle