Andre Adrian, DL1ADR, 2026-02-28
Der 6502 ist der Mikroprozessor im MOS Technology KIM-1,
Apple 1, Apple 2, Commodore PET und vielen anderen. Grant Searle
hat unter Grant's
8-chip (or 7-chip) 6502 computer einen Breadboard 6502
Computer vorgestellt. Ben Eater hat mit Build a 6502 computer eine
Serie von YouTube Videos über den Bau eines 6502 Computer auf
Breadboards herausgebracht. Die nötigen Bauteile bietet er als Kit
an für einen vernünftigen Preis. Leider ruiniert Versand aus USA
und Zoll den guten Preis. Alle nötigen Bauteile gibt es immer noch
neu produziert bei Mouser.
Einige Bauteile gibt es bei Reichelt.
Mein minimal 5-chip 65C02 Computer benutzt nur immer noch
produzierte Bauteile und ist eher ein "Searle" als ein "Eater".
Ähnliche 65C02 Computer mit PCB (gedruckte Platine) gibt es z.B.
als 6502
Single Board Computer von Jeff Tranter. Eine gute 6502
Übersichts-Seite MOS
Technology 6502 ist von Hans Otten.
Vor der Software kommt die Hardware. Ein (klassischer)
Mikrocomputer benötigt die Bauteile CPU, ROM, RAM, IO und etwas
"glue logic". Die CPU U1 enthält das Rechenwerk
und die Ablaufsteuerung.
Das Rechenwerk des 65C02S ist bescheiden. Maximal können 8 Bit
Werte addiert und subtrahiert werden. Für 1976 war das eine
beachtliche Leistung für einen sehr guten Preis von US$ 25. Das
ROM U3 ist der Nur-Lese Speicher. Dieser Speicher verliert bei
Spannungsausfall seinen Inhalt nicht. Verwendet wird ein EEPROM.
Dieser Nur-Lese Speicher läßt sich einfach in einem
Programmiergerät wie XGecu TL866II oder dem Nachfolger XGecu T48
programmieren, d.h. mit Inhalt versorgen. Das RAM U2 ist der
Schreib-Lese-Speicher. Nach dem Einschalten enthält das RAM
zufällige Werte. Die CPU kann mit Hilfe des Monitorprogramms im
ROM und der IO sinnvolle Programme und Daten in das RAM laden und
ausführen. Die IO U4 65C51 verbindet den Mikrocomputer mit dem
"Rest der Welt". Dies ist ein größerer Computer mit einer USB
Schnittstelle. Zuerst werden die parallelen Daten von dem internen
Datenbus D0 bis D7 in asynchrone serielle Daten nach der RS232
Schnittstellen-Norm umgesetzt. Im zweiten Schritt werden durch
einen Seriell-nach-USB Konverter die Daten von RS232 nach USB
übersetzt. Es ist ein Seriell-nach-USB Konverter mit TTL Pegel
(USB to UART TTL converter) nötig. Es gibt auch Konverter mit
+12V/-12V RS232 Pegel. Diese zerstören den 65C51. Das IC auf dem
Seriell-nach-USB Konverter kann ein CP2102, CP2104, CH340 oder
FT232 sein. Hat der Seriell-nach-USB Konverter einen CTS
Anschluß kann mit RTS/CTS Hardware Handshake der download
vom "großen" Computer zum 6502 Computer fehlerfrei mit maximaler
Geschwindigkeit erfolgen, und die ist RICHTIG schnell.
Nach den wichtigen Bauteilen CPU, ROM, RAM, IO sind weitere
Bauteile nötig. Die Ablaufsteuerung in der CPU braucht ein
"Metronom" für ordentliche "Schritt für Schritt" Ausführung. Der
Quarzoszillator X1 liefert 1843200 Taktschläge pro Sekunde, oder
1.8432 MHz. Für uns Menschen ist eine solche Frequenz nicht
hörbar, für einen (heutigen) Computer ist sie langsam. Die CPU
braucht den Takt für die internen Abläufe. Der IO Baustein braucht
den Takt, genauer einen "heruntergeteilten" Takt, für die
asynchrone Schnittstelle. Dieser Takt ist 115200 Taktschläge pro
Sekunde, oder 115.2 kHz. Im "größeren" Computer wird auch ein Takt
von 115.2 kHz erzeugt. Nur bei gleichem "Taktmaß" auf beiden
Seiten können die seriellen Daten sinnvoll übertragen werden.
Um die einzelnen Bauteile "zusammenzubinden" ist "glue logic"
nötig. Das IC U5 74HC00 ist ein vierfach NAND Gatter und gehört zu
den einfachsten Logikbausteinen überhaupt. Zuerst soll die "Chip
Select (CS)" Logik besprochen werden. Die CPU 65C02 hat die
Adressleitungen A0 bis A15. Mit diesen 16 Leitungen lassen sich
ein "Postfach" aus insgesamt 65536 Postfächer oder Speicherstellen
auswählen. Diese "Postfächer" sind unterschiedlichen "Häusern"
zugeordnet. Der RAM hat die Speicherzellen 0 bis 32767 dezimal
oder $0000 bis $7FFF hexadezimal. Der IO Baustein hat die
Speicherzellen 32768 bis 49151 oder $8000 bis $BFFF. Der ROM hat
die Speicherzellen 49152 bis 65535 dezimal oder $C000 bis $FFFF
hexadezimal. Achtung: manche "Mieter" haben mehrere Postfächer.
Der IO Baustein hat nur 4 Mieter (Speicherzellen), der ROM
Baustein hat nur 8192 Mieter. Nur beim RAM-Baustein gibt es hinter
jedem Postfach einen anderen Mieter.
| Speicherbereich |
benutzt |
A15 |
A14 |
Baustein |
Methode |
| $0000..$7FFF |
$0000..$7FFF |
0 |
X |
RAM |
A15 an /CS Eingang |
| $8000..$BFFF |
$8000..$8003 |
1 |
0 |
IO |
A15 an CS1 Eingang, A14 an /CS2 Eingang |
| $C000..$FFFF |
$E000..$FFFF |
1 |
1 |
ROM |
A15 und A14 an NAND U5A. NAND Ausgang an /CE
Eingang |
Die beiden "wichtigsten" Adressleitungen A15 und A14 werden
benutzt um den Speicher auf die "Häuser" aufzuteilen. Ein "X"
bedeutet es ist egal ob 0 oder 1 anliegt.
Die NAND Gatter U5B, U5C und U5D sind "Übersetzer" von einer Art
der Ablaufsteuerung auf eine andere Art der Ablaufsteuerung. Zum
Hintergrund: Zu gewissen Zeiten ist die CPU mit sich selbst
beschäftigt und will nicht das Daten gelesen oder geschrieben
werden. Zu anderen Zeiten will die CPU lesen (read) oder schreiben
(write). Die Anschlüsse /OE am ROM und am RAM bedeuten "wenn
dieser Anschluß 0 ist, dann will die CPU lesen". Der Anschluß /WE
am RAM bedeutet "wenn dieser Anschluß 0 ist, dann will die CPU
schreiben". Wenn beide Anschlüsse /OE und /WE 1 sind, dann ist die
CPU intern beschäftigt. Die CPU 65C02 und die IO 65C51 benutzen
eine andere Logik. Wenn der Anschluß PHI0 (Pin 37 der CPU) auf 0
ist, dann ist die CPU intern beschäftigt. Bei 1 erfolgt lesen oder
schreiben. Der Anschluß RWB (Pin 34 der CPU) ist für lesen auf 1
und für schreiben auf 0. Die drei NAND Gatter übersetzen von PHI0
und RWB Logik auf /OE und /WE Logik. Ein klassisches Beispiel wie
das gleiche Ziel über unterschiedliche Wege erreicht werden kann.
Der IO Baustein 65C51 hat zwei Anschlüsse zur freien Verwendung.
Bemerkung: Die 65C51 Eingänge /DCD und /CTS müssen auf 0 liegen,
damit die serielle Schnittstelle arbeiten will. Früher, im letzten
Jahrtausend, wurden diese Anschlüße von einem Modem angesteuert.
Der Eingang /DSR liegt an einem Taster, der Ausgang /RTS liegt an
einer LED. Auch diese Anschlüße gingen früher an ein Modem. Die
Anschlüße RxD und TxD sind für Empfangen und Senden der
asynchronen seriellen Daten unbedingt nötig. Weiterhin der GND
Anschluß, damit die Stromkreise geschlossen sind. Der
Seriell-nach-USB Konverter liefert (aus dem großen Computer) eine
5V Versorgungsspannung für den 65C02 Computer. Über den /DTR
Anschluß wird später geschrieben.
Alle Bauteile lassen sich auf zwei Breadboards mit je 63
Anschlußreihen aufbauen. Dabei werden auf dem ersten Breadboard
die Bauteile U1 CPU 65C02, U5 NAND 74HC00, U2 RAM 62256 und U3 ROM
28C64 aufgesteckt. Auf das zweite Breadboard kommen die restlichen
Bauteile U4 65C51, X1 Quarzoszillator 1.8432 MHz, TTL-USB
Konverter und Kleinteile wie Taster und LED. Die Widerstände und
Kondensatoren werden in der Nähe der entsprechenden
Bauteileanschlüsse eingesteckt. Achtung: Der 65C51 muss von
WDC (Western
Design Center) sein. Nur dieser 65C51 hat bestimmte
Features oder Bugs die bei den Programmen gebraucht werden.
Nach dem Aufbau und vor dem ersten Programm muß das EEPROM mit
dem Monitor-Programm programmiert werden. Ausgangspunkt des BIOS
ist der Wozmon, das Monitor-Programm des Apple 1. Dokumentiert
wird Wozman im Apple-1
Operation Manual von 1976. Ben Eater hat Wozmon an den 65C51
angepasst, ich habe den "Eater Wozmon" an die andere Adresse des
65C51 angepasst und nutze Interrupt für schnellen Empfang der
Daten auf der seriellen Leitung. Weiterhin gibt es eine
einfache Trace Funktion für den BRK opcode. Im Moment wird nur das
Programmieren des EEPROM vorgestellt. Der Monitor wird später im
Detail besprochen, besonders die Anpassungen wegen dem
fehlerhaften WDC 65C51.
Zuerst einen EEPROM Programmer kaufen. Ich habe den XGecu TL866II.
Dann die Programmiersoftware
installieren. Nun wird eine Datei mit dem EEPROM Inhalt gebraucht
und natürlich ein EEPROM. Bei Reichelt gibt es unter Artikel-Nummer 28C64-150
ein 8 KByte EEPROM im 28-poligen DIL Gehäuse mit Herstellernummer
AT28C64B-15PU. Das ist ein Atmel EEPROM mit 150 ns Zugriffszeit.
Diese Zugriffszeit genügt bei meinem 65C02 Computer für maximal 3
MHz CPU Takt.
Nach Start des Xgpro Programmes zuerst das IC aussuchen mit
Menu|Select IC(S)|Search and Select IC(S). Dann 28C64 oder 28C64B
in das "Search Device" Feld eingeben. Zum Programmieren zuerst die
Datei auswählen mit Menu|File(F)|Load File(O). Die Datei iwozmon.bin ist binary. Auch die anderen
Defaults passen. Zum Programmieren Menu|Device(D)|Program(P)
auswählen. Eine erfolgreiche Programmierung zeigt das folgende
Bildschirmfoto:

Für Neugierige: Hier ist mein iwozmon.asm,
das wozmon Assembler Listing und iwozmon.lst,
Assembler und Maschinesprache Listing. Die Binärdatei ist 8 KByte
groß um alles so einfach als möglich zu gestalten.
Hier das Hex Listing meines iWozmon für den 65C02 5-chips
Computer:

Mein Wozmon hat drei zusätzliche Funktionen:
GETC = $FEC1 ;Hole ASCII Zeichen von Tastatur nach Register A
KEYBD = $FED5 ;Melde "Zeichen verfügbar" in Register A
PRCRLF = $FED8 ;Carriage return, Line feed ausgeben
Natürlich gibt es auch die im Apple-1 Operation Manual
beschriebenen Funktionen:
RESET = $FF00 ;Monitor Kaltstart
GETLINE = $FF1F ;Monitor entry point
PRBYTE = $FFDC ;Register A als zweistellige hexadezimale Zahl
ausgeben
PRHEX = $FFE5 ;untere 4 Bit von Register A als einstellige
hexadezimale Zahl ausgeben
ECHO = $FFEF ;Register A als ASCII Zeichen ausgeben
Der iWozmon benötigt die Bereiche $00E6 bis $00EA und $0200
bis $0242 für Interrupt Empfang von seriellen Daten. Weitere
Bereiche werden für den iWozman Monitor benötigt. Machen diese
Funktionen schon ein Betriebssystem? Naja, soviel Betriebssystem
wie man für 400 Bytes auf einem 65C02 halt bekommt. Wir können
Programme entweder in Maschinensprache von Hand eintippen oder im
"Wozmon Format" per Copy and Paste laden und wir können Programme
starten. Die Funktionen KEYBD, GETC und ECHO stellen die
einfachste Hardware-Abstraktion da.
Der Opcode BRK ist eine Debug-Hilfe. Erreicht das Programm ein
BRK, dann werden Stackpointer, Programmpointer, PSR (Programm
Status Register) und Register A, X, Y ausgegeben. In der aktuellen
Version kann das Programm nach BRK nicht fortgesetzt werden.
Das kleine Programm ab Adresse $300 lädt $11 in Register A, $22 in
X und $33 in Y. Der Wert $00 in Adresse $306 ist ein BRK. Die
Ausgabe nach ! ist der Wert des Stackpointer 01F9, dann die
Returnadresse als zwei einzelne Bytes 03 08, d.h. $0308, dann PSR
und zuletzt die Register A, X, Y.
Bemerkung: BRK ist in einem Byte kodiert, aber bei der Ausführung
wird er als 2-Byte Opcode behandelt. Der BRK Opcode liegt auf
$306, aber die Returnadresse ist $308.
Das test_trace Programm ist:
.ORG $300
LDA #$11
LDX #$22
LDY #$33
BRK
; BRK increments PC by 2
BRK
.END
Die Quelltextdatei ist test_trace.asm
.
Das erste Programm ist natürlich das Monitorprogramm im ROM. Das hier vorgestellte erste Programm wird selbst eingetippt und gestartet, als 65C02 Computer Version von "Hello, world". Wir rollen das Problem vom Ende auf. Wird brauchen eine Terminalemulation auf dem "großen Computer", bei mir ein PC mit MS-Windows 11, der die elektromechanische ASR-33 aus den 1970er Jahren ersetzt. Die Einstellung ist 115200 Bit pro Sekunde asynchron, 7 Datenbits, keine Parität, 1 Stopbit. Ich benutze TeraTerm. Nach der Installation muß TeraTerm passend für den 65C02 Computer konfiguriert werden. Zuerst Menu|Setup|Serial Port ...

Der richtige Port, wie z.B. COM3, hängt von verschiedenen
Bedingungen ab. COM1 und COM2 sind für die (heute meist nicht mehr
vorhandenen) "echten" RS232 Ports des Mainboard reserviert. Der
erste je vom Computer erkannte Seriell-nach-USB Konverter ist
COM3. Weil der "große" Computer viel schneller ist als der 65C02
Computer, ist es sinnvoll bei Transmit delay line einen kleinen
Wert einzugeben. Die Verzögerung macht den Download von großen
Programmen wie BASIC Interpreter in den 65C02 Computer langsamer,
aber auch weniger fehleranfällig.
Nun Menu|Setup|Terminal ...

Zum Abschluß Konfiguration speichern mit Menu|Setup|Save Setup
...
Bei korrekter Konfiguration und erfolgreicher Verbindung
erscheint nach Druck auf Reset Taste am 65C02 Computer der Monitor
Prompt:
Der Prompt ist ein bescheidener "\". Die Reset Taste führt auch einen kleinen Hardware Test aus. Solange die Reset Taste gedrückt ist, soll die LED D1 in der Nähe von 65C51 aufleuchten. Doch nun endlich zum ersten Programm.
Das erste Programm entspricht dem Test Programm auf Seite 2 des Apple-1 Operation Manual. Von San Bergmans gibt es eine ausführliche Wozmon Bedienungsanleitung. Der fettgedruckte Text wird vom Benutzer getippt und mit "Return" abgeschlossen. Achtung: Großschreibung ist wichtig. Bemerkung: Im TeraTerm Fenster ist ALT-C Copy und ALT-V Paste. Der kursivgedruckte Text ist die Antwort vom Computer.0400: A9 20 20 EF FF 1A 10 FA 80 F6
0400: A9
0400 R
0400: A9
Die Ausgabe sind die druckbaren ASCII Zeichen, immer wieder:

0300: AD 01 80 29 40 D0 04 A9 0B 80 02 A9 03 8D 02 80 80
EE
0300: AD
0300 R
0300: AD
Wird nun die Taste SW2 gedrückt, sollte die LED aufleuchten.
Echte 6502 Fans haben das Programm bestimmt schon im Kopf
disassembliert und denken schon über Optimierung nach. Für alle
anderen das Assemblerlisting von Test2.asm
hinter dem "Hexsalat":
; Test2.asm
; Taster, LED Test fuer 65C02, WDC 65C51
.OPT Proc65c02
.ORG $0300
forever:
; for(;;) {
LDA $8001 ; A =
ACIA_STATUS
AND #$40 ;
A &= DSR_MASK
BNE else ;
if (A != 0) {
LDA #$0B
; A = RTS_Low
BRA endif
else:
; } else {
LDA #$03
; A = RTS_High
endif:
; }
STA $8002 ;
ACIA_COMMAND = A
BRA forever ; }
.END
In einem größeren Assembler Programm werden die "magischen
Konstanten" wie $8001, $40 durch symbolische Namen wie ACIA_STATUS
und DSR_MASK ersetzt. Zuerst nur das Allernötigste.
Die Optimierung einer zweiseitigen Verzweigung (IF mit ELSE)
erfolgt in Test2a.asm so:
Zuerst wird Register X mit dem Wert für den ELSE Zweig geladen.
Dann wird eine einseitige Verzweigung ausgeführt, welche den
Inhalt von X vielleicht ändert. Im letzten Schritt wird X
ausgegeben. Wir haben durch mehr Daten (zusätzlich Register X)
weniger Programmlogik erreicht. Solche "Tauschgeschäfte" sind oft
möglich. In unserem Fall wird das Programm kleiner und schneller.
Und, weil Programmverzweigung immer eine mögliche Fehlerquelle
ist, wahrscheinlich sogar besser. Das Programm als Hex Eingabe in
Wozmon:
0300: A2 03 AD 01 80 29 40 D0 02 A2 0B 8E 02 80 80 F0
Wir erwarten einen RAM Speicher der fehlerfrei arbeitet. Ein
kleines Programm welches einen Speicherbereich mit einem Wert
füllt hilft bei der Überprüfung des RAM. Bis jetzt haben wir
Maschinensprache (Hexadezimale Zahlen) eingetippt. Von Norbert
Landsteiner gibt es einen Online 6502
Assembler der Object Code passend für den Wozmon produziert.

Der Assembler-Text kann von dem Bildschirmfoto abgetippt werden
oder mit Copy und Paste in das "src" Feld gebracht werden. Der
"Assemble" Button erzeugt "object code" im Wozmon Format. Mit
CTRL-C Copy aufnehmen und mit ALT-V Paste im TeraTerm
Fenster ablegen. Weitere Automatisierung ist bestimmt mit ChatGPT
möglich, meiner Meinung aber nicht nötig.
; memfill.asm
; fill continous pages with pattern
.ORG $7F00
RESET = $FF00 ; Wozmon cold start
pattern = $00
startpage = $00
endpage = $7E
ptr = $00 ; pointer in zero
page
LDA #pattern ; fill pattern
LDY #0
STY ptr ; ptr lower
byte
LDX #startpage
STX ptr+1 ; ptr upper byte
doFill:
STA (ptr),y
INY
BNE doFill
INC ptr+1
INX
CPX #endpage+1
BCC doFill ; BCC jumps while
; reg < const
jmp RESET ; go back to OS
.END
Achtung: Mit startpage = $00 überschreibt das Programm
die Variable ptr. Das funktioniert nur gut mit pattern = $00.
Beliebige Werte für pattern sind ab startpage = $01 möglich.
Das Programm benutzt alle drei Register A, X, Y und einen 16-Bit
Zeiger (Pointer) in der Zero Page. Die Zero Page ist der
Speicherbereich von $0000 bis $00FF, d.h. die ersten 256 Bytes.
Der Opcode STA (ptr),y erledigt folgende Aufgaben: hole
die zwei Bytes unter Adresse ptr und ptr+1 in ein "verstecktes"
16-Bit Register in der CPU. Addiere den 8-Bit Wert im Register Y.
Speichere den Inhalt von Register A unter dieser Adresse. Ziemlich
komplex für eine CPU aus 3500 Transistorfunktionen! Rodney Zaks
zeigt diesen Zusammenhang in seinem Buch "Programmierung des
6502":
Das restliche Programm ist geschickt um diesen Kern gebaut.
Register Y wird zwangsläufig für die "indirekt indizierte"
Adressierung benötigt. Der Anfangswert von 0 wird als unteres Byte
des 16-Bit Zeiger abgelegt und als laufender 8-Bit Index benutzt.
Das obere Byte des 16-Bit Zeiger wird über Register X abgelegt.
Später wird Speicherstelle ptr+1 und Register X beide im
"Gleichtakt" durch INC ptr+1 und INX erhöht.
Deshalb können wir den "Vergleich Register X mit Konstante" Opcode
CPX #endpage+1 benutzen. Der "Branch if Carry Clear"
Opcode springt wenn Inhalt im Register, als vorzeichenlose Zahl,
kleiner ist als Konstante. Weil wir von Anfangswert startpage
bis einschließlich Endwert endpage den Speicher füllen
wollen, wird einfach der Endwert um eines erhöht. Diese "Assembler
Konstantenrechnung" benötigt keine Opcodes. Das abgelegte Muster pattern
muß im Register A vorliegen. Das Programm wird mit dem Sprung jmp
RESET in den Wozmon beendet. Kritiker nennen den 6502 gerne
eine "Ansammlung von Sonderfällen". Es sind aber gute Sonderfälle.
Hier die Assembler Datei memfill.asm.

Nach dem Programmlauf wird mit 200.25F die Speicherseite (memory
page) mit Interrupt Ringpuffer und Monitor Eingabezeile angezeigt.
Gut lesbar ist das letzte eingebene Kommando zweimal, einmal im
Ringpuffer ab $200, zweitens in der Eingabezeile ab $246. Das
Auslesen von (ungeschützten) Zwischenspeichern haben Hacker schon
nutzvoll gefunden ...
Der 6502
Macroassembler & Simulator versteht die 65C02 Opcodes,
liefert aber nur Binärdateien wie memfill.bin
als Ausgabe. Von Dave
Schmenk gibt es das Programm bintomon.c
welches eine Binärdatei in eine "Wozmon Format" Datei umwandelt.
Meine Version bin2mon.c hat eine
bescheidene Benutzeroberfläche. Nach dem Start von bin2mon.exe wird Adresse und
Dateiname(ohne Extension) abgefragt.
Am besten liegt die Exe-Datei im gleichen Verzeichnis wie die
Bin-Dateien. Die Ausgabe memfill.mon
ist ungewöhnlich, aber trotzdem "Wozmon Format":
7F00: A9 00 A0 00 84 00 A2 00
: 86 01 91 00 C8 D0 FB E6
: 01 E8 E0 7F 90 F4 4C 00
: FF 00 00 00 00 00 00 00
:
Wir können den Quelltext beim Online Assembler eintippen, oder
aus der Datei Zufallszahl.asm
laden.

Nach "Assemble" erscheint der object code im rechten Fenster.
Dort kann dieser ASCII-Text mit CTRL-C kopiert und in die TeraTerm
Terminalemulation mit ALT-V kopiert werden:

Damit eine Zufallszahl erzeugt wird, muß Taste SW2 gedrückt und
wieder losgelassen werden. Dann erscheinen ein bis drei 8-Bit
Hexzahlen, unsere Zufallszahlen. Wie funktioniert alles? Zur
Erklärung wieder C-Pseudocode als Kommentar zum Assembler-Listing:
ACIA_STATUS=$8001
DSR_MASK=$40
SPACE=$20
ECHO=$FFEF
PRBYTE=$FFDC
forever:
; for(;;) {
dsr_low:
; do {
lda ACIA_STATUS
; A = ACIA_STATUS
and
#DSR_MASK
; A &= DSR_MASK
beq
dsr_low
; } while (A == 0)
dsr_high:
; do {
inx
; ++X
lda ACIA_STATUS
; A = ACIA_STATUS
and
#DSR_MASK
; A &= DSR_MASK
bne
dsr_high
; } while (A != 0)
lda
#SPACE
; A = SPACE
jsr
ECHO
; ECHO(A)
txa
; A = X
jsr
PRBYTE
; PRBYTE(A)
jmp
forever
; }
Die "magischen" Zahlen im Quelltext sind durch symbolische
Konstanten ersetzt worden. Wieder einmal ist das Programm in einer
Endlosschleife. Zuerst prüft eine do-while Schleife ob das DSR Bit
in der Speicherstelle ACIA_STATUS noch 0 ist, d.h. die Taste nicht
gedrückt ist. Im zweiten Schritt der Schrittkette
prüft eine do-while Schleife ob das DSR Bit in der Speicherstelle
ACIA_STATUS noch 1 ist, d.h. die Taste gedrückt ist. Zusätzlich
wird bei jedem Schleifendurchlauf Register X erhöht. Der Inhalt
von X ist später unsere Zufallszahl. Im dritten Schritt wird ein
SPACE Zeichen mit Unterprogramm ECHO ausgegeben, die Zufallszahl
von Register X nach Register A kopiert und dann mit Unterprogramm
PRBYTE als hexadezimale Zahl ausgegeben.
Jeder Tastendruck ist unterschiedlich lang, wenigstens für einen
Computer mit einer Zeitauflösung von 543 ns (Nanosekunden), dem
Kehrwert der Taktfrequenz von 1.8432 MHz. Warum gibt es manchmal
zwei oder drei Ausgaben für einmal Taste drücken und loslassen?
Das liegt am Tastenprellen. Nachdem die beiden Metallkontakte im
Taster sich berührt haben, kann es sein, das der Kontakt wieder
"wegspringt" und die Verbindung kurzzeitig wieder unterbrochen
ist. Deshalb wird oft ein Kondensator parallel zum Taster
geschaltet, um den Effekt des Tastenprellen zu reduzieren. Der
Kondensator soll sich schon beim ersten kurzen Kontakt entladen
und soll während der Prellzeit als Kurzschluß wirken, d.h. der
Computer sieht nur einen Pegelwechsel.
Mein erster Kontakt mit Programmieren war in der Form eines
programmierbaren Taschenrechners in den 1970er Jahren. Das
interessanteste Programm war "Zahlen raten". Der Taschenrechner
hat eine Zufallszahl im Bereich 1 bis 100 erzeugt. Der Benutzer
hat geraten. Als Anwort vom Taschenrechner gab es "zu klein", "zu
groß" oder "Korrekt". Über 50 Jahre später programmiere ich
"Zahlen raten" auf 65C02 Computer nach einer Elektroniker Lehre
und einem Informatik Studium ...
to be continued ...
Der 6551 und andere UART Bausteine die ich kenne haben folgende
Besonderheiten: Wenn die Handshake-Anschlüsse nicht die
"richtigen" Pegel führen, dann werden die Sendeeinrichtung
und/oder die Empfangseinrichtung lahmgelegt. Für die 6551
Handshake Eingänge /DCD, /DSR und /CTS ist das nachvollziehbar. Das
gleiche gibt aber auch für die Handshake Ausgänge! Wird /DTR
Ausgang auf "high" geschaltet, funktioniert Datenempfang nicht
mehr. Wird /RTS Ausgang auf "high" geschaltet, funktioniert
Datensenden nicht mehr. Diese "Features" werden kaum im Datenblatt
vorgestellt. Der langsame 6502 Computer muß den Datenempfang vom
"großen" Computer drosseln. Dazu ist nur der /RTS Ausgang
geeignet. Nun die Auswirkung dieser seltsamen Logik: Wenn die
Interrupt-Routine den /RTS Ausgang auf "high" schaltet um den
"großen" Computer zu bremsen, werden weiterhin Daten empfangen,
während der "große" Computer eine Reaktionszeit auf den
"Bremsbefehl" hat. Während dieser "Bremszeit" werden Daten
erfolgreich empfangen, aber nicht mehr an den "großen" Computer
als Echo zurückgesendet. Erst wenn /RTS wieder auf "low"
geschaltet wird, werden empfangene Zeichen als Echo wieder
sichtbar.
Das TeraTerm Bildschirmfoto zeigt den download von tictactoe.bas
per "Copy and paste" in TinyBasic. Das ganze sieht aus wie
fehlerhafte Datenübertragung, aber nach LIST wird ein
vollständiger und korrekter BASIC Quelltext sichtbar. "Sieht
schlimm aus, ist aber so".
Im WDS Datenblatt zum 65C51 steht: "TDRE bit cannot be polled to
determine when to write the next byte to the TDR/TSR". In anderen
6551 oder 65C51 Datenblättern steht an dieser Stelle: "This bit
goes to a 1 when the 65C51 transfers data from the Transmit Data
register to the Transmitter Shift Register, and goes to a 0 when
the processor writes new data onto the Transmitter Data Register".
Die ECHO Funktion für den WDC 65C51 ist:
; Subroutine prints one byte (ASCII)
ECHO:
PHA
; Save A on stack
LDA #26
; Initialize 115200 bps 7-N-1 delay loop
TXDELAY:
.BYTE
$3A
; 65C02 Opcode "Decrement A"
BNE
TXDELAY ; Count
down to 0
PLA
; Restore A from stack
STA
ACIA_DATA ; Print
character
RTS
; Return
Als Work-around für den TDR bug empfiehlt WDC: "A delay should be
used to insure that the shift register is empty before the TDR/TSR
is reloaded". Die Verzögerung im CPU Taktzyklen hängt von der
Zeichenlänge in Bits (kürzer ist besser) und Baudrate (schneller
ist besser) ab. Die bestmögliche Einstellung ist 1 Startbit, 7
Datenbits, 1 Stopbit, d.h. 9 Bits pro Zeichen und 115200 bps. Dies
ergibt eine Zeichenlänge von 144 Taktzyklen. Die DEA, BNE Schleife
braucht 5 Taktzyklen für einen Durchlauf.
Für eine fehlerfreie 6551, 65C51 ist folgende Funktion nötig:
; Subroutine prints one byte (ASCII)
ECHO:
PHA
; Save A on stack
doNotEmpty:
; do {
LDA ACIA_STATUS
; A = Status
AND
#$10
; A &= TDRE_Mask
BEQ
doNotEmpty ; } while (TDRE == 0)
PLA
STA
ACIA_DATA ; Print character
RTS
; Return
Anstelle einer festen Verzögerungszeit wird in einer "Busy
Waiting" Schleife auf Transmit Data Register Empty geprüft. Werden
Daten aus einem Speicherblock "wieselflink" an die ACIA übertragen
nähert sich die Laufzeit beider ECHO Versionen an.
Zusammenfassung: Schön ist der WDC 65C51 Bug nicht. Schlimm ist
er aber auch nicht. Die Lösung für den WDC 65C51 funktioniert auch
mit den anderen 6551/65C51 Bausteinen, umgekehrt gilt es nicht.
Neben dem 6551 habe ich mir auch andere UARTs angesehen und früher
auch eingesetzt wie Motorola 6850, Intel 8251, Zilog Z80 SIO,
National Semiconductor 8250, 16450 usw. Meine Meinung: alle haben
kleinere oder größere Macken. Am besten ist der 16C550 mit
eingebauten 16 Bytes FIFO. Früher ein "Wegwerfartikel" bei PCs und
ATs, heute kaum mehr zu kaufen im DIL Gehäuse.
Das Zeitverhalten des Wozmon Programmes ist ungleichmäßig. Eine Eingabe wie '0' oder 'F' wird schnell verarbeitet, weil nur das Zeichen im "input line buffer" abgelegt wird. Die Eingabe von 'Carriage return' wird langsam verarbeitet, der ganze "input line buffer" muß abgearbeitet werden. Deshalb sind im Terminalemulator Programm Wartezeiten nach jedem übertragenen Zeichn nötig damit der "kleine" 6502 Computer nicht "überrannt" wird. Datenempfang mit Interrupt verbessert das Zeitverhalten. Der Interrupt reagiert immer schnell auf neue Daten, auch wenn "parse und execute" der "input line" immer noch langsam ist. Für Interrupt nutzen ist eine Hardware-Verbindung zwischen Interrupt-Ausgang des ACIA (Asynchron Communication Interface Adapter) 65C51 und Interrupt-Eingang der CPU nötig. Das original Wozmon Programm braucht diese Verbindung nicht. Zweitens muß Interrupt Verarbeitung durch Software bei ACIA und CPU eingeschaltet werden. Drittens braucht es eine spezielle Subroutine welche bei Interrupt aufgerufen wird, einen Interrupt-Handler.
Initialisierung oder Einrichtung ist nur einmal nach Einschalten des 6502 Computers nötig. Zuerst sollen die benutzen Speicherstellen, die Daten vorgestellt werden:
; Monitor cold start (after pressing RESET key)
RESET:
LDX #0
STX monwrptr
STX monrdptr
STX monbfcnt
DEX
; X=$FF
TXS
; init stack
JMP RESET1
; Monitor cold start continued (1)
RESET1:
CLD
; binary mode
LDA #>monbuf
STA monwrptr+1
JMP RESET2
; Monitor cold start continued (2)
RESET2:
STA monrdptr+1
; redirect IRQ entry to iWozmon
LDA
#$4C
; opcode JMP
STA JMPIRQ
LDA #<monirq
STA
JMPIRQ+1 ; lower byte
LDA #>monirq
STA
JMPIRQ+2 ; higher byte
LDA
#$30
; 7-N-1, 115200 baud.
STA ACIA_CTRL
LDA
#$09
; No parity, no echo, enable interrupts.
STA ACIA_CMD
CLI
; enable CPU interrupt
BNE
RESET3 ; branch
always
Die iWozmon Initialisierung ist auf drei Speicherbereiche
aufgeteilt um das EEPROM gut zu füllen. Dies ist der Preis für
Kompatibilität und Kompaktheit. Zuerst werden werden Schreibzeiger
und Lesezeiger auf den Anfang des Ringpuffers gestellt und Anzahl
der Bytes auf 0. Im zweiten Schritt werden die drei Bytes für den
unbedingten Sprung zum Interrupt-Handler ab Speicherstelle JMPIRQ
abgelegt. Als dritter Schritt wird ACIA 65C51 auf passende
Parameter 115200 bps, 7 Datenbits usw. eingestellt, Interrupt beim
ACIA eingeschaltet UND Interrupt bei der CPU eingeschaltet.
Zwischendurch wird Stackpointer Register S auf $FF gesetzt und
Binärmodus mit CLD eingeschaltet.
Der Hardware Handshake RTS/CTS ist im Schaltbild eingezeigt, es
gibt eine Verbindung zwischen ACIA RTS und "USB Serial Konverter"
CTS. Wenn der ACIA RTS Ausgang "low" liefert, darf der große
Computer Daten senden. Bei RTS Ausgang "high" nicht.
; Subroutine for "character available". A != 0 if char
available, else 0
KEYBD:
LDA monbfcnt
RTS
Die einfachste Funktion ist KEYBD . Natürlich kann ein
User Programm die Speicherstelle direkt abfragen ohne eine
"Betriebssystemfunktion" zu nutzen. Das widerspricht aber der
Hardwareabstraktion. Die Auswirkung solcher Sünden waren schön am
IBM PC zu sehen.
; Subroutine to get a character
GETC:
LDA monbfcnt
BEQ GETC
DEC monbfcnt
; low water mark is 0
; RTS/CTS handshake
BNE
notlow ;
jump if A != 0
LDA
#$09
; /rts=low ...
STA ACIA_CMD
notlow:
.BYTE
$67, monrdptr ; RMB6 monrdptr
.BYTE
$B2, monrdptr ; LDA (monrdptr)
INC monrdptr
RTS
Lesen aus dem Ringpuffer erfolgt in der Funktion GETC .
Zuerst wird "Füllstand" Variable monbfcnt gelesen.
Solange Ringpuffer leer ist, wird in einer "Busy waiting" Schleife
gewartet. Es muß ein Interrupt passieren welcher den Ringpuffer
füllt, damit die Schleife verlassen wird. Im zweiten Schritt wird
die Handshake Leitung /RTS bedient. Klassisch ist die Lösung mit
"low Watermark" und "high Watermark". Ist der Füllstand kleiner
gleich "low Watermark", wird /RTS auf "low" geschaltet und
Datenempfang ist freigegeben. Ist der Füllstand größer gleich
"high Watermark", wird /RTS auf "high" geschaltet und Datenempfang
ist gesperrt. Die "low Watermark" ist 0. Im dritten Schritt
erfolgt die Ringpuffer Verwaltung. Die benutzen 65C02S Opcodes
werden als .BYTE Kommandos kodiert, damit auch ein nur 6502
Assembler sie verarbeiten kann. Der RMB6 Opcode löscht das Bit 6.
Dadurch wird der monrdptr Zeiger nach Erreichen von $40
wieder auf $00 zurückgesetzt. Der 65C02 Opcode LDA (monrdptr)
ist praktischer als der 6502 Opcode LDA (monrdptr),y .
Zur Lese-Verwaltung des Ringpuffers gehört erniedrige "Füllstand"
Variable und erhöhe "Lesezeiger" Variable.
; Interrupt-Handler
monirq:
; on stack psr, pcl, pch. TOS (top of stack) is left
PHA
; Push A on stack
.BYTE
$DA
; PHX
.BYTE
$5A
; PHY
; on stack y, x, a, psr, pcl, pch
BIT ACIA_STATUS
; IRQ is cleared when Status is read
BPL
trace
; Jump if Bit7 == 0
.BYTE
$67, monwrptr ; RMB6 monwrptr
LDA ACIA_DATA
.BYTE
$92, monwrptr ; STA (monwrptr)
INC monwrptr
INC monbfcnt
; RTS/CTS handshake
LDA monbfcnt
CMP
#HIWATER ; high water
mark
BNE
nothigh ; jump
if A != HIWATER
LDA
#$01
; /rts=high ...
STA ACIA_CMD
nothigh:
notacia:
; restore y, x, a, psr, pcl, pch
.BYTE
$7A
; PLY
.BYTE
$FA
; PLX
PLA
; Pull A from stack
RTI
; restores psr, pcl, pch
Natürlich kommt der Interrupt-Handler, das "dicke Ende", zum
Schluß. Bei der 6502 CPU muß der Interrupt-Handler mit RTI
beendet werden. Zuerst werden die Register A, X und Y auf den
Stack gesichert. Die 65C02 Opcodes PHX und PHY werden wieder für
nur 6502 Assembler kompatibel kodiert. Weiterhin muß ACIA_STATUS
gelesen werden um das Interrupt Signal von ACIA an CPU zu
bestätigen. Weil oft mehrere Interruptquellen angeschlossen sind,
die 6502 aber nur einen Interrupteingang hat, ist es gute Sitte
die Interruptquellen durch "Polling" abzufragen. Das macht BIT
ACIA_STATUS. Das wichtigste Statusbit ist Bit 7 und wird
durch den BIT Opcode in das Vorzeichen Flag (N) kopiert. Der BPL
(Branch PLus) kann es leicht abprüfen. Die Interruptverarbeitung
in der CPU hat schon das CPU Statusregister automatisch gesichert.
Somit kann der Interrupt "unsichtbar" für das Hauptprogramm
gemacht werden. Nach dem Interrupt ist wie vor dem Interrupt durch
Push und Pull. Das Datenbyte wird von der ACIA gelesen und in den
Ringpuffer kopiert. Diesmal Schreibzeiger monwrptr
erhöhen und monbfcnt erhöhen. Die "high Watermark" ist
32 bei einer Ringpuffer Größe von 64. In der "Bremszeit", d.h.
zwischen Wechsel von /RTS von low auf high und keine Daten mehr
senden vom "großen" Computer können noch maximal 32 Bytes
übertragen werden. Auch bei enem Ringpuffer gilt: "so groß wie
nötig, so klein wie möglich". Die Ringpuffer Verwaltung ist dank
der 65C02 und 65C02S Opcodes kompakt. Der Qualitätsbeauftragte ist
auch glücklich: die Ursache der meisten Bugs ist ein "IF", welches
es hier kaum gibt.
Mit einem Eingang und einem Ausgang neben der asynchronen
serielle Schnittstelle ist die IO etwas sparsam. Der 65C22 ist
eine gute parallele IO. Zweimal 8 Bit mit zweimal 2 bit für
Handshake oder synchrone serielle Datenübertragung (mit gut
bekannten Bugs und Work-arounds). Weil ein neues "Haus" in der
"Stadt" erscheint, müssen die "Postfächer" umsortiert werden. Der
Adressbereich $8000 bis $BFFF wird auf zwei Häuser aufgeteilt. Der
Bereich $8000 bis $9FFF bleibt der seriellen IO mit 65C51, den
Bereich $A000 bis $BFFF bekommt die parallele IO 65C22 mit ihren
16 "Mietern". Für die Chip Select Logik von U4 und U6 wird ein
zweiter 74HC00 eingesetzt. Natürlich gibt es "Fachleute" wie den 3
zu 8 Decoder 74HC138 für diesen Job, aber für unsere Zwecke reicht
der Billigheimer 74HC00.

Der Anschluß /CS2 von U4 65C51 liegt nicht mehr an Adressleitung
A14 sondern am Ausgang des NAND U7C. Sonst ist an der 5-chips
Schaltung nichts zu ändern. Zwischen /IRQ Ausgang des 65C22 und
/IRQ Eingang des 65C02 muss eine Schottkydiode kleiner Leistung
wie BAT41 geschaltet werden. Der Interrupteingang des 6502 war als
"Wired OR" geplant. Dazu passen aber nur "Open Collector"
Ausgänge. Diese Ausgänge scheinen beim 65C22S nicht möglich zu
sein, beim 65C51N aber schon. Die Schottkydiode simuliert einen
Open Collector Ausgang. Ohne dieses Bauteil führt ein 0 Pegel am
/IRQ vom 65C51 zu einem Kurzschluß, wenn der /IRQ vom 65C22 einen
1 Pegel liefert.
to be continued ...
Anstelle von dem 32 KByte RAM 62256 kann auch ein 128 KByte RAM
AS6C1008 im 32-poligen DIL Gehäuse eingesetzt werden. Um ein
solches RAM mit den restlichen Bauteilen sinnvoll zu nutzen ist
eine Aufteilung in 4 KByte für Monitor-ROM, 4 KByte für IO-Bereich
und 56 KByte RAM sinnvoll und einfach möglich. Der "4-bit
magnitude comparator" 74HC85 ist gut geeignet als Herzstück der
Chip Select Logik. An den 74HC85 Eingängen B0 bis B3 wird der
Vergleichwert eingestellt. Ist die angelegte Adresse an A0 bis A3
gleich diesem Wert, werden die IO Bausteine ausgewählt. Ist die
Adresse kleiner, dann das RAM und ist die Adresse größer, dann das
ROM. Leider sind die Ausgänge nicht-invertiert. Zwei Inverter aus
einem 74HC04 (enthält 6 Inverter) erzeugen invertierte Chip
selects. Der 3 aus 8 Decoder 74HC138 unterteilt den 4 KByte IO
Bereich in acht 512 Byte Bereiche für maximal acht IO Bausteine.
Der Nachteil dieser eleganten Chip Select Lösung ist die
Gatterlaufzeit. Der IO Select muß durch 74HC85 und 74HC138 laufen.
Das sind typisch 18 ns + 15 ns = 33 ns. Andererseits, programmierbare
Logikbausteine (PLD) im DIL Gehäuse sind nicht schneller und
brauchen ein Programmiergerät.
Bekannt für 6502 BASIC sind TinyBasic
von Tom Pittman, Apple
Integer BASIC von Steve Wozniak und Microsoft 6502
BASIC von Ric Weiland (und vielleicht Bill Gates). Dem
TinyBasic fehlen Fließkomma-Zahlen und FOR NEXT, dem Integer BASIC
fehlen Fließkomma-Zahlen. Microsoft BASIC benötigt in der
"6-digit" (32-Bit Fließkomma-Zahl) knapp 8 KByte, Tiny BASIC
benötigt etwas mehr als 2 KByte und Apple Integer BASIC liegt mit
4 KByte dazwischen.
TinyBasic war eine frühe Open Source
und in der 6502 Version für 5 US$ günstig. Besser lesbar als der
original Microsoft 6502 Quelltext, der PDP-10 Macroassembler zur
Erzeugung von 6502 Opcodes vergewaltig, ist der Assembler
Quelltext von Michael Steil für 9 verschiedene
Microsoft 6502 BASIC .
Eine neue Entwicklung ist EhBASIC
Source Code von Lee Davison (leider schon verstorben) mit 10
KByte Umfang.
Zuerst diese Notiz von Tom Pittman: "TinyBasic interpreter
Copyright 1976 Itty Bitty Computers, used by permission."
Im Internet gibt es den TinyBasic Simulator TinyBasicBlazor
zum "Online reinschnuppern" in z.B. die TinyBasic Spiele
Tic-Tac-Toe, Life, Tiny Adventure und The Kingdom of Euphoria. Es
gibt keine Pixel-Grafik und keinen Sound, aber trotzdem Spielspaß.
Hier ist der Quelltext von 6502
TinyBasic via Bill O'Neill. Hier ein weiterer Quelltext in
Datei tb_pittman.zip auf der Internet Seite BASIC/DEBUG
mit mehr Kommentaren, sowie Tiny
Basic source by Bob Applegate. Im Buch The First
Book of Tiny BASIC Programs gibt es einen Speedup-Patch für
TinyBasic. Die "suche BASIC Zeile" Funktion arbeitet dann mit
einer Variante der binären Suche.
Hier meine TinyBasic speedup Version für den Michal Kowalski 6502 Simulator und
meine Version für einen 6502 Computer mit
iWozmon Monitor und 32 KByte RAM. Das TinyBasic Programm im Wozmon Format
wird vom Wozmon mit 290 R gestartet.
Im TinyBasic
User Manual gibt es zwei Beispielprogramme welche im 6502
Simulator und auf "echter" Hardware laufen. Mehr Info über
TinyBasic auf IL Basis enthält das National
Semiconductor TinyBasic Users Manual für die SC/MP oder
INS8060 CPU.
Das erste Programm ist Zufallszahlen ausgeben rnd.bas
. Zuerst TinyBasic starten, dann
im Simulator: Basic Quelltext mit CTRL-C kopieren und mit Einfg
(Ins) Taste ins Simulator In/Out Window einfügen.
im TeraTerm: Basic Quelltext mit CTRL-C kopieren und mit ALT-V ins
TeraTerm Window einfügen.
Bemerkung: Der Zeileneditor von TinyBasic versteht
Backspace-Taste zum Löschen des letzten Zeichen und CTRL-X zum
Abbruch der Zeileneingabe.






Die TinyBasic Implementierung hat eine Schwäche bei der Funktion
"BASIC Zeile suchen". Im "The First
Book of Tiny BASIC Programs" wird im Appendix "Binary
Search Speedup Code" eine Variante der binären
Suche für "Zeile suchen" vorgestellt:
; Y = 0, TEMP
= search lineno
BINARY STA
LO-1 ; LO = BASIC
LDA BASIC+1
STA LO
LDA MEND+1 ; HI = MEND high
byte
SLICE
; for(;;) {
STA HI
SEC
; A = (HI - LO) >> 1
SBC LO
LSR
BEQ DONE
; if (0 == A) break
CLC
; BP_high = ((HI - LO) >> 1) + LO
ADC LO
STA BP+1
; basic pointer
LDA LO-1
; BP_low = LO-1
STA BP
JSR CHAR
; get char from BASIC program
BNE *-3
; set zero flag if carriage return
JSR CHAR
; read over lineno low
JSR CHAR
; read over lineno high
JSR CHAR
BNE *-3
; read behind carriage return
LDA TEMP
; 16-bit accumulator, search lineno
CMP (BP),Y ; if
(*BP >= TEMP) continue
INY
; Y = 1
LDA TEMP+1
SBC (BP),Y
LDA BP+1
BCC SLICE
STA LO
; LO = BP
LDA BP
STA LO-1
LDA HI
BNE SLICE+3 ; } // BRA
DONE LDA
LO-1 ; BP = LO
STA BP
LDA LO
RTS
Eine TinyBasic Zeile im Speicher besteht aus einer little-endian
16-Bit Zeilennummer im Bereich 1 bis 32767 und ASCII Zeichen als
Zeileninhalt. Die Zeile wird mit CR (carriage-return, 0x0D)
beendet. Das Zeichen 0x0D kann als Zeilenende, als erstes Zeichen
der Zeilennummer oder als zweites Zeichen der Zeilennummer
auftreten.
Die Routine implementiert eine Variante der üblichen binären
Suche. Die untere Grenze wird durch den 16-Bit Wert in
Speicherstellen LO-1 und LO bestimmt. Die obere Grenze ist der
8-Bit Wert HI, der mit dem High Byte der "Ende BASIC Programm"
Adresse geladen wird. Die Berechnung des mittleren Elements ((HI
- LO) >> 1) + LO wird nur mit den
High-Bytes ausgeführt. Das Unterprogramm CHAR
liest die Zeichen auf die BP zeigt
ein. Das Zero flag wird gesetzt wenn CR Zeichen gelesen wurde.
Weil 0x0D (ASCII Code von CR) auch als Low Byte oder High Byte von
Zeilennummer auftreten kann, wird mit zwei BNE *-3
Schleifen nach einem "0x0D ist wirklich CR" gesucht. Ein RTS
führt zurück zur linearen Zeilensuche im TinyBasic Programm. Als
kleine Optimierung benutzt meine Speedup Implementierung nicht
zusätzlichen Speicher in der Zero page, sondern benutzt das high
Byte des USR Pointer und zwei Bytes aus dem "Binär nach Dezimal
PAD", benutzt in der IL__PN Funktion.
| Allison
IL |
Pittman
IL |
| TST S12,'INPUT' TSTV S17 |
BC RETN "INPUT" BV * |
| TST S13,'RETURN' DONE |
BC END "RETURN" BE * |
Die TinyBasic Implementierung von Tom Pittman implementiert die
"Intermediate Language (IL)" mit 8-bit Opcodes, mit Bytecode.
Diese Lösung erreicht die beste Codedichte, aber der
Bytecode-Parser ist umfangreich und langsam:
; IL Bytecode
:STRT PC "Number:"
GL
BN NaN
Eine schnellere Implementierung ist bei Forth
als "direct compilation" bekannt: Der "IL" benutzt 3-bytes lange
JSR Befehle anstelle von Bytecode. Nötige Parameter wie die
Stringkonstante nach dem PC IL-Opcode wird hinter den JSR
geschrieben. Die PC Opcode Routine muß diese Konstante lesen und
die Returnadresse manipulieren. Das Bytecode Beispiel in "direct
compilation", immer noch kompakter und langsamer als "echter
Assembler":
; IL Direct
compilation
STRT: JSR PC
.DB
"Number", ':'+$80
JSR GL
JSR BN
.DB
NaN-*
Zwischen diesen zwei Extremen liegt Forth "Direct Threaded Code
(DTC)". Anstelle von JSR Opcodes werden 2-bytes lange Adressen der
Subroutinen abgelegt:
; IL Direct Threaded Code (DTC)
STRT: .WORD PC
.DB
"Number", ':'+$80
.WORD GL
.WORD BN
.DB
NaN-*
Meiner Meinung nach ergibt Forth DTC die beste neue
Implementierung von TinyBasic. Längere IL Befehle wie bei DTC
wurden 1976 von
Dennis Allison abgelehnt: "Your mechanization will, of
course, work, but requires one more byte per IL instruction".
Vielleicht hat Allison den Aufwand für die Bytecode nach
Subroutine Umsetzung ignoriert. Diese Umsetzung erfolgt über eine
Tabelle, die neben dem IL Programm auch Speicherplatz benötigt und
bei DTC entfällt. Anstelle des CPU Program Counter (PC) wird ein
Interpreter Pointer (IP) benutzt. Über diesen IP ist der Zugriff
auf die Parameter leichter als bei "direct compilation",
wenigstens bei dem 6502.
Der DTC Code braucht einen "inneren Interpreter" um den Threaded
Code abzuarbeiten. Traditionell gehören dazu folgende Register:
W = Working register
IP = Interpreter Pointer
PSP = Parameter Stack Pointer
RSP = Return Stack Pointer
UP = User Pointer
Eine beliebte Optimierung ist
TOS = Top of Parameter Stack in Register
Für die 65C02 CPU benutze ich diese Zuordnung von DTC Register
auf "zero page" Register:
W, IP, RSP, TOS = Zero page
PSP = Register S (stack)
UP = nicht benutzt
Der innere Interpreter mit dem Einsprungpunkt NEXT ergibt sich
somit als:
NEXT: LDA
(IP) ; inline LDAIP
INC IP
BNE *+4
INC IP+1
STA
W ; WL = *IP++
LDA (IP) ; inline
LDAIP
INC IP
BNE *+4
INC IP+1
STA W+1 ; WH =
*IP++
JMP (W)
NEXT kann man als Kombination aus RTS und JSR sehen. Natürlich ist
jeder Interpreter langsamer als reiner Assembler. Aber die 6502
CPU umwandeln in eine simulierte 16-Bit CPU die TinyBasic IL
Opcodes direkt ausführen kann finde ich schick. Der LN Opcode
kopiert eine 16-Bit Konstante auf den Parameter Stack. Hier die
Implementierung mit TOS im Register:
LN: LDA
TOS+1 ; *PSP-- = TOSH
PHA
LDA TOS ; *PSP--
= TOSL
PHA
JSR LDAIP ; TOSL = *IP++
STA TOS
JSR LDAIP ; TOSH = *IP++
STA TOS+1
JMP NEXT
Die Vorteile von TOS im Register und S als PSP werden bei der
Implementierung von SU (signed 16-bit Subtraktion) sichtbar:

Die Version 0.1 ist noch fehlerhaft und unvollständig. Aber es
funktioniert schon genug um es zu zeigen. Der Assembler Quelltext
dtc_tbSim.asm für den 6502 Simulator
stellt geringe Ansprüche an den Assembler. Neben .org werden noch
die Pseudo-Opcodes .db für 8-bit Konstante und .word für 16-bit
Konstante im little-endian Format benötigt.

Die TinyBasic Kommandos LET und PRINT funktionieren schon (etwas)
im Direkt-Modus (control, immediate mode). Dafür waren 1477
Quelltextzeilen oder 1800 bytes nötig. Das komplette DTC 6502
TinyBasic dürfte 2 KByte oder ein wenig mehr lang werden. Mehr
Speicher für IL Implementierung (661 bytes) in DTC anstelle von
Bytecode (435 bytes für IL Bytecode und IL Index-nach-Adresse
Tabelle) wird durch den deutlich einfacheren "DTC parser", d.h.
NEXT, anstelle dem "Bytecode parser" ausgeglichen.
Die Eingabe "PR -30000/-100" kann (in jeder TinyBasic
Implementierung nach der IL Spezifikation) nicht verarbeitet
werden. Die ERR Routine gibt die zwei Hex-Zahlen 07E5 0584 aus,
zuerst die RTS Adresse von JSR ERR, dann den Wert von IP. Als
Fehlerquelle im Assemblerprogramm finden wir:
01007 0007D6 B2
02
LDA (IP)
01008 0007D8 F0
09
BEQ BC9
01009 0007DA 4C 32 08
JMP BR ;
take IP jump
01010 0007DD 20 32 03
BC3: JSR
INCIP ; skip IP jump
01011 0007E0 4C 19 03
JMP NEXT
01012 0007E3 20 E6 05
BC9: JSR
ERR ; no more tests
Bei der 6502 ist die Returnadresse auf dem Stack die (echte)
Returnadresse-1, d.h. der Return erfolgt auf Adresse $07E6 und der
JSR erfolgte von $07E3. Die ERR Routine wurde von BC aufgerufen,
der "String Match Branch" Routine welche die Schlüsselwörter wie
"LET" oder ">" erkennt. Als Fehlerquelle in der IL finden wir:
0581 AB 07
F5
.WORD $07AB ; BC * "(" OTHERWISE MUST
BE (EXPR)
0583
A8
.BYTE $A8
0584
00
.BYTE $00
Der Parameter * in der original IL Spezifikation oder der
Parameter 0 in der DTC Spezifikation bedeutet "keine weitere
Schlüsselwort-Suche, Fehler-Abbruch". Natürlich können die
Fehlerbehandlung und/oder die Debugging-Unterstützung immer
verbessert werden. Aber die zwei Hexzahlen helfen schon. Und
natürlich zeigt das Beispiel: Gibt es in einem Programm zwei
Abstraktionsebenen, dann gibt es auch Fehlersuche/Debugging auf
zwei Abstraktionsebenen.
Im TinyBasic von Tom Pittman wird die Pseudo-Zufallszahl mit der
Formel R:=R*2345+6789 erzeugt. Heutzutage ist ein "Galois
Linear-feedback shift register" die bessere Wahl. Dieser
Generator durchläuft alle Zahlen zwischen 1 und 65535, d.h. hat
die maximale Periodenlänge. Zuerst die Implementierung in
Programmiersprache C:
#define Polynom 0x002D // x^16+x^14+x^13+x^11+1
#define Seed 0xACE1 // any number >0
uint16_t rnd_num; // 2 bytes
16-bit pseudo random number
void rnd_lfsr(void)
{
uint16_t carry_flag = rnd_num & 0x8000;
rnd_num <<= 1;
if (carry_flag) {
rnd_num ^= Polynom;
}
}
Die Parameterübergabe findet, ungewöhnlich für Hochsprache, durch
die globale Variable rnd_num statt. Der Galois LFSR besteht nur
aus drei Operationen: Ein Shift nach links, ein Test des Carry
Flag und eventuell eine Exklusiv-Oder Operation. Die
Leistungfähigkeit hängt von dem Polynom Wert ab. Es gibt einige
weitere gleich leistungsfähige 16-bit Polynome. Für das "schnelle
C Programm zwischendurch" benutzt ich Code::Blocks und gcc
Compiler unter MS-Windows. Nun die 6502 Assembler Version:
Polynom = $002D ; x^16+x^14+x^13+x^11+1
Seed = $ACE1 ; any number >0
rnd_num = $10 ; 2 bytes 16-bit pseudo random number
rnd_lfsr:
ASL
rnd_num ;
rnd_num <<= 1
ROL rnd_num+1
BCC
noCarry ; if
(carry_flag) {
LDA
rnd_num
; rnd_num ^= Polynom
EOR #<Polynom
STA rnd_num
LDA rnd_num+1
EOR #>Polynom
STA rnd_num+1
noCarry:
; }
rts
Für beide Galois LFSR Implementierungen gibt es die Testprogramme
rnd_lfsr.c und rnd_lfsr.asm
für den 6502 Simulator. Die Ergebnisse beider Implementierungen
sind identisch. Zuerst die C Variante:
Dann die 6502 Assembler Variante:

Der Quelltext und die Programmausgaben sehen sich ABSICHTLICH
ähnlich. Ein guter Rat ist: entwickle immer auf der
höchstmöglichen Abstraktionsebene. Hier wurde C benutzt,
genau gesagt nur die Teile von C die auch (einfach) unter 6502
Assembler zur Verfügung stehen. Deshalb do-while Schleife anstelle
von for Schleife und Parameterübergabe in globale Variable.