Raster-Timing

Aus C64-Wiki
Zur Navigation springenZur Suche springen

Unter Raster-Timing versteht man die, mehr oder minder exakte, Synchronisation eines Programms mit dem Rasterstrahl. Mit einem derart synchronisierten Programm sind zahlreiche Rastertricks möglich. Für manche Tricks genügt es dabei, mit der Rasterzeile synchronisiert zu sein, für andere benötigt man zyklengenaues Timing.

Grobe und exakte Synchronisation[Bearbeiten | Quelltext bearbeiten]

Zur Grob-Synchronisation verwendet man in der Regel die Rasterzeile, die vom VIC in den Registern 17 (nur Bit 7) und 18 ($D011 und $D012) mitgeteilt wird. Das Abfragen der Rasterzeile wird allerdings dadurch erschwert, dass man die beiden Register nicht gleichzeitig abfragen kann, sich zwischen zwei Abfragen aber die Rasterzeile ändern kann. Dadurch kann es passieren, dass das Ergebnis fehlerhaft ist.

Es gibt einige Möglichkeiten, wie man damit umgehen kann:

  • Rasterzeilen zwischen 56 und 255 enthalten kein Pendant mit gesetztem Bit 7 in Register 17. Wenn man diese ausgelesen hat, kennt man die genaue Rasterzeile.
  • Man kann in mehreren Schritten vorgehen: Nach der ersten Abfrage lässt sich die aktuelle Zeile meist stark eingrenzen. Schätzt man zudem noch die Zeit ab, die bis zur zweiten Abfrage vergeht, wird das Ergebnis dieser zweiten Abfrage oft eindeutig. Zur Not muss man noch eine dritte Abfrage vornehmen.
  • Man kann auch den Rasterzeilen-Interrupt benutzen. Dieser kann eindeutig auf jede beliebige Rasterzeile gelegt werden.

Die Synchronisation, die man auf diese Weise erhält, ist in der Regel leicht ungenau, da einerseits die Maschinenbefehle für die Abfrage mehrere Taktzyklen dauern und man à priori nicht weiß, auf welchen Zyklus innerhalb der Rasterzeile die Abfrage fällt. Andererseits wird vor dem Auslösen des Interrupts der laufende Maschinenbefehl noch vollständig abgearbeitet, wobei dessen Dauer unterschiedlich lang sein kann. Dadurch wird der Rasterzeilen-Interrupt auch nicht immer zur selben Zeit gestartet.

Es gibt einige Rastertricks, für die eine grobe Synchronisation ausreicht. Beispielsweise kann man einen vertikalen Hyperscreen auf diese Weise einfach hinbekommen. Solche Rastertricks lassen sich auch in BASIC realisieren: Mit Hilfe des WAIT-Befehls beziehungsweise durch den Befehl SYS65374 ist eine ausreichende Synchronisation möglich.

Für andere Rastertricks ist eine taktzyklen-genaue Synchronisation notwendig. Beispielsweise für einen horizontalen Hyperscreen. Es gibt zahlreiche Techniken, mit denen man eine solche exakte Synchronisation erreichen kann - siehe Abschnitt Techniken. Jede hat ihre eigenen Vor- und Nachteile.

Ist eine Synchronisation erst einmal erreicht, kann man diese durch Taktzyklen zählen auf längere Zeit aufrecht erhalten, notfalls sogar für den Rest des Programms.

Probleme[Bearbeiten | Quelltext bearbeiten]

Badlines und Sprites erschweren das Raster-Timing, da hierfür der Prozessor für einige Takte abgeschaltet wird. Wann genau und für wie viele Takte der Prozessor abgeschaltet wird, hängt zum einen davon ab, welche Sprites in einer Rasterzeile aktiv sind. Ersetzt man beispielsweise bei einem funktionierenden Timing Sprite 0 durch Sprite 7, kann dies das komplette Timing zerstören.

Zum anderen hängt das Timing aber auch von der Art der Speicherzugriffe des Prozessors während des Abschaltevorgangs ab. Schreibzugriffe werden nämlich noch ausgeführt, während Lesezugriffe auf die Zyklen nach dem Wiederanschalten verschoben werden.

Badlines haben noch einen weiteren Effekt, der sich negativ auf das Timing auswirken kann: In Badlines stehen dem Prozessor durch das Abschalten nur sehr wenig Taktzyklen zur Verfügung, um auf ein Ereignis zu reagieren. Das sorgt für Unflexibilität. So ist es beispielsweise nicht möglich, den Prozessor um exakt einen Taktzyklus zu verzögern, da alle Befehle mindestens zwei Taktzyklen lang sind. In solchen Fällen hilft es oft nur noch, das ganze Programm nochmal komplett umzustellen.

Und noch ein weiteres Problem tritt auf: Es gibt verschiedene Versionen des VIC, die unterschiedlich viele Taktzyklen pro Rasterzeile haben. Möchte man ein Programm schreiben, welches auf allen Versionen lauffähig ist, muss man die Programme auf die unterschiedlichen Versionen zuschneiden. Meist verwendet man hierfür selbstmodifizierenden Code, da während der Ausführung des Codes keine Zeit mehr bleibt, um zwischen den Versionen zu unterscheiden.

Techniken[Bearbeiten | Quelltext bearbeiten]

Die nachfolgenden Beispiele gehen von einem (im deutschsprachigen Raum üblichen) 6569-VIC (oder dessen neuerer Version, dem 8565-VIC) aus. Für andere Versionen des VIC sind meist kleinere Anpassungen notwendig. Möchte man ein Programm schreiben, das auf allen VIC-Versionen funktioniert geht man meist so vor, dass man zu Beginn des Programms die Version ermittelt und dann den Code so ändert, sodass dieser auf die benutzte VIC-Version angepasst ist. Beispielsweise kann man zwei NOP-Befehle durch einen CMP #$00-Befehl ersetzen, um bei gleicher Programmlänge zwei Taktzyklen einzusparen.

Um die Synchronisation sichtbar zu machen, wird in den nachfolgenden Programmen die Hintergrundfarbe kurz erhöht und dann wieder erniedrigt. Dadurch kann man gut sehen, zu welchem Zeitpunkt der Farbwechsel stattfand. Von diesen Zeitpunkten kann man auf des Timing des Programms zurückschließen. Das Timing ist bei jeder Technik auch nochmal in einem Timing-Diagrammen unterhalb des Programms abgebildet. Die Zählung der Zyklen beginnt dabei bei 1 (z.B. VICE fängt bei 0 an), und das Timing folgt der Registersicht, nicht dem Timing der C/G-Zugriffe (siehe Bemerkung zum Timing in VIC#Arbeitsweise).

Bei den Programmen wurde Wert darauf gelegt, diese kurz zu halten, um das eigentliche Prinzip zu verdeutlichen. Dabei wurden einige Annahmen gemacht, die beim Einschaltzustand des Computers vorliegen, wie beispielsweise, dass der BCD-Modus abgeschaltet ist, dass keine Sprites angeschaltet sind, der Start im Textmodus erfolgt, und dergleichen mehr. Für ein echtes Programm sollten solche Dinge den Erfordernissen angepasst werden.

Busy-Wait-Schleife (grob)[Bearbeiten | Quelltext bearbeiten]

Synchronisation mit einer Busy-Wait-Schleife: Die Synchronisation ist nicht exakt.

Die einfachste Möglichkeit ein grobes Timing hinzubekommen ist mit einer Busy-Wait-Schleife: Eine vorgegebene Zeile - im Beispiel Zeile 111 - wird so lange mit der Rasterzeile aus Register 18 ($D012) verglichen bis diese erreicht wurde.

*=$c000
         sei           ; Interrupt abschalten, der stört nur

loop     lda #111
-        cmp $d012     ; auf Zeile 111 warten
         bne -

         nop           ; noch etwas abwarten, damit der Effekt
         nop           ; nicht vom Rahmen verdeckt wird
         nop
         nop
         nop
         nop

         inc $d021     ; Hintergrund gelb
         dec $d021     ; Hintergrund wieder blau

         lda #112
-        cmp $d012     ; auf Zeile 112 warten
         bne -

         jmp loop      ; von neuem beginnen

Zuerst wird auf Zeile 111 gewartet, dann noch weitere 12 Taktzyklen, damit sich der Rasterstrahl im Ausgabebereich befindet und man was sehen kann. Dann wird kurz die Farbe des Hintergrunds auf gelb gewechselt und gleich wieder zurück. Dies wird endlos wiederholt.

Der gelbe Balken steht nicht still, weil das Timing nur grob ist: Der Wechsel der Rasterzeile kann zu einem beliebigen Zeitpunkt innerhalb der ersten Schleife stattfinden und diese ist 7 Taktzyklen lang. Demnach gibt es auch 7 verschiedene Positionen für den Balken, von denen mal die eine, mal die andere ausgegeben wird, je nachdem, zu welchem Zeitpunkt der Wechsel der Rasterzeile stattgefunden hat. Das nachfolgende Diagramm zeigt die sieben möglichen Timings:

Timing-Diagramm der Busy-Wait-Schleifen-Synchronisation: Oben werden die Taktzyklen seit dem Start der Rasterzeile durchgezählt. Darunter ist, zur besseren Orientierung, der Bildschirminhalt oberhalb von Rasterzeile 111 angezeigt.[1] Jeder Streifen im unteren Bereich der Grafik zeigt eines der sieben möglichen Befehls-Timings an.

Der CMP-Befehl liest die Speicherzelle $D018 im letzten Taktzyklus des Befehls aus. Zu diesem Zeitpunkt kann der Wechsel frühestens bemerkt werden (1. Streifen). Im schlechtesten Fall erfolgt der Wechsel direkt nach dem CMP-Befehl, also im ersten Taktzyklus des BNE-Befehls. Dann wird er erst im 7. Taktzyklus der Rasterzeile bemerkt (7. Streifen).

Sobald der Wechsel bemerkt wurde, beendet der BNE-Befehl die Schleife und benötigt deswegen nur 2 Taktzyklen. Danach folgen die sechs NOP-Befehle, die in der Abbildung der Übersichtlichkeit halber, nicht aufgeführt wurden. Der INC-Befehl verändert das Register $D021 im letzten Taktzyklus des Befehls, was sich optisch erst im folgenden Taktzyklus durch einen gelben Balken bemerkbar macht. Der Balken hat exakt die Länge des DEC-Befehls, denn dieser setzt in seinem letzten Taktzyklus die Hintergundfarbe wieder zurück.

Busy-Wait-Schleife
Vorteile:
  • Einfach zu programmieren.
  • Funktioniert auch in Badlines.
  • Unabhängig vom verwendeten Video-Chip.
  • Synchronisation nach 3-9 Taktzyklen erreicht.
Nachteile:
  • Synchronisation ist ungenau.
  • Bremst Hauptprogramm aus.
  • Muss für Synchronisation vor Zeile 56 und nach Zeile 255 etwas angepasst werden.
Anpassung für NTSC-VIC: Nicht notwendig. Der Code funktioniert mit allen VIC-Versionen.

Einfacher Rasterzeilen-Interrupt (grob)[Bearbeiten | Quelltext bearbeiten]

Synchronisation mit einem einfachen Rasterzeilen-Interrupt: Die Synchronisation ist nicht exakt. (Die Buchstaben am rechten Rand dienen der Orientierung im Timing-Diagramm.)

Alternativ zur Busy-Wait-Schleife aus dem vorigen Abschnitt kann man auch den Rasterzeilen-Interrupt für ein grobes Timing benutzen. Dieser wird auf eine Zeile - im Beispiel Zeile 111 - programmiert und löst einen Interrupt aus, sobald der Rasterstrahl diese Zeile erreicht hat.

*=$c000
         sei

         lda #<irq    ; Interrupt auf eigene Routine legen
         sta $0314
         lda #>irq
         sta $0315

         lda #111     ; Rasterzeile 111 programmieren
         sta $d012
         lda $d011
         and #$7f
         sta $d011

         lda $d01a    ; Rasterzeilen-Interrupt anschalten
         ora #$01
         sta $d01a

         lda #$7f     ; Timer-Interrupt abschalten
         sta $dc0d

         cli

; Die nachfolgende Warteschleife simuliert ein komplexes Hauptprogramm.
; Der Interrupt kann zu jedem Zeitpunkt innerhalb des Programms aktiviert
; werden.
-        inc $c100,x  ; benötigt 7 Taktzyklen
         bne -        ; benötigt 2 oder 3 Taktzyklen
         beq -        ; benötigt 3 Taktzyklen

irq      inc $d021    ; Hintergrund gelb
         dec $d021    ; Hintergrund wieder blau

         lda $d019    ; Rasterzeilen-Interrupt bestätigen
         sta $d019

         pla          ; Interrupt geordnet beenden
         tay
         pla
         tax
         pla
         rti

Als Vorbereitung wird der Interrupt auf die eigene Routine umgebogen, dann wird der Rasterzeilen-Interrupt programmiert und gestartet. Zuletzt wird der Timer-Interrupt (der den Cursor blinken lässt und die Tastatur abfragt) abgeschaltet. Danach kann in ein beliebiges Hauptprogramm gesprungen werden.

In der Interrupt-Routine selbst wird einfach nur die Hintergrundfarbe kurz geändert und danach der Interrupt ordentlich beendet.

Der gelbe Balken steht nicht still, weil das Timing nur grob ist: Der Aufruf des Interrupts kann zu einem beliebigen Zeitpunkt innerhalb des Hauptprogramms stattfinden. Insgesamt gibt es 8 verschiedene Zeitpunkte, zu denen der Aufruf der Interrupt-Routine beginnen kann.[2] Das nachfolgende Diagramm zeigt das zugehörige Timing:

Timing-Diagramm der Synchronisation mit einfachem Rasterzeilen-Interrupt: Oben werden die Taktzyklen seit dem Start der Rasterzeile durchgezählt, wobei in der Mitte 18 Taktzyklen ausgelassen wurden (markiert durch Zickzacklinie). Darunter ist, zur besseren Orientierung, der Bildschirminhalt oberhalb von Rasterzeile 111 angezeigt.[1] Jeder Streifen im unteren Bereich der Grafik zeigt eines der acht möglichen Befehls-Timings an (In den Streifen 2 und 3 gibt es vor dem Aufruf der Interrupt-Routine weitere Varianten, bei denen nach einem Sprungbefehl das Hauptprogramm unterbrochen wird; diese sind hier nicht gesondert aufgeführt.)

In einem der Takte 3 bis 10 beginnt der Sprung in die Interrupt-Routine. Dafür benötigt der Prozessor 7 Taktzyklen. Danach wird allerdings erst die Interrupt-Routine des Kernals ausgeführt (diese kann man durch geeignete Programmierung auch umgehen - siehe Doppelter Rasterzeilen-Interrupt). Die Interrupt-Routine des Kernals dauert exakt 29 Taktzyklen und springt dann zu der Routine im Programm.

Hinweis: Normalerweise muss der Interrupt spätestens beim vorletzten Zyklus eines Befehls ausgelöst worden sein, damit die Interrupt-Routine direkt nach dem Befehl gestartet werden kann. Bei den Branch-Befehlen gibt es allerdings eine kleine Anomalie in dieser Hinsicht: Wenn der Befehl verzweigt, dann muss der Interrupt bereits im drittletzten Zyklus ausgelöst worden sein. Deswegen wird in der untersten Zeile nicht direkt nach dem BNE-Befehl der Interrupt gestartet, sondern erst nach dem CMP-Befehl.

Einfacher Rasterzeilen-Interrupt
Vorteile:
  • Relativ einfach zu programmieren.
  • Während auf die Rasterzeile gewartet wird, kann anderer Code ausgeführt werden.
  • Unabhängig vom verwendeten Video-Chip.
  • Funktioniert in jeder Rasterzeile.
  • Synchronisation nach 38-45 (ohne Kernal nach 9-16) Taktzyklen erreicht.
  • Kann auch mit anderen Interrupt-Quellen kombiniert werden.
Nachteile:
  • Synchronisation ist ungenau.
  • Synchronisation in Badlines möglicherweise erst in der Folgezeile.
Anpassung für NTSC-VIC: Nicht notwendig. Der Code funktioniert mit allen VIC-Versionen. Man beachte allerdings, dass in Badlines die Synchronisation, je nach Programmierung, erst in der Folgezeile erfolgen kann. Dann muss die unterschiedliche Länge der Rasterzeilen ausgeglichen werden.

Vier-Zeilen-Busy-Wait (exakt)[Bearbeiten | Quelltext bearbeiten]

Synchronisation mit Vier-Zeilen-Busy-Wait: Die Synchronisation ist exakt aber erst drei Zeilen später, als sie angefangen wurde.

Beginnend mit einer Busy-Wait-Schleife oder einem einfachen Rasterzeilen-Interrupt kann man an jedem weiteren Zeilenwechsel die Synchronisation verfeinern. Dazu fragt man gegen Ende der Zeile die Rasterzeile ab. Ist man noch in der selben Zeile, wartet man ein paar zusätzliche Taktzyklen, sonst nicht. Das wiederholt man so lange, bis die Synchronisation exakt ist.

Im nachfolgenden Programm erfolgt der Start durch eine Busy-Wait-Schleife. Mit dem Rasterzeilen-Interrupt funktioniert diese Methode analog, man muss nur die Wartezyklen nach dem Aufruf der Interrupt-Routine etwas anpassen.

*=$c000
         sei           ; Interrupt abschalten, der stört nur

loop     lda #111      ; auf Zeile 111 warten
-        cmp $d012
         bne -

         cmp ($00,X)   ; 54 Zyklen warten
         cmp ($00,X)   ; für NTSC-VICs noch 1 oder 2 mehr
         cmp ($00,X)
         cmp ($00,X)
         cmp ($00,X)
         cmp ($00,X)
         cmp ($00,X)
         cmp ($00,X)
         cmp ($00,X)

         lda $d012
         cmp #112
         beq +         ; falls noch in Zeile 111
         nop           ; drei Extra-Zyklen warten
         nop

+        cmp ($00,X)   ; 52 Zyklen warten
         cmp ($00,X)   ; für NTSC-VICs noch 1 oder 2 mehr
         cmp ($00,X)
         cmp ($00,X)
         cmp ($00,X)
         cmp ($00,X)
         cmp ($00,X)
         cmp ($00),Y
         cmp ($00),Y

         lda $d012
         cmp #113
         beq +         ; falls noch in Zeile 112
         cmp $00       ; zwei Extra-Zyklen warten

+        cmp ($00,X)   ; 53 Zyklen warten
         cmp ($00,X)   ; für NTSC-VICs noch 1 oder 2 mehr
         cmp ($00,X)
         cmp ($00,X)
         cmp ($00,X)
         cmp ($00,X)
         cmp ($00,X)
         cmp ($00,X)
         cmp ($00),Y

         lda $d012     ; falls noch in Zeile 113  
         cmp #114      ; einen Extra-Zyklus warten
         bne +

+        nop           ; noch etwas abwarten, damit der Effekt
         nop           ; nicht vom Rahmen verdeckt wird
         nop
         nop

         inc $d021     ; Hintergrund gelb
         dec $d021     ; Hintergrund wieder blau

         jmp loop      ; von neuem beginnen

Das nachfolgende Timing-Diagramm veranschaulicht, was passiert:

Timing-Diagramm der Synchronisation mit Vier-Zeilen-Busy-Wait: Das Diagramm zieht sich über vier Rasterzeilen hinweg. Oben werden jeweils die Taktzyklen seit dem Start der Rasterzeile durchgezählt, wobei in den oberen drei Diagrammen in der Mitte 46 Taktzyklen ausgelassen wurden (markiert durch Zickzacklinie). In der letzten Zeile ist darunter, zur besseren Orientierung, der Bildschirminhalt oberhalb von Rasterzeile 111 angezeigt.[1] Die Streifen darunter zeigen die möglichen Befehls-Timings an: In Rasterzeile 111 sind es noch sieben, in Zeile 112 vier, in Zeile 113 zwei und in Zeile 114 ist die Synchronisation exakt.

Die Abfrage der Rasterzeile ist in der ersten Zeile so getimed, dass diese in drei Fällen noch in der gleichen Rasterzeile erfolgt und in den anderen vier Fällen in der darauffolgenden. Das Timing der ersten Fälle wird dann, durch Extrazyklen, an das der anderen vier angepasst. Dies wird noch zweimal wiederholt.

Vier-Zeilen-Busy-Wait
Vorteile:
  • Relativ einfach zu programmieren.
  • Synchronisation ist exakt.
  • Kann mit leichten Anpassungen auch in Badlines benutzt werden.
  • Kann in der Interrupt-Variante auch mit anderen Interrupt-Quellen kombiniert werden.
Nachteile:
  • Synchronisation erst nach 194 Taktzyklen erreicht.
  • Muss für unterschiedliche VIC-Chips angepasst werden.
  • Muss in der Busy-Wait-Variante für Synchronisation vor Zeile 56 und nach Zeile 255 etwas angepasst werden.
Anpassung für NTSC-VIC: In den drei Wartebereichen muss je nach verwendetem Chip ein oder zwei Taktzyklen zusätzlich gewartet werden.

Doppelter Rasterzeilen-Interrupt (exakt)[Bearbeiten | Quelltext bearbeiten]

Synchronisation mit doppeltem Rasterzeilen-Interrupt: Synchronisation ist exakt aber erst zwei Zeilen später, als sie angefangen wurde.

Der Prozessor des C64 erlaubt es, dass ein Interrupt von einem weiteren Interrupt unterbrochen werden kann. Dies kann man zur fast exakten Synchronisation nutzen.

Die Idee ist, dass man im ersten Interrupt nur Befehle ausführt, die exakt zwei Taktzyklen lang sind. Der zweite Interrupt kann dann nur noch an einem von zwei möglichen Zeitpunkten erfolgen. Für einige Anwendungen ist dies bereits exakt genug. Wenn man ganz exaktes Timing benötigt, kann man, wie am Ende der Vier-Zeilen-Busy-Wait-Methode, noch einmalig am Zeilenende eine Abfrage hinzufügen.

*=$c000
         sei

         lda #$35     ; Kernal-ROM abschalten
         sta $01

         lda #<irq    ; Interrupt auf eigene Routine legen
         sta $fffe
         lda #>irq
         sta $ffff

         lda #111     ; Rasterzeile 111 programmieren
         sta $d012
         lda $d011
         and #$7f
         sta $d011

         lda $d01a    ; Rasterzeilen-Interrupt anschalten
         ora #$01
         sta $d01a

         lda #$7f     ; Timer-Interrupt löschen
         sta $dc0d

         lda $d019    ; Rasterzeilen-Interrupt bestätigen,
         sta $d019    ; falls bereits einer losgetreten wurde

         cli

; Die nachfolgende Warteschleife simuliert ein komplexes Hauptprogramm.
; Der Interrupt kann zu jedem Zeitpunkt innerhalb des Programms aktiviert
; werden.
-        inc $c100,x  ; benötigt 7 Taktzyklen
         bne -        ; benötigt 2 oder 3 Taktzyklen
         beq -        ; benötigt 3 Taktzyklen

irq      pha          ; Akku-Inhalt des Hauptprogramms sichern
         lda #112     ; Rasterzeile 112 programmieren
         sta $d012
         lda $d011
         and #$7f
         sta $d011

         lda #<irq2   ; Interrupt auf zweite Routine legen
         sta $fffe
         lda #>irq2
         sta $ffff

         lda $d019    ; Raster-Interrupt bestätigen
         sta $d019

         cli          ; neue Interrupts zulassen
         nop          ; auf Interrupt warten
         nop
         nop
         nop
         nop
         nop
         nop
         nop          ; spätestens nach diesem Befehl wird
                      ; der Interrupt aufgerufen, der nie 
                      ; zurückkehren wird; für NTSC-VICs ist ein
                      ; weiteres NOP notwendig, welches bei PAL-VICs
                      ; nicht weiter stört

irq2     pla          ; Rückkehr-Daten des zweiten Interrupts löschen
         pla
         pla

         lda #<irq    ; Interrupt wieder auf erste Routine legen
         sta $fffe
         lda #>irq
         sta $ffff

         lda #111     ; Rasterzeile 111 programmieren
         sta $d012
         lda $d011
         and #$7f
         sta $d011

         cmp ($00),y  ; Noch 10 Zyklen auf Zeilenende warten
         cmp ($00),y  ; Bei NTSC-VICs noch 1 oder 2 Zyklen mehr

         lda $d012
         cmp #113     ; falls noch in Zeile 112
         bne +        ; einen Extra-Zyklus

+        nop          ; noch etwas abwarten, damit der Effekt
         nop          ; nicht vom Rahmen verdeckt wird
         nop
         nop

         inc $d021    ; Hintergrund gelb
         dec $d021    ; Hintergrund wieder blau

         lda $d019    ; Rasterzeilen-Interrupt bestätigen
         sta $d019

         pla          ; Akku-Inhalt des Hauptprogramms wieder laden
         rti          ; Aus erstem(!) Interrupt zurückkehren

Bei den Vorbereitungen wird der Interrupt auf die eigene Interrupt-Routine umgebogen und auf Rasterzeile 111 programmiert. Dabei wird die Interrupt-Routine des Kernals übergangen. Innerhalb der Internet-Routine wird der Interrupt auf eine zweite Interrupt-Routine umgebogen und auf Rasterzeile 112 programmiert. Der Interrupt wird kurz darauf unterbrochen.

In der zweiten Interrupt-Routine werden die Rücksprungdaten dieses zweiten Interrupts entfernt. Dadurch erfolgt der Rücksprung am Ende des zweiten Interrupts direkt in das Hauptprogramm. Nachdem der Interrupt wieder auf die erste Routine zurückgebogen wurde und Rasterzeile 111 für den nächsten Durchlauf programmiert wurde, wird noch das Zeilenende abgepasst, um die exakte Synchronisation zu erreichen.

Das nachfolgende Diagramm zeigt das zugehörige Timing:

Timing-Diagramm der Synchronisation mit doppeltem Interrupt: Das Diagramm zieht sich über drei Rasterzeilen hinweg. Oben werden jeweils die Taktzyklen seit dem Start der Rasterzeile durchgezählt. In der letzten Zeile ist darunter, zur besseren Orientierung, der Bildschirminhalt oberhalb von Rasterzeile 111 angezeigt.[1] Die Streifen darunter zeigen die möglichen Befehls-Timings an: In Rasterzeile 111 sind es acht verschiedene Timings. Die NOP-Befehle am Ende der Zeile sorgen dafür, dass in Rasterzeile 112 die Anzahl auf zwei Timings sinkt. Durch eine Abfrage am Ende dieser Zeile kann dann exakte Synchronisation in Rasterzeile 113 hergestellt werden.

In Rasterzeile 111 kann der erste Zyklus der Interrupt-Routine zwischen Zyklus 10 und Zyklus 17 liegen. In Rasterzeile 112 kann der Anfang nur noch in den Zyklen 10 oder 11 liegen, da sichergestellt ist, dass der Befehl vor dem Interrupt-Aufruf ein NOP-Befehl war.

Doppelter Rasterzeilen-Interrupt
Vorteile:
  • Synchronisation ist exakt.
  • Stört das Hauptprogramm kaum.
  • Kann in jeder Rasterzeile benutzt werden.
Nachteile:
  • Synchronisation erst nach 131 Taktzyklen erreicht.
  • Benötigt in Badlines eine zusätzliche Rasterzeile.
  • Muss für unterschiedliche VIC-Chips angepasst werden.
  • Ist relativ kompliziert.
  • Ist (ohne viel Aufwand) nicht kompatibel mit weiteren Interrupt-Routinen.
Anpassung für NTSC-VIC: Im Wartebereich der Rasterzeile 112 muss je nach verwendetem Chip ein oder zwei Taktzyklen zusätzlich gewartet werden. Zudem wird in Rasterzeile 111 ein weiterer NOP-Befehl benötigt.

Künstliche Badline (exakt)[Bearbeiten | Quelltext bearbeiten]

Synchronisation mit einer künstlichen Badline: Die Synchronisation ist exakt, aber es tauchen Artefakte auf, die in realen Programmen hin- und herspringen.

Diese Technik nutzt aus, dass der Prozessor in einer Badline für einige Zeit abgeschaltet wird. Wenn die Badline künstlich erzeugt wird, findet das Abschalten direkt nach dem Befehl statt, der die Badline erzeugt hat. Der nächste Befehl ist automatisch exakt synchronisiert.

Als Voraussetzung benötigt diese Technik eine grobe Synchronisation auf eine Zeile, die keine Badline ist. Hier wird eine Busy-Wait-Schleife dafür benutzt.

*=$c000
         sei            ; Interrupt abschalten, der stört nur

loop     lda #111
-        cmp $d012      ; Auf Rasterzeile 111 warten
         bne -

         nop            ; Timing, damit der letzte Taktzyklus des
                        ; nachfolgenden sta-Befehls in der Rasterzeile
                        ; auf einen Zyklus >=14 fällt

         lda #$1f
         ldx $d011
         sta $d011      ; Badline erzeugen
         stx $d011      ; direkt wieder zurück

         nop            ; noch etwas abwarten, damit der Effekt
         nop            ; nicht vom Rahmen verdeckt wird
         nop
         nop
         nop
         nop
         nop
         nop
         nop

         inc $d021      ; Hintergrund gelb
         dec $d021      ; Hintergrund wieder blau

         jmp loop       ; von neuem beginnen

Eine Badline tritt immer auf, wenn die letzten drei Bits von Register 17 ($D011, YSCROLL) mit den letzten drei Bits von Register 18 ($D012, Rasterzeile) übereinstimmen. Ändert man die letzten drei Bits von Register 17 so, dass sie mit denen von Register 18 übereinstimmen, erzeugt man auf künstlichem Weg eine Badline.

Da nach der Schleife am Anfang des Programms klar ist, dass die aktuelle Rasterzeile die Rasterzeile 111 ist, kann der neue Wert von YSCROLL direkt angegeben werden. Mit Berechnung (lda $d012: and #$07: ora #$18: sta $d011) würde die Technik auch funktionieren.

Direkt nach dem Erzeugen wird Register 17 wieder zurückgesetzt, sonst würde ab dieser Zeile der Bildschirm verschoben dargestellt werden und der Trick würde beim nächsten Durchgang nicht mehr funktionieren.

Dieser Befehl stx $d011 ist bereits exakt synchroisiert: Er beginnt in Taktzyklus 55 in Rasterzeile 111. Das nachfolgende Timing-Diagramm zeigt, wie dies funktioniert:

Timing-Diagramm der Synchronisation mit einer künstlichen Badline. Oben werden die Taktzyklen seit dem Start der Rasterzeile durchgezählt. In Rasterzeile 111 wurden in der Mitte mehrere Taktzyklen ausgelassen, in denen der Prozessor abgeschaltet ist. Ab Taktzyklus 55 besteht eine exakte Synchronisation.

Der sta-Befehl erzeugt eine Badline, weshalb der Prozessor sofort abgeschaltet wird. Dies funktioniert allerdings nur in den Taktzyklen 14 bis 54. Durch die Abschaltung des Prozessors werden nach dem sta-Befehl keine weiteren Befehle ausgeführt. In einer Übergangsphase, die im Diagramm gestrichelt dargestellt ist, könnte der Prozessor aber noch Schreibzugriffe ausführen. Da der erste Zugriff des nachfolgenden stx-Befehls aber ein Lesezugriff ist, unterbleiben diese.

Die Übergangsphase ist die Ursache für die Artefakte, die angezeigt werden: Während dieser drei Taktzyklen kann der VIC noch nicht auf den Speicher zugreifen, versucht es aber. Das Ergebnis ist, dass der VIC statt des tatsächlich vorhandenen Zeichens, das Zeichen 255 ausliest und dann auch als Artefakte anzeigt. Die Artefakte kann man nicht verhindern, man kann sie aber kaschieren:

  • Man kann Sprites in der Hintergrundfarbe benutzen, um die Artefakte zu überdecken.
  • Nutzt man einen eigenen Zeichensatz, kann man das Zeichen 255 geeignet anpassen. Im Beispielprogramm wären es acht Null-Bytes.
  • Man kann die Farbe der Artefakte auf die Hintergrundfarbe setzen. Die Farbe der Zeichen bestimmt sich aus dem Opcode des ersten Befehls nach dem Abschalten (hier STX mit Opcode $8E). Das untere Nibble des Obcodes (hier $E) ist die Farbe der Artefakte (hier hellblau). Verwendet man einen anderen Befehl, kann man damit die Farbe anpassen.
Künstliche Badline
Vorteile:
  • Relativ einfach zu programmieren.
  • Unabhängig vom verwendeten Video-Chip.
  • Synchronisation nach 54 Taktzyklen erreicht.
Nachteile:
  • Geht nicht in einer Badline.
  • Funktioniert nicht im oberen und unteren Rahmen.
  • Erzeugt Artefakte.
Anpassung für NTSC-VIC: Nicht notwendig. Der Code funktioniert mit allen VIC-Versionen.

Lichtgriffel-Methode (exakt)[Bearbeiten | Quelltext bearbeiten]

Synchronisation mit der Lichtgriffel-Methode: Exakte Synchronisation in nur 36 Taktzyklen.

Die erste Idee, die einem sicherlich kommt, wenn man Synchronisation programmieren möchte, ist die Abfrage des aktuellen Taktzyklus, um dann darauf zu reagieren. Ein entsprechendes Register stellt der VIC allerdings nicht zur Verfügung.

Es gibt aber dennoch die Möglichkeit, den aktuellen Taktzyklus abzufragen. Hierzu bedient man sich eines virtuellen Lichtgriffels: Wenn ein echter Lichtgriffel den Rasterstrahl an seiner Spitze registriert, schickt er ein Signal auf Pin 6 des Controlport 1. Dieser Pin ist direkt mit dem VIC verbunden. Kommt dort ein Signal an, so speichert der VIC die aktuelle X-Koordinate des Rasterstrahls in Register 19 ($D013).

Dies lässt sich auch durch Software simulieren: Pin 6 von Controlport 1 ist nämlich zudem auch noch mit Pin B4 des CIA1 verbunden. Dadurch kann man auch über den CIA1 ein Signal auf diesen Pin legen und damit das Speichern der X-Koordinate auslösen.

*=$c000
         sei             ; Interrupt abschalten, der stört nur

         lda #%00010000  ; Pin B4 des CIA1 auf Schreiben setzen
         sta $dc03
         lda #$ff        ; Reset von Pin B4
         sta $dc01

loop     lda #111
-        cmp $d012       ; auf Zeile 111 warten
         bne -

         nop             ; Verzögerung, um bis mindestens
         nop             ; Zyklus 17 zu warten

         lda #$00        ; Signal auf Pin 4 legen
         sta $dc01
         lda $d013       ; X-Koordinate des Lichtgriffels abfragen
         lsr
         lsr
;        and #$07        ; Absicherung, siehe Text
         sta mark+1      ; Sprungziel speichern
mark     bpl mark
         !by $c9         ; siehe Text
         !by $c9
         !by $c9
         !by $c9
         !by $c9
         !by $c5
         nop

         inc $d021       ; Hintergrund gelb
         dec $d021       ; Hintergrund wieder blau

         lda #$ff        ; Reset von PIN B4
         sta $dc01

         lda #112
-        cmp $d012       ; auf Zeile 112 warten
         bne -

         jmp loop        ; von neuem beginnen

Um das Programm zu verstehen, muss man erst einmal wissen, welche Werte in Register 19 des VIC ($D013) zurückgeliefert werden: Die Taktzyklen 1 bis 16 zählen hierbei noch zur vorigen Zeile und liefern große Werte. Diese könnte man durchaus auch zur Synchronisation benutzen, nur wird die Berechnung danach komplizierter. Mit einer großen Tabelle könnte man aber so nochmal 4 Taktzyklen einsparen. Das Programm hier geht den einfacheren Weg und wartet, bis Taktzyklus 17 sicher erreicht ist.[3]

In Taktzyklus 17 liefert das Register den Wert 2 zurück. Danach werden die Werte mit jedem Taktzyklus um 4 größer. Dementsprechend teilt das Programm den Wert mit den beiden LSR-Befehlen durch 4. Im Akku des Prozessors befindet sich danach einer der Werte von 0 bis 6. Jetzt muss man nur noch die Programmausführung entsprechend dieses Werts verzögern, und schon ist die exakte Synchronisation erreicht.

Um diese Verzögerung zu erhalten, wird der Wert des Akkus als Parameter des BPL-Befehls gespeichert. Der verzweigt immer, denn die Zahlen von 0 bis 6 sind alle positiv. Je nach Wert im Akku, springt der BPL zur nächsten Anweisung. Daraus ergeben sich unterschiedliche Befehlssequenzen, die wie folgt aussehen:

; a=0:
cmp #$c9
cmp #$c9
cmp #$c5
nop
; 8 Zyklen
; a=1:
cmp #$c9
cmp #$c9
cmp $ea

; 7 Zyklen
; a=2:
cmp #$c9
cmp #$c5
nop

; 6 Zyklen
; a=3:
cmp #$c9
cmp $ea


; 5 Zyklen
; a=4:
cmp #$c5
nop


; 4 Zyklen
; a=5:
cmp $ea



; 3 Zyklen
; a=6:
nop



; 2 Zyklen

Das nachfolgende Timing-Diagramm zeigt dies nochmal:

Timing-Diagramm der Synchronisation mit der Lichtgriffel-Methode: Oben werden die Taktzyklen seit dem Start der Rasterzeile durchgezählt. Darunter ist, zur besseren Orientierung, der Bildschirminhalt oberhalb von Rasterzeile 111 angezeigt.[1]. Jeder Streifen im unteren Bereich der Grafik zeigt eines der sieben möglichen Befehls-Timings an.

Im obersten Streifen wird die Lichtgriffel-Position in Zyklus 17 abgefragt, eine 2. Geteilt durch 4 ergibt 0 (es wird abgerundet). Dieser Wert wird als Sprungziel für den BPL-Befehl gesetzt. Die 0 bedeutet hierbei nichts anderes, als dass einfach beim nächsten Befehl weitergemacht wird.

Im zweiten Streifen wird die Lichtgriffel-Position in Zyklus 18 abgefragt, eine 6. Geteil durch 4 ergibt 1. Der BPL-Befehl überspringt dadurch ein $c9. Wodurch die Sequenz einen Taktzyklus kürzer ist.

Die anderen Streifen funktionieren analog. Nach Taktzyklus 35 sind alle Streifen synchronisiert.

Es gibt allerdings auch ein paar Dinge, die man bei diesem Ansatz beachten muss:

  • Man kann diese Methode nur einmalig während eines kompletten Bildschirmaufbaus verwenden. Der VIC speichert die Position des Lichtgriffels so lange und liefert immer den selben Wert zurück.
  • Wenn der Benutzer des Computers die Leertaste drückt oder beim Joystick 1 die Feuertaste, geht das Timing kaputt, denn diese sind ebenfalls mit Pin B4 des CIA1 verbunden und senden das Lichtgriffel-Signal. Dadurch wird die X-Koordinate des Rasterstrahls zu einem völlig anderen Zeitpunkt gespeichert. Im Programm oben hat das fatale Folgen: Der BPL-Befehl springt an eine unvorhersagbare Stelle im Speicher und führt den Code dort aus. Das führt meist zum Absturz des Computers. Um einen solchen Absturz zu vermeiden, fügt man in der Regel noch ein and #$07 ein, welches dafür sorgt, dass das Sprungziel des BPL-Befehls immer an einer sinnvollen Adresse liegt. Dann wird zwar immer noch das Timing ungenau, aber der Computer stürzt wenigstens nicht mehr ab.
Lichtgriffel-Methode
Vorteile:
  • Synchronisation ist exakt.
  • Unabhängig vom verwendeten Video-Chip.
  • Synchronisation nach 35 Taktzyklen erreicht.
Nachteile:
  • Nicht ganz so einfach zu verstehen.
  • Kann nur einmalig pro Bildschirmaufbau benutzt werden.
  • Exaktheit geht verloren, wenn der Benutzer die Leertaste oder den Feuerknopf an Joystick 1 drückt.
  • Die Abfrage von Tastatureingaben und von Joystick 1 sind erschwert.
Anpassung für NTSC-VIC: Nicht notwendig. Der Code funktioniert mit allen VIC-Versionen.

JSR-Befehl (exakt)[Bearbeiten | Quelltext bearbeiten]

Synchronisation mit dem JSR-Befehl: Exakte Synchronisation in nur 14 Taktzyklen, allerdings mit längerer Vorarbeit.

Der JSR-Befehl ist der einzige Sprungbefehl, der Schreibzyklen enthält. Überraschenderweise reicht dies aus, um eine exakte Synchronisation zu erhalten.

*=$c000
         sei

         ldx #$03      ; Bug in Kernal-Interrupt-Routine ausgleichen
         lda #$00
-        sta $0200,x
         dex
         bpl -

         lda #<irq     ; Interrupt auf eigene Routine verbiegen
         sta $0314
         lda #>irq
         sta $0315

         lda #111      ; Rasterzeile 111 programmieren
         sta $d012
         lda $d011
         and #$7f
         sta $d011

         lda $d01a     ; Rasterzeilen-Interrupt anschalten
         ora #$01
         sta $d01a

         lda #$7f      ; Timer-Interrupt abschalten
         sta $dc0d

         lda $d019     ; Rasterzeilen-Interrupt bestätigen
         sta $d019     ; um einem fehlerhaften ersten Interrupt
                       ; vorzubeugen

         cli

-        jsr -         ; selbstsynchronisierende JSR-Schleife

irq      inc $d021     ; Hintergrund gelb
         dec $d021     ; Hintergrund wieder blau

         lda $d019     ; Interrupt bestätigen
         sta $d019

         pla           ; Interrupt geordnet verlassen.
         tay
         pla
         tax
         pla
         rti

Der JSR-Befehl speichert bei jedem Aufruf die Rücksprungadresse auf den Prozessorstapel. Was passiert, wenn der Stapel voll ist? Der Prozessor schreibt dann einfach am anderen Ende des Stapels weiter. Er betrachtet den Stapel also eher als eine Art Ringpuffer. Dadurch ist es überhaupt möglich, den JSR-Befehl in einer Schleife auszuführen; sonst würde der Stapel schnell überlaufen und der Computer abstürzen.

Da die Interrupt-Routine des Kernals davon ausgeht, dass ein Stapel nie überlaufen kann, bügelt das Programm diesen Fehler zuerst aus: Es schreibt in die Speicherzellen $0200 bis $0203 Nullen. In diesen Speicherzellen steht normalerweise die letzte Befehlszeile aus dem Direktmodus, in unserem Falle der SYS-Befehl. Durch den Fehler kann es passieren, dass die Routine das Token des SYS-Befehls ausliest und für Prozessor-Flags hält. Dort ist das BRK-Flag gesetzt und dementsprechend wird nicht die Interrupt-Routine aus dem Programm, sondern die BRK-Routine angesprungen und ein Reset durchgeführt.

Danach wird der Interrupt auf die eigene Routine umgebogen und Rasterzeile 111 programmiert. Vor der Beschreibung des Timings in der Interrupt-Routinen, wird noch das Timing des JSR-Befehls in einer Badline benötigt:

Timing-Diagramm eine JSR-Schleife in einer Badline: Oben werden die Taktzyklen seit dem Start der Rasterzeile durchgezählt, wobei in der Mitte 39 Taktzyklen ausgelassen wurden (markiert durch Zickzacklinie). Jeder Streifen im unteren Bereich der Grafik zeigt eines der sechs möglichen Befehls-Timings an. Schreibzyklen sind durch ein 'w' markiert. In den Taktzyklen 12 bis 14 wird der Prozessor angehalten.

Da der JSR-Befehl sechs Taktzyklen lang ist, gibt es sechs mögliche Timing-Streifen. Wichtig sind vor allem die Zyklen 12 bis 14: In diesem Zeitraum kann der Prozessor noch Schreibzugriffe ausführen, aber keine Lesezugriffe. Dies führt dazu, dass sich die Streifen 2 bis 4 synchronisieren: Wenn der Prozessor in Taktzyklus 55 wieder angeschaltet wird, haben diese drei Streifen das gleiche Timing.

In der nächsten Badline gibt es demnach nur noch vier verschiedene Timing-Streifen. Da man die Anzahl der Taktzyklen bis dahin kennt, kann man ausrechnen, welche dies sind: Die Streifen 1, 2, 3 und 6. Es erfolgt in dieser Badline erneut eine Synchronisation der Streifen 2 und 3 und in der nächsten Badline sind nur noch die Streifen 1, 2, und 3 vorhanden. Nach der dritten Badline sind es noch die Streifen 2 und 3 und nach der vierten Badline ist der JSR-Befehl komplett synchronisiert.

Tritt danach der Interrupt auf, sieht das Timing-Diagramm so aus:

Timing-Diagramm des Interrupts bei der JSR-Methode: Oben werden die Taktzyklen seit dem Start der Rasterzeile durchgezählt, wobei in der Mitte 20 Taktzyklen ausgelassen wurden. In der Zeile darunter ist, zur besseren Orientierung, der Bildschirminhalt oberhalb von Rasterzeile 111 angezeigt.[1] Da zu diesem Zeitpunkt der JSR-Befehl bereits vollständig synchronisiert ist, gibt es auch nur noch einen Timing-Streifen.

In Taktzyklus 8 wird der Interrupt gestartet, danach folgen die 29 Takte der System-Interrupt-Routine und danach wird der Balken an- und ausgeschaltet.

JSR-Befehl
Vorteile:
  • Kann auch ohne jegliches Verständnis der Abläufe durch Ausprobieren benutzt werden.
  • Synchronisation ist exakt.
  • Synchronisation wird bereits nach 11 oder 14 Taktzyklen erreicht.
  • Kann mit leichten Anpassungen auch in Badlines benutzt werden.
Nachteile:
  • Synchronisation hängt von der Rasterzeile ab.
  • Liefert unterschiedliche Synchronisationszeitpunkte für unterschiedliche VIC-Chips und funktioniert mit dem 6567R56A gar nicht.
  • Funktioniert nicht mit anderen Interrupts.
  • Vor dem Interrupt muss die JSR-Schleife vier Badlines durchlaufen haben. In dieser Zeit ist keine andere Programmausführung möglich.
  • Die Nutzung des Stapels ist stark eingeschränkt.
  • Ein Bug in der Kernal-Interrupt-Routine erschwert die Nutzung.
Anpassung für NTSC-VIC: Mit einem 6567-NTSC-Chip funktioniert die Selbstsynchronisation ebenfalls, allerdings ist die Synchronisation eine andere. Mit dem älteren 6567R56A ist die Selbstsynchronisation unvollständig: Es bleiben zwei unterschiedliche Zustände übrig, die mit anderen Methoden unterschieden werden müssen.

Auswirkungen von Sprites[Bearbeiten | Quelltext bearbeiten]

Sprites sorgen in den Rasterzeilen, die sie überdecken, für kurze Abschaltzeiten des Prozessors. Möchte man exaktes Timing auch in Rasterzeilen mit Sprites hinbekommen, sollte man verstehen, welche Auswirkungen die Sprites haben.

Die nachfolgende Tabelle gibt für jedes Sprite die Taktzyklen an, in denen der Prozessor abgeschaltet ist, wenn das Sprite in der entsprechenden Zeile angeschaltet ist:

Sprite Taktzyklen
0 58/59 (vorige Zeile)
1 60/61 (vorige Zeile)
2 62/63 (vorige Zeile)
3 1/2
4 3/4
5 5/6
6 7/8
7 9/10

Hinzu kommen jedesmal noch drei Taktzyklen vor dem ersten genannten Taktzyklus, in dem der Prozessor nur Schreib-Zugriffe ausführen kann. Allerdings entfallen diese, wenn der Prozessor ohnehin zuvor abgeschaltet war. Im Extremfall, nämlich wenn alle acht Sprites angeschaltet sind und eine Badline vorliegt, bedeutet dies, dass in dieser Zeile nur ein einziger Taktzyklus vom Prozessor ausgeführt wird (es handelt sich dabei um den 11. Taktzyklus der Zeile). Auf diesen können noch bis zu drei Schreibzugriffe folgen.

Weiterhin muss man dabei beachten, dass die Y-Koordinate eines Sprites gegenüber den Rasterzeilen um eins versetzt ist: Ein Sprite mit Y-Koordinate 106 wird erst ab Rasterzeile 107 angezeigt. Dementsprechend finden die Prozessorabschaltungen auch erst in Rasterzeile 107 (beziehungsweise für die ersten drei Sprites am Ende von Rasterzeile 106) statt.

Weblinks[Bearbeiten | Quelltext bearbeiten]

Quellen und Referenzen[Bearbeiten | Quelltext bearbeiten]

  1. 1,0 1,1 1,2 1,3 1,4 1,5 Genau genommen findet zwischen den Taktzyklen und der tatsächlichen Ausgabe am Bildschirm eine Verzögerung von einigen Pixeln statt. Für die Programmierung ist es aber einfacher, sich vorzustellen, dass dies synchron abläuft.
  2. Genau genommen kommt noch ein neunter Zeitpunkt hinzu, wenn illegale Opcodes im Hauptprogramm vorkommen.
  3. Genau genommen wird die X-Koordinate des Rasterstrahls im vierten Taktzyklus des STA-Befehls gespeichert, also vier Taktzyklen eher, als sie durch den LDA-Befehl ausgelesen wird. Im Text wird aber so getan, als geschähe das Speichern zeitgleich mit dem Auslesen.