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:- Digitale Logik und Logikgatter einfach erklärt
- Verwendung des 555-Timer als Taktgeber / Clock
- Das Clock-Modul: Taktgeber für unseren Breadboard-Computer
- Speichertypen und Zugriff auf Speicher
- Erste Schritte mit der CPU
- Eine echte WDC W65C02-CPU
- Das Speicher-Modul: Anbindung von RAM und ROM
- Erstes Programm in Maschinensprache: RAM-Test
- Das Sniffer-Modul: Ein Arduino/STM32 zeigt an, was auf dem Bus los ist.
- Erstes Ausgabegerät: Adressierung und Ausgabe auf 8 LEDs
- Programmiersprache-Evolution: von Maschinensprache zu Assembler
- 3-fach 7-Segment-Anzeige als dezimale Ausgabe, Teil 1: Taktungsprobleme
- 3-fach 7-Segment-Anzeige als dezimale Ausgabe, Teil 2: 20 Nanosekunden, die nicht sein sollten
- 3-fach 7-Segment-Anzeige als dezimale Ausgabe, Teil 3: Assembler-Programme
- 3-fach 7-Segment-Anzeige als dezimale Ausgabe, Teil 4: neue Adressierungsarchitektur mit 74HC688 und 74HC138
- 3-fach 7-Segment-Anzeige als dezimale Ausgabe, Teil 5: Programmierung in Assembler
- 3x16 Zeichen LCD DOGM163 als Textausgabe-Gerät, Teil 1: Breadboard-Aufbau und erste Programmversion
- 3x16 Zeichen LCD DOGM163 als Textausgabe-Gerät, Teil 2: Debugging
- Clock-Modul upgraden: höherer Takt (2 KHz) und Frequenzgenerator (bis 160 KHz)
- Erstes Eingabe-Gerät: ein 4x4 Keypad als Tastatur (wird mit 65C22 abgefragt)
- Erweiterung um eine Debug-Anzeige des Prozessorstatus und der Register auf dem LCD
- Die Tastatur spinnt: Hardware-Debugging, Fake-6522 aus China und mehr
- Assembler-Programmierung: Dezimal Hexadezimal Binär Konverter und Spiel
- Ein Arduino ATmega328p dient dem Breadboard Computer als CoProzessor
- Integration eines CoProzessors auf Arduino-Basis
- Assembler-Programmierung: weitere Funktionen des CoProzessors auf Arduino-Basis abfragen
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: