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
Startseite Seite vor
Erik Buchmann
EMail an: Owner@ErikBuchmann.de