picoZ80 Entwicklerhandbuch
picoZ80 Entwicklerhandbuch
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
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
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.
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);
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.
z80_data) Ablauf fuer einen Lesezyklus:
- Setzt IRQ 1 und wartet — "bereit fuer Richtung/Daten".
- Core 1 loescht IRQ 1 nachdem Pin-Richtung (Eingabemodus) und ein Dummy-Datenbyte geschoben wurden.
- Die SM setzt Pin-Richtungen auf Eingang (Tristate), wodurch der Host-Speicher D0–D7 treiben kann.
- Die SM wartet auf
wait 0 irq 0bis die naechste Adressaenderung das Zyklusende anzeigt. - Die SM setzt die Pin-Richtungen in einen definierten Zustand zurueck.
Wichtige Typen und Datenstrukturen
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
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
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
_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)
// 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)
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;
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)
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;
- 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 wiecpu->_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
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);
- cpu — Zeiger auf den Z80CPU-Kontext. Gibt Ihnen Zugriff auf
_membankPtr[],_z80PSRAMund 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). - read —
truewenn dies ein Lesezyklus ist (Z80 liest);falsewenn 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).
- 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.
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
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
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;
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.
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
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);
}
}
}
- 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-TransaktionZ80CPU_readMem(),Z80CPU_writeMem(),Z80CPU_readIO()undZ80CPU_writeIO()als Callbacks auf.
Speicher-Lese-Dispatch
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);
}
}
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
- Top-Level-Treiber (auch Personas genannt) — registriert in
virtualFuncMap[]inZ80CPU.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
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]);
// 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
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.
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.
{"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(...).
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) |
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.
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 (0x0000–0x003F → Vektoren-Block, 0x0040–0x01FF → Anfang der TPA).
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
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)
"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
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 |
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
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)
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.
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.
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 alscpu->_Z80.inta, der Interrupt-Acknowledge-Callback.MZ800_retiHandler()— installiert alscpu->_Z80.reti, der RETI-Callback.
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
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]);
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
// 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,
};
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 vonZ80CPU_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()
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
}
_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()
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;
}
Einen neuen Treiber schreiben — Schritt fuer Schritt
Schritt 1 — Quelldateien erstellen
// 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
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
)
target_include_directories-Aufruf erforderlich ist.
Schritt 3 — Header in Z80CPU.c einbinden
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
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},
};
"mydriver", "MyDriver" und "MYDRIVER" im JSON alle mit diesem Eintrag uebereinstimmen.
Schritt 5 — Treiber zu config.json hinzufuegen
"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
BaseZ80(pZ80-BaseZ80) — universelle Binaerdatei mit allen Treibern (Sharp + Amstrad + Tatung). DefiniertINCLUDE_SHARP_DRIVERS,INCLUDE_AMSTRAD_DRIVERSundINCLUDE_TATUNG_DRIVERS.SharpZ80(pZ80-SharpZ80) — nur Sharp-MZ-Treiber. DefiniertINCLUDE_SHARP_DRIVERS. Erzeugt eine kleinere Firmware-Binaerdatei.AmstradZ80(pZ80-AmstradZ80) — nur Amstrad-PCW-Treiber. DefiniertINCLUDE_AMSTRAD_DRIVERSundTARGET_MODEL_AMSTRAD. Erzeugt eine kleinere Firmware-Binaerdatei.TatungZ80(pZ80-TatungZ80) — nur Tatung-Einstein-Treiber. DefiniertINCLUDE_TATUNG_DRIVERSundTARGET_MODEL_TATUNG. Erzeugt eine kleinere Firmware-Binaerdatei.
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
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
CONFIG_IF_WIFI_ENABLED— aktiviert WiFi-Funk und AP/Client-Code.CONFIG_IF_USB_NCM_ENABLED— aktiviert USB-NCM-Netzwerkinterface und DHCP-Server.
idf.py menuconfig) oder die oben aufgefuehrten vorgefertigten sdkconfig-Dateien gesetzt.
Speicher-Hook-Muster im Detail
Muster 1 — Rein virtuelles Geraet (FUNC-Block)
// 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
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
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)
// 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
// 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
Erforderliche Funktionen fuer ein Sub-Interface
// 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
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},
};
"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
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)
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 globaleng_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},
"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
Die Intercore-Queue verwenden
- 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).
- Der Handler setzt ein Zustands-Flag (z.B.
diskState.pendingRead = true) und kehrt sofort zurueck — er fuehrt die I/O nicht aus. - Ihr
poll_ptr(ebenfalls auf Core 1, wird alle ~2048 Zyklen aufgerufen) prueft das Zustands-Flag und schiebt, falls gesetzt, eine Anforderungsnachricht incpu->requestQueue. - 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. - Ihr
task_ptrwird aufgerufen (auf Core 1) mit dem Task-Ergebnis. Er kopiert die Sektordaten ins PSRAM und loescht das Pending-Flag.
</ol>
- Blockieren in einem Handler. Der haeufigste Fehler. Jeder Aufruf von
debugf,sleep_ms,fopenoder 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 Sieplogf()stattdebugf()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[]- oderioPtr[]-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 inZ80CPU_getVirtualFunc()hinzu, wenn Ihr Treiber nicht initialisiert wird —debugfist 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.
- 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
0x117EF004muss 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. 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]=Ffuer jeden ausgefuehrten Befehl aufzeichnet.- Schreibt die Diagnosestruktur ins PSRAM bei
0x117FFF00mit dem entsprechenden Magic-Marker und Fehlertyp. - Gibt den Register-Dump und die Fehlerdetails ueber
debugf()aus (wenn USB verfuegbar ist). - Tritt in eine Endlosschleife ein (
while(1)), wodurch der Watchdog einen Reset ausloesen kann. - 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 verwendetesp_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.
- 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.
// 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
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");
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
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.
| 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 |
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
// 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)
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.
| Code | Konstante | Beschreibung |
|---|---|---|
0x01 | BOOTP_START | Einstiegspunkt erreicht |
0x02 | BOOTP_CLK_SET | Systemtakt konfiguriert (CPU-Freq, PSRAM-Freq, Spannung) |
0x03 | BOOTP_PSRAM_INIT | PSRAM-Initialisierung gestartet |
0x04 | BOOTP_PSRAM_OK | PSRAM initialisiert und getestet |
0x05 | BOOTP_STDIO_INIT | USB-stdio initialisiert |
0x06 | BOOTP_PIO_INIT | PIO-Zustandsmaschinen geladen und gestartet |
0x07 | BOOTP_Z80_INIT | Z80-CPU-Kontext erstellt |
0x08 | BOOTP_USB_INIT | USB-Bridge initialisiert |
0x0A | BOOTP_ESP_HS_SYNC | ESP32-SPI-Handshake-Synchronisation |
0x0B | BOOTP_CORE1_LAUNCH | Core 1 gestartet ueber multicore_launch_core1() |
0x0D | BOOTP_FSPI_INIT | FSPI-Binaer-IPC initialisiert (DMA-Kanaele beansprucht) |
0x0E | BOOTP_ESP_INIT | ESP32-Kommunikationsschicht bereit |
0x10 | BOOTP_MAIN_LOOP | Hauptschleife betreten — Boot abgeschlossen |
0x11 | BOOTP_ML_POLL_USB | Hauptschleife: USB abfragen |
0x12 | BOOTP_ML_INTERCORE | Hauptschleife: Intercore-Befehle verarbeiten |
0x20 | BOOTP_IC_DEQUEUE | Intercore: Anforderung aus Queue entnehmen |
0x21 | BOOTP_IC_FD_LOAD | Intercore: Floppy-Disk-Image laden |
0x22 | BOOTP_IC_QD_LOAD | Intercore: QuickDisk-Image laden |
0x23 | BOOTP_IC_RF_LOAD | Intercore: RAMFILE-Image laden |
0x24–0x27 | BOOTP_IC_FILE_* | Intercore: Datei laden/schreiben/Antwort/fertig |
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.
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:
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.
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
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
debugf()-Ausgabepuffer (fluechter Zeiger bei 0x117EF004) |
| `0x117FF000 – 0x117FFEFF` | ~4KB | plogf() persistentes Boot-Protokoll |
| `0x117FFF00 – 0x117FFFFF` | 256B | Fehlerdiagnose-Snapshot |
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.
// 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
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.