Hello, Guest the thread was viewed9.9k times and contains 67 replies

last post from 1570 at the

C64-Rebuild auf 3,3V-Basis mit X uCs möglich?

  • Wäre es eigentlich möglich, einen timingexakten C64-Klon auf Microcontroller-Basis zu basteln? Ich denke da an sowas wie

    • Busse wie beim Original-C64, nur auf 3,3V-Basis
    • CPU, SID, CIAs, (VIC?), ROMs etc. jeweils durch einen Microcontroller (z.B. RPi 2040) emuliert, also MEHRERE Microcontroller insgesamt
    • Ggf. Vereinfachung: VIC nutzt (nur) 16-Bit-Adressbus, nicht CAS/RAS; Refresh entfallen lassen, SRAM benutzen (ähnlich wie bei der Max Machine)
    • Umsetzung von Userport/Expansionsport/IEC via Level-Shifter

    Vorteil zu FPGA: sehr hackable und günstig (RPi 2040 kostet 1€). Könnte man evtl. sogar beinahe rein als mehrere "gestackte" RPi Picos ausführen...?


    Bin mir nur nicht sicher, ob man das mit den Timinganforderungen hinbekommt und ob die GPIOs reichen (speziell beim VIC).


    Edit: Wobei man ja z.B. die 16 Adressleitungen vermutlich - wenn man schon die 2040er mit ihren PIOs und anderen Bussen hat - auch durch was mit geringerem Pin Count ersetzen kann...

  • Ein paar weitere Gedanken:

    • Eigene Chips für ROMs und RAM ganz weglassen
    • Um Timing im Emulations-Code zu entspannen, Daten soweit möglich "dezentral"/gespiegelt vorhalten. Exaktes Timing kann dann über die PIOs des RP2040 gemacht werden, während der C-Code nur die PIO-FIFOs bearbeiten muss.


      • KERNAL-ROM, BASIC-ROM, Charset-ROM direkt im CPU-µC
      • Charset-ROM (auch) direkt im VIC-µC
      • RAM sowohl im CPU-µC als auch im VIC-µC
    • Um GPIOs zu sparen alle Signale z.B. zum Pixeltakt multiplexen
    • VIC auftrennen in "PAL"-Generator (µC, der digital Pixelstream ausgibt) und HDMI-Konverter (liest Pixelstream und erzeugt HDMI-Signal, siehe PicoDVI - geht sogar mit Ton)

    Meilensteine wären zum Beispiel:

    1. CPU/RAM/ROM-µC läuft mit "Fake"-CIA - das sollte sogar z.B. rein auf Online-Emulator https://wokwi.com/ hinzubekommen sein (der kann z.Zt. leider nur einen µC gleichzeitig emulieren)
    2. CPU+VIC gibt Signal aus
    3. CPU+VIC+CIA1 mit Tastatur-Emulation
    4. CPU+VIC+CIAs kompatibel mit Original-Floppy
    5. CPU+VIC+CIAs kompatibel mit einfachen Expansionsport-Modulen (ROMs)
    6. Kompatibilität mit weiteren Expansionsport-Modulen (EasyFlash1, Sidekick)
    7. Kompatibilität mit weiteren Expansionsport-Modulen (EasyFlash3, REU, SuperCPU)
  • Um Timing im Emulations-Code zu entspannen, Daten soweit möglich "dezentral"/gespiegelt vorhalten. Exaktes Timing kann dann über die PIOs des RP2040 gemacht werden, während der C-Code nur die PIO-FIFOs bearbeiten muss.

    Speicher spiegeln macht Sinn (mache ich z.B. auch im Sidekick20 und mehr oder weniger an anderen Stellen der Erweiterungen (*)). Die PIOs können das Timing erleichtern, aber Du musst noch die Fälle berücksichtigen bei denen ein Baustein Daten vom Bus lesen und die "Antwort" auf den Bus schreiben muss (z.B. ein Register lesen). Inwieweit da die PIOs helfen kann ich nicht sagen. Das 8k-Cartridge auf Basis eines RP2040 verwendet m.W.n. nur PIOs und DMA, da ist vermutlich nicht viel mehr als Speicherauslesen zu machen.


    (*) z.B. bei der 6510-Emulation oder dem CPU-Beschleuniger auf dem RAD


    Hm, ich hatte das andere Projekt nicht als Chip-Replacements gedacht. Die Idee wäre, da etwas zu bauen, das hackable und günstig² ist und trotzdem IEC/Expansionsport/Userport mit exaktem Timing anbietet

    den Punkt sehe ich -- aber warum wäre es dann nicht eine Option (zumindest mal darüber nachzudenken) nur einen größeren "uC" zu verwenden und die Schnittstellen anzubieten? Oder ggf., wenn Timing-kritisch, mehrere uCs, deren einzelne Aufgaben nicht notwendigerweise denen der MOS-ICs genau entsprechen?

  • Du musst noch die Fälle berücksichtigen bei denen ein Baustein Daten vom Bus lesen und die "Antwort" auf den Bus schreiben muss (z.B. ein Register lesen).

    Du meinst sowas wie INC $D020? Ja, da muss man mal schauen. Eventuell kann man dafür spezialisierte PIO-Unterroutinen basteln, wobei die Länge der PIO-Programme ja sehr begrenzt ist. Ich kenne mich damit noch nicht gut genug aus, um das einzuschätzen. Guter Hinweis jedenfalls!

    wäre es dann nicht eine Option (zumindest mal darüber nachzudenken) nur einen größeren "uC" zu verwenden und die Schnittstellen anzubieten?

    Ach ich finde die Herausforderungen, die man sich damit einfängt (Timing im Zusammenhang mit Caches etc., da weißt Du ja ein Lied von zu singen) einfach nicht so charmant im Vergleich zum Spielen mit dem RP2040. Außerdem ist die Langzeitverfügbarkeit und Preis des RP2040 vermutlich kaum zu toppen. Nur in Sachen RAM (und eingebautes Flash) ist er etwas mickrig, aber das spielt für den Anwendungsfall hier (vermutlich) keine Rolle, solange man z.B. nicht noch direkt eine REU emulieren möchte (für die müsste man wohl noch irgendwie externes RAM anbauen).

    Oder ggf., wenn Timing-kritisch, mehrere uCs, deren einzelne Aufgaben nicht notwendigerweise denen der MOS-ICs genau entsprechen?

    Ja, genau dahin geht die Reise hier denke ich. :) Ein Stück weit wäre so ein Verhau von mehreren RP2040 ja auch komplett in Software konfigurierbar, sodass man die Aufgaben einfach durch geänderte "Firmware" verschieben kann wie man will. Im Zweifelsfall halt einen eigenen RP2040 nur für RMW-Instruktionen. ;)

  • Das mit RAM/ROM einfach spiegeln klappt hier leider doch nicht im allgemeinen, weil z.B. im Ultimax-Modus das RAM extern sein kann und das CharROM sogar definitiv ist. Also muss die "CPU" doch immer über den Bus gehen.

    Die PIOs kann man dann wohl nur zum "leichten" Entkoppeln des Timings nehmen, sodass man immerhin im Emulations-C-Code keine Zyklen zählen muss oder so.

    Ich bastle gerade mal ein winziges internes Busprotokoll. Könnte eventuell mit gerade mal 11 GPIOs auskommen mit etwas Glück.

    Eine volle CIA-Emulation mit ihren eigenen 21 nach außen sichbaren Pins wird aber auch dann schon knapp, muss man PA/PB irgendwie multiplexen... aber das ist erstmal was für viel später.

  • Ich hab mal über die Feiertage etwas gebastelt. Die Idee ist erstmal (aufbauend auf Chips-Code):

    • erster RP2040 für CPU, PLA, RAM, CIAs (Core 1) sowie SID (Core 2)
    • zweiter RP2040 für VIC-II (Core 1) sowie PicoDVI (Core 2)
    • beide RP2040 per an C64-Verhältnisse angelehnten Bus (gemultiplext, 8 Bit :) ) koppeln

    ...und das läuft auch schon zu sagen wir mal 70%. Insbesondere kann man den Emulator auch auf dem PC starten - dort wird der Bus über SHM nachgebildet, und zwei separate Prozesse kommunizieren darüber (mit SID wären es drei, aber da hab ich noch nichts gemacht). Damit lässt sich dann schön fix und bequem entwickeln. Das Ganze läuft auch tatsächlich inkl. Datenfluss auf dem Bus so wie beim C64, also nix mit Tricks wie lokal im VIC gespiegeltem RAM oder ähnlichem. Bildausgabe erfolgt über ein sekündlich aktualisiertes BMP. :)


    Beide Teile des Emulators laufen auch schon auf dem RP2040 (sogar inkl. Grafikausgabe per PicoDVI!), allerdings ist die RP2040-Bus-Implementierung erst halb fertig (der Bus-RP2040-Code auf VIC-II-Seite fehlt noch, sollte aber überschaubar sein), gibt also nur Testbild im Moment.


    Performance-mäßig ist das allerdings alles sehr knapp:

    • Ein CPU-Zyklus (Phi High) braucht etwa 0.8µS (schon mit dem RP2040 auf 295MHz getaktet - was wegen PicoDVI sowieso gemacht werden muss und kein Problem ist). Dazu kommt dann noch Verschnitt durch das Interfacing mit dem Bus und PLA. Das müsste man noch auf rund die Hälfte drücken (Phi High = 0.5µS insgesamt im Original). Allerdings ist der Code absolut unoptimiert, und es gibt einige offensichtliche Optimierungsmöglichkeiten (z.B. sind etliche Variablen nicht static, und es gibt einige zentrale 64-Bit-Variablen inkl. viel Bitshifting und -Maskierung).
    • Noch knapper ist allerdings der VIC-II. Das Rendern von 50 Frames (also eine Sekunde auf dem C64) braucht z.Zt. auf dem RP2040 etwa 4 Sekunden, ganz ohne Bus-Overhead (und bisher nur Textmodus ohne Sprites). Ist aber bei gut 6 Millionen gezeichneten Pixeln pro Sekunde (auf dem C64) auch kein Wunder, da bleiben nur etwa 50 ARM-Zyklen pro Pixel, in denen im Worst Case Bitmapgrafik plus acht Sprites "angefasst" werden müssen PLUS ggf. Interaktion mit der CPU (ein POKE$D010,X ist ggf. richtig teuer). Aber auch hier gilt, dass etliches völlig unoptimiert ist, z.B. kann man den D010-Code sicher irgendwie optimieren, und auch die Tatsache, dass mein Glue-Code für m6569 zur Zeit nicht direkt in den Framebuffer von PicoDVI schreibt, sondern über einen Zwischenbuffer geht, ist nicht gerade optimal. So richtig sehe ich aber nicht, dass eine echt zyklengenaue Emulation auf dem RP2040 hier klappt. Vielleicht könnte man das eigentliche Rendering auf einem Core machen und Bus-Interfacing etc. auf dem anderen, wie in #2 angedacht. Aber dann bräuchte man schon drei RP2040 und noch einen weiteren (separaten) Bus zum PicoDVI-RP2040, was etwas hässlich wäre, und der Code würde dadurch auch wesentlich komplizierter.
    • Im Gegensatz zu normalen Emulatoren (für PC etc.) muss das Timing natürlich auch im Worst Case für jeden einzelnen Taktzyklus immer eingehalten werden, jedenfalls solange das mit Originalhardware irgendwann mal kompatibel sein soll.

    Falls jemand mitprogrammieren mag: Melden. :)

  • Nochmal kurz unter die Haube geschaut. In m6569.h braucht der typische VIC-Zyklus (damit meine ich das hier) um die 260 ARM-Takte und das zugehörige Textmodus-Decoding inkl. meinem etwas kaputten doppel-kopier-Hack etwa 1700 ARM-Takte. Insgesamt stehen für beides bei 295MHz aber eben nur 295 ARM-Takte zur Verfügung...


    Mit Sprites wird das Decoding auch nochmal deutlich länger brauchen. Das Decoding läuft pixelweise, das könnte man auf 8-Pixel-Blöcke umschreiben (dann hätte man hier keine for-Scheife mehr und stattdessen sowas wie _m6569_sunit_decode_8_pixels()).


    Hmhmhm...

  • Etwas am Dekodieren in m6569.h gebastelt. Sprites im Code ausgestellt: noch 670 Takte (8 Pixel x 8 Sprites = 64 Überprüfungen kosten Zeit, auch wenn alle False ergeben). Optimierter Code, der u.a. gunit_tick und gunit_decode_mode0 zusammenlegt: noch 380 Takte. Doppelkopiererei wegoptimiert: noch 340 Takte. Also wenigstens ohne Sprites und mit Benutzung beider Kerne ist das in Reichweite... ;) muss mir auch mal den erzeugten Code anschauen, kommt mir immer noch ziemlich viel vor.


    Erstaunliche Entdeckung am Rand gemacht: some_function(some_type *struct) { struct->bla++; } ist irgendwie insgesamt schneller als some_function() { global_struct.bla++; } ? Seltsam. Muss ich nochmal nachschauen.

  • Etwas am Dekodieren in m6569.h gebastelt. Sprites im Code ausgestellt: noch 670 Takte (8 Pixel x 8 Sprites = 64 Überprüfungen kosten Zeit, auch wenn alle False ergeben). Optimierter Code, der u.a. gunit_tick und gunit_decode_mode0 zusammenlegt: noch 380 Takte. Doppelkopiererei wegoptimiert: noch 340 Takte. Also wenigstens ohne Sprites und mit Benutzung beider Kerne ist das in Reichweite... ;) muss mir auch mal den erzeugten Code anschauen, kommt mir immer noch ziemlich viel vor.

    ich habe mit der VIC-II Emulation von Andre noch nicht experimentiert (nur mit der VIC-I-Emulation für's Sidekick20, die ich aber dann anders gelöst habe -- und die übrigens auch sehr zeitkritisch war), aber nur zur Sicherheit: die "assert" hast Du deaktiviert? Hatte ich zuerst mal übersehen und hatte die Code-Größe aufgebläht ...

  • Bin jetzt bei rund 280 Zyklen fürs Textmodus-Decode. Ohne Sprites. Aber auf die kann ich ggf. auch erstmal verzichten, Ziel ist für den Moment, dass Gold Quest 6 nicht völlig schnarchlangsam läuft, und das hat keine Sprites. :)


    ARM-Assembler und die ganze Toolchain ist noch etwas neu für mich. Wenn ich im gcc mit -S -O0 -fverbose-assembly den generierten Code anschaue, sieht der häufig etwas apokalyptisch aus:

    14 Instruktionen für ein simples OR von zwei Bools? Oder lese ich da grob was falsch? Die ersten beiden ldr/movs/ldrb lesen die Operanden? Kann man das irgendwie beschleunigen? Das wäre so eine Stelle gewesen, an der ich gedacht hätte, dass eine globale/statische Instanz von vic eigentlich was hätte bringen sollen...

  • Wenn du dem Compiler das Optimieren verbietest ist der erzeugte Code typischerweise nicht optimal.


    Die ersten beiden ldr/movs/ldrb lesen die Operanden?

    Sieht so aus.


    Kann man das irgendwie beschleunigen?

    Ja, mit -Os oder -O2 compilieren dürfte besseren Code erzeugen. Wenn man die Optimierungen verbietet erkennt der Compiler ja nicht mal, dass er die Basisadresse der Datenstruktur schon in einem Register hat und nicht nochmal neu erzeugen muss.


    Das wäre so eine Stelle gewesen, an der ich gedacht hätte, dass eine globale/statische Instanz von vic eigentlich was hätte bringen sollen...

    Bedenke, dass ARM-Instruktionen nur 16 oder 32 Bit breit sein können und eine Adresse 32 Bit braucht - die kann typischerweise nicht mit einer Immediate-Instruktion geladen werden sondern kommt oft aus Konstantenpools in Reichweite eines anderen Registers, zB PC.

  • Danke. Ich hatte allerdings den Eindruck, dass auch -O2 oder -O3 nicht wirklich tollen Code erzeugt, aber habe halt auch keine Ahung von ARM-Assembler. Ich habe den inneren Loop mal auf https://godbolt.org/z/8zodzjdG3 zum Spielen hochgeladen, da kann man auch fix andere Compiler etc. ausprobieren. Wenn wer was sieht... interessant beim Loop war auch, dass es schneller scheint, es so zu machen wie auf Godbolt gerade zu sehen (die Bitmaske für g_data/shift ändern und einmal am Schluss erst den Wert shiften), statt den Wert im Loop zu shiften.

  • Da jagt ein Rabbit Hole das nächste. :) Aber ich habe jetzt das ganze Ding (noch sehr langsam und unoptimiert) in einer Emulation (zwei rp2040js-Instanzen mit eigenen Bugfixes plus Glue Code zum Verbinden der beiden SoCs) laufen. Mal schauen, wann ich die Nerven habe, das auf echter Hardware auszuprobieren...


    Demnächst kommt übrigens der ESP32-P4 raus. Zweimal 400MHz RISC-V, der sollte dann hoffentlich deutlich mehr als die rund 800 Coremark des (übertakteten) RP2040 schaffen und wird vermutlich auch sehr günstig sein. Also selbst wenn der 2040 sich definitiv als zu langsam erweisen sollte, löst sich das Problem grundsätzlich irgendwann von alleine. :)


    Oh und was mir noch als Optimierungspotenzial aufgefallen ist: Die ganzen uint8 und uint16 sind nicht toll, weil der Compiler dann ständig die nativen 32 Bit des ARM maskieren muss. Man müsste wohl den Code durchgehen und da wo möglich alles durch uint_fast8/16 ersetzen.

  • Läuft auf echter Hardware. SEHR langsam erstmal. :) Die Zahlen im Hintergrund sind $DC00, ausgelesen von einem "CRT" bei $8000, und die 247 kam durch etwas Wackeln an den GPIO-Pins des einen RP2040, die den Joystick-Port machen: Funktioniert. :)

    (das Bild wird per PicoDVI über DVI/HDMI ausgegeben, das angelötete Dings ist ein Pico DVI Sock - nur passive Bauteile drauf. Bild sieht etwas seltsam aus weil auf dem Hauptmonitor per PIP über dem Browser mit Forum64 eingeblendet.)


    Da arbeiten also zwei RP2040/RPi Pico mit einem 8-Bit-Bus gekoppelt zusammen, bisher werden drei von den vier CPU-Cores genutzt.

  • Ja, der Bus wird über die PIOs realisiert, allerdings können die einem leider in dem Fall nicht so viel Arbeit abnehmen, weil relativ viel "Reaktivität" gebraucht wird: "Wenn vom Bus gelesene Adresse VIC-Adresse ist und I/O der PLA aktiviert, dann lese ein Byte aus der VIC-Emulation" ist halt doch zum Großteil eine Aufgabe für C-Code, und dabei kann der "kritische Pfad" auch lang werden:

    1. CPU legt Adresse auf Bus
    2. Expansionsportmodul sieht Adresse, ändert EXROM/GAME-Leitungen
    3. PLA verarbeitet Adresse und EXROM/GAME/CPU-Port
    4. Adresse zeigt in offenen Adressbereich, da Modul den Ultimax-Modus gewählt hat: alle internen Enable-Leitungen bleiben inaktiv
    5. Steckmodul behandelt Leseanfrage, schreibt Daten auf Datenbus
    6. CPU liest Daten zurück

    ...und irgendwie "dazwischen" findet sich auch die Behandlung u.a. des BA-Signals des VIC-II. Ist also recht viel zu tun, und PIO entlastet eigentlich nur so richtig bei sehr einfachen Aufgaben wie "Per DMA in bestimmten Takt Daten auf GPIOs schreiben" (vielleicht noch mit einem "nur falls Pin X high"). Das ist hier aber leider viel komplexer.


    That said hat meine aktuelle Implementierung noch 1000 offensichtlich suboptimale Stellen, also mal schauen. Ich werde wohl als nächstes noch ein paar weitere GPIOs nehmen, um im Logic Analyzer klarer zu sehen, wann wo warum auf was gewartet wird.

  • Stand der Dinge: War etwas zäh. Ich habe versucht, fix ein paar Speedups zu bekommen, aber da verhaspelt sich der Bus nur. Ich weiß nicht genau, warum, weil ich zu faul bin, da mit einem echten (physischen) Logic Analyzer ranzugehen, und außerdem der Meinung bin, dass eigentlich die ganze Entwicklung im Emulator möglich zu sein hat. :)


    Bisher ist da eine Art Unit Test rausgekommen (automatisch testen können ist immer netter); ein Mini-Test-KERNAL, der nach nur wenigen Zyklen den VIC schon am Laufen hat (damit man in der Emulation nicht ewig warten muss, bis das alles in die Gänge kommt); und diverse Bugfixes für rp2040js. Unter anderem geht dort jetzt der Systick-Zähler - dessen Werte tatsächlich gar nicht so arg von der echten Hardware abweichen, sogar inkl. dem Zusammenspiel mit PIO, aber ich frage mich trotzdem, warum es die Abweichung (auch abseits von PIO) überhaupt gibt - Probleme mit Contention sollte es eigentlich Single Threaded und ohne DMA nicht geben (ich hab mir erstmal nur den CPU-RP2040-Code angesehen, auf dem VIC-RP2040 ist das komplizierter), oder?


    Der echte RP2040 scheint für den gleichen ARM-Code konsistent mehr Zyklen zu brauchen als die Emulation, also fehlen im cortex-m0-core.ts im simpelsten Fall nur irgendwo ein paar this.cycles++. Mal durchsehen bzw. Testfälle schreiben.


    Wobei die paar Zyklen (sind so 10% Unterschied) nicht unbedingt die Ursache für das aktuelle Problem sein müssen und beim Bus irgendwas anderes querliegen kann. Aber das werden die Tests dann zeigen - die testen im Betrieb, ob auf dem Bus auch genau die Signale kommen, die im Emulator verbrieft korrekt waren.

  • aber ich frage mich trotzdem, warum es die Abweichung (auch abseits von PIO) überhaupt gibt - Probleme mit Contention sollte es eigentlich Single Threaded und ohne DMA nicht geben

    Modelliert der Emulator denn auch die Waitstates der APB-Bridge und (falls verwendet) die zusätzlichen Zyklen und Waitstates für atomare Registermodifikationen (Datenblatt Sektion 2.1.2)?

  • Guter Hinweis! Das macht der Emulator nicht, aber die per APB angebunderen Peripherals werden während der Tests hier auch gar nicht benutzt (z.B. UART/USB wird erst nach den Messungen bzw. bei der Ausgabe der gesammelten Ergebnisse initialisiert, anders würde das im Betrieb auch zu sehr stören).


    Aber gerade in der Mittagspause den Krams mal durchlaufen lassen, und zwar passen die Zyklen beim ARM-Code nicht 100%ig in der Emulation, die PIO zieht das aber wieder glatt, da dort die Zyklenzählung stimmt (echt faszinierend, wenn über sechs 6510-Zyklen die gezählten ARM-Zyklen in rp2040js und auf echter Hardware exakt übereinstimmen). Ist halt die Preisfrage, was dann da auf der echten Hardware bei schnellerem Timing klemmt. Ein heißer Kandidat ist die letzte Busphase, in der die IRQ-Leitung behandelt wird - dann wird einer der GPIOs bei beiden MCUs auf Pullup gestellt, und ggf. zieht nur der VIC-RP2040 nach unten. Möglich, dass die Pullups dann nicht immer für ein sicheres "1" in der kurzen Zeit reichen. Aber selbst bei kaputter IRQ-Signalisierung sollte der VIC ja immer grundsätzlich korrektes Bild bringen und nicht z.B. (fälschlich) Nullbytes aus dem Bildschirmspeicher holen. Hm. Mal weiter testen am Wochenende.