Hello, World!
Aus guter Tradition steckt man sich beim Einstieg in eine Programmiersprache
zunächst gerne das Ziel, ein Programm zu erstellen, das lediglich
eine simple Meldung auf den Bildschirm schreibt. Wer nun fürchtet,
einen Textstring Byte für Byte in den Bildschirmspeicher übertragen
zu müssen, kann beruhigt werden: Die sehr häufig gebrauchte Funktion
Nr. 9 des DOS-Interrupts 21h (oft auch mit "Int 21,9" bezeichnet) gibt
nämlich bereits ganze Zeichenketten auf dem Bildschirm aus und stellt
so gewissermaßen den PRINT-Befehl in Assembler dar. Die Zeichenkette
selbst darf sich an beliebiger Stelle im Datensegment befinden. Ihre Position,
d.h. der Offset des ersten Zeichens innerhalb des Datensegments, muß
vor dem Aufruf des Interrupts 21h in das Register DX geladen werden und
das Ende des auszugebenden Strings muß mit einem "$"-Zeichen gekennzeichnet
sein.
Hier nun das komplette Programm (ohne assemblerspezifischen Header):
mov dx,Offset
Meldung
mov ah,9
; Funktion 9
int 21h
; Print
mov ax,4C00h
int 21h
; beenden
Meldung: DB 'Hello, World!'
DB 10,13,'$'
Datenbereiche im Programm
Die Zeichenkette "Hello,World!" sowie ein Linefeed-, ein Carriage-Return-
und das Dollarzeichen liegen im Programm direkt hinter dem eigentlichen
Code. Die Assemblerdirektive "DB" (define Byte) bindet beliebige Bytefolgen
- die meisten Assembler akzeptieren auch Zeichenketten - direkt in das
Programm ein. Entsprechend setzt man die Direktiven "DW" und "DD" zur Definition
von Word- bzw. Doubleword-Speicherplätzen (16 bzw. 32 Bit) ein.
Das Label "Meldung" im Programm dient als Referenz auf die Zeichenkette:
Der Ausdruck "Offset Meldung" wird beim Assemblieren automatisch durch
die Position der Zeichenkette im Datenegment ersetzt.
Variablenspeicher
Datenbereiche mit variablem Inhalt bedürfen oft gar keiner Initialisierung. Für diesen Fall erlauben die Direktiven DB, DW und DD die Verwendung eines Fragezeichens als Argument. Solche Datenbereiche sollten stets am Ende des Programms angeordnet werden, damit der Assembler sie nicht unnötiger Weise mit Nullen gefüllt ins Programm einfügen muß. Besonders gilt dies für umfangreiche Byte-Felder (Arrays) wie im folgenden Beispiel, das unter Verwendung eines Stringbefehls mit Präfix (REP) 100 Bytes von Feld1 nach Feld2 kopiert:
mov
si,Offset Feld1
mov
di,Offset Feld2
mov
cx,100 ; Anzahl
cld
; aufwärts
REP
movsb
...
Feld1: DB 100 Dup(?)
Feld2: DB 100 Dup(?)
Dummy: DB ?
Natürlich ist es auch möglich, ohne den Umweg über die Register SI oder DI auf Speicherplätze zuzugreifen. Beispiel für 16-Bit- (Word-) Speicherzugriffe (Byte-Zugriffe funktionieren analog):
mov
ax,Word Ptr [Offset Zahl1]
add
ax,Word Ptr [Offset Zahl2]
mov
Word Ptr [Offset Summe],ax
...
Zahl1: DW 400
Zahl2: DW 500
Summe: DW ? ; wird 900
Die meisten Assembler unterstützen für die hier auftretenden Speicherreferenzen eine spezielle Form von Labels ohne Doppelpunkt, mit der sich das vorangehende Beispiel deutlich über sichtlicher darstellen läßt:
mov ax,Zahl1
add ax,Zahl2
mov Summe,ax
...
Zahl1 DW 400
Zahl2 DW 500
Summe DW ? ; wird 900
Auf Nummer sicher
Dank der "Pseudo-Initialisierung" mit "?" sind die beiden Arrays nicht
Bestandteil des COM- Programms. Allerdings ist nun nicht mehr sichergestellt,
daß DOS einen Programmstart verhindert, wenn nicht genügend
Speicher zur Verfügung steht. Natürlich könnte man argumentieren,
daß eine Speichererschöpfung bei einem sehr kurzen Programm
mit womöglich nur wenigen uninitialisierten Bytes in der Praxis kaum
vorkommt, aber auf solch unkalkulierbare Risiken sollte sich niemand einlassen.
Eine einfache Möglichkeit, den "Speicherstand" während der
Laufzeit zu testen besteht in der Auswertung des Stackpointers (Register
SP). Beim Programmstart wird SP von DOS nämlich so hoch initialisiert,
wie es die Menge des freien Speichers gestattet (jedoch maximal 0FFFEh,
weil das der letzte 16-Bit-Speicherplatz im gemeinsamen Code-/Daten-/Stack-Segment
ist). Da der Stapel abwärts dem Programmcode entgegenwächst,
muß ein gewisser Sicherheitsabstand (200 Bytes sollten es schon sein)
zum letzten verwendeten Speicherplatz vorhanden sein. In unserem Beispiel
würde man prüfen, ob die Differenz von SP und Offset Dummy mindestens
200 beträgt.
Noch nobler ist es aber, dem Betriebssystem zur Laufzeit mitzuteilen,
wieviel Speicher das Programm tatsächlich benötigt. Die Speicherverwaltung
von DOS gibt dann den überschüssigen Speicher wieder frei, was
beim Portfolio den Vorteil hat, daß sich dann meist die internen
Applikationen parallel nutzen lassen. Ein universeller Programmanfang,
der diese sogenannte Anpassung der Segmentlänge vornimmt, gestaltet
sich wie folgt:
Start:
mov bx,Offset Stapel+200
mov sp,bx ; Vorsicht!
mov cl,4
shr bx,cl ; bx/16
inc bx
mov ah,4Ah ; Segment
int 21h ; anpassen
jnc Speicher_ok
mov dx,Offset Fehler
mov ah,9
int 21h
mov ah,4Ch
int 21h ; beenden
Speicher_ok: ...
Fehler:
DB 'No Mem!',10,13,'$'
Stapel:
DB 200 Dup(?)
Hier wird zuerst der Stapelspeicher in den 200 Bytes großen Bereich
hinter der Stringkonstante verlegt (es dürfen sich keine Daten auf
dem Stapel befinden), um anschließend mit der Funktion 4Ah des Interrupts
21h allen weiteren Speicher freizugeben. Letzteres ist nur in
16-Byte-Portionen, den sogenannten Paragraphen, möglich. Die Anzahl der benötigten
Paragraphen wird durch eine Division der höchsten Speicheradresse
durch 16 mit anschließendem Aufrunden (Rechts-Shiften um 4 Bit und
Erhöhen um 1) ermittelt. Anmerkung: Die Berechnung zur Laufzeit ist
eigentlich unschön, aber mir ist es leider nicht gelungen, meinem
Assembler den Ausdruck (Offset Stapel)/16+1 schmackhaft zu machen.
Im Fehlerfall (nicht genug Speicher vorhanden) setzt DOS das Carry-Flag,
worauf die Fehlermeldung ausgegeben wird und das Programm abbricht.
Endlich: Tastatureingaben
Nach diesen zugegebenermaßen etwas abstrakten Betrachtungen soll
im folgenden wieder ein (be)greifbares Objekt im Mittelpunkt stehen: die
Tastatur.
Auch für Eingaben hält der Interrupt 21h passende Funktionen
parat. Am gebräuchlichsten dürften die Funktionen 1 und 8 sein,
die auf ein Zeichen von der Tastatur warten und dessen ASCII-Wert im Register
AL ablegen. Gleichzeitig stellt die Funktion 1 das getippte Zeichen auf
dem Bildschirm dar (Echo), wodurch sie sich von der Funktion 8 unterscheidet.
Eine Erwähnung verdient auch die Funktion 0Bh, die mit AL=0FFh
signalisiert, daß sich Zeichen im Tastaturpuffer befinden (leerer
Puffer: AL=0).
Zur Veranschaulichung folgt ein kurzes Programm, das den Benutzer so
lange Zeichen tippen läßt, bis die Escape-Taste (ASCII-Wert
27) betätigt wurde:
Start: mov ah,1
int 21h
cmp al,27 ; ESC?
jne Start
mov ah,4Ch
int 21h ; Ende
DOS oder BIOS?
Auch der vom BIOS belegte Interrupt 16h stellt ähnliche Funktionen
zur Tastaturabfrage bereit. So ist beispielsweise
mov ah,8
int 21h ; DOS
austauschbar durch
mov ah,0
int 16h ; BIOS.
Beide Programmfragmente warten auf eine Taste, wobei der ASCII-Wert
danach im Register AL steht. Die DOS-Variante bietet aber den Vorteil,
daß auch Eingabeumleitungen - etwa aus einer Steuerdatei - verarbeitet
werden können.
Das Tor zur Hardware
Die Bedeutung der Ein-/Ausgabeports und der zugehörigen Instruktionen
(IN und OUT) wurde in der letzten Ausgabe bereits angesprochen. Kommen
wir also gleich zur Sache. Die folgende Übersicht zeigt die wichtigsten
(hexadezimalen) Portadressen des Portfolio:
8000 Keyboard-Scancode (>128 => losgelassen)
8010 LCD controller Datenregister
8011 LCD controller Addressregister
8020 Soundchip (128 = aus)
8030 Power management
8040 Zähler (2 Hz)
8051 Batterie-Status (C2h=ok, 82h=leer)
8060 LCD-Kontrast
8070 Serial Interface
8078 Parallel Interface Datenregister (out)
8079 Parallel Interface Steuerregister (out)
807A Parallel Interface Statusregister (in)
Grüße von der Atari-Taste
Man mag es kaum glauben, aber es gibt Situationen, in denen einem weder
DOS noch das BIOS weiterhelfen. Oder kennt vielleicht jemand den ASCII-Wert
der Atari-Taste? Und wenn ja, wie lange wurde sie gedrückt? Oder:
Wieviele Tasten werden im Augenblick gedrückt gehalten?
Zwei Zeilen Assemblercode genügen, um festzustellen, welche Taste
zuletzt niedergedrückt oder losgelassen wurde:
mov dx,8000h
in al,dx
Jede der 63 Tasten des Portfolio besitzt einen eindeutigen "Make Code",
der beim Niederdrücken an der Portadresse 8000h erscheint. Beim Loslassen
einer Taste geschieht dasselbe mit ihrem "Break Code", der stets um 128
größer ist als der Make Code. Leider folgt die Numerierung
der Tasten einem sehr eigenwilligen Schema, doch mit Hilfe der untenstehenden
Tabelle ist es ein leichtes, beliebige Tasten zu detektieren.
Zur Ehrenrettung des BIOS muß noch ergänzt werden, daß
der portfoliospezifische Interrupt 61h sehr wohl eine eigene Funktion (2Fh)
zur Detektion der Atari-Taste besitzt (bei der "Fn"- Taste läßt
es einen dann aber doch im Stich). Mit dem Interrupt 61h werden wir uns
sicher noch einmal beschäftigen.
Make-
Make-
Taste Code
Taste Code
------------------------------
,
38 A
62
-
26 B
58
.
52 C
56
/
61 D
5
0
24 E
19
1
2 F
40
2
3 K
47
3
4 G
35
4
34 H
41
5
6 I
25
6
7 J
42
7
13 K
47
8
46 L
39
9
15 M
60
;
51 N
59
=
53 O
12
links 43
P 33
rechts 44
Q 10
oben 29
R 20
unten 37
S 32
lShift 27
T 21
rShift 36
U 11
Fn 54
V 57
Esc 63
W 17
Enter 22
X 55
Space 50
Z/Y 23
Ä
30 Y/Z 49
Ü
28 Alt
9
\ / < 48
Atari 0
+ / ] 31
BS 14
Caps 45
Ctrl 18
Del 8
Zur Verdeutlichung wieder ein konstruierter Programmausschnitt, in dem die Cursortasten zur Steuerung eines Ordinatenwerts dienen:
mov dx,8000h
in al,dx
cmp al,43 ; links?
jne nicht_li
dec x_Koo
nicht_li: cmp al,44 ; rechts?
jne nicht_re
inc x_Koo
nicht_re: ...
x_Koo DB ?
Vorschau
Nachdem sich nun Textausgaben als wenig spektakulär entpuppt haben, steht als nächstes der Einstieg in die Grafikprogrammierung bevor. Die relativ komfortablen BIOS-Funktionen eignen sich leider kaum für zeitkritische Anwendungen, weswegen es unumgänglich ist, sich mit dem LCD-Controller auseinanderzusetzen. Das und mehr gibt's beim nächsten Mal. Bleiben Sie dran!
Und wie immer: Fragen und Anmerkungen bitte an .
Eventuelle Korrekturen oder Ergänzungen werden zu finden sein
auf der WWW-Seite:
http://leute.server.de/peichl/pf.htm