65C02 Computer

Andre Adrian, DL1ADR, 2026-02-28

Einführung

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.

Schaltbild 5-chips

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.

EEPROM programmieren

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

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:




Programm Abbruch mit Reset Taste. Ich benutze den 6502 Assembler 6502 Macroassembler & Simulator von Michal Kowalski. Die Dateiendung ist defaultmäßig *.65s. Die übliche Dateiendung ist *.asm bei MS-Windows und *.s bei UNIX. Das Assembler-Listing von Test1.asm ist:

; Test1.asm
; ASCII Zeichen Test fuer 65C02, WDC 65C51

        .OPT    Proc65c02
        .ORG    $0400

forever:                ; for(;;) {
        LDA     #$20    ;   A = SPACE
do:                     ;   do {
        JSR     $FFEF   ;     ECHO(A)
        INA             ;     ++A
        BPL     do      ;   } while(A >= 0)
        BRA     forever ; }

        .END


Wenn wir zuerst nur auf den Kommentar schauen, d.h. den Text hinter den ; Zeichen, sehen wir ungefähr C-Quelltext. Das Programm besteht aus zwei geschachtelten Schleife. Eine äußere Endlosschleife und eine innere do-while Schleife. Das Register A wird mit dem Wert des Space-Zeichen geladen. Dann startet die do-while Schleife. Zuerst wird das Unterprogramm ECHO() aufgerufen. Diese Funktion gehört zum Monitor-Programm und gibt den Inhalt von Register A auf der seriellen Schnittstelle aus. Dann wird Register A um 1 erhöht. Der bedingte Sprung BPL (Branch if PLus) beendet die do-while Schleife. Hier wird trickreiche die unterschiedliche Betrachtung des Registerinhalts als vorzeichenlose Zahl beim INA Opcode und als Zahl mit Vorzeichen beim BPL Opcode benutzt. Wird der Wert 127 durch INA erhöht ist der neue Wert 128. Der vorzeichenlose Wert 128 hat das höchste Bit auf 1 gesetzt und kann auch als Zahl mit Vorzeichen -128 interpretiert werden. BRA (BRanch Always) beendet die for Schleife. Bemerkungen: Im 65C02 Datenblatt wird Opcode INA (INcrement A) Opcode INC genannt. Die nützlichen Opcodes BRA und INA gibt es nur bei 65C02, nicht bei 6502.

Das zweite Programm

Die Tätigkeit des Programms ist bescheiden: Wird die Taste SW2 gedrückt, leuchtet die LED. Wird die Taste wieder losgelassen, wird die LED wieder dunkel. Natürlich kann man dieses Ziel auch ohne Computer erreichen. Wieder nur den fettgedruckten Text in die Terminalemulation eingeben:

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

Wieder schauen wir zuerst auf den Kommentar. Eine Endlosschleife und ein IF mit ELSE. Strukturierte Programmierung ist für Maschinensprache oder Assembler "zu hoch". Wir müssen unsere bekannten und bewährten Programmiermuster aus primitiven Mustern zusammensetzen. Der Ausdruck hinter einem IF muß CPU Flags setzen. Die bedingten Sprungbefehle wie BNE (Branch if Not Equal) reagieren auf die CPU Flags. Typisch für Assembler und IF ist der "jump away" auf den ELSE Zweig. Wenn die Sprungbedingung NICHT erfüllt ist, dann wird ohne Sprung der IF Teil ausgeführt. Wenn der IF-Teil abgeschlossen ist, sorgt ein unbedingter Sprung BRA (BRanch Always) dafür, das der IF-Teil nicht in den ELSE-Teil läuft.

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:

; Test2a.asm
; Taster, LED Test fuer 65C02, WDC 65C51

        .OPT    Proc65c02
        .ORG    $0300
   
forever:                ; for(;;) {
        LDX     #$03    ;   X = RTS_High
        LDA     $8001   ;   A = ACIA_STATUS
        AND     #$40    ;   A &= DSR_MASK
        BNE     endif    ;   if (A != 0) {
        LDX     #$0B    ;     X = RTS_Low
endif:                  ;   }
        STX     $8002   ;   ACIA_COMMAND = X
        BRA     forever ; }

        .END

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


RAM Speicher testen

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
:

Programm Zufallszahl

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.

Zahlen raten Programm

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 ...

Besonderheiten Hardware-Handshake

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".



Der WDC 65C51 Transmitter Data Register Empty Bug

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.



Wozmon Erweiterung ACIA Empfang mit Interrupt

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:

; memory necessary for 65C51N interrupt receive
monwrptr= $E6   ; 2 bytes write pointer used by getc
monrdptr= $E8   ; 2 bytes read pointer used by getc
monbfcnt= $EA   ; 1 byte buf count used by getc
monbuf  = $0200 ; 64 bytes used by getc
                ; must be on page boundery: $XX00
JMPIRQ  = $0240 ; 3 bytes used for IRQ redirect


Die ersten drei "Datenobjekte" liegen in der Zero Page. Das erste monwrptr und zweite monrdptr Datenobjekt sind zwei 16-Bit Zeiger. Der 8-Bit Wert monbfcnt meldet die Anzahl der gespeicherten Bytes. Beim original Wozmon zeigt der "IRQ Vektor" auf die Adresse $0000. Diese Adresse wird aber von TinyBasic genutzt. An der Adresse $0240 wird ein superkurzes Programm abgelegt: unbedingter Sprung zum Interrupt Handler im EEPROM. Ab der Speicheradresse monbuf liegt ein 64 Bytes langer Zwischenspeicher. Wird ein Byte in den Ringbuffer geschrieben, wird Zeiger monwrptr erhöht. Wird ein Byte gelesen, wird Zeiger monrdptr erhöht. Der Lesezeiger "läuft" dem Schreibzeiger hinterher. Am Ende des monbuf Bereiches "springen" die Zeiger wieder auf den Anfang, deshalb der Name Ringpuffer. Der Ringpuffer muß auf einer Adresse $XX00 beginnen. Das macht Zeigerverwaltung sehr einfach.


; 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.

Schaltbild 7-chips

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 ...

Mehr RAM Speicher

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.

BASIC Interpreter für 6502

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.

TinyBasic

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.




Das Bildschirmfoto zeigt rnd.bas im Simulator. Dadurch wird die Eingabe abgebrochen. Das zweite Programm hex.bas erzeugt einen Hex-Dump. Hier zeigen sich die Schwächen von TinyBasic. Die Sprachdefinition von Dennis Allison erlaubte für den BASIC Befehl INPUT nur Integer Zahlen. Tom Pittman, bei seiner 6502 Assembler Implementierung von TinyBasic, nutze die BASIC USR() Funktion und kleine Assembler Programme in TinyBasic um erstens PEEK und POKE zu implementieren und zweitens über USR() PEEK Benutzereingaben aus dem Eingabezeile Puffer zu holen. Eine klassische "von hinten durch die Brust ins Auge" Lösung. Für den USR() PEEK Trick ist die Anfangsadresse von TinyBasic nötig. Traditionell wird diese Adresse in der TinyBasic Variable S abgelegt. Auf dem 6502 Computer mit 32 KByte RAM beginnt die RAM Version von TinyBasic auf Adresse $0290 oder 656 dezimal.




Das Bildschirmfoto zeigt hex.bas in TeraTerm. Anstelle von Ziffer 0 muß ein Großbuchstabe O eingeben werden. Wer kompliziert anfängt, muß auch so weitermachen! Nach überwundenen Hürden läuft das Programm 2026 so schön wie 1976 und zeigt den Anfang von Tiny Basic als Hex-Dump.

Bemerkung: Bei TinyBasic muß eine FOR NEXT Schleife durch IF GOTO ersetzt werden. Leerzeichen sind bei TinyBasic nicht nötig und PRINT kann zu PR abgekürzt werden.




Das Bildschirmfoto zeigt first.bas in TeraTerm. Der Fehlercode !95 AT bedeutet: "IF not followed by END" laut User Manual. Das vielleicht größte TinyBasic Programm war Adventure von Tom Pittman, beschrieben in seinem Buch The First Book of Tiny BASIC Programs . In der Tradition der Zork Textadventures wird ein Farmhaus und Umgebung erforscht. Anstelle von Verb wird nur der erste Buchstabe (in Großbuchstaben) von Verb eingegeben, d.h. G für go (gehe), A für attack (Angriff), usw. Nach G für go wird die Richtung abgefragt. Dann N für North (Norden) usw. eingeben. TinyBasic hat auch den kleinsten Textparser ...




Das Bildschirmfoto zeigt den ersten Bildschirminhalt von adventure.bas von Tom Pittman aus dem Jahr 1981. Diesmal liegt die TinyBasic Startadresse plus PEEK Offset in Variable Z. Das Strategiespiel euphoria.bas aus dem Tom Pittman Buch The First Book of Tiny BASIC Programs läßt den Spieler über ein Königreich herrschen. Die Einnahmen des Königreiches müssen sinnvoll ausgegeben werden für Ernährung, Erweiterung, Eroberung oder Verteidigung usw.




Die "Y" oder "N" Fragen bei Euphoria funktionieren ohne USR() PEEK Trick. Wie genau ist mir noch nicht klar. Kleine Fehler im Basic Quelltext wurden repariert. Die OCR Software hat manchmal "l" (kleines L) mit "1" (Ziffer 1) verwechselt. Das Spiel Tic-Tac-Toe gibt es auch für TinyBasic.




Bei Tic-Tac-Toe enthält Variable I die "TinyBasic Cold Start" Adresse. Die Implementierung hat eine Schwäche. Die Eingaben 1, 9, 3, 2 gewinnen.

BASIC Zeile suchen

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.

Bei Assemblerprogrammierung wird üblicherweise "jump away" benutzt. Die 6502 kann bei unsigned 16-Bit Vergleichen am besten einen "bedingten Sprung wenn a != b" und einen "bedingten Sprung wenn a < b" ausführen. Deshalb wurden im C Quelltext nur die "no jump" Fälle "a == b" und "a >= b" benutzt.

Zuerst ein "Implementierungs"-Codefragment welches "jump away" für 16-bit ungleich oder "no jump" für gleich zeigt:

        STA     LnOut
        CMP     LnIn            ;    if (LnOut == LnIn) {
        BNE     fndLn4
        LDA     LnOut+1
        CMP     LnIn+1
        BNE     fndLn4
        LDA     #0              ;      return 0; // and LnOut, pLnO
        RTS                     ;    }
fndLn4


Zuerst werden die 8-bit Werte LnOut und LnIn verglichen, die unteren 8-bit der 16-bit Werte. Sind diese unterschiedlich, wird zu fndLn4 gesprungen und die Verarbeitung von Fall 2 ausgeführt. Waren LnOut und LnIn gleich, werden die 8-bit Werte LnOut+1 und LnIn+1 verglichen, die oberen 8-bit der 16-bit Werte. Nur wenn LnOut+1 und LnIn+1 auch gleich sind, wird A Register mit 0 geladen und die Subroutine mit RTS beendet.

Nun ein Codefragment für "jump away" bei 16-bit kleiner oder "no jump" bei 16-bit größer-gleich:


        LDA     TEMP    ;   il accumulator, search line
        CMP     (BP),Y  ;   if (*BP >= TEMP) continue
        INY             ;   Y = 1
        LDA     TEMP+1
        SBC     (BP),Y
        BCC     SLICE

Auch die 65C02S hat nur die Kombination SEC und SBC für eine Subtraktion ohne Übertrag. Diese beiden Opcodes können durch CMP ersetzt werden, siehe Bruce Clark Comparison by subtraction . Zuerst werden die unteren 8-bit abgezogen, dann die oberen 8-bit. Das Ergebnis wird nicht genutzt, nur das Carry-Flag. Bemerkung: das Zero-Flag ist nur abhängig vom Ergebnis der Subtraktion der oberen 8-bit, ungewöhnlich für 8080, 8085, Z80 Assembler Programmierer.

Für das Erhöhen um 1 oder das Erniedrigen um 1 bei 16-bit Zahlen gibt es kleine 6502 Codefragmente. Zuerst Erhöhen:

        INC     pLnO            ;      ++pLnO;
        BNE     *+4
        INC     pLnO+1

Zuerst werden die unteren 8-bit erhöht. Ist das Ergebnis nicht Null ($00), dann wird der INC Opcode für die oberen 8-bit übersprungen. Das Sprungziel *+4 bedeutet "hinter den folgenden 2-byte Befehl". Nun Erniedrigen:

        DEC     pLnO            ;      --pLnO; // vermeide "Zeile 128" Fehler
        BNE     *+4
        DEC     pLnO+1          ;    }

Der 6502 wurde konsequent als 8-bit CPU geplant. Bei der 8080 CPU genügt ein 1-byte Befehl um eines der 3 16-bit Register zu erhöhen. Die 6502 kann 128 16-bit "Register" in der Zero page unterbringen, benötigt aber 6-bytes für Increment oder Decrement.

to be continued ...

TinyBasic Historie

Dennis Allison schrieb über die TinyBasic Herkunft: "The IL approach to implementation is quite standard and dates back to Schorre's META II, Gleenie's Syntax Machine". Der Glennie Bericht "On the Syntax Machine and the Construction of a Universal Compiler" ist von 1960. Der Schorre Bericht "META II A Syntax-oriented Compiler Writing Language" ist von 1964. Zum Vergleich: FORTRAN ist frühestens von 1956. Eine teilweise Implementierung der Syntax Machine lief auf einem Röhrencomputer IBM 650, die vollständige Implementierung von Meta II lief auf einem Transistorcomputer IBM 1401, beides Computer lange vor der IBM 360.
Wie bekannt ist FORTRAN nicht-rekursiv und der Kellerspeicher Expression-Evaluator von Bauer und Samuelson ist auch nicht rekursiv, siehe Patent DE1094019B. Die von Glennie und Schorre vorgestellten Meta-Programmiersprachen sind beide rekursiv. Durch die Rekursion werden einfache Details schwieriger, aber schwierige Details einfacher.
Das Allison Design Dokument benutzt eine andere IL (Intermediate Language) als Pittman in seinem TinyBasic für CPUs Motorola 6800, MOS Technology 6502 und RCA 1802. Die grundlegende Idee ist in beiden Fällen gleich: Suche ein Schlüsselwort wie "INPUT" oder "RETURN" im BASIC Programm. Wenn das Schlüsselwort erkannt wurde, führe den nächsten IL Befehl aus, sonst springe zu einer anderen Stelle im IL Programm. Nach dem Schlüsselwort "INPUT" wird ein Variablen-Name erwartet. Nach dem Schlüsselwort "RETURN" ein Zeilenende-Zeichen.

Allison IL
Pittman IL
TST     S12,'INPUT'
TSTV    S17

BC RETN "INPUT"
BV *

TST     S13,'RETURN'
DONE

BC END "RETURN"
BE *



Die Dokumentation zu TST ist:
TST lbl,'string'        delete leading blanks
                        If string matches the BASIC line, advance cursor over the
                        matched string and execute the next IL instruction If a match fails,
                        execute the IL instruction at the labled lbl.


Die Dokumentation zu BC ist:
BC a "xxx"   80xxxxXx-9FxxxxXx  String Match Branch.
                                The ASCII character string in the IL
following this opcode is compared to the string beginning with the
current position of the BASIC pointer, ignoring blanks in the BASIC
program. The comparison continues until either a mismatch is found,
or an IL byte is reached with the most significant bit set to one.
This is the last byte of the string in the IL, and it is compared as
a 7-bit character; if equal, the BASIC pointer is positioned after
the last matching character in the BASIC program and the IL program
continues with the next instruction in sequence. Otherwise the BASIC
pointer is not altered and the low five bits of the Branch opcode
are added to the IL program counter to form the address of the next
IL instruction. If the strings do not match and the branch offset is
zero an error stop occurs.

Die Dokumentation von Pittman ist genauer als die Dokumentation von Allison.

Mit der IL Befehle Dokumentation und dem IL Programm können Interpreter und Compiler für verschiedene CPUs gebaut werden. Die anspruchsvolle Aufgabe wird aufgeteilt: in den mehrfach verwendbaren IL Teil und in den speziellen CPU Teil. Ob ein Compiler/Interpreter mit IL langsamer ist, hängt von Details ab. Wenn der IL Teil als Assembler Makros implementiert wird, gibt es keinen Geschwindigkeitsnachteil. Eine Bytecode Implementierung erzeugt das kleinste IL Programm mit der größten Laufzeit. Eine DTC (Direct Threaded Code) Implementierung liegt zwischen diesen Extremen.

Neue TinyBasic Implementierung

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:

SU:     PLA             ; TOSL = *++PSP - TOSL
        SEC
        SBC     TOS
        STA     TOS
        PLA             ; TOSH = *++PSP - TOSH
        SBC     TOS+1
        STA     TOS+1
        JMP     NEXT


Subtraktion auf der 6502 mit einem Zahlenstack geht nicht schneller. Nun ein kleines DTC Programm:

STRT:   .word   PC
        .db             "Number", ':'+$80
        .word   GL
        .word   BN
        .db             NaN-*
        .word   LN
        .word           42
        .word   SU
        .word   PC
        .db             $0D, $0A, "minus 42", '='+$80
        .word   PN
        .word   PC
        .db             $0D, $0A, "ok", $0D, $0A+$80
        .word   BR
        .db             STRT-*

NaN:    .word   PC
        .db             $0D, $0A, "NaN", $0D, $0A+$80
        .word   FIN


Zuerst wird eine Stringkonstante mit PC ausgegeben. Stringende wird mit gesetzten Bit 7 erkannt. Dann Eingabezeile initialisieren mit GL und Zahl abfragen mit BN. Die Zahl kommt auf den Parameterstack (PS). Wenn keine Zahl erkannt wird, dann Sprung zum Label NaN, sonst wird mit LN die Zahlenkonstante 42 auch auf den PS gelegt. SU subtrahiert beide Zahlen und legt das Ergebnis auf den PS. Nach Stringkonstanteausgabe mit PC erfolgt Zahlenausgabe von PS mit OPcode PN. Der unbedingte Sprung BR erzeugt eine Endlosschleife im IL Programm. IL Unterprogrammaufruf und Return gibt es auch. Nach dem Label NaN erfolgt Stringkonstanteausgabe und Beenden des IL Programms mit FIN. DTC IL ist immer noch Assemblersprache, ein Bytecode Generator ist nicht nötig. Der * bedeutet "aktueller PC (Program Counter)". Die Abstraktion ist bei IL höher als bei Assembler.




Als Bildschirmfoto ein Programmlauf im 6502 Simulator. Von der eingegebenen Zahl wird 42 abgezogen. Es ist signed 16-bit implementiert, deshalb ergibt 0 - 42 = -42. Eingabe von negativen Zahlen ist noch nicht möglich. Die Eingabe von "x" wird als Nicht-Zahl erkannt und führt zu Programmende. Der Text "bye" wird durch Assemblerbefehle erzeugt. Der Quelltext dtc_test1.asm enthält schon signed 16-bit Multiplikation und Division. Die unsigned Versionen sind von der Seite Multiplying and Dividing on the 6502 von Neil Parker.

DTC TinyBasic Version 0.1

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.

to be continued ...

Zufallszahl erzeugen

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.

to be continued ...