8-Bit-Breadboard-Computer auf Basis einer 6502-CPU - selektives Interrupt Handling über den VIA 6522

Bisherige Artikel dieser Serie - hier könnt ihr nochmal alle Grundlagen nachlesen, falls ihr jetzt erst einsteigt: Ein klein bisschen Platz ist ja noch auf dem Breadboard. Und am 6522 sind ja auch noch vier Leitungen unbenutzt: CA1, CA2, CB1 und CB2.

Diese Leitungen wollen wir heute benutzen, um dort über den VIA 6522 Interrupts entgegenzunehmen und an die 6502 CPU weiterzuleiten. Dank der Interrupt Register im 6522er können wir dann unterscheiden, von welcher Leitung der Interrupt kam und entsprechend reagieren.

Zum Triggern der vier Leitungen verwende ich vier kleine Taster, die noch gerade so auf das Breadboard passen und als Besonderheit eine eingebaute LED haben, die wir über einen 74HC374 - Latch-Baustein ansteuern können, wie wir es auch schon bei der LED-Ausgabe getan haben.

Rechts neben dem Debug und Continue-Knopf ist gerade noch genügend Platz für die vier kleinen Taster und darunter für den 374er, wenn wir die Fernbedienungs-Garage ein wenig umarrangieren:



Die Hardware verdrahten


Alle Buttons werden per Pullup-Widerstand auf High gezogen und an die 6522-CA/CB-Leitungen (graue Kabel) weitergegeben. Wird ein Button gedrückt, wird er auch Ground kurzgeschlossen und die Flanke geht damit auf Low.

Der 374er wird an die unteren 4 Datenbits des Datenbusses angeschlossen (gelbe Kabel). Wird der 374er durch ein Clock-Signal getriggert (etwa durch ein STA BTN), dann wird den Zustand der Datenbits festgehalten und an die Output-Leitungen (schwarz) weitergegeben, wo dann die LEDs der Buttons über Vorwiderstände auf High gezogen und zum Leuchten gebracht werden.

Zum Schluss verlegen wir noch ein langes Kabel (blau) vom IRQ-Ausgang des 6522 zum IRQ-Eingang des 6502, der dann auf die Tastendrucke reagieren kann.

Der Trick bei der Sache ist, dass sich der 6522 in einem Register merkt, von welcher Leitung der IRQ ausgelöst wurde und wir diese Information durch die CPU abrufen können und somit wissen, welche der vier Knöpfe gedrückt wurde. Wurde keiner der vier weißen Buttons gedrückt und trotzdem ein IRQ ausgelöst, dann geschah das nach dem Ausschlussprinzip durch unseren grünen Continue-Button, der direkt an der IRQ-Leitung des 6502 hängt.

Den 6522 für Interrupts konfigurieren

Rufen wir uns noch einmal die Adressen des 6522 ins Gedächtnis: ; --- 6522er --- VIA: equ $6000 ; 6522 #1 VIA_PORTB: equ VIA+$0 ; Port B: ATmega328P CoProzessor VIA_PORTA: equ VIA+$1 ; Port A: 4x4 Keypad VIA_DDRB: equ VIA+$2 ; Data Direction Port B VIA_DDRA: equ VIA+$3 ; Data Direction Port A VIA_T1CL: equ VIA+$4 ; Timer 1 Low Order Latches (W) / Counter (R) VIA_T1CH: equ VIA+$5 ; T1 High-Order Counter VIA_T1LL: equ VIA+$6 ; T1 Low-Order Latches VIA_T1LH: equ VIA+$7 ; T1 High-Order Latches VIA_T2CL: equ VIA+$8 ; Timer 2 Low Order Latches (W) / Counter (R) VIA_T2CH: equ VIA+$9 ; T2 High-Order Counter VIA_SR: equ VIA+$A ; Shift Register VIA_ACR: equ VIA+$B ; Auxiliary Control Register VIA_PCR: equ VIA+$C ; Peripheral Control Register VIA_IFR: equ VIA+$D ; Interrupt Flag Register VIA_IER: equ VIA+$E ; Interrupt Enable Register VIA_PA_No_SHAKE: equ VIA+$F ; Same as Reg 1 except no "Handshake" Für das Interrupt Handling sind die 3 Register PCR, IFR und IER wichtig.

Im Peripheral Control Register (PCR) konfigurieren wir, ob auf eine steigende oder fallende Flanke reagiert werden soll. Das PCR ist auch für das Schieberegister und Timer zuständig, aber die interessieren uns für IRQs nicht. Und für Reaktion auf fallende Flanken (also wenn durch Tastendruck ein Low ausgelöst wird), müssen in die entsprechenden Bits überall Nullen. lda #%00000000 ; alle negative active edge sta VIA_PCR ; Peripheral Control Register: 7 6 5 4 3 2 1 0 ; CB2-Control CB1 CA2-Control CA1 erledigt das für uns. Ich habe den Code in die Routine init_6522 in ioInit in io.inc geschrieben. Dort wird er immer zu Programmbeginn ausgeführt.

Als nächsten müssen wir die Interrupts aktivieren (enablen), für die der 6522 reagieren soll. Wir möchten auf alle vier Leitungen reagieren und setzen darum die entsprechenden Bits des IER (Interrupt Enable Register) auf 1: lda #%10011011 sta VIA_IER ; Interrupt Enable Register: 7 6 5 4 3 2 1 0 ; SET TM1 TM2 CB1 CB2 SHR CA1 CA2 Damit ist der 6522 auch schon konfiguriert. Am Ende der Initialisierungsroutine gehen wir dann sicher, dass auch die 6502 CPU auf Interrupts reagieren und setzen ein CLI (clear interrupt disable flag), um Interrupts zu ermöglichen.

Bei jedem eingehenden IRQ wird dann in die in Adresse $$FFFE/F angegebene Adresse gesprungen, dass ist im 6502 so hart kodiert. ; --- Sprungvektoren --- ORG $FFFA word nmivektor ; NMI (bei $FFFA/B) word startvektor ; Programmstartadresse (bei $FFFC/D) word irqvektor ; BRK/IRQ (bei $FFFE/F) Ich habe die Adresse auf die Marke irqvektor gesetzt. Der Assembler setzt dann automatisch die richtige Adresse, an dem die Interrupt-Routine beginnt ein. Die Interrupt-Routine habe ich in der Include-Datei irq.inc untergebracht.

In irqvektor frage ich dann das Register IFR (Interrupt Flag Register) des 6522 ab: lda VIA_IFR ; Interrupt Flag Register: 7 6 5 4 3 2 1 0 ; IRQ TM1 TM2 CB1 CB2 SHR CA1 CA2 In CB1, CB2, CA1 oder CA2 steht dann eine eins, je nachdem welche Taste gedrückt wurde bzw. welche Leitung getriggert wurde.

Das ist auch schon das ganze Geheimnis. Mehr braucht es nicht. Obwohl: doch, eine Kleinigkeit gibt es noch: Damit die IRQ nicht wie im automatischen Dauerfeuer immer wieder gefeuert werden, solange der Knopf gedrückt wird, hält der 6522er die IRQ auf Low, bis von Port A (für CA1 und CA2) bzw. Port B (CB1 und CB2) gelesen (oder auch geschrieben wird). Erst dann setzt er die IRQ-Flanke wieder auf High und erst danach kann ein neuer IRQ getriggert werden.

Das kann man bewerkstelligen durch einen bit-OpCode. Der hat den Vorteil gegenüber einem lda, dass er kein Register ändert. Da man den IRQ erst unmittelbar vor dem rti, also der Rückkehr aus der Interrupt-Routine zurücksetzen sollte, also nachdem der Stack auf seinen Ursprung vor dem IRQ-Eintritt wiederhergestellt wurde, ist der bit-Befehl hier sehr praktisch: er liest hardwaretechnisch, verändert aber weder A, X noch Y-Register. ; IRQ löschen durch lesen der Ports bit VIA_PORTA bit VIA_PORTB

Anwendungsbeispiele

Mit IRQs kann man unabhängig vom gerade laufenden Programm Befehle ausführen. Wird ein IRQ-Knopf gedrückt, wird das Pramm unterbrochen, die IRQ-Routine abgearbeitet und dann wieder mit dem Programm weitergemacht. Darum ist es wichtig, in der IRQ-Routine alles so zu hinterlassen, wie man es vorgefunden hatm sprich: alle Register auf den Stack zu packen und später wiederherzustellen.

Auch sollte man sich in IRQ-Routinen nicht zu lange aufhalten, denn, wie gesagt: das Programm steht solange still.

Ein Anwendungsfall wäre z. B. 4 Funktionen auszuführen, je nach Knopf. Etwa zum Debugging. Vielleicht auf dem ersten Knopf die Ausgabe der Registerwerte, auf den zweiten Knopf ein Tool zur Anzeige des RAM-Inhaltes, auf dem dritten Knopf eine Protkollierung oder sowas.

Da ich aber die praktischen (naja, sie lassen sich ein bisschen unpräzise drücken) Buttons mit LEDs eingebaut habe, habe ich mich entschieden, die Knöpfe dazu zu benutzen, 4 Zustände umzuschalten und den Zustand über die LED anzuzeigen.

Am Anfang stehen die Buttonstates auf 0000. Drücke ich den rechten Knopf soll das rechte Bit getogglet werden, auf der 0 also eine 1 werden -> 0001. Die LED der rechten LED soll angehen, damit ich sehe kann, dass dieses Bit jetzt gesetzt ist. Mit den vier Knöpfen kann ich somit 4 Bits invividuell manipulieren.

Mit den vier Bits kann ich 16 Zustände abbilden. So könnte ich zum Beispiel am Anfang des Programmes warten, bis eine Taste auf dem KEypad gedrückt wird. Vorher könnte ich mit den Buttons eine Zahl zwischen 0 und 15 einstellen. Und je nachdem, welche Zahl eingestellt ist, würde dann ein anderes Programm angesprungen werden. Je Programmbank habe ich ja 32 KB Speicher, was für die meisten Programme zuviel ist. Hier könnte ich auch 16 Programme je 2 KB unterbringen. Das wären bei 16 Programmbänken 256 Programme, die ich auf einem EPROM unterbringen könnte.

Im Grunde kann ich die Buttons wie eine Art DIP-Schalter verwenden, nur mit dem Vorteil, den Anfangszustand der Schalterpositionen durch die Software selbst bestimmen zu können. Die Anwendungsfälle dieser Statusbits sind vielfältig

Ein Beispielprogramm

Ich habe mich für das Beispielprogramm dafür entschieden, vier Zähler loslaufen zu lassen und anzuhalten, sobald das Statusbit für einen Timer durch einen IRQ gesetzt wurde. Dabei frage ich im Hauptprogramm nur die Variable btnstates in der ZeroPage ab. Das setzen der Bits dieser Variablen und die Anzeige der Zuständen auf den Buttons-LEDs geschieht automatisch und programmunabhängig per Interrupt-Routine.

za3: equ $1001 za2: equ $1002 za1: equ $1003 za0: equ $1004 ; --- Hauptprogramm --- reset: jsr ioInit start: stz za0 stz za1 stz za2 stz za3 anzeigen: LCD_POS #0 ; LCD-Cursor positionieren (Makro) LCD_VAL za3, 3 lda #32 ; Leerzeichen LCD_CHAR_AKKU LCD_VAL za2, 3 lda #32 ; Leerzeichen LCD_CHAR_AKKU LCD_VAL za1, 3 lda #32 ; Leerzeichen LCD_CHAR_AKKU LCD_VAL za0, 3 lda #32 ; Leerzeichen LCD_CHAR_AKKU lda #100 jsr delayMs lda btnstates ; Button-LEDs zum Check nochmal auf LED ausgeben sta LED ; wenn das btnstates-bit gesetzt ist, dann wird der entspr. zähler angehalten bbs0 btnstates, no_state0 inc za0 no_state0: bbs1 btnstates, no_state1 inc za1 no_state1: bbs2 btnstates, no_state2 inc za2 no_state2: bbs3 btnstates, no_state3 inc za3 no_state3: jmp anzeigen Die für das IRQ-Handling zuständigen Routinen finden sich in irq.inc in der Routine irqvektor ; Unterroutinen zum IRQ-Handling ; last edit: Oliver Kuhlemann (www.cool-web.de), 2020-10-09 ; --- Interrupt-Routinen --- ; ----- IRQ ----- irqvektor: ; BRK/IRQ: jeder Druck auf eine der 4 IRQ-Tasten mit LED ändert den ; Status dieses Buttons-Bits (aus (Standard) / an) in btnstates. ; Die LEDs zeigen den Status an (an=1, aus=0) ; So kann btnstates jederzeit außerhalb des Programmes manipuliert ; werden und im Programm dann darauf reagiert werden. ; sei ; weitere Interrupts verbieten bis wieder aus der routine raus pha phx phy ldx #0 ; X setzen, wenn ein IRQ durch CA1...CB2 ausgelöst ; wenn am Ende X immer noch 0, dann wurde der Interrupt ; nicht durch den 6522, sondern direkt durch die grüne ; Cont-Taste ausgelöst ; checken, von wo der IRQ kam und entsprechend btnstates setzen lda VIA_IFR ; Interrupt Flag Register: 7 6 5 4 3 2 1 0 ; IRQ TM1 TM2 CB1 CB2 SHR CA1 CA2 sta zpirq ; in zeropage zwischenspeichern zur schnelleren Verzweigung ; btnstates Aufbau: 0 0 0 0 CB2 CB1 CA2 CA1 ; CB2 = Bit 3 bbr3 zpirq, irq2 ; Bit nicht gesetzt -> überspringen ; Bit gesetzt, IRQ CB2 ausgelöst: Bit 3 negieren in btnstates inx ; IRQ kam vom 6522 bbr3 btnstates, irq3_set rmb3 btnstates ; reset bra irq2 irq3_set: smb3 btnstates ; set ; bra irq2 irq2: ; CB1 = Bit 4 bbr4 zpirq, irq1 ; Bit nicht gesetzt -> überspringen ; Bit gesetzt, IRQ CB1 ausgelöst: Bit 2 negieren in btnstates inx ; IRQ kam vom 6522 bbr2 btnstates, irq2_set rmb2 btnstates ; reset bra irq1 irq2_set: smb2 btnstates ; set ; bra irq1 irq1: ; CA2 = Bit 0 bbr0 zpirq, irq0 ; Bit nicht gesetzt -> überspringen ; Bit gesetzt, IRQ CA2 ausgelöst: Bit 1 negieren in btnstates inx ; IRQ kam vom 6522 bbr1 btnstates, irq1_set rmb1 btnstates ; reset bra irq0 irq1_set: smb1 btnstates ; set ; bra irq0 irq0: ; CA1 = Bit 1 bbr1 zpirq, irqdone ; Bit nicht gesetzt -> überspringen ; Bit gesetzt, IRQ CA1 ausgelöst: Bit 0 negieren in btnstates inx ; IRQ kam vom 6522 bbr0 btnstates, irq0_set rmb0 btnstates ; reset bra irqdone irq0_set: smb0 btnstates ; set ; bra irqdone irqdone: ; wenn "Continue (grüner Knopf)" ausgelöst wurde, dann ist ; btnstates gleichgeblieben und Reg. X steht nicht mehr auf 0 cpx #1 beq irqdone2 ; --- hier evtl. Code zur Behandlung von non-6522-IRQ (Cont-Button) ; derzeit: springt zurück, ohne etwas zu tun ; so kann ich WAI als Breakpoints in den Source einbauen, bei dem die CPU pausiert ; ... bra irqdone3 irqdone2: ; wenn ein Button-IRQ-Ausgelöst wurde ; debouncing lda #250 jsr delayMs irqdone3: lda btnstates ; Button-LEDs aktualisieren sta BTN ply ; Register wiederherstellen plx pla ; IRQ löschen durch lesen der Ports bit VIA_PORTA bit VIA_PORTB ; cli ; interrupts wieder erlauben rti ; ----- NMI ----- ; bei einkommenden NMI-Signal (Taster an NMI) springt er kurz hier rein und ; macht dann im Code weiter ; NMI -> Debug-Ausgabe nmivektor: php jsr debug plp wai rti Wie immer ist der Code gut dokumentiert und sollte sich selbst erklären. Ich habe aber auch wieder ein Video gemacht, bei dem ich den Source-Code noch einmal erkläre.

Video

Im folgenden Video zeige ich noch einmal die notwendige Verdrahtung und führe das Beispielprogramm vor. Danach erkläre ich die Grundlagen (Register des 6522) und den Assembler-Code:



Aussichten

Im nächsten Teil wollen wir feststellen, auf welche Geschwindigkeit, sprich Taktfrequenz wir unseren Breadboard-Computer maximal bringen können.