8-Bit-Breadboard-Computer auf Basis einer 6502-CPU - Integration eines CoProzessors auf Arduino-Basis
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
Denn: Wenn man das Spiel häufiger spielt, wird man die angezeigten Zahlen, zumindestens die ersten, bald auswendig kennen, denn es sind immer die gleichen. Und eigentlich soll beim Spiel ums Kopfrechnen und nicht ums Auswendiglernen gehen.
Ein Pseudozufallszahlengenerator wäre also etwas tolles, damit immer wieder andere Zahlen kommen. Der wäre auch für zukünftige Programme, insbesondere Spiele nützlich. Doch woher sollen diese Zahlen kommen?
Ich habe mir so einige Gedanken gekommen, und habe dann als praktikabelste Lösung einen an den 6522 angebundenen CoProzessor auserkoren, der über den Datenbus gesteuert werden soll: die oberen 4 Bits sollen eine von 16 Funktionen auswählen und die unteren 4 Bits sollen die Daten übertragen, dann aber halt nur mit 4 Bit Breite. Für die Zufallszahlen kein Problem, ich kann diese Funktion ja einfach 2x hintereinander abrufen und dann die zwei mal 4 Bits zu 8 Bits (oder auch 12 oder 16 Bits) zusammenfügen, wenn ich größere Zahlen brauche.
Als CoProzessor soll es ein Atmel ATmega328P sein, den ich natürlich erst einmal entsprechend programmieren muss. Wie ich das genau gemacht habe, habe ich in diesem Arduino-Projekt näher beschrieben. Die Grundlagen, einen Atmel ATmega stand-alone, also ohne Arduino-Platine zu benutzen, finden sich auf einer weiteren Webseite.
Das ergibt einen schön kompakten Aufbau auf unserem Breadboard. Und da sich der ATmega328P und dessen zugehörigen Sensor-Module bei 5 Volt recht wohl fühlen, ergibt das ein passendes Team.
Programmiert und getestet habe ich den ATmega328P auf einem Arduino Uno-Board, wo er sehr zu meiner Zufriedenheit funktioniert hat.
Aufbau auf dem Breadboard
Jetzt muss ich die Schaltung nur noch auf die Breadboards meines 6502 BBC umbauen.Wo was angeschlossen werden muss ergibt sich aus dem Source-Code der CoProzessor-Programmierung.
#define PinData0 2
#define PinData1 3
#define PinData2 4
#define PinData3 5
#define PinData4 6
#define PinData5 7
#define PinData6 8
#define PinData7 9
#define PinFBA 10 // 433 MHz Fernbedienung
#define PinFBB 11
#define PinFBC 12
#define PinFBD 13
#define PinFBVT A1
#define PinDHT A2 // DHT11
#define PinLED A3 // Aktivitäts-LED
#define PinSDA A4 // RTC
#define PinSCL A5
Zuerst einmal baue ich den ATmega328P mit Quartz und Kondensatoren auf und schließen ihn an GND (schwarzes Kabel) und 5V (rotes Kabel) an. Auch die Aktivitäts-LED an A3 (Pin 26, oranges Kabel) kann schon einmal angeschlossen werden.Dann folgt der Datenbus. Da ich D0 und D1 des ATmega für die serielle Schnittstelle freihalten möchte, bleiben die frei und ich verbinde die 8 Daten-Leitungen D2...D9 (Pin 4, 5, 6, 11, 12, 13, 14, 15) des CoProzessors mit den 8 Datenleitungen PB0...PB7 (Pin 10...17) des Ports B am 6522 (violette Leitungen).
Das sieht dann schon einmal so aus:

Damit könnte ich schon die Zufallszahlen auslesen. Ich mache einen kurzen Test, ob die LED aus bleibt, wenn der BBC an ist, was korrekt funktioniert - die Standardeinstellung für PB0...PB7 ist High, damit ist die Funktion 15 gewählt, und die tut nichts (Idle-Mode). Als ich eine Datenleitung herausziehe, wird diese floating, das zugehörige Bit kippt ab und an und wählt dadurch eine Funktion aus, was den CoProzessor dazu bringt, Daten zu liefern und die LED einzuschalten. Auch das funktioniert. Erster Test bestanden.
Wo ich schon beim Hardware-Aufbau bin, bringe ich auch noch die anderen CoProzessor-Komponenten auf das Breadboard und verbinde sie:

Mit deren Programmierung und Test beschäftigen wir uns die nächsten Male. Aber ich wollte sie schon mal aufbauen. Als kleiner Ausblick, von links nach rechts sind dies:
- Echtzeituhr für Daten und Uhrzeit mit großer Bufferbatterie (2x AA)
- 433 MHz-Empfänger für eine Fernbedienung mit 4 Knöpfen A...D
- DHT11-Sensor für Temperatur und Luftfeuchtigkeit
Mit den Sensoren sind die 16 Funktionen belegt und zwar nach folgender Schnittstellendefinition:
Funktionen:
0 0b0000 = Zufallswert (4 Bit)
1 0b0001 = 433 MHz Fernbedienung DCBA (4 Bits, gedrücktes gesetzt)
DHT11 Temp (Messbereich: 0...50°C, Auflösung 1°C (+/- 2°)
2 0b0010 = DHT11 Temp High (0...50) = 6 Bits
3 0b0011 = Temp Low
DHT11 Luftfeuchte (Messbereich: 20...90% RH, Auflösung 1% (+/- 5%)
4 0b0100 = DHT11 Fcht High (0...90) = 7 Bits
5 0b0101 = Fcht Low
RTC DS1307
6 0b0110 = YYYY Jahr High 1900 + 0...255
7 0b0111 = YYYY Low
8 0b1000 = MMMM Monat (0...12) = 4 Bits
9 0b1001 = DDDD Tag (0...31) = 5 Bits
10 0b1010 = DHHH Stunde (0...23) = 5 Bits
11 0b1011 = HHMM Minute (0...59) = 6 Bits
12 0b1100 = MMMM
13 0b1101 = xxSS Sekunde (0...59) = 6 Bits
14 0b1110 = SSSS
15 0b1111 = idle (Normalzustand 6522 Port)
Software zur Abfrage von Zufallszahlen
Heute wollen wir uns nur mit der Funktion Pseudozufallszahlgenerator beschäftigen, die anderen Funktionen werden in einem späteren Teil behandelt. Aus der Tabelle lesen wir ab, dass dies Funktion 0 ist.Wir müssen dazu erst einmal für Port B des 6522 die Datenrichtungen angeben, also welche der acht Datenleitungen für Input und welche für Output verwendet werden. Das geht ganz ähnlich wie wir es schon bei der Tastatur programmiert haben. Es reicht, wenn wir das einmal am Anfang beim ioInit machen. Zur Sicherheit setzen wir alle Bits noch einmal auf High (was eigentlich Standard sein sollte, aber man weiß ja nie), damit der CoProzessor die Funktion 15 für Idle erhält und nicht unnötigt die ganze Zeit irgendwas berechnet oder abfragt.
; 6522 Port B, Data Direction Register setzen für CoProzessor
lda #%11110000 ; 4 Bit OUT = Funktion, 4 Bit IN = Data Read
sta VIA_DDRB
lda #$FF
sta VIA_PORTB ; Idle
Zur Abfrage einer Zufallszahl müssen wir die Funktion (also die oberen vier Bits des Ports B des 6522) auf 0 setzen. Das erkennt der CoProzessor und meldet sich so schnell wie er kann mit einem Ergebnis, der Zufallszahl (von 0 bis 15) in der unteren vier Bits zurück, die wir dann via 6522 auslesen können:
; VIA Port B, Funktion 0 (oberen 4 Bits) -> Zufallszahl abrufen
lda #%00000000
sta VIA_PORTB
; dem CoProzessor ein bisschen Zeit gönnen zum Antworten
NOP
NOP
NOP
NOP
; und Ergebnis lesen (in die 4 unteren Bits)
ldy VIA_PORTB
; fertig, der CoProzessor darf wieder faulenzen
lda #%11110000
sta VIA_PORTB
Das sollte auch schon die ganze Magie sein.Als ich das Programm teste, fällt mir auf, dass die Zufallszahlen immer "FFF" lauten. Nach ein bisschen Nachdenken komme ich darauf, dass der CoProzessor ein paar Sekunden nach dem Einschalten bzw. Reset braucht, und noch nicht bereit ist zu liefern. Das mit dem Verbinden der 6502-Reset-Leitung an den CoProzessor ist also kontraproduktiv. Also wird die Leitung gerupft und der Reset-Pin des CoProzessors über einen 10 KOhm Widerstand permanent auf High gezogen. Damit ist er ein paar Sekunden nach Einschalten des BBC bereit und der Reset beeinflusst ihn nicht weiter.
Als nächstes frage ich 3 Zufallszahlen hintereinander ab und lasse diese auf den drei 7-Segment-Anzeigen in Hexadezimal ausgeben, also von 0 bis F. Bei meinen Tests funktioniert die Anfrage schon ganz gut, nur in seltenen Fällen wird FFF angezeigt. Da scheint noch etwas nicht ganz optimal zu laufen.
Ich vermute den Fehler darin, dass ich dem CoProzessor nicht genügend Zeit lasse, zu antworten und bereits vor dessen Antwort den 6522 abfrage und dann nur die Standardwerte (alles High) bekomme. Also experimentiere ich mit unterschiedlich langen Wartezeiten zwischen dem Abschicken der Funktionsnummer und dem Lesen des Ergebnisses.
Es ergibt sich, das seltsamerweise etwa alle 2 Sekunden für ca. 200 ms keine Erkennung und Rückgabe erfolgt, was ich gut an der nicht leuchtenden LED neben dem CoProzessor erkenne. Die sollte eigentlich durchgehend leuchten, wenn durchgehen abgefragt wird.
Als erstes habe ich den ATmega im Verdacht. Habe ich irgendwie dort vielleicht etwas falsch programmiert? Also teste ich den CoProzessor noch einmal im Arduino Uno und Ausgabe über die serielle Schnittstelle. Doch dort gibt er flott, ständig und ohne Unterbrechungen Zufallswerte aus. Daran liegt es also nicht.
Ich versuche es mit anderen Pausen nach der Funktions-Abfrage und damit, jedesmal explizit und neu die Data Direction anzugeben. Das nützt allerdings alles nichts. Das Phänomen bleibt bestehen.
Um die Pausen besser angeben zu können, programmiere ich noch eine Funktion delayMs, die abhängig von der im Source-Code angegeben Taktfrequenz soundso viele Millisekunden wartet. Aber egal, wie lang die Pause ist, das Phänomen bleibt bestehen.
Ob wohl der 6522 schuld ist? Führt der alle X Zyklen oder X Sekunden einen Interrupt aus und ignoriert sonst alles? Vielleicht habe ich etwas im Data Sheet zum 6522 überlesen? ICh beschließe, dass Problem erst einmal zu umgehen. Für das Spielchen kann ich eh keine Zufallszahl 0 gebrauchen, das wäre zu einfach zu "berechnen", wenn alle LEDs aus sind. Ich beschließen also, die Lösungssuche auf später zu verschieben; es sind ja noch ein paar Sensoren mehr abzufragen.
Kümmern wir uns zuerst um unser Kopfrechenspiel und dass dieses gescheite Zufallszahlen abfragt und nicht immer wieder die selben Werte. Dazu müssen wir den Code nur geringfüfif anpassen:
binToHexScore: equ $1000
binToHexRichtig: equ $1001
binToHexEingabe: equ $1002
zufallszahl: equ $1003
startBinToHex: ; Spielchen für den User -------------------------------------------
; eine Zufallszahl wird geholt und in LED angezeigt und in HEX umgerechnet
; der User gibt sein Rechenergebnis ein, ist es richtig, gibt es einen
; Punkt, ansonsten endet das Spiel
stz binToHexScore;
startBinToHex2a:
LCD_CLEAR ; LCD löschen (Makro)
LCD_PRINT msgEingabeBinToHex ; Makro-Aufruf, siehe macros.inc
LCD_POS #26 ; LCD-Cursor positionieren (Makro)
startBinToHex2:
; ldy binToHexScore
; lda zufall, y ; Zufallszahl holen und auf LED anzeigen
neueRnd:
jsr getRandom ; Zufallszahl holen (wird im Akku abgelegt)
cmp #0
beq neueRnd ; 0 kann eine ungütlige Zufallszahl sein, außerdem will ich die eh nicht!
sta zufallszahl
jsr getRandom ; Zufallszahl holen (wird im Akku abgelegt)
cmp #0
beq neueRnd ; 0 kann eine ungütlige Zufallszahl sein, außerdem will ich die eh nicht!
asl A
asl A
asl A
asl A
ora zufallszahl
sta LED
sta binToHexRichtig
eingabeBinToHex:
...
getRandom: ; liefert eine Zufallszahl zwischen 0 und 15
; ACHTUNG! Die Zufallszahl 0 kann ungültig sein, dann
; einfach noch einmal eine neue aufrufen
phy
; VIA Port B, Funktion 0 (oberen 4 Bits) -> Zufallszahl abrufen
lda #%00000000
sta VIA_PORTB
; dem CoProzessor ein bisschen Zeit gönnen zum Antworten
lda #2
jsr delayMs ; Akkuwert lang in Millsekunden warten
; und Ergebnis lesen (in die 4 unteren Bits)
ldy VIA_PORTB
; fertig, der CoProzessor darf wieder faulenzen
lda #%11110000
sta VIA_PORTB
tya ; Rückgabe im Akku
ply
rts
Die Funktion getRandom packe ich in das Include-File math.inc, das werde ich sicher noch öfters brauchen. Ansonsten macht der Code nicht viel mehr als zweimal eine 4-Bit-Zufallszahl abzurufen und dann in 8 Bit zu kombinieren, um sie als Rätsel anzuzeigen.Und mit dieser kleinen Anpassung habe ich ein Spielchen, dass immer andere Zahlen liefert und herausfordernd bleibt, auch wenn man es länger spielt. Bis man schließlich ein Geek im Hexadezimalen Kopfrechnen ist.
Video
Über meine Tests, die Fehlersuche und den abschließenden Funktionstest mit dem neuen Spiel habe ich natürlich auch wieder ein Video gemacht: