8-Bit-Breadboard-Computer auf Basis einer 6502-CPU - Ein Bus-Sniffer mit einem Arduino/STM32

Mein Breadboard-Computer-Projekt verfügt ja nun auch über RAM und ROM, die zum CPU-Modul mit einem W65C02 Prozessor und dem Clock-Modul, das den Takt angibt, hinzukam.

Allerdings ist das Debugging doch ein bisschen umständlich. Der Adressbus wird zwar über 16 kleine rote LEDs und der Datenbus über 8 kleine grüne LEDs angezeigt und diese sind auch in Vierergruppen aufgeteilt, aber so hunderprozentig habe ich die Binär in Hex Umwandlung im Kopf noch nicht drauf. Viel schöner wäre doch eine Anzeige, die mir das gleich hexadezimal anzeigt.

Vielleicht auch mit Statusanzeige, ob gerade ein Read oder Write anliegt oder ob ein OpCode geholt wird - diese Signale liegen ja auch an der W65C02-CPU an. Und wenn ich schon weiß, dass jetzt ein OpCode geholt wird, dann könnte ich den doch auch gleich deassemblieren, sprich für den Bytewert des OpCodes den Assemblerbefehl anzeigen.

Das i-Tüpfelchen wäre natürlich noch, wenn ich so eine Art History oder Buffer hätte und in den abgelaufenen Befehlen zurück- und wieder vorblättern könnte. Dann könnte ich ein paar Befehle ablaufen lassen, das Clock Modul wieder auf manuell stellen und einfach ablesen, was die letzten Taktzyklen so passiert wäre. Ja, das würde das Debugging schon erheblich vereinfachen...

Mein 6502-Bus-Schnüffler-Breadboard (oder auch Sniffer) soll aus einem Mikrocontroller, einem Display und drei 74HC165 PISO Schieberegistern, die die Daten einsammeln, bestehen.

Zuerst wollte ich wegen der geringen Größe einen Arduino Pro Mini (Artikel dazu siehe hier) verwenden, den ich in der 5V Version da hatte.

Platzsparend ist er schon, doch liefert er halt auch 5V Pegel, die nicht zu den 3.3V-Pegeln passen, die das Nokia 5110 Display, dass ich mir wegen der guten Ablesbarkeit ausgesucht habe. Zuerst habe ich es mit einer Reihe Spannungsteiler-Schaltungen versucht:



Aber damit wollte das Display nicht so recht. Jetzt war ich mir nicht sicher, ob es an der Library lag, ich etwas falsch verkabelt hatte, das Display defekt oder falsch beschriftet war oder das Display doch höhere Ströme erwartete. Außerdem waren die vielen Widerstände und deren Verkabelung sehr fehleranfällig. Und es müssten ja noch viel mehr LEitungen verlegt werden. Ob das mit den wackeligen Widerständen gut gehen würde?

Also bin ich vom Arduino Pro Mini als Mikrocontroller wieder weggekommen, besonders als mir dann noch vom Compiler ins Gedächtnis zurückgerufen wurde, dass Arduinos ja nur 2 KB RAM haben. Das wird zu wenig sein für einen Speicher, in dem ich auch zurückblättern will.

Also kommt wieder der Tausendsassa, die Blue Pill mit STM32 zum Einsatz, auch wenn die größer ist. Dafür hat sie zehnmal soviel RAM, glatte 20 KB.

Außerdem ist die Blue Pill eine 3.3V Board, liefert also schon die richtigen Pegel für das 5110-Display (Artikel dazu siehe hier) und ist außerdem an vielen Pins 5 Volt-tolerant, die ich dann für die eingehenden 5V-Pegel vom 6502-Adress- und Datenbus benutzen kann. Sprich: ich kann mir die ganzen stromverschwendenden und störanfälligen Pegelwandler sparen.

Display Nokia 5110 LCD

Nachdem das Nokia 5110 Display verkabelt war, stellte sich heraus, dass die beim vorherigen mal eingesetze Adafruit Library nicht mit dem STM32 zusammen funktionierte. Irgendwelche Befehle, die in der Arduino SPI-Library aufgerufen wurde, kannte der STM32 nicht. Mit der Adafruit Library ließ sich der Code nicht kompilieren.

Also musste eine Alternative her. Ich stieß auf die Nokia 5110 Library von Pawel Pawel A. Hernik, die sich zwar zuerst mit ein paar Kompilierungsfehlern weigerte zu funktionieren (-5 ist halt kein uint8_t), nach ein paar kleinen Anpassungen dann aber doch gut funktionierte. Und ab jetzt war klar: das Display funktioniert. Auch mit der Blue Pill.

Ein erstes Prototyping sah auf dem Display schon mal nicht schlecht aus:



Wie man sieht, passt mit 14 Zeichen Breite auf das Display gerade so, was ich darstellen will. Und es reicht für 5 Zeilen Daten und 1 Zeile Status. Im Status will ich die Position anzeigen, an der man sich gerade befindet, denn mit dem Drehregler soll man ja zurück- und vorblättern können.

Drehregler KY

Das Drehgeber-Modul KY-040 hatte ich in dem Artikel Das Multi Function Shield mit einem Drehgeber erweitern
Diesmal will ich einen anderen Ansatz versuchen. Ich will mein Programm diesmal ereignisgesteuert mittels Interrupt-Routinen gestalten. Das heißt: die Blue Pill soll reagieren, sobald das Taktsignal sich ändert, oder wenn man am Drehgeber dreht.

Dazu werden die entsprechenden Interrupt-Handler-Routinen geschrieben und mittels attachInterrupt im Setup() definiert: Setup() { attachInterrupt(PinDrehSW, irqDrehSwitchChange, CHANGE); attachInterrupt(PinDrehCLK, irqDrehCLKDTChange, CHANGE); attachInterrupt(PinDrehDT, irqDrehCLKDTChange, CHANGE); } Weil Interrupt-Routinen möglichst schnell wieder beendet sein sollen (weil immer nur 1 Interrupt auf einmal ausgeführt werden kann), werden dort nur Variablen gesetzt, die in der Loop() abgefragt werden und ggf. das Display und die serielle Schnittstelle aktualisiert werden.

Die Abfrage erfolgt, indem ich die Pins CLK und DT des Drehgebers beobachte. Beim Drehen zeigt sich folgendes Muster beim Drehen: rechts clk 1 1 0 0 1 dt 1 0 0 1 1 links clk 1 0 0 1 1 dt 1 1 0 0 1 Man sieht das "11" der stabile Ruhezustand ist. Ist dieser (wieder) erreicht, erfolgt eine Auswertung, dazwischen speichere ich die Bits für DT und CLK in einer Variable, die ich bei "11" auswerte und - da sich das fertige Byte für links vom dem für rechts unterscheidet - erkennen kann, in welche Richtung gedreht wurde. byte drehState=0; byte drehStatePos=0; void irqDrehCLKDTChange() { byte dt = digitalRead(PinDrehDT); byte clk = digitalRead(PinDrehCLK); // bei 11 ist der Prozess zuende und wird ausgewertet if (dt == 1 && clk == 1) { if (drehState == 0b1000001 || drehState == 0b1) { // rechts ... } if (drehState == 0b10100 || drehState == 0b10000) { // links ... } drehState=0; drehStatePos=0; } else { // Bits speichern drehState |= (1 << drehStatePos) * clk; drehState |= (1 << (4+drehStatePos)) * dt; drehStatePos++; } } Leider ist mein Drehgeber nicht so präzise, wie man sich das wünschen würde und "versaut" schon mal eine sehr langsame oder eine sehr schnelle Bewegung. Umd das auszugleichen, habe ich statt der eigentlich nur gütligen Sequenzen "1000001" für rechts und "0010100" auch die aufgenommen, von denen ich meinte, dass sie als gültige Umdrehung gelten sollten.

Nachdem ich durch ein mit Textdaten gefülltes Array problemlos blättern konnte, war meine nächste Aufgabe, den prg genannten Speicherbereich mit Echtdaten zu füllen.

PISO Shift-Register-Kaskade

Der Scrollback-Buffer soll zum Schluss so groß wie möglich sein. Zuerst wähle ich erst einmal 1000 Einträge und erhalte so 4000 Bytes Speicherverbrauch, was 4K von den 20K RAM des STM32F103 belegt.

Ich habe nämlich prg so organsisiert, dass jeder Eintrag aus 4 Bytes besteht: Byte 0/1 = Adresse (16 bit) Byte 2 = Datenbyte Byte 3 = Flags Bit 0 = 6502 SYNC (1=OpCode Fetch) Bit 1 = 6502 R/W (1=read, 0=write) Die beiden Adressbytes und das Datenbyte kommen vom Bus und sind nichts anderes als das, was auch die Bus-LEDs naher der 6502-CPU anzeigen. Das Flag-Byte wird mit den Zuständen der drei Leitungen gefüllt, die ich vom 6502 zur Blue Pill führe.

Wie man auf dem Foto oben sieht, steckt die Blue Pill nur etwas über die Hälfte im Breadboard. Das war nötig, damit der Rest der Koponenten für den Sniffer noch gerade so eng auf das Breadboard passt.

Darum sind natürlich auch nicht alle Pins über das Breadboard zugänglich. Und 24 Pins für den Bus plus 3 Pins für den Drehgeber plus 5 Pins für das Display plus 3 Pins für die 6502-Flags wären zusammen 35 Pins, die die Blue Pill nicht hat. Aber wenn ich 24 Bus-Leitungen mit 74HC165-Parallel-In-Serial-Out-Shift-Registern einsammle und dann serielle an den STM32 weitergeben, dann brauche ich dafür nur 4 Leitungen.

Wie das genau geht habe ich in dem Artikel Mit dem 74HC165-Schieberegister Eingabe-Pins und Leitungen einsparen erklärt. Und wie man mehrere 165er hintereinanderschaltet, steht in meinem Artikel 74HC165-Schieberegister kaskadieren und mehr als 8 Taster verwalten.

Verkabeln muss ich jede einzelne Adress- und Datenleitung natürlich trotzdem. Nicht mit dem Mikrocontroller direkt, sondern mit den A bis H Eingängen der drei 165er, denn jeder 165er kann 8 Bit aufnehmen:



Es ist ganz ähnlich wie bei dem Schema, dass ich einmal für 24 Taster gemacht habe, nur dass statt der 24 Highs und Lows der Taster die 16 Highs und Lows des Adressbuses und die 8 des Datenbuses abgefragt werden:



Die Controller Software

Im Programm des STM32muss ich dann nur den Bitstrom entgegennehmen, um die Flags ergänzen und in die 4 Bytes pro Eintrag schreiben. Auch das soll mit einer Interrupt-Routine passieren.

attachInterrupt(Pin6502CLK, irq6502CLKChange, CHANGE); sorgt dafür, dass die entsprechende Interrupt-Routine immer dann aufgerufen wird, wenn sich die Flanke vom Taktsignal des 6502 ändert. Es ist wichtig, auf beide Flanken (Falling und Rising) zu reagieren. Denn schon im letzten Teil unseres Breadboard-Computer-Projekts beim Test des RAMs haben wir ja gesehen, dass in einem Taktzyklus gleich zwei Dinge passieren: Schreiben in das RAM und holen des nächsten Befehls.

Doppelte Einträge pro Taktzyklus fischt die Software heraus und verwirft sie. Dann schreibt sie einen 4 Byte langen Eintrag an die aktuelle Schreibposition in PRG: die zwei Adress-Bytes, ein Datenbyte und ein Byte mit Flags: ob die Read/Write und die Sync-Leitung High oder Low waren.

Mit setzen der globalen Variablen doShowUpdate auf true weiß dann die Loop()-Routine, dass sie die Anzeige aktualisieren soll.

Dann werden die letzten 5 Einträge in PRG angezeigt, also immer die aktuellsten. Es sei denn, man hat am Drehregler nach links gedreht und die Leseposition verändert, dann werden die entsprechend der Leseposition gewählten Einträge angezeigt.

Das hat den Vorteil, dass man den Takt einfach mal eine Weile laufen lassen kann, dann pausiert (auf manuellen Takt schaltet) und dann in aller Ruhe Im Scroll-Back-Buffer nachgucken kann, was die letzten Taktzyklen passiert ist. Ist man damit fertig, drückt man den Drehgeber einmal und springt damit wieder ans Ende. Will man den Buffer löschen, drückt man den Geber für mindestens eine Sekunde.

Nachdem das Programm soweit fertig war, habe ich alles übrige STM32-RAM in diesen Buffer gesteckt. Immerhin sind 14'000 Bytes bzw. 3'500 Einträge dabei rumgekommen: uint8_t prg [14000]; RAM: [==========] 96.2% (used 19704 bytes from 20480 bytes) Flash: [=== ] 32.9% (used 43160 bytes from 131072 bytes) Die OpCode-Tabelle, die Fonts für das Nokia 5110-Display und alles weitere, das ging, habe ich natürlich in den Flash-Speicher per PROGMEM-Direktive ausgelagert.

Außer auf dem Display wird der Takt bzw. Programmverlauf auch auf die serielle Schnittstelle des STM32 ausgegeben. Über den USB-Port kann man dank STM32duino-Bootloader dann das, was auf dem LCD ausgegeben werden, noch einmal am PC mitschneiden.

Für unseren SRAM-Test vom letzten mal sieht das dann so aus. Der Aufbau ist: Adresse, Datenbyte, r/w (read/write) und ggf. OpCode: 8000 A9 r LDA# 8001 01 r 8002 8D r STA 8003 00 r 8004 10 r 1000 00 W 1000 01 W ... 8023 A9 r LDA# 8024 80 r 8025 8D r STA 8026 07 r 8027 10 r 1007 00 W 1007 80 W ... 8028 AD r LDA 8029 00 r 802A 10 r 1000 01 r ... 803D AD r LDA 803E 07 r 803F 10 r 1007 80 r 8040 4C r JMP 8041 00 r 8042 80 r Geht das Sync-Signal des 6502 auf High (was wir ja auch an der LED dort erkennen können), dann bedeutet dies, dass gerade ein OpCode geholt wird. In diesem Taktzyklus wird für das Datenbyte der entsprechende OpCode aus einer Tabelle gesucht und angezeigt, z. B. LDA# für 0xA9. Ein Mini-Deassembler sozusagen.

Da auf dem LCD nur 4 Zeichen für einen OpCode frei sind, besteht ein OpCode aus 3 Zeichen für den Befehl gefolgt von einem Zeichen für die Adressierungsart. Dabei bedeuten an 4. Stelle: So kann man das Programm schon ganz gut lesen und nachvollziehen, was im Innern des W65C02 vor sich geht.

Das Resultat



Wie man sieht, ist das Breadboard picke-packe-voll und nicht ein Steckkontakt ist mehr frei. Das ist mir nur gelungen, indem ich die Blue links über das Breadboard schauen lasse. Die verbundenen Anschlüsse des STM32 reichen aber für die Aufgabe.

Die meiste Arbeit haben natürlich wieder Adress- und Datenbus gemacht, natürlich bin ich bei den Farben orange für den Adressbus und gelb für den Datenbus geblieben. Beide Buse gehen in die 74HC165er, der rechte ist für den Datenbus, die beiden linken für den Adressbus. Der rechte 165er gibt seine Daten seriell an den mittleren, der an den linken, und der an den STM32 weiter.

Außerdem benötigt der STM32 noch die Signalleitung Takt, Sync und RW vom 6502 (in blau), die unten links in den STM32 laufen. Die schwarzen Kabel gehen an das Display und den Drehgeber (beides 3.3V-Pegel). An den 165ern ist noch eine Pinreihe frei, so dass ich von hier die Buses ggf. weitergeben kann an das Breadboard rechts vom Sniffer, das noch frei ist.

Übrigens: wenn der Breadboard-Computer über die Blue Pill am USB-Port des PC hängt, dann wird er darüber versorgt und dann sollte die externe 5V-Versorgung aus sein bzw. umgekehrt. Es sollte nur eine Spannungsquelle gleichzeitig aktiv sein.

Den Aufbau des Sniffer-Moduls erkläre ich noch einmal in folgendem Video. Zudem führe ich es natürlich auch einmal vor:



Im nächsten Teil der Reihe erweitern wir unseren Breadboard-Computer um unser erstes Ausgabegerät: 8 LEDs.