Erstellt: 12. Juni 2013 (zuletzt geändert: 1. Januar 2018)

Interrupts

STOP! Unterbrechung!

C64 Studio, AMCE & TASM

Damit wir bei unserem Spiel L.O.V.E., mit dem Schritt Landung 002 weitermachen können, gibt es hier eine erste kleine Einführung zum Thema Interrupts. Die Unterbrechungen machen das Programmiererleben erst so richtig schön 😉 und nur durch sie könnt ihr dem C64 all die schönen Effekte entlocken, die ihr aus den Spielen und besonders aus Demos kennt.

Dieser Beitrag behandelt allgemeine Grundlagen und wir programmieren unseren ersten einfachen IRQ. Im nächsten Beitrag schauen wir uns dann die NMIs an und vergleichen diese nochmal mit den IRQs. Anschließend kommen wir dann endlich zum Rasterzeileninterrupt (wohl der wichtigste IRQ für Spiele und Demos).

Was sind Interrupts?

Computer (zumindest Systeme, die nur eine CPU mit einem Kern besitzen, wie der C64) können nur eine Aufgabe zur Zeit bewältigen. Wir haben aber den Eindruck, sie würden mehrere Sachen gleichzeitig erledigen. Da diese Aufgaben sehr schnell ausgeführt werden, sieht es für uns so aus, als geschehen sie gleichzeitig. So blinkt z. B. der Cursor unablässig, die Tastatur wird abgefragt und die interne Uhr des C64 läuft auch mehr oder weniger korrekt 😉 weiter.

Genau hier kommen jetzt die Interrupts zu ihrem Einsatz. Treten bestimmte Ereignisse ein, die man teilweise auch selbst definieren kann, dann unterbricht der C64 das aktuelle Programm, springt zur Adresse des Interrupts, arbeitet den Interrupt ab und kehrt schließlich zum ursprünglichen Programm zurück. So gibt es z. B. einen Interrupt, der dafür sorgt, dass der Cursor blinkt, die Tastatur abgefragt und die Uhr weitergestellt wird.

Dabei müssen wir zwei Arten von Interrupts (Unterbrechungen) unterscheiden:

  • IRQ (InterruptReQuest): Diese stellen eine Anfrage für einen Interrupt dar. Die Anfrage lässt sich von uns mit SEI und CLI abgelehnen oder erlauben. Warum man Interrupts verhindern möchte, kann verschiedene Gründe haben. Habt ihr z. B. einen sehr „friggeligen“ Rasterzeileninterrupt programmiert, würde ein anderer Interrupt evtl. euer Timing zerstören. Einen weiteren Grund sehen wir gleich im Beispielprogramm.
  • NMI (Non Maskable Interrupt): Diese Interrupts werden immer ausgeführt und lassen sich nicht ablehnen (unterdrücken). Diese Interrupts verwendet man z. B. häufig für die Datenübertragung. Damit dort alles reibungslos läuft, wird über einen NMI alles im Takt gehalten. NMIs werden im nächsten Beitrag etwas ausführlicher behandelt.

Führen wir uns Interrupts mal an Hand eines Beispiels aus dem Alltag vor Augen. Mir hat die Erklärung aus dem 64‘er Assemblerkurs bzw. aus Assembler ist keine Alchemie sehr gefallen, also versuche ich es mit einer etwas modernisierten Version:

  • Stellt euch vor, ihr schaut gerade ein Video auf YouTube (dies steht für das Hauptprogramm), dann klingelt euer Handy.

    • Ihr unterbrecht das Video und geht ans Handy (ein „normaler“ Interrupt), ihr wollt gerade von dem Video berichten, da tönt euer Rauchmelder im Flur.

      • Den könnt ihr natürlich nicht ignorieren und unterbrecht (dies stellt einen NMI da) jetzt auch euer Handygespräch, um nach dem Rechten zu sehen.
    • Da es sich zum Glück um einen Fehlalarm gehandelt hat, setzt ihr das Handygespräch, nach deaktivieren des Alarms, fort und berichtet endlich von dem YouTube-Video. Sobald das Gespräch (der „normale“ Interrupt) beendet ist, wendet ihr euch wieder dem Video zu.
  • Ihr wollt euch nun endlich das Video in Ruhe zu Ende anschauen (hier sind wir wieder im Hauptprogramm) und nehmt euch vor, euch von nichts mehr unterbrechen zulassen.
  • Kurz darauf bekommt ihr über Skype eine Einladung zum Retro-Chat (eine „normale“ Interruptanfrage). Da ihr aber das YouTube-Video anschauen wollt, ignoriert ihr die Chat-Einladung einfach (s. SEI / CLI bei den Mnemonics).
  • Würde der Rauchmelder jetzt nochmal anspringen, würdet ihr das Video natürlich erneut unterbrechen und wieder nachschauen, was da los ist, den Rauchmelderalarm kann man schließlich nicht ignorieren (dies wäre wieder ein NMI).
Interrupts: Übersicht Programm, IRQ, NMI
Übersicht Programm, IRQ, NMI
Wer sorgt nun für die Interrupts?

Wenn wir die Interruptquellen des C64 mal grob einteilen, so kommen wir auf vier:

  1. RESET: Der RESET stellt einen besonderen Interrupt dar. Dieser wird nicht vom C64 bzw. einem seiner Bausteine ausgelöst. Der RESET ist ein Interrupt, den wir als User von außen auslösen. Der 6510 führt einen RESET aus, wenn Pin-40 auf LOW gezogen wird.
  2. NMI: Ein NMI (None Maskable Interrupt) kann einzig vom CIA-2 ausgelöst werden, er wird daher auch manchmal NMI-CIA genannt. Dieser ist dazu direkt mit mit Pin-4 des 6510 verbunden. Ein NMI läßt sich, wie oben bereits erwähnt, nicht mit SEI / CLI unterdrücken! Ein weiterer Auslöser für einen NMI ist die RESTORE-Taste, auch diese ist direkt mit dem NMI-Eingang der CPU verbunden.
  3. IRQ: Eine „standard“ Interruptanfrage (InterruptReQuest) kann von verschiedenen Bausteinen ausgelöst werden, z. B. dem CIA-1 (über den Timer) oder dem VIC-II (für einen Rasterzeileninterrupt). Dazu sind die Bausteinen mit dem IRQ-Eingang des 6510 an Pin-2 verbunden. IRQs lassen sich mit SEI / CLI unterdrücken.
  4. BRK: Auch der BRK-Befehl stellt einen besonderen Interrupt dar. Hierbei handelt es sich um einen sog. Software-Interrupt. Trifft die CPU auf ein BRK unterbricht sie sich quasi selbst. Der Einsatzbereich ist relativ klein, am Häufigsten wird ein Break bei Assemblern (z. B. SMON) auf dem C64 verwendet. Treffen diese auf ein BRK, so unterbrechen sie das aktuelle Programm und zeigen z. B. die Prozessorregister oder den Quellcode an.
Was passiert bei einem IRQ eigentlich?
  1. Sobald ein Interrupt auftritt, unterbricht die CPU das aktuelle Programm, nachdem der aktive Befehl verarbeitet wurde.
    Handelt es sich bei der Quelle um einen RESET, dann geht es direkt bei Punkt 6 weiter.
  2. Die Adresse des nächsten Befehls wird auf dem Stack abgelegt, erst das MSB, dann LSB. Denkt dran, dass der Stack, von oben nach unten gefüllt wird und somit die Adresse wieder im gewohnten LSB/MSB-Format im Speicher liegt.
  3. Handelt es sich beim Auslöser um den BRK-Befehl, dann wird das B-Flag gesetzt.
  4. Danach landet das Statusregister (inkl. ggf. gesetzem B-Flag) ebenfalls auf dem Stack.
  5. Jetzt werden noch weitere Interrupts durch Setzen des I-Flags verhindert.
  6. Die CPU springt zu der laut Hardware-Vektoren zugehörigen Speicherstelle. Abhängig von der jeweiligen Interruptquelle gibt es unterschiedliche Einsprungadressen.
    Bei einem RESET ist hier Schluß, schließlich startet der Rechner alles neu.
  7. Trifft der Prozessor auf ein RTI (ReTurn from Interrupt), dann wird zunächst das Statusregister vom Stack geholt. Da das I-Flag erst nach der Ablage auf dem Stack gesetzt wurde, wird es so auch automatisch wieder gelöscht. Anschließend wird die Adresse des nächsten Befehls (s. Punkt 2) vom Stack geholt und das Programm dort fortgesetzt.
Die Vectoren

Die drei Hardware Interrupt-Vektoren des 6510, findet ihr ganz am Ende des Speichers, in den Adressen $fffa bis $ffff.

                                 Zieladresse
NMI-Vektor....: $fffa / $fffb -> $fe43
RESET-Vektor..: $fffc / $fffd -> $fce2
IRQ/BRK-Vektor: $fffe / $ffff -> $ff48       (B-Flag beachten)

Da wir vier Interrupt-Quellen (IRQ, NMI, BRK und RESET), aber nur drei Vektoren haben, teilen sich IRQ und BRK einen Vektor. Man kann (muss) diese durch Prüfen des B-Flags unterscheiden.

Für uns ist der IRQ-Vektor am wichtigsten, schauen wir uns doch einfach mal an, was dort genau geschieht.

Zunächst kontrollieren wir unter $fffe / $ffff, wo das System hinspringt. Dazu starten wir einfach WinVICE und rufen dann mit ALT-M den Monitor (falls ihr mit diesem wenig Erfahrung habt, unter Ein kleiner ‚Crack‘ wurde etwas mehr damit gemacht) auf. Im Monitor schauen wir uns im Memory-Fenster die letzten beiden Bytes des Speichers an:

IRQ_002

Wie wir sehen, zeigt der IRQ-Vektor auf die Adresse $ff48, wie sie auch oben in der Übersicht steht. Das heißt also, wenn ein IRQ auftritt, springt der C64 zu dieser Adresse. Werfen wir nun einen Blick auf die Routine, die wir unter $ff48 finden. Dazu verwenden wir das Disassambly-Fenster des VICE-Monitors:

IRQ_003

$ff48: PHA           ;Akku auf den Stack
$ff49: TXA           ;X-Register
$ff4a: PHA           ;auf den Stack
$ff4b: TYA           ;Y-Register
$ff4c: PHA           ;auf den Stack
$ff4d: TSX           ;Stackpointer ins X-Register
$ff4e: LDA $0104,X   ;Statusregister vom Stack in den Akku
$ff51: AND #$10      ;B-Flag gesetzt (BRK?)
$ff53: BEQ $ff58     ;nein -> IRQ
$ff55: JMP ($0316)   ;indirekter Sprung über den BRK-Vektor
$ff58: JMP ($0314)   ;         -''-     über den IRQ-Vektor

Zunächst werden Akku, X- und Y-Register auf dem Stack abgelegt. Als nächstes muss geprüft werden, ob die Quelle ein IRQ oder ein BRK war. Dazu greift die Routine direkt auf den Stack zu! Wir benutzen bisher nur die Push- und Pull-Befehle des 6510, um auf den Stack zuzugreifen. Aber da es sich im Prinzip um einen ganz normalen Speicherbereich handelt (Page 1 von $0100 bis $01ff), können wir auch direkt darauf zugreifen.

Gehen wir mal davon aus, dass der Stackpointer vor der Unterbrechung auf $f6 gezeigt hat. Wenn nun ein Interrupt ausgelöst wird, dann landen zunächst ja das MSB und dann das LSB auf dem Stack. Wie ihr wisst, wird der Stack von oben $01ff nach unten $0100 gefüllt, daher müsst ihr die folgende Liste von unten nach oben lesen.

...      SP zeigt jetzt auf $F0
$01f1 -> Y-Register
$01f2 -> X-Register
$01f3 -> Akku
$01f4 -> Statusregister (inkl. evtl. gesetztem B-Flag)
$01f5 -> LSB der nächsten Anweisung
$01f6 -> MSB der nächsten Anweisung
...   -> Interrupt! SP = $F6
$01ff    hier beginnt der Stack (wird von 'oben' nach 'unten' gefüllt)

Wollen wir nun das Statusregister vom Stack in den Akku holen, dann müssen wir zur Adresse $0104 den aktuellen Stackpointer addieren. Nachdem Akku, X- und Y-Register auf dem Stack abgelegt wurden, zeigt der SP auf einen um vier höheren Wert, daher wird nicht $0100 als Basis genommen. Hier zeigt der SP also auf $f0, addieren wir nun $0104, dann landen wir bei $01f4 und dort finden wir das gespeicherte Statusregister. Dann wird per AND geprüft, ob das B-Flag (4. Bit) gesetzt ist. Falls nicht wird zum indirekten Sprung über den IRQ-Vektor $0314 verzweigt, sonst geht es weiter mit dem indirekten Sprung über den BRK-Vektor $0316.

Wie jetzt, schon wieder Vektoren?
Ja, wir müssen die ROM (oder Hardware) Vektoren $fffa$ffff und die RAM-Vektoren unterscheiden. Unter Ein Modul erstellen haben wir schon zwei dieser Vektoren benutzt.
Für die Interrupts sind folgende Vektoren im RAM wichtig. Finden können wir diese RAM-Vektoren auf Page 3.

                           Zieladresse
$0314/$0315: IRQ-Vektor -> $ea31
$0316/$0317: BRK-Vektor -> $fe66
$0318/$0319: NMI-Vektor -> $fe47

Also springen die letzten beiden JMP-Befehle aus der IRQ-Routine, je nach Quelle, zur Adresse, die an $0314 (IRQ) oder $0316 (BRK) zufinden ist. Die Zieladressen, wie sie nach einem RESET zufinden sind, stehen ganz rechts.

Da diese Vektoren im RAM liegen erlauben sie es uns nun, die Interrupts anzuzapfen und für unsere Zwecke zu verwenden. Das RAM können wir schließlich überschreiben.
Der Vollständigkeithalber sei erwähnt, dass es auch Möglich ist direkt die Vektoren $fffa bis $ffff zu verändern. Dazu müssen wir nur das ROM aus- und das RAM einblenden. Da uns so aber auch die Kernalfunktionen fehlen würden, verzichten wir erstmal darauf.

Einen IRQ ‚anzapfen‘

Wie eingangs bereits erwähnt, liefert der CIA-1 einen IRQ beim Unterlauf von Timer A (also wenn dieser heruntergezählt wurde). Dieser IRQ wird 60 mal in der Sekunde ausgelöst und vom System z. B. zum Weiterstellen der Uhr, abfragen der Tastatur und blinken des Cursors verwendet.
Wir haben nun die Möglichkeit, uns in diesen Interrupt einzuklinken, um unseren eigenen Code auszuführen. So könnten wir z. B. Musik abspielen lassen, während wir nebenbei BASIC programmieren. Das klappt nur, da (wie eben gesehen) beim Interrupt nicht direkt ins ROM gesprungen wird, sondern über die RAM-Vektoren verzeigt wird.

Wir werden jetzt einfach mal den Rahmen blinken lassen, während wir nebenbei ganz normal mit dem BASIC weiterarbeiten.

*=$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 #<irq                          ;LSB unserer IRQ-Routine in den Akku
 sta $0314                          ;und in $0314 speichern
 lda #>irq                          ;MSB
 sta $0315                          ;in $0315 ablegen
 cli                                ;Interrupts wieder freigeben
 rts                                ;zurück zum BASIC

*=$1000
irq                                 ;Beginn unserer Interrupt-Funktion
 inc $d020                          ;Rahmenfarbe ändern
 jmp $ea31                          ;weiter zur System-Routine springen

Zu Beginn legen unsere BASIC-Startzeile fest. Diesmal ist es aber nicht die gewohnte. Damit wir etwas Platz für ein BASIC-Programm haben, wird unser IRQ erst an der Adresse $1000 beginnen. Außerdem sorgen wir mit einem abschließenden NEW dafür, dass wir ein sauberes BASIC haben und direkt mit dem Programmieren beginnen können. Beachtet, dass wir uns bei $1000 immer noch im BASIC-Speicher befinden, falls eurer BASIC-Listing zu lang wird, überschreiben wir unsere IRQ-Routine. Ihr könnt diese auch durchaus weiter nach hinten verlegen. Legt ihr den IRQ nach $c000, besteht keine Gefahr mehr, dass er vom BASIC überschrieben wird. Allerdings kommt der Turbo Assembler damit nicht direkt zurecht, daher habe ich mich hier für $1000 entschieden.

Wenn wir jetzt die Adresse unserer IRQ-Routine an $0314/15 ablegen wollen, dann sollten wir zunächst dafür sorgen, dass kein Interrupt auftritt. Stellt euch nur mal vor, wir haben gerade unser LSB in $0314 abgelegt und dann tritt der IRQ auf. Da nun eine ungewollte Adresse im IRQ-Vektor steht, wird sich der C64 sehr wahrscheinlich „aufhängen“. Also sperren wir mit sei die Interrupts, dann speichern wir die Adresse unserer IRQ-Funktion an $0314/15 und erlauben, vor der Rückkehr zum BASIC, die Interrupts wieder.

Unsere IRQ-Routine irq erhöht einfach mal wieder die Rahmenfarbe. Damit der C64 nichts von unserem Zwischenfunken merkt und normal weiterarbeitet, springen wir anschließend einfach zur ursprünglichen Kernal-Funktion nach $ea31.

Startet das Programm und ihr könnt nun z. B. ein BASIC-Programm, begleitet vom blinkenden Rahmen, schreiben. Unsere IRQ-Routine wird nun 60 mal in der Sekunde aufgerufen.

Interrupts: Der Rahmen blinkt unablässig, während wir im BASIC arbeiten.
Der Rahmen blinkt unablässig, während wir im BASIC arbeiten.

Probleme könnt ihr natürlich bekommen, wenn euer IRQ-Code länger läuft, als ihr eigentlich Zeit habt. Also wenn ihr z. B. obigen IRQ „anzapft“ und eure IRQ-Funktion binnen einer sechzigstel Sekunde nicht beendet ist, dann tritt schon der nächste IRQ auf, bevor ihr mit eurem fertig seid.


Das war es erstmal mit der Einführung in die Interrupt-Programmierung.

Als nächstes folgt dann der NMI. Später werden uns die Interrupts noch weiter beschäftigen, besonders die vom VIC-II und da natürlich der Rasterzeileninterrupt.

Auf BRK und RESET möchte ich nicht gesondert eingehen, da deren Einsatz doch sehr speziell ist. Bei Ein Modul erstellen wird der RESET (allerdings nicht direkt der Interrupt) verwendet. Dort seht ihr auch, wie ihr euer Programm „RESET fest“ machen könnt.


Schrott!!Naja...Geht so...Ganz gut...SUPER! (8 Bewertungen | Ø 5,00 von 5 | 100,00%)

Loading...


ZurückWeiter

6 Gedanken zu „Interrupts“

  1. Hallo Jörn,

    ich hab mich jetzt auf deinem Blog gut umgesehen und auch ein wenig mit den Interrupts herum gespielt, jetzt hab ich nur ein Problem…

    Ich hab da ein kleines Intro für meine geplanten und hoffentlich irgendwann fertigen Spiele geschrieben. Nur wenn ich meine Interrupts bei *=1000 bis *=3FFF starten lasse, bekomme ich eine schwarze Linie am unteren Bildschirmrand (also da wo man Zeichen hinschreiben kann nicht im Rahmen). Erst ab *=4000 tritt dieser Effekt nicht mehr auf. Im Bereich von *=3000 bis *=3FFF ist die Linie dann auch nicht durchgängig sondern kann auch gestrichelt sein… Ich verstehe das irgendwie nicht, weil dort ist doch eigentlich der BASIC-Ram, das dürfte doch keine Auswirkungen auf die Zeichenausgabe haben, oder?

    Ich hab es getestet, das tritt bei mir im Emulator und auch auf originaler Hardware auf… Kannst du mir bei so einer wagen Beschreibung helfen, oder kann ich dir zufällig ein Bild von meinem Problem schicken? Oder den Quelltext…

    Also nur wenn du Zeit und Lust hast.

      1. Ja, genau die schwarze Linie meinte ich! :0D
        Wenn ich meinen Screen-Shake-Algorithmus auskommentiere ist der weg. \o/

        Das hat mir super geholfen! :0D
        Aber warum passiert das nur, wenn ich den Speicherbereich weiter nach vorne verschiebe?? Das verstehe ich nicht ganz…
        Meine Routine sah wie folgt aus:

        !zone shake_screen
        shake_screen

        lda screen_shake
        beq .clear_shake

        jsr rndTimer
        and #%00000111
        eor #%00011000
        sta $d011
        dec screen_shake
        jmp .exit

        .clear_shake
        lda #%00011000
        sta $d011
        .exit
        rts

        Wenn ich jetzt die Zeile hinter .clear_shake auf folgendes ändere, funktioniert es auch, wenn ich *=1000 als Speicherbereich angebe:
        lda #%00011011

        Danke für deinen Hinweis mit dem Scrolling! Der hat mir wirklich weiter geholfen! :0D

        Einen schönen Gruß zurück,
        Robert

        1. Hallo Robert,
          statt es direkt zu erklären, gebe ich dir einfach mal die folgenden Hinweise, vielleicht bist du ja motiviert, die Lösung zu suchen…

          1. Das eigentliche Problem besteht auch, wenn du das Programm ab $4000 ablegst. Nur fällt es dir nicht direkt auf. Den Grund für deine unterschiedlichen Anzeigen (schwarz, gestrichelt, vermeintlich ok) findest du hier. Der Schlüssel ist eine von dir bereits erwähnte Speicherstelle.
          2. In deiner obigen Version ist ein grundsätzlicher Fehler, aber du hast bereits die richtige Lösung. Schau dir direkt nach einem Reset mal den Inhalt von $D011 an.

          Gruß,
          Jörn

          1. Hahahaha! Danke schön Jörn!

            Jetzt ergibt alles einen Sinn!

            Die gestrichelte Linie kam, weil ich die Speicherstelle $3FFF mit einem Wert belegt habe. Diese Stelle ist (Bei Standardeinstellungen des Bildschirmspeichers) zuständig, für die Darstellung hinter dem Rahmen. Und dadurch, dass ich davon ausgegangen bin, dass %00011000 die Ausgangseinstellung für das Scrolling ist, habe ich den Bildschirm um 4 Pixel nach oben gescrollt und das Bild hinter dem Rahmen tauchte ein wenig auf!

            Das heißt im Umkehrschluss, dass ich, wenn ich meinen Screenshake auf einem anderen als einem schwarzen Hintergrund machen möchte, darauf achten muss, dass ich bei $3FFF $00 zu stehen habe!

            Danke für deine Hilfe!!! Du bist echt super Jörn! :0D
            Vielen vielen Dank!

            Und wieder einen schönen Gruß zurück,
            Robert

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Protected by WP Anti Spam