Inhaltsverzeichnis
Assemblerprogrammierung für den Atari Portfolio
Teil 1: Überblick (aus PofoInfo 2/97)
Einleitung
Im Normalfall ist die Assemblerprogrammierung heute nicht mehr ganz zeitgemäß und kommerziell sicher nur noch in Spezialfällen rentabel. Als Kunde hat man sich schließlich an ständig zunehmende Programmgrößen gewöhnt, ja erwartet sie sogar. Wir Portfolio-Anwender wollen uns freilich eine derartige Verschwendungssucht nicht leisten und hier bietet sich die Assemblerprogrammierung zur Erstellung effizienter Programme an. Dieser Kurs soll zunächst eine allgemeine Einführung in diese „Programmiersprache“ geben und später portfoliospezifische Lösungen vorstellen. Da viele Leser bereits mit Pascal oder Basic vertraut sind, werden gewisse Programmiererfahrungen vorausgesetzt. Andererseits wird sich sicher die eine oder andere Stelle finden, die nicht jeder kritischen Prüfung standhält.
Doch jetzt zur Sache. Bekanntlich tut im Portfolio ein Intel-Prozessor namens 8088 seinen Dienst. Das Faszinierende an dieser 1979 mit dem 8086 (16-Bit-Datenbus, sonst wie 8088) eingeführten Architektur ist, daß sie uns bis in heutige PCs verfolgt. Wenn man aus einem Pentium-PC das Letzte herausholen möchte, ist deshalb auch heute das Erlernen der 8086-Maschinensprache durchaus noch aktuell.
Maschinensprache und Assembler
Jedes ausführbare Programm besteht letztlich aus einer Aneinanderreihung von Maschinenbefehlen, wobei ein Maschinenbefehl je nach Zahl der Operanden zwischen einem und sechs Bytes lang ist. Man kann ein Programm also als Folge von Byte-Werten (0…255) darstellen, vorzugsweise im hexadezimalen Zahlenformat, weil man dann mit 2 Ziffern pro Byte auskommt. Folgende 15 Bytes sind z.B. der Maschinencode für ein Programm, das einen kurzen Soundeffekt auf dem Portfolio erzeugt:
BA 20 80 31 C0 EE 48 75 FC B8 80 4C EE CD 21
In den Zeiten als die Programme noch kurz, Disketten Luxus und Mailboxen die Domäne von Hackern waren, wurden Listings ja gern in so einer Form gedruckt. Zur Programmentwicklung ist diese Darstellung jedoch denkbar ungeeignet. Um sich nicht für jeden Maschinenbefehl eine Zahl merken zu müssen, bedient man sich einer anderen Schreibweise. Für alle Maschinenbefehle des Prozessors existieren eingängige Abkürzungen, die meist aus drei Buchstaben bestehen und Mnemonics genannt werden. Ein Assemblerquelltext besteht hauptsächlich aus diesen Mnemonics und den zum jeweiligen Maschinenbefehl gehörenden Parametern. Dabei beginnt man für jeden Befehl eine neue Zeile, läßt links Platz für Sprungmarken und fügt eventuell rechts nach einem Strichpunkt einen Kommentar an. Obiges Beispiel stellt sich damit wie folgt dar:
mov dx,8020h ; Portadresse xor ax,ax ; Zähler auf 0 Marke: out dx,al ; Ton erzeugen dec ax ; Zähler -1 jnz Marke ; Schleife mov ax,4c80h out dx,al ; Ton aus int 21h ; beenden
Unser Handwerkszeug
Dieser Quelltext muß nun in zwei Schritten mit einem Assembler und einem Linker in ein lauffähiges Programm übersetzt werden. Man kann diese zwei Schritte mit einem Batchprogramm zusammenfassen. Die bekanntesten Assembler sind MASM von Microsoft (zugehöriger Linker: LINK), TASM (Linker: TLINK) von Borland sowie der Sharewareassembler A86. Ich selbst benutze Borlands Turbo Assembler, jedoch scheint mir A86 den einfachsten Einstieg zu ermöglichen, denn dieser kann ohne externen Linker obigen Beispielquelltext direkt in ein .com-Programm übersetzen. Zudem ist er mit 23kB (Version 3.22) mit Abstand der kleinste Vertreter seiner Gattung und läuft damit sogar direkt auf dem Portfolio. A86.COM befand sich auf der Begleitdiskette zur ersten Ausgabe der Pofoinfo, ist aber auch auf der Club-CD, in der Club-Mailbox und in vielen Shareware-Archiven vorhanden. Turbo-Pascal (6.0ff) wartet mit einem integrierten Assembler auf. Allerdings bettet es den damit erzeugten Code in ein unhandliches EXE-Programm ein. Wer noch keines dieser Programme besitzt, kann zum Experimentieren vorerst auf DEBUG zurückgreifen.
Neben einem Assembler sollten zur Grundausstattung aber auch Nachschlagewerke zu den Themen Prozessorarchitektur, Befehlssatz und Betriebssystem gehören, denn eine ausführliche Behandlung im Rahmen dieses Kurses ist kaum möglich. Eine große Hilfe ist hier eine Online-Referenz, wie z.B. das Shareware-Programm HelpPC für DOS von David Jurgens, das alle genannten Themenkomplexe abdeckt (Quelle: z.B. ftp://ftp.simtel.net/pub/simtelnet/msdos/info/helppc21.zip, Größe ca. 255 KiB).
Das erste Programm
Um unser Beispielprogramm zu erzeugen gibt es nun folgende Möglichkeiten:
1. Verwendung von DEBUG.COM
Das Dienstprogramm Debug gehört zum Lieferumfang von MSDOS und eignet sich vor allem zur Fehlersuche in Maschinenprogrammen. Es ermöglicht aber auch die Eingabe von Bytefolgen und sogar Mnemonics. Trotzdem haben wir es nicht mit einem ausgewachsenen Assembler zu tun, da z.B. keine symbolischen Adressen verwendet werden dürfen, wie oben mit „Marke“ geschehen.
Die wichtigsten Befehle, die in Debug immer hinter einem Minuszeichen als Prompt eingegeben werden, lauten:
Kommando | Funktion |
---|---|
? | Hilfe (nur neuere Versionen) |
e Adresse | Eingabe einer Bytefolge |
a | Assemble |
u | Unassemble |
nName | Dateiname festlegen |
r cx | Dateilänge festlegen |
w | Write |
Weiterführende Erläuterungen zu Debug sowie eine ältere Version (knapp 10KB), die auch auf dem PF funktioniert, finden sich auf der Club-CD. Unser Programm können wir damit bereits eingeben, und alle Debug-Neulinge sollten dies jetzt einmal versuchen. Zur Orientierung diene dabei das folgende Bildschirmprotokoll:
C:\>debug ton.com File not found -a 0F38:0100 mov dx,8020 0F38:0103 xor ax,ax 0F38:0105 out dx,al 0F38:0106 dec ax 0F38:0107 jnz 105 0F38:0109 mov ax,4c80 0F38:010C out dx,al 0F38:010D int 21 0F38:010F -r cx CX 0000 :f -w Writing 0000F bytes -q
Fertig! Anstelle der Sprungmarke „Marke“ mußte hier eine der Speicheradressen eingesetzt werden, die vor jedem Befehl am linken Rand erscheinen. Nach dem Verlassen von Debug sollte das Programm TON.COM mit einer Länge von 15 Bytes im aktuellen Directory zu finden sein. Noch etwas schneller gelangt man ans Ziel, wenn man die 15 Bytes mit Hilfe des E-Kommandos von Debug direkt im Hexcode eingibt. Hierbei ist zu beachten, daß nach den zwei Zeichen jedes Bytes Space betätigt werden muß, nach dem letzten Byte stattdessen Enter. Ach ja: Dieses Programm läuft nur auf dem Portfolio und könnte einen normalen PC zum Absturz bringen.
2. Assemblierung mit A86
Vor der Anwendung eines Assemblers muß man den Quelltext mit einem normalen ASCII-Editor in eine Datei schreiben, die man gewöhnlich mit der Endung .ASM versieht. Zur Demonstration tippe man hierzu unser Beispiel (in der Form wie es vor „Unser Handwerkszeug“ steht) ab und nenne es TON.ASM. Der Rest ist wirklich kinderleicht:
C:\>a86 ton.asm A86 macro assembler, V3.22 Copyright 1990 Eric Isaacson Sources: ton.asm Object: ton.COM Symbols: ton.SYM
Das Resultat ist unser gewünschtes Programm TON.COM. Die Symbollistendatei TON.SYM benötigen wir nicht und können sie auch löschen. Wer sich den generierten Code genauer ansieht, wird feststellen, daß das vierte Byte von der mit Debug erzeugten Version abweicht (33h statt 31h). Das liegt daran, daß sich manche Assemblerbefehle unterschiedlich codieren lassen. Der Autor des A86 verwendet bewußt ungewöhnliche Codierungen, um feststellen zu können, ob ein Programm mit seinem Assembler erzeugt wurde. Diese Spuren lassen sich aber leicht verwischen, indem man den Code z.B. mit Debug erst disassembliert und dann wieder assembliert.
3. Das Borland-Gespann TASM und TLINK
Der klassische Weg vom Quelltext zum Code führt über die sogenannte Objektdatei. Sie wird vom Assembler aus dem Quelltext erzeugt und enthält einen Mix aus Symbolen und Code. Erst durch das anschließende Linken gelangt man zum ausführbaren Programm. Dieses Verfahren ist zwar vielseitiger (man kann z.B. mehrere Objektdateien zusammenlinken), für den Einsteiger allerdings erst einmal umständlicher als die direkte Methode von A86. Damit aber nicht genug: Der Quelltext bedarf noch ein paar Ergänzungen:
.model tiny .code ORG 100h Start: mov dx,8020h ; Portadresse xor ax,ax ; Zähler auf 0 Marke: out dx,al ; Ton erzeugen dec ax ; Zähler -1 jnz Marke ; Schleife mov ax,4c80h out dx,al ; Ton aus int 21h ; beenden END Start
Die hinzugekommenen Assemblerdirektiven machen Angaben über das Speichermodell, den Beginn des Codesegments und die Einsprungadresse. Die hier verwendeten Minimaleinstellungen dürften für jedes Portfolioprogramm ausreichend sein und können jedesmal so übernommen werden. Hat man TON.ASM wie oben ergänzt, kann die Assemblierung folgen (hier auf dem Desktop-PC):
C:\>tasm ton.asm Turbo Assembler Version 2.0 Copyright (c) 1988, 1990 Borland International Assembling file: ton.asm Error messages: None Warning messages: None Passes: 1 Remaining memory: 474k
Die erzeugte Objektdatei muß jetzt gelinkt werden. Die Option /t gibt an, daß ein .COM-Programm erstellt werden soll:
C:\>tlink /t ton.obj Turbo Link Version 3.0 Copyright (c) 1987, 1990 Borland International
Das fertige TON.COM ist übrigens identisch zu der mit A86 erzeugten Version (was nicht selbstverständlich ist). Die zusätzlich entstandenen Dateien TON.OBJ und TON.MAP können gelöscht werden.
4. Microsoft MASM und LINK
Das umständlichste - pardon professionellste - Werkzeug stammt, wie sollte es anders sein, aus dem Hause Microsoft. Die hier verwendete MASM-Version 1.25 ist allerdings schon leicht in die Jahre gekommen, so daß das Beispiel eventuell nicht mit aktuelleren Versionen nachvollziehbar ist. Trotzdem hier der angepaßte Quelltext:
code SEGMENT ASSUME CS:code ORG 100h Start: mov dx,8020h ; Portadresse xor ax,ax ; Zähler auf 0 Marke: out dx,al ; Ton erzeugen dec ax ; Zähler -1 jnz Marke ; Schleife mov ax,4c80h out dx,al ; Ton aus int 21h ; beenden code ENDS END Start
Bis zum Endprodukt sind jetzt drei Schritte erforderlich. Zunächst wie beim Borland-Produkt die Assemblierung und das Linken. LINK erzeugt allerdings eine ausführbare Datei im .EXE-Format (783 Bytes), die danach noch ins COM-Format konvertiert werden muß:
C:\>masm ton.asm The Microsoft MACRO Assembler , Version 1.25 Copyright (C) Microsoft Corp 1981,82,83 Object filename [ton.OBJ]: Source listing [NUL.LST]: Cross reference [NUL.CRF]: Warning Severe Errors Errors 0 0 C:\>link ton.obj Microsoft Object Linker V2.01 (Large) (C) Copyright 1982, 1983 by Microsoft Inc. Run File [TON.EXE]: List File [NUL.MAP]: Libraries [.LIB]: Warning: No STACK segment There was 1 error detected. C:\>exe2bin ton.exe ton.com
Die Warnung können wir ignorieren, und die Dateien TON.OBJ, TON.MAP, TON.EXE löschen. Das Resultat ist wieder genau die selbe 15 Bytes lange Datei TON.COM, die auch schon die beiden anderen Assembler geliefert haben.
5. Der integrierte Assembler von Turbo Pascal
Mit der Anweisung ASM lassen sich Assemblerbefehle auch in ein Pascalprogramm einbauen. Dabei ist zu beachten, daß Labels mit dem Klammeraffen beginnen müssen und Kommentare in geschweifte Klammern gestellt werden müssen:
begin ASM mov dx,8020h {Portadresse } xor ax,ax {Zähler auf 0 } @Marke: out dx,al {Ton erzeugen } dec ax {Zähler -1 } jnz @Marke {Schleife } mov ax,4c80h out dx,al {Ton aus } int 21h {beenden } end; end.
Kompiliert mit TP 6.0 ergibt dieser Quelltext ein 1488 Bytes langes .EXE-Programm. Da der Befehl int 21h das Programm vorzeitig beendet, sollte man diese Zeile besser weglassen. Es ist aber auch möglich (z.B. mit DEBUG), unsere 15 Bytes aus dem .EXE Programm herauszuschneiden und als .COM-Programm zu speichern. Der betreffende Abschnitt reicht übrigens vom 16. bis zum 30. der 1488 Bytes.
Wie geht's weiter?
Wer selbst Assemblerprogramme für den Portfolio entwickeln will, kommt nicht umhin, sich ein wenig mit der Architektur (insbes. Registersatz) des 8086 vertraut zu machen. Außerdem benötigen wir einen gewissen Vorrat an Befehlen. Zum Glück ist man hier mit einem guten Dutzend schon recht gut gewappnet. Ein weiterer Schwerpunkt ist die Ein- und Ausgabe, sowohl über das Betriebssystem als auch durch direkte Hardwarezugriffe. Aus Platzgründen können diese Themen leider erst in den kommenden Heften behandelt werden.
Teil 2: Prozessorregister und Befehlssatz (aus PofoInfo 3/97)
Der erste Teil dieses Kurses zeigte ausgehend von einem gegebenen Assemblerlisting den Weg über Compiler und Linker hin bis zum ausführbaren COM-Programm. Hierzu noch ein Nachtrag:
Von Jan Laitenberger gibt es den Freeware-Assembler JASMIN, der unkomprimiert nur knapp 20 KByte groß ist und speziell für den Einsatz auf dem Portfolio konzipiert wurde. Der begrenzte Funktionsumfang (keine Makros, nur 8086/88, nur COM-Programme) ermöglicht eine sehr einfache Bedienung, so daß JASMIN besonders für Einsteiger gut geeignet ist. Trotz seiner Eigenheiten (Labels müssen mit @ beginnen, Speicherzugriffe sind durch eckige Klammern kenntlich zu machen) ist dieser Assembler ein Muß für Portfolio-Fans! (Bezugsquelle: http://www.franksteinberg.de/ZIPS/JASMIN16.ZIP)
Update 12.11.2024: Robert Riebisch hat dem PofoWiki die Datei jasmin16s.zip, die auch den JASMIN-Quellcode für Turbo Pascal 6.0 enthält, zur Verfügung gestellt.
Auszug aus jasmin.txt
:
Seit dem 7.10.2006 ist fuer dieses Programm auch der Source-Code beigefuegt. Dieser darf in nicht-kommerziellen Programmen verwendet werden. Jegliche kom[m]erzielle Verwendung des Sourcecodes im Ganzen oder in Teilen bedarf der schriftlichen Einwilligung des Autors!
Vielleicht wartet der eine oder andere Leser bereits ungeduldig darauf, endlich die ersten 8086-Befehle (der 8088 ist bekanntlich kompatibel) zu lernen, um selbst ein Assemblerprogramm zu „stricken“. Und in der Tat ist man mit einem Dutzend gelernten Befehlen schon ganz brauchbar gerüstet. Zunächst sollte der Neuling jedoch die (zugegebenermaßen etwas chaotische) Architektur des Prozessors kennenlernen:
Die Register des Prozessors
Die Hauptbeschäftigung des Prozessors ist es, Befehle und Daten aus dem Speicher zu holen, die Daten zu verarbeiten und z.B. an eine andere Stelle des Speichers zurückzuschreiben, etwa in den Grafikspeicher. Zur Zwischenspeicherung im Prozessor dienen dabei die Register. Die 14 Register des 8088, die alle 16 Bit breit sind, sollte man beim Programmieren stets vor Augen haben. Es folgt hier deshalb eine Übersicht mit knapper Erläuterung. Das 2-Zeichen-Kürzel zur Benennung eines Registers weist auf seinen bevorzugten Verwendungszweck hin, denn jedes Register besitzt besondere Fähigkeiten. Einige der dazu benötigten Spezialbefehle sind in der Übersicht aufgeführt, auch wenn der Anfänger sich darum zunächst nicht zu kümmern braucht.
1. Die vier Allzweckregister (z.B. für arithmetische und logische Operationen)
Diese Register sind am vielseitigsten einsetzbar. Die oberen bzw. unteren 8 Bits lassen sich auch getrennt verwenden, d.h. es stehen hiermit bis zu acht 8-Bit-Register zur Verfügung. Für die ersten Assembler-Gehversuche reichen die Allzweckregister bereits.
Register | Name/Funktion | Typische Operation |
---|---|---|
AX = AH+AL | Akkumulator | MUL, DIV |
BX = BH+BL | Basisregister | XLAT |
CX = CH+CL | Count-Register | LOOP |
DX = DH+DL | Daten-Register | IN, OUT |
2. Die Index- und Pointerregister
Abgesehen von SP und IP dürfen auch diese Register nach Belieben verwendet werden. Insbesondere eignen sie sich aber zur indirekten Adressierung und für die sog. Stringbefehle.
Register | Name/Funktion | Typische Operation |
---|---|---|
IP | Instruction Pointer (Finger weg!) | |
SP | Stackpointer (Finger weg!) | PUSH, POP |
BP | Basepointer | |
SI | Sourceindex | LODSB |
DI | Destination index | STOSB |
3. Die Segmentregister
Die Segmentregister haben eine spezielle Funktion, die später noch genauer erklärt wird. Nur ES steht zur freien Verfügung.
Register | Name/Funktion | Typische Operation |
---|---|---|
CS | Codesegment (Finger weg!) | |
DS | Datensegment (Vorsicht!) | Ändern des Datensegments: MOV DS,AX |
SS | Stacksegment (Vorsicht!) | Adressierung mit Segment-Präfix: MOV AL,[SS:BX] |
ES | Extrasegment | STOSB |
4. Das Flag-Register
Dieses Register, auf das nicht direkt zugegriffen werden kann, benötigt der Prozessor intern zur Speicherung von Status-Bits, den sog. Flags. Das für uns wichtigste Flag ist das Zero-Flag, welches die Gleichheit der beiden Operanden des letzten Compare-Befehls (CMP) signalisiert. Die Abfrage erfolgt mit dem bedingten Sprungbefehl JZ (jump if zero), der nur bei gesetztem Zero-Flag vollzogen wird.
Die Sache mit den Segmenten
Standardmäßig werden alle Speicherzugriffe über 16-Bit-Adressen abgewickelt, schließlich stehen uns ja auch nur 16 Bit breite Register zur Verfügung. Beispielsweise lädt der Befehl „mov al,[si]“ das Register al mit dem Byte-Wert, der an der Speicherstelle steht, die durch das Register si bestimmt ist (indirekte Adressierung). Bekanntlich lassen sich mit 16 Bit aber nur 65536 Speicherplätze, sprich 64 KByte adressieren. Mit den vier Segmentregistern können vier Bereiche zu je 64 KByte aus dem gesamten Adreßraum des Prozessors (1 MByte) ausgewählt werden. Diese Segmente lassen sich in 16-Byte-Schritten plazieren und können sich auch überlappen. Für kleine Programme ist es aber oft überhaupt nicht nötig, die Segmente zu verschieben.
Wie die Namen der Segmentregister vermuten lassen, ist es vorgesehen, für Programmcode, Daten und den Stack (Zwischenspeicher für Unterprogramme) jeweils ein eigenes Segment zu spendieren. Für uns spielt das aber keine Rolle, weil das Betriebssystem bei COM-Programmen die drei Segmentregister CS, DS und SS mit dem selben Wert initialisiert. Es besteht also zum Glück kaum die Chance, einen Befehl zu verwenden, der sich nicht auf das gewünschte Segment bezieht. Allerdings muß sich ein COM-Programm deshalb mit höchstens 64 KByte begnügen, was aber für die meisten Portfolio-Programme mehr als ausreichend ist. Zudem sind compilierte Assemblerprogramme so kompakt, daß ihre Größe in der Regel nur etwa 1/10 von der des Quelltexts beträgt.
Eine handvoll Befehle
Wie gesagt läßt sich mit einigen wenigen Assemblerbefehlen bereits eine Menge (im Prinzip alles, nur nicht immer sehr elegant) anfangen. Da der Befehlssatz des 8088 zu komplex für eine komplette Betrachtung an dieser Stelle ist, beschränken wir uns auf folgende oft benötigte Befehle: MOV, CMP, JMP, JZ, ADD, SUB, IN, OUT, INT, CALL, RET. Die Wirkungsweise dieser (und anderer) Befehle läßt sich dank der Registeranzeige gut mit DEBUG erforschen. Viele Befehle erlauben nur bestimmte Adressierungsarten oder sind nicht auf alle Register anwendbar. Hier heißt es studieren oder probieren!
MOV Ziel,Quelle
Der vielseitigste und meist genutzte Assemblerbefehl ist sicherlich der MOV-Befehl (move). Er überträgt ein Datenwort z.B. aus dem Speicher in ein Register oder aus einem Register in ein anderes. Zudem unterstützt dieser Befehl verschiedene Adressierungsarten für Speicherzugriffe. Hierzu eine Auswahl an Beispielen:
mov al,2 ; Lädt das Register al mit dem Wert 2 mov al,[2] ; Lädt al mit dem Byte-Wert, der an der ; Speicherstelle 2 im Datensegment steht mov ax,bx ; Kopiert bx nach ax mov al,[bx] ; Lädt al mit dem Byte-Wert, der an der Speicherstelle ; bx im Datensegment steht (indirekte Adressierung) mov cl,[si+3] ; Lädt cl mit dem Byte-Wert, der an der Speicherstelle ; si+3 im Datensegment steht (indirekte indizierte Adr.) mov [7],dh ; Speichert dh an der Speicherstelle 7 im Datensegment
Diese Beispiele wickeln den Datenaustausch mit dem Speicher nur über die 8-Bit-„Halbregister“ ab, wodurch genau 1 Byte im Speicher angesprochen wird. Es sind aber auch 16-Bit-Speicherzugriffe möglich. Dabei ist es oft hilfreich zu wissen, daß gemäß Intel-Konvention an der angegebenen Speicherstelle das niederwertige Byte (Lowbyte) und an der darauffolgenden Speicherstelle das höherwertige Byte (Highbyte) angesiedelt ist. Wer sich mit DEBUG einmal den Maschinencode für obige Varianten des MOV-Befehls ansieht, kann feststellen, daß wir es hier eigentlich mit verschiedenen Maschinenbefehlen zu tun haben:
B002 MOV AL,02 A00200 MOV AL,[0002] 89D8 MOV AX,BX 8A07 MOV AL,[BX] 8A4C03 MOV CL,[SI+03] 88360700 MOV [0007],DH
Besonders effizient codiert wurden die ersten beiden Zeilen: Die Befehlscodes (OP-Codes) bestehen dort aus einem einzigen Byte (B0 bzw. A0), gefolgt von einem bzw. zwei Bytes für den zweiten Parameter. Man sieht auch, daß die 16-Bit-Konstante 0002 im Format Lowbyte/Highbyte gespeichert ist. Die anderen vier - etwas weniger gebräuchlichen - Varianten benötigen jeweils 2 Bytes für den OP-Code und bis zu zwei weitere Bytes für Konstanten.
CMP Operand1,Operand2
Wie erwähnt, dient der Befehl CMP dem Vergleich zweier (gleichberechtigter) Operanden. Intern führt der Prozessor dazu eine Subtraktion durch und setzt das Zero-Flag, falls das Ergebnis null ist.
JZ Sprungziel (jump if zero)
Dieser bedingte Sprungbefehl setzt den Instruction pointer auf die angegebene Adresse, falls das Zero-Flag gesetzt ist. Die Kombination aus CMP und JZ entspricht etwa folgender BASIC-Anweisung:
IF Operand1=Operand2 THEN GOTO Sprungziel
Zum Befehl JZ existiert die alternative Schreibweise JE (jump if equal), die im Einzelfall zum besseren Verständnis verwendet werden kann. Soll ein Sprung erfolgen, wenn der letzte Vergleich keine Übereinstimmung ergab, setzt man den Befehl JNZ (jump if not zero) oder JNE ein. Beispiel:
cmp al,27 ; ist al=27? jz Escape ; Ja: Springe zur Marke 'Escape'
JMP Sprungziel
Unbedingter Sprungbefehl. BASIC-Äquivalent: „GOTO Sprungziel“
INT Nummer
Dieser Befehl ruft die durch Nummer spezifizierte Interruptroutine auf. Der INT-Befehl stellt die Schnittstelle zum Betriebssystem dar, denn sowohl BIOS als auch DOS belegen etliche Interruptvektoren mit Routinen, die von Anwendungsprogrammen genutzt werden können (und sollen). Je nach Funktion müssen zuvor diverse Parameter in bestimmten Registern abgelegt werden. Beispiel:
mov ah,7 ; 7=Zeicheneingabe nach al int 21h ; 21=wichtigster DOS-Interrupt mov dl,al ; eingelesenes Zeichen mov ah,6 ; 6=Zeichenausgabe (dl=ASCII) int 21h
Übrigens lautet der Maschinencode für Int 21h 'CD 21' (dezimal 205, 33) und dürfte eine der häufigsten Bytekombinationen in DOS-Programmen darstellen.
IN Register,dx
Eine wichtige Eigenart der 80×86-Prozessorfamilie sind die Portadressen. Parallel zum Arbeitsspeicher existiert ein 64 KByte großer Adreßraum, über den Peripheriebausteine (z.B. paralleles Interface) angesprochen werden. Der Prozessorbefehl IN liest einen 8-Bit- oder 16-Bit-Wert von der Portadresse dx ein. Folgendes Beispiel übernimmt den Zustand der Statusleitungen des Druckerports in das Register al:
mov dx,807Ah in al,dx ; nur dx möglich!
OUT dx,Register
Als Pendant zu IN schreibt OUT einen 8-Bit- oder 16-Bit-Wert an die Portadresse dx. IN und OUT werden benötigt, wenn am Betriebssystem „vorbeiprogrammiert“ werden soll, um Hardwarezugriffe zu beschleunigen. Der Grafikmodus des Portfolio läßt sich so überhaupt erst effizient nutzen. Der nächste Kursteil wird sich damit genauer befassen.
ADD Operand1,Operand2
Addiert zum Operand1 den Operand2. Falls das Ergebnis zu groß für Operand1 ist, wird das Carry-Flag gesetzt (Abfrage z.B. über den bedingten Sprungbefehl JC - jump if carry), welches somit das im Ergebnis fehlende höchste Bit repräsentiert. Beispiel:
mov cx,1997 ; cx=1997 add cx,3 ; cx=2000
SUB Operand1,Operand2
Subtraktion analog zu ADD. Beispiel:
mov bx,0605h ; bh=6, bl=5 sub bl,bh ; bl=FFh
Hier wird das Carry-Flag gesetzt, was bedeutet, daß FFh als -1 zu interpretieren ist.
CALL Sprungziel und RET
Der pure Luxus: Strukturierte Programmierung in Maschinensprache durch Unterprogramme! CALL springt an den Anfang des Unterprogramms und sichert automatisch die Rücksprungadresse auf dem Stack. RET beendet das Unterprogramm, indem es die Rücksprungadresse wieder vom Stack holt, um das Programm mit dem Befehl hinter 'CALL' fortzusetzen. Ein anständiges Unterprogramm sichert zunächst alle verwendeten Register mit PUSH auf dem Stack und stellt sie am Ende mit POP wieder her (Diese Befehle sind allerdings - wie auch CALL und RET - mit etwa 20 Takten recht langsam). Beispiel:
... call PortAus ... PortAus: push dx ; Register push al ; retten mov dx,8078h ; Pofo-Druckerport mov al,0 out dx,al pop al ; umgekehrte pop dx ; Reihenfolge ret
Weitere Prozessorbefehle
Aus Platzgründen kommen hier leider viele nützliche Befehle zu kurz. Wenigstens nicht unerwähnt bleiben sollten diese Instruktionen:
Mnemonic | Funktion |
---|---|
AND, OR, XOR | Bitweise Verknüpfungen |
INC, DEC | Operand um 1 erhöhen/vermindern |
ADC, SBC | Addition/Subtraktion mit Übertrag |
LOOP | Schleife mit cx als Zähler |
SHL,SHR ROL,ROR | Bitweises Schieben und Rotieren |
LODSB, STOSB | Stringbefehle (Laden/Speichern von Bytes mit automatischer Indexerhöhung |
Ein Wort zur Ausführungsgeschwindigkeit
Als klassischer CISC-Prozessor geizt der 8088 nicht gerade mit den Taktzyklen, die pro Instruktion benötigt werden. Man kann mit etwa 2 bis 20 Takten pro Befehl rechnen, einige Spezialbefehle (insbes. MUL, DIV) können sogar über 100 Taktzyklen in Anspruch nehmen. Zur Erinnerung: Der Prozessortakt des Portfolio liegt standardmäßig bei 4.9 MHz.
Zwischenbilanz
Das Programmbeispiel aus dem ersten Kursteil sollte nun eigentlich nicht mehr mit Hieroglyphen zu verwechseln sein. Auch das damalige Programmende kann jetzt erklärt werden:
mov ax,4c80h ; ah=4c und al=80 out dx,al int 21h
Die Funktion 4Ch (ah=4Ch) des DOS-Interrupts 21h beendet das Programm. Im Register al wird der Wert 80h nur benötigt, um damit den Lautsprecher per Portausgabe abzuschalten. Als Nebeneffekt (feature?) ist 80h gleichzeitig der Rückgabewert des Programms, der mit ERRORLEVEL abgefragt werden kann.
Ausblick
Der nächste Kursteil wird sich voraussichtlich mit der Ein- und Ausgabe auf dem Portfolio befassen. Dabei wird der Verwendung von Betriebssystemfunktionen der direkte Zugriff auf die Hardware gegenübergestellt. Weiterhin steht noch eine Beschreibung zu Variablen- und Konstantendefinitionen aus.
Teil 3: Textein- und Ausgabe (aus PofoInfo 1/98)
Wie versprochen stehen in diesem dritten Teil unseres Assemblerkurses verschiedene Ein- und Ausgabemethoden auf dem Programm, wie sie in fast jedem Programm benötigt werden. Außerdem müssen wir uns noch mit der Speicherreservierung für konstante oder variable Daten befassen.
Hello, World!
Aus guter Tradition setzt 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 ; source mov di,Offset Feld2 ; destination 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 übersichtlicher 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 „Speicherfüllstand“ 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:
Adresse | Funktion |
---|---|
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.
Taste | Make Code | Taste | Make 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!
Teil 4: Grafik (aus PofoInfo 2/98)
Unser nun bereits vierter Ausflug in die Assemblerprogrammierung führt uns endlich in die wunderbare Welt der Grafik. Während wir zunächst den bequemen Weg über die BIOS-Routinen beschreiten, wollen wir uns anschließend durch den gefürchteten Hardwaredschungel schlagen und in kaum erschlossene Performance-Dimensionen vordringen.
Der Grafikmodus des Portfolio
Der Displaycontroller des Portfolio (HD61830 von Hitachi) verfügt neben dem wohlbekannten Textmodus auch über einen Grafikmodus, in dem sich alle 240×64 Pixel beliebig ein- oder ausschalten lassen. Zum Umschalten des Videomodus bedienen wir uns der Funktion 0 (ah=0) des BIOS-Interrupts 10h:
mov ax,6 ; Grafikmodus int 10h ... mov ax,0 ; Textmodus int 10h
Die Unterfunktion 6 (al=6) aktiviert bei CGA-und VGA-Karten den Monochrom-Grafikmodus mit einer Auflösung von 640×200 Pixeln. Man kann sich vorstellen, daß der Bildschirm des Portfolio hiervon nur die linke obere Ecke darstellt. Manche andere Grafikmodi (z.B. al=0Ah) sind ebenfalls verwendbar, wobei aber beim Portfolio keine Unterschiede existieren. Zurück in den Textmodus gelangt man beispielsweise durch Aktivieren des Modus 0 („BW40“). Auch hierzu gibt es gleichwertige Alternativen, wie z.B. den Modus 3 („CO80“).
Auf den Punkt gebracht
Wenn der Grafikmodus aktiviert wurde, stehen die Funktionen 0Ch und 0Dh von Int 10h zur Verfügung, um den Zustand einzelner Bildpunkte zu setzen bzw. auszulesen. Die gewünschten Koordinaten müssen sich in den Registern cx und dx befinden und beziehen sich auf die linke obere Bildschirmecke als Ursprung. Das Register al enthält die „Farbe“ des Pixels, wobei (nur) der Wert 0 dem abgeschaltetem Zustand entspricht. Werte ab 128 bewirken beim Pixel-Setzen übrigens eine Invertierung. Das folgende Beispiel aktiviert zunächst das Pixel bei (x=100;y=50), löscht es sofort wieder, und liest schließlich seine Farbe aus, was zu al=0 führen muß:
mov cx,100 ; x = 0..239 mov dx,50 ; y = 0..63 mov ax,0C01h ; Pixel setzen int 10h dec ax ; wieder löschen int 10h ; (ax=0C00h) mov ah,0Dh ; Pixelfarbe int 10h ; ermitteln
Ein photographisches Gedächtnis
Konventionelle PC-Grafikkarten besitzen traditionell einen Bildspeicher, der dem Prozessor im Adressraum z.B. ab B800:0 (je nach Karte und Modus) direkt zugänglich ist. Der LCD-Controller des Portfolio verfügt dagegen über ein separates 2 KByte großes RAM ohne direkte Zugriffsmöglichkeit durch den Prozessor. Um trotzdem die Kompatibilität mit Anwendungen zu gewährleisten, die direkt in den Bildspeicher schreiben, bietet der Portfolio bekanntlich in seinem System-Menü verschiedene Optionen für den Bildaufbau. 4 KByte des installierten Hauptspeichers (beim Standard-Pofo bleiben deshalb nur 124 KByte verfügbar) sind hierzu 16 mal hintereinander in den Speicherbereich zwischen B000:0 und C000:0 eingeblendet.
Je nach gewählter Bildaufbau-Option wird nun timergesteuert oder bei jedem Tastendruck der dortige „Phantombildspeicher“ ins LCD übertragen. Die Größe von 4 KByte ergibt sich aus der Repräsentation des Textbildschirms durch 80x25x2=4000 Bytes (Je Zeichen: ASCII-Code und Attribut).
Der Textmodus einer herkömmlichen Grafikkarte wird auf diese Art recht brauchbar emuliert. Zwar unterhält das BIOS auch im Grafikmodus einen Bildspeicher bei B800:0, doch weicht dieser leider in seiner Organisation vom PC-Standard ab, so daß PC-Programme, die den Videospeicher zur Grafikdarstellung direkt ansprechen, nicht verwendbar sind. Da ein Byte des Videospeichers 8 Pixel repräsentiert, benötigt beim Portfolio jede Pixelzeile (240 Pixel) 30 Bytes. Alle 64 Pixelzeilen des Displays werden lückenlos hintereinander abgelegt, so daß der gesamte Grafikbildschirm in 1920 Bytes - der Länge einer PGF-Datei - untergebracht wird. Dieses vom BIOS verwaltete Abbild des Grafikbildschirms bietet im wesentlichen nur den Vorteil, daß der Zustand einzelner Pixel nicht nur gesetzt, sondern auch ausgelesen werden kann. Der Versuch, über entsprechende Befehle des LCD-Controllers an den eigentlichen Bildspeicher heranzukommen, scheitert nämlich (ich lasse mich aber gerne vom Gegenteil überzeugen).
Verkehrte Welt
Völlig unverständlich, weil ineffizient, ist die Art, auf die jeweils 8 Pixel vom BIOS zu einem Byte zusammengefaßt werden. Obwohl wie gesagt die Organisation des Bildspeichers inkompatibel zu allen PC-Grafikmodi ist, ordneten die Entwickler in Anlehnung zum PC dem linken Pixel einer Achtergruppe das höchstwertige Bit im Byte zu. Dagegen wäre nichts einzuwenden, wenn nicht der LCD-Controller genau die umgekehrte Bitreihenfolge verwenden würde. Darunter leidet bereits die Performance der „Putpixel“-Funktion (0Ch) von Int 10h. Regelrecht bei der Arbeit zusehen kann man aber der „Refresh“-Funktion (12h) von Int 61h, die den Grafikspeicher zum LCD überträgt. Die Bitmuster der Achtergruppen werden dazu auf bemerkenswert langsame Weise gespiegelt. Es sind deshalb diverse alternative Routinen bekannt, die mit Leichtigkeit eine Beschleunigung um den Faktor 10 oder mehr erzielen (siehe [1] und [2]). Im Grunde kann man aber auch sehr gut ganz auf diese Art der Refresh-Funktion verzichten, indem man einen eigenen Bildspeicher anlegt, der eine identische Kopie des LCD-RAMs enthält. Dieser Bildspeicher könnte sich durchaus bei B800:0 befinden, einfacher ist es jedoch, direkt im Datensegment 1920 Bytes zu reservieren. Die folgende Abbildung veranschaulicht hierzu die Organisation des LCD-Bildspeichers:
Wenn das komplette Bild am Stück berechnet werden kann und nicht wieder ausgelesen werden muß, kann man davon absehen, überhaupt ein Abbild der Grafik im Arbeitsspeicher zu erzeugen und stattdessen die Bilddaten direkt Byte für Byte dem LCD-Controller übergeben. Wenn die Bilddaten noch nicht komplett im Speicher vorbereitet wurden, kann zudem die ohnehin nötige Wartezeit (ca. 25 Taktzyklen beim Standard-Pofo, getunt entsprechend mehr, siehe auch [6]) zwischen den Zugriffen auf den LCD-Controller sinnvoll genutzt werden. Dieser schnellen Variante habe ich in dem Spiel „FoliDash“ den Vorzug gegeben.
Wie sag ich's meinem Controller?
Die Kommunikation mit dem bereits mehrfach angesprochenen LCD-Controller des Portfolio erfolgt über nur zwei Portadressen:
Adresse (hex) | Funktion |
---|---|
8010 | Datenregister |
8011 | Befehlsregister |
Ein Kommando wird übergeben, indem zuerst ein gültiger Kommandocode in das Befehlsregister und anschließend das zugehörige Argument ins Datenregister geschrieben wird. Für unsere Zwecke sind folgende Kommandos des HD61830 von Interesse:
Kommando | Funktion |
---|---|
8 | Bildspeicher-Startadresse Low festlegen |
9 | Bildspeicher-Startadresse High festlegen |
10 | Cursorposition Low festlegen |
11 | Cursorposition High festlegen |
12 | Byte(s) zum LCD übertragen |
14 | Bit setzen |
15 | Bit löschen |
Die weiteren Erläuterungen und Programmbeispiele setzen voraus, daß der LCD-Controller wie beschrieben in den Grafikmodus versetzt wurde.
Die Kommandos 8 und 9 legen fest, ab welcher Adresse im 2048 Byte großen LCD-Video-RAM die 1920 dargestellten Bytes auszulesen sind. Das Bild kann um bis zu 4 Pixelzeilen ohne Überlappung nach oben verschoben werden, weswegen sich diese Technik zur Realisierung eines pixelgenaues vertikalen Scrollings anbietet.
Im folgenden Beispiel soll der Bildschirm um 1 Pixelzeile gescrollt werden. Obwohl die neue Startadresse nur 30 lautet, muß laut Datenblatt auch das höherwertige Byte, also 0, geschrieben werden.
mov dx,8011h mov al,8 ; Set AdrLow out dx,al mov al,30 ; 1 Pixelzeile dec dx out dx,al inc dx mov al,9 ; Set AddHigh out dx,al mov al,0 dec dx out dx,al
Analog zur Auswahl der Startadresse des Bildspeichers geschieht über die Kommandos 10 und 11 die Festlegung der aktuellen „Cursorposition“. Gemeint ist die Adresse im Video-RAM, auf die sich die Kommandos 12, 14 und 15 beziehen. Es ist zweckmäßig, ein Unterprogramm zur Cursorpositionierung etwa in der folgenden Art zu definieren:
Set_LCD_Cursor: ; Eingabe: si=Adresse push ax push dx mov dx,8011h mov al,10 ; Set CursorLow out dx,al mov ax,si dec dx out dx,al inc dx mov al,11 ; Set CursorHigh out dx,al mov al,ah dec dx out dx,al pop dx pop ax ret
Soll ein kompletter Bildaufbau folgen, muß die Cursoradresse auf null gesetzt werden:
xor si,si ; si=0 call Set_LCD_Cursor
Dagegen adressiert das nächste Beispiel eine Pixel-Achtergruppe nahe der Bildschirmmitte (x=15*8; y=32):
mov si,15+32*30 call Set_LCD_Cursor
Das Kommando 12 leitet die Übertragung von Bilddaten ein. Im Anschluß daran dürfen beliebig viele Bytes (z.B. 1920) nacheinander ins Datenregister geschrieben werden, wobei aber, wie erwähnt, zwischen den Portzugriffen eine gewisse Wartezeit einzuhalten ist. Man bedenke hierbei, daß ein einziger Prozessorbefehl mit Speicherzugriff bereits etwa 12 Takte benötigt (wie z.B. LODSB). Wer ganz sicher gehen will, kann das Busyflag des LCD-Controllers als Bit 7 von Port 8011h auslesen. Hier zur Demonstration eine Routine, die den Bildschirm mit einem vertikalen Linienmuster füllt und dieses dann ständig invertiert.
mov ah,55h ; Bitmuster Effekt: xor si,si call Set_LCD_Cursor mov dx,8011h mov al,12 ; Daten an- out dx,al ; kündigen mov cx,1920 ; Byte-Anzahl L: in al,dx test al,dh ; Busy? jnz L dec dx mov al,ah ; Byte out dx,al ; schreiben inc dx loop L xor ah,255 ; invertieren jmp Effekt
Das Ergebnis - ein nervöses Flimmern - beweist, daß der byteweise Transfer von Bilddaten sehr effizient ist.
Trotzdem hat in manchen Fällen (Grafiken mit geringer durchschnittlicher „Schwärzung“) das gezielte Setzen und Löschen einzelner Pixel seine Berechtigung. Unter Verwendung der Kommandos 14 und 15 des LCD-Controllers läßt sich relativ einfach eine „PutPixel“-Routine formulieren, die in ihrer Anwendung der Funktion 0Ch von Int 10h entspricht. Die Ausführung von [7] verzichtet auf die Aktualisierung des Grafikspeichers bei B800:0 und die damit verbundene Invertierungsmöglichkeit (Farbe 128), erreicht dafür aber ungefähr die vierfache Geschwindigkeit des BIOS-Pendants. Man sollte sich allerdings darüber im Klaren sein, daß solcherart generierte Grafiken für Screen-Grabber-Programme unsichtbar bleiben.
Zur Funktionsweise der Kommandos 14 und 15 ist anzumerken, daß zunächst wieder die gewünschte Cursoradresse eingestellt werden muß, und zwar nach wie vor bytebezogen. Das zum Kommando gehörige Argument legt lediglich die Bit-Nummer innerhalb des spezifizierten Bytes fest und darf somit Werte von 0 bis 7 annehmen.
Sofern die Startadresse des Bildspeichers nicht verändert wurde (Scrolling!), aktiviert der nachfolgende Programmausschnitt das Pixel in der rechten unteren Bildschirmecke:
mov si,1919 call Set_LCD_Cursor mov dx,8011h mov al,14 ; 14=Set, 15=Clear out dx,al dec dx mov al,7 ; Bit-Nummer out dx,al
Vorschau
So viel zum Thema Grafik auf dem Portfolio. Weitere Informationen rund um den LCD-Controller finden sich in [3], [4] und [5]. Ganz im Sinne von Multimedia geht es im nächsten (bislang nicht erschienenen) Teil weiter mit Wissenswertem über die Tonerzeugung beim Portfolio.
Autor: Klaus Peichl, http://leute.server.de/peichl/main.htm
Referenzen
[1] Prozedur „grefresh“ der Pascal-Unit auf Gunnar Thöles WWW-Seite
[2] Newsletter von Paul Jolliffe, Ausgabe 4
[3] Newsletter von Paul Jolliffe, Ausgabe 5
[4] Newsletter von Paul Jolliffe, Ausgabe 9
[5] Datenblatt zum HD61830: „H9CD0.PDF“
[6] Programm LCD_TEST.COM auf meiner WWW-Seite (http://leute.server.de/peichl/pf.htm)
[7] Assembler-Routine „PutPixel“ auf meiner WWW-Seite