Editorial Inhalt Impressum

Programmieren in Assembler - Kurs Teil 1

Vorwort

Dies ist der allererste Kurs den ich schreibe. Wenn also irgendwo falsche Informationen zu finden sind, oder es zu einigen Routinen bessere Lösungen gibt: Sorry, ich bin nur ein Hobbyprogrammierer und programmiere nur nebenbei in meiner Freizeit. Irgendwelche Wünsche oder Kritik zu diesem Kurs könnt ihr direkt als Leserbrief an die AmigaTimes schicken.

Einleitung

Wolltet ihr schon immer mal tolle Intros oder Demos schreiben? Bunte Bälle über den Screen jagen und Schrift springen lassen? Dann habt ihr jetzt die Gelegenheit dies zu erlernen. Alles was ihr dazu braucht ist irgend ein Amiga und etwas Geduld. Die nötige Software konnten wir leider aus Platzmangel nicht dem Mag beilegen, wer es haben möchte, es ist im Aminet unter "....." zufinden. Ein anderer Assembler, z.B. von Maxon, geht auch!

Inhalt

So, nun mal ein Überblick was der Kurs so bietet:

Diese Themen werden dann über die einzelnen Teile des Kurses verteilt sein, aber nicht in dieser Reihenfolge.

Computer sind doof...

Im Grunde genommen ist unser Amiga genau so dumm wie diese Intel-Dinger. Er kann gerade mal bis 1 zählen. Ursache ist, daß die elektrischen Schaltkreise im Amiga nur zwei Zustände annehmen können, nämlich Strom an oder Strom aus. Eine ganze Menge dieser Schaltkreise bilden nun den Prozessor, das Herz des Computers. Mehrere Millionen Schaltkreise bilden den Speicher. Ein Programm ist nichts anderes als ein bestimmter Zustand des Speichers. Der eine Zustand heißt 0, der andere 1. Ein Programm könnte also so aussehen:

1001100101011101110101100011010111101011010110101010111010110101010101

Sieht blöd aus, nicht wahr? Tja, das ist Maschinensprache, mehr nicht.

Assembler

Nun will man sein Programm ja nicht aus lauter Nullen und Einsen schreiben, also wurde 1950 Assembler entwickelt. Statt 0 und 1 schreibt man nun dafür Mnemonics. Das sind nur Abkürzungen für bestimmte 0-1-Folgen. Nun kann der arme Amiga aber keine Mnemonics lesen, also muß man diese wiederrum in Nullen und Einsen übersetzen. Genau das macht ein Assembler. Assembler ist nun zum einen die Sprache selber und zum anderen das Übersetzungsprogramm. Und genau so einen Assembler brauchen wir für den Kurs. Nun gibt es für den Amiga Assembler wie Sand am Meer. Ich habe mich dazu entschlossen den Kurs für "TFA AsmOne" zu schreiben. Es handelt sich dabei um eine modifizierte PD-Version des Assemblers AsmOne.

Installation von AsmOne v1.29

Wenn ihr die oben gezeigten Listings bloß seht, habt ihr auch nichts davon. Also werden wir erstmal den Assembler installieren. Entpackt die Datei "AsmOne.lha" irgendwo hin (z.B nach RAM:). Danach kopiert die Datei "AsmOne" in ein Verzeichnis eurer Wahl und die Dateien "RegsData" und "AGA.guide" nach "S:". Die Datei "ASM-One.Pref" kommt nun noch nach "SYS:Prefs/Env-Archive". Das war's auch schon.

Bits und Bytes

Diese am Anfang genannten Nullen und Einsen haben natürlich auch einen Namen, man nennt sie Bits. Ein Bit ist die kleinste Informationseinheit die ein Computer kennt. Es kann nur die Zustände 0 oder 1 aufnehmen. 8 Bit zusammengefaßt nennt man ein Byte. Zwei zusammengefaßte Bytes nennt man ein Wort. Vier zusamengefaßte Bytes nennt man Langwort.

Binär und Hexadezimal

Bei der Assemblerprogrammierung werden Zahlen meisst binär oder hexadezimal angegeben. Binäre Zahlen können nur aus den Ziffern 0 und 1 bestehen, hexadezimale aus 0-F. Damit man nun unterscheiden kann, um was für eine Zahl es sich handelt, stellt man hexadezimalen Zahlen ein $ voran (z.B. $A) und binären ein % (z.B. %0011). Um Zahlen nun zwischen den einzelnen Zahlensystemen umzurechnen werden wir den AsmOne nutzen.

Speicher

Nun wollen wir uns mal das Gedächtnis des Amigas anschauen: den Speicher. Im Amiga gibt es ChipRAM und FastRAM. Im ChipRAM handtieren vorrangig die Customchips (die für Grafik und Sound). Will die CPU dann auf den Speicher zugreifen, muß sie warten bis die CustomChips fertig mit der Arbeit sind. Das ist auch der Grund warum Amigas die nur ChipRAM besitzen so langsam sind. Im FastRAM kann nur die CPU werkeln, die CustomChips haben da drauf keinen Zugriff. Ein Computer mit FastRAM ist deshalb auch schneller als einer ohne. Die ersten 512 KByte ChipRAM befinden sich bei den Adressen $000000 bis $080000, geht der Speicher bis $100000 haben wir 1 MByte ChipRAM, bis $200000 dann 2 MByte. Alles was über $200000 geht ist dann FastRAM.

Spezielle Adressen

Extrem wichtige Adressen sind $dffxxx, $bfexxx und $bfdxxx. Von $dff000 bis $dff1fe finden wir die CustomChips, die für Grafik und Sound zuständig sind. Bei dem alten Amigas heissen sie Agnus, Denise und Paula. Bei den neueren AGA-Amigas heissen sie Lisa (ehemals Agnus), Alice (ehemals Denise) und Paula (funktional unverändert). Von $bfe000 bis $bfef01 finden wir den CIAA, zuständig z.B. für die Kontrolle des Parallelports und des Serialports. Analog dazu gib es noch den CIAB, zu finden ab Adresse $bfd000. Diese speziellen Adressen werden auch Register genannt.

CPU - Central Processing Unit

Assembler ist immer an eine bestimmte CPU gebunden. Wir werden hier für Prozessoren der 68000er Familier von Motorola Programme schreiben. Dazu sollten wir zumindest grob wissen wie diese Prozessoren aussehen. Eine 68000er-CPU hat 8 Datenregister (D0-D7), 7 Adreßregister (A0 - A6), einen Stackpointer (A7), einen Programmzähler (PC) und ein Statusregister (SR). Jedes dieser Register ist 32 Bit breit. In Datenregistern kann man Bytes, Worte oder Langworte ablegen. In den Adressregistern ist der Typ Byte nicht möglich. Die restlichen Registern sind immer "long".

Adressierungsarten

So, nun habt ihr erstmal genug Hintergrundwissen um mit der Assembler- programmierung beginnen zu können. Tja, dann kommt nun der wohl für Anfänger schwierigste Teil von Assembler: die Adressierungsarten. Wenn ihr die alle verstanden habt, wird der Rest nur noch ein Kinderspiel. Eine CPU ist um so besser, je mehr sinvolle Adressierungsarten sie hat, und hier glänzt der 68000 ganz besonders. Dann mal los:

1. Register direkt: move.l a3,a4
Hier wird der Inhalt des Adressregisters A3 nach A4 kopiert. Move heißt kopiere, obwohl es wörtlich übersetzt bewege heissen sollte. Das "l" bedeutet, daß wir das ganze Register (was ja 32 Bit breit ist) kopieren wollen, also ein Langwort, darum auch das "l". Wollen wir nur ein Wort kopieren, schreiben wir "move.w", für ein Byte würden wir "move.b" schreiben.

2. Adressregister indirekt (ARI): move.l (a3),(a4)
Die Inhalte der Register werden als Adressen gesehen. Hat z.B. A3 den Wert 4711 und A4 den Wert 5711, dann wird ein Langwort von Adresse 4711 nach 5711 kopiert.

3. Adressregister indirekt mit Postinkrement: move.w (a0)+,d0
Im Klartext: Kopiere das Wort, auf das A0 zeigt nach D0 und erhöhe (post) A0 um 2. Zwei deshalb, weil ein Wort zwei Byte hat. Möchte man z.B. um 1 erhöhen, müsste man "move.b (a0)+,d0" schhreiben (ein Byte kopieren und dann um 1 erhöhen).

4. Adressregister indirekt mit Predekrement : move.l d0,-(a5)
In diesem Fall wird vorab (pre) 4 (ein Langwort hat 4 Byte) von A5 subtrahiert, dann wird D0 dahin kopiert, wohin A5 nun zeigt.

5. Adressregister indirekt mit Adressdistanz: move.w -100(a0),d0
Wäre A0=500, würde das Wort von Adresse 400 nach D0 kopiert.

6. Adressregister indirekt mit Adressdistanz und Index: move.w 100(a0,d0),4711
100 ist die Adressdistanz, A0 enthält die Basisadresse, in D0 steht der Index. Alle drei werden addiert. Die Summe ist eine Adresse, das Wort, welches da steht, wird ins Ziel (hier Adresse 4711) kopiert.

7. Absolute Adressierung: move.w 4711,5713
Hier wird das Wort von Adresse 4711 nach Adresse 5713 kopiert.

8. Konstanten-Adressierung: move.l #65,d0 oder move.l #'A',d0
Um eine Konstante zu kopieren, braucht man dem Zeichen nur ein # vorzusetzen.

9. PC-relative Adressierung
Hier muß ich mal was erklären, damit ihr diese Feature auch würdigen könnt. Sobald wir in einem Assembler-Programm eine absolute Adresse angeben, ist unser Programm an einem Ort im Speicher gebunden. Der von uns geplante Adressbereich kann aber nun schon belegt sein, also muß unser Programm auch an einem anderen Ort laufen können. Dazu gibt es zwei Möglichkeiten. Erstens, unser Programm ist verschiebbar. Dafür sorgt der Assembler, indem er in unserem Programm eine Tabelle aller absoluten Adresse speichert. Der Lader (ein Teil des Betriebssystems) kann dann diese Adresse korrigieren, indem er die Differenz zwischen geplanter und tatsächlicher Startadresse auf alle absoluten Adressen laut Tabelle addiert. Die zweite Möglichkeit ist, unser Programm lageunabhängig zu schreiben. Wir dürfen dann natürlich keine absoluten Adressen verwenden, und genau da brauchen wir die PC-relative Adressierung. Dabei wird die Adresse gerechnet als aktueller Stand des PC (Program Counter) und den Offset. Der Offset ist auf -32768... 32767 begrenzt. Hier noch ein Beispiel: move.w 100(pc),d0

10. PC-relativ mit Adressdistanz und Index: move.w 100(pc,a0.w),d0
Hier gilt sinngemäß das für "Adressregister indirekt mit Adressdistanz und Index", nur das die Basisadresse hier PC+2 ist.

So, nun wollen wir uns mal wieder der Hardware des Amigas widmen.

Der Copper

Um zu erklären was der Copper ist und wozu er gut ist, solltet ihr erstmal wissen wie ein Bild vom Amiga auf dem Monitor erzeugt wird. Was Ich hier schreibe gilt für Monitore mit Kathodenstrahlröhre und nicht für LCD- oder Plasmabildschirme. In der Kathodenstrahlröhre gibt es einen Rasterstrahl, der wenn er auf die Bildschirmfläche trifft, diese dann an diesem Punkt zum leuchten bringt. Um nun ein ganzes Bild darzustellen fängt der Strahl oben links an zu "zeichnen", bewegt sich dann Punkt für Punkt nach rechts bis zum Zeilenende, und fängt danach das ganze Spiel für die nächste Zeile nocheinmal an. Das alles geht so schnell, daß man es nicht sieht. In einer Sekunde wird das Bild 50 mal (sofern es sich um ein PAL-System (50 Hz) handelt) neu aufgebaut. Damit der Strahl nun weiß wo ein Punkt auf den Bildschirm hin soll, bekommt er seine Informationen vom Copper, einen Teil des Customchips Agnus. Mit diesem Copper kann man nun den Strahl steuern, und so jede Menge schöne Effekte auf den Bildschirm zaubern.

Die Copperliste

Und woher soll der Copper nun wissen, was er zu tun hat? Ganz einfach: er arbeitet eine Liste mit Anweisungen ab, die Copperliste. In dieser Liste stehen spezielle Befehle für den Copper, nämlich MOVE, WAIT und SKIP. Mehr Befehle kennt der Copper nicht. Mit MOVE können wir Daten in die Custom- Register schreiben. Als ersten Operanden wird der Wert angegeben, als zweiten das Register. Mit WAIT können wir auf eine bestimmte Position des Rasterstrahls warten. Der erste Operand setzt sich aus auf die zu wartende Position zusammen (X- und Y-Koordinate) und der zweite aus einer Bitmaske, die den WAIT-Befehl genauer beschreibt (z.B. nur auf horizontale Position warten und die vertikale ignorieren). Mit SKIP kann man die nächste An- weisung in der Liste überspringen, wenn der Rasterstrahl eine bestimmte Position erreicht hat. Der erste Operand beinhaltet die Position und der zweite eine Bitmaske (wie bei WAIT, bloss Bit 0 ist 1). Wir werden hier nur MOVE und WAIT benutzen. SKIP habe Ich persönlich noch nie benutzt.

Unsere erste Copperliste

Nun wollen wir unsere erste Copperliste schreiben. Ziel ist es, zwei blaue Farbverläufe auf den Bildschirm zu bringen. Als erstes müssen wir die Copperliste der Workbench sichern, damit der Amiga nach Beendigung unseres Programms nicht abstürzt. Natürlich werden wir nicht die gesamte Liste abspeichern, sondern nur die Adresse ab wo sie sich im Speicher befindet. Die Adresse der Copperliste steht im Register COP1LC ($dff080). Da dieses Register nur ein Schreibregister ist, können wir da keine Adresse auslesen. Einen Weg gibt es aber dennoch, und zwar über das Betriebsystem. Im ROM existiert die "graphics.library", mit deren Hilfe wir die Adresse der Copperliste der Workbench in Erfahrung bringen können. Um zu verstehen was eine Library ist, werde Ich euch erstmal das Konzept der Libraries er- klären.

Shared-Libraries

Eine Shared-Library ist eine Ansammlung von Funktionen die jedes Programm benutzen kann. Um nun Funktionen aus einer Library zu nutzen muß man diese erst öffnen. Die Library wird dann gegebenenfalls in den Speicher geladen und von nun an kann das Programm daraus Funktionen benutzen. Am Ende muß das Programm die Library wieder schließen. Und wie öffnen wir nun so eine Library? Ganz einfach, mit der Funktion "OpenLibrary" der "exec.library". Na toll, und wie bekomme ich die "exec.library" auf, wenn die Funktion zum Öffnen in dieser enthalten ist? Auch ganz einfache, die "exec.library" ist immer offen. Zum Nutzen einer Funktion aus einer Library brauchen wir die Basis-Adresse der Library und den Library-Vector-Offset der gewünschten Funktion. Die Basis-Adresse erhalten wir als Ergebnis der Funktion "OpenLibrary", die LVOs muß man wissen. Noch ein Problem: Wie erhalte ich die Basis-Adresse der "exec.library" wenn die sowieso offen ist und ich kein "OpenLibrary" anwenden kann? Tja, auch ganz simpel, die Basis-Adresse der "exec.library" ist $4. Diese Adresse ist fest und ändert sich nie. Doch nun schauen wir und die Funktion "OpenLibrary" genauer an: Prototyp: struct Library * library=OpenLibrary(STRPTR name, ULONG version); A1 D0 Im Adressregister A1 muß der Name der Library rein (genauer gesagt ein Zeiger dadrauf, bzw. die Adresse ab wo der Name im Speicher steht) und im Datenregister D0 kommt die gewünschte Version der Library rein.

Hier noch eine Übersicht aller Versionen:

Version Kickstart Version
301.0 (nicht mehr unterstützt)
311.1 (nur NTSC, nicht mehr unterstützt)
321.1 (nur PAL, nicht mehr unterstützt)
331.2 (veraltet)
341.3 (veraltet)
35spezielle Version zur Unterstützung des Monitors A2024
362.00 bis 2.02 (voller Fehler)
372.04 bis 2.05 (fast alle Fehler der Version 36 behoben)
382.1 (nur Erweiterung der diskresidenten Libraries)
393.0
403.1 (das z.Zt. aktuellste OS)

So, diese Informationen sollten reichen. Klickt hier drauf und ihr seht ein kommentiertes Assemblerlisting zum Öffnen der "graphics.library".

Wenn wir nun die Basisadresse der "graphics.library" haben, brauchen wir nur noch $26 hinzuaddieren und wir finden an dieser Adresse die Adresse der aktuellen Copperliste. Diese Adresse speichern wir ab und schreiben anschließend die Adresse unserer Copperliste in das Register COP1LC ($dff080). Nur müssen wir nur noch den Inhalt des Registers COPJMP1 ($dff088) ändern, und schon arbeitet der Copper unsere Liste ab. COPJMP1 ist ein Stroberegister, daß heißt man muß nur den Inhalt des Registers ändern und schon wird eine Aktion ausgelöst.

Aufbau einer Copperliste

Um nun z.B. die Werte der Farbe Rot in das Register COLOR00 ($dff0180) zu schreiben würde man für den Prozessor einfach "move.l #f00,dff0180" schreiben. Das "#" bedeutet, daß der Wert als Konstante und nicht als Adresse anzusehen ist. Der Farbwert setzt sich aus drei Werten zusammen. Der erste Wert bestimmt den Rotanteil der Farbe, der zweite den Grünanteil und der dritte den Blauanteil. Bei OCS/ECS-Amigas kann jeder Farbanteil einen Wert von $0 bis $F (dez. 15) haben. Da eine Copperliste nur Daten im Speicher sind, schreiben wir die Daten mit der Assemblerdirektive "dc" in den Speicher: dc.w $180,$f00. Als erstes das Register und als zweites den Wert. Da wir hier jeweils ein Wort in den Speicher schreiben, müssten wir eigentlich "dc.w $0180,$0f00" schreiben, aber in Copperlisten können wir die Null nach dem Dollarzeichen einfach weglassen. Nun könnten wir unsere erste Copperliste schreiben. Klickt hier drauf um das fertige Programm zu sehen.

Nur ein Listing sehen bringt ja auch nichts, also starten wir schnell mal den Assembler.

Start und Bedienung von AsmOne

Nach dem Start werdet ihr gefragt was für Speicher und wieviel Speicher AsmOne nutzen soll. Für unsere Zwecke tippt "c" für ChipRAM und anschliessend "200". Das heißt wir haben 200 KByte ChipRAM für AsmOne reserviert. Nun solltet ihr noch einige Einstellungen vornehmen: Menupunkt Assembler/ Preferences/Environment für allgemeine Einstellungen und Assembler/ Preferences/Assembler für den Assembler selber. Lasst einfach alle Einstellungen so wie sie sind, nur bei "Default Dir" bei den allgemeinen Einstellungen könnt ihr euer Startverzeichnis eintragen. Dieses Verzeichnis wird dann immer beim Laden oder Speichern gewählt. Bei den Assembler-Einstellungen solltet ihr noch die richtige CPU anwählen, der Rest kann so bleiben. Nun könnt ihr mit dem Befehl "v <verzeichnis" in das Verzeichnis wechseln wo sich die Listings befinden. Mit "r <dateiname" wird das Listing eingelesen. Mit ESC könnt ihr dann in den Editor gelangen. Drückt ihr nochmal ESC, verlasst ihr den Editor wieder. Mit "a" wird der Assembler gestartet. Mit "wo" könnt ihr euer Programm als ausführbares Programm speichern. Mit "=r" könnt ihr euch Informationen über alle Register der CustomChips Auskunft geben lassen. Wollt ihr z.B. etwas über das Register COP1LC ($dff080) wissen, schreibt ihr nur "=r cop1lc" oder "=r 080". Um nun Zahlen zwischen den einzelnen Zahlensystemen umrechnen zu können, tippt z.B. "? 10". Nun seht ihr die dezimale Zahl 10 erst als hexa- dezimale Zahl, dann als dezimale Zahl, dann als ASCII-Zeichen und zuletzt als binäre Zahl. Nur mal so nebenbei, ihr könnt natürlich auch die Menus nutzen.

Copperanimationen

Nun wollen wir mal einen roten Balken auf den Bildschirm bringen und ihn hoch und runter bewegen. Das Einzige was wir dazu tun müssen, ist vor jedem Bildschirmaufbau die WAITs in der Copperliste ändern. Erstmal müssen wir aber zwei Probleme bewältigen: 1. Wie bekommen wir in Erfahrung wann der Copper den Bildschrim neu aufbauen will? 2. Wie kommen wir an die WAITs in der Copperliste um sie zu ändern? Das erste Problem ist ganz einfach zu lösen: Im Register VHPOSR ($dff006) ist die gegenwärtige Position des Rasterstrahl enthalten. Nun müssen wir auf Zeile 255 warten, dann ändern wir unsere Copperliste und warten anschließend auf Zeile 0. Das zweite Problem ist auch einfach zu lösen: Wir setzen vor jedem Wait in der Copper- liste ein Symbol und können dann über diese Symbole auf die WAITs zu- greifen. Wir können natürlich auch nur vor das erste und letzte WAIT ein Symbol setzen und dann mit der Adressierungsart "Adressregister indirekt mit Adressdistanz" auf alle folgenden WAITs zugreifen. Wie das alles in der Praxis funktioniert, könnt ihr hier sehen.

Nicht übel, oder? Wie können natürlich auch die horizontalen WAITs ändern. Zu beachten ist nur, daß ihr immer ungerade Werte in die horizontale Posi- tion schreibt (darum warten wir auch immer auf $07 und nicht auf $06). Nehmt ihr gerade Werte, riskiert ihr einen Absturz des Coppers. Der größte Wert für eine horizontale Positon ist übrigens $e1. So, nun klickt noch hier drauf, und ihr seht ein Beispielprogramm für horizontale Bewegungen.


Ok, das war nun der erste Teil des Kurses, der zweite folgt sogleich. Naja, jedenfalls in zwei Monaten (es sei denn die AmigaTimes erscheint irgendwann mal jeden Monat). Na dann: viel Glück beim Programmieren.