- Count on me, I'm gonna win the race.
- -- Yello, "The Race"
Dieser Artikel ist entstanden im Rahmen meines Vorhabens entstanden, mir ein Entwicklungssystem für das
Atari 2600 zu bauen. Genauer genommen, ist es bei dem Versuch entstanden, mein erstes eigenes Demo auf dem Atari 2600 in
Stella zu programmieren. Der Titel ist deshalb englisch, weil eine deutsche Übersetzung entweder zu lang ist, sich doof anhört, der Kern nicht trifft oder alles zusammen.
Programmieren auf dem Atari 2600 unterscheidet sich vom Programmieren für andere Konsolen. Normalerweise ist es üblich das Spielgeschehen Bild für Bild in den Bildschirmspeicher zu schreiben. Man verändert dabei den Bildschirmspeicher zu einem Zeitpunkt an dem er nicht gerade dargestellt wird, und bereitet so das nächste Bild vor. Beim Atari 2600 gibt es keinen Bildschirmspeicher, der Seitenweise aufgebaut ist, sondern nur zeilenweise. Man befindet sich also in einem Wettrennen mit dem Rasterstrahl, "Racing The Beam".
Dabei gibt hilft einem der Video-Chip, TIA genannt, keinen Fehlstart hinzulegen. Dafür stellt er einem Methoden zur Verfügung, den Prozessor so lange anzuhalten, bis entweder der Bildschirmanfang oder der Anfang einer neuen Zeile erreicht ist. Ohne diese wäre es quasi unmöglich auf dem Atari 2600 ein Bild darzustellen.
Für den Television Interface Adapter (TIA, der Grafikchip) besteht, besteht ein Bild aus 262 Zeilen (in der NTSC-Version, die als Referenz gilt, weil der Großteil aller Spiele in USA entwickelt wurden), und er kennt pro Zeile 228 Positionen, auf denen sich der Rasterstrahl befinden kann. Ich vermeide hier jetzt den sonst üblichen Begriff "Pixel" aus zwei Gründen: zum Einen, weil ein Teil davon sich auf einem nicht darstellbarem Bereich befindet. Außerdem kann man nicht jedem "Unpixel" in einen (wenigstens relativ) unabhängigen Zustand versetzen. In der offiziellen Stella Dokumentation wird der Begriff "Clock-Cycles" verwendet. (Hier bezieht sich Stella jetzt auf den Codenamen des Projektes "Atari-Spielekonsole", nachdem sich der oben erwähnte Emulator benannt hat.) Pro Prozessorzyklus (CPU-Cycle) läuft der Rasterstrahl über drei Clock-Cycles.
Die 262 Zeilen werden in folgende Bereiche aufgeteilt:
- 3 Zeilen "Vertical Sync" (VSYNC)
- 37 Zeilen "Vertical Blank" (VBLANK)
- 192 Zeilen, die das eigentliche Bild enthalten
- 30 Zeilen "Overscan"
Jede Zeile besteht aus 68 Clock-Cycles, in denen nicht "gemalt" wird (HBLANK), gefolgt von 160 Clock-Cycles bei denen es dann "heiß her geht". Damit bleibt ein Bereich von (68,40) bis (231,227) in dem der TIA etwas darstellt. So zumindest die Dokumentation. Da der TIA recht frei programmiert werden kann, kann man auch im VBLANK- und Overscan-Bereich Bilddaten zeichnen. Diese werden allerdings dann in einem Bereich dargestellt, die ein Fernseher normalerweise nicht anzeigt.
Damit stehen der CPU also 228 / 3 = 76 Taktzyklen pro Zeile zur Verfügung. Nach 76 Takten befindet sich also der Rasterstrahl schon in der nächsten Zeile. Ein Assembler-Befehl benötigt für die Ausführung zwischen 2 und 7 Takten. Bauen wir uns doch mal ein kleines Beispiel. Wir wollen mit Hilfe der Hintergrundgrafik das Wort "Hallo" malen. Die kleinste Einheit der Hintergrundgrafik hat eine "Auflösung" von 4 Clock-Cycles, bei 160 Color-Cycles pro Zeile macht das eine Auflösung von 40 Pixeln. Kling nach nicht viel, ist es auch. Aber es kommt noch besser: der TIA hat nicht einmal 40 Bit Speicher für die Grafik, sondern nur 20 Bit. Für die Darstellung dieser 20 Bit gibt es zwei Modi: entweder wird nach den ersten 20 Bit für die nächsten 20 Bit wird wieder auf den selben Speicher zugegriffen. Alternativ kann der Speicher für die rechte Hälfte auch gespiegelt dargestellt werden.
Für die Darstellung wollen wir einen Zeichensatz mit einer Größe von 8x8 Pixel benutzen. Dieser war zu der Zeit durchaus üblich und wurde von einem Großteil der 8-Bit Rechner dieser Zeit verwendet. Bei den 5 Zeichen des Wortes "Hallo" kommen wir genau auf die 40 Pixel, die uns der TIA mit seiner Hintergrundgrafik zur Verfügung stellt. Da wir aber auch noch andern Text darstellen wollen, müssen die Daten im RAM vorliegen. Die 20 Bit Grafikdaten des TIA sind in drei Bytes abgelegt. Für unseren selbst programmierten Bildschirmspeicher benötigen wir also:
3 Bytes x 2 Hälften x 8 Zeilen = 48 Bytes
Damit haben wir also schon mal 48 von 128 Bytes RAM verbraten, was schon mehr als ein Drittel des verfügbaren RAMs ist.
Unser Code nur für die Darstellung könnte zum Beispiel so aussehen:
; das X-Register enthält den Anfang der aktuellen "RAM-Zeile"
; die Zahlen in Klammern sind die vom Befehl benötigen Taktzyklen
; (für aktuellem Befehl; Summe innerhalb der längsten laufenden Schleife)
CLC ; für spätere Addition (2; 0)
LDX #$00 ; Anfang "RAM-Zielenpuffer" (2; 0)
loop1:
LDY #$08 ; jede "RAM-Zeile" soll 8 mal wiederholt werden (2; 2)
loop2:
STA $02 ; WSYNC: Prozessor bis der Anfang der Zeile anhalten (3; 5)
; im HBLANK Bereich kann jetzt noch was getan werden...
; die Register für das Spielfeld Playfield beschreiben:
LDA $80,X ; (4; 9)
STA $0D ; PF0 linke Hälfte (3;12)
LDA $81,X ; (4;16)
STA $0E ; PF1 linke Hälfte (3;19)
LDA $82,X ; (4;23)
STA $0F ; PF2 linke Hälfte (3;26)
; der Rasterstrahl ist mittlerweile so weit, dass wir die Register
; neu beschreiben können.
LDA $83,X ; (4;30)
STA $0D ; PF0 rechte Hälfte (3;33)
LDA $84,X ; (4;37)
STA $0E ; PF1 rechte Hälfte (3;40)
LDA $85,X ; (4;44)
STA $0F ; PF2 rechte Hälfte (3;47)
DEY (2;49)
BNE loop2 ; 8 Wiederholungen (2+1;51)
TXA (2;53)
ADC #$06 ; 6 aufaddieren für nächste Zeile (2;55)
TAX (2;57)
CPX #$30 ; schon der komplette RAM-Zeilenpuffer dargestellt? (2;59)
BNE loop1 ; (2+1;62)
; fertig, jetzt kann der Code was anderes machen...
Die verwendeten Befehle in der Reihenfolge ihres ersten Auftretens kurz erklärt:
- CLC
- Übertrags-Register (Carry-Flag) löschen, für spätere Addition
- LDX #$00
- (Index-) X-Register mit dem Wert 0 laden
- LDY #$08
- (Index-) Y-Register mit dem Wert 8 laden
- STA $ZP
- Akkumulator in den Speicher an der Stelle 0xZP beschreiben (von 0x00-0x2C liegen die TIA Register)
- LDA $ZP,X
- Akkumulator aus dem Speicher an der Stelle 0xZP+X lesen (von 0x80-0xFF liegt das RAM)
- DEY
- Y = Y - 1; Zero-Flag setzen, wenn Y == 0; Zero-Flag löschen, wenn Y != 0
- BNE
- springen, wenn Zero-Flag gesetzt ist
- TXA
- X-Register in den Akkumulator kopieren
- ADC #$06
- Akkumulator = Akkumulator + 0x06 + Carry-Flag
- TAX
- Akkumulator in das X-Register kopieren
- CPX #$30
- mit dem Wert 0x30 vergleichen, Zero-Flag setzen bei X == 0x30 sonst löschen
Somit haben wir nun 62 der zur Verfügung stehenden 76 Taktzyklen verwendet. Die restlichen 14 können nun zum Beispiel dafür verwendet werden, um die Farben zu ändern, so dass jede Zeile eine andere Farbe hat. Um noch mehr Taktzyklen herauszuquetschen kann man zum Beispiel einen Codeteil für jede einzelne "RAM-Zeile" schreiben, um so 17 Takte zu sparen (6 durch die 6 Befehle ohne X-Register-Indizierung, und 6 weitere durch das Wegfallen der Addition von 6 auf das X-Register und 5 weitere durch den ebenfalls wegfallenden Vergleich und Sprung an den Anfang der Schleife). Das wird aber wiederum deutlich mehr Code im ROM beanspruchen, und davon gibt es normalerweise auch nur 4 Kilobyte.
Es wird bestimmt noch sehr interessant zu sehen, was sonst noch so möglich ist...
Kommentare