Effiziente, programmiererfreundliche Warteschleifen

Einleitung Zwei parallele Tasks Programm
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.

Code 1: Timer
 ORG 0Bh
; Interruptbehandlungsroutine Timer 0
        cpl P1.0
        reti

; Timer 0 aktivieren, 0.01 ms
        mov TL0, #235
        mov TH0, #235
        mov TMOD,#2
        mov TCON,#16
        mov IE,  #130

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.

Code 2: Warteschleife
; Warteschleife: 0.01 ms, 20 Maschinenzyklen
ws10us:
        push psw
        push 0
        mov  0,#3
ws10us_labelA:
        djnz 0,ws10us_labelA
        pop 0
        pop psw
        ret

start:
        call ws10us
        ; [... Aufgabe 1]
        call ws10us
        ; [... Aufgabe 2]
        call ws10us
        ; [... Aufgabe 3]
        jmp start

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.
zurück zum Anfang

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.

Code 3: Multitasking über Timer 0
;Autor     : Erik Buchmann
;Projekt   : Multitasking 2
; Quarz    : 24.000 MHz
; Generator: CodeGen Stable Version 1.0, Erik Buchmann '01
; Assembler: AS bzw. ASL
; Datum    : 29.11.03
;---------------------------------------------------------------
 CPU 8051
 INCLUDE stddef51

; Macros
;---------------------------------------------------------------
; Liste pushen
pushl MACRO regs
        IRP op, ALLARGS
        push op
        ENDM
      ENDM

; Liste popen
popl  MACRO regs
        IRP op, ALLARGS
        pop op
        ENDM
      ENDM
Diese Macros dienen nur dazu, die Tipparbeit bei den PUSH- und POP-Befehlen zu verkürzen. Leider sind sie assemblerspezifisch; Anwendern eines anderen Assemblers als AS müssen die push-Befehle ausschreiben.
; Konstanten-, Speicher- und Portbelegung
;---------------------------------------------------------------
 SEGMENT data
 ORG 10h

; Stack-Zeiger
rtSP:       db ?
mSP:        db ?

; Stacks
rtStack:    db 30 dup(?)
mStack:     db 30 dup(?)

Hier werden die Bytes zum zwischenspeichern der beiden Stack-Pointer definiert, sowie ein Platz von jeweils 30 Bytes für die Stacks selbst reserviert.
; Programmbeginn
;---------------------------------------------------------------
 SEGMENT code
 ORG 0h
        jmp start

; Interruptroutinen
;---------------------------------------------------------------
 ORG 0Bh
; Timer anhalten, während Ausgabeschleife läuft
        clr TR0

; Programm-Teil sichern
        pushl PSW, Acc, B, DPL, DPH
        mov mSP, SP

; Ausgabeschleifen-Teil restaurieren
        mov SP, rtSP
        popl DPH, DPL, B, Acc, PSW

; Einsprung zum unterbrochenen Programm
        ret
Dieser Codeblock wird ausgeführt, wenn Timer 0 einen Interrupt auslöst. Dies passiert immer dann, wenn die zuvor eingestellte Wartezeit erreicht wurde. Die Aufgabe dieser Timer-Routine ist nun, die Variablen des unterbrochenen Threads zu sichern bzw. die des Echtzeit-Threads wiederherzustellen; und dazu zwischen beiden Stacks umzuschalten, damit ein Thread nichts vom anderen bemerkt.

Es ist zu beachten, dass der Rücksprung in den Echtzeit-Thread mittels RET erfolgt, und nicht mit RETI. Sämtliche Interrupts sind also fürs erste gesperrt.

; Funktionen
;---------------------------------------------------------------
taskswitch:
; Ausgabeschleifen-Teil sichern
        pushl PSW, Acc, B, DPL, DPH
        mov rtSP, SP

; Programm-Teil restaurieren
        mov SP, mSP
        popl DPH, DPL, B, Acc, PSW

; Timer reaktivieren, Rücksprung
        setb TR0
        reti
Der Taskswitcher macht genau das Gegenteil der Interrupt-Routine: er beendet den Echtzeit-Thread, sichert die Register, aktiviert den Timer und schaltet für die Timer-Laufzeit zum anderen Thread um.

Die Umkschaltung geschieht hier mit RETI, sodass Interrupts ab hier auch wieder zugelassen sind.

;- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
; Warteschleife: 0.182 ms
;  Anzahl Maschinenzyklen: 364
timer0.182:
        mov TH0, #254
        mov TL0, #147
        call taskswitch
        ret

; Warteschleife: 1.25 ms
;  Anzahl Maschinenzyklen: 2500
timer1.25:
        mov TH0, #246
        mov TL0, #59
        call taskswitch
        ret
Diese Pseudo-Warteschleifen initialisieren den Timer mit der gewünschten Warteperiode. Obwohl mit Timer und Task-Switcher etwas relativ kompliziertes im Hintergrund abläuft, sind sie vom Programm aus genauso leicht zu benutzen wie ganz simple Warteschleifen: Aufrufen, und aus Sicht des Prozesses wird so lange blockiert, bis die vorgegebene Zeitspanne verstrichen ist.

Es ist zu beachten, dass nur der Echtzeit-Thread diese Methoden aufrufen darf, da der Timer während der Laufzeit des anderen Threads bereits aktiv und auf das Ausführen des Echtzeit-Threads eingestellt ist.

; Initialisierung
;---------------------------------------------------------------
start:
; Stacks initialisieren:
; für Echtzeit-Thread
        clr a
        mov SP, #rtStack
        mov DPTR, #rtThread
        setb RS0       ; Bank 1
        clr  RS1
        pushl DPL, DPH, PSW, Acc, Acc, Acc, Acc
        mov rtSP, SP

; für Standard-Thread
        clr RS0        ; Bank 0
        clr RS1
        mov SP, #mStack

; Timer 0 aktivieren
        mov TL0, #59
        mov TH0, #246
        mov TMOD,#1
        mov TCON,#16
        mov IE,  #130
Die Initialisierung ist relativ kompliziert, da sie den Stack des Echtzeit-Threads vorbereiten und den Timer einschalten muss. Nachdem der Timer einmal läuft, wird der Echtzeit-Thread bald vom Stack restauriert. Darum müssen zuvor, um Puffer-Unterläufe zu vermeiden, soviele Leer-Bytes auf dem Stack abgelegt werden, wie Register gesichert werden. Weiterhin ist das PSW-Register mit der aktivierten Registerbank 1 abzulegen, sowie die Einsprungadresse für die Echtzeit-Routine.
;---------------------------------------------------------------
; der Nicht-Echtzeit Thread
main:
        ; irgendwas
        jmp main

;---------------------------------------------------------------
; der Echtzeit-Thread
rtThread:
        call timer0.182
        ; irgendwas
        call timer1.25
        ; irgendwas
        jmp rtThread
END

zurück zum Anfang

Seite zurück Startseite Seite vor
Erik Buchmann
EMail an: Owner@ErikBuchmann.de