6502-Crossdevelopment mit Common Lisp

  • Hallo Retrohasen,


    heute morgen habe ich ein paar Stunden mit meiner Lieblingsprogrammiersprache gespielt und das hat mir wieder so gut gefallen, dass ich Euch teilhaben lassen möchte:


    Die Vorgeschichte:


    Mein Apple IIe steht gegenüber meinem Schreibtisch und wenn ich in Zoom-Calls bin, kann man ihn sehen. Daher dachte ich, ein bisschen Eye-Candy auf dem Grünmonitor wäre ganz nett. Da ich erstmal keine Disketten habe, habe ich so eine Art Matrix-Screensaver eingetippt. Hübsch, aber leider lähmend langsam. Dann konnte ich ein paar Disketten auftreiben und habe das Ding mit dem Microsoft-BASIC-Compiler übersetzt - Hoffend, dass die Performance dann annehmbarer sein könnte. Leider war das nicht der Fall, so dass ich das Ding dann in Metacrafts FORTH nachgebaut habe. Ich hab's einem Freund gezeigt, und der regte dann an, ich könne ja auch mal Conway´s Game of Life schreiben - Gesagt, getan, aber dafür ist FORTH wiederum zu langsam, zumal Metacrafts FORTH auch keine Primitiven zur Bitmanipulation hat.


    Also muss es also Assembler sein - ein willkommener Anlass, meine 6502-Allergie zu überwinden, natürlich viel zu spät, aber egal. Ich bin zwar ein Fan davon, Sachen direkt auf dem Zielsystem zu entwickeln, aber es erschien mir doch zu unpraktikabel und zeitraubend, auf dem Apple einen geschmeidigen Workflow aufzusetzen - Zumal ich immer noch nur ein Diskettenlaufwerk habe und sich die Disketten beim Entwickeln mit FORTH auch als nicht wirklich komplett zuverlässig erwiesen haben.


    Erstmal habe ich mit virtual 6502 auf masswerk.at angefangen. Für kleinere Sachen geht das sehr gut, aber insgesamt ist es dann doch relativ viel nervige Herumklickerei und eine HTML-Textarea ist auch nicht der beste Platz für ein größer werdendes Programm. Ich habe mich also nach Alternativen umgesehen und die recht populäre lib6502-Bibliothek gefunden. Gut geschrieben und dokumentiert, aber dann eben leider doch in C und entsprechend nicht sehr geschmeidig in der Handhabung, zumal man eben auch noch einen externen Assembler braucht. Klar, gibt es alles irgendwie integriert für VScode, aber von den entsprechenden Angeboten hat mich nichts schnell überzeugen können.


    cl-6502 erweitern


    Die einzige Programmiersprache, die mich immer wieder begeistern kann, ist Common Lisp. Kommerzielle Projekte sind damit im Moment nicht realistisch zu bauen, aber in meiner Freizeit kann mir das ja Wurst sein. Ich habe also cl-6502 von Brit Butler hergenommen und probiert, ob ich meine stümperhaften 6502-Life-Rudimente damit übersetzt bekomme. Zu meiner Überraschung akzeptierte der Assembler meinen Code ohne Fehlermeldung, was aber daran lag, dass er die Assemblerdirektiven einfach ignoriert und überlesen hat. Immerhin, mit den 6502-Instruktionen und der Syntax für die Adressierungsmodi kam er zurecht. Also musste ich nur die notwendigen Assemblerdirektiven nachrüsten, und will Euch hier berichten, was ich dazu in dem Lisp-Programm tun durfte:


    Für die Implementation der Assemblerdirektiven habe ich erstmal eine passende generische Funktion definiert:


    (defgeneric process-directive (name value pc))


    Die generische Funktion process-directive erhält als name Parameter den Namen der Direktive. In value werden die vorhandenen Argumente übergeben, pc ist der aktuelle Program Counter. Die Funktion kann jedoch erst aufgerufen werden, wenn eine oder mehrere Methoden existieren. Welche Methode ausgewählt wird, hängt von den Typen der Parameter ab. Im Gegensatz zu den meisten anderen Sprachen ist es dabei möglich, über mehrere Parameter zu dispatchen, aber diese Möglichkeit brauche ich hier nicht. Ich hätte aber schon gerne eine Fehlermeldung, wenn ich eine unbekannte Assemblerdirektive benutze, daher definiere ich eine Methode, die für beliebige Argumenttypen und -werte passt:


    (defmethod process-directive (name value pc)

    (format t "Unknown assembly directive ~A skipped~%" name)

    nil)


    Die einzelnen Direktiven definiere ich nun als weitere Methoden der generischen Funktion process-directive, wobei das Dispatching nicht über einen Typ, sondern über einen Literalwert vorgenommen wird. Beispielsweise könnte ich eine Direktive ".byte" so definieren:


    (defmethod process-directive ((name (eql :byte)) value pc)

    ;; whatever

    )


    Die Argumentliste bestimmt, dass diese Methode aufgerufen wird, wenn der name-Parameter das Keyword :byte ist. Ein Aufruf (process-directive :byte x y) würde also diese Methode ansprechen, ein Aufruf (process-directive :word x y) jedoch nicht. Das passiert zur Laufzeit und ist effizient. Die Methodendefinition ist aber ein bisschen schlecht zu lesen, da man den Namen der Direktive in der Argumentliste zwischen den ganzen Klammern finden muss. Mit einem kleinen Makro lässt sich das aber verschönern:


    (defmacro defdirective (name (value pc) &body body)

    `(defmethod process-directive ((,(gensym) (eql ,name)) ,value ,pc)

    ,@body))


    Makros werden zur Compilezeit auswertet, und das hier definierte Makro defdirective sorgt dafür, dass wir die Methode von oben wie folgt definieren können:


    (defdirective :byte (value pc)
    ;; whatever

    )


    Es gehört natürlich noch ein bisschen mehr dazu, dem Assembler die neuen Assemblerdirektiven beizubringen, aber wirklich nur ein bisschen (siehe Link). Damit wird mein Programmfragment korrekt übersetzt und erzeugt den gleichen Maschinencode wie virtual 6502.


    Jetzt möchte ich auch noch komfortabel und interaktiv entwickeln und testen. Das geht direkt aus der Kommandozeile meines Lisp-Systems ("REPL - Read Eval Print Loop"):


    Erstmal assembliere ich mein Programm und erhalte als Ergebnis einen Vektor mit den Maschinensprache-Bytes:


    MAIN> (cl-6502:asm (read-file-into-string "life.s"))

    #(76 0 16 0 1 0 0 0 0 36 0 32 0 0 0 0 0 0 0 0 ...)


    Dann kopiere ich diesen Vektor in den Speicher der emulierten 6502:


    MAIN> (setf (cl-6502:get-range 0) *)

    #(76 0 16 0 1 0 0 0 0 36 0 32 0 0 0 0 0 0 0 0 ...)


    Nun lasse ich die 6502 laufen, bis sie auf eine BRK-Instruktion trifft.


    MAIN> (cl-6502:execute cl-6502:*cpu*)

    NIL


    Und jetzt schaue ich, was sich im Speicher ab $2400 befindet - Das ist mein aktuelles Ergebnis, viel kann ich noch nicht :)


    MAIN> (cl-6502:get-range #x2400)

    #(3 3 3 3 3 3 3 3 0 0 0 0 0 0 0 0 0 0 0 0 ...)


    Jetzt würde ich gerne noch Debuggen können, falls doch der unwahrscheinliche Fall eintritt und ich einen Fehler in meinem 6502-Code habe. Es wäre nützlich, wenn ich mir den Zustand der emulierten 6502 ansehen könnte, und dazu definiere ich mir eine Methode für die Systemfunktion print-object, die für Objekte der Klasse 6502:cpu verwendet wird. Ich kann nun die (globale) Variable cl-6502:*cpu* in der REPL evaluieren, und bekomme den Status der CPU ausgegeben:


    MAIN> cl-6502:*cpu*

    #<CPU PC:$0000 A:$03 X:$03 Y:$05 SR:$37 (CZI.B-..) SP:$F7 (6,014) - JMP $1000>


    Jetzt würde ich beim Debuggen gerne meinem Code bei der Ausführung zugucken. Dazu bietet es sich an, die Funktion cl-6502:step-cpu zu tracen - Sie wird in der inneren Schleife des Emulators für jede auszuführende 6502-Instruktion aufgerufen:


    MAIN> (trace 6502:step-cpu)

    (|6502|:STEP-CPU)

    MAIN> (cl-6502:reset cl-6502:*cpu*)

    #<CPU PC:$0000 A:$00 X:$00 Y:$00 SR:$24 (..I..-..) SP:$FD (0) - JMP $1000>

    MAIN> (cl-6502:execute cl-6502:*cpu*)

    0: (|6502|:STEP-CPU #<CPU PC:$0000 A:$00 X:$00 Y:$00 SR:$24 (..I..-..) SP:$FD (0) - JMP $1000> 76)

    0: |6502|:STEP-CPU returned 3

    0: (|6502|:STEP-CPU #<CPU PC:$1000 A:$00 X:$00 Y:$00 SR:$24 (..I..-..) SP:$FD (3) - LDA #$00> 169)

    0: |6502|:STEP-CPU returned 5

    ...


    Nicht schlecht, aber irgendwie doch nicht so ergonomisch, denn die Details des Aufrufs von step-cpu interessieren mich ja nicht, sondern eigentlich nur der Prozessorzustand. Also muss ich trace ein bisschen besser parametrisieren:


    MAIN> (trace 6502:step-cpu :report nil :print 6502:*cpu*)

    WARNING: |6502|:STEP-CPU is already TRACE'd, untracing it first.

    (|6502|:STEP-CPU)

    MAIN> (cl-6502:reset cl-6502:*cpu*)

    #<CPU PC:$0000 A:$00 X:$00 Y:$00 SR:$24 (..I..-..) SP:$FD (0) - JMP $1000>

    MAIN> (cl-6502:execute cl-6502:*cpu*)

    #<CPU PC:$0000 A:$00 X:$00 Y:$00 SR:$24 (..I..-..) SP:$FD (0) - JMP $1000>

    #<CPU PC:$1000 A:$00 X:$00 Y:$00 SR:$24 (..I..-..) SP:$FD (3) - LDA #$00>

    ...


    Jetzt wird also vor jeder Ausführung von step-cpu der Status der CPU ausgegeben, und dank der definierten print-object-Methode enthält die Ausgabe fast alles, was man so wissen möchte. trace kann aber natürlich auch noch mehr, ist aber Implementations-spezifisch (d.h. unterschiedliche Common-Lisp-Implementationen unterstützen unterschiedliche trace-Optionen). Ich habe dazu vor Jahren einen Blogpost geschrieben.


    So, und nun habe ich meine alte Liebe zu Common Lisp ausgelebt und morgen früh kann ich mich der Bitfummelei widmen. Danke für´s Lesen bis hier, und lasst es mich wissen, wenn ich irgendwelche Fragen zu dem Zeug beantworten kann.


    -Hans