8-Bit-Breadboard-Computer auf Basis einer 6502-CPU - Erweiterung um ein Debug-Tool
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)
Allerdings habe ich bei der Entwicklung des Spielchens bemerkt, dass man doch mehr Fehler beim Assembler Programmieren macht, als einem lieb ist. Zu schnell schleicht sich ein Fehler ein. Z. B. habe ich in einer Zeile das #-Zeichen vergessen und AND %00001111 statt AND #%00001111 in den Code geschrieben. Das hatte natürlich die Folge, dass er den Inhalt von der Speicherzelle $0F und-verknüpft hat und nicht den Wert $0F. Und Adresse $0F ist nicht initialisiert. Da kann also alles drin stehen. Wie oft habe ich bei der Fehlersuche über diese Zeile drüber weg gelesen, ohne dass mir auffiel, das dort ein "#" fehlte...
Ich habe mich dann immer gewundert, warum so komische Werte bei der Umrechnung von Hexadezimal nach Dezimal herauskommen, denn das war das Nächste was ich auf der Anzeige lesen konnte.
Man bräuchte eine Möglichkeit, den Programmverlauf im Source-Code anzuhalten und sich die Register ausgeben zu lassen. Bisher hatte ich das ja so gemacht, dass ich ein STP an die Stelle im Source schrieb, an dem ich den Fehler vermutete und dann mit dem Sniffer im Einzelschritt durch den Code hing. Nach dem STP ging es dann aber nicht weiter und ich musste mit Reset neu starten.
Dann stolperte ich mal wieder über den W65C02-only Befehl WAI für Wait. Dieser setzt das Ready-Flag des Prozessorstatus auf 0 und zieht die RDY Leitung (Pin 2) auf Low (weshalb dann auch die dort von mir angebrachte LED leuchtet). WAI bedeutet soviel wie "wait for interrupt". Die CPU pausiert also an der Stelle, an der dieser Befehl auftaucht und macht erst weiter, wenn ein Interrupt-Signal über die Hardware (IRQ, Pin 4 oder NMI, Pin 6) hereinkommt.
Also habe ich die NMI-Leitung per Pullup-Widerstand auf High gezogen und einen Taster eingebaut, der den Pin kurzzeitig auf Low zieht und so einen Interrupt auslöst, der dann das Programm weiterlaufen lässt.
Das ist schon mal sehr praktisch, um mehrere Waits einzubauen und im Sniffer zu beobachten, was der Code weiterhin macht. Der Sniffer hat allerdings ein Problem: Es zeigt nicht den Inhalt der Register (A, X, Y, P) nicht an, sondern nur den Code. Man bräuchte irgendwie noch eine Möglichkeit, in die 6502-CPU "herein zu schauen".
Da stellt sich natürlich zuerst die Frage, was man denn anzeigen kann. Was gibt es alles für Register in der CPU? Da wären:
Prozessor Register 6502
76543210 Akkumulator (A)
76543210 Index-Register X (X)
76543210 Index-Register Y (Y)
76543210 76543210...Program Counter (PC)
8 76543210 Stack Pointer (S)
76543210 Prozessor Status (P) Branch-Befehle
NV1BDIZC Register
^--------- N negative fl. 1, wenn Bit 7 gesetzt BPL (0) BMI (1)
^-------- V overflow fl. 1, Überlauf bei Vorzeichen-Rechn. BVC (0) BVS (1)
^------- 1 unbenutzt immer 1
^------ B BRK enabled 1, wenn BRK-Befehl möglich ist
^----- D decimal mode 1, wenn decimal mode aktiv
^---- I Interrupt en 1, wenn Interrupts möglich sind
^--- Z zero flag 1, wenn null BEQ (0) BNE (1)
^-- C carry flag 1, wenn Übertrag bei Add. BCC (0) BCS (1)
Interessant ist eigentlich alles, insbesondere A, X, Y und die Flags des Prozessor Status Registers. Damit kann man dann überprüfen, ob das erwartete in A, X und Y steht, also etwa wo der Schleifenzähler steht und welcher Wert in den Akku geladen wurde. Und das Status Register zeigt an, warum etwa ein Sprungbefehl nicht gezündet hat, weil das entsprechende Flag-Bit nicht gesetzt war.Nun könnten wir mit einem JSR in eine Unterroutine springen und dort die Werte auf dem LCD anzeigen lassen. Kleines Problem dabei:

JSR pfuscht uns ins Status-Register rein und ändert das N und Z Flag. Also müssen wir es vorher retten. Das geht nur auf den Stack mit

welches die Statusregister in Ruhe lässt. Unser Debug-Aufruf muss also lauten:
php
jsr debug
plp
, damit alles wieder wie zuvor ist.In der Unterroutine können wir dann das A, X und Y-Register sichern und dann nacheinander auf dem LCD ausgeben. Sogar den Programmcounter bekommen wir mit, denn der wurde von der CPU automatisch auf den Stack gepackt, als wir das JSR ausgeführt haben, denn das ist ja die Rücksprungadresse, an die die CPU wieder springen muss, wenn sie bei RTS aus der Subroutine wieder zurückkehrt.
In der Debug-Routine holen wir also vom Stack, jeweils in 1-Byte-Häppchen:
- Programm-Counter Low-Byte
- Programm-Counter High-Byte
- Status Register
Um mit folgendem Debug-Aufruf im Hauptprogramm
; --- Hauptprogramm ---
reset:
jsr ioInit
lda #$11
ldx #$22
ldy #$33
php
jsr debug
plp
folgende Anzeige zu erreichen:
ist folgender Code nötig, damit der Debug-Aufruf den Programmablauf nicht beeinflusst.
Source-Code
; Unter-Routinen fürs Debugging
; last edit: Oliver Kuhlemann (www.cool-web.de), 2020-09-06
; Aufruf:
; -------
; php ; Prozessor Status sichern
; jsr debug
; plp ; und wieder herstellen
debug:
sta regA ; Register retten
stx regX
sty regY
pla ; Rücksprungadresse (=PC) von jsr-Einsprung holen und retten
sta regPCL
pla
sta regPCH
pla
sta regP ; dann liegt P-Register von PHP oben, holen und retten
pha ; P-Register wieder auf den Stack
lda regPCH ; und dann die Rücksprungadresse
pha ; wieder auf den Stack
lda regPCL
pha
LCD_CLEAR
; --- auf LCD ausgeben: NV1BDIZC A=$xx
LCD_PRINT msgDebug1
LCD_HEX regA
LCD_CHAR #32
; --- auf LCD ausgeben: NV1BDIZC - Registerwerte S, wenn set; leer, wenn reset
lda regP
sta zptmp
pst7:
bbs7 zptmp, pst7set
LCD_CHAR #32 'leer
bra pst6
pst7set:
LCD_CHAR #83 'ASC S
pst6:
bbs6 zptmp, pst6set
LCD_CHAR #32 'leer
bra pst5
pst6set:
LCD_CHAR #83 'ASC S
pst5:
bbs5 zptmp, pst5set
LCD_CHAR #32 'leer
bra pst4
pst5set:
LCD_CHAR #83 'ASC S
pst4:
bbs4 zptmp, pst4set
LCD_CHAR #32 'leer
bra pst3
pst4set:
LCD_CHAR #83 'ASC S
pst3:
bbs3 zptmp, pst3set
LCD_CHAR #32 'leer
bra pst2
pst3set:
LCD_CHAR #83 'ASC S
pst2:
bbs2 zptmp, pst2set
LCD_CHAR #32 'leer
bra pst1
pst2set:
LCD_CHAR #83 'ASC S
pst1:
bbs1 zptmp, pst1set
LCD_CHAR #32 'leer
bra pst0
pst1set:
LCD_CHAR #83 'ASC S
pst0:
bbs0 zptmp, pst0set
LCD_CHAR #32 'leer
bra pstdone
pst0set:
LCD_CHAR #83 'ASC S
pstdone:
LCD_CHAR #32 'leer
LCD_CHAR #32 'leer
; --- auf LCD ausgeben: X=$xx
LCD_PRINT msgDebug2
LCD_HEX regX
LCD_CHAR #32
; --- auf LCD ausgeben: PC=$
LCD_PRINT msgDebug3
LCD_HEX regPCH
LCD_HEX regPCL
LCD_CHAR #32
LCD_CHAR #32
; --- auf LCD ausgeben: Y=$xx
LCD_PRINT msgDebug4
LCD_HEX regY
LCD_CHAR #32
; register wieder herstellen
lda regA
ldx regX
ldy regY
wai ; auf Cont-Taste warten
rts
msgDebug1:
ASCII NV1BDIZC A=$
BYTE 0
msgDebug2:
ASCII X=$
BYTE 0
msgDebug3:
ASCII PC=$
BYTE 0
msgDebug4:
ASCII Y=$
BYTE 0
Der Code ist eigentlich durch die vielen Kommentare selbst erklärend, aber ich erkläre in noch einmal ausführlich in folgendem Video:Video

Nach dem Debug-Aufruf werden alle Register wiederhergestellt. Ein Manko hat die Sache allerdings noch: Der Inhalt des LCD wird zerstört.
Um den wiederherstellen zu können, müsste ich in den LCD-Routinen mitschreiben, wie sich der LCD-Inhalt ändert. Ein Auslesen direkt am LCD geht ja nicht, weil wir es als Output-only Gerät konfiguriert haben. Dieses Mitschreiben ist eigentlich kein Problem, nur kostet es natürlich wieder Zeit und die Ausgabe würde dadurch langsamer werden. Eventuell werde ich das einmal angehen, wenn ich mit höheren Frequenzen fahre und auf den Sniffer (der den Takt ja auf 2 KHz begrenzt) verzichten kann.