Ein Propeller Controller verfügt über acht Prozessoren, die man auch als Kerne oder Cogs bezeichnet und von 0 bis 7 durchnummeriert. In allen Programmen startet das Hauptprogramm automatisch immer Cog 0. Werden Bibliotheksdateien und daraus spezielle Funktionen aufgerufen, dann können diese Programme auch weitere Cogs starten und Aufgaben im Hintergrund erledigen. Man kann auch eigene Funktionen schreiben und sie von einem neuen Cog ausführen lassen; dies geschieht über den Befehl cog_run.
Im folgenden Beispiel wird gezeigt, wie man eine cog_run Funktion aufruft. Das Hauptprogramm läuft im Cog 0, der den Funktionsaufruf cog_run ausführt. Der neue Cog 1 lässt dann die LED blinken.
Die Funktion enthält zwei Parameter:
Man sollte immer ausreichend Speicherplatz für den Cog bereitstellen. In diesem Beispiel ist ein Wert von 128 eingetragen, der bei dem kleinen Programm gar nicht benötigt wird.
Bei einer Funktion, die über cog_run gestartet wird, gibt es ein paar Regeln zu beachten. Sie
Die Schaltung
In dieser ersten einfachen Übung werden die eingebauten LEDs an den fest verdrahteten I/O Ports 26 und 27 des Propeller Board of Education bzw. des Propeller Activity Boards genutzt.
Die Funktion cog_run | |
Material |
|
Aufgaben |
|
Wie arbeitet das Programm multicore_1.c?
Zeile 8 enthält eine forward-Deklaration für die Funktion blink. Die Funktion blink wird erst nach der Funktion main (ab Zeile 15) spezifiziert; dem Compiler wird auf diese Weise mitgeteilt, dass eine Funktion mit diesem Namen zu erwarten ist.
Die Hauptroutine enthält nur eine Befehlszeile
int main()
{
cog_run(blink, 128);
}
Damit wird die Funktion blink gestartet und ein Stack von 128 32-Bit Wörtern bereit gestellt, um Berechnungen während der Ausführung des Programms blink erledigen zu können, auch wenn, wie in diesem Fall, der Stackbereich nicht benötigt oder nicht in dem Umfang benötigt wird.
Die aufgerufene Funktion blink (Programmzeile 15 - 24) folgt im Programm nach dem Hauptprogramm int main() und sie wird von einem anderen Cog ausgeführt. Sie enthält keinen Rückgabewert und keine Parameterliste und der auszuführende Befehlsblock befindet sich in einer Endlosschleife. Die LED an P26 wird also solange blinken, bis die Energiequelle ausgeschaltet wird. Der ausführende Cog und sein Stackbereich sind deshalb für eine andere Nutzung nicht verfügbar.
void blink() { while(1) { high(26); pause(100); low(26); pause(100); } }
Was du wissen solltest!
Wie viel Stackplatz wird benötigt? 10 ist das absolute Minimum für den Stack. Werden der Funktion blink weitere Anweisungen im Befehlsblock hinzugefügt, sollte dieser Wert erhöht werden. Erhöhe den Stack um den Wert
Print, scan und anderen Funktionen, die mit dem SimpleIDE Terminal interagieren, können immer nur von einem Cog zur Zeit ausgeführt werden. Normaerweise werden diese Funktionen über das Hauptprogramm main() aufgerufen, auch wenn sie Werte ausgeben sollen, die von anderen Cogs verändert wurden. Aber die Funktion print kann auch von anderen Cogs ausgeführt werden, wie wir später sehen werden.
Das Programm multicore_1.c wird jetzt so erweitert, dass zusätzlich die LED an P27 mit einer anderen Frequenz blinkt. Das dazu notwendige Programm ist simple: füge eine zweite Funktion dem Programm hinzu und übergib ihre Ausführung einem weiteren Cog über die Funktion cog_run. Dazu ist eine weitere forward-Deklaration notwendig und ein zweiter Cog-Pointer, der den Cog herunterfahren kann.
Zwei Funktionen - zwei Cogs | |
Material |
|
Aufgaben |
|
Das Programm multicore_2.c
Na ja, so spektakulär war das jetzt vielleicht nicht mit den beiden blinkenden LEDs. Aber schauen wir uns das Ergebnis einmal genauer an: eine LED blinkt alle 100ms, die andere alle 223ms. Wie müsste man eigentlich einen Arduino UNO oder eine BASIC Stamp programmieren, damit die LEDs mit unterschiedlichen Frequenzen blinken? Oder noch verrückter, wir lassen fünf oder sechs LEDs mit unterschiedlichen Frequenzen blinken. Dieses einfache Beispiel soll zeigen, wie man mit Multiprocessing zeitsensitive Abläufe relativ einfach bewältigen kann.
Um nachzuweisen, dass drei Cogs tatsächlich drei unterschiedliche Prozesse zur selben Zeit bearbeiten, lassen wir den ersten Cog (Cog 0) etwas mehr als nur den Start der anderen beiden Cogs ausführen.
Drei Cogs steuern drei Prozesse gleichzeitig | |
Material |
|
Aufgaben |
|
In das bestehende Programm mulitcore_2.c werden die print-Anweisungen (Programmzeilen 14, 16 und 18) eingefügt.
Bis jetzt haben wir uns nur damit befasst, wie man einen Cog mit Hilfe der Funktion cog_run startet. Jetzt geht es darum, wie man einen Cog mit Hilfe der Funktion cog_end und die von ihm bearbeitete Funktion abschaltet.
Einen Cog herunterfahren | |
Material |
|
Aufgaben |
|
Das Programm multicore_4.c
Wie arbeitet das Programm multicore_4.c?
Programmzeilen 6 - 8
Die simpletools Bibliothek wird in das Programm eingebunden; damit sind unter anderen auch die Funktionen cog_run und cog_end verfügbar. In der folgenden Zeile erscheint die forward-Deklaration einer Funktion blink, die unterhalb der Hauptfunktion main spezifiziert wird.
Programmzeile 12
Die Funktion cog_run liefert einen Rückgabewert; in diesem Beispiel ist es eine lokale Variable, die mit
int *cog =
deklariert wird. Das Sternchen * weist darauf hin, dass cog ein Pointer ist, dass der in ihm abgelegte Wert eine Speicheradresse ist. Das ist die Stelle, bei der der Funktionsaufruf von cog_run Stackplatz einplant und die ID Nummer des gestarteten Kerns erfasst.
int *cog = cog_run(blink, 128);
Programmzeile 13
Mit pause(3000) verweilt das Programm für 3s in einer Pausenschleife. In der Zwischenzeit blinkt die LED an P26, da sie von einem anderen Cog gesteuert wird.
Programmzeile 14
Der Parameterwert der cog_end Funktion benutzt den Rückgabewert der Funktion cog_run. Erinnern wir uns, dass cog die Speicheradresse ist, bei der die ID Nummer des neu gestarteten Kerns abgelegt ist. Cog_end geht zur angegebenen Adresse, holt sich dort die ID Nummer und fährt den Cog herunter.
Die Funktion blink befindet sich unterhalb der Hauptfunktion main. Verwirrend mag sein, dass sich die Ausführungsroutine für die blinkende LED in einer Endlosschleife befindet und doch beendet wird. Das liegt daran, dass der Kern, der für die Ausführung dieser Programmzeilen verantwortlich zeichnete, mit der Funktion cog_end heruntergefahren wurde. Aufgerufen wurde diese Funktion vom Cog, der das Hauptmenü ausführt.
Recycle benutzte Cogs!
Fahre die Cogs, die nicht mehr benötigt werden in einem Programm, mit dem Aufruf der Funktion cog_end herunter; ganz egal, ob sie mit einer Endlosschleife arbeiten oder nicht. Damit wird Stackplatz freigegeben und der Cog ist für andere Aufgaben im Programm erneut einsetzbar.
Denke lokal und spare ein paar Bytes
In der eben vorgestellten Technik, bei der *cog als lokale Variable deklariert und von cog_end in der selben Funktion aufgerufen wurde, benötigt weniger Speicherplatz, als wenn man *cog global deklariert.
Werde global und delegiere
Wird *cog global deklariert, lässt er sich von jeder Funktion der Anwendung aufrufen, auch von denen, die von anderen Kernen gesteuert werden. Dafür benötigt man etwas mehr Programm-Speicherplatz, entlastet aber damit das Hauptprogramm von dieser Aufgabe.
Ein Cog-Pointer kann global oder lokal deklariert werden. Geschieht dies global, kann die Funktion cog_end von jeder Funktion einer Anwendung genutzt werden. Die cog_end Funktion kann zum Beispiel direkt in die Funktion blink eingebaut werden, so dass der Cog, der diese Funktion ausführt, sich selbst herunterfährt.
Shut-down mit globaler Variablen | |
Material |
|
Aufgaben |
|
Programm
Bis jetzt haben wir gezeigt, wie man einen Kern aus dem Hauptprogramm herunterfährt und wie ein Kern sich selbst herunterfährt. In der folgenden Übung werden zwei Cogs gestartet und es wird gezeigt, wie dieser Cog sich selbst und den anderen Cog herunterfährt.
Shut-down | |
Material |
|
Aufgaben |
|
Das Programm multicore_6.c
Programmcode, der mit verschiedenen Cogs arbeitet, kann über die Installation globaler Variablen gemeinsame Daten verwalten. Anders als bei der Arbeit mit Funktionen muss bei der Multicore-Programmierung globalen Variablen das Schlüsselwort volatile vorangestellt werden. Praktisch sieht das dann so aus, dass statt
die Deklaration lautet
Im folgenden Programmbeispiel wird eine globale volatile Variable mit Namen t deklariert, mit der die Hauptfunktion die Blinkrate einer LED steuert. Die Hauptfunktion legt den Wert von t fest und die Funktion blink benutzt den Wert von t, um die Pausenzeiten zwischen HIGH - LOW Zuständen zu setzen.
Kerne und Daten verwalten | |
Material |
|
Aufgaben |
|
Das Programm multicore_7.c
Wie arbeitet das Programm?
Das Programm startet mit drei Deklarationen (Zeilen 6 - 8). Die erste gibt uns Zugang zu allen Funktionen der Bibliotheksfunktion simpletools, die zweite ist eine forward-Deklaration für die Funktion blink, die nach der Hauptfunktion beschrieben ist und die dritte deklariert eine globale volatile Variable int dt. Da sie außerhalb einer Funktion deklariert wurde, ist sie global und damit für alle Funktionen des Programms verfügbar. Sie ist vom Typ volatile und kann damit von allen Funktionen, die von unterschiedlichen Kernen gesteuert werden, benutzt werden.
Im Hauptprogramm wird in Programmzeile 12 die globale Variable dt mit dem Wert 100 initialisiert. Mit dem Aufruf von cog_run in Zeile 13 wird ein neuer Cog gestartet (Cog 1), der die Funktion blink mit dem globalen Variablenwert dt = 100 in einer Endlosschleife ausführt.
In Zeile 14 passiert für 2s beim Cog 0 gar nichts, während Cog 1 die LED an P26 im Takt von 100ms blinken lässt.
In Zeile 16 wird der volatilen globalen Variablen dt ein neuer Wert 50 zugewiesen. Die Funktion blink und mit ihr Cog 1 sind sofort davon betroffen. Der alte Pausenwert 100ms wird durch den neuen 50ms Wert ersetzt und ausgeführt, mit dem Erfolg, dass die LED an P26 jetzt doppelt so schnell blinkt.
Nach weiteren 2s (Zeile 17) stoppt das Hauptprogramm Cog 1 (Shut down), der die Funktion blink steuerte.
Unterhalb des Hauptprogramms befindet sich die Funktion blink, die bereits aus den vorherigen Übungen bekannt sein sollte.
Im letzten Beispiel hat der Kern der Hauptfunktion das Verhalten einer Funktion, die von einem anderen Kern gesteuert wird, beeinflusst. Ein Kern kann das Verhalten eines anderen Kernes mit Hilfe einer Variablen überwachen. Im folgenden Beispiel wird geprüft, wie oft die Funktion blink eine LED ein- und ausgeschaltet hat.
Ein Kern überwacht einen andern Kern | |
Material |
|
Aufgaben |
|
Das Programm multicore_8.c
Die entsprechende Terminalausgabe zum Programm multicore_8.c zeigt Abb. 10.
Bisher wurde bei allen vorgestellten Programmen, in denen ein print-Befehl benutzt wurde, dieser in der Hauptroutine aufgerufen, die vom Cog 0 ausgeführt wird. Wie man einen Ausdruck auch von einem anderen Kern ausführen lassen kann, zeigt diese Übung.
Der Trick dabei ist, dass immer nur ein Kern zur Zeit mit dem SimpleIDE Terminal Verbindung aufnehmen kann und diese Verbindung geschlossen werden muss, bevor eine neue Verbindung aufgebaut wird.
Beim Prop-BoE und dem Prop-AB sind die I/O Ports P30 und P31 zum Programmieren und zur Kommunikation mit dem SimpleIDE Terminal über den USB Port vorgesehen.
print-Befehl mit verschiedenen Kernen ausführen | |
Material |
|
Aufgaben |
|
Das Programm multicore_9.c
Wie arbeitet das Programm?
Mit dem Einbinden der Bibliothek simpletools.h (Programmzeile 4) stehen die Funktionen print, simpleterm_close und simpleterm_open zur Verfügung.
Die Hauptfunktion main, die vom Cog 0 gesteuert wird, startet mit einer print-Anweisung, die einen Text im Terminal ausgibt.
int main()
{
print("Cog 0 ist jetzt dran ...\n");
simpleterm_close();
}
Anschließend wird die Verbindung zum Terminal geschlossen (Zeile 12) und ein anderer Cog kann sie wieder öffnen. In Zeile 13 wird die Ausführung der Funktion anderer von einem neuen Cog übernommen. Der Rückgabewert wird in der Variablen andererCog abgelegt.
Die Funktion anderer beginnt ab Programmzeile 17. Sie läuft unter der Regie von Cog 1, der in Zeile 19 den Zugang zum Terminal erneut öffnet und dann einen Text ausgibt. Anschließend wird der Zugang wieder geschlossen und mit einem Shut down Cog 1 aus seinen Diensten entlassen.
Einen Text oder Werte auszudrucken kostet Stackplatz - Einen einfachen Text durch einen anderen Kern auszudrucken kostet nicht viel Stackplatz. Der vorgegebene Wert von 128 kann auf den Minimalwert 10 reduziert werden und das Programm funktioniert immer noch.
Sobald ein Kern aber den Wert globaler Variablen überprüft und das Programm auch mit lokalen Variablen arbeitet und die Ergebnisse ausgedruckt werden sollen, muss ein entsprechender Stackbereich zur Verfügung gestellt werden. In einem solchen Fall ist ein Wert von 128 das Minimum. Erscheinen die benutzerdefinierten Druckanweisungen nicht so im Terminal wie erwartet, kann der Stackplatz für den entsprechenden Cog erhöht werden.
Der Aufbau einer seriellen Verbindung benötigt Zeit - Sollten nicht alle print-Anweisungen so im Terminal erscheinen wie erwartet, dann kann eine pause(100) Befehl direkt nach dem simpleterm_open Aufruf möglicherweise Abhilfe schaffen.
Nicht nur PRINT! Schau nach in der Datei simpletext.h - Es gibt eine Reihe von Funktionen in dieser Bibliothek(printi, scan, scani und alle Befehle, die mit put oder get beginnen), die mit dem SimpleIDE Terminal zusammenarbeiten. Wie schon beim print-Befehl gezeigt, können sie alle nur von einem Kern zur Zeit benutzt werden. Vorher muss die alte Terminalverbindung geschlossen und die neue geöffnet werden.