Wie ein Rasterzeileninterrupt prinzipiell funktioniert
Nachdem geklärt wurde, was ein Interrupt eigentlich ist und was dabei passiert, geht es nun um den, für Spiele- und Demo-Programmierer wohl wichtigsten Interrupt des C64, den Rasterzeileninterrupt. Dieser ist zunächstmal ein ganz normaler IRQ, aber woher kommt er und wann tritt er auf?
Auslöser des Rasterinterrupts ist der Grafikchip VIC-II. Um zu verstehen was dieser Rasterzeileninterrupt genau ist und wann er auftritt, ist es hilfreich zu wissen, wie das Bild, dass man auf dem Fernseher (Monitor) sieht, eigentlich entsteht. Da heutige LCDs und TFTs anders funktionieren, benutze ich zur Erklärung die Funktionsweise eines guten alten Röhrenfernsehers, wie er zur Zeit des C64 verwendet wurde.
Grundprinzip eines Röhrenfernsehers
Ein Röhrenfernseher besteht im Prinzip nur aus einer „Kanone“ (die sog. Kathode), die einen Elektronenstrahl auf die beschichtete Innenseite des Bildschirms schießt. Treffen die Elektronen jetzt auf die Beschichtung, so beginnt diese an der Stelle zu leuchten und man sieht einen Punkt. Um ein ganzes Bild darzustellen, wird der Strahl durch die sog. Ablenkeinheit bewegt. Er wandert so (wenn man von vorne auf den Ferseher schaut) erstmal von links nach rechts. Ist er am Ende der Zeile angelang, wird er abgeschaltet und an den Beginn der nächsten Zeile gebracht (dies nennt sich horizontal blank). Dann wird diese Zeile geschrieben usw. Ist der Strahl rechts unten, an der letzten Position angelangt, wird er auch wieder abgeschaltet, aber dann ganz zurück nach links oben an die erste Position gebracht (dies ist der sog. vertical blank).
Beim Erstellen des Bildes bestimmt die Intensität des Strahls, ob bzw. wie stark der jeweilige Punkt leuchten soll. Die Sonne muss schließlich heller dargestellt werden als eine Wand im dunklen Zimmer.
Bei einem Farbbildschirm gibt es drei Strahlen und die Beschichtung besteht pro Bildpunkt aus drei unterschiedlichen Punkten je Farbe. Leuchten diese drei Punkte zusammen auf, so ergeben sie (durch additive Farbmischung) die Farbe eines einzelnen Bildpunktes.
Die drei Farben sind euch bestimmt schon bekannt, es handelt sich um ROT, GRÜN und BLAU häufig auch kurz als RGB bezeichnet. Geht ihr sehr dicht an einen Röhrenfernseher heran oder benutzt eine Lupe, dann könnt ihr die Farben auch direkt erkennen.
Das war jetzt natürlich wieder sehr vereinfacht, ganz so simpel ist der Vorgang dann doch nicht, aber weitere technische Details (wie z. B. die Halbbilder) spare ich mir. Für uns als Programmierer ist erstmal wichtig, dass der Strahl von links nach rechts und von oben nach unten läuft. Die Geschwindigket mit der er dass macht hängt von der verwendeten Fernsehnorm ab. In Deutschland (und den meisten Ländern Europas) ist es PAL, in Amerika aber NTSC. Bei PAL wird das Bild 50 mal in der Sekunde aufgebaut. Diese 50Hz kennt ihr bestimmt auch von unserer Netzspannung, aus der Steckdose kommen 230V mit 50Hz. Auf einem NTSC-Fernseher sind es sogar 60 Bilder pro Sekunde! (In den USA kommen 110V mit 60Hz aus der Steckdose). Die höhere Bildwiederholrate wird aber durch eine geringere Zeilenanzahl erkauft. NTSC-Geräte haben üblicherweise 480 Zeilen, PAL-Systeme dagegen 576. Der Unterschied zwischen PAL und NTSC, wird uns später bei der Programmierung noch weiter beschäftigen. Es kann passieren, dass ihr eure Programme für die jeweilige Fernsehnorm anpassen müsst.
Wenn nicht explizit etwas Anderes erwähnt wird, gehe ich immer von einem PAL-C64 aus!
Wie ein Rasterzeileninterrupt entsteht
Der VIC-II zählt im Register 18 $d012 die aktuelle Rasterzeile mit. Ähnlich wie bei der Sprite-X-Position reichen 8 Bit aber nicht aus, sodass im Register 17 $d011 das höchste Bit als 9. Bit für die aktuelle Rasterzeile verwendet wird. Damit man jetzt nicht laufend diese Register manuell kontrollieren muss (könnt ihr aber durchaus machen), gibt es die schöne Möglichkeit, automatisch einen Interrupt auszulösen, wenn eine bestimmte Rasterzeile beginnt. Dies ist dann der RASTERZEILENINTERRUPT.
Einen Rasterzeileninterrupt einrichten
Um den Raster-IRQ einzurichten, sind eigentlich nur zwei Schritte notwendig.
- Dem VIC-II mitteilen, wann genau (also in welcher Zeile) er einen Rasterzeileninterrupt auslösen soll.
Dazu können die Register $d011 & $d012 nicht nur gelesen, sondern auch beschrieben werden. Schreiben wir jetzt z. B. etwas nach $d012, dann wird sich der von uns geschriebene Wert in einem internen Register gemerkt. Dies ist notwendig, da der VIC-II $d011 und $d012 ja verändert, um die aktuelle Rasterzeile mitzuzählen. - Den Interrupt zu unserer Routine springen lassen.
Da der Rasterzeileninterrupt nichts anderes, als ein ganz normaler Interrupt ist, benutzt er ebenfalls den RAM-Vector an der Adresse $0314/15, den wir bereits kennen. Wir müssen diesen also nur wieder auf unsere Interrupt-Routine umbiegen.
Auf ins Abenteuer…
Es ist an der Zeit auch mal einen ersten Rasterinterrupt in Aktion zu sehen. Man möge mir den nun folgenden Patriotismus verzeihen, aber die Deutschland-Flagge bietet sich hier einfach an. Das Beispiel soll unseren Bildschirm in den Farben schwarz, rot und gold erstrahlen lassen, während wir weiterhin mit dem BASIC arbeiten können. Auch hier nehme ich Rücksicht auf die Turbo Assembler-Nutzer und lege den IRQ bei $1000, statt $c000 ab.
STARTBLACK = $00 ;Wo beginnt schwarz STARTRED = $70 ; rot STARTGOLD = $D0 ; gold *=$0801 ;*** Startadresse BASIC-Zeile: 2018 SYS 2064:NEW !word main-2, 2018 !byte $9e !text " 2064:" !byte $a2,$00,$00,$00 main sei ;Interrupts sperren lda #<rasterIrq ;unsere Interrupt-Routine sta $0314 ;in den IRQ-Vector eingtragen lda #>rasterIrq ;auch das MSB sta $0315 lda #STARTBLACK ;Bei STARTBLACK soll ein sta $d012 ;Raster-IRQ ausgelöst werden lda $d011 ;Zur Sicherheit auch noch and #%01111111 ;das höhste Bit für den sta $d011 ;gewünschten Raster-IRQ löschen lda $d01a ;IRQs vom ora #%00000001 ;VIC-II aktivieren sta $d01a cli ;Interrupts wieder erlauben rts ;zurück zum BASIC
Zu Beginn werden drei Konstanten festgelegt, in denen jeweils die Zeile steht, ab der die jeweilige Farbe verwendet wird. Später kann man durch Ändern der Werte die Auswirkung auf den Rasterzeileninterrupt testen. Danach folgt wieder die BASIC-Startzeile 2018 SYS 2064:NEW.
Dann wieder die Interrupts sperren und in $0314/15 die Adresse der neuen Interruptroutine speichern. Das ist bekannt, es wurde schon in den beiden vorangegangenen Beiträgen erklärt. Jetzt muss der VIC-II wissen, wann er für uns einen Rasterzeileninterrupt auslösen sollen. Die Flagge soll mit schwarz beginnen, also soll der IRQ in Zeile 0 ausgelöst werden, STARTBLACK hat genau diesen Wert bekommen. Diesen nun einfach ins Register 18 $d012 schreiben. Wie bereits erwähnt, landet der Wert, den wir nach $d012 schreiben, in einem internen Register. Da der IRQ in Zeile 0 und nicht versehentlich in der 256. Zeile ausgelöst werden soll, wird anschließend auch noch das „9. Bit der Rasterzeile“ in $d011 gelöscht. Schließlich muss dem VIC-II noch mitgeteilt werden, dass er überhaupt den gewünschten Rasterzeileninterrupt auslösen soll. Dazu im Register 26 $d01a Bit-0 setzen. Schon ist alles vorbereitet, jetzt die Interrupts wieder freigeben und zurück zum BASIC.
Jetzt noch die passende Interrupt-Routine:
*=$1000 ;*** unsere eigene Interrupt-Routine rasterIrq lda $d019 sta $d019 ;IRQ bestätigen
Damit der VIC-II weiß, dass der Interrupt behandelt wurde, muss man dies durch lesen und schreiben von Register 25 $d019 bestätigen. Ich wähle hier den langen Weg, da $d019 gleich noch geprüft werden soll. Man könnte den IRQ auch einfach mit asl $d019 bestätigen. Dies würde auch mit lsr, inc und dec klappen, da es sich dabei um sog. READ-MODIFY-WRITE-Befehle handelt. Diese Befehle lesen die aktuelle Speicherstelle, schreiben dann (eigentlich unnötigerweise, aber schon hierdurch wird der IRQ bestätigt) den aktuellen Wert nochmal zurück, verändern ihn anschließend und speichern zum Schluß das Ergebnis. Dadurch ist die Anforderung des Bestätigens, des Rasterzeileninterrupts erfüllt. Mit einer SuperCPU klappt das übrigens nicht! Der dort eingesetzte Prozessor, verzichtet auf das (unnötige) Zurückschreiben des unveränderten Wertes und somit wird der IRQ dann auch nicht bestätigt!!
lda $d012 ;aktuelle Rasterzeile in den Akku bne doRed ;wenn ungleich 0 'rot' prüfen lda #$00 ;sonst 'schwarz' in den Akku sta $d020 ;und als Rahmen- sta $d021 ;sowie Hintergrundfarbe setzen lda #STARTRED ;Jetzt die Zeile für den nächsten sta $d012 ;Raster-IRQ festlegen jmp rasterIrqExit ;zum Ende springen
Beim Auftreten des Raster-Interrupts, wird dann $d012 in den Akku geladen. Dort steht ja die aktuelle Rasterzeile. Ist diese ungleich 0, geht es weiter zur Prüfung für rot beim Label doRed. Wenn sie aber Null ist, dann schwarz in den Akku laden und Rahmen- & Hintergrundfarbe darauf setzen. Nach schwarz folgt bekanntlich rot, also den Beginn für rot nach $d012 schreiben, damit dort der nächste Rasterinterrupt ausgelöst wird. Um die Interruptroutine zu verlassen, nach rasterIrqExit springen.
doRed cmp #STARTRED ;haben wir 'rot' erreicht? bne doGold ;falls nicht weiter zu 'gold' lda #$02 ;rot sta $d020 ;als Rahmen- sta $d021 ;sowie Hintergrundfarbe setzen lda #STARTGOLD ;Rasterzeile für 'gold' sta $d012 ;festlegen jmp rasterIrqExit ;und raus
Bei rot läuft es nahezu identisch, wie bei schwarz. Es wird geprüft, ob die aktuelle Rasterzeile (die steht immer noch im Akku!), die von rot ist, falls nicht geht es bei doGold mit Gold weiter. Sonst wird die Rahmen- und Hintergrundfarbe einfach auf rot gesetzt und der Beginn von Gold für den nächsten IRQ nach $d012 geschrieben. Danach geht es bei rasterIrqExit weiter.
doGold lda #$07 ;setze gelb sta $d020 ;als Rahmen- sta $d021 ;und Hintergrundfarbe lda #STARTBLACK ;Rasterzeile für schwarz (wir beginnen sta $d012 ;von vorne) festlegen
Da nur drei Rasterzeilen als Auslöser für den IRQ in Frage kommen, wird bei doGold nicht mehr geprüft, ob es die Zeile von Gold ist. Es wird (in Ermangelung der Farbe Gold) gelb verwendet und als nächste Zeile für den Rasterzeileninterrupt, die von schwarz nach $d012 geschrieben. Hier läuft das Programm nun automatisch zu rasterIrqExit weiter.
rasterIrqExit pla ;Y vom Stack tay pla ;X vom Stack tax pla ;Akku vom Stack rti ;Interrupt verlassen
Das Ende sollte bekannt sein. Da die System-Routine ab $ff48 (es handelt sich, wie bereits mehrfach erwähnt, beim Rasterzeileninterrupt um einen ganz normalen Interrupt) die Register auf den Stack gelegt hat, werden diese zurück geholt und dann per rti der Interrupt verlassen.
Zur Übersicht nochmal das komplette Programm:
STARTBLACK = $00 ;Wo beginnt schwarz STARTRED = $70 ; rot STARTGOLD = $D0 ; gold *=$0801 ;*** Startadresse BASIC-Zeile: 2018 SYS 2064:NEW !word main-2, 2018 !byte $9e !text " 2064:" !byte $a2,$00,$00,$00 main sei ;Interrupts sperren lda #<rasterIrq ;unsere Interrupt-Routine sta $0314 ;in den IRQ-Vector eingtragen lda #>rasterIrq ;auch das MSB sta $0315 lda #STARTBLACK ;Bei STARTBLACK soll ein sta $d012 ;Raster-IRQ ausgelöst werden lda $d011 ;Zur Sicherheit auch noch and #%01111111 ;das höhste Bit für den sta $d011 ;gewünschten Raster-IRQ löschen lda $d01a ;IRQs vom ora #%00000001 ;VIC-II aktivieren sta $d01a cli ;Interrupts wieder erlauben rts ;zurück zum BASIC *=$1000 ;*** unsere eigene Interrupt-Routine rasterIrq lda $d019 sta $d019 ;IRQ bestätigen lda $d012 ;aktuelle Rasterzeile in den Akku bne doRed ;wenn ungleich 0 'rot' prüfen lda #$00 ;sonst 'schwarz' in den Akku sta $d020 ;und als Rahmen- sta $d021 ;sowie Hintergrundfarbe setzen lda #STARTRED ;Jetzt die Zeile für den nächsten sta $d012 ;Raster-IRQ festlegen jmp rasterIrqExit ;zum Ende springen doRed cmp #STARTRED ;haben wir 'rot' erreicht? bne doGold ;falls nicht weiter zu 'gold' lda #$02 ;rot sta $d020 ;als Rahmen- sta $d021 ;sowie Hintergrundfarbe setzen lda #STARTGOLD ;Rasterzeile für 'gold' sta $d012 ;festlegen jmp rasterIrqExit ;und raus doGold lda #$07 ;setze gelb sta $d020 ;als Rahmen- sta $d021 ;und Hintergrundfarbe lda #STARTBLACK ;Rasterzeile für schwarz (wir beginnen sta $d012 ;von vorne) festlegen rasterIrqExit pla ;Y vom Stack tay pla ;X vom Stack tax pla ;Akku vom Stack rti ;Interrupt verlassen
Ein Start offenbart dann aber, wie fehlerhaft es noch ist. Der Bildschirm hat überhaupt keine Ähnlichkeit mit einer Flagge und Eingaben kann man ebenfalls nicht vornehmen. 🙁
Was läuft da schief?
Bei genauer Betrachtung sind alle drei Farben zwar vertreten, aber eben nicht so wie gedacht. Wer die letzten beiden Beiträge über die Interrupts verfolgt hat, dem ist evtl. schon klar, was hier das Problem ist. Der Raster-IRQ stellt ja einen ganz normalen Interrupt dar. Wie bei Interrupts beschrieben, findet aber auch 60 mal in der Sekunde der Interrupt für die Tastatur, Uhrzeit und das Cursor-Blinken statt. Auch dieser springt über den RAM-Vector an $0314/15 zur Interrupt-Routine und landet so bei rasterIrq. Dadurch kommt dann das ganze System aus dem Tritt. Da $d012 für schwarz und rot geprüft wird, läuft die Routine meistens in den Abschnitt für Gold, also dominiert diese Farbe. Um festzustellen, ob der Interrupt vom VIC-II kam, kann $d019 geprüft werden. Ist das höchste Bit gesetzt, dann kam der Interrupt vom VIC-II und man kann mit den unteren Bits die genaue Quelle identifizieren.
Also einfach den Abschnitt mit den Befehlen lda $d019 und sta $d019 etwas umbauen (die Änderungen sind gelb hervorgehoben):
rasterIrq lda $d019 bmi doRasterIrq ;wenn ja -> Raster IRQ jmp rasterIrqExit ;*** hier beginnt die Raster-Interrupt-Funktion doRasterIrq sta $d019 ;IRQ bestätigen
Da von uns nur der Raster-IRQ verwendet wird, reicht es zu prüfen, ob $d019 negativ ist. Falls der Akku nun negativ ist, kam der Interrupt vom VIC-II und es geht bei doRasterIrq weiter, wenn er positiv ist geht es zum EXIT.
Das sieht doch schon besser aus, aber es bestehen noch zwei Probleme, um die wir uns kümmern sollten:
- Eine Eingabe ist immer noch nicht möglich
- Es kommt zu einem Versatz (s. Markierungen im Bild)
Um endlich wieder die Eingabe zu ermöglichen, muss die Routine nur zur bekannten System-Routine nach $ea31 springen, wenn es sich um keinen Raster-IRQ handelt.
Also den Beginn der Interrupt-Routine nochmal etwas umbauen:
rasterIrq lda $d019 bmi doRasterIrq ;wenn ja -> Raster IRQ lda $dc0d ;sonst, CIA-IRQ bestätigen cli ;IRQs erlauben jmp $ea31 ;und zur ROM-Routine springen
Mit lda $dc0d wird der Interrupt, der ja vom CIA1 kam, bestätigt, dann werden die Interrupts wieder erlaubt und zur Systemfunktion gesprungen. Ich empfehle übrigens sich mal anzusehen, was passiert, wenn man auf das lda oder den cli verzichtet.
Jetzt sollte sich auch automatisch das zweite Problem gelöst haben, der Versatz ist verschwunden.
Das war die erste kurze Einführung zum Rasterzeilen-Interrupt.
Wer jetzt verstanden hat, was hier genau geschieht, hat den Schlüssel zur Effekt-Programmierung in der Hand. Mit den hier gezeigten Grundlagen, ist es z. B. möglich einen Splitt-Screen (oben High-Res-Grafik / unten Textbildschirm) oder mehr als acht Sprites gleichzeitig auf den Bildschirm zu zaubern. So, wie hier die Rahmen- und Hintergrundfarbe abhängig von der jeweiligen Rasterzeile geändert wurde, kann man auch alle anderen Register ändern. Laßt eurer Phantasie einfach mal freien Lauf. Auch ein Öffnen des Rahmens, läßt sich durch eine entsprechende Manipulation der zugehörigen Register zur richtigen Zeit erzielen.
Ich möchte jetzt bei Niemandem den Eindruck erwecken, mit diesen paar Infos die neuste Super-Demo schreiben zu können, aber für eigene Experimente ist man nun gewappnet. Um in die Superliga der Rasterprofis aufzusteigen, ist noch viel mehr an Wissen und Arbeit notwendig. Schlagworte, die einem auf dem Weg dahin begegnen sind z. B. Anzahl der Taktzyklen je Rasterzeile bei PAL / NTSC oder die sog. Bad Lines.
Ich werde diesen Bereich hin und wieder erweitern. Außerdem plane ich einen DEMO-Bereich. Dort stelle ich dann kurz und knapp Spielereien wie oben erwähnt (mehr als acht Sprites, HiRes & Text gemischt usw.) vor.
Hallo Jörn, ein toller Artikel, sehr gut erklärt! Ich habe unter https://github.com/jklingel/Raster-Interrupts eine Code-Version für Turbo Macro Pro und Turbo Assembler abgelegt mit Kommentaren in Englisch. Viele Grüße, Jan
Hallo Jörn,
ich verstehe noch nicht ganz, wieso es nur 9 Bits für die aktuelle Rasterzeile beim PAL-System gibt.
Um 576 Zeilen mitzuzählen werden 10 Bits (1001000000) benötigt. Über das neute Bit in $d011 lässt sich maximal bis 511 zählen. Was passiert denn ab Zeile 512?
Habe ich etwas überlesen?
Viele Grüße
ok, ok, heute ist nicht mein Tag.
Auf der nächsten Seite steht alle genau beschrieben.
Keine Ahnung wieso ich das nicht gelesen habe.
Bitte diese Frage ignorieren.
Sorry nochmals
Hallo Jörn,
großartige Webseite! Vielen Dank für die ausführlichen und didaktisch prima gelungenen Lektionen.
Eine Frage dennoch:
Wird der Interrupt, der ja CIA1 kam, nicht mit STA $dc0d bestätigt? Oben steht nur das LDA.
Aber egal ob mit oder ohne STA, das System verhält sich ganz “normal”.
Und noch eine zweite:
Warum verschwindet denn der Versatz, wenn wir zur ROM-Routine springen?
Danke und viele Grüße,
Torsten
das Register $DC0D (Interrupt Control und Status) wird schon durchs Lesen gelöscht.
Das kannst du z. B. mit VICE oder dem C64 Studio prüfen. Verdoppel die Zeile mit dem LDA $DC0D und setzte einfach einen Breakpoint auf das erste. Beim Einzelschritt siehst du dann, dass das zweite LDA $DC0D nur noch 0 liefert.
Der Versatz ist natürlich nicht wirklich weg. Durch das geänderte Timing landet es unter dem Rahmen. Falls du VICE verwendest, schalte mal Rahmen-Debuggen an, dann siehst du den Versatz wieder.
Gruß,
Jörn
Hallo Jörn,
meine eigenen Rasterzeilen-Interrupt-Routinen trage ich ja in $0314 / $0315 ein, damit sie aufgerufen werden. Was muss ich eigentlich tun, um sie wieder auszutragen? (also um beim Beispiel zu bleiben: Wenn ich die deutsche Fahne nur während der Anzeige einer HIghscoreliste sehen will, aber nicht im Spiel selbst, wie kann ich das entsprechend steuern?)
Vielen Dank & Grüße!
Carsten
leg doch einfach mehrere Raster-IRQ-Routinen an und trage dann immer, die gerade benötigte, bei
$0314 / $0315
ein. So kannst du eine fürs Spiel und eine für die Highscoreliste verwenden.PS: Falls du die System-Routine wieder verwenden möchtest, dann musst du dir zu Beginn die Werte aus
$0314 / $0315
merken und diese bei Bedarf wieder zurückschreiben. Dies kommt dem von dir gewünschten Austragen wohl am nächsten.danke! Klappt 🙂
bzw., ich muss bei mir noch ein
lda $D01A
and #%11111110
sta $D01A
machen (also die IRQs vom VIC wieder deaktivieren), sonst kann ich keine Tastatureingaben abfragen
Wenn du wieder die System-Routine verwenden möchtest, dann muss auch noch der Raster-IRQ deaktiviert werden. Den nutzt das System nämlich nicht, dort wird nur durch den Timer über
$0314 / $0315
gesprungen.Er(Sie) meint dieses hier:
“findet auch 60 mal in der Sekunde”
Das problem sind PAL und NTSC.
dort ist aber nicht vom Rasterzeileninterrupt die Rede:
Dieser IRQ kommt vom CIA und läuft auch auf einem PAL-System 60 mal in der Sekunde.
Daher meine Frage, wo steht etwas von den erwähnten 60 Bildern in der Sekunde bei PAL?
ich mag mich täuschen, aber reden wir bei PAL nicht von 50 Bildern/Sekunde anstatt 60?
Sonst supergeiler Beitrag! Danke dafür!
Natürlich sind es bei PAL 50Hz, ich würde einen Fehler ja gerne korrigieren, aber wo steht hier etwas von 60 Bildern bei PAL? Ich finde es einfach nicht.