Raster-Timing
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]
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:
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: |
|
Nachteile: |
|
Anpassung für NTSC-VIC: | Nicht notwendig. Der Code funktioniert mit allen VIC-Versionen. |
Einfacher Rasterzeilen-Interrupt (grob)[Bearbeiten | Quelltext bearbeiten]
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:
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: |
|
Nachteile: |
|
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]
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:
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: |
|
Nachteile: |
|
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]
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:
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: |
|
Nachteile: |
|
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]
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:
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: |
|
Nachteile: |
|
Anpassung für NTSC-VIC: | Nicht notwendig. Der Code funktioniert mit allen VIC-Versionen. |
Lichtgriffel-Methode (exakt)[Bearbeiten | Quelltext bearbeiten]
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:
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 einand #$07
ein, welches dafür sorgt, dass das Sprungziel desBPL
-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: |
|
Nachteile: |
|
Anpassung für NTSC-VIC: | Nicht notwendig. Der Code funktioniert mit allen VIC-Versionen. |
JSR-Befehl (exakt)[Bearbeiten | Quelltext bearbeiten]
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:
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:
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: |
|
Nachteile: |
|
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]
- Raster-IRQ: Timing Probleme bei Retro-Programming (Webarchiv)
- Raster-IRQ: Endlich stabil!!! bei Retro-Programming (Webarchiv)
- Interrupts and timing bei codebase64.org
Quellen und Referenzen[Bearbeiten | Quelltext bearbeiten]
- ↑ 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.
- ↑ Genau genommen kommt noch ein neunter Zeitpunkt hinzu, wenn illegale Opcodes im Hauptprogramm vorkommen.
- ↑ Genau genommen wird die X-Koordinate des Rasterstrahls im vierten Taktzyklus des
STA
-Befehls gespeichert, also vier Taktzyklen eher, als sie durch denLDA
-Befehl ausgelesen wird. Im Text wird aber so getan, als geschähe das Speichern zeitgleich mit dem Auslesen.