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: Als nächstes will ich etwas halbwegs sinnvolles programmieren, dass die bis jetzt durch den Breadboard-Computer zur Verfügung gestellten Geräte (Tastatur, LED-Ausgabe, 7-Segment-Anzeige und LCD Ausgabe) nutzt. Mir kam ein kleines Spielchen in den Sinn, bei dem der Nutzer den auf der LED-Ausgabe angezeigten Binärwert im Kopf nach Hexadezimal umrechnen und eingeben soll. Außerdem soll er noch einen Dezimal nach Hexadezimal / Binär, sowie Hexadezimal zu Dezimal / Binär -Konverter implementieren.

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: Was wir vom Stack genommen haben, müssen wir in umgekehrter Reihenfolge auch wieder drauf packen, denn sonst weiß die CPU die Rücksprungadresse nicht mehr und springt ins Irgendwo zurück.

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



Jetzt kann ich an jeder beliebigen Stelle im Hauptprogramm ein PHP / jsr debug / PLP einbauen und bekomme dann eine Momentanzeige des Prozessor-Status. Mit einem Knopfdruck auf den Continue-Taster auf dem Breadboard wird das Programm dann weiter ausgeführt. Praktisch zum Beispiel in einer Schleife, um zu sehen, ob die Register hier richtig zählen. Jeder Knopfdruck führt zum nächsten Schleifendurchlauf.

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.

Ausblick

Im nächsten Teil geht es um HArdware-Debugging. Aus irgendeinem Grund werden meine Tasteneingaben nicht mehr angenommen. Ich muss der Sache auf den Grund gehen, bevor ich weiter an meinem Hexadezimal / Dezimal / Binär Konvertierungs-Tool und Spielchen programmieren kann.