Unterbrechen und weitermachen, Interrupts ("Programmieren" lernen)

  • Was macht eine CPU (bzw. ein Rechner) so, wenn sie nichts macht ... sie tut trotzdem etwas. Und das liegt vor allem daran, daß so ein Mikrocomputer (und andere auch) ein paar Dinge tun, die sie sozusagen immer machen. Ein Rechner ohne Grafikkarte bzw. aktive Textmodekarte stellt meistens auch im Leerlauf etwas auf dem Bildschirm dar, was er selber erzeugt - und sei es ein blinkender Cursor. Desweiteren steht der Rechner ständig parat Eingaben über die Tastatur anzunehmen. Es gibt evtl. eine Uhr, die weitergeschaltet werden muß und manchmal will auch eine angeschlossene Hardware Aufmerksamkeit einfordern.


    Damit das funktioniert gibt es ungefähr zwei Varianten

    • entweder es läuft permanent ein "Hauptprogramm", was sich genau um solche Sachen kümmert (etwa in der Art eines Softwareterminals)
    • oder der Rechner macht tatsächlich nichts, wird dabei aber ständig gestört, weil eine Unterbrechung ihn zwingt, den meditativen Zustand zu verlassen und nach der Tastatur zu schauen, oder den Cursor umzufärben.


    Die zweite ist die übliche Version für Mikrocomputer.



    Das geht aber auch nur, wenn die CPU einen Anschluß hat, der, wenn er aktiviert wird, mitteilt, daß nun alles andere zu unterbinden ist - auch die totale Ruhe - und an einer bestimmten Stelle bitteschön erstmal ein bestimmtes Programm abzuarbeiten ist, die Interruptroutine. Wenn das fertig ist, kann dann auch - sollte zufällig doch gerade normale Rechnerei stattfinden, z.B. für ein Apfelmännchen - mit dem normalen Programm fortgefahren werden.


    Je nach CPU gibt es evtl. nur einen, oder auch mehrere solche Anschlüsse, oder auch nur einen und dazu aber eine Art Adressleitung, die mitteilt, welches Gerät bzw. wer gerade um eine Unterbrechung bittet.

    Eines haben diese Sachen aber alle gemein: Sie reagieren schnell auf mögliche Ereignisse und auch die Software, die diese Unterbrechungen dann auswertet und darauf reagiert, ist oft so gebaut, daß sie schnellstmöglich reagieren kann.


    Solch eine Unterbrechung des normalen Ablaufs nennt man einen Interrupt.



    Da auch Interrupts wieder unterschiedlich "wichtig" sein können, gibt es solche, die man kurzzeitig deaktivieren kann. Daneben existieren auch solche, die man nicht unterdrücken, kann, die also immer stattfinden können.

    Da das Abschalten durch "Maskieren" eines Bits geschieht, spricht man, wenn eben dies nicht möglich ist, von einem Non Maskable Interrrupt, einem NMI.

    Alle anderen Interrupts, die i.a. per Hardware eingeleitet werden, laufen unter "Interrupt", mit dem Kürzel IRQ.

    Und dann kann man auch Interrupts per Software anfordern - das sind dann sogenannte Softwareinterrupts, SWI.


    (Nebenbei: Da am C64 viele "Spielernaturen" und echte "Code-Zauberer" unterwegs waren, wurde dort eine Möglichkeit gefunden, den NMI zu unterdrücken - also etwas, was die Hardware eigentlich überhaupt nicht (nie) erlauben würde.)



    Interessant wird solch ein System dann, wenn man eine schnellstmögliche Reaktion auf ein Ereignis benötigt, z.B. weil am seriellen Port Daten angekommen sind, die sofort ausgewertet werden müssen.

    (noch schneller wäre nur möglich, wenn man alle IRQs abschaltet und quasi den Port permanent abfragt, ob gerade was ankommt)

    Mit IRQs kann man mithin so eine Art Hardware-Multitasking hinbekommen, was aktiv wird, wenn ein "Teilnehmer" es aufruft.


    Daneben lassen sich so auch ganze Softwarebibliotheken aufrufen, wenn der Prozessor das unterstützt und verschiedene Softwareinterrupts erlaubt, die dann an unterschiedliche Stellen im Speicher verzweigen. Die ARMs etwa haben so ein System.


    Und es ermöglicht, wenn das Betriebssystem dies mit vorsieht, eine Ergänzung der Interruptroutinen vorzunehmen und damit zusätzliche Effekte einzubauen, die dann regelmäßig abgearbeitet werden, sobald der zugehörige Interrupt auftritt.

    -- 1982 gab es keinen Raspberry Pi , aber Pi und Raspberries

    Einmal editiert, zuletzt von ThoralfAsmussen ()

  • =6502=


    Auch der 6502 kann, natürlich, Interrupts, IRQs. Und auch NMI. Und auch SWI.


    Zusätzlich erlaubt er einen sogenannten RESET Interrupt, was ein kontrolliertes "weiches" Zurücksetzen der CPU und deren Neustart in definiertem Zustand ermöglicht und auch beim Einschalten benutzt wird.


    Beim 6502 werden alle Interruptformen über die 6 obersten Bytes im adressierbaren Speicher "abgewickelt".

    Also über $FFFA/$FFFB oder $FFFC/$FFFD oder $FFFE/$FFFF.

    Diese Speicherstellenpärchen enthalten jeweils einen Adresswert in Low-Byte/High-Byte Darstellung, der aussagt, wo genau die jeweilige Abarbeitung stattfindet. Dabei wird bei Auftreten eines Interrupts, der aktuelle Adresswert, also die Adresse des gerade aktuellen Befehls zunächst gesichert, auf dem Stack, und dann mit dem Wert aus dem zum Interrupt gehörigen Adresspärchen ersetzt. (Es werden dazu einfach die beiden Adressbytes direkt in den Programmcounter kopiert.)


    Dabei benutzt


    der NMI das Adresspärchen $FFFA/$FFFB

    der Reset die Werte aus $FFFC/$FFFD

    der IRQ und der SWI die von $FFFE/$FFFF


    Da es sich hier um eine Eigenschaft des 6502 handelt, nämlich eben genau diese obersten 6 Bytes so zu benutzen, weiß man jetzt auch, warum eigentlich alle Mikros mit diesem Chip ihre ROMs im oberen Speicherbereich "liegen" haben. Es ist schlicht nötig, damit das System überhaupt schonmal starten kann, denn der Reset wird auch schon beim Anschalten der Maschine aufgerufen und somit als allererstes die Byte aus $FFFC/$FFFD in den Programmzähler als Adresse übernommen und dann dort begonnen.


    Das erklärt z.B. , warum, obwohl beim C16 im RAM bei $3FFF "Schluß" ist (16kB), das ROM erst bei $8000 beginnt. Das ROM wird in seiner Position vom oberen Ende her bestimmt, weil der 6502 das so erfordert.



    Die ersten beiden - NMI und Reset - sind (zumindest auf C16, Plus/4) nicht wirklich nutzbar, da sie keine rechte Möglichkeit bieten, eigene Routinen einzubinden.


    Anders ist das bei IRQ und dem Softwareinterrupt, weshalb es im Weiteren um diese beiden gehen soll.



    Dabei haben wir hier bisher einen davon auch schon regelmäßig benutzt. Nämlich den Softwareinterrupt. Das ist beim 6502 ganz schlicht der Befehl BRK.


    Dieser Befehl macht eigentlich nicht viel mehr, als über $FFFE/$FFFF in die normale Interruptroutine zu springen. Bevor er das tut, speichert er den aktuellen Ort im Speicher, d.h. die Adresse, wo der BRK Befehl steht, zuzüglich +2 ! Diese um zwei erhöhte Adresse, wird einfach auf dem Stack abgelegt. Dazu kommt auch noch das Statusregister, also das, wo alle Flags drinstehen, mit auf den Stack - wobei dort noch zusätzlich das Bit %00010000 als die sogenannte "B-Flagge" gesetzt wird. Diese sagt somit, daß ein BRK stattgefunden hat. Sie wird später benutzt, um zu schauen, ob es ein BRK oder ein anderer IRQ war.


    Man kann sich nun anschauen, was genau passiert.




    Dazu sieht man in Adresse $FFFE/$FFFF nach, welche Werte darinstehen: hier ist es die $FCB3. Dort findet sich ein bereits bekanntes Konstrukt - es werden Akku, X-Register, Y-Register nacheinander auf den Stack gerettet. (Von dort werden sie am Ende der Interrupthauptroutine wieder hergestellt werden. Der Code dafür steht gleich im Anschluß bei $FCBE.)

    Mit einem JMP Befehl geht es bei $CE00 weiter (das STA ist für Module wichtig), wo sich eine sehr seltsame Befehlsfolge findet: Es wird nämlich der Stackpointer (Stapelzeiger,SP) mit TSX ins X-Register gebracht. Anschließend wird damit ab $0104 auf einen Wert gezeigt, X-indiziert, und dieser geladen.

    Nun handelt es sich dabei um das Ausnutzen einer weiteren "fixen Eigenschaft" des 6502 - bei $0100 beginnt nämlich der Stackspeicher; und ist $100 Bytes groß. Also: Eigentlich beginnt er bei $01FF, weil es nämlich ein Kellerspeicher ist, der von oben nach unten befüllt wird, wobei der Stapelzeiger immer das nächste freie Element anzeigt. Bringt man nun den Stapelzeiger ins X-Register, kann man mit $0100,X den nächsten freien Platz im Stack finden, macht also per Software das gleiche wie die CPU, wenn sie diesen sucht. Jetzt steht im Programm jedoch $0104,X !??? Das ist also weiter oben, d.h. dort, wo schon Daten abgelegt worden sind. Und wir wissen ja, was da schon so liegt - nämlich als soeben abgelegte Daten im zuletzt beschriebenen Speicherfeld das Y-Register, das ist also Adresse $0101,X (der Wert $0100,X mit X=Stapelzeiger zeigt ja auf das erste "freie" Feld!), eines darüber das X-Register bei $0102,X und danach der Akku bei $0103,X. Und dann kommt: das was davor gespeichert wurde - und das war, zumindest beim BRK Befehl, das Register mit den Flags und der gesetzten "B-Flagge", die sagt, daß es ein BRK Befehl war, wenn es vorhanden ist. Dieses Byte mit Flags wird nun also direkt aus dem Stack geladen, ohne die Stackbefehle dafür zu benutzen !

    Anschließend kommt eine Verknüpfung mit AND#$10 - es werden so alle Bits ausmaskiert, die nicht im Muster %00010000 gesetzt sind. Nur Bit-5 bleibt und die Verknüpfung (UND) ist somit genau dann "0", wenn das B-Flag nicht an war, hat aber einen anderen Wert als "0", wenn das B-Flag gesetzt war. Die Auswertung dazu macht das folgende BNE - es springt somit auf den übernächsten Befehl, wenn das B-Flag "an" war, es sich daher um einen Interrupt nach einem BRK also einen Softwareinterrupt handelt, ansonsten wird direkt der anschließende Befehl ausgeführt, was folglich dann passiert, wenn die Interruptroutine von einem Interrupt benutzt wurde, der kein BRK war (bzw. das B-Flag nicht gesetzt hat), d.h. ein IRQ.


    Beide möglichen Folgebefehle sind nun JMP Befehle, und zwar indirekte.

    Sie springen zur Adresse, deren Low-Byte/High-Byte in der in Klammern geschriebenen Adresse zu finden sind:


    JMP($0314) - wenn es ein Interrupt ohne B-Flag ist, also ein IRQ

    JMP($0316) - wenn es ein Softwareinterrupt mittels BRK Befehl ist



    Das hier ist nun genau der spannende Teil an der Geschichte. Diese Adressen liegen NICHT im ROM, sondern im RAM. In der erweiterten Zeropage. Und dort können sie natürlich auch geändert werden.

    Man kann also bei $0314/$0315 bzw. bei $0316/$0317 das Low-Byte/High-Byte ändern und auf eine beliebige andere Stelle im RAM zeigen lassen. Man verbiegt den Sprungvektor. Wenn ein Interrupt auftritt (IRQ oder BRK) wird danach automatisch die eingefügte eigene Routine angesprungen - und zwar innerhalb und zu Beginn der eigentlichen Hauptinterruptroutine.

    Man sieht hier auch schon das Problem: Die Hauptroutine ist da noch nicht abgearbeitet und da die CPU zudem gerade in einem besonderen Modus ist und nicht in ihrem normalen "User-Mode", gibt es hierbei mannigfaltige Varianten den Rechner "abzuschießen", "ins Nirvana zu treiben", "komplett stillzulegen" usf.


    Es ist daher ratsam, am Ende der eigenen Routine ein JMP anzuhängen, was wieder in die Hauptinterruptroutine zurückverzweigt - und zwar genau an die Adresse, die man bei $0314/$0315 bzw. $0316/$0317 überschrieben hatte.


    Beim C16 ist dieser Rücksprung für den IRQ die $CE0E und für das BRK die $F44C.

    Man kann sich diese mit M0314 auch direkt anschauen. Bei anderen Rechnern sind möglicherweise die Adressen der Vektoren andere, mit ziemlicher Sicherheit aber die dort liegende Rücksprungadresse. Beim C64 z.B. ist der Sprungvektor für den IRQ ebenfalls in $0314/$0315 hinterlegt, die dort stehende Adresse ist aber die $EA31.



    Generell gilt also (für IRQ):

    man ändert den Sprungvektor in $0314/$0315 - läßt ihn auf eine neue Adresse zeigen - und springt am Ende der eigenen Routine auf den eigentlichen Wert weiter - dort wird dann die primäre Interruptroutine abgearbeitet.


    Und weil das so viel zu einfach wäre, muß noch eine Kleinigkeit beachtet werden: Während man gerade dabei ist, den Sprungvektor zu verbiegen, darf natürlich nicht exakt zu diesem gleichen Zeitpunkt ein Interrupt auftreten !

    Denn: Wenn nur gerade ein Byte des Vektors geändert ist, wenn der IRQ auftritt, würde der natürlich auf diese Adresse mit einem Byte "alte Adresse" und dem anderen Byte "neue Adresse" springen - und so ein Sprung führt meist nicht auf ein sinnvolles Programm, und wenn doch, ist es auf jeden Fall nicht das, was man eigentlich eintragen wollte. Deshalb MUSS(!) während des Änderns des Sprungvektors für den IRQ der Interrupt untersagt werden - was man durch Setzen des Interrupt Flags macht:

    SEI - setzt das Interrupt Flag und weist weitere IRQs ab, es maskiert sie

    CLI - löscht das Flag, und erlaubt damit wieder eine Interruptbehandlung.

    -- 1982 gab es keinen Raspberry Pi , aber Pi und Raspberries

  • =6502=


    Zunächst mal ein unspektakuläres "Tool".



    Dies kann man zwar auch so starten, sinnvoll wird es aber nur, wenn es in den Sprungvektor für den BRK Interrupt "eingehängt" wird. Dafür wird mit

    >0316 00 50

    dieser Interruptvektor verbogen. Das aktiviert die Routine - und es passiert: nichts ! Zunächst.


    Warum kann man das überhaupt so machen, wenn doch eben erklärt wurde, daß man den Interrupt mit SEI stillegen muß, bevor man was ändert ?

    Dies hier betrifft den BRK Interrupt - und der wird nur aktiviert, wenn ein BRK auftaucht. Das passiert nun eigentlich nie, wenn man gerade beim Tippen ist, weshalb man die Adresse auch direkt eintragen kann. Nur das Weiterspringen auf den ehemaligen Inhalt sollte man schon tun. Also ein JMP $F44C ans Ende stellen. Das ist der Wert, der eigentlich in $0316/$0317 steht, bzw. stand.


    Man kann auch probieren, was geschieht, wenn G $F44C direkt aufgerufen wird.

    Um nun ein BRK zu provozieren, benötigt man noch ein kleines Program ala

    .5050 BRK

    was man dann startet, wodurch der Softwareinterrupt ausgelöst wird und sich die Routine bei $5000 aktiviert.


    Diese zeichnet nun in die oberste Bildschirmzeile ein Bitmuster. Und zwar genau das, was bei Adresse $0104,X im Stack zu finden ist, und das war die, welche die Flags enthält. Es werden also die Flags aus dem Statusregister "visualisiert" (wobei man den Programmstart nicht ganz unten am Bildschirm machen darf, sonst scrollt es weg).


    Dargestellt wird so der letzte Zustand der Flags VOR dem Eintritt in die BRK Routine, d.h. am Ende des kleinen Programms bei $5050.


    Dieses kann man nun abändern zu z.B.

    .5050 LDA#$00

    .5052 BRK


    oder

    .5050 SEC

    .5051 BRK


    oder

    .5050 CLC

    .5051 BRK


    und jeweils die Ausgabe ansehen.



    Sowas kann bei Fehlersuchen ganz hilfreich sein, da man dann nur ein BRK an eine "verdächtige" Stelle im zu untersuchenden Programm schreiben muß, um eine einfach zu lesende Flag-Darstellung zu bekommen.


    Die Flags haben folgende Anordnung:


    Negativ | Overflow | BRK | Decimal | Interrupt | Zero | Carry

    -- 1982 gab es keinen Raspberry Pi , aber Pi und Raspberries

  • =6502=


    Nach dem Softwareinterrupt BRK bleibt noch der Hardwareinterrupt IRQ.


    Dies ist nun - naturgemäß - eine sehr maschinenspezifische Geschichte. Vielleicht lassen sich trotzdem ein paar allgemeine Info's entnehmen. Sonst gilt hierbei: Jeder Rechner macht das - und jeder anders.


    Hier wird nun das oben erwähnte Flagsetzen beim Starten obligat

    SEI - setzt das Interrupt Flag und weist weitere IRQs ab, es maskiert sie

    CLI - löscht das Flag, und erlaubt damit wieder eine Interruptbehandlung.


    Zwischen diese beiden Kommandos kommen die Befehle, die beim Start die neue Adresse in $0314/$0315 abspeichern.

    Die eigentliche eigene Interruptroutine, muß sich dann damit nicht unbedingt befassen, denn das Interrupt-Flag ist natürlich beim Auftreten eines Interrupt sowieso schonmal gesetzt worden. Und die KERNEL Interrupt Routine kümmert sich normalerweise auch ums löschen und gibt so die Interruptbearbeitung wieder frei.


    Man kann nun einfach "Mitreisender" der Kernelroutine sein oder auch selbst gezielt Interrupts initiieren und verwalten. Mitreisen bedeutet, daß man eine kleine Erweiterung der Kernelroutine vorschaltet und dann nach $CE0E weiterspringt. Das eignet sich für Sachen wie das Anzeigen der Uhrzeit oben rechts oder Infos wie Akkuinhalt permanent auf dem Bildschirm ausgeben. Das Bekannteste (für C16) dürfte das hier sein:

    INC $FF19 : JMP $CE0E



    Es gibt verschiedene Möglichkeiten, wodurch gezielt ein Hardwareinterrupt IRQ ausgelöst werden kann.

    Viele davon sind zumindest auf den Commodore Mikros recht ähnlich. Bei der 264er Reihe (C16) gibt es die Varianten

    • Timer
    • Rasterzeile
    • Lightpen

    die alle ein IRQ Signal bei der CPU ansagen können, was dann über $FFFE/$FFFF bearbeitet wird.


    Damit das funktioniert, muß irgendein Chip das Signal an der CPU aktivieren. Das ist in diesem speziellen Fall der TED Chip.

    (Beim C64 wäre z.B. der VIC (Videochip) für Interrupts über die Rasterzeile, Spritekollisionen und Lightpen zuständig. Der Timer IRQ würde dort vornehmlich vom CIA-1 Chip kommen. Beim VC20 wird es vermutlich zumindest Timer und Rasterstrahl geben und das dürfte auch das Minimum sein, was vermutlich so ziemlich alle Mikros anbieten können.)


    Damit man den IRQ kontrollieren kann, gibt es zwei Register

    das Interrupt Request Register bei Adresse $FF09

    das Interrupt Mask Register bei Adresse $FF0A


    In beiden stehen (fast) die gleichen Bits, nur daß das Mask Register entscheidet, welcher Interrupt überhaupt erlaubt ist und das Request Register anzeigt, welcher gerade aktiv angefordert wurde.


    Request Register:

    IRQ | Timer3 | ... | Timer2 | Timer1 | Lightpen | Rasterstrahl | ...


    Mask Register:

    IRQ | Timer3 | ... | Timer2 | Timer1 | Lightpen | Rasterstrahl | Rasterzeile Bit-9


    (Ähnliche Strukturen gibt es beim C64 z.B. als Interrupt Latch Register bei $D019 und als Interrupt Enable Register bei $D01A.)


    Man hat hier (TED) also primär die IRQ "Quellen" Timer 1 bis 3 und den Rasterzeilen-Interrupt.

    Wer hat schon einen Lightpen ? (Ist übrigens ein sehr feines Eingabegerät, hat sich aber nicht durchgesetzt.)



    Dabei meint Timer : Es gibt zwei zusammengehörige Adressen, worin ein 16-Bit Wert steht. Dieser Wert wird bei jedem Taktzyklus der Maschine einen Wert heruntergezählt, also -1. Sobald die Zahl von Null heruntergezählt wird, passiert wie bei jedem Register ein "Unterlauf", d.h. es wird aus Null der höchstmögliche Wert - in den beiden Bytes steht dann wieder $FF/$FF - und dieses Ereignis löst einen Interrupt aus, bzw. meldet ihn erstmal im Request-Register an.


    Ob er dann auch tatsächlich stattfinden darf, darüber entscheidet nämlich noch das Mask-Register - denn nur, wenn dort das zugehörige Bit auf "1" gesetzt ist, wird der IRQ auch ausgelöst. Wenn dies geschieht, wird die CPU "informiert" und zusätzlich noch das IRQ Bit im Request Register gesetzt.

    Mit dem Mask-Register entscheidet man, welche Interrupts man zulassen möchte.

    Beim Request Register fragt man nach Auftreten eines IRQs ab, welcher es gewesen ist.


    Es gibt dabei zwei Timer, die genau das Beschriebene machen, und man hat keinen Einfluß auf ihre Werte. Timer2 zählt in $FF02/$FF03 von $FFFF bis $0000 und Timer3 bei $FF04/$FF05 macht es ebenso.

    Timer1 dagegen kann gesetzt werden, man kann also einen "Wunschwert" in seine Adresse bei $FF00/$FF01 schreiben, den der TED sich auch merkt. Dieser wird immer wieder heruntergezählt und bei Überschreiten der Null wird eine IRQ Anforderung geschickt.



    Und Rasterstrahl meint: Das Monitor-/Fernsehbild wird zeilenweise aufgebaut. Von oben nach unten. Dabei ist dem Videochip natürlich klar, welche Rasterzeile gerade ausgegeben wird. Diesen aktuellen Wert der Rasterzeile kann der TED mit einem Wert vergleichen, der in $FF0B + dem hintersten Bit des Mask-Registers (als Bit-9 von $FF0B) steht. In diese kann man einen Wert eintragen. Stimmen beide - Wunschwert und gerade aktuelle Rasterzeile - überein, wird im Request Register im Bit-1 ein IRQ angemeldet.

    Dann entscheidet wieder das Mask-Register, wenn dort Bit-1 gesetzt ist, ob der Rasterzeilen-IRQ überhaupt erfolgen darf und wenn ja, wird der Interrupt der CPU gemeldet und noch das IRQ Bit im Request Register gesetzt.



    Das Vorgehen ist also: Bit-Maske im Mask-Register setzen, evtl. Timer1 laden oder einen bestimmten Rasterzeilenwert vorgeben - auf den Interrupt warten - in der eigenen Routine darauf reagieren, evtl. in Abhängigkeit vom Inhalt des Request Registers.

    Wichtig ist nun noch, daß man dem TED am Ende quasi mitteilt, wenn man einen IRQ bereits bearbeitet hat. Dies geschieht dadurch, daß man das Request Register einmal lädt und in sich selbst zurückschreibt, also LDA$FF09:STA$FF09 ausführt. Das setzt die "angemeldeten Anforderungen" und v.a. das IRQ-Bit zurück.



    Allerdings ist man gelegentlich nicht der Einzige, der IRQs benutzt. So verwendet der KERNEL etwa die Rasterzeilen und da v.a. $A1 und $CC für seine eigenen Zwecke. Insbesondere sind das auch die Bereiche, die eine Teilung des Bildschirms in einen zweigeteilten Text/Graphicmodus zulassen (im BASIC: GRAPHIC 2,1).

    Desweiteren wertet die KERNEL-Interrupt-Routine RS-232 Daten aus, betreibt die Datasette, aktualisiert die Uhr, kümmert sich um die Tonausgabe und ruft letztlich die Tastaturabfrage auf, bevor sie den Zustand VOR dem Interrupt wieder herstellt und die CPU dort weitermacht, wo sie unterbrochen worden war.


    Das Verlassen der Interrupt Routine erfolgt prinzipiell mit einem

    RTI - Return from Interrupt

    der auch bereits in der Hauptroutine mit drinsteht.

    Der Befehl lädt dabei das bei Auftreten eines Interrupts automatisch auf dem Stack gerettete Statusregister mit den Flags wieder zurück und stellt die Adresse wieder her, wo der nächste zu bearbeitende Befehl von vor der Unterbrechung zu finden ist. Die ebenfalls (s.o.) geretteten Werte von Akku, X-Register, Y-Register werden noch vor dem RTI per Software zurückgeholt (und auch die richtige Modulbank wieder eingestellt). Man kann auch - unter kompletter Auslassung der Hauptroutine - direkt dort zurückspringen, über $FCBE. Nur: Ton, Uhr, Tastaturabfrage etc. müssen dann selbst geregelt werden.


    Alles zusammen zzgl. der eigenen Routine darf nicht länger dauern, als bis zum nächsten regulären Auftreten eines Interrupts - sonst ist man "trapped and doomed" und das eigentliche Programm kommt, vor lauter Unterbrechung, auch nicht mehr zum Arbeiten.

    -- 1982 gab es keinen Raspberry Pi , aber Pi und Raspberries

  • =6502=


    Rasterinterrupt - bedeutet: man kann an einer bestimmten Zeile des Bildaufbaus irgendwas machen. Etwa die Hintergrundfarbe ändern.




    Bei $5000 sieht man so eine typische Einrichtung einer Interruptroutine. Der Interrupt wird kurzzeitig "gesperrt" (die CPU nimmt keine IRQs an) mit SEI. Dann wird der Sprungvektor beladen - hier mit $5050. CLI läßt die Interrupts wieder zu.


    Bei $5050 steht dann die eigentliche "Raster"routine.


    Diese lädt zuerst den Wert, bei dem der Raster-IRQ ausgelöst werden sollte.

    Ist dieser $24, werden in den Akku $43 und ins X-Register $23 geladen.

    Ist er anders als $24, werden in Akku $24 und in XR $12 geladen.


    Dabei steht der Wert im Akku für die "neue" Rasterzeile, also die bei der - wenn der Rasterstrahl dort vorbeikommt - erneut ein Raster-IRQ stattfinden soll. Der Akku ist also die Vorbereitung für die nächste Runde.

    Im XR steht dagegen einfach ein Farbwert, einmal der, der AB(!) der jetzigen Rasterzeile gesetzt wird und dann der, der für die nächste Runde benutzt wird, wenn sie stattfindet.


    Alles wird ab $5062 in die passenden Adressen geschrieben - die Farbwerte in Hintergrund und Rand, der Akku mit der jeweils nächsten Rasterzeile nach $FF0B.


    Zum Schluß wird das IRQ Bit in $FF09 gelöscht und in die Hauptroutine verzweigt.


    Allerdings: Diese sieht nun keinen IRQ mehr, und daher ist das Keyboard etc. nicht mehr benutztbar




    Das ist prinzipiell das Gleiche wie eben.

    Lediglich die fixen Werte der Rasterzeilen im Akku für den nächsten Raster-IRQ sind "ausgelagert" worden. Nach $D0 und $D1.


    Demzufolge sollten dort auch vor dem Starten solche Werte drin stehen. Am Besten einfach die gleichen (erstmal).




    SUBWAIT2 wird ab $6000 benötigt.


    Und hier kommt nun das Hauptprogramm dazu.

    Bisher wechselt ja lediglich im Interrupt die Farbe, je nachdem welche Rasterzeile gerade erreicht wurde.


    Da die Rasterzeilen in $D0,$D1 aber auch verändert werden können, sollte man das auch tun. Das macht die Routine ab $7012.

    Davor steht die Sprungvektor-Einstellung und es werden zwei Rasterzeilen vorgegeben, damit in $D0,$D1 schon was steht.


    Der Rest läuft einfach immer wieder ab $7012 durch. Eine Verzögerung ist auch eingebaut.

    Ansonsten passiert nur, daß die Werte in $D0, $D1 um +1 erhöht werden - bis $D0 bei dem Wert $64 angekommen ist. Da erfolgt ein Rücksetzen.


    Schön ist es nicht ... aber es erlaubt evtl. das Geschehen nachzuvollziehen.




    So was nennt man i.Ü. auch einen "Rasterbalken" oder "Rasterbar".

    Sollte bekannt sein.


    Wenn man soetwas in schöner haben will, ist ein gute Variante Tabellen mit Farbwerten einzusetzen und v.a. den Balken an einem Stück zu zeichnen - also nicht den unteren Wert per Raster-IRQ zu setzen - sondern nur den oberen. Dazu kommt dann eine Verzögerung die angibt, wie lange man die zugehörige Farbe aus der anderen Tabelle setzen will.


    Am Beginn wird auf Rasterinterrupt getestest. Dann werden acht Werte aus Tabelle geholt und die Farben gesetzt - man kann hier schön optisch sehen wie schnell so ein Mikro sein kann.

    Zum Schluß wird $D0 geladen und als nächste Raster-Zeile für den kommenden IRQ nach $FF0B geschrieben.

    Das IRQ Bit wird zurückgesetzt und in die Hauptroutine verzweigt (die wieder nix mehr macht).


    Die Tabellen können prima geändert werden. Farbe und Breite.


    Bewegt wird das Ganze einfach mit der $7000 Routine von direkt darüber - nur den Sprunkvektor muß man auf $5200 anpassen. Wer mag entfernt das nun unnütze $D1 und denkt sich was aus, wie der Balken langsam wieder nach oben wandert - statt rabiat auf Rasterzeile $00 gesetzt zu werden.

    -- 1982 gab es keinen Raspberry Pi , aber Pi und Raspberries

  • =6502=


    Die Rasterinterrupts erlauben aber natürlich nicht nur "Bildschirmänderungen", man kann die auch für andere Sachen benutzen. Es ist nur als Demonstration wahrscheinlich erstmal ganz hilfreich, mit der Rasterzeile auch was im Bildschirm zu machen.

    Oft werden sie auch für die Unterteilung des Bildschirms in unterschiedliche Bereiche eingesetzt. Dadurch lassen sich Graphic und Text gleichzeitig anzeigen, oder Multicolor und Hochauflösende Grafik gleichzeitig, oder verschiedene Zeichensätze, oder unterschiedliche Farbpaletten. Stichwort: Split-Screen. Es ist eigentlich alles, was irgendwie umschaltbar ist, damit exakt auf einer bestimmten Höhe des Bildschirms an- bzw. abschaltbar.

    Da der TED weiches Scrolling beherrscht, läßt sich auch dieses so auf z.B. eine Laufschrift beschränken.


    Man kann damit aber durchaus auch Timer "nachbauen", indem man in jedem Rasterinterrupt eine Speicherstelle runterzählt. Damit hätte man ein zweites/anderes "Zeitnormal" im Rechner, was zudem von den "Timern" unabhängig ist.


    Nun macht lustigerweise der Plus/4 genau so etwas, was mit der Eigenheit den Rasterinterrupt recht "selbstherrlich" zu vereinnahmen, wahrscheinlich mit ein Grund ist, weshalb die Programmierer und Firmen damals(TM) ein bißchen einen Bogen um das Gerät gemacht haben.

    (Der andere wesentlichere Punkt ist sicher, daß "der Markt" einfach gesättigt war, zumindest für 8-Bit. Eine ähnliche Geschichte hat Acorn mit seinem Electron erlebt, was eigentlich ein ähnlich schickes Gerät 2ter oder gar 3ter Generation ist. Und der Sinclair QL war auch nicht so der ganz große Hit, aus ähnlichen Gründen.)


    Was passiert also eigenartiges, um die Zeit zu messen ?


    Normalerweise was ganz Einfaches: Man nimmt so einen Timer, wie oben beschrieben. D.h. 2 Bytes (oder auch mehr) werden als Zahl gesehen und in einem exakt definierten Zeitraum definiert verändert. Wenn man dann nach einiger Zeit wieder draufschaut, kann man anhand der Zahl direkt auf die verstrichene Zeit zurückrechnen.


    Beim C64, VC20 etc. ist das auch ganz einfach: Die Zahl hat 65536 Werte. Bei $FF/$FF geht es los. Dann immer -1 bei jedem Takt.

    Da sich die Idee mit flexiblen Taktraten bis heute nicht durchgesetzt hat (flexibel(!), keine Stufen), weiß man bei so einem Gerät mit 1MHz Takt, daß es 1 Million Werte pro Sekunde zählt. Das tut es auch extrem zuverlässig, was jeder weiß, der mal vom Schweizer Uhrensterben gehört hat und das mit Casio, Citizen, Seiko und dem Begriff Quarzuhr zu verbinden weiß.

    Man kann in einer einzigen Sekunde den Timer insgesamt 1.000.000 / 65536 = 15.25 , ca. 15mal komplett durchzählen. Oder anders: 1 Millisekunde ist erst nach 1.000 gezählten Zahlen vorbei.


    So ein Timer ist daher - für menschliche Maßstäbe - ein ziemlich exaktes Ding zur Zeitmessung.

    Und darum nützlich.


    Der Plus/4 hat nun gleich drei davon.

    Und - das ist nun der Witz - benutzt davon keinen, um seine interne Uhrzeit zu verstellen !

    Daß soetwas beim aufs "Normale" konditionierten Programmierer um 1985 auf Unverständnis stößt, ist ja zu erwarten.


    Stattdessen wird hierzu der Rasterstrahl benutzt. An einer bestimmten Stelle löst dieser einen IRQ aus - ganz normal s.o. - und im Rahmen dessen wird einfach ein Wert in einer Adresse erhöht (INC $A5).

    Das kann man nun eigentlich auch gut benutzen - allerdings hat es den kleinen Haken, daß der Rasterstrahl mit 50Hz das Bild wiederholt - also 50mal pro Sekunde an der gleichen Stelle vorbeikommt. Man hätte also pro Sekunde 50 Zählwerte hochgezählt bzw. eine +1 wäre exakt eine 1/50-tel Sekunde.

    Könnte man durchaus benutzen; für die Systemuhr, würde man dann einfach 50 Werte abwarten bevor man den Sekundenwert um +1 Sekunde erhöht.

    Passiert aber nicht ! Stattdessen hat Commodore festgelegt, daß die Uhr bittschön' in 1/60-tel Sekunden hochzuzählen ist. Und damit das klappt, steht im ROM ab $CFD1 ein ganz fabelhaftes Wunderwerk der Softwaretechnik: Es wird ein extra Zusatzzähler benutzt, der von 4 auf 0 herunterzählt - pro erfolgtem RasterIRQ eine Position. Parallel dazu wird die Systemuhr hochgezählt, um jeweils +1. Wenn der zusätzliche Zähler die 0 unterschreitet, wird er natürlich wieder auf 4 gesetzt und (!): Es wird die Systemuhr bei dieser einen Position noch ein weiteres, zusätzliches Mal hochgezählt.

    Wenn man so will: Die Systemuhr läuft eigentlich mit einer 1/50-tel Sekunde Zeit-Auflösung und zwar genau für 4 Schläge, beim 5ten Hochschalten der Uhr wird aber um +2 Einheiten hochgeschaltet, weshalb sich nun in der Summe über alle 6 hochgezählten Werte eine Zeitauflösung von 1/60-tel Sekunde ergibt.

    Oder - aller 5 Takte vergeht bei dieser sonderbaren Uhr die Zeit plötzlich kurzeitig schneller (genau doppelt so schnell) was in Summe (über alle 5 Takte) dann eine um 20% "schnellere" Uhr ergibt.


    Wer sich also irgendwann mal wundern sollte, warum das mit dem Timing von Systemzeit und Rasterstrahlzeiten dort nicht hinhaut, wie man sich das denkt ... es gibt Gründe; geheimnisvolle, unerklärliche ...



    Hier nun noch ein Beispiel mit den echten Timern.


    forum.classic-computing.de/index.php?attachment/38601/


    forum.classic-computing.de/index.php?attachment/38602/


    Bei $5000 wird wieder die Interrupt Routine vorbereitet und beim Interruptvektor eingetragen. Zusätzlich wird im Interrupt Mask Register der Timer3 eingeschaltet. Diese löst nun immer bei Unterlauf einen IRQ aus.


    Damit man was Sinnvolles sieht, müssen bei $D0 bis $D7 je der Startwert $30 eingetragen sein.


    Bei $5050 wird erstmal jeder andere Interrupt abgewiesen und in die Hauptroutine weitergeschickt, nur Timer3 IRQs werden "angenommen". In $D7 wird eine Zahl erhöht, um +1. Für den Fall daß dadurch ein Überlauf entstanden ist, wird dieser auf die Stelle davor addiert, wenn dort wieder ein Überlauf entsteht, noch eine davor usf. Wenn dies eintritt, wird der Wert der Speicherstelle auch auf $30 zurückgesetzt. Ein Überlauf entsteht von $39 auf $3A. Am Ende werden alle vorhandenen Stellen und abgefragten Stellen ($D7 bis $D1) auf den Bildschirm oben rechts geschrieben. Dann wird der IRQ als bearbeitet markiert und in die Hauptroutine gesprungen.


    Vermutlich niemand baut professionell in der Form eine Systemuhr !

    Aber man kann schön sehen was dabei passiert. Es wird nämlich simpel mit dem TimerIRQ eine Reihe von Registern hochgezählt, die in irgeneiner Form "Zeit" repäsentieren. Normalerweise würde man sowas auch gleich auf Sekunden o.ä umrechnen. Hier wird zudem pro Stelle nur in den Zahlen 0-9 (nämlich direkt den Zifferncodierungen im Bildschirm) gerechnet - man verschwendet also pro Byte 256-10=246 potentielle Stellen. Dafür kann man es schön direkt dezimal ablesen !



    Frage: Und die Einheit der angezeigten Zahl ist dann welche ?

    Der Timer3 läuft ja für einen IRQ die 16-Bit einmal durch und das in einem System mit einem Systemtakt von nominal 1.76MHz, wobei der Timer nur mit der halben Rate läuft.


    Frage: Was genau ist mit Stelle $D0 ? Welcher Struktur in einer CPU entspricht das am ehesten ?



    Mehr Info zu dem (hochinteressanten) TED-Chip findet sich z.B. hier

    http://plus4world.powweb.com/plus4encyclopedia/500024 und hier

    http://mclauchlan.site.net.au/…king/C-Hacking12/gfx.html


    Mehr und aktuelle Info's zum Thema Timer kann man sich unter dem Schlagwort

    HPET - High Precision Event Timer

    anlesen.

    -- 1982 gab es keinen Raspberry Pi , aber Pi und Raspberries