picoZ80 Entwicklerhandbuch

picoZ80 Entwicklerhandbuch

Dieses Handbuch ist eine umfassende Referenz fuer Entwickler, die die Interna der picoZ80-Firmware verstehen und eigene Peripherietreiber schreiben moechten. Es behandelt die gesamte Softwarearchitektur von der Core-1-Bus-Dispatch-Schleife ueber die Treiberregistrierung, die Installation von Speicher-Hooks bis hin zur I/O-Virtualisierung. Der Sharp MZ-700-Treiber (MZ700.c) wird durchgehend als konkretes Praxisbeispiel verwendet.
Vorkenntnisse der picoZ80-Codebasis werden nicht vorausgesetzt. Jedes Konzept wird von Grund auf erklaert und anschliessend im tatsaechlichen Quellcode gezeigt. Am Ende dieses Handbuchs werden Sie in der Lage sein, einen vollstaendigen Treiber von Grund auf zu schreiben, ihn dem Build-System hinzuzufuegen, im Framework zu registrieren und ueber JSON zu konfigurieren.
Fuer die Hardwarearchitektur, Details zur PIO-Busschnittstelle und die JSON-Konfigurationsreferenz siehe den picoZ80 Technischen Leitfaden. Fuer die Einrichtung durch den Endbenutzer siehe das picoZ80 Benutzerhandbuch.

Quellcode-Struktur

Der gesamte Quellcode befindet sich unter projects/tzpuPico/ innerhalb des Repository-Stammverzeichnisses (der genaue Checkout-Pfad haengt von Ihrem System ab). Das folgende Layout zeigt die fuer die Treiberentwicklung relevanten Dateien:
tzpuPico/
├── CMakeLists.txt                          Top-level Build-Datei
├── src/
│   ├── CMakeLists.txt                      Source-level Build-Datei — neue Treiberdateien hier hinzufuegen
│   ├── Z80CPU.c                            Haupt-Z80-Emulation, Bus-Dispatch, Treiberframework
│   ├── Z80CPU.h                            (Legacy — eingebunden ueber include/)
│   ├── M6502CPU.c                          6502-Parallele (gleiche Architektur)
│   ├── FSPI.c / FSPI.h                     Flash-SPI-Schnittstelle
│   ├── ESP.c / ESP.h                       ESP32-Kommunikationsschicht
│   ├── psram.c / psram.h                   PSRAM-Zuordnung und -Verwaltung
│   ├── cJSON.c / cJSON.h                   JSON-Parser fuer config.json
│   ├── include/
│   │   ├── Z80CPU.h                        *** SCHLÜSSELDATEI: alle Typdefinitionen und Makros ***
│   │   ├── dbgsh.h                         Debug-Shell (ICE-Debugger) Header
│   │   └── drivers/
│   │       ├── Z80SIO.h                    Zilog Z80 SIO/2 Emulations-Header (gemeinsam)
│   │       ├── Sharp/
│   │       │   ├── MZ.h                    MZ-Serien gemeinsame Konstanten
│   │       │   ├── MZ700.h                 MZ-700 Treiber-Header
│   │       │   ├── MZ80A.h                 MZ-80A Treiber-Header
│   │       │   ├── MZ2000.h                MZ-2000 Treiber-Header
│   │       │   ├── MZ2200.h                MZ-2200 Treiber-Header
│   │       │   ├── MZ80B.h                 MZ-80B Treiber-Header
│   │       │   ├── MZ2500.h                MZ-2500 Treiber-Header
│   │       │   ├── MZ800.h                 MZ-800 Treiber-Header
│   │       │   ├── MZ1500.h                MZ-1500 Treiber-Header
│   │       │   ├── MZ8BIO3.h               MZ-8BIO3 RS-232C-Karte (Z80 SIO) Header
│   │       │   ├── MZ1E24.h                MZ-1E24 RS-232C-Karte (Z80 SIO) Header
│   │       │   ├── RFS.h / TZFS.h          Dateisystem-Header
│   │       │   ├── WD1773.h                Floppy-Controller-Header
│   │       │   ├── QDDrive.h               QuickDisk-Header
│   │       │   ├── MZ-1R23.h               MZ-1R23 Kanji-ROM-Header
│   │       │   ├── MZ-1R37.h               MZ-1R37 EMM-Header
│   │       │   ├── PIO-3034.h              PIO-3034 EMM-Header
│   │       │   ├── Celestite.h             Celestite-Composite-Board-Header
│   │       │   ├── SASI.h                  SASI-Festplattenprotokoll-Header
│   │       │   ├── MZ1E30.h               MZ-1E30 SASI-Controller-Header
│   │       │   └── MZ8BFI.h                MZ-8BFI Floppy-Interface-Header
│   │       ├── Amstrad/
│   │       │   ├── PCW9512.h               Amstrad PCW-9512 Treiber-Header
│   │       │   └── uPD765.h                NEC uPD765 FDC-Header
│   │       └── Tatung/
│   │           ├── EinsteinTC01.h           Tatung Einstein TC-01 Treiber-Header
│   │           ├── EinsteinFDC.h            Einstein FDC Sub-Interface-Header
│   │           └── WD1770.h                 WD1770 FDC-Header
│   ├── dbgsh.c                             Debug-Shell (ICE-Debugger) — USB CDC Kanal 1
│   ├── PIT8253.c                           Intel 8253 PIT Emulationsmodul
│   ├── PPI8255.c                           Intel 8255 PPI Emulationsmodul
│   ├── drivers/
│   │   ├── Z80SIO.c                        Zilog Z80 SIO/2 Emulation (gemeinsam genutzt von RS-232C-Karten)
│   │   └── Sharp/
│   │       ├── MZ700.c                     *** BEISPIELTREIBER ***
│   │       ├── MZ80A.c                     MZ-80A Persona-Treiber
│   │       ├── MZ2000.c                    MZ-2000 Persona-Treiber
│   │       ├── MZ2200.c                    MZ-2200 Persona-Treiber
│   │       ├── MZ80B.c                     MZ-80B Persona-Treiber
│   │       ├── MZ2500.c                    MZ-2500 Persona-Treiber
│   │       ├── MZ800.c                     MZ-800 Persona-Treiber (Dual-Modus MZ-700/MZ-800)
│   │       ├── MZ1500.c                    MZ-1500 Persona-Treiber
│   │       ├── MZ8BIO3.c                   MZ-8BIO3 RS-232C-Karte (gemeinsame SIOCard_*-Implementierung)
│   │       ├── MZ1E24.c                    MZ-1E24 RS-232C-Karte (SIOCard_*-Wrapper)
│   │       ├── RFS.c                       ROM Filing System Treiber
│   │       ├── TZFS.c                      TranZPUter Filing System Treiber
│   │       ├── WD1773.c                    WD1773 Floppy-Controller-Treiber
│   │       ├── QDDrive.c                   QuickDisk-Laufwerkstreiber
│   │       ├── MZ-1E05.c / MZ-1E14.c / MZ-1E19.c   Peripherie-Schnittstellenkarten
│   │       ├── MZ8BFI.c                    MZ-8BFI / E0054PA Floppy-Interface (MZ-2000)
│   │       ├── MZ-1R12.c / MZ-1R18.c      RAM-Erweiterungskarten
│   │       ├── MZ-1R23.c                   MZ-1R23 Kanji-ROM / MZ-1R24 Woerterbuch-ROM
│   │       ├── MZ-1R37.c                   MZ-1R37 640KB EMM
│   │       ├── PIO-3034.c                  IO DATA PIO-3034 320KB EMM
│   │       ├── Celestite.c                 Celestite LAN / Speicher-Composite-Board
│   │       ├── SASI.c                      SASI-Festplattenprotokoll-Treiber
│   │       └── MZ1E30.c                    MZ-1E30 SASI-Festplattencontroller
│   ├── drivers/
│   │   ├── Amstrad/
│   │   │   ├── PCW9512.c                   *** AMSTRAD PCW-9512 TREIBER ***
│   │   │   └── uPD765.c                    NEC uPD765 FDC-Treiber (CPC DSK-Format)
│   │   └── Tatung/
│   │       ├── EinsteinTC01.c              Tatung Einstein TC-01 Persona-Treiber
│   │       ├── EinsteinFDC.c               Einstein FDC Sub-Interface (2 Laufwerke, DSK/D88)
│   │       └── WD1770.c                    WD1770 FDC-Emulation (wiederverwendbares Modul, getrennt von WD1773.c)
│   │   └── Other/
│   │       └── Open.c                      OpenZ80 Vanilla / Experimenter-Persona-Treiber
│   └── model/
│       ├── BaseZ80/                        Alle Treiber (Sharp + Amstrad)
│       │   ├── CMakeLists.txt              Pro-Modell Build-Targets
│       │   ├── main.c                      Einstiegspunkt (Core 0 + Core 1 Start)
│       │   ├── main_memmap_partition_1.ld  Linker-Skript fuer Slot 1
│       │   └── main_memmap_partition_2.ld  Linker-Skript fuer Slot 2
│       ├── SharpZ80/                       Nur Sharp MZ-Treiber (kleinere Binaerdatei)
│       │   ├── CMakeLists.txt
│       │   ├── main.c
│       │   ├── main_memmap_partition_1.ld
│       │   └── main_memmap_partition_2.ld
│       ├── AmstradZ80/                     Nur Amstrad PCW-Treiber (kleinere Binaerdatei)
│       │   ├── CMakeLists.txt
│       │   ├── main.c
│       │   ├── main_memmap_partition_1.ld
│       │   └── main_memmap_partition_2.ld
│       ├── TatungZ80/                      Nur Tatung Einstein-Treiber (kleinere Binaerdatei)
│       │   ├── CMakeLists.txt
│       │   ├── main.c
│       │   ├── main_memmap_partition_1.ld
│       │   └── main_memmap_partition_2.ld
│       ├── OpenZ80/                         Nur OpenZ80 Experimenter-Persona (maschinenagnostische Karten)
│       │   ├── CMakeLists.txt
│       │   ├── main.c
│       │   ├── main_memmap_partition_1.ld
│       │   └── main_memmap_partition_2.ld
│       └── Bootloader/
└── tools/
    └── NetFileServer/
        └── netfs.py                       Netzwerk-Dateiserver fuer MZF-Dateien ueber TCP

PIO-Busschnittstelle — Wie C-Code die Zustandsmaschinen steuert

Die Z80-Busschnittstelle laeuft vollstaendig in PIO-Hardware (z80.pio), aber der C-Code auf Core 1 orchestriert, welcher Buszyklus wann ausgefuehrt wird. Das Verstaendnis dieses Zusammenspiels ist wichtig fuer jeden, der Bus-Timing debuggt oder neue Zyklustypen hinzufuegt.
Der zentrale Mechanismus ist out exec, 16 — ein PIO-Befehl, der einen 16-Bit-Wert aus dem TX-FIFO holt und ihn als PIO-Befehl ausfuehrt. Die z80_cycle-Zustandsmaschine (PIO 0 SM 2) verwendet dies in einer engen Schleife:
// z80_cycle SM-Programm (PIO 0 SM 2):
//
// start_cycle:
//     wait 0 irq 6          ; Anhalten wenn BUSREQ aktiv
//     irq set 0             ; Signal "bereit"
//     wait 0 irq 0          ; Warten bis C-Code Adresse geladen und IRQ 0 geloescht hat
//     wait 1 gpio CLK       ; Synchronisation auf steigende T1-Flanke
// cycle_exec:
//     out exec, 16           ; Befehl aus FIFO holen, ausfuehren
//     jmp cycle_exec         ; Wiederholen
//
// Der C-Code schiebt eine Sequenz kodierter 16-Bit-PIO-Befehle
// in den TX-FIFO der Zyklus-SM. Die Sequenz steuert jeden Aspekt
// des Buszyklus: welche Steuersignale aktiviert/deaktiviert werden, wann
// auf Taktflanken gewartet wird, und wann Daten abgetastet werden.
// Der letzte Befehl in jeder Sequenz ist ein JMP zurueck zu start_cycle.
Der C-Code kodiert Befehlssequenzen fuer jeden Zyklustyp zur Compile-Zeit vor. Diese werden als Arrays von uint16_t-Werten gespeichert. Zur Laufzeit schiebt die Hot-Loop von Core 1 die entsprechende Sequenz basierend auf der aktuellen Bus-Transaktion in den FIFO:
// Vereinfachter Core-1-Ablauf fuer einen Speicher-Lesezyklus:

// 1. z80_cycle SM setzt IRQ 0 — sie ist bereit fuer einen neuen Zyklus.
//    z80_addr SM setzt IRQ 0 — sie ist bereit fuer eine Adresse.

// 2. Core 1 loest die Adresse auf und bereitet die Bus-Transaktion vor:
pio_sm_put(pio0, SM_ADDR, (pindirs_16 << 16) | address);  // Push an z80_addr
pio_interrupt_clear(pio0, 0);                                // IRQ 0 loeschen → Addr-SM laeuft

// 3. Core 1 schiebt die Zyklustyp-Befehlssequenz:
pio_sm_put(pio0, SM_CYCLE, encoded_wait_clk_low);   // wait 0 gpio CLK
pio_sm_put(pio0, SM_CYCLE, encoded_set_mreq_rd);    // set pins: /MREQ low, /RD low
pio_sm_put(pio0, SM_CYCLE, encoded_wait_clk_high);  // wait 1 gpio CLK (T2)
pio_sm_put(pio0, SM_CYCLE, encoded_wait_clk_low);   // wait 0 gpio CLK (T2)
// ... Wait-State-Pruefung, T3, Datenabtastung ...
pio_sm_put(pio0, SM_CYCLE, encoded_jmp_start);       // JMP start_cycle (Ende)

// 4. Core 1 liest das Datenbyte aus dem RX-FIFO:
uint8_t data = pio_sm_get(pio0, SM_DATA);
Koordination zwischen Zustandsmaschinen: Die Adress-SM (z80_addr) und Daten-SM (z80_data) verwenden jeweils ihr eigenes IRQ-Flag (IRQ 0 bzw. IRQ 1) zur Implementierung eines Producer/Consumer-Handshakes:
  • Die SM setzt ihr IRQ-Flag und blockiert bei wait 0 irq N — "Ich bin bereit, sende mir Daten".
  • Core 1 schiebt die Adresse oder Daten in den TX-FIFO der SM und loescht dann das IRQ-Flag.
  • Die SM wird geweckt, holt die Daten aus dem FIFO und treibt die Pins.
Dieser Handshake stellt sicher, dass Bussignale niemals getrieben werden, bevor die korrekten Werte geladen sind, und dass Core 1 niemals FIFO-Eintraege ueberschreibt, die die SM noch nicht verarbeitet hat.
Der Daten-SM (z80_data) Ablauf fuer einen Lesezyklus:
  1. Setzt IRQ 1 und wartet — "bereit fuer Richtung/Daten".
  2. Core 1 loescht IRQ 1 nachdem Pin-Richtung (Eingabemodus) und ein Dummy-Datenbyte geschoben wurden.
  3. Die SM setzt Pin-Richtungen auf Eingang (Tristate), wodurch der Host-Speicher D0–D7 treiben kann.
  4. Die SM wartet auf wait 0 irq 0 bis die naechste Adressaenderung das Zyklusende anzeigt.
  5. Die SM setzt die Pin-Richtungen in einen definierten Zustand zurueck.
Fuer einen Schreibzyklus schiebt Core 1 Ausgangs-Pin-Richtungen und das tatsaechliche Datenbyte; die SM treibt D0–D7 mit den Daten.

Wichtige Typen und Datenstrukturen

Bevor wir uns das Treiberframework ansehen, ist es wesentlich, die in src/include/Z80CPU.h definierten Kerndatenstrukturen zu verstehen. Diese Strukturen werden an jede Treiberfunktion uebergeben und sind das primaere Mittel, mit dem ein Treiber mit dem Speicher- und I/O-System interagiert.

Speicherblocktyp-Konstanten

Jeder 512-Byte-Block im 64KB-Adressraum des Z80 hat einen Typ, der in den oberen 8 Bits seines membankPtr-Eintrags kodiert ist. Der Typ teilt der Dispatch-Schleife von Core 1 mit, wie Bus-Transaktionen behandelt werden sollen, die in diesen Block fallen.
// src/include/Z80CPU.h

#define MEMBANK_TYPE_UNKNOWN        0x00  // Nicht initialisiert — sollte zur Laufzeit nie auftreten
#define MEMBANK_TYPE_PHYSICAL       0x01  // Durchleitung: RP2350 gibt Bus frei, Host-Hardware antwortet
#define MEMBANK_TYPE_PHYSICAL_VRAM  0x02  // Durchleitung mit Wait-States fuer Host-Video-RAM
#define MEMBANK_TYPE_PHYSICAL_HW    0x04  // Durchleitung fuer I/O-gemappte Host-Hardwareregister
#define MEMBANK_TYPE_RAM            0x08  // Lese/Schreib — durch PSRAM-Bank gestuetzt
#define MEMBANK_TYPE_VRAM           0x10  // PSRAM-Video-RAM — Schreibvorgaenge werden auch ins physische VRAM gespiegelt
#define MEMBANK_TYPE_ROM            0x20  // Nur Lesen — durch PSRAM-Bank gestuetzt; Schreibvorgaenge werden stillschweigend ignoriert
#define MEMBANK_TYPE_FUNC           0x40  // Virtuelles Geraet — jeder Zugriff ruft eine C-Funktionsbehandlung auf
#define MEMBANK_TYPE_PTR            0x80  // Umleitung — jedes Byte verweist auf eine andere Adresse
Der Typ bestimmt, was in Z80CPU_readMem() und Z80CPU_writeMem() geschieht:
  • PHYSICAL / PHYSICAL_VRAM / PHYSICAL_HW — der RP2350 faengt die Bus-Transaktion nicht ab; echte Hardware auf der Host-Platine antwortet. Verwenden Sie dies fuer jeden Bereich, in dem die eigenen Chips des Hosts (ROM, RAM, Videohardware) die Kontrolle behalten muessen.
  • RAM — Lese- und Schreibzugriffe gehen an eine 64KB-Bank im 8MB-PSRAM. Wenn eine memioPtr-Funktion fuer die spezifische Adresse installiert ist, wird diese Funktion anstelle von (bei FUNC) oder neben dem PSRAM-Zugriff aufgerufen (Handler koennen abfangen oder nachbearbeiten). Wait-States und T1-Synchronisation koennen pro Block konfiguriert werden.
  • ROM — Lesezugriffe kommen aus dem PSRAM (typischerweise aus einer Imagedatei beim Booten geladen). Schreibzyklen erreichen weiterhin jeden installierten memioPtr-Handler, aber das PSRAM wird nicht modifiziert — nuetzlich fuer ROM-Banking-Register, die in einem ROM-gemappten Adressbereich liegen.
  • VRAM — Lesezugriffe aus dem PSRAM; Schreibzugriffe gehen parallel sowohl ins PSRAM als auch ins physische Host-VRAM. Dies ermoeglicht Software, eine Schattenkopie des Videopuffers zu pflegen und gleichzeitig den echten Bildschirm zu aktualisieren.
  • FUNC — es gibt keine PSRAM-Stuetzung. Jedes Lesen und Schreiben ruft die in memioPtr[addr] installierte Funktion auf. Verwenden Sie dies fuer virtualisierte Hardwareregister, in den Speicherraum gemappte Banking-Steuerports und jede Ressource ohne echten RAM dahinter.
  • PTR — jedes Byte des 512-Byte-Blocks kann unabhaengig auf einen anderen PSRAM-Ort oder Speichertyp verweisen. Wird fuer sehr feingranulare Adressraum-Manipulation verwendet.

membankPtr-Kodierung

Das _membankPtr[]-Array hat 128 Eintraege — einen pro 512-Byte-Block des 64KB-Z80-Adressraums. Jeder Eintrag ist ein einzelner 32-Bit-Wert, der drei Felder kodiert:
Bit 31..24  =  Speichertyp   (MEMBANK_TYPE_xxx Konstante)
Bit 23..16  =  PSRAM-Bank    (0-63, welche 64KB-Bank im 8MB-PSRAM)
Bit 15..0   =  Z80-Adresse   (Basisadresse des Blocks innerhalb der Bank)
Um Typ und Bank eines Blocks zu setzen, packen Sie diese drei Werte mittels bitweisem OR in eine einzelne 32-Bit-Ganzzahl:
// Block idx (jeder Block = 512 Bytes) als RAM-Typ in Bank 0 mappen:
cpu->_membankPtr[idx] = (MEMBANK_TYPE_RAM  << 24)   // Typ im oberen Byte
                      | (MZ700_MEMBANK_0   << 16)   // Banknummer
                      | (idx * MEMORY_BLOCK_SIZE);  // Basisadresse dieses Blocks

// Gleichen Block als ROM in Bank 2 mappen:
cpu->_membankPtr[idx] = (MEMBANK_TYPE_ROM  << 24)
                      | (2                 << 16)
                      | (idx * MEMORY_BLOCK_SIZE);

// Gleichen Block als physisch (Durchleitung) mappen:
cpu->_membankPtr[idx] = (MEMBANK_TYPE_PHYSICAL << 24)
                      | (0 << 16)
                      | (idx * MEMORY_BLOCK_SIZE);
MEMORY_BLOCK_SIZE ist 512 Bytes. Es gibt 128 Bloecke die 0x0000–0xFFFF abdecken. Der Blockindex fuer eine gegebene Z80-Adresse ist: addr / MEMORY_BLOCK_SIZE = addr >> 9.

Speicherattribute (t_memAttr)

Jeder Block hat auch einen t_memAttr-Eintrag, der Wait-States und T-Zyklus-Synchronisation steuert. Diese werden in einem 2D-Array gespeichert, indiziert nach Bank und Block:
// src/include/Z80CPU.h
typedef struct {
    uint8_t   waitStates;   // Anzahl zusaetzlicher T-Zyklus-Wait-States bei Zugriff
    bool      tCycSync;     // true = PSRAM-Zugriff mit steigender T1-Flanke jedes Buszyklus synchronisieren
} t_memAttr;

// Zugriffsmuster:
cpu->_memAttr[bank][idx].waitStates = 1;
cpu->_memAttr[bank][idx].tCycSync   = true;
waitStates: Wenn Ihr PSRAM-gestuetzter Speicherbereich mehr Zeit zum Antworten benoetigt (z.B. weil die Handler-Funktion zusaetzliche Arbeit leistet), fuegen Sie Wait-States hinzu. Jeder Wait-State verlaengert den Buszyklus um einen T-Zyklus des Host-Takts. RAM-Bereiche verwenden typischerweise 1 Wait-State; ROM-Bereiche, die vorgeladene Daten liefern, koennen oft 0 verwenden.
tCycSync: Wenn auf true gesetzt, verzoegert die PIO-z80_sync-Zustandsmaschine den PSRAM-Zugriff bis zur steigenden T1-Flanke des aktuellen Buszyklus. Dies verhindert, dass interne PSRAM-Operationen Timing-Drift in Host-Software einfuehren, die auf praezises Taktzyklen-Timing angewiesen ist (Kassetten-I/O, serielles Bit-Banging, Verzoegerungsschleifen). Setzen Sie dies auf true fuer RAM/ROM-Bereiche, auf die zeitkritische Software des Hosts zugreift.

Die PSRAM-Struktur (t_Z80PSRAM)

Das 8MB externe PSRAM wird in eine einzelne t_Z80PSRAM-Struktur gemappt. Diese wird einmalig beim Start alloziert und durch cpu->_z80PSRAM referenziert. Sie ist die groesste und wichtigste Datenstruktur im System — alles, worauf der Z80 zugreifen kann, lebt hier.
// src/include/Z80CPU.h
typedef struct {
    // 4MB Datenbereich: 64 Baenke x 64KB RAM/ROM-Imagespeicher
    uint8_t     RAM[MEMORY_PAGE_BANKS * MEMORY_PAGE_SIZE];

    // 64KB Pro-Byte-Umleitungstabelle (verwendet von MEMBANK_TYPE_PTR Bloecken)
    uint32_t    memPtr[MEMORY_PAGE_SIZE];

    // 64KB Speicheradress-Funktionszeigertabelle
    // Index = Z80-Adresse (0x0000-0xFFFF)
    // Wert = NULL (keine Ueberschreibung) oder Zeiger auf eine C-Handler-Funktion
    MemoryFunc  memioPtr[MEMORY_PAGE_SIZE];

    // 64KB I/O-Port-Funktionszeigertabelle
    // Index = Z80-I/O-Portadresse (0x0000-0xFFFF; Z80 verwendet die unteren 8 Bits fuer den Port)
    // Wert = NULL (Durchleitung zur physischen I/O) oder Zeiger auf eine C-Handler-Funktion
    MemoryFunc  ioPtr[IO_PAGE_SIZE];
} t_Z80PSRAM;
Die vier Unter-Arrays haben unterschiedliche Aufgaben:
  • RAM[] — roher Byte-Speicher fuer alle PSRAM-gestuetzten Speicherbaenke. Die Gesamtgroesse betraegt 64 Baenke x 64KB = 4MB. ROM-Images, die von der SD-Karte oder dem Flash geladen werden, werden hier beim Booten geschrieben. Waehrend der Z80-Ausfuehrung greifen Lese- und Schreibzugriffe auf RAM/ROM/VRAM-Typ-Bloecke auf dieses Array zu.
  • memPtr[] — wird nur von PTR-Typ-Bloecken verwendet. Jeder Eintrag ist ein vollstaendiger membankPtr-Wert (gleich kodiert wie cpu->_membankPtr[]), der den Zugriff eines einzelnen Bytes auf einen voellig anderen Ort umleitet. Ermoeglicht Byte-Level-Remapping innerhalb des 64KB-Raums.
  • memioPtr[] — die Speicher-Funktions-Hook-Tabelle. Ein Slot pro Z80-Adresse. Wenn ein Slot nicht NULL ist, ruft Core 1 die Funktion an diesem Slot bei jedem Speicherzugriff auf diese Adresse auf, unabhaengig vom Blocktyp (RAM, ROM oder FUNC). So fangen Treiber bestimmte Speicheradressen ab oder ueberschreiben sie, ohne den gesamten Blocktyp zu aendern.
  • ioPtr[] — die I/O-Port-Hook-Tabelle. Ein Slot pro Z80-I/O-Adresse (Port A0–A15 breit, obwohl der Z80 nur A0–A7 als eigentliche Portnummer verwendet; die oberen Bits koennen zusaetzlichen Kontext tragen). Wenn nicht NULL, wird die Funktion fuer jeden IN- oder OUT-Befehl aufgerufen, der auf diesen Port abzielt. Wenn NULL, wird der I/O-Zyklus an die physische Hardware durchgeleitet.

Die MemoryFunc-Handler-Signatur

Sowohl memioPtr[]- als auch ioPtr[]-Slots halten Funktionszeiger desselben Typs — MemoryFunc:
// src/include/Z80CPU.h
typedef uint8_t (*MemoryFunc)(Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
Parameter:
  • cpu — Zeiger auf den Z80CPU-Kontext. Gibt Ihnen Zugriff auf _membankPtr[], _z80PSRAM und alles andere. Modifizieren Sie niemals _membankPtr[] aus einem I/O-Handler heraus, der von der Hot-Loop von Core 1 aufgerufen werden kann; verwenden Sie die Intercore-Queue fuer solche Operationen (siehe Core-Interaktion).
  • readtrue wenn dies ein Lesezyklus ist (Z80 liest); false wenn dies ein Schreibzyklus ist (Z80 schreibt).
  • addr — die vollstaendige Z80-Adresse (0x0000–0xFFFF fuer Speicher, 0x0000–0xFFFF fuer I/O-Ports). Fuer I/O verwendet der Z80 nur die unteren 8 Bits als eigentliche Portnummer (A0–A7); die oberen 8 Bits (A8–A15) sind der Wert des B-Registers waehrend des Befehls.
  • data — bei einem Schreibzyklus das Byte, das der Z80 schreibt. Bei einem Lesezyklus aus einem RAM/ROM-Block ist dies der aktuelle Wert an dieser Adresse im PSRAM (Sie koennen ihn verwenden oder ignorieren).
Rueckgabewert:
  • Bei einem Lesen: das Byte, das an den Z80 zurueckgegeben wird. Dies ist der Wert, den der Z80 auf dem Datenbus sieht.
  • Bei einem Schreiben: der Rueckgabewert wird fuer I/O-Handler generell nicht verwendet. Fuer memioPtr-Handler auf RAM-Typ-Bloecken wird der Rueckgabewert anstelle der Originaldaten ins PSRAM zurueckgeschrieben — verwenden Sie dies, um zu modifizieren oder zu bereinigen, was gespeichert wird.
Wichtig: Handler-Funktionen werden direkt aus der Hot-Loop von Core 1 aufgerufen. Sie laufen auf Core 1 mit deaktivierten Interrupts. Sie muessen kurz, deterministisch sein und duerfen niemals blockieren, schlafen, debugf aufrufen oder irgendeine Operation ausfuehren, die Core 1 blockieren koennte. Datei-I/O und UART-Kommunikation muessen ueber die Intercore-Queue an Core 0 gesendet werden.

Die Z80CPU-Kontextstruktur

Jede Treiberfunktion erhaelt einen Z80CPU *cpu-Zeiger. Dies ist der Hauptkontext fuer die gesamte Emulation. Die fuer Treiberentwickler relevantesten Felder sind:
// src/include/Z80CPU.h (vereinfacht)
struct Z80CPU {
    Z80           _Z80;               // Zeta Z80-Emulatorzustand (Register, Flags, PC, etc.)

    // Schnelle Dispatch-Tabelle: 128 Eintraege, einer pro 512-Byte-Block des Z80-Adressraums
    uint32_t      _membankPtr[MEMORY_PAGE_BLOCKS];  // MEMORY_PAGE_BLOCKS = 128

    // Pro-Block Wait-State- und Sync-Attribute, indiziert [Bank][Block]
    t_memAttr     _memAttr[MEMORY_PAGE_BANKS][MEMORY_PAGE_BLOCKS];

    // Zeiger auf die 8MB PSRAM-Struktur (RAM[], memPtr[], memioPtr[], ioPtr[])
    t_Z80PSRAM   *_z80PSRAM;

    // Geladene Treiberkonfigurationen (aus JSON-Parsing)
    t_drivers     _drivers;

    // Intercore-Kommunikations-Queues (Core 1 → Core 0 und Core 0 → Core 1)
    queue_t       requestQueue;
    queue_t       responseQueue;

    bool          halt;        // Z80 ist im HALT-Zustand
    bool          hold;        // Core 0 fordert Core 1 zum Pausieren auf
    bool          holdAck;     // Core 1 bestaetigt die Hold-Anforderung
    bool          forceReset;  // Asynchrones Reset-Flag
};

Treiber-Konfigurationsstrukturen

Wenn ein Treiber initialisiert wird, erhaelt er einen Zeiger auf eine t_drvConfig-Struktur, die vom JSON-Konfigurationsparser befuellt wurde. Diese teilt dem Treiber mit, welche Schnittstellen ihm zugewiesen wurden, welche ROM-Images geladen werden sollen, welche Adress-Remaps anzuwenden sind und welche Parameter der Benutzer konfiguriert hat.
// src/include/Z80CPU.h

// Ein einzelnes Parameter-Schluessel/Wert-Paar (aus JSON "param"-Array)
typedef struct {
    const char   *name;   // Parametername-Zeichenkette
    const char   *value;  // Parameterwert-Zeichenkette (immer eine Zeichenkette; bei Bedarf parsen)
} t_ifParam;

// Eine einzelne ROM-Image-Zuweisung (aus JSON "rom"-Array)
typedef struct {
    const char   *file;       // SD-Karten-Pfad zur ROM-Datei
    uint16_t      addr;       // Z80-Zieladresse fuer dieses ROM
    uint8_t       bank;       // PSRAM-Bank zum Laden
    uint16_t      size;       // Groesse in Bytes zum Laden
    uint32_t      fileofs;    // Byte-Offset in der ROM-Datei
    uint8_t       waitStates; // Wait-States fuer diesen Block
    bool          tCycSync;   // T1-Sync fuer diesen Block
} t_drvROMConfig;

// Ein einzelnes Adressraum-Remap (aus JSON "addrmap"-Array)
typedef struct {
    uint16_t      srcaddr;    // Urspruengliche Z80-Adresse
    uint16_t      dstaddr;    // Umgeleitete Zieladresse
    uint16_t      size;       // Bereichsgroesse
    uint8_t       bank;       // PSRAM-Bank
    uint8_t       type;       // MEMBANK_TYPE_xxx
} t_addrReMap;

// Ein einzelnes I/O-Remap (aus JSON "iomap"-Array)
typedef struct {
    uint16_t      srcaddr;    // I/O-Portadresse
    uint16_t      size;       // Portbereichsgroesse
    const char   *funcName;   // Name der Handler-Funktion (wird in der Memory-Func-Map nachgeschlagen)
} t_ioReMap;

// Ein Interface-Block (ein Eintrag im JSON "if"-Array)
typedef struct t_drvIFConfig {
    const char       *name;           // Interface-Name (z.B. "RFS", "MZ-1E05")
    bool              isPhysical;     // Virtuelles oder physisches Interface
    int               romCount;       // Anzahl der ROM-Eintraege
    t_drvROMConfig   *romConfig;      // Array von ROM-Konfigurationen
    int               addrMapCount;   // Anzahl der Adress-Remaps
    t_addrReMap      *addrMap;        // Adress-Remap-Tabelle
    int               ioMapCount;     // Anzahl der I/O-Remaps
    t_ioReMap        *ioMap;          // I/O-Remap-Tabelle
    int               ifParamCount;   // Anzahl der Parameter
    t_ifParam        *ifParam;        // Parameter-Array
} t_drvIFConfig;

// Top-Level-Treiberkonfiguration (ein Eintrag im JSON "drivers"-Array)
typedef struct t_drvConfig {
    const char       *name;           // Treibername — muss mit einem virtualFuncMap-Eintrag uebereinstimmen
    bool              isPhysical;     // Virtueller oder physischer Treiber
    int               ifCount;        // Anzahl der Interfaces
    t_drvIFConfig    *ifConfig;       // Interface-Array
    ResetFunc         reset_ptr;      // Reset-Handler, vom Treiber-Init gesetzt
    PollFunc          poll_ptr;       // Poll-Handler, vom Treiber-Init gesetzt
    TaskFunc          task_ptr;       // Task-Handler, vom Treiber-Init gesetzt
} t_drvConfig;
Verschiebbare Basis-I/O-Ports. Damit die maschinenagnostischen Karten auf kundenspezifischen / Experimentier-Platinen verwendet werden koennen (siehe OpenZ80), lesen mehrere Treiber ihren Basis-I/O-Port aus dem iomap-Eintrag der Schnittstelle, anstatt ihn fest zu verdrahten: der dstaddr des Eintrags wird zur Basis der Karte (wobei srcaddr der authentische Port der Karte ist), und jede solche Karte fuehrt in ihrem Header eine *_DEFAULT_BASE-Konstante, die verwendet wird, wenn kein iomap-Eintrag vorhanden ist. Die verschiebbaren Karten und Standardwerte sind MZ-1R12 (0xF8/3), MZ-1R18 (0xEA/2), MZ-1R23 (0xB8/2), MZ-1R37 (0xAC/2), PIO-3034 (0x00/4), MZ-8BIO3 / MZ-1E24 (0xB0/4), MZ-1E05 (0xD8/7) und Celestite (0x60/16). Die JSON-Schluessel iomap/addrmap sind in Kleinbuchstaben (srcaddr / dstaddr) und werden als Zahlen geparst; das Feld Base I/O Port der GUI-Konfigurationsseite schreibt sie fuer Sie.
Die Felder reset_ptr, poll_ptr und task_ptr werden nicht aus JSON gesetzt — sie werden von der Init-Funktion Ihres Treibers gesetzt, damit Core 1 die Verwaltungsfunktionen Ihres Treibers zu den entsprechenden Zeitpunkten aufrufen kann.

Die Core-1-Dispatch-Schleife

Core 1 fuehrt eine enge Endlosschleife in Z80CPU_cpu() aus. Das Verstaendnis dessen, was in dieser Schleife geschieht, ist fundamental fuer das Schreiben korrekter Treiber.

Hauptschleifenstruktur

// src/Z80CPU.c — Core 1 Einstiegspunkt
void __func_in_RAM(Z80CPU_cpu)(Z80CPU *cpu)
{
    // Core 0 signalisieren, dass Core 1 laeuft
    multicore_fifo_push_blocking(1);

    while(1)
    {
        // --- HOLD-PRUEFUNG ---
        // Core 0 kann Core 1 pausieren (z.B. fuer sicheres Config-Neuladen)
        if(cpu->hold == true)
        {
            cpu->holdAck = true;
            while(cpu->hold == true);  // Spin-Wait
            cpu->holdAck = false;
        }

        // --- TREIBER-POLL ---
        // Poll-Handler jedes Treibers fuer kurze periodische Verwaltung aufrufen
        for(int idx = 0; idx < cpu->_drivers.drvCount; idx++)
        {
            if(cpu->_drivers.driver[idx].poll_ptr != NULL)
                cpu->_drivers.driver[idx].poll_ptr(cpu);
        }

        // --- Z80-AUSFUEHRUNG ---
        // Z80-Emulator fuer 2048 Taktzyklen ausfuehren
        z80_run(&cpu->_Z80, 2048);

        // --- RESET-PRUEFUNG ---
        // PIO IRQ 3 = Hardware-RESET auf dem Host-Bus aktiviert
        if(cpu->forceReset || (pio_2->irq & (1u << 3)) != 0)
        {
            Z80CPU_reset(cpu);
            CLEAR_IRQ(pio_2, 3);
        }
    }
}
Wichtige Punkte:
  • Die Schleife fuehrt z80_run() fuer 2048 Zyklen pro Iteration aus. Zwischen den Iterationen werden alle Treiber-Poll-Handler aufgerufen. Das bedeutet, Poll-Handler werden ungefaehr alle 2048 Z80-Taktzyklen aufgerufen — bei 3,5 MHz entspricht das etwa alle 585 Mikrosekunden.
  • Poll-Handler muessen extrem kurz sein. Sie befinden sich auf dem kritischen Pfad der Emulation. Ein langsamer Poll-Handler fuehrt zu Jitter im Z80-Bus-Timing.
  • Die Funktion z80_run() (aus der Zeta-Z80-Bibliothek) fuehrt Z80-Befehle aus und ruft dabei fuer jede Bus-Transaktion Z80CPU_readMem(), Z80CPU_writeMem(), Z80CPU_readIO() und Z80CPU_writeIO() als Callbacks auf.

Speicher-Lese-Dispatch

Wenn der Z80-Emulator einen Speicher-Lesevorgang durchfuehrt, wird Z80CPU_readMem() aufgerufen. Diese Funktion ist das Herzstu?ck des Speichersystems — sie zu verstehen sagt Ihnen genau, was Ihre Handler tun muessen.
// src/Z80CPU.c (vereinfacht und kommentiert)
uint8_t __func_in_RAM(Z80CPU_readMem)(Z80CPU *cpu, uint16_t addr)
{
    // Schritt 1: Den 512-Byte-Block nachschlagen, der diese Adresse enthaelt
    uint8_t  blockIdx   = addr >> 9;                        // addr / 512
    uint32_t membankptr = cpu->_membankPtr[blockIdx];       // 32-Bit kodierter Eintrag

    // Schritt 2: Die drei Felder aus dem kodierten Eintrag extrahieren
    uint8_t  memType    = (membankptr >> 24) & 0xFF;        // Oberes Byte = Typ
    uint8_t  bank       = (membankptr >> 16) & 0xFF;        // Mittleres Byte = Bank
    uint16_t blockBase  = membankptr & 0xFFFF;              // Untere 16 Bits = Basisadresse

    // Schritt 3: Offset in der PSRAM-Bank fuer diese Adresse berechnen
    uint32_t RAMaddr    = (bank * MEMORY_PAGE_SIZE) + blockBase;
    uint16_t blockOfs   = addr & (MEMORY_BLOCK_SIZE - 1);  // addr % 512 = Offset im Block

    uint8_t  waitStates = cpu->_memAttr[bank][blockIdx].waitStates;
    uint8_t  data       = 0x00;

    switch(memType)
    {
        case MEMBANK_TYPE_PHYSICAL:
        case MEMBANK_TYPE_PHYSICAL_VRAM:
        case MEMBANK_TYPE_PHYSICAL_HW:
            // Durchleitung: Host-Hardware antworten lassen
            data = Z80CPU_readPhysicalMem(cpu, addr);
            break;

        case MEMBANK_TYPE_RAM:
        case MEMBANK_TYPE_VRAM:
        case MEMBANK_TYPE_ROM:
            if(cpu->_z80PSRAM->memioPtr[addr] != NULL)
            {
                // Ein Handler ist fuer diese spezifische Adresse installiert.
                // Den aktuellen PSRAM-Wert als 'data' uebergeben, damit der Handler ihn verwenden oder aendern kann.
                data = cpu->_z80PSRAM->memioPtr[addr](
                           cpu, true, addr,
                           cpu->_z80PSRAM->RAM[RAMaddr + blockOfs]);
            }
            else
            {
                // Kein Handler — direkt aus PSRAM lesen
                data = cpu->_z80PSRAM->RAM[RAMaddr + blockOfs];
            }
            if(waitStates) Z80CPU_waitPhysicalStates(cpu, waitStates);
            break;

        case MEMBANK_TYPE_FUNC:
            // Rein virtuelles Geraet — keine PSRAM-Stuetzung
            if(cpu->_z80PSRAM->memioPtr[addr] != NULL)
                data = cpu->_z80PSRAM->memioPtr[addr](cpu, true, addr, 0);
            break;

        case MEMBANK_TYPE_PTR:
            // Indirekt — dem Pro-Byte-Zeiger folgen und rekursiv aufloesen
            data = Z80CPU_readMem(cpu, addr, cpu->_z80PSRAM->memPtr[addr]);
            break;
    }
    return data;
}

Speicher-Schreib-Dispatch

// src/Z80CPU.c (vereinfacht und kommentiert)
void __func_in_RAM(Z80CPU_writeMem)(Z80CPU *cpu, uint16_t addr, uint8_t data)
{
    uint8_t  blockIdx   = addr >> 9;
    uint32_t membankptr = cpu->_membankPtr[blockIdx];
    uint8_t  memType    = (membankptr >> 24) & 0xFF;
    uint8_t  bank       = (membankptr >> 16) & 0xFF;
    uint16_t blockBase  = membankptr & 0xFFFF;
    uint32_t RAMaddr    = (bank * MEMORY_PAGE_SIZE) + blockBase;
    uint16_t blockOfs   = addr & (MEMORY_BLOCK_SIZE - 1);
    uint8_t  waitStates = cpu->_memAttr[bank][blockIdx].waitStates;

    switch(memType)
    {
        case MEMBANK_TYPE_PHYSICAL:
        case MEMBANK_TYPE_PHYSICAL_VRAM:
        case MEMBANK_TYPE_PHYSICAL_HW:
            Z80CPU_writePhysicalMem(cpu, addr, data);
            break;

        case MEMBANK_TYPE_RAM:
        case MEMBANK_TYPE_VRAM:
            if(cpu->_z80PSRAM->memioPtr[addr] != NULL)
            {
                // Handler faengt den Schreibvorgang ab — der Rueckgabewert ersetzt 'data'
                // bevor es ins PSRAM geschrieben wird (Handler kann Daten bereinigen oder transformieren)
                data = cpu->_z80PSRAM->memioPtr[addr](cpu, false, addr, data);
            }
            // (moeglicherweise modifizierte) Daten ins PSRAM schreiben
            cpu->_z80PSRAM->RAM[RAMaddr + blockOfs] = data;
            if(waitStates) Z80CPU_waitPhysicalStates(cpu, waitStates);
            break;

        case MEMBANK_TYPE_ROM:
            // ROM: Handler wird aufgerufen falls installiert (z.B. um Banking-Schreibzugriffe auf ROM-Raum zu erkennen)
            // aber das PSRAM wird NICHT beschrieben
            if(cpu->_z80PSRAM->memioPtr[addr] != NULL)
                cpu->_z80PSRAM->memioPtr[addr](cpu, false, addr, data);
            break;

        case MEMBANK_TYPE_FUNC:
            // Rein virtuell — nur Handler, kein PSRAM
            if(cpu->_z80PSRAM->memioPtr[addr] != NULL)
                cpu->_z80PSRAM->memioPtr[addr](cpu, false, addr, data);
            break;

        case MEMBANK_TYPE_PTR:
            Z80CPU_writeMem(cpu, addr, cpu->_z80PSRAM->memPtr[addr], data);
            break;
    }
}

I/O-Port-Dispatch

// src/Z80CPU.c (vereinfacht und kommentiert)
uint8_t __func_in_RAM(Z80CPU_readIO)(Z80CPU *cpu, uint16_t addr)
{
    // addr = vollstaendige 16-Bit Z80-Adresse waehrend I/O-Befehl (A0-A15)
    // Portnummer = addr & 0xFF (A0-A7 = unteres Byte)

    if(cpu->_z80PSRAM->ioPtr[addr] != NULL)
    {
        // Virtuelle I/O: Handler aufrufen
        return cpu->_z80PSRAM->ioPtr[addr](cpu, true, addr, 0);
    }
    else
    {
        // Physische I/O: RP2350 gibt Bus frei, Host-Hardware antwortet
        return Z80CPU_readPhysicalIO(cpu, addr);
    }
}

void __func_in_RAM(Z80CPU_writeIO)(Z80CPU *cpu, uint16_t addr, uint8_t data)
{
    if(cpu->_z80PSRAM->ioPtr[addr] != NULL)
    {
        cpu->_z80PSRAM->ioPtr[addr](cpu, false, addr, data);
    }
    else
    {
        Z80CPU_writePhysicalIO(cpu, addr, data);
    }
}
Beachten Sie, dass der I/O-Dispatch einfacher ist als der Speicher-Dispatch — es gibt kein Blocktyp-Konzept fuer I/O-Ports. Jede I/O-Adresse hat entweder einen Handler in ioPtr[] oder sie geht an die physische Hardware. Es gibt keine ROM/RAM/FUNC-Unterscheidung fuer I/O.
Beachten Sie auch: Der Z80 verwendet bei I/O-Befehlen eine 16-Bit-Adresse — die unteren 8 Bits sind die Portnummer; die oberen 8 Bits enthalten den Inhalt des B-Registers (bei IN r,(C) / OUT (C),r Befehlen). Wenn Sie unterschiedliches Handler-Verhalten abhaengig vom B-Register wuenschen, untersuchen Sie das obere Byte von addr. Fuer einfachen Port-Nummern-Abgleich maskieren Sie auf addr & 0xFF.

Das Treiberframework

Das Treiberframework ist der Mechanismus, durch den C-Treibermodule entdeckt, aus der JSON-Konfiguration instanziiert und in die Speicher- und I/O-Systeme eingebunden werden. Es hat zwei Ebenen:
  • Top-Level-Treiber (auch Personas genannt) — registriert in virtualFuncMap[] in Z80CPU.c. Jede Persona richtet eine gesamte Maschinenpersoenlichkeit ein: Speicherlayout, Banking, I/O-Ports und optional eine Reihe von Sub-Interfaces (Schnittstellenkarten).
  • Interface-Treiber — registriert in der eigenen interfaceFuncMap[] der Persona. Jeder Interface-Treiber fuegt der Persona ein bestimmtes Peripheriegeraet hinzu (Floppy, QuickDisk, RAM-Erweiterung, Dateisystem).

virtualFuncMap — Top-Level-Treiberregistrierung

Das virtualFuncMap[]-Array in src/Z80CPU.c mappt einen Zeichenkettennamen auf eine Treiber-Init-Funktion. Jeder Top-Level-Treiber (Persona) muss hier einen Eintrag haben. Die Zeichenkette muss exakt mit dem "name"-Feld im JSON-"drivers"-Array uebereinstimmen (Gross-/Kleinschreibung wird nicht beachtet).
// src/Z80CPU.c

// Typdefinition fuer eine Top-Level-Treiber-Init-Funktion
typedef uint8_t (*VirtualFunc)(Z80CPU *cpu,
                               t_FlashAppConfigHeader *appConfig,
                               t_drvConfig *config,
                               const char *ifName);

// Map-Eintragsstruktur
typedef struct {
    const char  *virtualFuncName;   // Zeichenkettenname (muss mit JSON "name"-Feld uebereinstimmen)
    VirtualFunc  virtual_func_ptr;  // Aufzurufende C-Funktion
} t_VirtualFuncMap;

// DIE REGISTRIERUNGSTABELLE — fuegen Sie Ihren Treiber hier hinzu
static const t_VirtualFuncMap virtualFuncMap[] = {
#ifdef INCLUDE_SHARP_DRIVERS
    {"MZ700",   MZ700_Init},   // Sharp MZ-700 Persona
    {"MZ80A",   MZ80A_Init},   // Sharp MZ-80A Persona
    {"MZ2000",  MZ2000_Init},  // Sharp MZ-2000 Persona
    {"MZ2200",  MZ2200_Init},  // Sharp MZ-2200 Persona
    {"MZ80B",   MZ80B_Init},   // Sharp MZ-80B Persona
    {"MZ2500",  MZ2500_Init},  // Sharp MZ-2500 Persona
    {"MZ800",   MZ800_Init},   // Sharp MZ-800 Persona (Dual-Modus MZ-700/MZ-800)
    {"MZ1500",  MZ1500_Init},  // Sharp MZ-1500 Persona
#endif
#ifdef INCLUDE_AMSTRAD_DRIVERS
    {"PCW9512", PCW9512_Init}, // Amstrad PCW-9512 Persona
#endif
#ifdef INCLUDE_TATUNG_DRIVERS
    {"EinsteinTC01", EinsteinTC01_Init}, // Tatung Einstein TC-01 Persona
#endif
#ifdef INCLUDE_OPEN_DRIVERS
    {"Open",    Open_Init},   // OpenZ80 Vanilla / Experimenter-Persona
#endif
};
static const size_t virtualFuncMapSize = sizeof(virtualFuncMap) / sizeof(virtualFuncMap[0]);
Die Nachschlagefunktion, die diese Tabelle nach Namen durchsucht, ist:
// src/Z80CPU.c
VirtualFunc Z80CPU_getVirtualFunc(const char *funcName)
{
    for(size_t i = 0; i < virtualFuncMapSize; i++)
    {
        if(strncasecmp(funcName, virtualFuncMap[i].virtualFuncName,
                       strlen(virtualFuncMap[i].virtualFuncName)) == 0)
            return virtualFuncMap[i].virtual_func_ptr;
    }
    return NULL;  // Name nicht gefunden — Treiber wird uebersprungen
}

OpenZ80 — Aufbauen auf einem bestehenden Treiber oder BIOS

Das OpenZ80-Modell (src/drivers/Other/Open.c, gebaut mit build_tzpuPico.sh open) ist der Ausgangspunkt fuer zwei Arten von Experimentatoren: jemanden, der den picoZ80 in eine selbst entworfene Platine einbaut, und jemanden, der eine Z80-Maschine ohne dedizierte Persona in Betrieb nimmt. Open.c ist an MZ700.c angelehnt, aber von jeglicher Maschinenhardware befreit: im PHYSICAL-Modus leitet es den gesamten 64K-Speicher- und I/O-Raum an die reale Platine durch (Schnittstellenkarten legen ihre festen Ports darueber); im VIRTUAL-Modus praesentiert es ein flaches 64K-RAM, in das treiberseitige ROMs sequentiell ab 0x0000 geladen werden. Es ist als {"Open", Open_Init} unter #ifdef INCLUDE_OPEN_DRIVERS registriert und bietet nur die maschinenagnostischen Schnittstellenkarten (MZ-1R12/1R18/1R23/1R37, PIO-3034, MZ-8BIO3, MZ-1E24, MZ-1E05, Celestite), von denen jede auf einen beliebigen Basis-I/O-Port verschoben werden kann.
1 — Vom naechstgelegenen Treiber ausgehen
Kopieren Sie den Persona-Treiber, der Ihrem Ziel am naechsten kommt, und bearbeiten Sie dessen Speicherkarte, I/O-Handler und ROM-Layout. Die Persona-Treiber sind:
  • src/drivers/Other/Open.c — die Vanilla-Persona (beste Basis fuer eine brandneue Platine).
  • src/drivers/Sharp/MZ700.c, MZ80A.c, MZ80K.c, MZ80B.c, MZ800.c, MZ1500.c, MZ2000.c, MZ2200.c, MZ2500.c.
  • src/drivers/Amstrad/PCW9512.c — eine eigenstaendige Maschine mit integriertem uPD765 FDC.
  • src/drivers/Tatung/EinsteinTC01.c — eine eigenstaendige Maschine mit integriertem WD1770 FDC.
Registrieren Sie die neue Persona, indem Sie eine {"MyMachine", MyMachine_Init}-Zeile zum obigen virtualFuncMap[] hinzufuegen (unter einem passenden #ifdef), und binden Sie ihren Quellcode in src/CMakeLists.txt ein (z.B. fuegen Sie ihn der Liste pZ80_drivers_open_src hinzu) plus ein Modell-Target ueber add_z80_model_targets(...).
2 — Die ROMs der Maschine wiederverwenden oder patchen
Der Monitor, das IPL, das CP/M-BIOS und die Floppy-Boot-ROMs, die die Treiber laden, werden als kommentierter Z80-Assembler-Quellcode in den begleitenden RFS- und TZFS-Projekten unter deren asm/-Verzeichnissen gepflegt (assembliert mit dem GLASS-Z80-Assembler). Nehmen Sie diese als Basis und bauen oder patchen Sie sie fuer Ihre Maschine: </font>
Zweck Quelldateien
Monitor-ROMs RFS/asm/sp1002.asm (MZ-80K SP-1002), RFS/asm/sa1510.asm (MZ-80A SA-1510), RFS/asm/1z-013a.asm & TZFS/asm/1z-013a.asm (MZ-700), RFS/asm/mz800_iocs.asm (MZ-800 IOCS)
Bootloader (IPL) TZFS/asm/mz2000_ipl.asm (MZ-2000), TZFS/asm/mz80b_ipl.asm (MZ-80B), RFS/asm/ipl.asm
CP/M-BIOS RFS/asm/cbios.asm, RFS/asm/cpm22-bios.asm, TZFS/asm/cbios.asm, TZFS/asm/cbiosII.asm
Floppy- / QuickDisk-Boot-ROMs RFS/asm/mz80afi.asm (MZ-80A FDC), TZFS/asm/mz80kfdif.asm (MZ-80K FDIF), RFS/asm/mz-1e05.asm, RFS/asm/mz-1e14.asm (QD), RFS/asm/sfd700.asm
In-ROM-Dateisysteme RFS/asm/rfs.asm (gebankt), TZFS/asm/tzfs.asm (gebankt)
Assemblieren Sie das benoetigte ROM, legen Sie das resultierende Binary auf die SD-Karte und referenzieren Sie es aus einem rom[].loadaddr-Eintrag (fuer eine Schnittstellenkarte) oder, bei OpenZ80 im virtuellen Modus, als treiberseitiges ROM, das ab 0x0000 geladen wird. Sowohl RFS als auch TZFS liefern eigenstaendige Build-Skripte (siehe deren Entwicklerhandbuecher), die diese ROM-Images erzeugen.

TZFS — Speichermodi und der virtuelle Service-Prozessor

src/drivers/Sharp/TZFS.c ist ein gutes durchgearbeitetes Beispiel fuer einen Treiber, der bankgeschaltete Speichermodus-Emulation mit einem Intercore-Dienstmodell kombiniert. Er implementiert einen Multi-Bank-Low-Level-Monitor und ein Dateisystem (eine Erweiterung von MONITOR 1Z-013A) mit CP/M darunter, modelliert nach dem TZFS der tranZPUter SW und ihrem virtuellen K64F-I/O-Prozessor. Er ist als auswaehlbare Schnittstelle auf der MZ-700-Persona registriert (in MZ700.c, neben — und in der Praxis exklusiv mit — RFS), nicht als uebergeordnete Persona.
Speichermodus-Umschaltung (Port 0x60)
Der Z80 waehlt ein tranZPUter-Speicherlayout, indem er einen Moduswert an den I/O-Port 0x60 schreibt; der Treiber richtet daraufhin seine Bank-Zeiger neu aus. Die Modi sind TZMM_ORIG, TZMM_BOOT, TZMM_TZFS, TZMM_TZFS2, TZMM_TZFS3, TZMM_TZFS4, TZMM_CPM, TZMM_CPM2 und TZMM_COMPAT. Das CP/M-CPM2-Layout verwendet byte-granulares Block-0-Paging (0x00000x003F → Vektoren-Block, 0x00400x01FF → Anfang der TPA).
Virtueller K64F-Service-Prozessor (OUT 0x68 → Core 0)
Anstatt Dateisystem-Aufrufe auf dem Z80-Kern zu bedienen, verwendet TZFS einen virtuellen Service-Prozessor. Der Z80 fuehrt OUT (0x68) aus, was eine MSG_TZFS_SVCREQ-Nachricht an Core 0 einreiht; Core 0 verteilt sie ueber TZFS_processServiceRequest. Die Dateisystem-Dienste sind READDIR / NEXTDIR (gecachte 16-Eintraege-Verzeichnisbloecke), READFILE / NEXTREADFILE, LOADFILE (MZF-Header- und Bank-Objekt-bewusst), CHANGEDIR und CLOSE. Die CP/M-Dienste sind LOADBDOS (Warm-Boot-CCP + BDOS-Neuladung), ADDSDDRIVE, READSDDRIVE und WRITESDDRIVE sowie CPU-Frequenz-Dienste. Da der picoZ80 keinen direkten SD-Zugriff hat, werden CP/M-512-Byte-Sektoren ueber den ESP32 (ESP_readSector / ESP_writeSector) gegen ganze Image-Dateien geleitet; die Image-Pfade pro Laufwerk stammen aus den JSON-Eintraegen param[].file der Schnittstelle, mit Fallback-Vorlage CPM/SDC16M/RAW/CPMDSK<nn>.RAW. Das TZFS-ROM (roms/tzfs.bin) ist die assemblierte Ausgabe von TZFS/asm/tzfs.asm des Begleitprojekts (sein CP/M-BIOS aus TZFS/asm/cbios.asm / cpm22.asm).

Initialisierungsablauf

Nachdem der RP2350 config.json gelesen und geparst hat, erfolgt die Treiberinitialisierung in folgender Reihenfolge:
Z80CPU_configFromJSON()
    ├── Z80CPU_configDriversFromJSON()   "drivers"-Array aus JSON parsen
    │       Fuer jeden Treibereintrag:
    │           ├── Name in virtualFuncMap[] nachschlagen
    │           ├── Interface-Konfigurationen parsen (ROM, addrmap, iomap, param)
    │           └── VirtualFunc(cpu, appConfig, &drvConfig, NULL) aufrufen
    │                   ↓
    │               Treiber-Init richtet ein:
    │                   ├── _membankPtr[] Eintraege (Blocktypen und Baenke)
    │                   ├── _memAttr[][] Eintraege (Wait-States, Sync)
    │                   ├── memioPtr[] Handler (Speicheradress-Hooks)
    │                   ├── ioPtr[] Handler (I/O-Port-Hooks)
    │                   ├── config->reset_ptr = MyDriver_Reset
    │                   ├── config->poll_ptr  = MyDriver_PollCB
    │                   └── config->task_ptr  = MyDriver_TaskProcessor
    │
    ├── Z80CPU_configMemoryFromJSON()    "memory"-Array anwenden (ueberschreibt Treiber-Defaults)
    └── Z80CPU_configIOFromJSON()        "io"-Array anwenden (ueberschreibt Treiber-Defaults)
Beachten Sie die Reihenfolge: Treiber werden zuerst initialisiert, dann werden die JSON-"memory"- und "io"-Arrays angewendet. Das bedeutet, dass alle expliziten Eintraege in den memory- oder io-Arrays in config.json das ueberschreiben, was der Treiber fuer diese Adressen eingerichtet hat. Dies ermoeglicht es dem Benutzer, Treiber-Standardwerte fein abzustimmen, ohne den Treiber-Quellcode zu aendern.

Treiber-Lebenszyklus-Callbacks

Ein Treiber registriert drei laufende Callbacks, indem er waehrend der Initialisierung Funktionszeiger in seiner t_drvConfig-Struktur speichert. Core 1 ruft diese zu bestimmten Zeitpunkten waehrend der Ausfuehrung auf:
Callback Signatur Wann aufgerufen Typische Verwendung
reset_ptr uint8_t f(Z80CPU *cpu) Host-RESET-Leitung aktiviert; Z80 PC = 0x0000 Standard-Speicherkarte wiederherstellen, Bank-Zustand loeschen
poll_ptr uint8_t f(Z80CPU *cpu) Alle ~2048 Z80-Zyklen (auf Core 1) Status-Flags pruefen, Anforderungen an Core 0 senden
task_ptr uint8_t f(Z80CPU *cpu, enum Z80CPU_TASK_NAME task, char *param) Als Antwort auf Intercore-Task-Anforderungen Datei-I/O-Ergebnisse verarbeiten, Festplattensektoren ausliefern
Wichtig: poll_ptr wird von Core 1 aufgerufen und muss schnell sein. Wenn sie I/O ausfuehren muss (einen Festplattensektor laden, einen UART-Befehl senden), sollte sie eine Nachricht ueber cpu->requestQueue an Core 0 senden und sofort zurueckkehren. Core 0 fuehrt die I/O aus und liefert das Ergebnis ueber cpu->responseQueue zurueck, wodurch task_ptr bei der naechsten verfuegbaren Gelegenheit ausgeloest wird.

Praxisbeispiel: Der MZ-700-Treiber

Der Sharp MZ-700-Treiber (src/drivers/Sharp/MZ700.c) ist der vollstaendigste Persona-Treiber in der Codebasis. Ihn im Detail durchzugehen zeigt jedes Muster, das Sie fuer Ihre eigenen Treiber benoetigen. Der MZ-80A-Treiber (src/drivers/Sharp/MZ80A.c) bietet eine weitere Referenzimplementierung, die die Intel-8253-PIT-Emulation und den MEMSW-Speicher-Swap-Mechanismus demonstriert. Der MZ-2000-Treiber (src/drivers/Sharp/MZ2000.c) ist eine weitere Referenz, die BST/NST-Speichermodus-Umschaltung, Zeichen- und Grafik-VRAM-Overlay mit Z80-PIO-gesteuerter Bankauswahl und MB8866-FDC-Integration zeigt. Er unterstuetzt sowohl den physischen Modus (Drop-in-Z80-Ersatz in einem echten MZ-2000 mit automatischer Boot/Normal-Modus-Erkennung) als auch den virtuellen Modus (vollstaendig PSRAM-basierte Emulation mit IPL-ROM-Spiegelung). Der MZ-800-Treiber (src/drivers/Sharp/MZ800.c) ist eine Dual-Modus-Persona, die das GDG-Display-Modus-Register verfolgt und seine Speicherkarten-Dekodierung zur Laufzeit neu aufbaut, und demonstriert das Bruecken der Interrupt-Daisy-Chain im virtuellen Modus (siehe Der MZ-800-Treiber unten).
Mehrere wiederverwendbare Peripherie-Emulationsmodule stehen zur Verfuegung: PIT8253.c (Intel 8253 Programmable Interval Timer — alle sechs Zaehler-Modi, BCD/Binaer-Zaehlung, Counter-Latch, LSB/MSB-Lese/Lade-Modi), PPI8255.c (Intel 8255 Programmable Peripheral Interface — Modus-0-I/O, Bit-Set/Reset fuer Port C, pro-Port-Ausgangs-Callbacks und Eingangs-Injektion), WD1773.c (WD1773 Floppy-Disk-Controller — verwendet von den Sharp-MZ-Persona-Treibern), WD1770.c (WD1770 Floppy-Disk-Controller — verwendet von der Tatung-Einstein-Persona, unterstuetzt Extended CPC DSK-, D88- und Standard-DSK-Formate) und uPD765.c (NEC uPD765 Floppy-Disk-Controller — verwendet von der Amstrad PCW-9512-Persona, unterstuetzt CPC-DSK-Format und physisches Disk-Imaging). Diese Module sind so konzipiert, dass sie von jedem Maschinen-Persona-Treiber instanziiert werden koennen.
#### Der MZ-800-Treiber (Dual-Modus-Persona)
Der MZ-800-Treiber (src/drivers/Sharp/MZ800.c) ist ein gutes Beispiel fuer eine Persona, deren Speicherkarte nicht fest ist, sondern sich zur Laufzeit unter Kontrolle der Host-Software aendert. Der MZ-800 ist eine Obermenge des MZ-700: Er bootet in einem MZ-700-kompatiblen Modus und kann in einen nativen MZ-800-Modus mit einem anderen Speicherlayout und einer anderen Grafikbreite umschalten. Der Treiber verfolgt den Maschinenmodus und baut seine Block-Dekodierungstabelle jedes Mal neu auf, wenn sich der Modus aendert.
GDG-Display-Modus-Register (Port 0xCE). Der Treiber installiert einen ioPtr[]-Handler auf Port 0xCE, der Schreibvorgaenge auf das GDG-Display-Modus-Register (DMD) abhoert:
  • Bit 3 waehlt den Maschinenmodus — geloescht = natives MZ-800, gesetzt = MZ-700-Kompatibilitaet.
  • Bit 2 waehlt die Grafikbreite — 320- vs. 640-Pixel.
Wenn ein Schreibvorgang eines der beiden Bits aendert, ruft der Treiber MZ800_applyMemoryMap() auf, das die 128 512-Byte-Bloecke durchlaeuft und die _membankPtr[]-Eintraege zur Laufzeit neu schreibt, um das neue Layout abzubilden. Dies ist derselbe Fast-Dispatch-Mechanismus, der in membankPtr-Kodierung beschrieben wird — es ist kein Bus-Stall erforderlich, da nur die Dekodierungstabelle neu aufgebaut wird, nicht das zugrunde liegende PSRAM.
Speicherkarten-Steuerflags. Das aktuelle Layout wird in einer kleinen Zustandsstruktur als Bitmaske von Flags gehalten: ROM_0000 (Monitor-ROM sichtbar bei 0x0000), ROM_1000 (CG-ROM-Fenster bei 0x1000), CGRAM_VRAM (Character-Generator-RAM/VRAM im 0x1000-Fenster eingeblendet) und ROM_E000 (IOCS-ROM- / Hardware-Bereich bei 0xE000). MZ800_applyMemoryMap() leitet die Blocktypen aus diesen Flags zusammen mit den aktuellen MZ-700/MZ-800- und 320/640-Modus-Bits ab. Die Einschalt-/Reset-Karte legt das Monitor-ROM, CG-ROM und IOCS-ROM offen. Die MZ-800-Memory-Banking-Ports (0xE0–0xE6) werden vom Treiber verarbeitet und auf die physische Hardware gespiegelt, sodass eine echte nachgeschaltete Maschine synchron bleibt.
Bruecken der Interrupt-Daisy-Chain (virtueller Modus). Dies ist der subtilste Teil des Treibers und folgt demselben Muster wie der MZ-2500-Treiber. Im nativen MZ-800-Modus wird der periodische Interrupt vom echten 8253 erzeugt und durch den physischen Z80-PIO gespeist, der in der Interrupt-Daisy-Chain sitzt. Der In-Service-Latch des PIO wird nur geloescht, wenn er einen RETI-Opcode-Fetch (ED 4D) auf dem realen Bus beobachtet — aber im virtuellen Modus werden die Interrupt-Service-Routine und ihr RETI aus dem PSRAM heraus ausgefuehrt, sodass der PIO sie nie sieht und gelatcht bliebe, wodurch alle nachfolgenden Interrupts blockiert wuerden. Der Treiber brueckt dies durch die Installation von zwei Hooks in den Zeta-Kern:
  • MZ800_readIntAck() — installiert als cpu->_Z80.inta, der Interrupt-Acknowledge-Callback.
  • MZ800_retiHandler() — installiert als cpu->_Z80.reti, der RETI-Callback.
Beide rufen mz800PhysicalReti() auf, das ein physisches RETI auf dem realen Bus abspielt: Es platziert die zwei Bytes ED 4D bei SP-2 im realen Host-RAM, fuehrt M1-Opcode-Fetches beider Bytes auf dem physischen Bus durch (sodass die Daisy-Chain-Logik des PIO das RETI beobachtet und seinen In-Service-Latch loescht) und stellt dann die urspruenglichen Bytes wieder her, die es verdraengt hatte. Diese Hooks werden nur installiert, wenn die Schnittstelle virtuell laeuft (!isPhysical); im physischen Modus sieht der echte PIO das echte RETI direkt und es ist keine Brueckung erforderlich.
Reset. MZ800_Reset() stellt die Einschalt-Speicherkarte wieder her, gibt ein OUT 0xE4 aus, um den Gate-Array-Reset nachzuahmen, loescht den RFS/MZF-Arbeitsbereich (0x1000–0x1168) und — da der 8253 PIT keinen Reset-Pin hat — programmiert und maskiert den 8253 im MZ-700-Modus neu, sodass ein veralteter Zaehler nach dem Reset keinen fehlerhaften Interrupt ausloesen kann.
#### Zurueck zum MZ-700-Beispiel
Der MZ-700 ist ein Sharp 8-Bit-Computer von 1982, basierend auf dem Z80A. Seine Speicherkarte hat einige besondere Eigenschaften, die ihn zu einem idealen Lernbeispiel machen:
  • Die unteren 4KB (0x0000–0x0FFF) sind beim Einschalten ein Monitor-ROM, koennen aber ueber I/O-Port-Schreibzugriffe gegen RAM getauscht werden — das sogenannte "MZ-700-Bank-Switching".
  • Der obere Bereich (0xD000–0xFFFF) enthaelt Video-RAM, Farb-VRAM und speichergemappte Hardwareregister. Der gesamte obere Bereich kann ebenfalls ueber I/O-Ports gegen RAM getauscht werden.
  • Das Speicher-Banking wird ueber sechs I/O-Ports (0xE0–0xE6) gesteuert, die Bloecke ein- und ausschalten.

Interface-Funktions-Map

Am Anfang von MZ700.c listet eine interfaceFuncMap[]-Tabelle alle Peripherie-Schnittstellenkarten (Sub-Treiber) auf, die die MZ-700-Persona kennt. Dies ist das MZ-700-Aequivalent zu virtualFuncMap[] — es mappt Interface-Namen (aus dem JSON-"if"-Array) auf Init-Funktionen fuer jede Erweiterungskarte.
// src/drivers/Sharp/MZ700.c

typedef struct {
    const char  *interfaceFuncName;  // Interface-Namenszeichenkette (stimmt mit JSON "if.name" ueberein)
    bool         active;             // true sobald dieses Interface initialisiert wurde
    InitFunc     init_func_ptr;      // Wird waehrend Treiber-Init aufgerufen wenn dieses Interface im JSON erscheint
    ResetFunc    reset_func_ptr;     // Wird bei RESET aufgerufen
    PollFunc     poll_func_ptr;      // Wird alle ~2048 Z80-Zyklen aufgerufen
    TaskFunc     task_func_ptr;      // Wird fuer Intercore-Task-Zustellung aufgerufen
} t_InterfaceFuncMap;

static t_InterfaceFuncMap interfaceFuncMap[] = {
    {"RFS",     false, RFS_Init,    RFS_Reset,    RFS_PollCB,    RFS_TaskProcessor},
    {"MZ-1E05", false, MZ1E05_Init, MZ1E05_Reset, MZ1E05_PollCB, MZ1E05_TaskProcessor},
    {"MZ-8BFI", false, MZ8BFI_Init, MZ8BFI_Reset, MZ8BFI_PollCB, MZ8BFI_TaskProcessor},
    {"MZ-1E14", false, MZ1E14_Init, MZ1E14_Reset, MZ1E14_PollCB, MZ1E14_TaskProcessor},
    {"MZ-1E19", false, MZ1E19_Init, MZ1E19_Reset, MZ1E19_PollCB, MZ1E19_TaskProcessor},
    {"MZ-1R12", false, MZ1R12_Init, MZ1R12_Reset, MZ1R12_PollCB, MZ1R12_TaskProcessor},
    {"MZ-1R18", false, MZ1R18_Init, MZ1R18_Reset, MZ1R18_PollCB, MZ1R18_TaskProcessor},
};
static const size_t interfaceFuncMapSize =
    sizeof(interfaceFuncMap) / sizeof(interfaceFuncMap[0]);
Hinweis: Das obige Beispiel zeigt die Interface-Liste der MZ-700-Persona. Jede Persona hat ihre eigene interfaceFuncMap[] mit einer anderen Teilmenge verfuegbarer Interfaces. Die vollstaendige Liste der Interface-Treiber, ihre JSON-"name"-Zeichenketten fuer das "if"-Array und welche Personas sie unterstuetzen, ist in der Tabelle Persona–Interface-Kompatibilitaet im Technischen Leitfaden dokumentiert. Fuer JSON-Konfigurationsbeispiele fuer Endbenutzer siehe das picoZ80 Benutzerhandbuch.

Banking-Zustandsstruktur

Da der Banking-Zustand ueber Bus-Transaktionen hinweg bestehen bleiben muss (ein Schreibzugriff auf 0xE0 muss gespeichert werden, damit nachfolgende Speicherzugriffe in die richtige Bank gehen), haelt der Treiber eine statische Zustandsstruktur:
// src/drivers/Sharp/MZ700.c (gekuerzt)
typedef struct {
    bool      loDRAMen;     // true = untere 4KB (0x0000-0x0FFF) auf RAM-Bank 1 gemappt
    bool      hiDRAMen;     // true = oberer Bereich (0xD000-0xFFFF) auf RAM-Bank 1 gemappt
    bool      inhibit;      // true = oberer Bank-Swap ist gesperrt (INHIBIT-Port beschrieben)

    // Gespeicherte membankPtr-Werte fuer den oberen Bereich wenn hi DRAM eingetauscht ist
    // (benoetigt um das Original-Mapping wiederherzustellen wenn hi DRAM zurueckgetauscht wird)
    uint32_t  upmembankPtr[MZ700_UPPERMEM_BLOCKS];
} t_MZ700Ctrl;

static t_MZ700Ctrl MZ700Ctrl = {
    .loDRAMen = false,
    .hiDRAMen = false,
    .inhibit  = false,
};
Statische Variablen wie diese sind sicher, weil es nur eine Z80CPU-Instanz gibt und Core 1 der einzige Thread ist, der die Handler-Funktionen aufruft. Sollten Sie jemals mehrere CPU-Instanzen haben (nicht das aktuelle Design), wuerden Sie diesen Zustand in t_drvConfig verschieben.

Die Init-Funktion — MZ700_Init()

MZ700_Init() dient einem doppelten Zweck. Sie wird in zwei verschiedenen Kontexten aufgerufen, identifiziert dadurch, welche Argumente NULL sind:
  • Validierungsmodus (ifName != NULL, config == NULL): Wird von Z80CPU_configDriversFromJSON() aufgerufen um zu fragen "Unterstuetzt du diesen Interface-Namen?" Gibt 1 zurueck wenn ja, 0 wenn nein. Dies ermoeglicht dem JSON-Parser, Interface-Namen gegen den Treiber zu validieren, bevor versucht wird, sie zu initialisieren.
  • Konfigurationsmodus (ifName == NULL, config != NULL): Die eigentliche Initialisierung — Speicherkarte einrichten, Hooks installieren, Interfaces konfigurieren.
// src/drivers/Sharp/MZ700.c
uint8_t MZ700_Init(Z80CPU *cpu, t_FlashAppConfigHeader *appConfig,
                   t_drvConfig *config, const char *ifName)
{
    // --- VALIDIERUNGSMODUS ---
    if(ifName != NULL && config == NULL)
    {
        // Pruefen ob ifName in unserer interfaceFuncMap ist
        for(size_t i = 0; i < interfaceFuncMapSize; i++)
        {
            if(strncasecmp(ifName, interfaceFuncMap[i].interfaceFuncName,
                           strlen(interfaceFuncMap[i].interfaceFuncName)) == 0)
                return 1;  // Ja, wir unterstuetzen dieses Interface
        }
        return 0;  // Unbekanntes Interface
    }

    // --- KONFIGURATIONSMODUS ---
    if(ifName == NULL && config != NULL)
    {
        // Bestimmen ob dies ein physischer (Durchleitungs-) oder virtueller (emulierter) Treiber ist
        bool isPhysical = config->isPhysical;

        // ------------------------------------------------------------------
        // SCHRITT 1: Flache Speicherkarte einrichten (membankPtr[] + memAttr[][])
        // ------------------------------------------------------------------
        // Alle 128 Bloecke durchlaufen und jedem einen Typ/Bank zuweisen
        for(int idx = 0; idx < MEMORY_PAGE_BLOCKS; idx++)
        {
            uint32_t memType   = MEMBANK_TYPE_PHYSICAL;  // Standard: Durchleitung
            uint8_t  bank      = MZ700_MEMBANK_0;
            uint8_t  waitStates = 0;
            bool     tCycSync  = false;

            // Bloecke 0-7 = 0x0000-0x0FFF (Monitor-ROM / untere 4KB)
            // Wenn nicht physisch, werden diese vom JSON "memory"-Array ueberschrieben.
            // Hier als PHYSICAL belassen damit JSON ROM oder RAM nach Bedarf zuweisen kann.

            // Bloecke 8-103 = 0x1000-0xCFFF (Haupt-RAM)
            if(idx >= 8 && idx <= 103)
            {
                if(!isPhysical)
                {
                    memType    = MEMBANK_TYPE_RAM;
                    waitStates = 1;
                    tCycSync   = true;
                }
            }

            // Bloecke 104-111 = 0xD000-0xDFFF (VRAM + Farb-VRAM)
            if(idx >= 104 && idx <= 111)
                memType = MEMBANK_TYPE_PHYSICAL_VRAM;

            // Bloecke 112-119 = 0xE000-0xE7FF (Hardwareregister: PPI, Timer, etc.)
            if(idx >= 112 && idx <= 119)
                memType = MEMBANK_TYPE_PHYSICAL_HW;

            // Bloecke 120-127 = 0xF000-0xFFFF (FDC-Adressraum)
            // Als PHYSICAL belassen damit physische FDC-Hardware antworten kann falls installiert

            // In membankPtr-Eintrag packen
            cpu->_membankPtr[idx] = (memType    << 24)
                                  | (bank       << 16)
                                  | (idx * MEMORY_BLOCK_SIZE);

            cpu->_memAttr[bank][idx].waitStates = waitStates;
            cpu->_memAttr[bank][idx].tCycSync   = tCycSync;
        }

        // ------------------------------------------------------------------
        // SCHRITT 2: memioPtr- und ioPtr-Tabellen fuer unseren Adressbereich loeschen
        // (sicherstellen dass keine veralteten Handler-Zeiger aus einer vorherigen Konfiguration vorhanden sind)
        // ------------------------------------------------------------------
        for(int idx = 0; idx < MEMORY_PAGE_SIZE; idx++)
            cpu->_z80PSRAM->memioPtr[idx] = NULL;

        for(int idx = 0; idx < IO_PAGE_SIZE; idx++)
            cpu->_z80PSRAM->ioPtr[idx] = NULL;

        // ------------------------------------------------------------------
        // SCHRITT 3: I/O-Port-Handler fuer Banking-Steuerports installieren
        // ------------------------------------------------------------------
        if(!isPhysical)
        {
            // MZ-700 Speicher-Banking-Ports 0xE0-0xE6
            for(int port = 0xE0; port <= 0xE6; port++)
                cpu->_z80PSRAM->ioPtr[port] = (MemoryFunc)MZ700_IO_MemoryBankPorts;
        }

        // ------------------------------------------------------------------
        // SCHRITT 4: Lebenszyklus-Callbacks registrieren
        // ------------------------------------------------------------------
        config->reset_ptr = (ResetFunc)MZ700_Reset;
        config->poll_ptr  = (PollFunc)MZ700_PollCB;
        config->task_ptr  = (TaskFunc)MZ700_TaskProcessor;

        // ------------------------------------------------------------------
        // SCHRITT 5: Sub-Interfaces initialisieren, die im JSON "if"-Array gelistet sind
        // ------------------------------------------------------------------
        for(int ifNo = 0; ifNo < config->ifCount; ifNo++)
        {
            t_drvIFConfig *ifcfg = &config->ifConfig[ifNo];

            // Das Interface in unserer Map finden
            for(size_t i = 0; i < interfaceFuncMapSize; i++)
            {
                if(strncasecmp(ifcfg->name, interfaceFuncMap[i].interfaceFuncName,
                               strlen(interfaceFuncMap[i].interfaceFuncName)) == 0)
                {
                    // Sub-Treiber-Init-Funktion aufrufen
                    interfaceFuncMap[i].init_func_ptr(cpu, appConfig, ifcfg);
                    interfaceFuncMap[i].active = true;
                    break;
                }
            }
        }
    }
    return 1;
}

Der Banking-Handler — MZ700_IO_MemoryBankPorts()

Dies ist der I/O-Handler, der alle sechs MZ-700-Banking-Steuerports verwaltet. Er wird von Z80CPU_writeIO() jedes Mal aufgerufen, wenn der Z80 auf die Ports 0xE0–0xE6 schreibt. Das Lesen von diesen Ports hat keinen Seiteneffekt (gibt 0xFF zurueck). Schreibvorgaenge aendern die Speicherkarte durch Modifikation von _membankPtr[]-Eintraegen in Echtzeit.
// src/drivers/Sharp/MZ700.c
uint8_t MZ700_IO_MemoryBankPorts(Z80CPU *cpu, bool read, uint16_t addr, uint8_t data)
{
    uint8_t port = (uint8_t)(addr & 0xFF);  // Portnummer aus Z80-Adresse extrahieren

    // Lesezugriffe haben keinen Seiteneffekt
    if(read) return 0xFF;

    // --- PORT 0xE0: Untere 4KB DRAM aktivieren ---
    // Tauscht Monitor-ROM (0x0000-0x0FFF) gegen RAM in Bank 1
    if(port == 0xE0 && !MZ700Ctrl.loDRAMen)
    {
        for(int idx = 0; idx < (0x1000 / MEMORY_BLOCK_SIZE); idx++)
        {
            cpu->_membankPtr[idx] = (MEMBANK_TYPE_RAM << 24)
                                  | (MZ700_MEMBANK_1  << 16)
                                  | (idx * MEMORY_BLOCK_SIZE);
        }
        MZ700Ctrl.loDRAMen = true;
    }

    // --- PORT 0xE1: Oberes DRAM aktivieren (0xD000-0xFFFF) ---
    // Speichert die aktuellen oberen membankPtr-Eintraege und ersetzt sie durch RAM in Bank 1
    if(port == 0xE1 && !MZ700Ctrl.inhibit && !MZ700Ctrl.hiDRAMen)
    {
        int startBlock = 0xD000 / MEMORY_BLOCK_SIZE;
        int endBlock   = 0x10000 / MEMORY_BLOCK_SIZE;

        for(int idx = startBlock; idx < endBlock; idx++)
        {
            // Aktuelles Mapping speichern damit wir es spaeter wiederherstellen koennen (Port 0xE3)
            MZ700Ctrl.upmembankPtr[idx - startBlock] = cpu->_membankPtr[idx];

            // Durch RAM ersetzen
            cpu->_membankPtr[idx] = (MEMBANK_TYPE_RAM << 24)
                                  | (MZ700_MEMBANK_1  << 16)
                                  | (idx * MEMORY_BLOCK_SIZE);
        }
        MZ700Ctrl.hiDRAMen = true;
    }

    // --- PORT 0xE2: Unteres ROM wiederherstellen ---
    // Tauscht Bank-1-RAM zurueck und stellt Monitor-ROM bei 0x0000-0x0FFF wieder her
    if(port == 0xE2 && MZ700Ctrl.loDRAMen)
    {
        for(int idx = 0; idx < (0x1000 / MEMORY_BLOCK_SIZE); idx++)
        {
            // Auf ROM in Bank 0 zuruecksetzen
            cpu->_membankPtr[idx] = (MEMBANK_TYPE_ROM << 24)
                                  | (MZ700_MEMBANK_0  << 16)
                                  | (idx * MEMORY_BLOCK_SIZE);
        }
        MZ700Ctrl.loDRAMen = false;
    }

    // --- PORT 0xE3: Oberes Hardware-Mapping wiederherstellen ---
    if(port == 0xE3 && MZ700Ctrl.hiDRAMen)
    {
        int startBlock = 0xD000 / MEMORY_BLOCK_SIZE;
        int endBlock   = 0x10000 / MEMORY_BLOCK_SIZE;

        for(int idx = startBlock; idx < endBlock; idx++)
        {
            // Gespeichertes Mapping wiederherstellen
            cpu->_membankPtr[idx] = MZ700Ctrl.upmembankPtr[idx - startBlock];
        }
        MZ700Ctrl.hiDRAMen = false;
    }

    // --- PORT 0xE4: Oberen DRAM-Swap sperren ---
    if(port == 0xE4)
        MZ700Ctrl.inhibit = true;

    // --- PORT 0xE5: Obere Sperre aufheben (Alias) ---
    if(port == 0xE5)
        MZ700Ctrl.inhibit = false;

    // PORT 0xE6: Speicherschutz (in diesem Beispiel nicht implementiert)

    return 0;  // Rueckgabewert wird fuer Schreib-I/O-Handler ignoriert
}
Dieser Handler demonstriert das wichtigste Muster beim Schreiben von Treibern: Modifikation von _membankPtr[] als Reaktion auf einen I/O-Schreibzugriff zur Implementierung von Speicher-Banking. Die Aenderungen treten sofort in Kraft — der naechste Speicherzugriff des Z80 wird das neue Mapping verwenden.
Das Speichern/Wiederherstellen-Muster fuer den oberen Speicherbereich (MZ700Ctrl.upmembankPtr[]) ist wichtig: Wenn Sie hardware-gemappte Bereiche gegen RAM tauschen, muessen Sie sich merken, was dort war, damit Sie es wiederherstellen koennen, wenn die Software zuruecktauscht. Einfach wieder PHYSICAL zuzuweisen wuerde alle benutzerdefinierten Mappings verlieren, die von Sub-Treibern oder der JSON-Konfiguration eingerichtet wurden.

Der Reset-Handler — MZ700_Reset()

Wenn der Host RESET aktiviert, ruft Core 1 den reset_ptr jedes Treibers auf. Der MZ-700-Reset-Handler stellt die Speicherkarte auf den Einschaltzustand zurueck (ROM bei 0x0000, VRAM und Hardware bei 0xD000+) und ruft dann alle aktiven Interface-Reset-Handler auf:
// src/drivers/Sharp/MZ700.c
uint8_t MZ700_Reset(Z80CPU *cpu)
{
    // Untere 4KB auf ROM zuruecksetzen falls sie auf DRAM getauscht waren
    if(MZ700Ctrl.loDRAMen)
    {
        for(int idx = 0; idx < (0x1000 / MEMORY_BLOCK_SIZE); idx++)
        {
            cpu->_membankPtr[idx] = (MEMBANK_TYPE_ROM << 24)
                                  | (MZ700_MEMBANK_0  << 16)
                                  | (idx * MEMORY_BLOCK_SIZE);
        }
        MZ700Ctrl.loDRAMen = false;
    }

    // Oberen Bereich wiederherstellen falls er auf DRAM getauscht war
    if(MZ700Ctrl.hiDRAMen)
    {
        int startBlock = 0xD000 / MEMORY_BLOCK_SIZE;
        int endBlock   = 0x10000 / MEMORY_BLOCK_SIZE;
        for(int idx = startBlock; idx < endBlock; idx++)
            cpu->_membankPtr[idx] = MZ700Ctrl.upmembankPtr[idx - startBlock];
        MZ700Ctrl.hiDRAMen = false;
    }
    MZ700Ctrl.inhibit = false;

    // Reset an alle aktiven Sub-Interfaces weitergeben
    for(size_t i = 0; i < interfaceFuncMapSize; i++)
    {
        if(interfaceFuncMap[i].active)
            interfaceFuncMap[i].reset_func_ptr(cpu);
    }
    return 0;
}

Der Poll-Handler — MZ700_PollCB()

// src/drivers/Sharp/MZ700.c
uint8_t MZ700_PollCB(Z80CPU *cpu)
{
    // Die MZ-700-Persona selbst hat in der Poll-Schleife nichts zu tun —
    // alles Polling wird an aktive Sub-Interfaces delegiert
    for(size_t i = 0; i < interfaceFuncMapSize; i++)
    {
        if(interfaceFuncMap[i].active)
            interfaceFuncMap[i].poll_func_ptr(cpu);
    }
    return 0;
}

Der Task-Prozessor — MZ700_TaskProcessor()

// src/drivers/Sharp/MZ700.c
uint8_t MZ700_TaskProcessor(Z80CPU *cpu, enum Z80CPU_TASK_NAME task, char *param)
{
    // Eingehende Task-Ergebnisse an das Sub-Interface weiterleiten, das sie angefordert hat
    for(size_t i = 0; i < interfaceFuncMapSize; i++)
    {
        if(interfaceFuncMap[i].active)
            interfaceFuncMap[i].task_func_ptr(cpu, task, param);
    }
    return 0;
}
Das Task-Prozessor-Muster ist ueber alle Treiber hinweg konsistent: Die Persona verteilt den Task einfach an alle aktiven Sub-Interfaces. Jedes Sub-Interface prueft, ob der Task fuer es relevant ist, und ignoriert ihn andernfalls.

Einen neuen Treiber schreiben — Schritt fuer Schritt

Dieser Abschnitt fuehrt durch jeden Schritt, der erforderlich ist, um einen vollstaendigen Treiber von Grund auf zu erstellen. Das Beispiel erstellt eine einfache RAM-Disk (einen 64KB-Block PSRAM, auf den der Z80 als I/O-gemappter Speicher zugreift), um alle Muster ohne die Komplexitaet echter Hardware-Emulation zu veranschaulichen.

Schritt 1 — Quelldateien erstellen

Erstellen Sie zwei Dateien. Die Header-Datei deklariert die Funktionen, die andere Module aufrufen werden; die C-Datei implementiert sie.
// Datei: src/include/drivers/Sharp/MyDriver.h
#ifndef MYDRIVER_H
#define MYDRIVER_H

#include "Z80CPU.h"
#include "flash_ram.h"   // Fuer t_FlashAppConfigHeader

// Top-Level-Init (registriert in virtualFuncMap)
uint8_t MyDriver_Init(Z80CPU *cpu,
                      t_FlashAppConfigHeader *appConfig,
                      t_drvConfig *config,
                      const char *ifName);

// Lebenszyklus-Callbacks (vom Init gesetzt, von Core-1-Schleife aufgerufen)
uint8_t MyDriver_Reset(Z80CPU *cpu);
uint8_t MyDriver_PollCB(Z80CPU *cpu);
uint8_t MyDriver_TaskProcessor(Z80CPU *cpu,
                                enum Z80CPU_TASK_NAME task,
                                char *param);

// Speicher-/I/O-Handler-Funktionen
uint8_t MyDriver_MemHandler(Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
uint8_t MyDriver_IOHandler(Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);

#endif // MYDRIVER_H
// Datei: src/drivers/Sharp/MyDriver.c
#include <string.h>
#include <stdio.h>

#include "Z80CPU.h"
#include "flash_ram.h"
#include "drivers/Sharp/MyDriver.h"

// ------------------------------------------------------------------
// Interner Zustand
// ------------------------------------------------------------------
#define MYDRIVER_BANK   8   // PSRAM-Bank 8 fuer unseren RAM-Bereich verwenden
                            // (Baenke 0-7 fuer MZ-700-Persona in diesem Beispiel reserviert;
                            //  waehlen Sie eine Bank die kein anderer Treiber verwendet)

#define MYDRIVER_IO_STATUS   0xC0  // I/O-Port: Lesen = Status-Byte
#define MYDRIVER_IO_CONTROL  0xC1  // I/O-Port: Schreiben = Steuer-Byte

typedef struct {
    uint8_t  controlReg;    // Letzter an den Steuerport geschriebener Wert
    bool     enabled;       // Ist der RAM-Bereich derzeit eingemappt?
} t_MyDriverState;

static t_MyDriverState MyState = {
    .controlReg = 0,
    .enabled    = false,
};

// ------------------------------------------------------------------
// Speicher-Handler: wird fuer jeden Zugriff auf 0x8000-0xBFFF aufgerufen
// wenn der RAM-Bereich als MEMBANK_TYPE_FUNC gemappt ist
// ------------------------------------------------------------------
uint8_t MyDriver_MemHandler(Z80CPU *cpu, bool read, uint16_t addr, uint8_t data)
{
    // Fuer FUNC-Typ-Bloecke gibt es keine PSRAM-Stuetzung.
    // Wir verwenden einen Teil einer PSRAM-Bank als unseren Hintergrundspeicher,
    // bedienen aber Lese- und Schreibzugriffe manuell hier.

    uint32_t bankBase  = MYDRIVER_BANK * MEMORY_PAGE_SIZE; // Start von Bank 8 in RAM[]
    uint16_t localAddr = addr - 0x8000;                    // Offset innerhalb unseres Bereichs

    if(read)
    {
        // Byte aus unserem Hintergrundspeicher zurueckgeben
        return cpu->_z80PSRAM->RAM[bankBase + localAddr];
    }
    else
    {
        // Byte in unseren Hintergrundspeicher schreiben
        cpu->_z80PSRAM->RAM[bankBase + localAddr] = data;
        return data;
    }
}

// ------------------------------------------------------------------
// I/O-Handler: wird fuer Ports 0xC0 und 0xC1 aufgerufen
// ------------------------------------------------------------------
uint8_t MyDriver_IOHandler(Z80CPU *cpu, bool read, uint16_t addr, uint8_t data)
{
    uint8_t port = (uint8_t)(addr & 0xFF);

    if(read)
    {
        // Port 0xC0: Status-Byte zurueckgeben
        if(port == MYDRIVER_IO_STATUS)
            return MyState.enabled ? 0x01 : 0x00;
        return 0xFF;
    }
    else
    {
        // Port 0xC1: Steuer-Byte
        if(port == MYDRIVER_IO_CONTROL)
        {
            MyState.controlReg = data;

            if(data & 0x01)
            {
                // Bit 0 = aktivieren: unseren RAM in 0x8000-0xBFFF mappen
                if(!MyState.enabled)
                {
                    int startBlock = 0x8000 / MEMORY_BLOCK_SIZE;  // = 64
                    int endBlock   = 0xC000 / MEMORY_BLOCK_SIZE;  // = 96

                    for(int idx = startBlock; idx < endBlock; idx++)
                    {
                        // FUNC-Typ verwenden damit MyDriver_MemHandler fuer jeden Zugriff aufgerufen wird
                        cpu->_membankPtr[idx] = (MEMBANK_TYPE_FUNC  << 24)
                                              | (MYDRIVER_BANK      << 16)
                                              | (idx * MEMORY_BLOCK_SIZE);

                        // Speicher-Handler fuer jede Adresse im Bereich installieren
                    }
                    // memioPtr-Handler fuer den gesamten Bereich installieren
                    for(uint32_t a = 0x8000; a < 0xC000; a++)
                        cpu->_z80PSRAM->memioPtr[a] = (MemoryFunc)MyDriver_MemHandler;

                    MyState.enabled = true;
                }
            }
            else
            {
                // Bit 0 = 0: Unmapping, physische Durchleitung wiederherstellen
                if(MyState.enabled)
                {
                    int startBlock = 0x8000 / MEMORY_BLOCK_SIZE;
                    int endBlock   = 0xC000 / MEMORY_BLOCK_SIZE;

                    for(int idx = startBlock; idx < endBlock; idx++)
                    {
                        cpu->_membankPtr[idx] = (MEMBANK_TYPE_PHYSICAL << 24)
                                              | (0 << 16)
                                              | (idx * MEMORY_BLOCK_SIZE);
                    }
                    // memioPtr-Handler entfernen
                    for(uint32_t a = 0x8000; a < 0xC000; a++)
                        cpu->_z80PSRAM->memioPtr[a] = NULL;

                    MyState.enabled = false;
                }
            }
        }
        return 0;
    }
}

// ------------------------------------------------------------------
// Reset-Handler: Einschaltzustand wiederherstellen
// ------------------------------------------------------------------
uint8_t MyDriver_Reset(Z80CPU *cpu)
{
    // Falls unser RAM eingemappt war, physische Durchleitung wiederherstellen
    if(MyState.enabled)
    {
        int startBlock = 0x8000 / MEMORY_BLOCK_SIZE;
        int endBlock   = 0xC000 / MEMORY_BLOCK_SIZE;
        for(int idx = startBlock; idx < endBlock; idx++)
        {
            cpu->_membankPtr[idx] = (MEMBANK_TYPE_PHYSICAL << 24)
                                  | (0 << 16)
                                  | (idx * MEMORY_BLOCK_SIZE);
        }
        for(uint32_t a = 0x8000; a < 0xC000; a++)
            cpu->_z80PSRAM->memioPtr[a] = NULL;
    }

    // Zustand zuruecksetzen
    MyState.controlReg = 0;
    MyState.enabled    = false;
    return 0;
}

// ------------------------------------------------------------------
// Poll-Callback: in diesem Beispiel nichts zu tun
// ------------------------------------------------------------------
uint8_t MyDriver_PollCB(Z80CPU *cpu)
{
    (void)cpu;
    return 0;
}

// ------------------------------------------------------------------
// Task-Prozessor: in diesem Beispiel nichts zu tun
// ------------------------------------------------------------------
uint8_t MyDriver_TaskProcessor(Z80CPU *cpu, enum Z80CPU_TASK_NAME task, char *param)
{
    (void)cpu; (void)task; (void)param;
    return 0;
}

// ------------------------------------------------------------------
// Init-Funktion: wird vom virtualFuncMap-Dispatch aufgerufen
// ------------------------------------------------------------------
uint8_t MyDriver_Init(Z80CPU *cpu,
                      t_FlashAppConfigHeader *appConfig,
                      t_drvConfig *config,
                      const char *ifName)
{
    // Validierungsmodus: Unterstuetzt dieser Treiber das genannte Interface?
    if(ifName != NULL && config == NULL)
    {
        // Dieser einfache Treiber hat keine Sub-Interfaces — immer 0 zurueckgeben
        return 0;
    }

    // Konfigurationsmodus
    if(ifName == NULL && config != NULL)
    {
        // Standardzustand: 0x8000-0xBFFF ist PHYSICAL (Host-Hardware antwortet)
        // Der Z80 kann unseren RAM durch Schreiben auf Port 0xC1 aktivieren.
        // Zum Init-Zeitpunkt die Speicherkarte unveraendert lassen und nur I/O-Hooks installieren.

        // I/O-Handler fuer unsere Steuer- und Status-Ports installieren
        cpu->_z80PSRAM->ioPtr[MYDRIVER_IO_STATUS]  = (MemoryFunc)MyDriver_IOHandler;
        cpu->_z80PSRAM->ioPtr[MYDRIVER_IO_CONTROL] = (MemoryFunc)MyDriver_IOHandler;

        // Lebenszyklus-Callbacks registrieren
        config->reset_ptr = (ResetFunc)MyDriver_Reset;
        config->poll_ptr  = (PollFunc)MyDriver_PollCB;
        config->task_ptr  = (TaskFunc)MyDriver_TaskProcessor;

        // Nach Parametern suchen, die der Benutzer im JSON "param"-Array gesetzt hat
        for(int p = 0; p < config->ifCount; p++)
        {
            // (keine Sub-Interfaces in diesem Beispiel)
        }
    }
    return 1;
}

Schritt 2 — Zu CMakeLists.txt hinzufuegen

Oeffnen Sie src/CMakeLists.txt und fuegen Sie Ihre neue Quelldatei zur Sharp-Treiberliste hinzu:
# src/CMakeLists.txt — fuegen Sie Ihre Datei zur Sharp-Treiberliste hinzu

set(pZ80_drivers_sharp_src
    ${CMAKE_CURRENT_LIST_DIR}/drivers/Sharp/MZ700.c
    ${CMAKE_CURRENT_LIST_DIR}/drivers/Sharp/RFS.c
    ${CMAKE_CURRENT_LIST_DIR}/drivers/Sharp/WD1773.c
    ${CMAKE_CURRENT_LIST_DIR}/drivers/Sharp/QDDrive.c
    ${CMAKE_CURRENT_LIST_DIR}/drivers/Sharp/MZ-1E05.c
    ${CMAKE_CURRENT_LIST_DIR}/drivers/Sharp/MZ8BFI.c
    ${CMAKE_CURRENT_LIST_DIR}/drivers/Sharp/MZ-1E14.c
    ${CMAKE_CURRENT_LIST_DIR}/drivers/Sharp/MZ-1E19.c
    ${CMAKE_CURRENT_LIST_DIR}/drivers/Sharp/MZ-1R12.c
    ${CMAKE_CURRENT_LIST_DIR}/drivers/Sharp/MZ-1R18.c
    # FUEGEN SIE IHREN TREIBER HIER HINZU:
    ${CMAKE_CURRENT_LIST_DIR}/drivers/Sharp/MyDriver.c
)

# Amstrad-Treiberliste (kompiliert wenn INCLUDE_AMSTRAD_DRIVERS gesetzt ist)
set(pZ80_drivers_amstrad_src
    ${CMAKE_CURRENT_LIST_DIR}/drivers/Amstrad/PCW9512.c
    ${CMAKE_CURRENT_LIST_DIR}/drivers/Amstrad/uPD765.c
)
Sie muessen ausserdem das Verzeichnis Ihres Headers dem Include-Pfad hinzufuegen, falls es sich in einem neuen Unterverzeichnis befindet. Fuer das Sharp-Treiberverzeichnis ist dies bereits eingerichtet, sodass kein zusaetzlicher target_include_directories-Aufruf erforderlich ist.

Schritt 3 — Header in Z80CPU.c einbinden

Oeffnen Sie src/Z80CPU.c und fuegen Sie ein Include fuer Ihren Treiber-Header neben den bestehenden Sharp-Treiber-Includes hinzu:
// src/Z80CPU.c — oben bei den anderen Treiber-Includes
#ifdef INCLUDE_SHARP_DRIVERS
#include "drivers/Sharp/MZ700.h"
#include "drivers/Sharp/RFS.h"
#include "drivers/Sharp/WD1773.h"
#include "drivers/Sharp/QDDrive.h"
#include "drivers/Sharp/MZ-1E05.h"
#include "drivers/Sharp/MZ8BFI.h"
#include "drivers/Sharp/MZ-1E14.h"
#include "drivers/Sharp/MZ-1E19.h"
#include "drivers/Sharp/MZ-1R12.h"
#include "drivers/Sharp/MZ-1R18.h"
// FUEGEN SIE IHR INCLUDE HINZU:
#include "drivers/Sharp/MyDriver.h"
#endif

#ifdef INCLUDE_AMSTRAD_DRIVERS
#include "drivers/Amstrad/PCW9512.h"
#include "drivers/Amstrad/uPD765.h"
#endif

Schritt 4 — In virtualFuncMap registrieren

Noch in src/Z80CPU.c, finden Sie das virtualFuncMap[]-Array und fuegen Sie Ihren Eintrag hinzu. Die Zeichenkette "MyDriver" ist das, was das JSON-"name"-Feld enthalten muss:
// src/Z80CPU.c
static const t_VirtualFuncMap virtualFuncMap[] = {
#ifdef INCLUDE_SHARP_DRIVERS
    {"MZ700",    MZ700_Init},
    {"MZ80A",    MZ80A_Init},
    {"MZ2000",   MZ2000_Init},
    {"MZ2200",   MZ2200_Init},
    {"MZ80B",    MZ80B_Init},
    {"MZ2500",   MZ2500_Init},
    {"MZ800",    MZ800_Init},
#endif
#ifdef INCLUDE_AMSTRAD_DRIVERS
    {"PCW9512",  PCW9512_Init},
#endif
#ifdef INCLUDE_TATUNG_DRIVERS
    {"EinsteinTC01", EinsteinTC01_Init},
#endif
    // FUEGEN SIE IHREN TREIBER HINZU:
    {"MyDriver", MyDriver_Init},
};
Die Suche ist Gross-/Kleinschreibung-unabhaengig, sodass "mydriver", "MyDriver" und "MYDRIVER" im JSON alle mit diesem Eintrag uebereinstimmen.

Schritt 5 — Treiber zu config.json hinzufuegen

Fuegen Sie einen "drivers"-Eintrag zu Ihrer config.json auf der SD-Karte hinzu. Das "name"-Feld muss mit der Zeichenkette uebereinstimmen, die Sie in virtualFuncMap[] registriert haben:
{
  "rp2350": {
    "core": { "cpufreq": 300000000, "psramfreq": 133000000, "voltage": 1.10 },
    "z80": [
      {
        "memory": [
          { "enable": 1, "addr": "0x0000", "size": "0x1000",
            "type": "ROM", "bank": 0, "tcycwait": 0, "tcycsync": 0,
            "task": "", "file": "/ROM/mz700.rom", "fileofs": 0 },
          { "enable": 1, "addr": "0x1000", "size": "0xCFFF",
            "type": "RAM", "bank": 0, "tcycwait": 1, "tcycsync": 1,
            "task": "", "file": "", "fileofs": 0 }
        ],
        "io": [],
        "drivers": [
          {
            "enable": 1,
            "name":   "MZ700",
            "type":   "VIRTUAL",
            "if": []
          },
          {
            "enable": 1,
            "name":   "MyDriver",
            "type":   "VIRTUAL",
            "if": []
          }
        ]
      }
    ]
  }
}

Schritt 6 — Bauen und Testen

# Aus dem Projekt-Stammverzeichnis
./build_tzpuPico.sh DEBUG

# Firmware-Ausgabe:
# build/bin/model/BaseZ80/BaseZ80_0x10020000.elf   (Debug-ELF — verwenden mit GDB)
# build/bin/model/BaseZ80/BaseZ80_0x10020000.bin   (OTA-Binaerdatei)

# Per OTA-Webseite flashen oder direkt debuggen:
openocd -f interface/cmsis-dap.cfg -f target/rp2350_tzpu.cfg -c "adapter speed 5000" &
cd build/bin/model/BaseZ80
gdb-multiarch BaseZ80_0x10020000.elf
(gdb) break MyDriver_Init
(gdb) continue
Das Build-System erzeugt Anwendungs-Firmware-Binaerdateien fuer jedes Modell-Target — zwei Partitionen (Partition 1 bei 0x10020000, Partition 2 bei 0x10520000) jeweils in Standard- und DBGSH-Varianten. Drei Modell-Targets stehen zur Verfuegung:
  • BaseZ80 (pZ80-BaseZ80) — universelle Binaerdatei mit allen Treibern (Sharp + Amstrad + Tatung). Definiert INCLUDE_SHARP_DRIVERS, INCLUDE_AMSTRAD_DRIVERS und INCLUDE_TATUNG_DRIVERS.
  • SharpZ80 (pZ80-SharpZ80) — nur Sharp-MZ-Treiber. Definiert INCLUDE_SHARP_DRIVERS. Erzeugt eine kleinere Firmware-Binaerdatei.
  • AmstradZ80 (pZ80-AmstradZ80) — nur Amstrad-PCW-Treiber. Definiert INCLUDE_AMSTRAD_DRIVERS und TARGET_MODEL_AMSTRAD. Erzeugt eine kleinere Firmware-Binaerdatei.
  • TatungZ80 (pZ80-TatungZ80) — nur Tatung-Einstein-Treiber. Definiert INCLUDE_TATUNG_DRIVERS und TARGET_MODEL_TATUNG. Erzeugt eine kleinere Firmware-Binaerdatei.
Jedes Modell-Target hat sein eigenes Verzeichnis unter src/model/ mit einer dedizierten CMakeLists.txt, Einstiegspunkt (main.c) und Linker-Skripten. Die Standardvariante laesst die Debug-Shell fuer eine kleinere Firmware-Image weg. Die DBGSH-Variante fuegt das INCLUDE_DBGSH-Compile-Time-Define hinzu, das die vollstaendige ICE-Debug-Shell auf USB CDC Kanal 1 aktiviert. DBGSH-Firmware-Dateinamen sind am _DBGSH-Suffix erkennbar.

ESP32-Firmware — Netzwerkmodus-Auswahl

Vor dem Bauen der ESP32-Firmware waehlen Sie den gewuenschten Netzwerkmodus, indem Sie die entsprechende vorgefertigte sdkconfig kopieren:
# Netzwerkmodus vor dem Bauen der ESP32-Firmware waehlen:
cp sdkconfig.mode_ncm_only sdkconfig       # Nur NCM (kein WiFi, FCC/RED-sicher)
# cp sdkconfig.mode_wifi_only sdkconfig     # Nur WiFi
# cp sdkconfig.mode_wifi_and_ncm sdkconfig  # Sowohl WiFi als auch NCM
idf.py build
Die wichtigsten Praeprocessor-Defines, die durch diese Konfigurationen gesteuert werden, sind:
  • CONFIG_IF_WIFI_ENABLED — aktiviert WiFi-Funk und AP/Client-Code.
  • CONFIG_IF_USB_NCM_ENABLED — aktiviert USB-NCM-Netzwerkinterface und DHCP-Server.
Diese werden ueber Kconfig (idf.py menuconfig) oder die oben aufgefuehrten vorgefertigten sdkconfig-Dateien gesetzt.

Speicher-Hook-Muster im Detail

Dieser Abschnitt beschreibt jedes Hook-Muster im Detail mit vollstaendigen Beispielen. Dies sind die Bausteine der gesamten Treiber-Speicherverwaltung.

Muster 1 — Rein virtuelles Geraet (FUNC-Block)

Verwenden Sie dies, wenn Sie moechten, dass ein Bereich des Z80-Adressraums vollstaendig von Ihrem Handler kontrolliert wird, ohne PSRAM-Stuetzung. Die Lese- und Schreibzugriffe des Z80 rufen immer Ihre Funktion auf. Nichts wird im PSRAM gespeichert.
// 0xC000-0xCFFF als rein virtuelles Geraet in Bank 4 mappen
int startBlock = 0xC000 / MEMORY_BLOCK_SIZE;  // = 96
int endBlock   = 0xD000 / MEMORY_BLOCK_SIZE;  // = 104

for(int idx = startBlock; idx < endBlock; idx++)
{
    cpu->_membankPtr[idx] = (MEMBANK_TYPE_FUNC << 24)
                          | (4               << 16)
                          | (idx * MEMORY_BLOCK_SIZE);
}

// Handler fuer jede Adresse im Bereich installieren
for(uint32_t addr = 0xC000; addr < 0xD000; addr++)
    cpu->_z80PSRAM->memioPtr[addr] = (MemoryFunc)MyVirtualDevice_Handler;

// Handler:
uint8_t MyVirtualDevice_Handler(Z80CPU *cpu, bool read, uint16_t addr, uint8_t data)
{
    uint16_t reg = addr - 0xC000;  // Register-Offset innerhalb des Geraets

    if(read)
    {
        switch(reg)
        {
            case 0x00: return myDevice.statusReg;
            case 0x01: return myDevice.dataReg;
            default:   return 0xFF;
        }
    }
    else
    {
        switch(reg)
        {
            case 0x01: myDevice.dataReg    = data; break;
            case 0x02: myDevice.controlReg = data; break;
        }
        return data;
    }
}

Muster 2 — Schreibzugriffe auf einen RAM-Bereich abfangen

Verwenden Sie dies, wenn Sie moechten, dass ein Bereich sich wie normales RAM verhaelt (Lesezugriffe geben PSRAM-Daten zurueck, Schreibzugriffe aktualisieren PSRAM), Sie aber auch ueber Schreibzugriffe benachrichtigt werden moechten — zum Beispiel um Video-RAM-Schreibzugriffe in einen Schattenpuffer zu spiegeln oder ein Hardware-Update auszuloesen. Der Blocktyp bleibt RAM; Sie installieren einen memioPtr-Handler, der den Schreibzugriff nachbearbeitet.
// 0xD000-0xD7FF als RAM mappen aber alle Schreibzugriffe abfangen
int startBlock = 0xD000 / MEMORY_BLOCK_SIZE;
int endBlock   = 0xD800 / MEMORY_BLOCK_SIZE;

for(int idx = startBlock; idx < endBlock; idx++)
{
    cpu->_membankPtr[idx] = (MEMBANK_TYPE_RAM << 24)
                          | (MY_VRAM_BANK     << 16)
                          | (idx * MEMORY_BLOCK_SIZE);
    cpu->_memAttr[MY_VRAM_BANK][idx].waitStates = 2;
    cpu->_memAttr[MY_VRAM_BANK][idx].tCycSync   = true;
}

// Handler nur fuer Schreib-Abfang-Adressen installieren
for(uint32_t addr = 0xD000; addr < 0xD800; addr++)
    cpu->_z80PSRAM->memioPtr[addr] = (MemoryFunc)MyVRAM_WriteIntercept;

// Handler — wird von Z80CPU_writeMem() fuer RAM-Typ mit Handler aufgerufen:
// Der zurueckgegebene Wert ist das, was im PSRAM gespeichert wird (nicht die Original-'data').
uint8_t MyVRAM_WriteIntercept(Z80CPU *cpu, bool read, uint16_t addr, uint8_t data)
{
    if(read)
    {
        // Beim Lesen: 'data' enthaelt bereits den PSRAM-Wert — einfach zurueckgeben
        return data;
    }
    else
    {
        // Beim Schreiben: unsere Schattenkopie aktualisieren, dann 'data' zurueckgeben damit PSRAM ebenfalls aktualisiert wird
        uint16_t vramOffset = addr - 0xD000;
        myVRAMShadow[vramOffset] = data;
        markDirty(vramOffset);     // z.B. Renderer signalisieren dass sich diese Zelle geaendert hat
        return data;  // PSRAM wird mit dem zurueckgegebenen Wert beschrieben
    }
}

Muster 3 — Schreibzugriffe auf einen ROM-Bereich abfangen

Manche Hardware verwendet Schreibzugriffe auf ROM-gemappte Adressen als Banking-Register-Schreibzugriffe (der Schreibzugriff wird von der Hardware "dekodiert", modifiziert aber nicht das ROM). Der Block bleibt ROM; Schreibzugriffe loesen Ihren Handler aus, aber das PSRAM wird nicht modifiziert.
// ROM bei 0x0000-0x0FFF, aber Schreibzugriffe auf 0x0000-0x001F sind Banking-Register
for(int idx = 0; idx < (0x1000 / MEMORY_BLOCK_SIZE); idx++)
{
    cpu->_membankPtr[idx] = (MEMBANK_TYPE_ROM << 24)
                          | (ROM_BANK         << 16)
                          | (idx * MEMORY_BLOCK_SIZE);
}

// Schreib-Trap-Handler nur fuer Adressen 0x0000-0x001F installieren
for(uint32_t addr = 0x0000; addr < 0x0020; addr++)
    cpu->_z80PSRAM->memioPtr[addr] = (MemoryFunc)MyROM_WriteTrap;

// Handler:
uint8_t MyROM_WriteTrap(Z80CPU *cpu, bool read, uint16_t addr, uint8_t data)
{
    if(read)
    {
        // ROM-Daten zurueckgeben — 'data' enthaelt bereits den PSRAM-Wert an dieser Adresse
        return data;
    }
    else
    {
        // Schreibzugriff auf ROM-Adresse — als Banking-Register-Schreibzugriff behandeln
        MyBankSwitch(cpu, addr, data);
        // Rueckgabewert wird fuer ROM-Schreibzugriffe ignoriert (PSRAM wird nicht beschrieben)
        return data;
    }
}

Muster 4 — Sparse Handler (einzelne Adressen)

Sie muessen nicht fuer einen gesamten Block Handler installieren. Sie koennen einen Handler auf einer einzelnen spezifischen Adresse innerhalb eines RAM- oder ROM-Blocks installieren. Der Blocktyp steuert, was mit allen anderen Adressen im Block geschieht; der spezifische Adress-Handler ueberschreibt nur diese eine Adresse.
// Der Block der 0x1234 enthaelt ist als RAM konfiguriert
// Wir moechten dass speziell 0x1234 einen Handler nur beim Schreiben aufruft

cpu->_z80PSRAM->memioPtr[0x1234] = (MemoryFunc)MySpecialHandler;

uint8_t MySpecialHandler(Z80CPU *cpu, bool read, uint16_t addr, uint8_t data)
{
    if(read)
        return data;  // Normales RAM-Lesen — PSRAM-Wert zurueckgeben
    else
    {
        // Spezielle Aktion beim Schreiben auf 0x1234
        triggerSomething(data);
        return data;  // Daten normal ins PSRAM schreiben
    }
}

Muster 5 — I/O-Port-Handler

I/O-Handler sind einfacher — es gibt keine PSRAM-Stuetzung fuer I/O-Ports. Der Handler wird entweder aufgerufen (falls installiert) oder der I/O-Zyklus geht an die physische Hardware. Es gibt kein Blocktyp-Konzept.
// Handler fuer I/O-Ports 0x80-0x8F installieren (16 Ports)
for(int port = 0x80; port <= 0x8F; port++)
    cpu->_z80PSRAM->ioPtr[port] = (MemoryFunc)MyIO_Handler;

uint8_t MyIO_Handler(Z80CPU *cpu, bool read, uint16_t addr, uint8_t data)
{
    uint8_t port = (uint8_t)(addr & 0xFF);   // Tatsaechliche Portnummer
    // uint8_t regB = (uint8_t)(addr >> 8);  // Register B waehrend IN r,(C) / OUT (C),r

    if(read)
    {
        switch(port)
        {
            case 0x80: return myDevice.status;
            case 0x81: return myDevice.rxData;
            default:   return 0xFF;
        }
    }
    else
    {
        switch(port)
        {
            case 0x80: myDevice.control = data; applyControl(); break;
            case 0x82: myDevice.txData  = data; sendByte(data); break;
        }
        return 0;
    }
}

Ein Sub-Interface zu einer bestehenden Persona hinzufuegen

Wenn Ihr neues Peripheriegeraet eine Karte ist, die in einen MZ-700 (oder eine andere bestehende Persona) gesteckt wird, implementieren Sie es als Sub-Interface statt als Top-Level-Treiber. Dies ist das Muster, das von RFS, WD1773, QDDrive, MZ-1E05, MZ8BFI und den RAM-Erweiterungskarten verwendet wird.

Erforderliche Funktionen fuer ein Sub-Interface

Ein Sub-Interface benoetigt vier Funktionen mit diesen Signaturen:
// Wird einmal waehrend MZ700_Init aufgerufen wenn dieser Interface-Name im JSON "if"-Array erscheint
uint8_t MyCard_Init(Z80CPU *cpu,
                    t_FlashAppConfigHeader *appConfig,
                    t_drvIFConfig *ifConfig);   // Beachte: t_drvIFConfig, nicht t_drvConfig

// Wird bei RESET aufgerufen
uint8_t MyCard_Reset(Z80CPU *cpu);

// Wird alle ~2048 Z80-Zyklen aufgerufen (muss sehr schnell sein)
uint8_t MyCard_PollCB(Z80CPU *cpu);

// Wird fuer Intercore-Task-Zustellung aufgerufen
uint8_t MyCard_TaskProcessor(Z80CPU *cpu, enum Z80CPU_TASK_NAME task, char *param);

Das Sub-Interface registrieren

Fuegen Sie Ihr Sub-Interface zur interfaceFuncMap[] in der C-Datei der uebergeordneten Persona hinzu (z.B. MZ700.c):
// src/drivers/Sharp/MZ700.c — zu interfaceFuncMap[] hinzufuegen
#include "drivers/Sharp/MyCard.h"  // Dieses Include oben in MZ700.c hinzufuegen

static t_InterfaceFuncMap interfaceFuncMap[] = {
    {"RFS",     false, RFS_Init,    RFS_Reset,    RFS_PollCB,    RFS_TaskProcessor},
    {"MZ-1E05", false, MZ1E05_Init, MZ1E05_Reset, MZ1E05_PollCB, MZ1E05_TaskProcessor},
    // ... bestehende Eintraege ...
    // FUEGEN SIE IHR SUB-INTERFACE HINZU:
    {"MyCard",  false, MyCard_Init, MyCard_Reset, MyCard_PollCB, MyCard_TaskProcessor},
};
Die Zeichenkette "MyCard" muss mit dem "name"-Feld des Interface-Eintrags im JSON-"if"-Array uebereinstimmen (Gross-/Kleinschreibung wird nicht beachtet):
"drivers": [
  {
    "enable": 1,
    "name":   "MZ700",
    "type":   "VIRTUAL",
    "if": [
      {
        "enable": 1,
        "name":   "MyCard",
        "type":   "VIRTUAL",
        "rom":    [],
        "addrmap": [],
        "iomap":   [],
        "param": [
          { "name": "myParam", "value": "42" }
        ]
      }
    ]
  }
]

JSON-Parameter in einem Sub-Interface lesen

Das t_drvIFConfig *ifConfig, das an Ihre Sub-Interface-Init-Funktion uebergeben wird, enthaelt alle JSON-konfigurierten Daten. Um einen benannten Parameter zu lesen:
uint8_t MyCard_Init(Z80CPU *cpu,
                    t_FlashAppConfigHeader *appConfig,
                    t_drvIFConfig *ifConfig)
{
    // Einen benannten Parameter aus dem "param"-Array lesen
    int myParamValue = 0;

    for(int p = 0; p < ifConfig->ifParamCount; p++)
    {
        if(strcasecmp(ifConfig->ifParam[p].name, "myParam") == 0)
        {
            myParamValue = atoi(ifConfig->ifParam[p].value);
            break;
        }
    }

    // ROM-Images laden die im "rom"-Array gelistet sind
    for(int r = 0; r < ifConfig->romCount; r++)
    {
        t_drvROMConfig *rom = &ifConfig->romConfig[r];

        if(rom->file != NULL && rom->file[0] != '\0')
        {
            // ROM von SD-Karte in PSRAM-Bank an der konfigurierten Adresse laden
            uint32_t bankBase = rom->bank * MEMORY_PAGE_SIZE;
            Z80CPU_ReadROM(appConfig, rom->file, NULL, NULL, NULL,
                           &cpu->_z80PSRAM->RAM[bankBase + rom->addr],
                           rom->size, rom->fileofs);

            // membankPtr fuer diesen ROM-Bereich einrichten
            int startBlock = rom->addr / MEMORY_BLOCK_SIZE;
            int endBlock   = (rom->addr + rom->size) / MEMORY_BLOCK_SIZE;
            for(int idx = startBlock; idx < endBlock; idx++)
            {
                cpu->_membankPtr[idx] = (MEMBANK_TYPE_ROM << 24)
                                      | (rom->bank        << 16)
                                      | (idx * MEMORY_BLOCK_SIZE);
                cpu->_memAttr[rom->bank][idx].waitStates = rom->waitStates;
                cpu->_memAttr[rom->bank][idx].tCycSync   = rom->tCycSync;
            }
        }
    }

    // I/O-Handler aus dem "iomap"-Array installieren
    for(int m = 0; m < ifConfig->ioMapCount; m++)
    {
        t_ioReMap *iomap = &ifConfig->ioMap[m];

        // Benannte Handler-Funktion nachschlagen
        MemoryFunc handler = Z80CPU_getMemoryFunc(iomap->funcName);
        if(handler != NULL)
        {
            for(uint32_t port = iomap->srcaddr;
                port < iomap->srcaddr + iomap->size; port++)
            {
                cpu->_z80PSRAM->ioPtr[port] = handler;
            }
        }
    }

    return 1;
}

Durchgearbeitetes Sub-Interface-Beispiel: RS-232C-Serienkarten (Z80 SIO)

Die RS-232C-Serienkarten sind ein vollstaendiges, praxisnahes Beispiel des obigen Sub-Interface-Musters und eines Sub-Interfaces, das ein wiederverwendbares Geraete-Emulationsmodul besitzt und es an USB brueckt. Es werden zwei Persona-Sub-Interfaces bereitgestellt — MZ-8BIO3 (src/drivers/Sharp/MZ8BIO3.c) und MZ-1E24 (src/drivers/Sharp/MZ1E24.c) —, beide aufgebaut auf einer gemeinsamen Zilog Z80 SIO/2-Emulation (src/drivers/Z80SIO.c / src/include/drivers/Z80SIO.h).

Die Z80-SIO-Emulation (Z80SIO.c). Dies ist ein eigenstaendiges, registergenaues Zilog Z80 SIO/2-Modell, das vollstaendig von USB und der CPU-Emulation entkoppelt ist, sodass es von jeder Karte wiederverwendet werden kann. Es implementiert:

  • Zwei Kanaele — Kanal A bei Offsets 0/1 (Daten/Steuerung) und Kanal B bei Offsets 2/3.
  • Den vollstaendigen Schreibregistersatz WR0–WR7 und Leseregistersatz RR0–RR2, einschliesslich des WR0-Zwei-Byte-Zeiger/Befehl-Protokolls (schreibe ein Registerauswahl-/Befehlsbyte, dann adressiert das Datenbyte das ausgewaehlte Register).
  • Z80-Mode-2-vektorisierte Interrupts mit einem 4-stufigen In-Service-Stack und strenger Daisy-Chain-Prioritaet (Kanal A Special-Rx am hoechsten, herunter bis Kanal B External/Status am niedrigsten) und die Status-affects-Vector-Option.
  • Zwei sperrfreie Single-Producer/Single-Consumer-Ringe pro Kanal, je 1 KB: ein Tx-Ring (Core 1 Producer → Core 0 Consumer) und ein Rx-Ring (Core 0 Producer → Core 1 Consumer). Ring-Push/Pop-Helfer sind fuer die USB-Pumpe offengelegt, und ein volatile bool* ist fuer die /INT-Leitung offengelegt.

Beim Betrieb ueber USB (statt einer echten seriellen Leitung) werden DCD und CTS aktiv gehalten, sodass Host-Firmware, die den Modem-Steuerstatus abfragt, nicht blockiert und auf einen Traeger wartet.

Gemeinsame Karten-Implementierung und der duenne Wrapper. MZ8BIO3.c enthaelt die gemeinsame SIOCard_*-Implementierung (Init, Reset, Poll, Task, USB-Pumpe). MZ1E24.c ist ein duenner Wrapper, der sich nur im an SIOCard_Init() uebergebenen Steckverbindermodus unterscheidet — SIOCARD_MODE_BI fuer den MZ-8BIO3 gegenueber SIOCARD_MODE_ST fuer den MZ-1E24. Die Init-Funktion der Karte:

  • Parst den "port"-Parameter (Standard 0xB0) und maskiert ihn auf eine 4-Port-Grenze, sodass die vier SIO-Register auf einer sauberen Basis landen.
  • Instanziiert den Z80 SIO und verdrahtet seine /INT-Leitung mit cpu->swIntAssert — dem Software-Interrupt-Quell-Hook in der CPU-Emulation.
  • Installiert den I/O-Handler auf allen 256 High-Byte-Varianten jedes der vier Ports (der Z80 legt Register B waehrend des I/O auf A8–A15, sodass jede High-Byte-Variante auf denselben Handler aufloesen muss).
  • Registriert die Software-Interrupt-Acknowledge-/RETI-Hooks (swIntAckVector / swIntReti), sodass die Mode-2-Vektor-Zustellung und das In-Service-Loeschen funktionieren.
  • Installiert die USB-Pumpe SIOCard_usbPump() in den globalen g_usbSerialPump-Hook.

Im physischen Modus tut die Karte nichts und gibt 0 zurueck — die echte Hardware-Karte antwortet auf dem Host-Bus.

USB-Bruecke. Kanal A ist auf USB CDC 2 (VSER_CHANNEL_A) und Kanal B auf USB CDC 3 (VSER_CHANNEL_B) gebrueckt. Diese beiden CDC-Ports haben keine physische UART dahinter; stattdessen ruft pollUSBtoUART() auf Core 0 SIOCard_usbPump() auf, um Bytes zwischen den CDC-FIFOs und den SIO-Kanalringen zu befoerdern (Host → Rx-Ring, Tx-Ring → Host). Die Karte legt die vier Standard-Sub-Interface-Callbacks offen — SIOCard_Reset, SIOCard_PollCB, SIOCard_TaskProcessor — und ist in der interfaceFuncMap[] jeder SIO-faehigen Persona (MZ-700, MZ-800, MZ-80B, MZ-1500) registriert, zum Beispiel: </div>

// In der interfaceFuncMap[] jeder SIO-faehigen Persona:
{"MZ-8BIO3", false, MZ8BIO3_Init, SIOCard_Reset, SIOCard_PollCB, SIOCard_TaskProcessor},
{"MZ-1E24",  false, MZ1E24_Init,  SIOCard_Reset, SIOCard_PollCB, SIOCard_TaskProcessor},
Beide Karten folgen dem oben beschriebenen Standard-Interface-Treiber-Vertrag — sie sind ROM-los (keine "rom"-Eintraege), installieren nur I/O-Handler und Callbacks und werden von der Persona ausgewaehlt, die sie in ihrer interfaceFuncMap[] auflistet. Auf der Web-Konfigurationsseite werden sie ueber configgui.js beworben (die driverInterfaces-Map und die interfaceRomLimits-Tabelle, wo sie eine ROM-Anzahl von null tragen).

Core 0 / Core 1 Interaktion

Treiber-Handler laufen auf Core 1 innerhalb der Hot-Loop. Jede Operation, die mehr als ein paar Mikrosekunden dauert (Datei-I/O, UART-Befehle an den ESP32, malloc), muss ueber die Intercore-Queue an Core 0 ausgelagert werden.

Die Intercore-Queue verwenden

Das Muster ist:
  1. Ihr Handler (auf Core 1) erkennt, dass eine Datei-I/O- oder aehnliche Operation benoetigt wird (z.B. hat der Z80 eine Sektornummer in ein Disk-Befehlsregister geschrieben).
  2. Der Handler setzt ein Zustands-Flag (z.B. diskState.pendingRead = true) und kehrt sofort zurueck — er fuehrt die I/O nicht aus.
  3. Ihr poll_ptr (ebenfalls auf Core 1, wird alle ~2048 Zyklen aufgerufen) prueft das Zustands-Flag und schiebt, falls gesetzt, eine Anforderungsnachricht in cpu->requestQueue.
  4. Core 0 empfaengt die Nachricht, fuehrt die Datei-I/O aus (z.B. liest einen Disk-Sektor von der SD-Karte) und schiebt das Ergebnis zurueck in cpu->responseQueue.
  5. Ihr task_ptr wird aufgerufen (auf Core 1) mit dem Task-Ergebnis. Er kopiert die Sektordaten ins PSRAM und loescht das Pending-Flag.
  6. </ol>
    // In Ihrem I/O-Handler (Core 1 — muss schnell sein):
    uint8_t MyDisk_IOHandler(Z80CPU *cpu, bool read, uint16_t addr, uint8_t data)
    {
        if(!read && (addr & 0xFF) == 0xFE)
        {
            // Z80 hat einen Sektor-Lese-Befehl ausgegeben
            diskState.pendingSector = data;
            diskState.pendingRead   = true;
            // Sofort zurueckkehren — hier KEINE Datei-I/O aufrufen
        }
        return 0;
    }
    
    // In Ihrem Poll-Handler (Core 1 — nur schnelle Pruefung):
    uint8_t MyDisk_PollCB(Z80CPU *cpu)
    {
        if(diskState.pendingRead)
        {
            // Anforderung aufbauen und an Core 0 senden
            t_intercoreMsg msg = {
                .taskId  = TASK_READ_SECTOR,
                .param1  = diskState.pendingSector,
                .dataPtr = diskState.sectorBuffer,
            };
            if(queue_try_add(&cpu->requestQueue, &msg))
                diskState.pendingRead = false;  // Anforderung gesendet
        }
        return 0;
    }
    
    // In Ihrem Task-Prozessor (Core 1 — wird aufgerufen wenn Core 0 den Task abgeschlossen hat):
    uint8_t MyDisk_TaskProcessor(Z80CPU *cpu, enum Z80CPU_TASK_NAME task, char *param)
    {
        if(task == TASK_READ_SECTOR)
        {
            // Sektordaten sind jetzt in diskState.sectorBuffer
            // Ins PSRAM an der DMA-Transferadresse kopieren
            uint32_t dmaAddr = DISK_BUFFER_BANK * MEMORY_PAGE_SIZE + diskState.dmaAddr;
            memcpy(&cpu->_z80PSRAM->RAM[dmaAddr],
                   diskState.sectorBuffer, SECTOR_SIZE);
    
            // Dem Z80 signalisieren dass Daten bereit sind (z.B. Status-Flag in einem FUNC-Register setzen)
            diskState.statusReg |= 0x01;  // DRQ-Bit
        }
        return 0;
    }
    </div> -------------------------------------------------------------------------------------------------------- ## Haeufige Fallstricke
    • Blockieren in einem Handler. Der haeufigste Fehler. Jeder Aufruf von debugf, sleep_ms, fopen oder einer beliebigen UART-Funktion aus einem Handler oder Poll-Callback heraus blockiert Core 1 und fuehrt dazu, dass der Host-Z80 fehlerhaftes Bus-Timing sieht. Verschieben Sie alle blockierenden Operationen ueber die Request-Queue auf Core 0. Verwenden Sie plogf() statt debugf() fuer Boot-Pfad-Protokollierung, die funktionieren muss bevor USB verfuegbar ist (siehe Debug-Protokollierung).
    • Falsche Schliesungstag-Reihenfolge bei Handler-Registrierung. Wenn Sie Handler ueber einen Bereich mit einer Schleife installieren, stellen Sie sicher, dass die Schleifengrenzen < statt <= fuer die Endadresse verwenden — Off-by-One-Fehler koennen benachbarte Handler beschaedigen.
    • Vergessen, Handler bei Treiber-Shutdown oder Reset zu loeschen. Wenn Ihr Reset-Handler die memioPtr[]- oder ioPtr[]-Slots, die Ihr Treiber installiert hat, nicht loescht, werden diese Handler nach dem Reset weiterhin aufgerufen, moeglicherweise mit veraltetem Zustand.
    • Banknummer-Kollision. Jede PSRAM-Bank ist 64KB gross. Die JSON-Konfiguration weist Baenke den Speicherbereichen zu. Wenn zwei Treiber die gleiche Banknummer verwenden, ueberschreiben sie gegenseitig ihre Daten. Verwenden Sie eindeutige Banknummern fuer jeden Treiber. Baenke 0–7 werden typischerweise von der MZ-700-Persona verwendet; verwenden Sie Baenke 8+ fuer Sub-Interfaces und zusaetzliche Treiber.
    • MEMBANK_TYPE_FUNC ohne installierten Handler. Wenn Sie einen Block auf den FUNC-Typ setzen, aber keinen memioPtr-Handler installieren, geben Lesezugriffe 0x00 zurueck und Schreibzugriffe werden stillschweigend verworfen. Dies ist gueltiges Verhalten, aber oft ein Fehler — installieren Sie immer den Handler bevor Sie den Blocktyp setzen.
    • Nicht uebereinstimmender virtualFuncMap-Name und JSON-Name. Die Suche ist Gross-/Kleinschreibung-unabhaengig, aber die Zeichenkette muss ansonsten exakt uebereinstimmen. Ein Tippfehler an einer der beiden Stellen fuehrt dazu, dass der Treiber stillschweigend uebersprungen wird, ohne Fehlermeldung. Fuegen Sie einen temporaeren debugf-Aufruf in Z80CPU_getVirtualFunc() hinzu, wenn Ihr Treiber nicht initialisiert wird — debugf ist ein Makro, das in Produktions-Builds deaktiviert oder ratenbegrenzt werden kann, sodass es das Bus-Timing nicht beeintraechtigt.
    • Vergessen, reset_ptr / poll_ptr / task_ptr zu setzen. Wenn Sie diese in Ihrer Init-Funktion nicht zuweisen, wird Core 1 niemals Ihre Reset-, Poll- oder Task-Funktionen aufrufen. Der Treiber wird korrekt initialisiert, aber nicht auf RESET reagieren oder periodische Verwaltung durchfuehren.
    -------------------------------------------------------------------------------------------------------- ## Debug-Protokollierung
    Das Debuggen der picoZ80-Firmware ist herausfordernd, weil es sich um ein Multi-Core-, Multi-Prozessor-System handelt: Der RP2350 betreibt zwei Cortex-M33-Kerne mit unterschiedlichen Echtzeit- und Nicht-Echtzeit-Verantwortlichkeiten, und der ESP32-Coprozessor uebernimmt alle Netzwerk- und Speicher-I/O. Traditionelles printf-Debugging ist nicht unkompliziert — USB ist moeglicherweise waehrend des fruehen Boots nicht verfuegbar, die Hot-Loop von Core 1 vertraegt keine blockierenden Aufrufe, und ein Watchdog-Reset zerstoert fluechtige Zustaende. Die Firmware bietet drei komplementaere Debug-Ausgabemechanismen, um diese Einschraenkungen zu adressieren.
    #### debugf() — Gepufferte Debug-Ausgabe
    debugf() ist das primaere Debug-Ausgabemakro. Es funktioniert wie printf(), schreibt aber in einen 64KB-Puffer im PSRAM (an Adresse 0x117EF004) statt direkt an eine serielle Schnittstelle. Der Puffer wird an USB CDC geflusht, wenn die Hauptschleife Leerlaufzeit hat. Ein Mutex (debugMutex) schuetzt den Puffer vor gleichzeitigem Zugriff durch beide Kerne.
    // src/include/debug.h
    
    // Primaere Debug-Ausgabe — mutex-geschuetzt, im PSRAM gepuffert, an USB geflusht
    debugf("Boot-Stufe %d erreicht, PSRAM-Groesse = %d Bytes\n", stage, psramSize);
    
    // Einzelzeichen-/Zeichenketten-Ausgabe (ebenfalls mutex-geschuetzt)
    debug_putchar('.');
    debug_puts("Init abgeschlossen\n");
    Wichtige Eigenschaften:
    • Puffergroesse: 64KB (MAX_DEBUG_BUFFER_SIZE = 65536) im PSRAM.
    • Thread-sicher: mutex-geschuetzt fuer Multi-Core-Zugriff.
    • PSRAM-resident: der Puffer ueberlebt Watchdog-Resets (PSRAM behaelt Daten), aber der Puffer-Zeiger bei 0x117EF004 muss nach dem Reset erneut validiert werden.
    • Niemals von Core-1-Handlern oder Poll-Callbacks aufrufen. Die Mutex-Erfassung kann blockieren, was Bus-Timing-Jitter einfuehrt. Verwenden Sie plogf() fuer Boot-Pfad-Nachrichten oder senden Sie Debug-Anforderungen ueber die Intercore-Queue an Core 0.
    #### plogf() — PSRAM-Persistentes Boot-Protokoll
    plogf() schreibt in einen dedizierten 4KB-Bereich am Ende des 8MB-PSRAM (Adresse 0x117FF000). Im Gegensatz zu debugf() verwendet es keinen Mutex und ist fuer die Core-0-Boot-Pfad-Protokollierung gedacht — zum Erfassen von Nachrichten waehrend der kritischen fruehen Boot-Phasen, bevor USB fuer normale Debug-Ausgabe verfuegbar ist. Da PSRAM seinen Inhalt ueber Watchdog-Resets hinweg behaelt, ueberleben diese Nachrichten einen Absturz und koennen beim naechsten erfolgreichen Boot untersucht werden.
    // src/include/debug.h
    
    // PSRAM-persistente Protokollstruktur (bei 0x117FF000)
    #define PLOG_ADDR   0x117FF000
    #define PLOG_SIZE   3840          // 4KB minus 256 Bytes reserviert fuer Fehlerdiagnose
    #define PLOG_MAGIC  0x504C4F47    // "PLOG"
    
    typedef struct {
        uint32_t  magic;              // PLOG_MAGIC wenn Puffer gueltige Daten enthaelt
        uint32_t  len;                // Aktuelle Schreibposition in buf[]
        char      buf[PLOG_SIZE - 8]; // Zirkulaerer Textpuffer
    } t_PsramLog;
    
    // In persistentes Protokoll schreiben (kein Mutex — nur Core-0-Boot-Pfad)
    plogf("FSPI init: DMA TX=%d RX=%d\n", gDmaTx, gDmaRx);
    
    // Erfasstes Protokoll beim naechsten erfolgreichen Boot ausgeben (aus Hauptschleife aufgerufen)
    dump_plog();  // Gibt plog-Inhalt ueber debugf() aus, loescht dann den Puffer
    Typisches Verwendungsmuster: Waehrend des Boots rufen Sie plogf() an jedem kritischen Meilenstein auf (PSRAM-Init, SPI-Handshake, Config-Parsing). Wenn der Watchdog ausloest, behaelt der Plog-Puffer alle Nachrichten bis zum Haengepunkt. Beim naechsten erfolgreichen Boot gibt dump_plog() die erfassten Nachrichten aus bevor der Puffer geloescht wird, was eine klare Ablaufverfolgung dessen liefert, was vor dem Absturz passiert ist.
    #### Die richtige Debug-Ausgabe waehlen
    Makro Speicherort Mutex Ueberlebt WDT-Reset Sicher von Core 1 Anwendungsfall
    debugf() PSRAM (64KB-Puffer) Ja Pufferinhalt ja; Zeiger muss erneut validiert werden Nein — wird blockieren Allgemeine Core-0-Debug-Ausgabe
    plogf() PSRAM (4KB am Ende) Nein Ja Nein — nur Core 0 Boot-Pfad-Protokollierung vor USB
    SWD + GDB Hardware-Probe N/A N/A Ja (pro-Core-Ports) Live-Debugging, Breakpoints, Inspektion
    -------------------------------------------------------------------------------------------------------- ## Watchdog und Boot-Fortschrittsverfolgung
    Der picoZ80 arbeitet in einer anspruchsvollen Umgebung: ein Dual-Core-RP2350, der ueber SPI mit einem ESP32 kommuniziert, eine taktgenaue Z80-Busschnittstelle ueber PIO bedient und gleichzeitig 8MB externes PSRAM verwaltet. Ein Haenger an irgendeinem Punkt waehrend des Boots — PSRAM-Initialisierung, SPI-Handshake, Config-Parsing oder Core-1-Start — wuerde die Platine ohne diagnostische Ausgabe nicht ansprechbar machen. Der Hardware-Watchdog-Timer und das Boot-Fortschrittsverfolgungssystem wurden entwickelt, um solche Ausfaelle wiederherstellbar und diagnostizierbar zu machen.
    #### Watchdog-Timer
    Der RP2350-Hardware-Watchdog wird frueh in main() mit einem 30-Sekunden-Timeout aktiviert:
    // src/model/BaseZ80/main.c
    watchdog_enable(30000, true);  // 30s Timeout, Pause-bei-Debug aktiviert
    watchdog_update() wird an jedem Boot-Meilenstein und waehrend der gesamten Hauptschleife aufgerufen. Kritische lang laufende Operationen (Floppy-Disk-Image-Laden mit Wiederholungslogik, DMA-Transfers, ESP32-SPI-Handshake) enthalten explizite Watchdog-Kicks, um falsche Resets waehrend legitimer langsamer Operationen zu verhindern:
    // Floppy-Disk-Laden mit Wiederholungslogik — Watchdog zwischen Versuchen treten
    for (int attempt = 0; attempt < 10; attempt++)
    {
        watchdog_update();
        bytesXfer = ESP_readFloppyDiskFile(filename, ..., diskNo);
        if (bytesXfer > 0) break;
        watchdog_update();
        sleep_ms(500);
    }
    
    // QD-Diskwechsel — Watchdog treten waehrend Warten auf Core-1-Hold-Bestaetigung
    z80CPU->hold = true;
    for (int w = 3000; !z80CPU->holdAck && w > 0; w--)
    {
        sleep_ms(1);
        if ((w % 1000) == 0) watchdog_update();
    }
    #### Watchdog-Scratch-Register
    Der RP2350 bietet acht 32-Bit-Scratch-Register im Watchdog-Hardwareblock, die Watchdog-Resets ueberleben, aber bei einem Power-On-Reset geloescht werden. Die picoZ80-Firmware verwendet fuenf davon, um eine vollstaendige Boot-Diagnosehistorie zu pflegen:
    // src/model/BaseZ80/main.c
    
    // Scratch-Register-Zuordnung
    #define BOOTP_SCR_MAGIC    5   // Magischer Marker: 0xB00710BE
    #define BOOTP_SCR_STAGE    6   // Aktueller Boot-Stufen-Code
    #define BOOTP_SCR_RSTCAUS  7   // Reset-Ursache von der Hardware
    // scratch[0-3] = Boot-Versuchshistorie (FIFO, neuester in [3])
    // scratch[4]   = SPI-Diagnosezaehler (gepacktes Bitfeld)
    
    #define BOOTP_MAGIC  0xB00710BE  // "BOOT-PROBE" Marker
    
    // Inline-Funktion zum Aufzeichnen der aktuellen Boot-Stufe
    static inline void bootStage(uint32_t stage)
    {
        watchdog_hw->scratch[BOOTP_SCR_STAGE] = stage;
    }
    
    // Verwendung waehrend der Boot-Sequenz:
    bootStage(BOOTP_START);         // 0x01 — Einstiegspunkt
    // ... Takteinrichtung ...
    bootStage(BOOTP_CLK_SET);       // 0x02
    // ... PSRAM-Init ...
    bootStage(BOOTP_PSRAM_INIT);    // 0x03
    watchdog_update();
    bootStage(BOOTP_PSRAM_OK);      // 0x04
    // ... und so weiter bis BOOTP_MAIN_LOOP (0x10)
    Bei jedem Watchdog-Reset werden die aktuelle Stufe und Reset-Ursache in das Historien-FIFO (scratch[0–3]) verschoben, bevor sie ueberschrieben werden. Dies gibt Ihnen die letzten vier Reset-Versuche, wodurch es moeglich wird, einen einmaligen Fehler von einem sich wiederholenden Boot-Ausfall auf einer bestimmten Stufe zu unterscheiden.
    #### Boot-Stufen-Referenz
    Code Konstante Beschreibung
    0x01BOOTP_STARTEinstiegspunkt erreicht
    0x02BOOTP_CLK_SETSystemtakt konfiguriert (CPU-Freq, PSRAM-Freq, Spannung)
    0x03BOOTP_PSRAM_INITPSRAM-Initialisierung gestartet
    0x04BOOTP_PSRAM_OKPSRAM initialisiert und getestet
    0x05BOOTP_STDIO_INITUSB-stdio initialisiert
    0x06BOOTP_PIO_INITPIO-Zustandsmaschinen geladen und gestartet
    0x07BOOTP_Z80_INITZ80-CPU-Kontext erstellt
    0x08BOOTP_USB_INITUSB-Bridge initialisiert
    0x0ABOOTP_ESP_HS_SYNCESP32-SPI-Handshake-Synchronisation
    0x0BBOOTP_CORE1_LAUNCHCore 1 gestartet ueber multicore_launch_core1()
    0x0DBOOTP_FSPI_INITFSPI-Binaer-IPC initialisiert (DMA-Kanaele beansprucht)
    0x0EBOOTP_ESP_INITESP32-Kommunikationsschicht bereit
    0x10BOOTP_MAIN_LOOPHauptschleife betreten — Boot abgeschlossen
    0x11BOOTP_ML_POLL_USBHauptschleife: USB abfragen
    0x12BOOTP_ML_INTERCOREHauptschleife: Intercore-Befehle verarbeiten
    0x20BOOTP_IC_DEQUEUEIntercore: Anforderung aus Queue entnehmen
    0x21BOOTP_IC_FD_LOADIntercore: Floppy-Disk-Image laden
    0x22BOOTP_IC_QD_LOADIntercore: QuickDisk-Image laden
    0x23BOOTP_IC_RF_LOADIntercore: RAMFILE-Image laden
    0x24–0x27BOOTP_IC_FILE_*Intercore: Datei laden/schreiben/Antwort/fertig
    Debugging eines Watchdog-Resets: Schliessen Sie eine SWD-Probe an, halten Sie den RP2350 an und lesen Sie die Scratch-Register. Wenn scratch[5] == 0xB00710BE, enthalten die Register gueltige Boot-Fortschrittsdaten. Lesen Sie scratch[6] fuer den Stufen-Code. Wenn z.B. scratch[6] == 0x0A (BOOTP_ESP_HS_SYNC), hing die Firmware waehrend des ESP32-SPI-Handshakes — pruefen Sie, ob der ESP32 geflasht und betriebsbereit ist, und verifizieren Sie die SPI-Verdrahtung.
    -------------------------------------------------------------------------------------------------------- #### ICE-Debug-Shell (dbgsh.c)
    Die Debug-Shell (dbgsh.c / dbgsh.h) implementiert einen 49-Befehl-ICE-Debugger auf USB CDC Kanal 1. Sie laeuft auf Core 0 und kommuniziert mit Core 1 ueber gemeinsame Flags in der t_Z80CPU-Kontextstruktur:
    • cpu->hold / cpu->holdAck — Pause/Fortsetzen-Handshake zwischen Core-0-Shell und Core-1-Emulationsschleife.
    • cpu->dbgBpAddr[DBG_MAX_BP] — Breakpoint-Adress-Array (8 Slots, 0xFFFF = unbenutzt). Core 1 prueft vor jedem Opcode-Fetch.
    • cpu->dbgStepCount — Einzelschritt-Zaehler. Core 1 dekrementiert nach jedem Befehl und haelt automatisch bei Null an.
    • cpu->dbgTrace[DBG_TRACE_SZ] — 512-Eintrags-Ringpuffer, der [31:16]=PC, [15:8]=Opcode, [7:0]=F fuer jeden ausgefuehrten Befehl aufzeichnet.
    Wichtige Implementierungsmuster: Die Shell verwendet dbg_sprintf() (ein leichtgewichtiges, RAM-residentes printf), um XIP-Flash-Stalls zu vermeiden, wenn von Core 0 gedruckt wird waehrend Core 1 aktiv PSRAM verwendet. Physischer Speicher- und I/O-Zugriff wird ueber Z80CPU_readPhysicalMem() / Z80CPU_writePhysicalMem() / Z80CPU_readPhysicalIO() / Z80CPU_writePhysicalIO() durchgefuehrt, die echte Z80-Buszyklen ueber die PIO-Zustandsmaschinen ausfuehren.
    Debug-Hook-System: Mehrere Befehle (mmutrace, ipl) verwenden einen pro-Treiber-Callback-Mechanismus — jeder Persona-Treiber kann seinen eigenen Trace- oder Reset-Handler registrieren, sodass die Debug-Shell-Ausgabe sich an den aktiven Maschinenkontext anpasst, ohne maschinenspezifische Logik in dbgsh.c hart zu kodieren.
    -------------------------------------------------------------------------------------------------------- ## Fehlerbehandler und PSRAM-Diagnose
    Die Firmware installiert Cortex-M33-Fehlerbehandler, die einen vollstaendigen Diagnose-Snapshot ins PSRAM speichern, bevor sie dem Watchdog erlauben, das System zurueckzusetzen. Dies bietet Post-Mortem-Analysefaehigkeiten ohne Bedarf an einer Live-Debugger-Sitzung — wesentlich fuer die Diagnose intermittierender Fehler in einem Multi-Core-Echtzeitsystem.
    #### PSRAM-Fehlerdiagnose-Struktur
    Die letzten 256 Bytes des 8MB-PSRAM (Adresse 0x117FFF00) sind fuer Fehlerdiagnose reserviert. Wenn ein Fehler auftritt, speichert der Handler einen vollstaendigen Register-Snapshot:
    // src/fault_handlers.c
    
    #define PSRAM_DIAG_ADDR   0x117FFF00   // Letzte 256 Bytes des 8MB-PSRAM
    #define PSRAM_DIAG_MAGIC  0xFA017000   // "FAULT" Marker
    
    typedef struct {
        uint32_t  magic;       // PSRAM_DIAG_MAGIC wenn gueltig
        uint32_t  faultType;   // 1=Hard, 2=MemManage, 3=BusFault, 4=UsageFault
        uint32_t  pc;          // Programmzaehler beim Fehler
        uint32_t  lr;          // Link-Register (Ruecksprungadresse)
        uint32_t  sp;          // Stack-Zeiger
        uint32_t  r0, r1, r2, r3, r12;  // Allgemeine Register
        uint32_t  psr;         // Program Status Register
        uint32_t  cfsr;        // Configurable Fault Status Register
        uint32_t  hfsr;        // Hard Fault Status Register
        uint32_t  bfar;        // Bus Fault Address Register
        uint32_t  mmfar;       // Memory Management Fault Address Register
        uint32_t  coreId;      // Welcher Kern den Fehler hatte (0 oder 1)
    } t_PsramFaultDiag;
    #### Fehlerbehandler-Implementierung
    Jeder Fehlertyp (Hard Fault, Memory Management Fault, Bus Fault, Usage Fault) hat einen Assembly-Wrapper, der den Main Stack Pointer (MSP) oder Process Stack Pointer (PSP) extrahiert — je nachdem welcher zum Zeitpunkt des Fehlers aktiv war — und ihn an einen gemeinsamen C-Handler uebergibt. Der C-Handler:
    1. Schreibt die Diagnosestruktur ins PSRAM bei 0x117FFF00 mit dem entsprechenden Magic-Marker und Fehlertyp.
    2. Gibt den Register-Dump und die Fehlerdetails ueber debugf() aus (wenn USB verfuegbar ist).
    3. Tritt in eine Endlosschleife ein (while(1)), wodurch der Watchdog einen Reset ausloesen kann.
    Beim naechsten erfolgreichen Boot prueft die Firmware 0x117FFF00 auf den PSRAM_DIAG_MAGIC-Marker. Falls vorhanden, gibt sie die gespeicherten Fehlerinformationen ueber debugf() aus und loescht den Marker, was eine vollstaendige Post-Mortem-Ablaufverfolgung liefert.
    // Interpretation der Fehlerdiagnose (von GDB oder debugf-Ausgabe):
    //
    // faultType=1 (Hard Fault):
    //   HFSR Bit 30 (FORCED) pruefen — zeigt eskalierten Fehler an.
    //   CFSR fuer den urspruenglichen Fehlertyp pruefen.
    //
    // faultType=3 (Bus Fault):
    //   CFSR Bits [15:8] fuer Bus-Fault-Status pruefen.
    //   Wenn BFARVALID (Bit 15), enthaelt BFAR die fehlverursachende Adresse.
    //   Haeufige Ursache: PSRAM-SPI-Konkurrenz zwischen Core 0 und Core 1.
    //
    // faultType=4 (Usage Fault):
    //   CFSR Bits [25:16] fuer Usage-Fault-Status pruefen.
    //   UNDEFINSTR = undefinierter Befehl (beschaedigter Code in Flash/PSRAM).
    //   DIVBYZERO  = Division durch Null (wenn aktiviert).
    //
    // PC-Wert: der Befehl der den Fehler verursacht hat.
    // LR-Wert: die Ruecksprungadresse (Aufrufer der fehlerhaften Funktion).
    #### PSRAM-Diagnose-Speicherkarte
    Das obere Ende des 8MB-PSRAM ist in drei Diagnosebereiche unterteilt:
    | Adressbereich | Groesse | Inhalt | |---------------|---------|--------| | `0x117EF004 – 0x117FEFFF` | 64KB | debugf()-Ausgabepuffer (fluechter Zeiger bei 0x117EF004) | | `0x117FF000 – 0x117FFEFF` | ~4KB | plogf() persistentes Boot-Protokoll | | `0x117FFF00 – 0x117FFFFF` | 256B | Fehlerdiagnose-Snapshot |
    Alle drei Bereiche ueberleben Watchdog-Resets, weil PSRAM seinen Inhalt behaelt solange die Stromversorgung aufrechterhalten wird. Bei einem Power-On-Reset sind die Inhalte undefiniert und die Firmware reinitialisiert sie durch Pruefen der Magic-Marker.
    -------------------------------------------------------------------------------------------------------- ## Binaeres IPC-Protokoll (FSPI v1.1)
    Der RP2350 kommuniziert mit dem ESP32 ueber eine 50MHz 4-Draht-SPI-Verbindung unter Verwendung eines binaeren IPC-Protokolls (Version 1.1). Dies ersetzt ein aelteres textbasiertes Protokoll durch ein strukturiertes binaeres Frame-Format, das CRC32-Integritaetspruefung, Burst-Sektortransfers und vorallozierte DMA-Kanaele fuer reduzierte Latenz und verbesserte Zuverlaessigkeit unterstuetzt.
    Die ESP32-Firmware unterstuetzt drei Netzwerkmodi, die zur Build-Zeit ausgewaehlt werden: Nur WiFi, WiFi+NCM (beide gleichzeitig) und Nur NCM. Vorgefertigte sdkconfig-Dateien werden fuer jeden Modus bereitgestellt (sdkconfig.mode_wifi_only, sdkconfig.mode_wifi_and_ncm, sdkconfig.mode_ncm_only). Im NCM-Modus praesentiert der ESP32 einen USB CDC-NCM Ethernet-Adapter mit eingebautem DHCP-Server (Standard-IP: 192.168.7.1), der den Zugang zur Weboberflaeche ohne WiFi-Hardware ermoeglicht. Der Nur-NCM-Modus ist erforderlich fuer Platinen, die ohne FCC/RED-Zertifizierung ausgeliefert werden.
    #### Frame-Struktur
    Jede IPC-Transaktion besteht aus einem festen 64-Byte-Header gefolgt von einem optionalen Payload und einem 4-Byte-CRC32-Trailer:
    // src/include/ipc_protocol.h
    
    typedef struct __attribute__((packed)) {
        uint8_t   frameType;              // 0:  IPCF_TYPE_COMMAND / RESPONSE / NOP
        uint8_t   command;                // 1:  IPCF_CMD_* Opcode
        uint8_t   status;                 // 2:  IPCF_STATUS_* (nur Antwort)
        uint8_t   seqNum;                 // 3:  Sequenznummer (Wiederholungserkennung)
        uint16_t  payloadLen;             // 4:  Payload-Bytes (Little-Endian)
        uint16_t  sectorCount;            // 6:  Sektoren im Burst (1–16)
        uint32_t  fileOffset;             // 8:  Byte-Offset in der Datei
        uint8_t   diskNo;                 // 12: Laufwerksnummer
        uint8_t   flags;                  // 13: IPCF_FLAG_*
        uint16_t  reserved;               // 14: Reserviert
        char      filename[48];           // 16: Null-terminierter Pfad (48 Bytes)
    } t_IpcFrameHdr;                      // Gesamt: 64 Bytes
    
    // Frame-Layout:
    // [64-Byte Header][0–8192 Bytes Payload][4-Byte CRC32]
    
    #define IPCF_HEADER_SIZE    64
    #define IPCF_MAX_SECTORS    16        // Max. Sektoren pro Burst
    #define IPCF_SECTOR_SIZE    512       // Bytes pro Sektor
    #define IPCF_CRC_SIZE       4         // CRC32-Trailer
    #define IPCF_MAX_PAYLOAD    (IPCF_MAX_SECTORS * IPCF_SECTOR_SIZE)  // 8192
    #define IPCF_MAX_FRAME_SIZE (IPCF_HEADER_SIZE + IPCF_MAX_PAYLOAD + IPCF_CRC_SIZE)  // 8260
    #### Befehls-Opcodes | Opcode | Name | Beschreibung | |--------|------|--------------| | `0x00` | `IPCF_CMD_NOP` | Keine Operation (Dummy-TX waehrend Vollduplex-Lesen) | | `0x01` | `IPCF_CMD_RDS` | Einzelnen 512-Byte-Sektor lesen | | `0x02` | `IPCF_CMD_WRS` | Einzelnen 512-Byte-Sektor schreiben | | `0x03` | `IPCF_CMD_RBURST` | Burst-Lesen: 1–16 Sektoren in einer SPI-Transaktion | | `0x04` | `IPCF_CMD_WBURST` | Burst-Schreiben: 1–16 Sektoren | | `0x05` | `IPCF_CMD_RFILE` | Ganze Datei lesen (aufgeteilt wenn max. Payload ueberschritten) | | `0x06` | `IPCF_CMD_WFILE` | Ganze Datei schreiben | | `0x07` | `IPCF_CMD_INF` | RP2350-Versions-/Partitionsinfo an ESP32 uebertragen | | `0x08` | `IPCF_CMD_RFD` | Floppy-Disk-Image-Datei lesen | | `0x09` | `IPCF_CMD_RQD` | QuickDisk-Image-Datei lesen | | `0x0A` | `IPCF_CMD_RRF` | RAMFILE-Backup-Image lesen |
    #### DMA und Integritaet
    • Vorallozierte DMA-Kanaele: TX- und RX-DMA-Kanaele (gDmaTx, gDmaRx) werden einmalig bei der FSPI-Initialisierung beansprucht und nie freigegeben. Dies eliminiert den Claim/Unclaim-Overhead pro Transfer und verhindert DMA-Kanal-Erschoepfungs-Wettlaufbedingungen, die auftreten koennen wenn Core 1 um den QMI-Bus konkurriert.
    • RX-Prioritaetserhoehung: Der RX-DMA-Kanal wird auf HOHE PRIORITAET gesetzt, um SPI-RX-FIFO-Ueberlauf zu verhindern. Core 1s PSRAM-Zugriffe ueber den QMI-Bus koennen den AHB-Bus-Fabric blockieren, und wenn der RX-DMA-Kanal auf normaler Prioritaet ist, koennen seine Transfers lange genug verzoegert werden, dass der SPI-FIFO ueberlaeuft.
    • CRC32-Integritaet: Jeder Frame ist durch einen Standard-IEEE-802.3-CRC32 geschuetzt (Polynom 0xEDB88320, reflektiert). Der ESP32 verwendet esp_rom_crc32_le(), das das gleiche Ergebnis liefert. Bei CRC-Nichtuebereinst immung wird der Frame erneut versucht, wobei der Sequenzzaehler (seqNum) zur Duplikaterkennung verwendet wird.
    • Watchdog-Integration: DMA-Wartezeiten beinhalten ein 2-Sekunden-Timeout mit Watchdog-Kicks jede Sekunde. Wenn ein DMA-Transfer haengt (z.B. aufgrund eines ESP32-Resets), werden die Kanaele abgebrochen, CS wird freigegeben und der Boot faehrt fort.
    -------------------------------------------------------------------------------------------------------- ## Referenzseiten | Ressource | Link | |-----------|------| | picoZ80-Projektseite | [/picoz80/](/picoz80/) | | picoZ80 Benutzerhandbuch | [/de/picoz80-usermanual/](/de/picoz80-usermanual/) | | picoZ80 Technischer Leitfaden | [/de/picoz80-technicalguide/](/de/picoz80-technicalguide/) | | pico6502-Projektseite | [/pico6502/](/pico6502/) | | RP2350 Datenblatt | [datasheets.raspberrypi.com](https://datasheets.raspberrypi.com/rp2350/rp2350-datasheet.pdf) | | Pico SDK Multicore API | [raspberrypi.github.io/pico-sdk-doxygen](https://raspberrypi.github.io/pico-sdk-doxygen/group__multicore.html) | | Zeta Z80 Bibliothek | [github.com/superzazu/z80](https://github.com/superzazu/z80) | | Zilog Z80 CPU Benutzerhandbuch | [zilog.com](https://www.zilog.com/docs/z80/um0080.pdf) | | cJSON Bibliothek | [github.com/DaveGamble/cJSON](https://github.com/DaveGamble/cJSON) | --- ## Hinweis zur Funkregulierung
    Dieses Geraet enthaelt ein ESP32-S3-PICO-1 Funkmodul, das im 2,4 GHz ISM-Band sendet, wodurch es weltweit als absichtlicher Strahler unter Hochfrequenzvorschriften faellt (einschliesslich FCC Part 15 Subpart C in den Vereinigten Staaten und der Funkanlagenrichtlinie 2014/53/EU in der Europaeischen Union).
    Obwohl das ESP32-S3-PICO-1-Modul selbst bereits ueber bestehende regulatorische Zertifizierungen verfuegt (FCC, CE und andere), erstrecken sich diese Modul-Level-Zertifizierungen nicht automatisch auf ein Endprodukt, das das Modul integriert. Die Ausnahme fuer vorzertifizierte Module erlaubt es einzelnen Hobby-Anwendern, eine begrenzte Anzahl von Geraeten fuer den persoenlichen, experimentellen oder Bildungszweck zu bauen, ohne eine separate Geraetezulassung einzuholen.
    Wichtige Einschraenkungen
    • Zusammengebaute Geraete duerfen nicht verkauft, zum Verkauf angeboten, verschenkt oder anderweitig an Dritte verteilt werden, es sei denn, das fertige Produkt wurde unabhaengig getestet und hat eine eigene Geraetezulassung erhalten (z.B. FCC-ID, CE-Kennzeichnung mit Bewertung durch eine Benannte Stelle) in der jeweiligen Gerichtsbarkeit.
    • Das Bauen dieses Projekts fuer den persoenlichen Gebrauch in begrenzten Stueckzahlen ist unter Hobby- und Experimentier-Bestimmungen generell erlaubt (z.B. FCC § 15.23), vorausgesetzt das Geraet verursacht keine schaedlichen Stoerungen.
    • Regulatorische Anforderungen variieren je nach Land. Erbauer ausserhalb der Vereinigten Staaten sollten ihre nationale Hochfrequenzbehoerde fuer die geltenden Regeln konsultieren.
    Verantwortung des Erbauers
    Es liegt in der alleinigen Verantwortung des Erbauers sicherzustellen, dass jedes aus diesen Designs gebaute Geraet alle geltenden Hochfrequenzvorschriften in seiner Gerichtsbarkeit einfuellt. Der Autor stellt diese Designs fuer den persoenlichen, Bildungs- und Hobby-Gebrauch zur Verfuegung und macht keine Zusicherung, dass ein aus ihnen gebautes Geraet die regulatorischen Anforderungen fuer den kommerziellen Vertrieb erfuellt.