pico6502 Entwicklerhandbuch

Übersicht

Die pico6502-Firmware ist strukturell identisch mit der picoZ80-Firmware aufgebaut. Sie verwendet die gleiche Dual-Core-RP2350B-Architektur, das gleiche PSRAM-Speichermodell, das gleiche JSON-Konfigurationssystem und das gleiche Treiberframework — die wesentlichen Unterschiede sind:
  • M6502CPU.c / M6502CPU.h ersetzen Z80CPU.c / Z80CPU.h als Haupt-Dispatch- und Treiberframework-Dateien.
  • Der 6502 hat keinen separaten I/O-Adressraum — es gibt kein ioPtr[]-Array und keinen iomap-JSON-Abschnitt. Alle Peripheriegeräte werden als Blöcke vom Typ FUNC in der 64KB-Speicherbelegung virtualisiert.
  • Das PIO-Bus-Interface (M6502.pio) implementiert eine Zweiphasen-PHI1/PHI2-Taktung anstelle des Z80-Einzeltaktmodells.
  • Das Build-Zielverzeichnis ist src/model/Base6502 statt BaseZ80; der Build-Befehl lautet ./build_tzpuPico.sh 6502.
  • Der JSON-CPU-Abschnittsschlüssel ist "6502" statt "z80" — wie z80[] ist er ein nach Flash-Partition indiziertes Array, und jeder Partitionseintrag trägt cputype ("6502" oder "6512") und dbeoverride.
Alles andere — das Inter-Core-Queue-Muster, die Treiber-Lifecycle-Callbacks (reset_ptr, poll_ptr, task_ptr), die virtualFuncMap[]-Registrierungstabelle, die Speicherblocktyp-Konstanten, die t_6502PSRAM-Struktur, das ESP32-Koprozessor-Protokoll und das Build-System — wird mit der picoZ80-Codebasis geteilt.

Quellbaum

Der gesamte Quellcode befindet sich unter projects/tzpuPico/ im Repository-Stammverzeichnis. Das folgende Layout hebt die pico6502-spezifischen Dateien hervor:
tzpuPico/
├── CMakeLists.txt                          Top-Level-Build-Datei
├── src/
│   ├── CMakeLists.txt                      Quellcode-Build-Datei — neue Treiberdateien hier hinzufügen
│   ├── M6502.c                             *** SCHLÜSSELDATEI: der 6502-Emulationskern — alle 151 offiziellen
│   │                                           Instruktionen + jeder undokumentierte Opcode; keine RP2350-
│   │                                           Abhängigkeit, baut daher nativ für Tests und den Simulator
│   ├── M6502CPU.c                          *** SCHLÜSSELDATEI: Core-1-Bus-Orchestrator, Treiberframework,
│   │                                           virtualFuncMap, Konfigurations-Parsing (Gegenstück zu Z80CPU.c)
│   ├── M6502dis.c                          Disassembler + Einzeilen-Assembler (Tabelle generiert aus
│   │                                           den eigenen Decode-Fällen von M6502.c)
│   ├── M6502.pio                           PIO-Bus-Engine — Pinbelegung des gepufferten v2.3-Interface; alle
│   │                                           Bus-SMs synchronisieren auf CLK2_IN
│   ├── dbgsh.c                             CPU-neutrale Debug-Shell (mit dem Z80 geteilt — Dispatch
│   │                                           über die t_CpuOps-Vtable)
│   ├── Z80CPU.c / Z80CPU.h                 Z80-Gegenstück (nicht in pico6502-Builds verwendet)
│   ├── FSPI.c / FSPI.h                     Flash-SPI-Interface (gemeinsam)
│   ├── ESP.c / ESP.h                       ESP32-Kommunikationsschicht (gemeinsam)
│   ├── psram.c / psram.h                   PSRAM-Allokation und -Verwaltung (gemeinsam)
│   ├── cJSON.c / cJSON.h                   JSON-Parser für config.json (gemeinsam)
│   ├── include/
│   │   ├── M6502CPU.h                      *** SCHLÜSSELDATEI: alle 6502-Typdefinitionen und Makros
│   │   ├── M6502.h                         Kern-Definitionen
│   │   ├── M6502dis.h                      Assembler-/Disassembler-API
│   │   ├── cpu.h                           t_CpuOps-Vtable + CPU_MEMBANKPTR()/CPU_PSRAM() —
│   │   │                                       das, was dbgsh CPU-neutral macht
│   │   ├── dbghooks.h                      Gemeinsame Breakpoint-/Debug-Hook-Definitionen
│   │   ├── flash_ram.h                     Flash-Konfigurationsspeicher + der nichtflüchtige BBC-Bereich
│   │   └── drivers/
│   │       ├── Other/
│   │       │   └── Open6502.h             Open6502-Treiber-Header
│   │       ├── I8271.h                    Intel-8271-Chipmodell-API (hostunabhängig)
│   │       ├── WD177x.h                   WD1770/1772/1773-Chipmodell-API (hostunabhängig)
│   │       ├── MC6854.h                   Motorola-MC6854-ADLC-Chipmodell-API (hostunabhängig)
│   │       └── Acorn/
│   │           ├── BBCModelB.h            BBC-B-Treiber-Header
│   │           ├── BBCCMT.h               UEF-Bandlaufwerk-Header
│   │           ├── BBCFDC.h               Header des virtuellen 8271-Floppy-Interface
│   │           ├── BBCMMC.h               Header der virtuellen MMFS-Karte
│   │           └── BBCNet.h               Header des virtuellen Econet-Moduls
│   ├── drivers/
│   │   ├── Other/
│   │   │   └── Open6502.c                 Vanilla-/Experimentier-Persona — ROM-Laden, Bank-0-ROM-
│   │   │                                       Schreibschutz (darauf läuft der Apple I)
│   │   ├── I8271.c                        Intel-8271-FDC-Chipmodell — Datenblatt-Befehlssatz, abstrakte Medien
│   │   ├── WD177x.c                       WD1770/1772/1773-FDC-Chipmodell — Typ I–IV, Roh-Spur-Lesen/Schreiben
│   │   ├── MC6854.c                       MC6854-ADLC-Chipmodell — FIFOs auf Frame-Ebene, Status, Interrupts
│   │   └── Acorn/
│   │       ├── BBCModelB.c                *** DER BBC-MODEL-B-TREIBER: Maschinenwissen für die Bus-
│   │       │                                   Engine (BBCModelB_Tuning), virtuelle Sideways-Sockel,
│   │       │                                   OS-Sockel, Model-A-RAM, Master-Modus, CMOS-/SWRAM-Persistenz
│   │       ├── BBCCMT.c                   *** ERSTES VIRTUELLES GERÄT: UEF-Bandlaufwerk — virtueller 6850-ACIA
│   │       │                                   + Serial ULA, virtuelle IRQ-Leitung, LOAD + SAVE, gzip-UEF
│   │       ├── BBCFDC.c                   Virtuelle Floppy-Anbindung: 8271 bei $FE80 oder 1770 + Latch (Acorn- /
│   │       │                                   Watford- / Master-Boards), INT→/NMI über den Geräte-Timer, zwei
│   │       │                                   Laufwerke aus SSD/DSD/DDS/DDD/ADF/ADL/MMB-Images (Seitenpaarung),
│   │       │                                   PSRAM-Write-Back-Cache
│   │       ├── BBCMMC.c                   Virtuelle MMFS-Karte: partielles User-VIA-Overlay, SPI-SD-Zustands-
│   │       │                                   automat, FAT16-Fassade über dem MMB, Write-Back-Cache
│   │       └── BBCNet.c                   Virtuelles Econet: ADLC bei $FEA0 + Stationsverknüpfungen, Vier-Wege-
│   │                                           Handshake ↔ AUN über den ESP32-UDP-Socket, PSRAM-Ringe
│   ├── test/
│   │   └── M6502/                         Host-native Konformitätstests (make -C src/test/M6502 run)
│   └── model/
│       ├── CMakeLists.txt                 Makro add_m6502_model_targets() — 4 Varianten pro Modell,
│       │                                       partielles LTO auf M6502.c + M6502CPU.c
│       ├── Base6502/                      Vanilla-Modell (INCLUDE_OPEN6502_DRIVERS)
│       │   ├── main.c                     Einstiegspunkt (Core-0-Service-Schleife + Core-1-Start, Flash-
│       │   │                                   Konfigurationsspeicher, Takt/Spannung, POR-Supervisor)
│       │   ├── main_memmap_partition_1.ld Linker-Skript für Slot 1
│       │   └── main_memmap_partition_2.ld Linker-Skript für Slot 2
│       ├── BBC6502/                       Base6502 + INCLUDE_ACORN_DRIVERS (verwendet Base6502/main.c wieder)
│       └── Bootloader/
│           ├── CMakeLists.txt
│           └── main.c                     Gemeinsamer Bootloader (gleich wie picoZ80)
├── sim/
│   ├── 6502/                              pico6502sim — eigenständiger Host-Simulator (siehe unten)
│   └── src/
│       ├── host_apple1.c                  Synthetischer Apple-I-Host
│       └── host_bbc.c                     Synthetischer BBC-Model-B-Host (System-VIA, Tastatur, MODE 7)
├── tools/econet/                          Mitgelieferte AUN-Daemons (aund, PiEconetBridge) + build.sh,
│                                               fileserver.sh, examples/, README.md
├── PIPELINE_REDESIGN.md                   Das Design-Protokoll der Bus-Engine-Geschwindigkeitskampagne
├── timing/, *.kvdat, *.py                 Logikanalysator-Aufzeichnungen (echter 6502 vs. pico6502) + Werkzeuge
└── esp32/
    ├── CMakeLists.txt
    ├── personas/                          config_<Partition1>_<Partition2>.json — Basic6502, Apple-I, BBC-B
    └── main/
        └── ...                            ESP32-Firmware (mit picoZ80 geteilt; CPU-bewusste Web-GUI)

Wesentliche Unterschiede zum picoZ80

Kein separater I/O-Adressraum

Der wichtigste architektonische Unterschied ist, dass der 6502 keine IN/OUT-Instruktionen und kein IORQ-Signal hat. Jedes Peripheriegerät muss irgendwo in der 64KB-Speicherbelegung erscheinen. Beim picoZ80 können virtuelle Geräte entweder über memioPtr[] (speicherabgebildet) oder über ioPtr[] (I/O-abgebildet) registriert werden. Beim pico6502 gibt es nur memioPtr[] — da aber alle Peripheriezugriffe über den Speicherbus erfolgen, deckt dies alle Fälle ab.
Die t_6502PSRAM-Struktur hat daher kein ioPtr[]-Array. Die JSON-Konfiguration hat kein "io"-Array. Die Dispatch-Schleife in M6502CPU.c prüft nur das _membankPtr[]-Array und ruft für MEMBANK_TYPE_FUNC-Blöcke den entsprechenden memioPtr[]-Handler auf.

PIO-Bus-Interface-Unterschiede

Der 6502 verwendet ein nicht-überlappendes Zweiphasen-Taktschema. Im 6502-Modus ist PHI0 ein externer Eingang, und der RP2350 erzeugt daraus PHI1 und PHI2 über eine PIO-2-Zustandsmaschine (m6502_clock_6502); im 6512-Modus liefert der Host beide Phasen. In beiden Fällen synchronisiert jede Bus-Zustandsmaschine auf CLK2_IN, das tatsächlich am Sockel anliegende PHI2. Dies unterscheidet sich vom Z80, der ein einzelnes CLK-Signal verwendet — und die Engine selbst ist ein völlig anderes Wesen als die des Z80, wegen dessen, was die Inbetriebnahme des BBC Model B gelehrt hat: überhaupt kein Bus-Timing auf der C-Seite. Acht Zustandsmaschinen werden geladen:
PIO-Block Zustandsmaschinen Zweck
PIO 0 m6502_addr (SM 0), m6502_latch (SM 2) Adress-Engine — Leiter / gated / Burst — besitzt A0–A15, R/W und SYNC; transparentes Lese-Latch, das bei der fallenden PHI2-Flanke abtastet
PIO 1 m6502_cycle (SM 0), m6502_so, m6502_irq, m6502_nmi (SM 1–3) Zyklus-Sequenzer des gated-Pfads (RDY-Wartezustände); SO- / IRQ- / NMI-Monitore
PIO 2 m6502_clock_6502 oder m6502_clock_6512 (SM 0), m6502_data (SM 1) PHI1/PHI2-Erzeugung (oder geparkt für den 6512); Schreibdaten-Treiber, an R/W gekoppelt
Die PIO-IRQ-Flag-Konventionen:
IRQ-Flag Signal Bedeutung
IRQ 0 Gated-Park m6502_addr ist bei addr_gated geparkt, bis Core 1 ein Wort pusht und das Flag löscht (I/O, serialisierte und unter Quarantäne gestellte Blockoperationen)
IRQ 4 (PIO 0) Adresse gültig Eine Leseadresse wurde getrieben — m6502_latch beginnt eine Erfassung
IRQ 4 (PIO 1) NMI Pin für nicht-maskierbaren Interrupt low; Core 1 erkennt die Flanke
IRQ 5 IRQ Pin für maskierbaren Interrupt low; pro Instruktion abgetastet (Pegel)
IRQ 6 SO Set-Overflow-Pin aktiviert

PIO-Programmierung — Wie C-Code Buszyklen steuert

Beim picoZ80 pusht Core 1 für jede Bustransaktion 16-Bit-out exec-Instruktionssequenzen in eine Zyklus-Zustandsmaschine. Der schnelle Pfad des pico6502 macht etwas anderes: Core 1 pusht ein einzelnes 32-Bit-Wort {instr:16 | ~addr:16} an die Adress-Zustandsmaschine und rührt das Timing nie an. Die SM lädt die invertierte Adresse in X, führt das obere Halbwort aus (set y, count-1 für einen Lese-Burst von count Bytes, jmp wdrive für einen Schreibzugriff) und treibt den Bus auf ihrem eigenen Raster aus frischer fallender PHI2-Flanke plus tADS. Die Koordination für einen Burst-Instruktionsabruf:
                  Core 1 (C code)                       PIO State Machines
                  ─────────────                         ──────────────────
    1. Core calls fetchOpcode →             m6502_addr: idling on the ladder — after each
       m6502FetchOpcodeFused(addr)           genuine PHI2 fall + tADS it polls the FIFO at
                  │                          4 rungs (~cycles 17/30/43/68)
    2. Shadow valid for addr?  ──yes──→     (serve: byte returned to the core NOW; a no-capture
                  │ no                        read may still run so the bus stays identical)
    3. Push {set y,5 | ~addr} ─────────→    m6502_addr: out x,16 / out exec,16
       (burst of 6)                          read_next: mov pins,!x  → A0–A15 driven
                  │                                     irq set 4    → "address valid"
                  │                          m6502_latch: wait irq4, wait PHI2 rise, spin while
                  │                                     high, sample AFTER the fall, push
                  │                          m6502_addr: jmp x-- (address ascends), next fall,
                  │                                     drive again … six bytes, one per cycle
    4. Pop RX FIFO ←───────────────────     one capture word per driven address
       verify against shadow (must match)
       extra bytes → harvest buffer
                  │
    5. Core executes; operands come
       from the harvest buffer / shadow
       with zero bus operations

    WRITE: push {data} to m6502_data first, then {jmp wdrive | ~addr} to m6502_addr.
           wdrive: set R/W low, drive address — m6502_data (wait 0 gpio RNW) drives D0–D7,
           start_addr restores R/W high at the next fall (+~45ns hold) — one cycle, one strobe.
    I/O:   push a gated word (irq0 handshake) — the classic serialised cycle via m6502_cycle,
           followed by the I/O chase (a no-capture park read of $FFFC).
Für die PIO-Grundlagen und den out exec-Mechanismus siehe den picoZ80 Technischen Leitfaden — PIO-Architektur im Detail; für die vollständige Beschreibung von Leiter, Burst, Shadow, Serve und I/O-Chase siehe den pico6502 Technischen Leitfaden. Die 6502-spezifischen Punkte, die ein Treiberautor benötigt:
  • Spekulation ist opt-in und wird vom Treiber deklariert. Nichts oberhalb von Schritt 2 geschieht, solange das I/O-Fenster nicht bekannt ist (m6502IoKnown()). Eine Persona deklariert es — zusammen mit dem Shadow-Schreibfenster, dem Paging-Register und der Burst-Tiefe — über ihre Tuning-Funktion (unten), das JSON oder das Shell-Kommando buspipe iorange.
  • Der Shadow ist ein Orakel, kein Cache. Jedes Byte vom Bus wird mit ihm verglichen; eine Abweichung stellt diesen 256-Byte-Block unter Quarantäne auf dem gated-Pfad und erhöht einen Zähler, der nach jedem Dauertest null lesen muss. Alles, was ein Host hinter dem Rücken der CPU ändern kann (gepagte Bereiche, schreibignorierendes ROM, I/O), muss deklariert werden, damit der Shadow dort invalidiert oder gar nicht erst aufgebaut wird.
  • Taktphasen-Synchronisation — alles wartet auf CLK2_IN, niemals auf eine interne Kopie.
  • Kein BUSREQ/BUSACK — der 6502 hat keinen Bus-Request-Mechanismus; die Busfreigabe ist die DBE-Domäne des Hosts.
  • RDY statt WAIT — wird nur auf dem gated-Pfad berücksichtigt; keiner der getesteten Hosts schaltet es um.

t_6502PSRAM-Struktur

Entspricht t_Z80PSRAM beim picoZ80, jedoch ohne das ioPtr[]-Array:
// src/include/M6502CPU.h (simplified)
typedef struct {
    uint8_t      RAM[MAX_MEMORY_BANKS * MEMORY_PAGE_SIZE]; // 64 banks × 64KB = 4MB RAM/ROM area
    MemoryFunc   memPtr[MEMORY_PAGE_SIZE];                  // 64K per-byte read redirect (PTR type)
    MemoryFunc   memioPtr[MEMORY_PAGE_SIZE];                // 64K memory-mapped handler array (FUNC type)
    // NOTE: no ioPtr[] — the 6502 has no separate I/O space
} t_6502PSRAM;

Handler-Funktionssignatur

Die MemoryFunc-Typdefinition ist die gleiche wie beim picoZ80:
// Handler called for MEMBANK_TYPE_FUNC blocks on every access
typedef uint8_t (*MemoryFunc)(M6502CPU *cpu, bool read, uint16_t addr, uint8_t data);

// Parameters:
//   cpu  — pointer to the M6502CPU struct (access PSRAM, state, queues via this)
//   read — true for a 6502 read cycle (RNW=1), false for a write cycle (RNW=0)
//   addr — 16-bit address that triggered this handler
//   data — byte written (only meaningful when read=false)
// Return:
//   byte to place on the data bus (only meaningful when read=true)
Da es keinen I/O-Raum gibt, gibt es auch keine separate IOFunc-Typdefinition — MemoryFunc behandelt alle Peripheriezugriffe.

Debug-Shell — Eine Shell, zwei CPUs

src/dbgsh.c wird in beide Firmware-Familien kompiliert und dispatcht über die t_CpuOps-Vtable in src/include/cpu.h. Die Fähigkeits-Flags der Vtable — hasIoSpace, hasM1Cycle, hasInterruptModes, auf dem 6502 alle false — plus die CPU-spezifische bedingte Kompilierung formen den Befehlssatz: der Z80-Build hat 61 Befehle, der 6502-Build 45. Die sechzehn auf dem 6502 herauskompilierten sind die I/O- und Z80-Peripheriebefehle (iomap, in, out, fwait, iowait, intcount, dev, stall, sig, dma, ctc, pio, ser, rtc, prn); busdiag wird durch eine 6502-spezifische Implementierung ersetzt statt entfernt. Im Gegenzug gewinnt der 6502 die Board-Inbetriebnahme-Diagnosen — busdiag, busdiag clk, busdiag watch [n] und piodbg — die vor der v2.3-Hardware geschrieben wurden, damit ein brandneues Board von der Shell aus diagnostiziert werden kann (siehe den Technischen Leitfaden — Debug-Shell-Architektur).

Treiberframework

Das Treiberframework in M6502CPU.c ist strukturell identisch mit dem picoZ80-Treiberframework. Siehe den Abschnitt picoZ80 Entwicklerhandbuch — Treiberframework für die vollständige Beschreibung von:
  • Der virtualFuncMap[]-Registrierungstabelle und wie das JSON-Feld "name" einer Treiber-Init-Funktion zugeordnet wird.
  • Der VirtualFunc-Typdefinition und dem M6502CPU_getVirtualFunc()-Lookup.
  • Dem zweiphasigen Init-Ablauf (Validierungsdurchlauf, dann Konfigurationsdurchlauf).
  • Den Treiber-Lifecycle-Callbacks: reset_ptr, poll_ptr, task_ptr.
Die einzigen Namensunterschiede sind:
picoZ80 pico6502 Hinweise
Z80CPU-Struktur M6502CPU-Struktur Gleiche Felder, gleiches Layout
Z80CPU_getVirtualFunc() M6502CPU_getVirtualFunc() Gleiche Lookup-Logik
Z80CPU_cpu() M6502CPU_cpu() Einstiegspunkt der Core-1-Hot-Loop
Z80CPU_readMem() M6502CPU_readMem() Speicher-Lese-Dispatch
Z80CPU_writeMem() M6502CPU_writeMem() Speicher-Schreib-Dispatch
Z80CPU_readIO() — Kein Äquivalent — der 6502 hat keinen I/O-Raum
Z80CPU_writeIO() — Kein Äquivalent — der 6502 hat keinen I/O-Raum
cpu->_z80PSRAM cpu->_6502PSRAM PSRAM-Strukturzeiger

Der Open6502-Treiber

Zwei Maschinentreiber sind in der virtualFuncMap[] von M6502CPU.c registriert. Open6502 (src/drivers/Other/Open6502.c, INCLUDE_OPEN6502_DRIVERS) ist dem Open-Experimentiertreiber der Z80-Seite nachempfunden und bewusst minimal und maschinenunabhängig: er lädt ROM-Images auf Treiberebene in die konfigurierten bank:addr-Ausschnitte, markiert Bank-0-ROM-Blöcke als schreibgeschützt und lässt den Adressraum ansonsten genau so, wie das JSON-Array "memory" ihn definiert hat — eine nackte Persona reicht alles an den Host durch. Die Apple I-Replik läuft darauf. BBC-B (src/drivers/Acorn/BBCModelB.c, INCLUDE_ACORN_DRIVERS, nur BBC6502-Modell) ist der erste Maschinentreiber und wird unten in einem eigenen Abschnitt beschrieben; die Acorn-Familie führt zudem drei Interfaces mit, die sich an die Persona binden und nach ihr per #include eingebunden werden: BBCCMT.c (UEF-Bandlaufwerk), BBCFDC.c (virtueller Floppy-Controller — ein Intel 8271 oder ein WD1770 hinter einem Board-Latch, über die hostunabhängigen Chipmodelle in src/drivers/I8271.c und src/drivers/WD177x.c), BBCMMC.c (virtuelle MMFS-Karte) und BBCNet.c (virtuelles Econet-Modul über das MC6854-Modell in src/drivers/MC6854.c).
Was er bewusst nicht bietet, sind Interface-Karten. Die Open-Persona des Z80 bietet neun (MZ-1R12/18/23/37, PIO-3034, MZ-8BIO3, MZ-1E24, MZ-1E05, Celestite), aber jede davon ist gegen t_Z80CPU geschrieben und hängt sich über die Z80-I/O-Port-Tabelle ein — die der 6502 nicht hat. interfaceFuncMap ist im Quellcode als Erweiterungspunkt dokumentiert, wenn 6502-native Karten hinzukommen.

Core-1-Dispatch-Schleife

Die Core-1-Schleife in M6502CPU_cpu() ist die Welt des emulierten Prozessors: anders als auf der Z80-Seite, wo der echte Z80 der Busmaster ist und Core 1 seine Zyklen beantwortet, ist Core 1 hier die CPU, und die Buszyklen sind ihre eigenen Speicherzugriffe. Die Schleife:
  1. Power-on-Supervisor: den Host im Reset halten, bis die Dienste von Core 0 bereit sind (Obergrenze 15 s); R/W auf high parken.
  2. Hold-/Park-Anforderungen von Core 0 mit einer RAM-residenten Warteschleife bedienen (sicher während des Flash-Löschens), ebenso die entprellte /RES-Leitung (die Reset-Sequenz läuft nach dem Loslassen), den DBE-Busfreigabe-Hold des 6512 und den /NMI-Flankendetektor.
  3. g_fastTickHook aufrufen, falls ein Treiber einen installiert hat (die Baud-Taktung des Bandlaufwerks), und den poll_ptr der Treiber etwa alle 250 ms.
  4. m6502_run(1024) ausführen — oder jeweils eine Instruktion im Einzelschritt-, Breakpoint- oder Trace-Modus, mit optionaler Opcode-Wiederlese-Verifikation. Darin lösen die Callbacks fetchOpcode/readMem/writeMem des Kerns jede Adresse gegen _membankPtr[addr >> 9] auf und dispatchen nach Blocktyp: PHYSICAL / PHYSICAL_HW (eine Busoperation über die obige Engine), RAM / ROM (PSRAM), FUNC (memioPtr[addr]), PTR (memPtr[]-Umleitung).
  5. Interrupts einmal pro Instruktion abtasten — der physische /IRQ-Schnappschuss ODER-verknüpft mit g_m6502VirtIrq — und IRQ/NMI über eine Sieben-Zyklen-Sequenz ausliefern, die jeden laufenden Prefetch abräumt.
// Simplified M6502CPU_readMem / M6502CPU_writeMem dispatch
uint8_t M6502CPU_readMem(M6502CPU *cpu, uint16_t addr)
{
    uint32_t entry = cpu->_membankPtr[addr >> 9];
    uint8_t  type  = (entry >> 24) & 0xFF;
    uint8_t  bank  = (entry >> 16) & 0xFF;
    uint32_t base  = (entry & 0xFFFF) << 9;

    switch(type)
    {
        case MEMBANK_TYPE_RAM:
        case MEMBANK_TYPE_ROM:
            return cpu->_6502PSRAM->RAM[(bank * MEMORY_PAGE_SIZE) + addr];

        case MEMBANK_TYPE_FUNC:
            if(cpu->_6502PSRAM->memioPtr[addr] != NULL)
                return cpu->_6502PSRAM->memioPtr[addr](cpu, true, addr, 0);
            return 0x00;

        case MEMBANK_TYPE_PTR:
            if(cpu->_6502PSRAM->memPtr[addr] != NULL)
                return cpu->_6502PSRAM->memPtr[addr](cpu, true, addr, 0);
            return 0x00;

        case MEMBANK_TYPE_PHYSICAL:
        default:
            return 0xFF;  // Pass-through to real host hardware
    }
}

Einen neuen Treiber schreiben

Der Prozess zum Schreiben eines pico6502-Treibers ist identisch mit dem picoZ80-Prozess, der im picoZ80 Entwicklerhandbuch — Einen neuen Treiber schreiben beschrieben ist. Die sechs Schritte sind dieselben:
  1. Eine Header-Datei in src/include/drivers/<Family>/MyDriver.h anlegen
  2. Eine Implementierungsdatei in src/drivers/<Family>/MyDriver.c anlegen
  3. Die Compile-Definition INCLUDE_<FAMILY>_DRIVERS der Familie für den Modell-Build aktivieren (Treiberquellen werden in-situ per #include eingebunden, nicht als separate Übersetzungseinheiten aufgeführt)
  4. Den Treiber in M6502CPU.c unter dem #ifdef INCLUDE_<FAMILY>_DRIVERS-Guard einbinden
  5. Den Treiber in virtualFuncMap[] in M6502CPU.c registrieren
  6. Einen JSON-Treibereintrag in config.json hinzufügen
Der wesentliche Unterschied ist, dass alle Funktionen statt eines Z80CPU *cpu-Parameters einen M6502CPU *cpu-Parameter erhalten und Sie auf das PSRAM über cpu->_6502PSRAM statt über cpu->_z80PSRAM zugreifen. Es gibt keine ioPtr[]-Slots zu installieren — alle virtuellen Peripheriegeräte werden in memioPtr[] registriert.

Minimale Treibervorlage

Das Folgende zeigt einen minimalen 6502-Peripherietreiber — einen virtualisierten 6522 VIA (Versatile Interface Adapter) an Adresse 0xC000–0xC00F:
// src/include/drivers/Acorn/VIA6522.h
#ifndef VIA6522_H
#define VIA6522_H

#include "M6502CPU.h"

int VIA6522_Init(M6502CPU *cpu, t_drvConfig *drvConfig, int pass);

#endif
// src/drivers/BBC/VIA6522.c
#include "../../include/M6502CPU.h"
#include "../../include/drivers/Acorn/VIA6522.h"

// --- Internal state ---
typedef struct {
    uint8_t  regs[16];      // VIA internal registers (ORB, ORA, DDRB, DDRA, T1CL, T1CH, ...)
    bool     irqPending;
} t_VIA6522State;

static t_VIA6522State ViaState = { .irqPending = false };

// --- Memory handler (called for every access to 0xC000–0xC00F) ---
static uint8_t VIA6522_Handler(M6502CPU *cpu, bool read, uint16_t addr, uint8_t data)
{
    uint8_t reg = addr & 0x0F;   // VIA has 16 internal registers

    if(read)
    {
        return ViaState.regs[reg];
    }
    else
    {
        ViaState.regs[reg] = data;
        // TODO: implement timer, shift register, port logic as needed
        return 0;
    }
}

// --- Reset handler (called when host system resets) ---
static uint8_t VIA6522_Reset(M6502CPU *cpu)
{
    memset(&ViaState.regs, 0, sizeof(ViaState.regs));
    ViaState.irqPending = false;
    return 0;
}

// --- Init function (called during JSON config parse) ---
int VIA6522_Init(M6502CPU *cpu, t_drvConfig *drvConfig, int pass)
{
    if(pass == 0)
    {
        // Validation pass — just check the name matches
        return (strcasecmp(drvConfig->name, "via6522") == 0) ? 1 : 0;
    }

    // Configuration pass — install handlers
    // Install the handler for all 16 VIA registers at 0xC000–0xC00F
    for(uint16_t addr = 0xC000; addr <= 0xC00F; addr++)
    {
        cpu->_6502PSRAM->memioPtr[addr] = VIA6522_Handler;
    }

    // Register reset callback
    cpu->reset_ptr = VIA6522_Reset;

    debugf("VIA6522: Initialised at 0xC000–0xC00F\n");
    return 1;
}

JSON-Konfiguration

Um den VIA6522-Treiber zu aktivieren, fügen Sie der Speicherbelegung einen FUNC-Block und einen Treibereintrag hinzu:
{
  "rp2350": {
    "6502": {
      "memory": [
        { "enable":1, "addr":"0x0000", "size":"0xC000",
          "type":"RAM",  "bank":0, "task":"", "file":"" },
        { "enable":1, "addr":"0xC000", "size":"0x0010",
          "type":"FUNC", "bank":0, "task":"via6522", "file":"" },
        { "enable":1, "addr":"0xE000", "size":"0x2000",
          "type":"ROM",  "bank":0, "task":"", "file":"/ROM/bios.rom" }
      ],
      "drivers": [
        { "enable":1, "name":"via6522" }
      ]
    }
  }
}

In virtualFuncMap registrieren

Fügen Sie der virtualFuncMap[]-Tabelle in M6502CPU.c einen Eintrag hinzu. Der name-String wird groß-/kleinschreibungsunabhängig gegen das JSON-Treiberfeld "name" abgeglichen:
// In M6502CPU.c — add to virtualFuncMap[]
static const t_VirtualFuncMap virtualFuncMap[] = {
    { "Open6502",  Open6502_Init },    // existing entries
    { "BBC-B",     BBCModelB_Init },
    { "via6522",   VIA6522_Init  },    // your new driver
    { NULL, NULL }                     // terminator
};

CMakeLists.txt-Änderung

Treiberquellen folgen dem Z80-Muster: sie werden von M6502CPU.c in-situ unter dem INCLUDE_<FAMILY>_DRIVERS-Guard der Familie per #include eingebunden (so wird Open6502.c unter INCLUDE_OPEN6502_DRIVERS hereingezogen), sodass die Compile-Definition entscheidend ist und nicht ein Quelldatei-Eintrag. Aktivieren Sie das Define Ihrer Familie für das Modell in den Build-Dateien:
# The Acorn family already exists — it is what the BBC6502 model enables:
#   src/model/CMakeLists.txt
#   add_m6502_model_targets(BBC6502 "" "INCLUDE_OPEN6502_DRIVERS;INCLUDE_ACORN_DRIVERS")
# A new family gets its own INCLUDE_<FAMILY>_DRIVERS define in the model's list.

# And in M6502CPU.c, alongside the existing includes:
#ifdef INCLUDE_ACORN_DRIVERS
#include "../drivers/Acorn/BBCModelB.c"
#include "../drivers/Acorn/BBCCMT.c"      // binds to the persona, hence after it
#include "../drivers/Acorn/VIA6522.c"     // your new driver
#endif

Erstellen und Testen

# From the project root — Base6502 only, or the full build for BBC6502
./build_tzpuPico.sh 6502
./build_tzpuPico.sh

# Flash via USB mass-storage
cp build/bin/model/BBC6502/BBC6502_0x10020000.uf2 /media/$USER/RPI-RP2/

# Or OTA: upload via http://<device-ip>/ota-rp2350.htm

# Or prove the change first with no hardware at all:
cd sim/6502 && mkdir -p build && cd build && cmake .. && make -j$(nproc)
cd ../.. && ./run.sh apple1

Speicher-Hook-Muster

Da der 6502 keinen I/O-Raum hat, verwenden alle Hook-Muster memioPtr[]. Die Muster sind dieselben wie die im picoZ80 Entwicklerhandbuch — Speicher-Hook-Muster beschriebenen, wobei das I/O-Port-Handler-Muster durch einen speicherabgebildeten Handler an einem beliebigen Adressbereich ersetzt ist. Die wichtigsten Muster für 6502-Treiber sind:

FUNC-Block — Virtuelles Peripheriegerät

Installieren Sie einen Handler für einen zusammenhängenden Adressbereich. Dies ist das Standardmuster für jede 6502-Peripherie-Emulation:
// Install handler for address range 0xD000–0xD0FF (256-byte peripheral block)
for(uint16_t addr = 0xD000; addr <= 0xD0FF; addr++)
{
    cpu->_6502PSRAM->memioPtr[addr] = MyPeripheral_Handler;
}

RAM-Schreib-Abfangen

Schreibzugriffe auf einen RAM-Bereich abfangen und Lesezugriffe weiterhin aus dem PSRAM zulassen. Nützlich zum Spiegeln von Zero-Page-Zugriffen oder zum Erkennen von Schreibzugriffen auf bestimmte Adressen:
// Handler intercepts writes; passes reads to PSRAM
static uint8_t ZeroPage_Intercept(M6502CPU *cpu, bool read, uint16_t addr, uint8_t data)
{
    if(read)
    {
        // Read from PSRAM normally
        return cpu->_6502PSRAM->RAM[addr];
    }
    else
    {
        // Write to PSRAM and update shadow state
        cpu->_6502PSRAM->RAM[addr] = data;
        MyDriver_OnZeroPageWrite(addr, data);
        return 0;
    }
}

// In init: set the block type to FUNC and install the handler
cpu->_membankPtr[0x0000 >> 9] = MEMBANK_ENCODE(MEMBANK_TYPE_FUNC, 0, 0x0000);
cpu->_6502PSRAM->memioPtr[0x0000] = ZeroPage_Intercept;
// ... repeat for each 512-byte block in the range

Verteilte / einzelne Adressen

Für Hardware, bei der innerhalb eines größeren Bereichs nur wenige Adressen von Bedeutung sind (z.B. ein 4-Bit-Adressdecoder), installieren Sie den Handler nur an diesen speziellen Adressen und lassen den Rest 0xFF oder PSRAM-Daten zurückgeben:
// Only addresses 0xFFFE and 0xFFFF (6502 reset vector) are intercepted
cpu->_6502PSRAM->memioPtr[0xFFFE] = ResetVector_Lo_Handler;
cpu->_6502PSRAM->memioPtr[0xFFFF] = ResetVector_Hi_Handler;

Maschinen-Personas — Apple I und der BBC Model B

Drei Maschinen werden als Personas ausgeliefert, paarweise je eine pro Flash-Partition in esp32/personas/config_<Partition1>_<Partition2>.json: Basic6502 („Vanilla 6502" — ein schlichter 6502 im Sockel, ohne jede Virtualisierung; dies ist die Persona, mit der ein neues Board in Betrieb genommen wird), Apple-I (der Apple-I-/RC6502-Replik-Host auf Open6502; sein 6821-PIA, RAM und Monitor-ROM liegen alle auf dem Host, und der Woz-Monitor und BASIC laufen auf dem v2.3a-Board) und BBC-B — der Model B auf seinem eigenen Treiber.

Der BBC-B-Treiber — Maschinenwissen als Treiber

BBCModelB.c ist eine Physical-Passthrough-Persona: der Model B behält sein RAM, ROM und seine Peripherie, und der Treiber steuert die Speicherbelegungs-Fakten bei, die die Bus-Engine benötigt, bevor sie spekulieren darf. Diese Deklaration allein bringt die CPU von Vanilla-konservativ zur in der Kampagne bewährten Vollgeschwindigkeits-Engine, mit durchgehend byte-identischem Bus. Das Muster ist die Tuning-Funktion, die vom Konfigurations-Parser aufgerufen wird, wenn der Treiber ausgewählt wird — bevor die eigenen JSON-Schlüssel des Treiberobjekts angewendet werden, sodass jeder Wert ein Standardwert ist, den die Konfiguration überschreiben kann:
void BBCModelB_Tuning(t_M6502CPU *cpu)
{
    g_ioRegionLo   = 0xFC00;   // FRED/JIM/SHEILA: the engine never speculates into or across it.
    g_ioRegionHi   = 0xFF00;
    g_shadowWrLo   = 0x0000;   // main RAM: this CPU is the sole writer -> write-through shadow.
    g_shadowWrHi   = 0x8000;
    g_pageRegAddr  = 0xFE30;   // ROMSEL, partially decoded (hence the mask)...
    g_pageRegMask  = 0xFFF0;
    g_pageRegionLo = 0x8000;   // ...banks the sideways ROM window.
    g_pageRegionHi = 0xC000;
    g_burstSpec    = 6;        // MMFS-safe at 6 with ROMSEL declared (the map barrier).
}
Ein Treiber für eine andere Maschine liefert dieselben fünf Fakten für ihre eigene Speicherbelegung. Geschwistermaschinen mit anderem Silizium oder anderer Belegung gehören in eigene Treiber — der 65C12 des Master, sein Shadow-RAM und sein Paging werden heute als mastermode dieses Treibers behandelt und würden als erstes solches Geschwister herausgelöst. Über das Passthrough hinaus virtualisiert der Treiber das, womit die echte Maschine aufgerüstet werden kann:
  • Sideways-ROM/RAM-Sockel — sechzehn ROMSEL-Bänke können jeweils virtuell aus dem PSRAM bestückt werden (Pool-Bänke 1–4, 16K pro Sockel): ein ROM-Image von SD (virtuelles ROM) oder beschreibbares Sideways-RAM. Ein aktivierter Sockel überlagert die physische Bank; der ROMSEL-Schreibzugriff erreicht weiterhin das Hardware-Latch, damit die eigene Dekodierung des Hosts konsistent bleibt, aber solange eine überlagerte Bank ausgewählt ist, wird jeder Lese- und Schreibzugriff auf $8000–$BFFF aus dem PSRAM bedient. So wird MMFS oder ein Sprach-ROM hinzugefügt, ohne die Maschine zu öffnen.
  • OS-Sockel — ein 16K-OS-Image, über $C000–$FFFF virtualisiert, wobei das I/O-Fenster durchgereicht und die $FF00–$FFFF-Vektoren byteweise behandelt werden; fileoffset wählt das OS aus einem größeren (MegaROM-)Image aus.
  • Model-A-RAM — 16K PSRAM-gestütztes RAM, über $4000–$7FFF gelegt, sodass das OS einen 32K-Model-B sieht. Null Buszyklen — was zugleich der Vorbehalt ist: die Video-ULA holt den Bildschirmspeicher aus dem Host-DRAM und kann virtuelles RAM nicht sehen, sodass das Bildschirmverhalten auf einem echten Model A noch zu verifizieren ist (eine Write-Through-Option, wie sie der PCW-Videopfad des picoZ80 verwendet, ist der wahrscheinliche nächste Schritt).
  • Master-Modus — LYNNE 20K Shadow, HAZEL 8K und ANDY 4K mit virtualisiertem ACCCON ($FE34), ein virtuelles CMOS/RTC auf dem System-VIA, und der CMOS-Kern erzwungen, weil MOS 3.x 65C12-Code ist. Erwartet ein MOS-3.x-Image im OS-Sockel. Das DFS (Bank 9) und ADFS (Bank 13) des MegaROM werden nur voreingestellt, wenn BBCFDC_MasterFdc() einen virtuellen 1770 auf dem Master-Board meldet (oder presetdiscroms es erzwingt); andernfalls bleibt Bank 13 außerhalb der Alias-Maske, damit das eigene Filing-ROM des Hosts durchscheint, passend zum FILE-13-CMOS-Standard.
  • Persistenz — virtuelles CMOS und Sideways-RAM werden auf SD gespeichert (Standard), in den nichtflüchtigen Flash-Bereich (flash_ram.h, geschrieben über eine MSG_WRITE_NVRAM-Inter-Core-Nachricht, während Core 1 im RAM geparkt ist) oder gar nicht.
  • Interfaces — das if[]-Array bindet CMT-UEF (Bandlaufwerk), BBC-FDC (virtueller 8271 oder WD1770 mit zwei Laufwerken), BBC-MMC (virtuelle MMFS-Karte) und BBC-NET (virtuelles Econet-Modul, AUN über IP). Jedes wird von M6502CPU_configDriversFromJSON in den SetConfig-Aufruf des Interface übernommen, bevor das Init der Persona läuft, und jedes stellt die Einstiegspunkte Init / Remap / FastTick / Poll / HostReset bereit, die die Persona aufruft.

BBCCMT — das erste virtuelle Gerät und die virtuelle IRQ-Leitung

BBCCMT.c ist das Vorbild für jedes virtuelle 6502-Peripheriegerät, das Interrupts auslöst. Der BBC lädt Band über einen 6850-ACIA ($FE08/09), der von der Serial ULA ($FE10) gespeist wird, und das Laden ist interruptgesteuert: das MOS aktiviert den ACIA-Empfangsinterrupt und verarbeitet Bytes und DCD-Träger-Ereignisse in seinem IRQ-Handler. Der Treiber virtualisiert beide Chips mit byteweisen FUNC-Einträgen, spielt ein UEF-Image (optional gzip-komprimiert — in-place entpackt mit zlibs malloc-freiem puff) aus einem PSRAM-Staging-Puffer ab, der asynchron über die ESP-Verbindung geladen wird, und taktet Bytes mit echten 1200/300 Baud aus einem g_fastTickHook, der in der Core-1-Schleife läuft, selbst wenn kein Band im Laufwerk liegt. Jedes Ereignis setzt g_m6502VirtIrq, das der Kern bei seiner Abtastung pro Instruktion mit dem physischen /IRQ ODER-verknüpft — ein virtuelles Gerät unterbricht also genau wie ein echtes, ohne jede PIO-Beteiligung. Schreibzugriffe gehen weiterhin an die physischen Chips (das echte Motorrelais klickt), wobei die drei Interrupt-Enable-Bits des physischen ACIA maskiert werden, damit er keinen /IRQ auslösen kann, den das MOS im virtuellen Statusregister nicht sehen kann. SAVE ist dem MOS-1.20-ROM selbst nachmodelliert (B-em hat kein Sendemodell): das CFS-SAVE ist sendeinterruptgesteuert, daher nimmt der virtuelle ACIA bei einem Datenschreibzugriff TDRE zurück, setzt es eine Byte-Zeit später wieder und löst den IRQ aus, wann immer TDRE gesetzt ist und der Sendeinterrupt aktiviert ist; der erfasste Datenstrom wird in ein neues UEF kodiert, das nach dem CFS-Block-Header benannt wird. Die Registersemantik folgt B-em 0.6, gegen das das MOS-CFS-LOAD bewährt ist; das Laufwerk wurde im synthetischen BBC des Simulators entwickelt und erprobt, bevor es auf Hardware lief.

BBCFDC und BBCMMC — Chipmodelle, Block-Caches und NMI-Besitz

Die Floppy- und MMFS-Interfaces fügen Muster hinzu, die ein Autor von Speichergeräten wiederverwenden wird. Erstens: den Chip vom Host trennen. src/drivers/I8271.c modelliert den Intel 8271 rein nach seinem Datenblatt — Registernummern, wie der BBC sie dekodiert (+0 Befehl/Status, +1 Parameter/Ergebnis, +2 Reset, +4 Daten), jeden Befehl von Scan und Lesen/Schreiben über Read ID, Verify, Format, Seek, Specify bis zu den Spezialregistern, die Status- und Ergebnis-Bit-Semantik und das Nicht-DMA-Datenanforderungs-Übertragungsprotokoll, für das die Acorn-DFS-Familie geschrieben wurde ($8C pro Byte, $18 bei Abschluss). src/drivers/WD177x.c tut dasselbe für die WD1770/1772/1773-Familie: Typ-I-Befehle mit den Datenblatt-Schrittraten, Spin-up und Verify; Typ-II-Sektor-Lesen/Schreiben mit Multi-Sektor- und Settling-Optionen; Typ III Read Address, Read Track (die Roh-Spur mit Lücken, Adressmarken und CRCs, wie das Datenblatt sie anlegt) und Write Track (der Host streamt eine Roh-Spur, der Chip dekodiert die Marken und das Medium wird mit den gesehenen IDs formatiert); Typ IV Force Interrupt; Motor-Ein/Aus-Timing. Keines der Modelle weiß etwas von Disketten: eine Medien-Callback-Tabelle (t_I8271Media, t_WD177xMedia) liefert Bereitschaft, Schreibschutz, Geometrie, ein Sektorzeiger-Lookup, das mit PENDING antworten darf, während der Host den Block asynchron holt, einen Takt und was mit dem Interrupt-Ausgang zu tun ist. BBCFDC.c ist die Model-B-Anbindung um eines von beiden, ausgewählt über den Schlüssel chip, und fügt für den 1770 das Laufwerkssteuer-Latch des Boards hinzu — das Acorn-Upgrade ($FE80: Laufwerkswahl, Seite, Dichte, Reset), das Watford-Board (gleiche Adresse, eigenes Bit-Layout) oder der Master 128 ($FE24) — jedes Layout aus dem für dieses Board geschriebenen DFS-ROM herausgelesen. Jeder 6502-Host mit einem der beiden Chips kann das Modell mit seiner eigenen Anbindung wiederverwenden.
Zweitens: Medien als PSRAM-Block-Cache. Weder ein MMB (bis zu 100 MB) noch ein Satz Disketten-Images muss in den Speicher passen: beide Treiber halten einen direkt abgebildeten Cache aus 512-Byte-Slots (die 1024 Slots des FDC bei 0x070000, die 768 des MMC am oberen Ende des Pools, beide über den ungecachten PSRAM-Alias, sodass Core 0 füllt und Core 1 ein einziges kohärentes Fenster bedient), holen fehlende Blöcke mit MSG_READ_SECTOR — einer in Bearbeitung, das Chipmodell erhält PENDING und versucht es aus seinem Tick erneut — und schreiben schmutzige Blöcke mit MSG_WRITE_SECTOR 300 ms nach ihrer letzten Aktualisierung zurück, die Deferred-Schedule-Strategie der Z80-Floppy-Treiber auf Sektor-Granularität. Antworten werden vorab eingesehen und nur dann aus der Inter-Core-Queue entfernt, wenn sie zum Treiber gehören, sodass die Abschlussmeldungen anderer Treiber nie gestört werden. Der FDC bildet die beiden Seiten jedes Laufwerks auf bis zu zwei Dateien ab (SSD, DSD mit pro Spur verschränkten Seiten, oder eine Diskette bei 8192 + n × 204800 innerhalb eines MMB), mit einem Cache-Schlüssel aus Laufwerk × 2 + Dateiindex, sodass gepaarte Seitendateien nie einen Schlüsselraum teilen; der MMC hüllt das MMB in einen synthetischen FAT16-Bootsektor, FAT und Stammverzeichnis, sodass die MMFS-Annahme „erste 16 Stammeinträge, zusammenhängender erster Cluster" gilt und Datenbereichs-Blöcke direkt auf MMB-Byte-Offsets abgebildet werden.
Drittens: die Interrupt-Leitung besitzen, die Sie virtualisieren, und sie zeitlich steuern. Der Floppy-Controller des Model B treibt /NMI, und ein echter ist noch in der Maschine; der virtuelle Controller reicht seine Registerschreibzugriffe nie an den Bus durch (auf einem Model-B-Host würden die $FE24–$FE2F des Master sogar auf die Video-ULA dekodieren) und setzt, solange er aktiviert ist, g_m6502NmiPhysMask, sodass physische /NMI-Flanken gezählt und ignoriert werden — das INT (8271) bzw. INTRQ|DRQ (1770) des Chips wird zur einzigen /NMI-Flanke, die der Kern sieht. Interrupt-auslösende Statusänderungen werden im Chipmodell scharfgeschaltet und — Status, DRQ und INTRQ zusammen — angewendet, wenn der Geräte-Timer des Kerns eine Byte-Zeit später feuert (64 µs FM, 32 µs MFM), weil ein Dateisystem-NMI-Handler, der selbst den Chip liest, das nächste Ereignis niemals bereits innerhalb des Handlers verschachtelt vorfinden darf. Im ungedrosselten Modus fällt jede mechanische Verzögerung auf null zusammen, und der Indeximpuls wechselt bei jedem Statuslesen, was die Spin-Wait-Schleifen der ROMs brauchen. Der MMC tut das Entsprechende für den User-VIA: Port-B-Schreibzugriffe werden nie durchgereicht (eine physische MMFS-Karte würde sie ausführen), die SR-Modus-Bits werden aus dem ACR, das der echte VIA sieht, herausmaskiert, und IER-Bit 2 wird maskiert, damit der echte Chip nie auf Rechnung des Treibers schiebt oder unterbricht. Der Host-Reset wird weitergeleitet (HostReset), weil der RESET-Pin des echten 8271 an /RES hängt und ein über ein BREAK hinweg in Bearbeitung befindliches Byte sonst dem frisch gebooteten DFS geliefert würde. Live-Image-Wechsel kommen von Core 0 als CHGD,<drive>,<file>[,<side>,<kind>] bei angehaltenem 6502; schmutzige Blöcke des ausgehenden Images werden vor dem Neueinhängen synchron geschrieben.

BBCNet — Econet als Protokollbrücke

Das Econet-Modul ist das erste Interface, dessen Gegenstelle eine andere Maschine ist, und es zeigt, wie ein virtuelles 6502-Gerät das Netzwerk des ESP32 erreicht. Das Chipmodell, src/drivers/MC6854.c, arbeitet auf Frame-Ebene statt auf Bit-Ebene: der Host übergibt ihm einen ganzen empfangenen Frame, der mit einem Byte pro Leitungs-Byte-Zeit (40 µs bei 200 kbit/s) in das 3-Byte-Empfangs-FIFO tropft, mit ADDRESS PRESENT beim ersten Byte und FRAME VALID beim letzten; CPU-Schreibzugriffe füllen das Sende-FIFO, das mit der Byte-Rate in einen Frame-Puffer abfließt, wobei TDRA die Zwei-Bytes-pro-NMI-Schleife des ROM taktet, und ein Schreibzugriff auf das Terminate-Register (oder CR2 TX LAST DATA) schließt den Frame und gibt ihn als Ganzes zurück. Jede Empfänger- oder Senderbedingung, die bei gesetztem Interrupt-Enable eintritt, aktiviert IRQ, was die Anbindung in eine /NMI-Flanke umsetzt.
Oberhalb des Chips betreibt BBCNet.c auf dem Core-1-Fast-Tick einen Zustandsautomaten, der der Econet-Vier-Wege-Handshake aus Sicht der Gegenstelle ist: wenn das ROM einen Scout sendet, bestätigt die Anbindung ihn nach einer realistischen Frame-Zeit lokal, nimmt den folgenden Datenframe und sendet ihn als ein AUN-Unicast-Datagramm, und wenn das AUN-Ack der Gegenstelle eintrifft, präsentiert sie es dem ROM als Abschlussframe; in der Gegenrichtung wird ein eintreffender Unicast dem ROM als Scout angeboten, das Ack des ROM gibt den Datenframe frei, und das abschließende Ack des ROM geht als AUN-Ack zurück. Immediates und Broadcasts werden auf ihre eigenen AUN-Typen abgebildet, ein Scout, den das ROM nicht innerhalb von 50 ms beantwortet, wird als „auf diesem Port nicht lauschend" behandelt, und eine Gegenstelle, die nicht innerhalb des Stufen-Timeouts antwortet (5 s — WLAN-Umlaufzeiten von 1–2 s wurden beobachtet), wird aufgegeben, wie es der eigene Timeout des ROM auch täte. Core 1 rührt das Netzwerk nie an: Frames gehen in einen PSRAM-Ring (8 ausgehende Slots) und kommen aus einem anderen (16 eingehende), beide mit 1600-Byte-Slots bei 0x0F0000 über den ungecachten Alias, und BBCNet_Core0Service() — in jeder Iteration aus der Hauptschleife von Core 0 aufgerufen — öffnet über das Netzwerk-IPC einen UDP-Socket auf dem ESP32 (NET_SOCK mit Datagramm-Typ und aktiviertem Broadcast, adressiertes NET_SEND mit dem Ziel im Frame-Header, NET_RECV), leitet unsere Station aus der vom ESP32 gemeldeten IP-Adresse ab, wenn station 0 ist, und ordnet jedes eingehende Datagramm anhand seiner Quelladresse einer Station zu — zuerst der explizite Eintrag des Dateiservers, dann die hosts[]-Tabelle, dann die AUN-Konvention. Ein Treiber, der das Netzwerk benötigt, braucht daher überhaupt keine ESP32-Änderungen; er braucht einen Core-0-Service-Hook und zwei Ringe.

Host-Simulator (pico6502sim)

sim/6502/ baut pico6502sim: die echten pico6502-Firmware-Quellen — M6502.c, M6502CPU.c, M6502dis.c, dbgsh.c, model/Base6502/main.c, plus die gesamte ESP32-Hälfte mit ihrer Web-GUI und einer virtuellen SD-Karte — nativ kompiliert und headless auf dem Entwicklungs-Host ausgeführt. So wurde die v2.3a-Firmware Ende-zu-Ende bewiesen, bevor irgendeine Hardware existierte — Debug-Shell, ESP32-Verbindung, Persona-Konfiguration und Web-GUI wurden alle erprobt, während das Board noch geroutet wurde — und er bleibt der Ort, an dem neue Treiber geschrieben werden: der synthetische BBC-Host und die Acorn-Treiber (INCLUDE_ACORN_DRIVERS in sim/6502/CMakeLists.txt) erlauben es, LOAD und SAVE des UEF-Laufwerks vor der Hardware gegen das echte MOS zu erproben.
Anders als pZ80sim der Z80-Seite, das seine Host-Maschinen vom Common Source Code Project entlehnt, wird keines der 6502-Ziele von CSP modelliert — pico6502sim ist daher eigenständig: kein CSP, kein Qt, kein SDL2, kein FFmpeg. Ein C/C++-Compiler, pthreads und zlib genügen (plus pioasm aus dem pico-sdk-Baum, um M6502.pio.h zu erzeugen). Und wo pZ80sim Core 1 inline aus dem CSP-Scheduler heraus schrittweise ausführt, gibt pico6502sim Core 1 einen echten Thread, der die eigene Schleife von M6502CPU_cpu() ausführt — derselbe Code, den die Hardware ausführt.

Erstellen und Ausführen

cd sim/6502
mkdir -p build && cd build
cmake .. && make -j$(nproc)

# Launch a machine (from sim/)
../run.sh apple1        # Apple I, synthetic host, 1.023 MHz
../run.sh bbcmodelb     # BBC Model B, synthetic host, 2 MHz — MODE 7 on the terminal
../run.sh bbcmaster     # BBC Master 128, headless, 2 MHz (CMOS core)
CMake-Optionen: -DPICO6502_WITH_ESP32=OFF lässt den virtuellen ESP32/die Web-GUI weg; -DPICO6502_WITH_DBGSH=OFF lässt die Debug-Shell weg. Kommandozeile: --host=apple1\|bbc\|none, --rom=<file> (oder die Umgebungsvariable PICO6502_ROM), --verbose, --trace. Die Umgebungsvariablen teilen das Präfix PZ80SIM_ mit pZ80sim (PZ80SIM_SD, PZ80SIM_RUN, PZ80SIM_HTTP_PORT, PZ80SIM_TCP_BASE, PZ80SIM_VERBOSE, PZ80SIM_TRACE) — der Launcher ist gemeinsam. Alle drei Maschinen können gleichzeitig laufen:
Maschine cputype Host-Takt Web-GUI Serieller TCP-Block dbgsh Host-Modell
Apple I 6502 1,023 MHz 8110 5400–5405 5401 apple1 — synthetischer Host
BBC Model B 6502 2 MHz 8111 5410–5415 5411 bbc — synthetischer Host
BBC Master 128 65C12 2 MHz 8112 5420–5425 5421 keines (headless), CMOS-Kern
Jeder serielle Kanal wird sowohl als Host-Pty als auch als TCP-Port bereitgestellt; die Debug-Shell liegt auf CDC 1 (tcp base+1), und beim Apple I ist das eigene Terminal der Maschine der virtuelle serielle Port A (tcp base+2). Der Launcher schreibt beim Start die Persona jeder Maschine auf ihre eigene virtuelle SD-Karte (sim/sdcard-<machine>; sdcard-bbccmt ist eine fertige Karte mit dem BBC-B-Treiber, dem CMT-UEF-Interface und zwei Testbändern). Wie auf der Hardware übernimmt die 6502-Firmware config.json bei einem CFG-Befehl in ihren Flash-Speicher — das Anwenden in der Web-GUI oder GET /tasks/reloadcfg — und bootet damit neu; das simulierte Flash-Image wird bei jedem Start aus der Karte neu aufgebaut, sofern nicht PZ80SIM_KEEP_FLASH=1 gesetzt ist.

Der synthetische Apple-I-Host

sim/src/host_apple1.c modelliert die Maschine, gegen die das Board in Betrieb genommen wird:
Bereich Inhalt
$0000–$7FFF RAM, 32K
$D010–$D013 6821-PIA — gespiegelt über ganz $D000–$DFFF (partielle Dekodierung, wie auf der echten Maschine)
$E000–$FFFF 8K-ROM-Fenster — ein kleineres Image wird am oberen Ende geladen
Tastatur und Anzeige liegen auf dem virtuellen seriellen Port A, Eingaben werden in Großbuchstaben umgewandelt, Return→CR, Backspace→_. Es wird kein ROM mitgeliefert (der Woz-Monitor gehört Apple): der Launcher durchsucht die ROM-Bibliothek und das Verzeichnis roms/APPLE1/ der Karte nach den üblichen Namen (apple1.rom, wozmon.rom, …), und --rom= überschreibt dies. Ohne ROM läuft ein eingebautes, ~35 Byte großes Banner-/Echo-Programm — das Fetch, Execute, Branch, Call und den PIA in beiden Richtungen beweist: auf einem Board, das noch nie funktioniert hat, das meiste von dem, was Sie zuerst sichergestellt haben wollen.

Der synthetische BBC-Model-B-Host

sim/src/host_bbc.c (September 2026) modelliert genug von einem Model B, damit das MOS booten kann und damit Persona, Sideways-Sockel und Bandlaufwerk dagegen entwickelt werden können. Sein System-6522-VIA — T1/T2-Timer, IFR/IER, das adressierbare IC32-Latch, der langsame Datenbus und die Tastaturmatrix — ist von B-em 0.6 (Tom Walker) portiert, mit einem 50 Hz-Vsync auf CA1; der 100 Hz-Timer und der Vsync sind das, was der MOS-Boot braucht. bbc_init() lädt das 16K-MOS (OS 1.20) und dann basic2.rom in Bank 15 und dfs.rom in Bank 14 aus demselben Verzeichnis (SD/default/roms/BBC/).
Bereich Inhalt
$0000–$7FFF RAM, 32K
$8000–$BFFF 16 Sideways-Bänke, ausgewählt über ROMSEL ($FE30)
$C000–$FBFF, $FF00–$FFFF MOS
$FC00–$FDFF FRED / JIM — lesen 0xFF
$FE00–$FEFF SHEILA — System-VIA modelliert; CRTC- und Video-ULA-Register nur gespeichert; User-VIA, FDC, Econet und Tube lesen als nicht vorhanden; ACIA inaktiv (das BBCCMT der Firmware virtualisiert ihn)
Tastaturzeichen auf dem virtuellen seriellen Port A werden mit einer Drücken-/Halten-/Loslassen-/Pause-Sequenz in die Matrix abgebildet; MODE 7-Teletext wird live als ANSI gerendert auf dasselbe Terminal, änderungsgesteuert mit etwa 12 Hz ab der CRTC-Startadresse. Grafikmodi werden nicht gezeichnet. Die Tastatur-Links sind auf MODE 7 und kein Auto-Boot gesetzt.

Eine eigene Host-Maschine anbinden

Das Hinzufügen eines Maschinenmodells ist ein Vier-Funktionen-Interface in sim/src/sim6502.h:
typedef struct {
    uint8_t (*fetchOp) (uint16_t addr);                // opcode fetch (SYNC asserted)
    uint8_t (*readMem) (uint16_t addr);                // data read
    void    (*writeMem)(uint16_t addr, uint8_t data);  // write
    void    (*hostReset)(void);                        // host-side reset
} t_Sim6502BusOps;

void sim6502_setBusOps(const t_Sim6502BusOps *ops);    // NULL restores the floating bus
void sim6502_assertIRQ(bool active);                   // level: re-assert every cycle while low
void sim6502_assertNMI(bool active);                   // edge-triggered
fetchOp ist von readMem getrennt, weil ein echter 6502 SYNC nur beim Opcode-Abruf aktiviert — sie getrennt zu halten, erprobt dieselben zwei Firmware-Pfade wie die Hardware. Ohne angebundenen Host liefern Buslesezugriffe 0xFF: ein ungetriebener 6502-Bus mit Pull-ups liest 0xFF, und 0xFF ist ISC $nnnn,X — ein undokumentierter Read-Modify-Write, der weder anhält noch verzweigt, sodass die Emulation den Adressraum genau so frei durchläuft wie ein nicht angeschlossener Sockel, und die busdiag-Ausgabe des Simulators direkt mit der eines toten Boards vergleichbar ist.
Was der Simulator Ihnen nicht sagen kann: es gibt keinen Takt, keine PHI1/PHI2-Phasenbeziehung, keine Setup- oder Hold-Zeiten, keine Puffer-Laufzeitverzögerung und keine Buskonflikte — genau die Dinge, die das v2.2-Board falsch gemacht hat, das gepufferte v2.3-Interface behoben hat und auf die die Inbetriebnahme im September 2026 dann ihre Logikanalysator-Zeit verwendet hat. Ein sauberer Lauf hier validiert die Logik der Firmware; die Korrektheit der Bus-Engine wird auf der Hardware durch den Shadow-Verifizierer, pipetest und die LA5016-Aufzeichnungen nachgewiesen.

Core 0 / Core 1-Interaktion

Das Inter-Core-Queue-Muster ist identisch mit dem picoZ80. Siehe das picoZ80 Entwicklerhandbuch — Core 0 / Core 1-Interaktion für die vollständige Beschreibung und das Codebeispiel. Der einzige Namensunterschied ist, dass auf die Queue über einen M6502CPU *cpu-Zeiger statt über Z80CPU *cpu zugegriffen wird.
Wichtige Regel: Handler- und Poll-Callbacks laufen auf Core 1. Jede Datei-I/O, jeder UART-Befehl an den ESP32 oder jede andere blockierende Operation muss über cpu->requestQueue an Core 0 gesendet werden. Ergebnisse werden über cpu->responseQueue zurückgegeben und in task_ptr verarbeitet.

Häufige Fallstricke

  • Einen Handler in ioPtr statt memioPtr installieren. Der pico6502 hat kein ioPtr[]-Array. Alle Peripherie-Handler gehören in memioPtr[]. Der Versuch, ioPtr[] zu verwenden, führt zu einem Build-Fehler oder undefiniertem Verhalten.
  • Einen I/O-Dispatch-Aufruf erwarten. Es gibt kein M6502CPU_readIO() oder M6502CPU_writeIO(). Wenn Sie einen Z80-Treiber portieren, der ioPtr[] verwendet, verschieben Sie alle diese Handler nach memioPtr[] an die passenden Speicheradressen.
  • Blockieren in einem Handler. Jeder Aufruf von debugf, sleep_ms oder fopen aus einem Handler heraus blockiert Core 1 und beschädigt das 6502-Bus-Timing. Verwenden Sie die Inter-Core-Queue für alle blockierenden Operationen.
  • Nicht übereinstimmender virtualFuncMap-Name. Das JSON-Feld "name" wird groß-/kleinschreibungsunabhängig gegen die virtualFuncMap[]-Tabelle in M6502CPU.c abgeglichen. Ein Tippfehler auf einer der beiden Seiten überspringt den Treiber stillschweigend. Fügen Sie zur Diagnose ein temporäres debugf in M6502CPU_getVirtualFunc() ein.
  • Off-by-one im Handler-Bereich. Verwenden Sie addr <= endAddr und nicht addr < endAddr + 1 — Letzteres riskiert einen Integer-Überlauf bei 0xFFFF. Verwenden Sie addr <= 0xFFFF mit einer uint32_t-Schleifenvariable, wenn der Bereich bis zum oberen Speicherende reicht.
  • Vergessen, den Blocktyp auf FUNC zu setzen. Die Installation eines Handlers in memioPtr[] hat keine Wirkung, wenn die entsprechenden _membankPtr[]-Einträge noch MEMBANK_TYPE_RAM oder MEMBANK_TYPE_ROM anzeigen. Rufen Sie immer MEMBANK_ENCODE(MEMBANK_TYPE_FUNC, ...) für jeden 512-Byte-Block in Ihrem Handler-Bereich auf.
  • Nicht deklarieren, was der Host hinter dem Rücken der CPU ändern kann. Der Shadow wird gegen jedes Bus-Byte verifiziert, und eine einzige Abweichung stellt den Block für die Sitzung unter Quarantäne. Wenn Ihre Maschine einen Bereich pagt, einen Bereich schreibignoriert oder irgendwo I/O hat, deklarieren Sie es (I/O-Fenster, Paging-Register und Maske, Shadow-Schreibfenster) in der Tuning-Funktion des Treibers — die ~10 ppm-Korruption bei Galaxians („Bits fallen immer nur weg") und der MMFS-Disketten-Erkennungsfehler waren beide Probleme einer nicht deklarierten Speicherbelegung.
  • Einen Interrupt aus einem Handler durch Setzen von PIO-Flags auslösen. Setzen Sie stattdessen g_m6502VirtIrq; der Kern tastet es zusammen mit der physischen Leitung einmal pro Instruktion ab. Die Taktung gehört in einen g_fastTickHook, nicht in den Handler.
  • Timing in C. Nichts auf Core 1 darf versuchen, eine Busflanke zu platzieren; sieben C-seitige Fast-Issue-Schemata haben jeden synthetischen Test bestanden und unter realer Ausführung Daten beschädigt. Wenn ein Host ein anderes Bus-Timing braucht, ist das eine PIO-Änderung, als Stellschraube nach außen geführt (readsettle, writestrobes, die Leiter-Verzögerungen).
  • reset_ptr beim Treiber-Shutdown nicht gelöscht. Wenn Ihr Treiber zur Laufzeit neu konfiguriert wird und der Reset-Callback nicht mehr gültig ist, setzen Sie cpu->reset_ptr vor der Neuinitialisierung auf NULL, andernfalls ruft Core 1 beim nächsten Reset einen veralteten Funktionszeiger auf.

Referenz-Websites

Ressource Link
pico6502 Projektseite /pico6502/
pico6502 Benutzerhandbuch /pico6502-usermanual/
pico6502 Technischer Leitfaden /pico6502-technicalguide/
picoZ80 Entwicklerhandbuch /picoz80-developersguide/
picoZ80 Projektseite /picoz80/
RP2350 Datenblatt datasheets.raspberrypi.com
Pico SDK Multicore API raspberrypi.github.io/pico-sdk-doxygen
MOS 6502 Datenblatt archive.org
BBC Micro Hardware Guide stardot.org.uk
cJSON-Bibliothek github.com/DaveGamble/cJSON

Regulatorischer Hinweis für Funkgeräte

Dieses Gerät enthält ein ESP32-S3-PICO-1-Funkmodul, das im 2,4 GHz ISM-Band sendet und es damit zu einem beabsichtigten Strahler gemäß den Funkfrequenzvorschriften weltweit macht (einschließlich FCC Part 15 Subpart C in den Vereinigten Staaten und der Funkanlagenrichtlinie 2014/53/EU in der Europäischen Union).
Obwohl das ESP32-S3-PICO-1-Modul selbst über bestehende regulatorische Zertifizierungen verfügt (FCC, CE und andere), erstrecken sich diese Modulzertifizierungen nicht automatisch auf ein fertiges Produkt, das das Modul enthält. Die Vorzertifizierungs-Ausnahme erlaubt es einzelnen Hobbyisten, eine begrenzte Anzahl von Geräten für den persönlichen, experimentellen oder bildungsbezogenen Gebrauch zu bauen, ohne eine separate Gerätezulassung zu erhalten.
Wichtige Einschränkungen
  • Zusammengebaute Geräte dürfen nicht verkauft, zum Verkauf angeboten, verschenkt oder anderweitig an Dritte verteilt werden, es sei denn, das fertige Produkt wurde unabhängig getestet und hat in der betreffenden Gerichtsbarkeit eine eigene Gerätezulassung erhalten (z.B. FCC-ID, CE-Kennzeichnung mit Bewertung durch eine benannte Stelle).
  • Der Bau dieses Projekts für den persönlichen Gebrauch in begrenzten Stückzahlen ist im Allgemeinen unter Hobbyisten- und Experimentiervorschriften gestattet (z.B. FCC § 15.23), vorausgesetzt das Gerät verursacht keine schädlichen Störungen.
  • Regulatorische Anforderungen variieren je nach Land. Bauherren außerhalb der Vereinigten Staaten sollten ihre nationale Funkfrequenzbehörde zu den geltenden Regeln konsultieren.
Verantwortung des Bauherrn
Es liegt in der alleinigen Verantwortung des Bauherrn sicherzustellen, dass jedes aus diesen Designs konstruierte Gerät allen anwendbaren Funkfrequenzvorschriften in seiner Gerichtsbarkeit entspricht. Der Autor stellt diese Designs für den persönlichen, bildungsbezogenen und Hobbygebrauch zur Verfügung und gibt keine Zusicherung, dass ein aus ihnen gebautes Gerät die regulatorischen Anforderungen für den kommerziellen Vertrieb erfüllt.