Genaues Timing ist ein leidiges Problem: einfach einzusetzende Warteschleifen verschwenden sinnlos Rechenzeit, aber die Verwendung von Timern ist jenseits simpler periodischer Aufrufe sehr komplex. Diese Seite stellt eine Möglichkeit vor, einen Timer nach Art der Warteschleifen einzusetzen, und zudem während der Warteperiode ein anderes Programm auszuführen. Damit ist die hier vorgestellte Methode eine direkte Nutzbarmachung der Multitasking-Experimente. | |||||||||||||||||||||||||||||
Einleitung |
|||||||||||||||||||||||||||||
Für einfache, periodisch wiederholte Aktionen wie z.B. bei der
Pulsweitenmodulation zur Steuerung von Motoren oder bei der Erzeugung
eines 38kHz-Signals für Infrarot-Fernbedienungen ist ein Timer
ideal. Der folgende Code-Schnipsel erzeugt beispielsweise ein
Rechtecksignal mit einer Pulsbreite von 10us an P1.0.
Für kompliziertere Aufgaben wird diese Lösung jedoch sehr rasch sehr aufwendig. Mein Stein des Anstoßes war die Implementierung des Märklin-Digital Protokolls für meine Modellbahn, die Datenpakete aus jeweils 18 Bits mit Pausen von 1.5, 4.0 und 6,0ms zwischen den Paketen benötigt. Mit einer einfachen Interrupt-Routine für den Timer wäre das nur schwer umzusetzen. Man müsste Status-Flags mitführen, nach jedem Aufruf den neuen Laufwert der Timer-Zähler TH und TL bestimmen, und jeweils an die richtige Position des Programms verzweigen -- das Debugging wird zum Albtraum. |
Die bequeme Lösung besteht im Einsatz von Warteschleifen.
Einfach für die benötigten Verzögerungen Schleifen
generieren -- mit Tools wie dem Code-Generator auf meiner Homepage
geht das vollautomatisch -- und an den entsprechenden Stellen im Code
einfügen. Der Einsatz ist kinderleicht, wie im folgenden
Beispiel zu sehen, und die logische Struktur des Codes bleibt
erhalten. Der Preis dafür besteht jedoch darin, dass die
Warteschleife im Grunde nutzlos Leistung verschwendet.
zurück zum Anfang |
||||||||||||||||||||||||||||
Zwei parallele Tasks |
|||||||||||||||||||||||||||||
Die Idee ist nun, zwei Programmteile (Threads) parallel
auszuführen, die voneinander unabhängig ablaufen. Der eine
Programmteil erhält Echtzeit-Priorität: er bestimmt, wann
welche Funktion ausgeführt werden soll, und hat gelegentliche
Warteperioden. Der andere Programmteil soll nun nur ablaufen,
während der Echtzeit-Thread in der Warteperiode schläft. In
meiner Anwendung übernimmt der Echtzeit-Thread die Ausgabe der
Datenpulse, während der Warte-Thread alle anderen, nicht
zeitkritischen Aufgaben wahrnimmt: Abfrage von Tastatureingaben,
Berechnung der auszugebenden Datenpulse etc.
Aus der Sicht des nicht echtzeitfähigen Hauptprogramms stellt sich alles wie gehabt dar: das Programm läuft ohne irgendwelche Zugeständnisse an andere Programmteile ab, nur gelegentlich unterbrochen von Timer-Interrupts. Das besondere am Echtzeit-Thread ist nun, dass auch für ihn keine speziellen Anpassungen notwendig sind, um mittels Timern präzise Timings einzuhalten! Der Trick liegt in der Task-Umschaltung, die vom Programm genauso aufgerufen wird wie die oben beispielhaft abgebildete Warteschleife. Die Task-Umschaltung sorgt nun dafür, dass nicht eine Schleife eine Anzahl von Maschinenzyklen verbrät, sondern die Ausführung für eine bestimmte Zeit an den anderen Programmteil übergeben wird. Dies geschieht folgendermaßen: |
zuerst wird die gewünschte Wartezeit in TH0 und TL0 geschrieben.
Dann werden die von beiden Programmteilen gemeinsam genutzten
Register auf dem Stack gesichert. Nun wird die aktuelle Position des
Stackpointers gespeichert, und der Stackpointer vom anderen
Programmteil geladen. Da nun dessen Stack aktiv ist, können
dessen vorher gesicherte Register vom Stack geholt werden.
Abschließend wird der Timer 0 aktiviert, und mit RETI
zur nun zuoberst auf dem Stack liegenden letzten
Ausführungsposition gesprungen.
Läuft Timer 0 ab, verzweigt der Controller zur Adresse 0Bh zur Interruptbehandlungsroutine, und legt dabei die letzte Ausführungsposition auf dem Stack ab. Da der Timer seine Aufgabe erfüllt hat, kann er nun stillgelegt werden. Das weitere Vorgehen ist analog zum oberen: Register sichern, Stacks umschalten, Register des Echtzeit-Threads wiederherstellen, mit RET zur letzten Position zurückspringen. Gesichert werden müssen unbedingt Acc und PSW. Die Rettung
der Register kann man vereinfachen, wenn man für beide
Programmteile unterschiedliche Registerbänke verwendet. Die
dafür nötigen Bits RS0 und RS1 liegen im PSW-Register, das
wegen des Carry-Flags ohnehin geschützt werden muss. Weitere
Register hängen von der Anwendung ab. Bei Divisionen kommt man
um das B-Register nicht umhin, und möchte man auf vorberechnete
Werte im Programm-Memory zugreifen, ist auch DPTR zu sichern. |
||||||||||||||||||||||||||||
Das Programm |
|||||||||||||||||||||||||||||
Bei der Adaption des Beispielprogramms an die eigenen Wünsche
ist im wesentlichen zu beachten, dass die Assemblerdirektiven
für Macros, die Segmentanweisungen zum Festlegen der Daten- und
Code-Segmente sowie einige weitere Feinheiten wie das Festlegen des
CPU-Typs Assembler-spezifisch sind. Kommt nicht wie bei diesem
Beispiel der AS zum Einsatz, so sind diese Assembler-Direktiven
anzupassen.
zurück zum Anfang |