Multitasking auf dem 89C2051
Einleitung
Die Projektierung
Das Programm
Zusammenfassung
|
Machen wir mal etwas ganz Verrücktes!
Diese Seite zeigt die Realisierung einer Idee, die mir schon
eine ganze Weile im Kopf herumschwirrte und bis jetzt ihrer
Realisierung harrte: ein echtes Multitasking-System
auf dem 89C2051 mit im Prinzip beliebig vielen Prozessen und
potentieller Echtzeitfähigkeit.
|
Einleitung
|
|
Multitasking bedeutet in letzter Konsequenz nur, mehrere Prozesse
quasiparallel ausführen zu können, die voneinander nichts mitbekommen,
also im Grunde so laufen, als wären sie allein auf dem Prozessor.
Drei verschiedene Prozesse bekommt man auch ohne größeren Aufwand
auf dem Rechner parallel zum laufen: Zum einen der Hauptprozess, der
gestartet wird, sobald der Prozessor Saft bekommt. Die anderen beiden
Prozesse lassen sich über die Interruptroutinen der beiden Timer
ausführen. Allerdings hat dies mehrere Nachteile: Zunächst
ist damit nach zwei Timern Schluss, mehr hat der 2051er nicht. Dann
müssen diese Prozesse speziell dafür designt sein, ständig gestartet
zu werden, da sie nicht kontinuierlich ablaufen. Und als weiteren
Nachteil ist der Rechner dann nicht Echtzeitfähig, weil eine minimale
Reaktionszeit für das Gesamtsystem nicht garantiert werden kann: die
Prozesse müssen die Kontrolle an das nächste Programm freiwillig
abgeben, und wenn der Prozess des Timers mit der höchsten Interruptpriorität
beschließt, in eine Endlosschleife zu verzweigen, stehen auch alle anderen
Prozesse.
Diesen Nachteilen kann mit dem hier vorgestellten Ansatz eines
Multitasking-Systems zu Leibe gerückt werden.
|
Die Anzahl der Prozesse
ist praktisch nur durch den verfügbaren Speicher begrenzt, was
allerdings eine ziemlich harte Einschränkung darstellt, man
muss etwas besser als üblich mit den Ressourcen umgehen :-).
Desweiteren laufen alle Prozesse unabhängig voneinander, soweit dies
auf einem MCS-51 Controller überhaupt möglich ist - schließlich hat
ein Microcontroller keinerlei Speicherschutzmechanismen, ein
marodierendes Programm kann durchaus die anderen Tasks überschreiben.
Aber Programmierfehler kann man sich auch mit einem Programm, welches
allein auf dem Controller läuft, nicht erlauben.
Was die Echtzeitfähigkeit betrifft, so ist diese natürlich auch
nur dann gewährleistet, wenn sich alle Tasks brav verhalten.
Für kritische Fälle wurde eine Funktion eingebaut, die
die Taskumschaltung sperrt und somit einem Prozess die
alleinige Kontrolle über die Rechenzeit des Controllers
ermöglicht.
Weiterhin lassen sich für jeden Task Prioritäten vergeben, d.h.
eine bestimmte Menge an Prozessorzeit zuteilen.
zurück zum Anfang
|
Die Projektierung
|
|
Das Konzept für dieses Multitasking-System ist einfach: für jeden
Task muss ein eigener Stack gesetzt werden. Bei einer Taskumschaltung
wird dann einfach der Stackpointer umgesetzt. Damit das funktioniert,
müssen noch alle veränderlichen Register des Tasks vor der
Umschaltung gesichert und hinterher wieder restauriert werden.
Beim PC ist das einfach gelöst: es werden einfach alle Register
gesichert. Dafür ist auf einem 8051-kompatiblen ohne
externes RAM einfach zu wenig Platz. Zudem
Damit also nicht zuviel Speicher verbraten wird, müssen die
|
veränderlichen Register individuell für jeden Task in eine Codetabelle
eingetragen werden, zu der dann die Taskumschaltroutine
verzweigt.
Die Taskumschaltung selbst bewerkstelligt Timer 0. So bleibt Timer
1 noch für die serielle Schnittstelle frei. Durch einfaches
Anhalten des Timers kann die Taskumschaltung gestoppt werden, und der
aktuelle Prozess hat die volle 'Rechenpower'. Nötig ist das
beispielsweise, wenn man eine weitere serielle Schnittstelle per
Software realisiert.
zurück zum Anfang
|
Das Programm
|
|
Das Programm ist ein in dieser Form vollständig ablauffähiges
Multitaskingsytem mit 5 Beispieltasks, die zu Demonstrationszwecken
von verschiedenen Ports lesen und schreiben, um die
Fähigkeiten des Systems unter Beweis zu stellen.
|
Bei dem Programm wird von den Assemblerfähigkeiten
sehr ausgiebig gebrauch gemacht, um die Taskanzahl möglichst
leicht anpassen zu können.
Vermutlich lässt sich das Programm darum nur noch auf dem ASEM-51
assemblieren.
|
;Autor : Erik Buchmann
;Projekt : Multitasking
; Quarz : 24 MHz
; Datum : 4.10.100
;---------------------------------------------------------------
$NOMOD51
$INCLUDE (89C1051.MCU)
|
Zuerst das übliche Gemüse: $NOMOD51 und $INCLUDE, um
den richtigen Prozessortyp einzustellen.
|
; Speicherbelegung
;---------------------------------------------------------------
TS_CURRTASK DATA 8
TS_R0BACKUP DATA 9
TS_ACCBACKUP DATA 10
TS_DPLBACKUP DATA 11
TS_DPHBACKUP DATA 12
TS_BBACKUP DATA 13
|
TS ist hier meine Abkürzung für 'TaskSwitching'.
CURRTASK enthält die Nummer des grade laufenden Task.
Weil die Taskumschaltung die Stackadresse selbst laufend
ändern muss, kann sie keine eigenen Register auf dem Stack sichern.
Stattdessen werden sie in den mit BACKUP benannten Bytes abgelegt.
|
TS_DATA DATA 14
|
TS_DATA ist der Beginn des Speicherfeldes mit den SP-Adressen der einzelnen
Tasks. Darum ist dafür genügend Platz vorzusehen.
|
; Ersatzvariablen
;---------------------------------------------------------------
TS_SP_TASK1 EQU 20
TS_SP_TASK2 EQU 40
TS_SP_TASK3 EQU 60
TS_SP_TASK4 EQU 80
TS_SP_TASK5 EQU 100
|
Die Startadressen der einzelnen Stacks jedes Tasks.
|
TS_TASKCOUNT EQU 5
|
Das ist die Anzahl der aktiven Tasks. Diese Zahl wird an verschiedenen
Stellen für die Grenze von Datenfeldern benutzt.
|
TS_STARTJMPVCT EQU 20
TS_SPACE4PANDP EQU 8
|
Diese Variablen bestimmen das Feld der PUSH- und POP-Befehle, die
die veränderlichen Bytes jedes Tasks auf dem Stack des jeweiligen
Tasks ablegen. Die erste Variable ist der der Parameter einer
ORG-Assembleranweisung, die den Anfang des Feldes bestimmt. Die zweite
legt den Abstand der einzelnen Feldeinträge fest. Da jeder Task eine
unterschiedliche Anzahl Bytes sichern muss, muss der Abstand zwischen den
Tabelleneinträgen gleich der Anzahl Bytes für die PUSH- oder POP-Operationen
+ AJMP für den Task mit den meisten Verändlichen sein.
|
org 0h
ajmp start
; Interruptroutinen
;---------------------------------------------------------------
org 0Bh
ajmp timer0_int
; Funktionen
;---------------------------------------------------------------
; Tabellen der push's und pop's
org TS_STARTJMPVCT + (TS_SPACE4PANDP*0) ; Task1
push 0
jmp TS_switching_pushready
org TS_STARTJMPVCT + (TS_SPACE4PANDP*1) ; Task2
push Acc
jmp TS_switching_pushready
org TS_STARTJMPVCT + (TS_SPACE4PANDP*2) ; Task3
push Acc
jmp TS_switching_pushready
org TS_STARTJMPVCT + (TS_SPACE4PANDP*3) ; Task4
push Acc
push 0
jmp TS_switching_pushready
org TS_STARTJMPVCT + (TS_SPACE4PANDP*4) ; Task5
jmp TS_switching_pushready
org TS_STARTJMPVCT + (TS_SPACE4PANDP*5) ; Task1
pop 0
jmp TS_switching_popready
org TS_STARTJMPVCT + (TS_SPACE4PANDP*6) ; Task2
pop Acc
jmp TS_switching_popready
org TS_STARTJMPVCT + (TS_SPACE4PANDP*7) ; Task3
pop Acc
jmp TS_switching_popready
org TS_STARTJMPVCT + (TS_SPACE4PANDP*8) ; Task4
pop 0
pop Acc
jmp TS_switching_popready
org TS_STARTJMPVCT + (TS_SPACE4PANDP*9) ; Task5
jmp TS_switching_popready
|
Das ist nun die Codetabelle der PUSH- und POP-Operationen
für jeden Task. Jeder Tabelleneintrag ist TS_SPACE4PANDP Bytes lang.
Daher läßt sich der gewünschte Einsprungspunkt in diese Tabelle
mit einer simplen Rechenoperation bestimmen.
Die Tabelle beginnt an TS_STARTJMPVCT und endet an
TS_STARTJMPVCT + (2 * TS_SPACE4PANDP * TS_TASKCOUNT)
|
; Tabelle der Prioritäten
; die Werte stehen für TL0,TH0
TS_priority:
db 223,177 ; Task1 10ms
db 223,177 ; Task2 10ms
db 239,216 ; Task3 5ms
db 223,177 ; Task4 10ms
db 103,197 ; Task5 7.5ms
|
Die Prioriätentabelle. Jeder Eintrag legt die erlaubte Laufzeit
für einen Task in Form der TL0/TH0-Parameter für den Timer fest.
|
; Eine eigene Lib mit Warteschleifen includen
$INCLUDE (time.inc)
; Arbeit getan, nächster Task kann den Prozessor haben
next_task:
; Interruptbehandlungsroutine Timer 0
; Hier beginnt die Task-Umschaltung
timer0_int:
; Speicher des letzten Tasks sichern
push PSW ; PSW muss sowieso gesichert werden
; die von der Taskumschaltung
; selbst genutzten Variablen sichern
mov TS_ACCBACKUP,a
mov TS_R0BACKUP,R0
mov TS_DPLBACKUP,DPL
mov TS_DPHBACKUP,DPH
mov TS_BBACKUP,b
|
Dies sind die Sicherungen von allgemeinen Registern. PSW muss sowieso
für jeden Task gesichert werden, weil darin die Flags enthalten sind,
die jede Rechenoperation verändert.
Die anderen Register werden von der Taskumschaltung selbst verändert
und müssen darum gesichert werden.
|
; variable Push's
mov DPTR,#TS_STARTJMPVCT
mov b,#TS_SPACE4PANDP
mov a,TS_CURRTASK
mul ab
jmp @A+DPTR
TS_switching_pushready:
|
Nun werden die vom letzten Task veränderten Register gesichert.
Dazu wird in die Tabelle mit den PUSH-Operationen verzweigt.
Der Tabelleneintrag wird anhand des Beginns der Tabelle, der Größe
der Tabellenfelder und der Nummer des noch aktiven Tasks ermittelt.
|
; SP sichern
mov R0,TS_CURRTASK
mov a,#TS_DATA
add a,R0
mov R0,a
mov @R0,SP
|
Der SP des noch aktiven Tasks wird im dafür vorgesehenen
Bytefeld, das an TS_DATA beginnt, gesichert.
|
inc R0 ; Zeiger für nächsten Task
inc TS_CURRTASK
; ist das Ende des Task-Feldes erreicht?
cjne R0,#TS_TASKCOUNT + TS_DATA,TS_Umschaltung_L1
mov R0,#TS_DATA ; Zähler zurücksetzen
mov TS_CURRTASK,#0
TS_Umschaltung_L1:
|
Hier wird die Nummer und parallel der Zeiger auf den
nächsten SP in TS_DATA ermittelt.
|
mov SP,@R0 ; SP wiederherstellen
|
Den SP des neuen Tasks holen
|
; die gesicherten Werte vom Stack holen
mov DPTR,#TS_STARTJMPVCT + (TS_SPACE4PANDP*TS_TASKCOUNT)
mov b,#TS_SPACE4PANDP
mov a,TS_CURRTASK
mul ab
jmp @A+DPTR
TS_switching_popready:
|
Analog zum Sichern der alten Task-Register werden hier die des neuen
restauriert.
|
; Timer laden
mov DPTR,#TS_priority
mov a,TS_CURRTASK
rl a
mov R0,a
inc R0
movc a,@A+DPTR
mov TL0,a
mov a,R0
movc a,@A+DPTR
mov TH0,a
|
Hier wird der Timer mit dem dem aktuellen Task zugeordnetem
TH/TL-Wert aus der Prioritätentabelle geladen.
|
mov R0,TS_R0BACKUP
mov a,TS_ACCBACKUP
mov DPL,TS_DPLBACKUP
mov DPH,TS_DPHBACKUP
mov b,TS_BBACKUP
pop PSW
|
Die von der Taskumschaltung selbst veränderten Variablen werden restauriert
und das am Anfang gesicherte PSW-Register wird wiederhergestellt.
|
reti
|
Die Taskumschaltung ist hiermit erfolgt. Mit diesem RETI wird mit
der nun aktivierte Task weiter abgearbeitet.
|
; Task-Switching abstellen
stop_switching:
clr TCON.4
clr IE.1
ret
; Task-Switching wieder anstellen
start_switching:
setb IE.1
setb TCON.4
ret
|
Start_switching und stop_switching erlauben es einem Task,
die volle Kontrolle über den Prozessor zu übernehmen und die
Taskumschaltung ausser Kraft zu setzen.
|
; Tasks initialisieren
; Parameter: R0 - Task-Nummer
; R1 - Startadresse des Stackpointer
; R2 - zusätzliche Bytes, die dieser Task auf dem Stack belegt
; DPTR - Einsprungadresse des Tasks
inittask:
; Für jeden Task vorbelegen:
; Einsprungadresse in den Task
; Adresse des Stackpointers für diesen Task im TS_DATA
; (Anfangsadresse SP + (Anzahl gesicherter Bytes-1) + 2 für RET-Adresse)
; PSW
push 1
mov @R1,DPL ; Einsprungadresse
inc R1
mov @R1,DPH
inc R1
mov @R1,#0 ; PSW-Vorbelegung
pop 1
|
Hier den Stack für den jeweiligen Task vorbereiten:
Die von RETI gelesene Rücksprungadresse ablegen und PSW vorbelegen.
|
dec R0
mov a,#TS_DATA
add a,R0
mov R0,a
mov a,R2
inc a
inc a
add a,R1
mov @R0,a
ret
|
Die Adresse des SP berechnen und im TS_DATA eintragen.
Die Adresse ist Startadresse + 2(für Rücksprungadresse) + Anzahl der
belegten Bytes auf dem Stack.
|
; Initialisierung
;---------------------------------------------------------------
start:
; Timer 0 aktivieren
; Software-Kontrolle
mov DPTR,#TS_priority
clr a
movc a,@A+DPTR
mov TL0,a
mov a,#1
movc a,@A+DPTR
mov TH0,a
|
Hier wird der Timer mit der Priorität des ersten Tasks geladen.
|
; die SFR's initialisieren
mov SP, #TS_SP_TASK1
mov TMOD,#1
mov IE, #128
mov P1,#00100000b
mov P3,#0
; Hauptprogramm
;---------------------------------------------------------------
main:
; Das Task-Switching vorbereiten
mov TS_CURRTASK,#0 ; Starten mit dem 1. Task
; Task1
; dieser Task läuft grade und wird beim nächsten
; Task-Wechsel sowieso gesichert. Eine Vorbelegung des
; Feldes ist daher nicht notwendig.
; Task2
mov DPTR,#task2 ; Einsprungadresse
mov R0,#2 ; Task-Nummer
mov R1,#TS_SP_TASK2 ; SP-Start für den Task
mov R2,#1 ; dieser Task push't 1 Byte (s. Tabelle)
call inittask
; Task3
mov DPTR,#task3
mov R0,#3
mov R1,#TS_SP_TASK3
mov R2,#1
call inittask
; Task4
mov DPTR,#task4
mov R0,#4
mov R1,#TS_SP_TASK4
mov R2,#2
call inittask
; Task5
mov DPTR,#task5
mov R0,#5
mov R1,#TS_SP_TASK5
mov R2,#0
call inittask
call start_switching
jmp task1
task1:
inc R0
mov P3,R0
call next_task ; Kontrolle an den nächsten Task abgeben
jmp task1
|
Das ist der erste Task. Als Aufgabe wird R0 incrementiert und
auf P3 ausgegeben.
Der Task demonstriert einen 'freiwilligen' Taskwechsel:
nachdem die Aktion fertig ausgeführt wurde, ist der nächste
Task an die Reihe, um keine Prozessorzeit mit warten zu verschwenden.
|
task2:
cpl P1.0
mov a,#100
call F_wait_m
jmp task2
|
Das ist ein simpler Arbeitstask: es wird P1.0 getoggelt und eine
Warteschleife ausgeführt.
|
task3:
cpl P1.2
mov a,#100
call F_wait_m
jmp task3
|
Task3 unterscheidet sich nur darin von Task2, dass er nur die
halbe Priorität besitzt. Damit wird die Fähigkeit der
Priorisierung demonstriert.
|
task4:
mov a,#1 ; warten
call F_wait_s
call stop_switching ; Alle anderen Tasks abstellen
mov R0,#14 ; Echtzeit-Kommandos ausführen
task4_L1:
mov a,#100
call F_wait_m
cpl P1.7
djnz R0,task4_L1
call start_switching ; Taskumschaltung wieder zulassen
jmp task4
|
Task 4 zeigt den Einsatz der Echtzeit-Funktion: in regelmäßigen
Zeitabständen wird mit exakt 100ms Takt P1.7 14x getoggelt.
|
task5:
; Arbeitstask: den Status von P1.5 nach P1.1 kopieren
mov c,P1.5
mov P1.1,c
call next_task ; Kontrolle an den nächsten Task abgeben
jmp task5
|
Der letzte Task liest bei jedem Aufruf P1.5 ein, kopiert dieses Bit
auf P1.1 und übergibt dann die Kontrolle freiwillig an den nächsten Task.
|
END
Zusammenfassung
|
|
Das hier vorgestellte Programm ist wenig mehr als ein interessantes
Konzept. Praktisch eingesetzt wurde es noch nicht, es war mehr
ein Experiment, um die Möglichkeiten des Controllers zu ergründen.
Dennoch ist es eine lohnenswerte Basis für eingene Experimente.
In der abgebildeten Form ist es ziemlich komplett. Spielraum für
Erweiterungen gibt es aber noch genug: so sind viele Einstellungen
statisch gehalten und werden vom Assembler selbst bei
der Assemblierung ausrechnet. Eine Änderung der Priorität
oder das Hinzufügen neuer Tasks zur
Laufzeit ist nicht implementiert. Eine bessere
Speicherverwaltung, die die Stacks der Tasks hintereinanderschiebt und
somit die Fragmentierung des Speichers verkleinert,
ist auch eine dankbare Aufgabe.
Damit das Ausprobieren leicht möglich ist, steht
der Quelltext zum Download bereit.
|
Um neue Tasks hinzuzufügen sind folgende Punkte zu beachten:
- Ist hinter TS_DATA noch genug Platz?
- Neue Stackadresse für den neuen Task festlegen.
- TS_TASKCOUNT erhöhen.
- In der TS_STARTJMPVCT-Liste die notwendigen PUSH- und POP-Befehle eintragen.
- TS_priority um einen entsprechenden Eintrag ergänzen.
- Im Hauptprogramm den Task initialisieren.
- Den Task programmieren.
Zum entfernen von Tasks sind die Schritte entsprechend umgekehrt
durchzuführen.
|
zurück zum Anfang
Erik Buchmann