C64 Assembler Programmierung – Interrupts und Scanlines
In diesem Tutorial zeige ich wie man Interrupts der Scanlines nutzt um den Rahmen vom Commodore 64 in unterschiedlichen Farben zu zeichnen. Mit diesem Trick erreicht man grafische Effekte die eigentlich nicht funktionieren sollten.
C64 Assembler Programmierung – Interrupts und Scanlines
Moment mal, alle 16 Farben im Rahmen vom Bildschirm? Wie ist das möglich, es gibt doch nur eine Adresse mit der Hintergrundfarbe im Speicher? Das Zauberwort heißt Interrupts. Mit diesen durch bestimmte Events ausgelösten Trigger lassen sich jede Menge toller grafischer Effekte verwirklichen. Ein sehr einfacher ist den Rahmen in unterschiedlichen Farben zu zeichnen.
Beispiel
Das Beispiel findet man wie immer auf meiner GitHub Seite. In einem früheren Tutorial habe ich bereits gezeigt, dass man den Hintergrund in bunten Farben zeichen kann. Das war damals aber ungeordnet und hat nur geflimmert. In diesem Beispiel verwende ich Interrupts der Scanlines um die Farbe gezielt alle paar Zeilen zu wechseln.
Bevor ich näher auf die Details des Programms eingehe sollte jeder interessierte Leser dieses einmal selber ausführen oder zumindest den Screenshot genau betrachten.
Funktionsweise
Der Rahmen der Bildschirmausgabe wird in 16 unterschiedlichen Farben dargestellt. Die Grundfarbe ist schwarz. Wird also die Hintergrundfarbe ($D021) auf schwarz (#$0) gestellt, dann wird der gesamte Rahmen schwarz gezeichnet. Um nun einzelne Zeilen des Rahmens in einer anderen Farbe zeichnen zu können müssen wir aufhören in Rahmen und Contentbereiche zu denken. Das gezeichnete Bild besteht aus Zeilen (Scanlines) die mit der Elektronenkanone eines CRT Bildschirms Zeile für Zeile gezeichnet werden. Für den Commodore 64 sieht das wie folgt aus:
Es werden 312 einzelne Zeilen gezeichnet, wobei jede aus 504 Pixel besteht. Diese 312 Zeilen werden 50 Mal in der Sekunde gezeichnet! Der Contentbereich hat eine Auflösung von 320×200 Pixel rund herum gibt es den Rahmen mit den Dimensionen 403×284. Um diesen Rahmen gibt es dann noch die HBlank und VBlank Bereiche in denen nichts gezeichnet wird. Nimmt man eine konstante Geschwindigkeit an, die der Strahl benötigt um die Zeilen durchzugehen, dann sind die HBlank Bereiche die Zeit die der Strahl benötigt um die nächste Zeile wieder von vorne beginnen zu können und VBlank die Zeit die der Strahl von unten rechts nach oben links für das nächst Bild benötigt.
Wir sehen in der Grafik zwei wesentliche Dinge:
- ein Bild wird Zeile für Zeile aufgebaut
- der Rechner muss irgendwo einen Zähler mitführen damit er weiß, welche Zeile gezeichnet wird und einen weiteren wann er wieder zurückspringen muss für die nächste Zeile oder mit einem neuen Bild startet
Die Theorie ist nun folgende: zwischen einer Zeilenänderung schaltet man einfach die in $D021 gespeicherte Farbe um. Nur woher weiß man, wann eine neue Zeile begonnen wird?
Interrupts
Ein Interrupt ist ein Signal an die CPU. Dieses bewirkt, dass die CPU die Ausführung des laufenden Programms unterbricht und einen anderen kurzen Code ausführt, bevor die CPU mit der Ausführung des ursprünglichen Programms fortsetzt. Der C64 unterscheidet dabei in:
- IRQ (Interrupt Request)
bei dieser Art der Interrupts kann die CPU entscheiden ob diese ignoriert werden oder nicht. Je nachdem wie sich die CPU entscheidet wird SEI oder CLI ausgeführt. CLI akzeptiert den Interrupt. - NMI
diese Interrupts können nicht ignoriert werden.
Der Wechsel in eine neue Zeile ist ein solcher Interrupt. Wir können beispielsweise festlegen, dass wir auf den Wechsel in Zeile 5 reagieren. Aus Sicht eines Assembler Programmierers müssen wir eine Funktion schreiben die ausgeführt werden soll und einen Interrupt Vektor darauf anlegen. Mit dem CLI Befehlt akzeptieren wir den Interrupt und dessen Abarbeitung.
Programm
Ich habe für mein Programm 2 Variablen (Adressen im Speicher) definiert. Im ersten speichere ich die aktuelle Hintergrundfarbe für den Rahmen (BORDERCOLOR) und in der anderen einen Zähler (COUNTER) in dem ich die Farben von max. 16 bis 0 herunterzähle und danach den Interrupt beende.
Mein Programm besteht aus 2 Teilen:
- Initialisierung
- Interrupt Funktion
Ich beginne bei der Interrupt Funktion. Das ist jener Code der ausgeführt wird, sobald der Interrupt getriggert wird. In meinem Programm wird der Code beim Aufbau jedes Bildes ab einer bestimmten Zeile aufgerufen:
Die Funktion beginnt mit dem Label „Irq“. In den ersten beiden Zeilen wird die COUNTER Variable mit der maximalen Anzahl an Farben (16) initialisiert. Danach beginnt eine Schleife mit dem „Loop“ Label. Erster Schritt ist die das Setzen der Rahmenfarbe auf den Wert in BORDERCOLOR. Die nächsten 3 Zeilen mit dem „Pause“ Label passiert nichts. Das X Register wird mit hexadezimal 90 initialisiert und danach heruntergezehlt. Solange nicht 0 wird das Herunterzählen fortgesetzt. In dieser Zeit passiert nichts außer, dass der Rasterstrahl das Bild zeichnet und den Balken in der aktuellen Hintergrundfarbe. Danach wird in BORDERCOLOR auf die nächste Farbe geschalten, der COUNTER um eins reduziert und so lange diese Loop fortgesetzt bis COUNTER 0 ist. Die Rahmenfarbe wird noch auf schwarz zurückgestellt, der Interrupt mit ASL $D019 bestätigt und in die Standard Interrupt Routine vom Kernal gesprungen.
Des gibt folgende Konstanten:
- 16
die Anzahl der unterschiedlichen Farben. 16 ist das Maximum, man kann diese Zahl auch reduzieren - $90
die Breite der Farbbalken. Diesen Wert kann man nach Lust und Laune verändern.
Damit der Interrupt auch irgendwann aufgerufen wird muss man ihn „registrieren“. Im Initialisierungsbereich des Beispielprogramms wird folgendes gemacht. Die BORDERCOLOR Variable wird mit 1 für die Farbe weiß initialisiert. Damit die Ausgabe etwas schöner aussieht setze ich noch den Hintergrund des Content Bereich schwarz und die Textfarbe weiß (gilt aber nur für neuen Text -> Ready). Danach wirds spannend, der Interrupt wird registriert:
- SEI
während der Initialisierung soll die CPU alle Interrupts ignorieren (wird am Ende der Initialisierung mit CLI wieder aufgehoben) - CIA-1 Interrupts ignorieren
$DC0D ist das „Interrupt Control and Status“ Bit. Schreibend können wir mit den einzelnen Bits spezielle Interrupts ignorieren. Mit dem Code ignorieren wir einfach alle. Zur Erinnerung: IRQ Interrupts können ignoriert werden. - VIC Interrupt Bit leeren
- CIA Interrupts bestätigen
mit einem lesenden Zugriff auf die Adressen $DC0C (IRQ, CIA-1 Chip) und $DD0D (NMI, CIA-2 Chip) bestätigen wir noch offene Interrupts requests. - die Zeile in der der Interrupt passieren soll setzen
wird auf den konstanten Wert $35 gesetzt, das ist die 53. Zeile. - Sprungadresse zur Irq Funktion setzen
mit diesem Code #<Irq wird das niedriger Byte angesprochen. Dieses wird in Adresse $0314 gespeichert. Mit #>Irq bekommt man das höherwertige Byte der Adresse, dieses kommt nach $0315. Nicht vergessen: 64kByte kann man nur mit 2 Bytes adressieren! - VIC Interrupts aktivieren
mit dem Setzen des ersten Bits in $D01A aktiviert man die Raster Interrupts vom VIC Grafikchip - CLI
Abbarbeitung von Interrupts für die CPU wieder aktivieren
Fazit
Dieses recht einfache Beispiel zeigt wie mächtig Interrupts sind. Beim C64 wird insbesondere bei Spielen damit viel getrixt. Fixe Limitierungen für die max. Anzahl von Farben oder Sprites für einen Bildbereich kann man somit aushebeln und Effekte zeigen, die rein von der Papierform nicht möglich sind. Dieser Artikel zeigt sehr eindrucksvoll, dass man nicht nur programmiert was angezeigt werden soll, sondern auch wann.