picoZ80 Guida dello Sviluppatore

picoZ80 Guida dello Sviluppatore

Questa guida rappresenta un riferimento completo per gli sviluppatori che desiderano comprendere gli aspetti interni del firmware picoZ80 e scrivere i propri driver per periferiche. Copre l'intera architettura software, dal loop di dispatch del bus su Core 1 alla registrazione dei driver, all'installazione degli hook di memoria e alla virtualizzazione dell'I/O. Il driver Sharp MZ-700 (MZ700.c) viene utilizzato come esempio pratico concreto in tutta la guida.
Non e' richiesta alcuna esperienza precedente con il codebase picoZ80. Ogni concetto viene spiegato partendo dai principi fondamentali e poi mostrato nel codice sorgente reale. Al termine di questa guida sarete in grado di scrivere un driver completo da zero, aggiungerlo al sistema di compilazione, registrarlo nel framework e configurarlo tramite JSON.
Per l'architettura hardware, i dettagli dell'interfaccia bus PIO e il riferimento alla configurazione JSON, consultare la Guida Tecnica picoZ80. Per la configurazione utente, consultare il Manuale Utente picoZ80.

Struttura del Codice

Tutto il codice sorgente si trova sotto projects/tzpuPico/ all'interno della radice del repository (la posizione esatta del checkout dipendera' dal vostro sistema). La struttura seguente mostra i file rilevanti per lo sviluppo dei driver:
tzpuPico/
├── CMakeLists.txt                          Top-level build file
├── src/
│   ├── CMakeLists.txt                      Source-level build file — add new driver files here
│   ├── Z80CPU.c                            Main Z80 emulation, bus dispatch, driver framework
│   ├── Z80CPU.h                            (legacy — included via include/)
│   ├── M6502CPU.c                          6502 parallel (same architecture)
│   ├── FSPI.c / FSPI.h                     Flash SPI interface
│   ├── ESP.c / ESP.h                       ESP32 communication layer
│   ├── psram.c / psram.h                   PSRAM allocation and management
│   ├── cJSON.c / cJSON.h                   JSON parser for config.json
│   ├── include/
│   │   ├── Z80CPU.h                        *** KEY FILE: all type definitions and macros ***
│   │   ├── dbgsh.h                         Debug shell (ICE debugger) header
│   │   └── drivers/
│   │       ├── Z80SIO.h                    Zilog Z80 SIO/2 emulation header (shared)
│   │       ├── Sharp/
│   │       │   ├── MZ.h                    MZ-series common constants
│   │       │   ├── MZ700.h                 MZ-700 driver header
│   │       │   ├── MZ80A.h                 MZ-80A driver header
│   │       │   ├── MZ2000.h                MZ-2000 driver header
│   │       │   ├── MZ2200.h                MZ-2200 driver header
│   │       │   ├── MZ80B.h                 MZ-80B driver header
│   │       │   ├── MZ2500.h                MZ-2500 driver header
│   │       │   ├── MZ800.h                 MZ-800 driver header
│   │       │   ├── MZ1500.h                MZ-1500 driver header
│   │       │   ├── MZ8BIO3.h               MZ-8BIO3 RS-232C card (Z80 SIO) header
│   │       │   ├── MZ1E24.h                MZ-1E24 RS-232C card (Z80 SIO) header
│   │       │   ├── RFS.h / TZFS.h          Filing system headers
│   │       │   ├── 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 hard disk protocol header
│   │       │   ├── MZ1E30.h               MZ-1E30 SASI controller header
│   │       │   └── MZ8BFI.h                MZ-8BFI floppy interface header
│   │       ├── Amstrad/
│   │       │   ├── PCW9512.h               Amstrad PCW-9512 driver header
│   │       │   └── uPD765.h                NEC uPD765 FDC header
│   │       └── Tatung/
│   │           ├── EinsteinTC01.h           Tatung Einstein TC-01 driver header
│   │           ├── EinsteinFDC.h            Einstein FDC sub-interface header
│   │           └── WD1770.h                 WD1770 FDC header
│   ├── dbgsh.c                             Debug shell (ICE debugger) — USB CDC Channel 1
│   ├── PIT8253.c                           Intel 8253 PIT emulation module
│   ├── PPI8255.c                           Intel 8255 PPI emulation module
│   ├── drivers/
│   │   ├── Z80SIO.c                        Zilog Z80 SIO/2 emulation (shared by RS-232C cards)
│   │   └── Sharp/
│   │       ├── MZ700.c                     *** EXAMPLE DRIVER ***
│   │       ├── MZ80A.c                     MZ-80A persona driver
│   │       ├── MZ2000.c                    MZ-2000 persona driver
│   │       ├── MZ2200.c                    MZ-2200 persona driver
│   │       ├── MZ80B.c                     MZ-80B persona driver
│   │       ├── MZ2500.c                    MZ-2500 persona driver
│   │       ├── MZ800.c                     MZ-800 persona driver (dual-mode MZ-700/MZ-800)
│   │       ├── MZ1500.c                    MZ-1500 persona driver
│   │       ├── MZ8BIO3.c                   MZ-8BIO3 RS-232C card (shared SIOCard_* impl)
│   │       ├── MZ1E24.c                    MZ-1E24 RS-232C card (SIOCard_* wrapper)
│   │       ├── RFS.c                       ROM Filing System driver
│   │       ├── TZFS.c                      TranZPUter Filing System driver
│   │       ├── WD1773.c                    WD1773 floppy controller driver
│   │       ├── QDDrive.c                   QuickDisk drive driver
│   │       ├── MZ-1E05.c / MZ-1E14.c / MZ-1E19.c   Peripheral interface cards
│   │       ├── MZ8BFI.c                    MZ-8BFI / E0054PA floppy interface (MZ-2000)
│   │       ├── MZ-1R12.c / MZ-1R18.c      RAM expansion cards
│   │       ├── MZ-1R23.c                   MZ-1R23 Kanji ROM / MZ-1R24 Dictionary ROM
│   │       ├── MZ-1R37.c                   MZ-1R37 640KB EMM
│   │       ├── PIO-3034.c                  IO DATA PIO-3034 320KB EMM
│   │       ├── Celestite.c                 Celestite LAN / Memory composite board
│   │       ├── SASI.c                      SASI hard disk protocol driver
│   │       └── MZ1E30.c                    MZ-1E30 SASI hard disk controller
│   ├── drivers/
│   │   ├── Amstrad/
│   │   │   ├── PCW9512.c                   *** AMSTRAD PCW-9512 DRIVER ***
│   │   │   └── uPD765.c                    NEC uPD765 FDC driver (CPC DSK format)
│   │   └── Tatung/
│   │       ├── EinsteinTC01.c              Tatung Einstein TC-01 persona driver
│   │       ├── EinsteinFDC.c               Einstein FDC sub-interface (2 drives, DSK/D88)
│   │       └── WD1770.c                    WD1770 FDC emulation (reusable module, separate from WD1773.c)
│   │   └── Other/
│   │       └── Open.c                      OpenZ80 vanilla / experimenter persona driver
│   └── model/
│       ├── BaseZ80/                        All drivers (Sharp + Amstrad)
│       │   ├── CMakeLists.txt              Per-model build targets
│       │   ├── main.c                      Entry point (Core 0 + Core 1 launch)
│       │   ├── main_memmap_partition_1.ld  Linker script for Slot 1
│       │   └── main_memmap_partition_2.ld  Linker script for Slot 2
│       ├── SharpZ80/                       Sharp MZ drivers only (smaller binary)
│       │   ├── CMakeLists.txt
│       │   ├── main.c
│       │   ├── main_memmap_partition_1.ld
│       │   └── main_memmap_partition_2.ld
│       ├── AmstradZ80/                     Amstrad PCW drivers only (smaller binary)
│       │   ├── CMakeLists.txt
│       │   ├── main.c
│       │   ├── main_memmap_partition_1.ld
│       │   └── main_memmap_partition_2.ld
│       ├── TatungZ80/                      Tatung Einstein drivers only (smaller binary)
│       │   ├── CMakeLists.txt
│       │   ├── main.c
│       │   ├── main_memmap_partition_1.ld
│       │   └── main_memmap_partition_2.ld
│       ├── OpenZ80/                         OpenZ80 experimenter persona only (machine-agnostic cards)
│       │   ├── CMakeLists.txt
│       │   ├── main.c
│       │   ├── main_memmap_partition_1.ld
│       │   └── main_memmap_partition_2.ld
│       └── Bootloader/
└── tools/
    └── NetFileServer/
        └── netfs.py                       Network file server for MZF files over TCP

Interfaccia Bus PIO — Come il Codice C Controlla le State Machine

L'interfaccia bus Z80 viene eseguita interamente nell'hardware PIO (z80.pio), ma il codice C su Core 1 orchestra quale ciclo di bus viene eseguito e quando. Comprendere questa interazione e' importante per chiunque stia facendo debug del timing del bus o aggiungendo nuovi tipi di ciclo.
Il meccanismo centrale e' out exec, 16 — un'istruzione PIO che preleva un valore a 16 bit dal TX FIFO e lo esegue come istruzione PIO. La state machine z80_cycle (PIO 0 SM 2) utilizza questo meccanismo in un loop serrato:
// z80_cycle SM program (PIO 0 SM 2):
//
// start_cycle:
//     wait 0 irq 6          ; Stall if BUSREQ active
//     irq set 0             ; Signal "ready"
//     wait 0 irq 0          ; Wait for C code to load address and clear IRQ 0
//     wait 1 gpio CLK       ; Sync to T1 rising edge
// cycle_exec:
//     out exec, 16           ; Pull instruction from FIFO, execute it
//     jmp cycle_exec         ; Repeat
//
// The C code pushes a sequence of encoded 16-bit PIO instructions
// into the cycle SM's TX FIFO. The sequence controls every aspect
// of the bus cycle: which control signals to assert/deassert, when
// to wait for clock edges, and when to sample data.
// The final instruction in every sequence is a JMP back to start_cycle.
Il codice C pre-codifica le sequenze di istruzioni per ogni tipo di ciclo al momento della compilazione. Queste vengono memorizzate come array di valori uint16_t. A runtime, il loop principale di Core 1 invia la sequenza appropriata nel FIFO in base alla transazione bus corrente:
// Simplified Core 1 flow for a memory read cycle:

// 1. z80_cycle SM sets IRQ 0 — it is ready for a new cycle.
//    z80_addr SM sets IRQ 0 — it is ready for an address.

// 2. Core 1 resolves the address and prepares the bus transaction:
pio_sm_put(pio0, SM_ADDR, (pindirs_16 << 16) | address);  // Push to z80_addr
pio_interrupt_clear(pio0, 0);                                // Clear IRQ 0 → addr SM runs

// 3. Core 1 pushes the cycle-type instruction sequence:
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 check, T3, data sample ...
pio_sm_put(pio0, SM_CYCLE, encoded_jmp_start);       // JMP start_cycle (end)

// 4. Core 1 reads the data byte from the RX FIFO:
uint8_t data = pio_sm_get(pio0, SM_DATA);
Coordinamento tra le state machine: La SM dell'indirizzo (z80_addr) e la SM dei dati (z80_data) utilizzano ciascuna il proprio flag IRQ (rispettivamente IRQ 0 e IRQ 1) per implementare un handshake produttore/consumatore:
  • La SM imposta il suo flag IRQ e si blocca su wait 0 irq N — "sono pronta, inviatemi i dati".
  • Core 1 invia l'indirizzo o i dati nel TX FIFO della SM, quindi cancella il flag IRQ.
  • La SM si risveglia, preleva i dati dal FIFO e pilota i pin.
Questo handshake garantisce che i segnali del bus non vengano mai pilotati prima che i valori corretti siano caricati, e che Core 1 non sovrascriva mai le voci del FIFO che la SM non ha ancora consumato.
Flusso della SM dati (z80_data) per un ciclo di lettura:
  1. Imposta IRQ 1 e attende — "pronta per direzione/dati".
  2. Core 1 cancella IRQ 1 dopo aver inviato la direzione dei pin (modalita' input) e un byte dati fittizio.
  3. La SM imposta le direzioni dei pin a input (tristate), permettendo alla memoria host di pilotare D0–D7.
  4. La SM attende su wait 0 irq 0 fino a quando il cambio dell'indirizzo successivo indica che il ciclo sta terminando.
  5. La SM riporta le direzioni dei pin a uno stato noto.
Per un ciclo di scrittura, Core 1 invia le direzioni dei pin in output e il byte dati effettivo; la SM pilota D0–D7 con i dati.

Tipi e Strutture Dati Chiave

Prima di esaminare il framework driver, e' essenziale comprendere le strutture dati fondamentali definite in src/include/Z80CPU.h. Queste strutture vengono passate a ogni funzione del driver e sono il mezzo principale con cui un driver interagisce con il sistema di memoria e I/O.

Costanti di Tipo dei Blocchi di Memoria

Ogni blocco da 512 byte nello spazio di indirizzi Z80 a 64KB ha un tipo codificato negli 8 bit superiori della sua voce membankPtr. Il tipo indica al loop di dispatch di Core 1 come gestire le transazioni bus che ricadono in quel blocco.
// src/include/Z80CPU.h

#define MEMBANK_TYPE_UNKNOWN        0x00  // Uninitialised — should never appear at runtime
#define MEMBANK_TYPE_PHYSICAL       0x01  // Pass-through: RP2350 releases bus, host hardware responds
#define MEMBANK_TYPE_PHYSICAL_VRAM  0x02  // Pass-through with wait states for host video RAM
#define MEMBANK_TYPE_PHYSICAL_HW    0x04  // Pass-through for host I/O-mapped hardware registers
#define MEMBANK_TYPE_RAM            0x08  // Read/write — backed by PSRAM bank
#define MEMBANK_TYPE_VRAM           0x10  // PSRAM video RAM — writes also mirrored to physical VRAM
#define MEMBANK_TYPE_ROM            0x20  // Read-only — backed by PSRAM bank; writes silently ignored
#define MEMBANK_TYPE_FUNC           0x40  // Virtual device — every access calls a C function handler
#define MEMBANK_TYPE_PTR            0x80  // Indirection — each byte redirects to another address
Il tipo determina cosa accade in Z80CPU_readMem() e Z80CPU_writeMem():
  • PHYSICAL / PHYSICAL_VRAM / PHYSICAL_HW — l'RP2350 non intercetta la transazione bus; l'hardware reale sulla scheda host risponde. Usare questo per qualsiasi regione in cui i chip dell'host (ROM, RAM, hardware video) devono rimanere in controllo.
  • RAM — letture e scritture vanno a un banco da 64KB nella PSRAM da 8MB. Se una funzione memioPtr e' installata per l'indirizzo specifico, quella funzione viene chiamata al posto di (per FUNC) o insieme all'accesso PSRAM (i gestori possono intercettare o post-elaborare). Wait state e sincronizzazione T1 possono essere configurati per blocco.
  • ROM — le letture provengono dalla PSRAM (tipicamente caricata da un file immagine all'avvio). I cicli di scrittura raggiungono comunque qualsiasi gestore memioPtr installato, ma la PSRAM non viene modificata — utile per i registri di banking che si trovano in una regione di indirizzi mappata come ROM.
  • VRAM — le letture dalla PSRAM; le scritture vanno sia alla PSRAM che alla VRAM fisica dell'host in parallelo. Questo permette al software di mantenere una copia shadow del buffer video aggiornando al contempo lo schermo reale.
  • FUNC — non c'e' supporto PSRAM. Ogni lettura e scrittura chiama la funzione installata in memioPtr[addr]. Usare questo per registri hardware virtualizzati, porte di controllo banking mappate nello spazio di memoria e qualsiasi risorsa senza RAM reale dietro.
  • PTR — ogni byte del blocco da 512 byte puo' puntare indipendentemente a una posizione PSRAM diversa o a un tipo di memoria diverso. Usato per manipolazioni dello spazio di indirizzi a granularita' molto fine.

Codifica membankPtr

L'array _membankPtr[] ha 128 voci — una per ogni blocco da 512 byte dello spazio di indirizzi Z80 a 64KB. Ogni voce e' un singolo valore a 32 bit che codifica tre campi:
Bit 31..24  =  Memory type   (MEMBANK_TYPE_xxx constant)
Bit 23..16  =  PSRAM bank    (0-63, which 64KB bank in the 8MB PSRAM)
Bit 15..0   =  Z80 address   (base address of the block within the bank)
Per impostare il tipo e il banco di un blocco, si impacchettano questi tre valori in un singolo intero a 32 bit usando l'OR bit a bit:
// Map block idx (each block = 512 bytes) to RAM type in bank 0:
cpu->_membankPtr[idx] = (MEMBANK_TYPE_RAM  << 24)   // type in top byte
                      | (MZ700_MEMBANK_0   << 16)   // bank number
                      | (idx * MEMORY_BLOCK_SIZE);  // base address of this block

// Map same block to ROM in bank 2:
cpu->_membankPtr[idx] = (MEMBANK_TYPE_ROM  << 24)
                      | (2                 << 16)
                      | (idx * MEMORY_BLOCK_SIZE);

// Map same block to physical (pass-through):
cpu->_membankPtr[idx] = (MEMBANK_TYPE_PHYSICAL << 24)
                      | (0 << 16)
                      | (idx * MEMORY_BLOCK_SIZE);
MEMORY_BLOCK_SIZE e' 512 byte. Ci sono 128 blocchi che coprono 0x0000–0xFFFF. L'indice del blocco per un dato indirizzo Z80 e': addr / MEMORY_BLOCK_SIZE = addr >> 9.

Attributi di Memoria (t_memAttr)

Ogni blocco ha anche una voce t_memAttr che controlla i wait state e la sincronizzazione dei cicli T. Questi sono memorizzati in un array 2D indicizzato per banco e blocco:
// src/include/Z80CPU.h
typedef struct {
    uint8_t   waitStates;   // Number of additional T-cycle wait states inserted on access
    bool      tCycSync;     // true = sync PSRAM access to the T1 rising edge of each bus cycle
} t_memAttr;

// Access pattern:
cpu->_memAttr[bank][idx].waitStates = 1;
cpu->_memAttr[bank][idx].tCycSync   = true;
waitStates: Se la vostra regione di memoria supportata dalla PSRAM richiede piu' tempo per rispondere (ad esempio perche' la funzione gestore esegue lavoro aggiuntivo), aggiungere wait state. Ogni wait state estende il ciclo di bus di un ciclo T del clock host. Le regioni RAM tipicamente usano 1 wait state; le regioni ROM che servono dati pre-caricati possono spesso usare 0.
tCycSync: Quando impostato a true, la state machine PIO z80_sync ritarda l'accesso PSRAM fino al fronte di salita T1 del ciclo di bus corrente. Questo impedisce alle operazioni interne della PSRAM di introdurre derive temporali nel software host che si basa su un timing preciso dei cicli di clock (I/O cassetta, bit-banging seriale, loop di ritardo). Impostare questo a true per le regioni RAM/ROM a cui il software sensibile al timing dell'host accede.

La Struttura PSRAM (t_Z80PSRAM)

La PSRAM esterna da 8MB e' mappata in una singola struttura t_Z80PSRAM. Questa viene allocata una volta all'avvio e puntata da cpu->_z80PSRAM. E' la struttura dati piu' grande e importante del sistema — tutto cio' a cui lo Z80 puo' accedere risiede qui.
// src/include/Z80CPU.h
typedef struct {
    // 4MB data space: 64 banks x 64KB RAM/ROM image storage
    uint8_t     RAM[MEMORY_PAGE_BANKS * MEMORY_PAGE_SIZE];

    // 64KB per-byte redirect table (used by MEMBANK_TYPE_PTR blocks)
    uint32_t    memPtr[MEMORY_PAGE_SIZE];

    // 64KB memory address function pointer table
    // Index = Z80 address (0x0000-0xFFFF)
    // Value = NULL (no override) or pointer to a C handler function
    MemoryFunc  memioPtr[MEMORY_PAGE_SIZE];

    // 64KB I/O port function pointer table
    // Index = Z80 I/O port address (0x0000-0xFFFF; Z80 uses lower 8 bits for port)
    // Value = NULL (pass through to physical I/O) or pointer to a C handler function
    MemoryFunc  ioPtr[IO_PAGE_SIZE];
} t_Z80PSRAM;
I quattro sotto-array hanno ruoli distinti:
  • RAM[] — memorizzazione grezza di byte per tutti i banchi di memoria supportati dalla PSRAM. La dimensione totale e' 64 banchi x 64KB = 4MB. Le immagini ROM caricate dalla scheda SD o dalla Flash vengono scritte qui all'avvio. Durante l'esecuzione Z80, le letture e le scritture ai blocchi di tipo RAM/ROM/VRAM accedono a questo array.
  • memPtr[] — usato solo dai blocchi di tipo PTR. Ogni voce e' un valore membankPtr completo (codificato allo stesso modo di cpu->_membankPtr[]) che reindirizza l'accesso di un singolo byte a una posizione completamente diversa. Permette il rimappaggio a livello di byte all'interno dello spazio a 64KB.
  • memioPtr[] — la tabella degli hook delle funzioni di memoria. Uno slot per ogni indirizzo Z80. Quando uno slot e' non-NULL, Core 1 chiama la funzione in quello slot a ogni accesso memoria a quell'indirizzo, indipendentemente dal tipo di blocco (RAM, ROM o FUNC). Questo e' il modo in cui i driver intercettano o sovrascrivono specifiche posizioni di memoria senza cambiare il tipo di blocco complessivo.
  • ioPtr[] — la tabella degli hook delle porte I/O. Uno slot per ogni indirizzo I/O Z80 (porte da A0 ad A15, sebbene lo Z80 usi solo A0–A7 come numero di porta effettivo; i bit superiori possono trasportare contesto aggiuntivo). Quando non-NULL, la funzione viene chiamata per ogni istruzione IN o OUT diretta a quella porta. Se NULL, il ciclo I/O passa all'hardware fisico.

La Signature del Gestore MemoryFunc

Sia gli slot memioPtr[] che ioPtr[] contengono puntatori a funzione dello stesso tipo — MemoryFunc:
// src/include/Z80CPU.h
typedef uint8_t (*MemoryFunc)(Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
Parametri:
  • cpu — puntatore al contesto Z80CPU. Vi da accesso a _membankPtr[], _z80PSRAM e tutto il resto. Non modificare mai _membankPtr[] dall'interno di un gestore I/O che puo' essere chiamato dal loop principale di Core 1; usare la coda intercore per tali operazioni (vedere Interazione tra Core).
  • readtrue se questo e' un ciclo di lettura (lo Z80 sta leggendo); false se questo e' un ciclo di scrittura (lo Z80 sta scrivendo).
  • addr — l'indirizzo Z80 completo (0x0000–0xFFFF per la memoria, 0x0000–0xFFFF per le porte I/O). Per l'I/O, lo Z80 usa solo gli 8 bit inferiori come numero di porta effettivo (A0–A7); gli 8 bit superiori (A8–A15) sono il valore del registro B durante l'istruzione.
  • data — in un ciclo di scrittura, il byte che lo Z80 sta scrivendo. In un ciclo di lettura da un blocco RAM/ROM, questo e' il valore corrente a quell'indirizzo nella PSRAM (potete usarlo o ignorarlo).
Valore di ritorno:
  • In una lettura: il byte da restituire allo Z80. Questo e' il valore che lo Z80 vede sul bus dati.
  • In una scrittura: il valore di ritorno e' generalmente inutilizzato per i gestori I/O. Per i gestori memioPtr su blocchi di tipo RAM, il valore di ritorno viene scritto nella PSRAM al posto del dato originale — usare questo per modificare o sanificare cio' che viene memorizzato.
Importante: Le funzioni gestore vengono chiamate direttamente dal loop principale di Core 1. Vengono eseguite su Core 1 con gli interrupt disabilitati. Devono essere brevi, deterministiche e non devono mai bloccarsi, andare in sleep, chiamare debugf, o eseguire qualsiasi operazione che possa stallare Core 1. L'I/O su file e la comunicazione UART devono essere inviate a Core 0 tramite la coda intercore.

La Struttura di Contesto Z80CPU

Ogni funzione del driver riceve un puntatore Z80CPU *cpu. Questo e' il contesto principale per l'intera emulazione. I campi piu' rilevanti per gli sviluppatori di driver sono:
// src/include/Z80CPU.h (simplified)
struct Z80CPU {
    Z80           _Z80;               // Zeta Z80 emulator state (registers, flags, PC, etc.)

    // Fast dispatch table: 128 entries, one per 512-byte block of Z80 address space
    uint32_t      _membankPtr[MEMORY_PAGE_BLOCKS];  // MEMORY_PAGE_BLOCKS = 128

    // Per-block wait state and sync attributes, indexed [bank][block]
    t_memAttr     _memAttr[MEMORY_PAGE_BANKS][MEMORY_PAGE_BLOCKS];

    // Pointer to the 8MB PSRAM structure (RAM[], memPtr[], memioPtr[], ioPtr[])
    t_Z80PSRAM   *_z80PSRAM;

    // Loaded driver configurations (from JSON parsing)
    t_drivers     _drivers;

    // Intercore communication queues (Core 1 → Core 0 and Core 0 → Core 1)
    queue_t       requestQueue;
    queue_t       responseQueue;

    bool          halt;        // Z80 is in HALT state
    bool          hold;        // Core 0 is requesting Core 1 to pause
    bool          holdAck;     // Core 1 acknowledges hold request
    bool          forceReset;  // Asynchronous reset flag
};

Strutture di Configurazione dei Driver

Quando un driver viene inizializzato, riceve un puntatore a una struttura t_drvConfig che e' stata popolata dal parser della configurazione JSON. Questa indica al driver quali interfacce gli sono state assegnate, quali immagini ROM caricare, quali rimappature di indirizzi applicare e quali parametri l'utente ha configurato.
// src/include/Z80CPU.h

// A single parameter key/value pair (from JSON "param" array)
typedef struct {
    const char   *name;   // Parameter name string
    const char   *value;  // Parameter value string (always a string; parse as needed)
} t_ifParam;

// A single ROM image assignment (from JSON "rom" array)
typedef struct {
    const char   *file;       // SD card path to ROM file
    uint16_t      addr;       // Z80 target address for this ROM
    uint8_t       bank;       // PSRAM bank to load into
    uint16_t      size;       // Size in bytes to load
    uint32_t      fileofs;    // Byte offset into the ROM file
    uint8_t       waitStates; // Wait states for this block
    bool          tCycSync;   // T1 sync for this block
} t_drvROMConfig;

// A single address-space remap (from JSON "addrmap" array)
typedef struct {
    uint16_t      srcaddr;    // Original Z80 address
    uint16_t      dstaddr;    // Redirected-to address
    uint16_t      size;       // Range size
    uint8_t       bank;       // PSRAM bank
    uint8_t       type;       // MEMBANK_TYPE_xxx
} t_addrReMap;

// A single I/O remap (from JSON "iomap" array)
typedef struct {
    uint16_t      srcaddr;    // I/O port address
    uint16_t      size;       // Port range size
    const char   *funcName;   // Name of handler function (looked up in memory func map)
} t_ioReMap;

// One interface block (one entry in JSON "if" array)
typedef struct t_drvIFConfig {
    const char       *name;           // Interface name (e.g. "RFS", "MZ-1E05")
    bool              isPhysical;     // Virtual or physical interface
    int               romCount;       // Number of ROM entries
    t_drvROMConfig   *romConfig;      // Array of ROM configurations
    int               addrMapCount;   // Number of address remaps
    t_addrReMap      *addrMap;        // Address remap table
    int               ioMapCount;     // Number of I/O remaps
    t_ioReMap        *ioMap;          // I/O remap table
    int               ifParamCount;   // Number of parameters
    t_ifParam        *ifParam;        // Parameter array
} t_drvIFConfig;

// Top-level driver config (one entry in JSON "drivers" array)
typedef struct t_drvConfig {
    const char       *name;           // Driver name — must match a virtualFuncMap entry
    bool              isPhysical;     // Virtual or physical driver
    int               ifCount;        // Number of interfaces
    t_drvIFConfig    *ifConfig;       // Interface array
    ResetFunc         reset_ptr;      // Reset handler set by driver init
    PollFunc          poll_ptr;       // Poll handler set by driver init
    TaskFunc          task_ptr;       // Task handler set by driver init
} t_drvConfig;
Porte I/O base rilocabili. Affinche' le schede indipendenti dalla macchina possano essere usate su schede custom / sperimentali (vedere OpenZ80), diversi driver leggono la propria porta I/O base dalla voce iomap dell'interfaccia invece di codificarla in modo fisso: il dstaddr della voce diventa la base della scheda (con srcaddr la porta autentica della scheda), e ciascuna di queste schede porta nel proprio header una costante *_DEFAULT_BASE usata quando non e' presente alcuna voce iomap. Le schede rilocabili e i relativi valori predefiniti sono 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) e Celestite (0x60/16). Le chiavi JSON iomap/addrmap sono in minuscolo (srcaddr / dstaddr) e vengono analizzate come numeri; il campo Base I/O Port della pagina di Configurazione della GUI le scrive automaticamente per voi.
I campi reset_ptr, poll_ptr e task_ptr non vengono impostati dal JSON — vengono impostati dalla funzione init del vostro driver in modo che Core 1 possa chiamare le funzioni di manutenzione del driver nei momenti appropriati.

Il Loop di Dispatch del Core 1

Core 1 esegue un loop infinito serrato in Z80CPU_cpu(). Comprendere cosa accade in questo loop e' fondamentale per scrivere driver corretti.

Struttura del Loop Principale

// src/Z80CPU.c — Core 1 entry point
void __func_in_RAM(Z80CPU_cpu)(Z80CPU *cpu)
{
    // Signal Core 0 that Core 1 is running
    multicore_fifo_push_blocking(1);

    while(1)
    {
        // --- HOLD CHECK ---
        // Core 0 can pause Core 1 (e.g. for safe config reload)
        if(cpu->hold == true)
        {
            cpu->holdAck = true;
            while(cpu->hold == true);  // Spin-wait
            cpu->holdAck = false;
        }

        // --- DRIVER POLL ---
        // Call each driver's poll handler for brief periodic housekeeping
        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 EXECUTION ---
        // Run the Z80 emulator for 2048 clock cycles
        z80_run(&cpu->_Z80, 2048);

        // --- RESET CHECK ---
        // PIO IRQ 3 = hardware RESET asserted on the host bus
        if(cpu->forceReset || (pio_2->irq & (1u << 3)) != 0)
        {
            Z80CPU_reset(cpu);
            CLEAR_IRQ(pio_2, 3);
        }
    }
}
Punti chiave:
  • Il loop esegue z80_run() per 2048 cicli per iterazione. Tra le iterazioni, tutti i gestori poll dei driver vengono chiamati. Questo significa che i gestori poll vengono chiamati approssimativamente ogni 2048 cicli di clock Z80 — a 3.5MHz circa ogni 585 microsecondi.
  • I gestori poll devono essere estremamente brevi. Si trovano sul percorso critico dell'emulazione. Un gestore poll lento introduce jitter nel timing del bus Z80.
  • La funzione z80_run() (dalla libreria Zeta Z80) esegue le istruzioni Z80, richiamando Z80CPU_readMem(), Z80CPU_writeMem(), Z80CPU_readIO() e Z80CPU_writeIO() per ogni transazione bus.

Dispatch Lettura Memoria

Quando l'emulatore Z80 esegue una lettura di memoria, viene chiamata Z80CPU_readMem(). Questa funzione e' il cuore del sistema di memoria — comprenderla vi dice esattamente cosa devono fare i vostri gestori.
// src/Z80CPU.c (simplified and annotated)
uint8_t __func_in_RAM(Z80CPU_readMem)(Z80CPU *cpu, uint16_t addr)
{
    // Step 1: Look up the 512-byte block that contains this address
    uint8_t  blockIdx   = addr >> 9;                        // addr / 512
    uint32_t membankptr = cpu->_membankPtr[blockIdx];       // 32-bit encoded entry

    // Step 2: Extract the three fields from the encoded entry
    uint8_t  memType    = (membankptr >> 24) & 0xFF;        // Top byte = type
    uint8_t  bank       = (membankptr >> 16) & 0xFF;        // Middle byte = bank
    uint16_t blockBase  = membankptr & 0xFFFF;              // Lower 16 bits = base addr

    // Step 3: Calculate the offset into the PSRAM bank for this address
    uint32_t RAMaddr    = (bank * MEMORY_PAGE_SIZE) + blockBase;
    uint16_t blockOfs   = addr & (MEMORY_BLOCK_SIZE - 1);  // addr % 512 = offset in 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:
            // Pass-through: let the host hardware respond
            data = Z80CPU_readPhysicalMem(cpu, addr);
            break;

        case MEMBANK_TYPE_RAM:
        case MEMBANK_TYPE_VRAM:
        case MEMBANK_TYPE_ROM:
            if(cpu->_z80PSRAM->memioPtr[addr] != NULL)
            {
                // A handler is installed for this specific address.
                // Pass the current PSRAM value as 'data' so the handler can use or modify it.
                data = cpu->_z80PSRAM->memioPtr[addr](
                           cpu, true, addr,
                           cpu->_z80PSRAM->RAM[RAMaddr + blockOfs]);
            }
            else
            {
                // No handler — read directly from PSRAM
                data = cpu->_z80PSRAM->RAM[RAMaddr + blockOfs];
            }
            if(waitStates) Z80CPU_waitPhysicalStates(cpu, waitStates);
            break;

        case MEMBANK_TYPE_FUNC:
            // Pure virtual device — no PSRAM backing
            if(cpu->_z80PSRAM->memioPtr[addr] != NULL)
                data = cpu->_z80PSRAM->memioPtr[addr](cpu, true, addr, 0);
            break;

        case MEMBANK_TYPE_PTR:
            // Indirect — follow the per-byte pointer and recurse
            data = Z80CPU_readMem(cpu, addr, cpu->_z80PSRAM->memPtr[addr]);
            break;
    }
    return data;
}

Dispatch Scrittura Memoria

// src/Z80CPU.c (simplified and annotated)
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 intercepts the write — the return value replaces 'data'
                // before it is written to PSRAM (handler can sanitise or transform data)
                data = cpu->_z80PSRAM->memioPtr[addr](cpu, false, addr, data);
            }
            // Write (possibly modified) data to PSRAM
            cpu->_z80PSRAM->RAM[RAMaddr + blockOfs] = data;
            if(waitStates) Z80CPU_waitPhysicalStates(cpu, waitStates);
            break;

        case MEMBANK_TYPE_ROM:
            // ROM: handler is called if installed (e.g. to detect banking writes to ROM space)
            // but the PSRAM is NOT written
            if(cpu->_z80PSRAM->memioPtr[addr] != NULL)
                cpu->_z80PSRAM->memioPtr[addr](cpu, false, addr, data);
            break;

        case MEMBANK_TYPE_FUNC:
            // Pure virtual — handler only, no 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;
    }
}

Dispatch Porte I/O

// src/Z80CPU.c (simplified and annotated)
uint8_t __func_in_RAM(Z80CPU_readIO)(Z80CPU *cpu, uint16_t addr)
{
    // addr = full 16-bit Z80 address during I/O instruction (A0-A15)
    // Port number = addr & 0xFF (A0-A7 = lower byte)

    if(cpu->_z80PSRAM->ioPtr[addr] != NULL)
    {
        // Virtual I/O: call handler
        return cpu->_z80PSRAM->ioPtr[addr](cpu, true, addr, 0);
    }
    else
    {
        // Physical I/O: RP2350 releases bus, host hardware responds
        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);
    }
}
Si noti che il dispatch I/O e' piu' semplice del dispatch di memoria — non esiste un concetto di tipo di blocco per le porte I/O. Ogni indirizzo I/O ha un gestore in ioPtr[] oppure va all'hardware fisico. Non esiste la distinzione ROM/RAM/FUNC per l'I/O.
Si noti inoltre: lo Z80 usa un indirizzo a 16 bit nelle istruzioni I/O — gli 8 bit inferiori sono il numero di porta; gli 8 bit superiori contengono il contenuto del registro B (durante le istruzioni IN r,(C) / OUT (C),r). Se desiderate un comportamento diverso del gestore a seconda del registro B, esaminate il byte alto di addr. Per un semplice matching solo sul numero di porta, mascherare con addr & 0xFF.

Il Framework Driver

Il framework driver e' il meccanismo attraverso il quale i moduli driver in C vengono scoperti, istanziati dalla configurazione JSON e collegati ai sistemi di memoria e I/O. Ha due livelli:
  • Driver di primo livello (chiamati anche persona) — registrati in virtualFuncMap[] in Z80CPU.c. Ogni persona configura un'intera personalita' della macchina: layout di memoria, banking, porte I/O e opzionalmente un insieme di sotto-interfacce (schede di interfaccia).
  • Driver di interfaccia — registrati nel proprio interfaceFuncMap[] della persona. Ogni driver di interfaccia aggiunge una specifica periferica (floppy, QuickDisk, espansione RAM, filing system) alla persona.

virtualFuncMap — Registrazione Driver di Primo Livello

L'array virtualFuncMap[] in src/Z80CPU.c mappa un nome stringa a una funzione init del driver. Ogni driver di primo livello (persona) deve avere una voce qui. Il nome stringa deve corrispondere esattamente al campo "name" nell'array JSON "drivers" (case-insensitive).
// src/Z80CPU.c

// Type definition for a top-level driver init function
typedef uint8_t (*VirtualFunc)(Z80CPU *cpu,
                               t_FlashAppConfigHeader *appConfig,
                               t_drvConfig *config,
                               const char *ifName);

// Map entry structure
typedef struct {
    const char  *virtualFuncName;   // String name (must match JSON "name" field)
    VirtualFunc  virtual_func_ptr;  // C function to call
} t_VirtualFuncMap;

// THE REGISTRATION TABLE — add your driver here
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},   // Personalita' Sharp MZ-800 (doppia modalita' 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]);
La funzione di ricerca che cerca in questa tabella per nome e':
// 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 not found — driver will be skipped
}

OpenZ80 — Costruire su un Driver o BIOS Esistente

Il modello OpenZ80 (src/drivers/Other/Open.c, compilato con build_tzpuPico.sh open) e' il punto di partenza per due tipi di sperimentatore: chi vuole inserire il picoZ80 in una scheda di propria progettazione, e chi vuole avviare una macchina Z80 che non ha ancora una persona dedicata. Open.c e' modellato su MZ700.c ma privato di tutto l'hardware della macchina: in modalita' PHYSICAL lascia passare l'intero spazio di memoria e I/O da 64K alla scheda reale (le schede di interfaccia sovrappongono le loro porte fisse); in modalita' VIRTUAL presenta una RAM piatta da 64K in cui le ROM a livello di driver vengono caricate sequenzialmente a partire da 0x0000. E' registrato come {"Open", Open_Init} sotto #ifdef INCLUDE_OPEN_DRIVERS, e offre solo le schede di interfaccia indipendenti dalla macchina (MZ-1R12/1R18/1R23/1R37, PIO-3034, MZ-8BIO3, MZ-1E24, MZ-1E05, Celestite), ciascuna delle quali puo' essere spostata su qualsiasi porta I/O base.
1 — Partire dal driver piu' vicino
Copiare il driver persona piu' vicino al vostro obiettivo e modificarne la mappa di memoria, i gestori I/O e il layout ROM. I driver persona sono:
  • src/drivers/Other/Open.c — la persona vanilla (base migliore per una scheda completamente nuova).
  • 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 — una macchina autonoma con un FDC uPD765 integrato.
  • src/drivers/Tatung/EinsteinTC01.c — una macchina autonoma con un FDC WD1770 integrato.
Registrare la nuova persona aggiungendo una riga {"MyMachine", MyMachine_Init} a virtualFuncMap[] sopra (sotto un appropriato #ifdef), e collegare il suo sorgente in src/CMakeLists.txt (ad esempio aggiungendolo alla lista pZ80_drivers_open_src) piu' un target di modello tramite add_z80_model_targets(...).
2 — Riutilizzare o applicare patch alle ROM della macchina
Le ROM monitor, IPL, CP/M BIOS e di boot floppy che i driver caricano sono mantenute come sorgente assembler Z80 commentato nei progetti collegati RFS e TZFS, sotto le rispettive directory asm/ (assemblate con l'assembler GLASS Z80). Prendere queste come base e ricompilarle o applicarvi patch per la vostra macchina: </font>
Scopo File sorgente
ROM monitor 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)
Boot loader (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
ROM di boot Floppy / QuickDisk 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
Filing system in-ROM RFS/asm/rfs.asm (con banking), TZFS/asm/tzfs.asm (con banking)
Assemblare la ROM necessaria, collocare il binario risultante sulla scheda SD e farvi riferimento da una voce rom[].loadaddr (per una scheda di interfaccia) oppure, su OpenZ80 in modalita' virtuale, come ROM a livello di driver caricata a 0x0000. Sia RFS che TZFS forniscono script di compilazione autonomi (vedere le rispettive Guide dello Sviluppatore) che producono queste immagini ROM.

TZFS — Modalita' di Memoria e il Processore di Servizio Virtuale

src/drivers/Sharp/TZFS.c e' un buon esempio pratico di un driver che combina l'emulazione delle modalita' di memoria a banchi commutati con un modello di servizio inter-core. Implementa un monitor a basso livello multi-bank e file system (un miglioramento del MONITOR 1Z-013A) con il CP/M sottostante, modellato sul TZFS del tranZPUter SW e sul suo processore I/O virtuale K64F. E' registrato come interfaccia selezionabile sulla persona MZ-700 (in MZ700.c, insieme a — e in pratica esclusivo con — RFS), non come persona di primo livello.
Commutazione delle modalita' di memoria (porta 0x60)
Lo Z80 seleziona un layout di memoria tranZPUter scrivendo un valore di modalita' sulla porta I/O 0x60; il driver ri-punta i propri puntatori di bank in risposta. Le modalita' sono TZMM_ORIG, TZMM_BOOT, TZMM_TZFS, TZMM_TZFS2, TZMM_TZFS3, TZMM_TZFS4, TZMM_CPM, TZMM_CPM2 e TZMM_COMPAT. Il layout CP/M CPM2 usa un paging del blocco 0 a granularita' di byte (0x00000x003F → blocco dei vettori, 0x00400x01FF → l'inizio della TPA).
Processore di servizio virtuale K64F (OUT 0x68 → Core 0)
Invece di gestire le chiamate di file system sul core Z80, TZFS usa un processore di servizio virtuale. Lo Z80 esegue OUT (0x68), che accoda un messaggio MSG_TZFS_SVCREQ al Core 0; il Core 0 lo smista attraverso TZFS_processServiceRequest. I servizi di file system sono READDIR / NEXTDIR (blocchi directory a 16 voci in cache), READFILE / NEXTREADFILE, LOADFILE (consapevole dell'header MZF e degli oggetti bank), CHANGEDIR e CLOSE. I servizi CP/M sono LOADBDOS (ricaricamento CCP + BDOS al warm-boot), ADDSDDRIVE, READSDDRIVE e WRITESDDRIVE, piu' i servizi di frequenza CPU. Poiche' picoZ80 non ha accesso diretto alla SD, i settori CP/M da 512 byte vengono instradati attraverso l'ESP32 (ESP_readSector / ESP_writeSector) su file immagine interi; i percorsi immagine per drive provengono dalle voci param[].file del JSON dell'interfaccia, con template di fallback CPM/SDC16M/RAW/CPMDSK<nn>.RAW. La ROM TZFS (roms/tzfs.bin) e' l'output assemblato del TZFS/asm/tzfs.asm del progetto complementare (il suo BIOS CP/M da TZFS/asm/cbios.asm / cpm22.asm).

Flusso di Inizializzazione

Dopo che l'RP2350 legge e analizza config.json, l'inizializzazione dei driver avviene nel seguente ordine:
Z80CPU_configFromJSON()
    ├── Z80CPU_configDriversFromJSON()   Parse "drivers" array from JSON
    │       For each driver entry:
    │           ├── Look up name in virtualFuncMap[]
    │           ├── Parse interface configs (ROM, addrmap, iomap, param)
    │           └── Call VirtualFunc(cpu, appConfig, &drvConfig, NULL)
    │                   ↓
    │               Driver init sets up:
    │                   ├── _membankPtr[] entries (block types and banks)
    │                   ├── _memAttr[][] entries (wait states, sync)
    │                   ├── memioPtr[] handlers (memory address hooks)
    │                   ├── ioPtr[] handlers (I/O port hooks)
    │                   ├── config->reset_ptr = MyDriver_Reset
    │                   ├── config->poll_ptr  = MyDriver_PollCB
    │                   └── config->task_ptr  = MyDriver_TaskProcessor
    │
    ├── Z80CPU_configMemoryFromJSON()    Apply "memory" array (overwrites driver defaults)
    └── Z80CPU_configIOFromJSON()        Apply "io" array (overwrites driver defaults)
Si noti l'ordine: i driver vengono inizializzati per primi, poi vengono applicati gli array JSON "memory" e "io". Questo significa che qualsiasi voce esplicita negli array memory o io in config.json sovrascrivera' qualunque cosa il driver abbia configurato per quegli indirizzi. Questo permette all'utente di perfezionare i valori predefiniti del driver senza modificare il codice sorgente del driver.

Callback del Ciclo di Vita dei Driver

Un driver registra tre callback continuativi memorizzando i puntatori a funzione nella sua struttura t_drvConfig durante l'init. Core 1 li chiama in momenti specifici durante l'esecuzione:
Callback Signature Quando viene chiamato Uso tipico
reset_ptr uint8_t f(Z80CPU *cpu) Linea RESET dell'host asserita; Z80 PC = 0x0000 Ripristinare la mappa di memoria predefinita, cancellare lo stato dei banchi
poll_ptr uint8_t f(Z80CPU *cpu) Ogni ~2048 cicli Z80 (su Core 1) Verificare i flag di stato, inviare richieste a Core 0
task_ptr uint8_t f(Z80CPU *cpu, enum Z80CPU_TASK_NAME task, char *param) In risposta a richieste di task inter-core Gestire i risultati dell'I/O su file, consegna di settori disco
Importante: poll_ptr viene chiamato da Core 1 e deve essere rapido. Se necessita di eseguire I/O (caricare un settore disco, inviare un comando UART), deve inviare un messaggio a Core 0 tramite cpu->requestQueue e ritornare immediatamente. Core 0 eseguira' l'I/O e restituira' il risultato tramite cpu->responseQueue, attivando task_ptr alla prima opportunita' disponibile.

Esempio Pratico: Il Driver MZ-700

Il driver Sharp MZ-700 (src/drivers/Sharp/MZ700.c) e' il driver persona piu' completo nel codebase. Analizzarlo in dettaglio mostra ogni pattern che vi servira' per i vostri driver. Il driver MZ-80A (src/drivers/Sharp/MZ80A.c) fornisce un'altra implementazione di riferimento, dimostrando l'emulazione del PIT Intel 8253 e il meccanismo di swap memoria MEMSW. Il driver MZ-2000 (src/drivers/Sharp/MZ2000.c) e' un ulteriore riferimento, che mostra la commutazione delle modalita' di memoria BST/NST, l'overlay di VRAM caratteri e grafica con selezione banco controllata da Z80 PIO, e l'integrazione del FDC MB8866. Supporta sia la modalita' fisica (sostituzione Z80 drop-in in un vero MZ-2000 con rilevamento automatico della modalita' boot/normale) che la modalita' virtuale (emulazione completa basata su PSRAM con mirroring della IPL ROM). Il driver MZ-800 (src/drivers/Sharp/MZ800.c) e' una personalita' a doppia modalita' che traccia il registro Display-Mode del GDG e ricostruisce al volo la decodifica della propria mappa di memoria, e dimostra il bridging della daisy-chain di interrupt in modalita' virtuale (vedere Il Driver MZ-800 di seguito).
Sono disponibili diversi moduli riutilizzabili di emulazione periferiche: PIT8253.c (Intel 8253 Programmable Interval Timer — tutti e sei i modi contatore, conteggio BCD/binario, latch contatore, modalita' lettura/caricamento LSB/MSB), PPI8255.c (Intel 8255 Programmable Peripheral Interface — Mode 0 I/O, bit set/reset per la Porta C, callback di output per porta e iniezione input), WD1773.c (controller floppy disk WD1773 — usato dai driver persona Sharp MZ), WD1770.c (controller floppy disk WD1770 — usato dalla persona Tatung Einstein, con supporto per formati Extended CPC DSK, D88 e DSK standard), e uPD765.c (controller floppy disk NEC uPD765 — usato dalla persona Amstrad PCW-9512, con supporto per formato CPC DSK e imaging di dischi fisici). Questi moduli sono progettati per essere istanziati da qualsiasi driver persona della macchina.

Il Driver MZ-800 (Personalita' a Doppia Modalita')

Il driver MZ-800 (src/drivers/Sharp/MZ800.c) e' un buon esempio di una personalita' la cui mappa di memoria non e' fissa ma cambia a runtime sotto il controllo del software host. L'MZ-800 e' un superset dell'MZ-700: si avvia in una modalita' compatibile MZ-700 e puo' passare a una modalita' MZ-800 nativa con un diverso layout di memoria e una diversa larghezza grafica. Il driver traccia la modalita' macchina e ricostruisce la sua tabella di decodifica dei blocchi ogni volta che la modalita' cambia.

Registro Display-Mode del GDG (porta 0xCE). Il driver installa un handler ioPtr[] sulla porta 0xCE che intercetta le scritture sul registro Display-Mode del GDG (DMD):

  • Bit 3 seleziona la modalita' macchina — azzerato = MZ-800 nativa, impostato = compatibilita' MZ-700.
  • Bit 2 seleziona la larghezza grafica — 320 vs 640 pixel.

Quando una scrittura cambia uno dei due bit, il driver chiama MZ800_applyMemoryMap(), che percorre i 128 blocchi da 512 byte e riscrive al volo le voci _membankPtr[] per riflettere il nuovo layout. Questo e’ lo stesso meccanismo di dispatch rapido descritto in Codifica membankPtr — non e’ richiesto alcuno stallo del bus perche’ viene ricostruita solo la tabella di decodifica, non la PSRAM sottostante.

Flag di controllo della mappa di memoria. Il layout corrente e’ mantenuto in una piccola struttura di stato come bitmask di flag: ROM_0000 (ROM monitor visibile a 0x0000), ROM_1000 (finestra CG-ROM a 0x1000), CGRAM_VRAM (RAM/VRAM del generatore di caratteri mappata nella finestra 0x1000) e ROM_E000 (ROM IOCS / regione hardware a 0xE000). MZ800_applyMemoryMap() deriva i tipi di blocco da questi flag insieme ai bit di modalita’ MZ-700/MZ-800 e 320/640 correnti. La mappa all’accensione/reset espone la ROM monitor, la CG-ROM e la ROM IOCS. Le porte di banking memoria dell’MZ-800 (0xE0–0xE6) sono gestite dal driver e rispecchiate all’hardware fisico affinche’ una macchina reale a valle rimanga sincronizzata.

Bridging della daisy-chain di interrupt (modalita’ virtuale). Questa e’ la parte piu’ sottile del driver e segue lo stesso pattern del driver MZ-2500. In modalita’ MZ-800 nativa l’interrupt periodico e’ generato dall’8253 reale e passato attraverso lo Z80-PIO fisico, che si trova nella daisy chain degli interrupt. Il latch in-service del PIO si azzera solo quando osserva un fetch di opcode RETI (ED 4D) sul bus reale — ma in modalita’ virtuale la routine di servizio interrupt e il suo RETI vengono eseguiti dalla PSRAM, cosi’ il PIO non li vede mai e rimarrebbe bloccato, impedendo tutti gli interrupt successivi. Il driver colma questa lacuna installando due hook nel core Zeta:

  • MZ800_readIntAck() — installato come cpu->_Z80.inta, la callback di interrupt-acknowledge.
  • MZ800_retiHandler() — installato come cpu->_Z80.reti, la callback RETI.

Entrambi chiamano mz800PhysicalReti(), che riproduce un RETI fisico sul bus reale: pianta i due byte ED 4D a SP-2 nella RAM host reale, esegue fetch di opcode M1 di entrambi i byte sul bus fisico (cosi’ che la logica daisy-chain del PIO osservi il RETI e azzeri il suo latch in-service), quindi ripristina i byte originali che aveva spostato. Questi hook sono installati solo quando l’interfaccia viene eseguita virtualmente (!isPhysical); in modalita’ fisica il PIO reale vede direttamente il RETI reale e non e’ necessario alcun bridging.

Reset. MZ800_Reset() ripristina la mappa di memoria all’accensione, emette un OUT 0xE4 per imitare il reset del gate array, azzera l’area di lavoro RFS/MZF (0x1000–0x1168) e — poiche’ il PIT 8253 non ha un pin di reset — riprogramma e maschera l’8253 quando in modalita’ MZ-700 affinche’ un contatore obsoleto non possa generare un interrupt spurio dopo il reset.

L'MZ-700 e' un computer Sharp a 8 bit del 1982 basato sullo Z80A. La sua mappa di memoria ha alcune caratteristiche distintive che lo rendono un esempio didattico ideale:
  • I 4KB inferiori (0x0000–0x0FFF) sono una ROM Monitor all'accensione ma possono essere scambiati con RAM tramite scritture su porte I/O — il cosiddetto "MZ-700 bank-switching".
  • La regione superiore (0xD000–0xFFFF) contiene Video RAM, VRAM colore e registri hardware mappati in memoria. L'intera regione superiore puo' anche essere scambiata con RAM tramite porte I/O.
  • Il banking della memoria e' controllato tramite sei porte I/O (0xE0–0xE6) che scambiano i blocchi dentro e fuori.

Mappa delle Funzioni di Interfaccia

In cima a MZ700.c, una tabella interfaceFuncMap[] elenca tutte le schede di interfaccia periferiche (sotto-driver) che la persona MZ-700 conosce. Questo e' l'equivalente MZ-700 di virtualFuncMap[] — mappa i nomi delle interfacce (dall'array JSON "if") alle funzioni init per ogni scheda aggiuntiva.
// src/drivers/Sharp/MZ700.c

typedef struct {
    const char  *interfaceFuncName;  // Interface name string (matches JSON "if.name")
    bool         active;             // true once this interface has been initialised
    InitFunc     init_func_ptr;      // Called during driver init when this interface appears in JSON
    ResetFunc    reset_func_ptr;     // Called on RESET
    PollFunc     poll_func_ptr;      // Called every ~2048 Z80 cycles
    TaskFunc     task_func_ptr;      // Called for inter-core task delivery
} 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]);
Nota: L'esempio sopra mostra la lista delle interfacce della persona MZ-700. Ogni persona ha il proprio interfaceFuncMap[] con un sottoinsieme diverso di interfacce disponibili. L'insieme completo dei driver di interfaccia, le loro stringhe JSON "name" per l'array "if" e quali persona li supportano e' documentato nella tabella Compatibilita' Persona–Interfaccia nella Guida Tecnica. Per esempi di configurazione JSON per l'utente finale, consultare il Manuale Utente picoZ80.

Struttura dello Stato di Banking

Poiche' lo stato di banking deve persistere tra le transazioni bus (una scrittura a 0xE0 deve essere ricordata affinche' gli accessi memoria successivi vadano al banco corretto), il driver mantiene una struttura di stato statica:
// src/drivers/Sharp/MZ700.c (abbreviated)
typedef struct {
    bool      loDRAMen;     // true = lower 4KB (0x0000-0x0FFF) is mapped to RAM bank 1
    bool      hiDRAMen;     // true = upper region (0xD000-0xFFFF) is mapped to RAM bank 1
    bool      inhibit;      // true = upper bank-swap is inhibited (INHIBIT port written)

    // Saved membankPtr values for the upper region when hi DRAM is swapped in
    // (needed to restore the original mapping when hi DRAM is swapped back out)
    uint32_t  upmembankPtr[MZ700_UPPERMEM_BLOCKS];
} t_MZ700Ctrl;

static t_MZ700Ctrl MZ700Ctrl = {
    .loDRAMen = false,
    .hiDRAMen = false,
    .inhibit  = false,
};
Le variabili statiche come questa sono sicure perche' esiste una sola istanza Z80CPU e Core 1 e' l'unico thread che chiama le funzioni gestore. Se doveste mai avere istanze CPU multiple (non e' il design attuale), dovreste spostare questo stato in t_drvConfig.

La Funzione Init — MZ700_Init()

MZ700_Init() serve a un duplice scopo. Viene chiamata in due contesti diversi, identificati da quali argomenti sono NULL:
  • Modalita' validazione (ifName != NULL, config == NULL): Chiamata da Z80CPU_configDriversFromJSON() per chiedere "supporti questo nome di interfaccia?" Restituisce 1 se si', 0 se no. Questo permette al parser JSON di validare i nomi delle interfacce rispetto al driver prima di tentare di inizializzarle.
  • Modalita' configurazione (ifName == NULL, config != NULL): La vera init — configurare la mappa di memoria, installare gli hook, configurare le interfacce.
// src/drivers/Sharp/MZ700.c
uint8_t MZ700_Init(Z80CPU *cpu, t_FlashAppConfigHeader *appConfig,
                   t_drvConfig *config, const char *ifName)
{
    // --- VALIDATION MODE ---
    if(ifName != NULL && config == NULL)
    {
        // Check if ifName is in our interfaceFuncMap
        for(size_t i = 0; i < interfaceFuncMapSize; i++)
        {
            if(strncasecmp(ifName, interfaceFuncMap[i].interfaceFuncName,
                           strlen(interfaceFuncMap[i].interfaceFuncName)) == 0)
                return 1;  // Yes, we support this interface
        }
        return 0;  // Unknown interface
    }

    // --- CONFIGURATION MODE ---
    if(ifName == NULL && config != NULL)
    {
        // Determine if this is a physical (pass-through) or virtual (emulated) driver
        bool isPhysical = config->isPhysical;

        // ------------------------------------------------------------------
        // STEP 1: Set up the flat memory map (membankPtr[] + memAttr[][])
        // ------------------------------------------------------------------
        // Walk all 128 blocks and assign a type/bank for each
        for(int idx = 0; idx < MEMORY_PAGE_BLOCKS; idx++)
        {
            uint32_t memType   = MEMBANK_TYPE_PHYSICAL;  // Default: pass-through
            uint8_t  bank      = MZ700_MEMBANK_0;
            uint8_t  waitStates = 0;
            bool     tCycSync  = false;

            // Blocks 0-7 = 0x0000-0x0FFF (Monitor ROM / low 4KB)
            // If not physical, these will be overridden by JSON "memory" array.
            // Leave as PHYSICAL here so that JSON can assign ROM or RAM as needed.

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

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

            // Blocks 112-119 = 0xE000-0xE7FF (hardware registers: PPI, timer, etc.)
            if(idx >= 112 && idx <= 119)
                memType = MEMBANK_TYPE_PHYSICAL_HW;

            // Blocks 120-127 = 0xF000-0xFFFF (FDC address space)
            // Left as PHYSICAL so physical FDC hardware can respond if installed

            // Pack into membankPtr entry
            cpu->_membankPtr[idx] = (memType    << 24)
                                  | (bank       << 16)
                                  | (idx * MEMORY_BLOCK_SIZE);

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

        // ------------------------------------------------------------------
        // STEP 2: Clear the memioPtr and ioPtr tables for our address range
        // (ensure no stale handler pointers from a previous config)
        // ------------------------------------------------------------------
        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;

        // ------------------------------------------------------------------
        // STEP 3: Install I/O port handlers for banking control ports
        // ------------------------------------------------------------------
        if(!isPhysical)
        {
            // MZ-700 memory banking ports 0xE0-0xE6
            for(int port = 0xE0; port <= 0xE6; port++)
                cpu->_z80PSRAM->ioPtr[port] = (MemoryFunc)MZ700_IO_MemoryBankPorts;
        }

        // ------------------------------------------------------------------
        // STEP 4: Register lifecycle callbacks
        // ------------------------------------------------------------------
        config->reset_ptr = (ResetFunc)MZ700_Reset;
        config->poll_ptr  = (PollFunc)MZ700_PollCB;
        config->task_ptr  = (TaskFunc)MZ700_TaskProcessor;

        // ------------------------------------------------------------------
        // STEP 5: Initialise sub-interfaces listed in the JSON "if" array
        // ------------------------------------------------------------------
        for(int ifNo = 0; ifNo < config->ifCount; ifNo++)
        {
            t_drvIFConfig *ifcfg = &config->ifConfig[ifNo];

            // Find the interface in our map
            for(size_t i = 0; i < interfaceFuncMapSize; i++)
            {
                if(strncasecmp(ifcfg->name, interfaceFuncMap[i].interfaceFuncName,
                               strlen(interfaceFuncMap[i].interfaceFuncName)) == 0)
                {
                    // Call the sub-driver init function
                    interfaceFuncMap[i].init_func_ptr(cpu, appConfig, ifcfg);
                    interfaceFuncMap[i].active = true;
                    break;
                }
            }
        }
    }
    return 1;
}

Il Gestore di Banking — MZ700_IO_MemoryBankPorts()

Questo e' il gestore I/O che gestisce tutte le sei porte di controllo banking dell'MZ-700. Viene chiamato da Z80CPU_writeIO() ogni volta che lo Z80 scrive sulle porte 0xE0–0xE6. La lettura da queste porte non ha effetti collaterali (restituisce 0xFF). La scrittura modifica la mappa di memoria modificando le voci _membankPtr[] in tempo reale.
// 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);  // Extract port number from Z80 address

    // Reads have no side-effect
    if(read) return 0xFF;

    // --- PORT 0xE0: Enable lower 4KB DRAM ---
    // Swaps Monitor ROM (0x0000-0x0FFF) for 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: Enable upper DRAM (0xD000-0xFFFF) ---
    // Saves the current upper membankPtr entries and replaces them with 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++)
        {
            // Save current mapping so we can restore it later (port 0xE3)
            MZ700Ctrl.upmembankPtr[idx - startBlock] = cpu->_membankPtr[idx];

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

    // --- PORT 0xE2: Restore lower ROM ---
    // Swaps bank 1 RAM back out, restoring Monitor ROM at 0x0000-0x0FFF
    if(port == 0xE2 && MZ700Ctrl.loDRAMen)
    {
        for(int idx = 0; idx < (0x1000 / MEMORY_BLOCK_SIZE); idx++)
        {
            // Restore to ROM in bank 0
            cpu->_membankPtr[idx] = (MEMBANK_TYPE_ROM << 24)
                                  | (MZ700_MEMBANK_0  << 16)
                                  | (idx * MEMORY_BLOCK_SIZE);
        }
        MZ700Ctrl.loDRAMen = false;
    }

    // --- PORT 0xE3: Restore upper hardware mapping ---
    if(port == 0xE3 && MZ700Ctrl.hiDRAMen)
    {
        int startBlock = 0xD000 / MEMORY_BLOCK_SIZE;
        int endBlock   = 0x10000 / MEMORY_BLOCK_SIZE;

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

    // --- PORT 0xE4: Inhibit upper DRAM swap ---
    if(port == 0xE4)
        MZ700Ctrl.inhibit = true;

    // --- PORT 0xE5: Enable upper inhibit (alias) ---
    if(port == 0xE5)
        MZ700Ctrl.inhibit = false;

    // PORT 0xE6: Memory protect (not implemented in this example)

    return 0;  // Return value ignored for write I/O handlers
}
Questo gestore dimostra il pattern piu' importante nella scrittura dei driver: modificare _membankPtr[] in risposta a una scrittura I/O per implementare il banking della memoria. Le modifiche hanno effetto immediato — il prossimo accesso memoria dallo Z80 usera' la nuova mappatura.
Il pattern di salvataggio/ripristino per la regione di memoria superiore (MZ700Ctrl.upmembankPtr[]) e' importante: quando si scambiano regioni mappate sull'hardware con la RAM, bisogna ricordare cosa c'era prima per poterlo ripristinare quando il software scambia indietro. Assegnare semplicemente PHYSICAL di nuovo perderebbe qualsiasi mappatura personalizzata configurata dai sotto-driver o dalla configurazione JSON.

Il Gestore di Reset — MZ700_Reset()

Quando l'host asserisce RESET, Core 1 chiama il reset_ptr di ogni driver. Il gestore di reset dell'MZ-700 ripristina la mappa di memoria allo stato di accensione (ROM a 0x0000, VRAM e hardware a 0xD000+) e poi chiama tutti i gestori di reset delle interfacce attive:
// src/drivers/Sharp/MZ700.c
uint8_t MZ700_Reset(Z80CPU *cpu)
{
    // Restore lower 4KB to ROM if it was swapped to DRAM
    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;
    }

    // Restore upper region if it was swapped to DRAM
    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;

    // Propagate reset to all active sub-interfaces
    for(size_t i = 0; i < interfaceFuncMapSize; i++)
    {
        if(interfaceFuncMap[i].active)
            interfaceFuncMap[i].reset_func_ptr(cpu);
    }
    return 0;
}

Il Gestore Poll — MZ700_PollCB()

// src/drivers/Sharp/MZ700.c
uint8_t MZ700_PollCB(Z80CPU *cpu)
{
    // The MZ-700 persona itself has nothing to do in the poll loop —
    // all polling is delegated to active sub-interfaces
    for(size_t i = 0; i < interfaceFuncMapSize; i++)
    {
        if(interfaceFuncMap[i].active)
            interfaceFuncMap[i].poll_func_ptr(cpu);
    }
    return 0;
}

Il Processore di Task — MZ700_TaskProcessor()

// src/drivers/Sharp/MZ700.c
uint8_t MZ700_TaskProcessor(Z80CPU *cpu, enum Z80CPU_TASK_NAME task, char *param)
{
    // Dispatch incoming task results to the sub-interface that requested them
    for(size_t i = 0; i < interfaceFuncMapSize; i++)
    {
        if(interfaceFuncMap[i].active)
            interfaceFuncMap[i].task_func_ptr(cpu, task, param);
    }
    return 0;
}
Il pattern del processore di task e' coerente in tutti i driver: la persona semplicemente distribuisce il task a tutte le sotto-interfacce attive. Ogni sotto-interfaccia verifica se il task e' rilevante per essa e lo ignora in caso contrario.

Scrivere un Nuovo Driver — Passo per Passo

Questa sezione illustra ogni passo necessario per creare un driver completo da zero. L'esempio crea un semplice RAM disk (un blocco da 64KB di PSRAM a cui lo Z80 accede come memoria mappata su I/O) per illustrare tutti i pattern senza la complessita' dell'emulazione hardware reale.

Passo 1 — Creare i File Sorgente

Create due file. Il file header dichiara le funzioni che gli altri moduli chiameranno; il file C le implementa.
// File: src/include/drivers/Sharp/MyDriver.h
#ifndef MYDRIVER_H
#define MYDRIVER_H

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

// Top-level init (registered in virtualFuncMap)
uint8_t MyDriver_Init(Z80CPU *cpu,
                      t_FlashAppConfigHeader *appConfig,
                      t_drvConfig *config,
                      const char *ifName);

// Lifecycle callbacks (set by init, called by Core 1 loop)
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);

// Memory / I/O handler functions
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
// File: src/drivers/Sharp/MyDriver.c
#include <string.h>
#include <stdio.h>

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

// ------------------------------------------------------------------
// Internal state
// ------------------------------------------------------------------
#define MYDRIVER_BANK   8   // Use PSRAM bank 8 for our RAM region
                            // (Banks 0-7 reserved for MZ-700 persona in this example;
                            //  choose a bank not used by any other driver)

#define MYDRIVER_IO_STATUS   0xC0  // I/O port: read = status byte
#define MYDRIVER_IO_CONTROL  0xC1  // I/O port: write = control byte

typedef struct {
    uint8_t  controlReg;    // Last value written to control port
    bool     enabled;       // Is the RAM region currently mapped in?
} t_MyDriverState;

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

// ------------------------------------------------------------------
// Memory handler: called for every access to 0x8000-0xBFFF
// when the RAM region is mapped in as MEMBANK_TYPE_FUNC
// ------------------------------------------------------------------
uint8_t MyDriver_MemHandler(Z80CPU *cpu, bool read, uint16_t addr, uint8_t data)
{
    // For FUNC-type blocks, there is no PSRAM backing.
    // We use a portion of a PSRAM bank as our backing store,
    // but we service reads and writes manually here.

    uint32_t bankBase  = MYDRIVER_BANK * MEMORY_PAGE_SIZE; // Start of bank 8 in RAM[]
    uint16_t localAddr = addr - 0x8000;                    // Offset within our region

    if(read)
    {
        // Return the byte from our backing store
        return cpu->_z80PSRAM->RAM[bankBase + localAddr];
    }
    else
    {
        // Store the byte into our backing store
        cpu->_z80PSRAM->RAM[bankBase + localAddr] = data;
        return data;
    }
}

// ------------------------------------------------------------------
// I/O handler: called for ports 0xC0 and 0xC1
// ------------------------------------------------------------------
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: return status byte
        if(port == MYDRIVER_IO_STATUS)
            return MyState.enabled ? 0x01 : 0x00;
        return 0xFF;
    }
    else
    {
        // Port 0xC1: control byte
        if(port == MYDRIVER_IO_CONTROL)
        {
            MyState.controlReg = data;

            if(data & 0x01)
            {
                // Bit 0 = enable: map our RAM into 0x8000-0xBFFF
                if(!MyState.enabled)
                {
                    int startBlock = 0x8000 / MEMORY_BLOCK_SIZE;  // = 64
                    int endBlock   = 0xC000 / MEMORY_BLOCK_SIZE;  // = 96

                    for(int idx = startBlock; idx < endBlock; idx++)
                    {
                        // Use FUNC type so MyDriver_MemHandler is called for every access
                        cpu->_membankPtr[idx] = (MEMBANK_TYPE_FUNC  << 24)
                                              | (MYDRIVER_BANK      << 16)
                                              | (idx * MEMORY_BLOCK_SIZE);

                        // Install the memory handler for every address in range
                    }
                    // Install memioPtr handler for the entire range
                    for(uint32_t a = 0x8000; a < 0xC000; a++)
                        cpu->_z80PSRAM->memioPtr[a] = (MemoryFunc)MyDriver_MemHandler;

                    MyState.enabled = true;
                }
            }
            else
            {
                // Bit 0 = 0: unmap, restore PHYSICAL pass-through
                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);
                    }
                    // Remove memioPtr handlers
                    for(uint32_t a = 0x8000; a < 0xC000; a++)
                        cpu->_z80PSRAM->memioPtr[a] = NULL;

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

// ------------------------------------------------------------------
// Reset handler: restore power-on state
// ------------------------------------------------------------------
uint8_t MyDriver_Reset(Z80CPU *cpu)
{
    // If our RAM was mapped in, restore physical pass-through
    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;
    }

    // Reset state
    MyState.controlReg = 0;
    MyState.enabled    = false;
    return 0;
}

// ------------------------------------------------------------------
// Poll callback: nothing to do in this example
// ------------------------------------------------------------------
uint8_t MyDriver_PollCB(Z80CPU *cpu)
{
    (void)cpu;
    return 0;
}

// ------------------------------------------------------------------
// Task processor: nothing to do in this example
// ------------------------------------------------------------------
uint8_t MyDriver_TaskProcessor(Z80CPU *cpu, enum Z80CPU_TASK_NAME task, char *param)
{
    (void)cpu; (void)task; (void)param;
    return 0;
}

// ------------------------------------------------------------------
// Init function: called by virtualFuncMap dispatch
// ------------------------------------------------------------------
uint8_t MyDriver_Init(Z80CPU *cpu,
                      t_FlashAppConfigHeader *appConfig,
                      t_drvConfig *config,
                      const char *ifName)
{
    // Validation mode: does this driver support the named interface?
    if(ifName != NULL && config == NULL)
    {
        // This simple driver has no sub-interfaces — always return 0
        return 0;
    }

    // Configuration mode
    if(ifName == NULL && config != NULL)
    {
        // Default state: 0x8000-0xBFFF is PHYSICAL (host hardware responds)
        // The Z80 can enable our RAM by writing to port 0xC1.
        // At init time, leave the memory map unchanged and just install I/O hooks.

        // Install I/O handlers for our control and status ports
        cpu->_z80PSRAM->ioPtr[MYDRIVER_IO_STATUS]  = (MemoryFunc)MyDriver_IOHandler;
        cpu->_z80PSRAM->ioPtr[MYDRIVER_IO_CONTROL] = (MemoryFunc)MyDriver_IOHandler;

        // Register lifecycle callbacks
        config->reset_ptr = (ResetFunc)MyDriver_Reset;
        config->poll_ptr  = (PollFunc)MyDriver_PollCB;
        config->task_ptr  = (TaskFunc)MyDriver_TaskProcessor;

        // Check for any parameters the user set in JSON "param" array
        for(int p = 0; p < config->ifCount; p++)
        {
            // (no sub-interfaces in this example)
        }
    }
    return 1;
}

Passo 2 — Aggiungere a CMakeLists.txt

Aprire src/CMakeLists.txt e aggiungere il nuovo file sorgente alla lista dei driver Sharp:
# src/CMakeLists.txt — add your file to the Sharp driver list

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
    # ADD YOUR DRIVER HERE:
    ${CMAKE_CURRENT_LIST_DIR}/drivers/Sharp/MyDriver.c
)

# Amstrad driver list (compiled when INCLUDE_AMSTRAD_DRIVERS is set)
set(pZ80_drivers_amstrad_src
    ${CMAKE_CURRENT_LIST_DIR}/drivers/Amstrad/PCW9512.c
    ${CMAKE_CURRENT_LIST_DIR}/drivers/Amstrad/uPD765.c
)
E' inoltre necessario aggiungere la directory dell'header al percorso di inclusione se si trova in una nuova sottodirectory. Per la directory dei driver Sharp questo e' gia' configurato, quindi non e' necessaria alcuna chiamata aggiuntiva a target_include_directories.

Passo 3 — Includere l'Header in Z80CPU.c

Aprire src/Z80CPU.c e aggiungere un include per l'header del vostro driver accanto agli include dei driver Sharp esistenti:
// src/Z80CPU.c — near the top with other driver 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"
// ADD YOUR INCLUDE:
#include "drivers/Sharp/MyDriver.h"
#endif

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

Passo 4 — Registrare in virtualFuncMap

Sempre in src/Z80CPU.c, trovare l'array virtualFuncMap[] e aggiungere la vostra voce. La stringa "MyDriver" e' cio' che il campo JSON "name" deve contenere:
// 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
    // ADD YOUR DRIVER:
    {"MyDriver", MyDriver_Init},
};
La ricerca e' case-insensitive, quindi "mydriver", "MyDriver" e "MYDRIVER" nel JSON corrisponderanno tutti a questa voce.

Passo 5 — Aggiungere il Driver a config.json

Aggiungere una voce "drivers" al vostro config.json sulla scheda SD. Il campo "name" deve corrispondere alla stringa registrata in virtualFuncMap[]:
{
  "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": []
          }
        ]
      }
    ]
  }
}

Passo 6 — Compilare e Testare

# From the project root
./build_tzpuPico.sh DEBUG

# Firmware output:
# build/bin/model/BaseZ80/BaseZ80_0x10020000.elf   (debug ELF — use with GDB)
# build/bin/model/BaseZ80/BaseZ80_0x10020000.bin   (OTA binary)

# Flash via OTA web page, or debug directly:
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
Il sistema di compilazione produce binari firmware applicativi per ogni target di modello — due partizioni (Partizione 1 a 0x10020000, Partizione 2 a 0x10520000) ciascuna in variante standard e DBGSH. Sono disponibili tre target di modello:
  • BaseZ80 (pZ80-BaseZ80) — binario universale con tutti i driver (Sharp + Amstrad + Tatung). Definisce INCLUDE_SHARP_DRIVERS, INCLUDE_AMSTRAD_DRIVERS e INCLUDE_TATUNG_DRIVERS.
  • SharpZ80 (pZ80-SharpZ80) — solo driver Sharp MZ. Definisce INCLUDE_SHARP_DRIVERS. Produce un binario firmware piu' piccolo.
  • AmstradZ80 (pZ80-AmstradZ80) — solo driver Amstrad PCW. Definisce INCLUDE_AMSTRAD_DRIVERS e TARGET_MODEL_AMSTRAD. Produce un binario firmware piu' piccolo.
  • TatungZ80 (pZ80-TatungZ80) — solo driver Tatung Einstein. Definisce INCLUDE_TATUNG_DRIVERS e TARGET_MODEL_TATUNG. Produce un binario firmware piu' piccolo.
Ogni target di modello ha la propria directory sotto src/model/ con un CMakeLists.txt dedicato, punto di ingresso (main.c) e script del linker. La variante standard omette la debug shell per un'immagine firmware piu' piccola. La variante DBGSH aggiunge la definizione di compilazione INCLUDE_DBGSH, che abilita la debug shell ICE completa su USB CDC Channel 1. I nomi dei file firmware DBGSH sono identificati dal suffisso _DBGSH.

Firmware ESP32 — Selezione della Modalita' di Rete

Prima di compilare il firmware ESP32, selezionare la modalita' di rete desiderata copiando l'sdkconfig pre-compilato appropriato:
# Select networking mode before building ESP32 firmware:
cp sdkconfig.mode_ncm_only sdkconfig       # NCM only (no WiFi, FCC/RED safe)
# cp sdkconfig.mode_wifi_only sdkconfig     # WiFi only
# cp sdkconfig.mode_wifi_and_ncm sdkconfig  # Both WiFi and NCM
idf.py build
Le definizioni del preprocessore chiave controllate da queste configurazioni sono:
  • CONFIG_IF_WIFI_ENABLED — abilita la radio WiFi e il codice AP/Client.
  • CONFIG_IF_USB_NCM_ENABLED — abilita l'interfaccia di rete USB NCM e il server DHCP.
Queste vengono impostate tramite Kconfig (idf.py menuconfig) o i file sdkconfig pre-compilati elencati sopra.

Pattern di Hook Memoria in Dettaglio

Questa sezione descrive ogni pattern di hook in dettaglio con esempi completi. Questi sono i mattoni fondamentali di tutta la gestione della memoria dei driver.

Pattern 1 — Dispositivo Virtuale Puro (blocco FUNC)

Usare questo quando si desidera che una regione dello spazio di indirizzi Z80 sia completamente controllata dal vostro gestore, senza supporto PSRAM. Le letture e scritture dello Z80 chiamano sempre la vostra funzione. Nulla viene memorizzato nella PSRAM.
// Map 0xC000-0xCFFF as a pure virtual device in bank 4
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);
}

// Install handler for every address in range
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 within the device

    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;
    }
}

Pattern 2 — Intercettare le Scritture in una Regione RAM

Usare questo quando si desidera che una regione si comporti come normale RAM (le letture restituiscono dati PSRAM, le scritture aggiornano la PSRAM) ma si vuole anche essere notificati delle scritture — ad esempio, per duplicare le scritture video RAM in un buffer shadow o per attivare un aggiornamento hardware. Il tipo di blocco rimane RAM; si installa un gestore memioPtr che post-elabora la scrittura.
// Map 0xD000-0xD7FF as RAM but intercept all writes
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;
}

// Only install handler for write-intercept addresses
for(uint32_t addr = 0xD000; addr < 0xD800; addr++)
    cpu->_z80PSRAM->memioPtr[addr] = (MemoryFunc)MyVRAM_WriteIntercept;

// Handler — called by Z80CPU_writeMem() for RAM type with a handler:
// The returned value is what gets stored in PSRAM (not the original 'data').
uint8_t MyVRAM_WriteIntercept(Z80CPU *cpu, bool read, uint16_t addr, uint8_t data)
{
    if(read)
    {
        // On read: 'data' already contains the PSRAM value — just return it
        return data;
    }
    else
    {
        // On write: update our shadow copy, then return 'data' so PSRAM is also updated
        uint16_t vramOffset = addr - 0xD000;
        myVRAMShadow[vramOffset] = data;
        markDirty(vramOffset);     // e.g. signal renderer that this cell changed
        return data;  // PSRAM is written with the returned value
    }
}

Pattern 3 — Intercettare le Scritture in una Regione ROM

Alcuni hardware usano le scritture a indirizzi mappati come ROM come scritture ai registri di banking (la scrittura viene "decodificata" dall'hardware ma non modifica la ROM). Il blocco rimane ROM; le scritture attivano il vostro gestore ma la PSRAM non viene modificata.
// ROM at 0x0000-0x0FFF, but writes to 0x0000-0x001F are banking registers
for(int idx = 0; idx < (0x1000 / MEMORY_BLOCK_SIZE); idx++)
{
    cpu->_membankPtr[idx] = (MEMBANK_TYPE_ROM << 24)
                          | (ROM_BANK         << 16)
                          | (idx * MEMORY_BLOCK_SIZE);
}

// Install write-trap handler only for addresses 0x0000-0x001F
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)
    {
        // Return ROM data — 'data' already contains PSRAM value at this address
        return data;
    }
    else
    {
        // Write to ROM address — treat as banking register write
        MyBankSwitch(cpu, addr, data);
        // Return value is ignored for ROM writes (PSRAM not written)
        return data;
    }
}

Pattern 4 — Gestori Sparsi (Indirizzi Individuali)

Non e' necessario installare gestori per un intero blocco. E' possibile installare un gestore su un singolo indirizzo specifico all'interno di un blocco RAM o ROM. Il tipo di blocco controlla cosa succede a tutti gli altri indirizzi nel blocco; il gestore dell'indirizzo specifico sovrascrive solo quell'unico indirizzo.
// The block containing 0x1234 is configured as RAM
// We want 0x1234 specifically to call a handler on write only

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

uint8_t MySpecialHandler(Z80CPU *cpu, bool read, uint16_t addr, uint8_t data)
{
    if(read)
        return data;  // Normal RAM read — return PSRAM value
    else
    {
        // Special action on write to 0x1234
        triggerSomething(data);
        return data;  // Write data to PSRAM as normal
    }
}

Pattern 5 — Gestore Porte I/O

I gestori I/O sono piu' semplici — non c'e' supporto PSRAM per le porte I/O. Il gestore viene chiamato (se installato) oppure il ciclo I/O va all'hardware fisico. Non esiste un concetto di tipo di blocco.
// Install handler for I/O ports 0x80-0x8F (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);   // Actual port number
    // uint8_t regB = (uint8_t)(addr >> 8);  // Register B during 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;
    }
}

Aggiungere una Sotto-Interfaccia a una Persona Esistente

Se la vostra nuova periferica e' una scheda che si inserisce in un MZ-700 (o in un'altra persona esistente), la si implementa come sotto-interfaccia anziche' come driver di primo livello. Questo e' il pattern usato da RFS, WD1773, QDDrive, MZ-1E05, MZ8BFI e le schede di espansione RAM.

Funzioni Richieste per una Sotto-Interfaccia

Una sotto-interfaccia necessita di quattro funzioni con queste signature:
// Called once during MZ700_Init when this interface name appears in the JSON "if" array
uint8_t MyCard_Init(Z80CPU *cpu,
                    t_FlashAppConfigHeader *appConfig,
                    t_drvIFConfig *ifConfig);   // Note: t_drvIFConfig, not t_drvConfig

// Called on RESET
uint8_t MyCard_Reset(Z80CPU *cpu);

// Called every ~2048 Z80 cycles (must be very fast)
uint8_t MyCard_PollCB(Z80CPU *cpu);

// Called for inter-core task delivery
uint8_t MyCard_TaskProcessor(Z80CPU *cpu, enum Z80CPU_TASK_NAME task, char *param);

Registrare la Sotto-Interfaccia

Aggiungere la sotto-interfaccia all'interfaceFuncMap[] nel file C della persona genitore (ad esempio MZ700.c):
// src/drivers/Sharp/MZ700.c — add to interfaceFuncMap[]
#include "drivers/Sharp/MyCard.h"  // Add this include at top of MZ700.c

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},
    // ... existing entries ...
    // ADD YOUR SUB-INTERFACE:
    {"MyCard",  false, MyCard_Init, MyCard_Reset, MyCard_PollCB, MyCard_TaskProcessor},
};
La stringa "MyCard" deve corrispondere al campo "name" della voce dell'interfaccia nell'array JSON "if" (case-insensitive):
"drivers": [
  {
    "enable": 1,
    "name":   "MZ700",
    "type":   "VIRTUAL",
    "if": [
      {
        "enable": 1,
        "name":   "MyCard",
        "type":   "VIRTUAL",
        "rom":    [],
        "addrmap": [],
        "iomap":   [],
        "param": [
          { "name": "myParam", "value": "42" }
        ]
      }
    ]
  }
]

Leggere i Parametri JSON in una Sotto-Interfaccia

Il t_drvIFConfig *ifConfig passato alla init della sotto-interfaccia contiene tutti i dati configurati via JSON. Per leggere un parametro con nome:
uint8_t MyCard_Init(Z80CPU *cpu,
                    t_FlashAppConfigHeader *appConfig,
                    t_drvIFConfig *ifConfig)
{
    // Read a named parameter from the "param" array
    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;
        }
    }

    // Load ROM images listed in the "rom" array
    for(int r = 0; r < ifConfig->romCount; r++)
    {
        t_drvROMConfig *rom = &ifConfig->romConfig[r];

        if(rom->file != NULL && rom->file[0] != '\0')
        {
            // Load ROM from SD card into PSRAM bank at the configured address
            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);

            // Set up membankPtr for this ROM region
            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;
            }
        }
    }

    // Install I/O handlers from the "iomap" array
    for(int m = 0; m < ifConfig->ioMapCount; m++)
    {
        t_ioReMap *iomap = &ifConfig->ioMap[m];

        // Look up the named handler function
        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;
}

Esempio Pratico di Sub-Interfaccia: Schede Seriali RS-232C (Z80 SIO)

Le schede seriali RS-232C sono un esempio completo e reale del pattern di sub-interfaccia visto sopra, e di una sub-interfaccia che possiede un modulo riutilizzabile di emulazione dispositivo e lo collega all'USB. Vengono fornite due sub-interfacce persona — MZ-8BIO3 (src/drivers/Sharp/MZ8BIO3.c) e MZ-1E24 (src/drivers/Sharp/MZ1E24.c) — entrambe costruite su un'emulazione condivisa di Zilog Z80 SIO/2 (src/drivers/Z80SIO.c / src/include/drivers/Z80SIO.h).

L’emulazione Z80 SIO (Z80SIO.c). Questo e’ un modello autonomo, register-accurate, dello Zilog Z80 SIO/2, completamente disaccoppiato sia dall’USB che dall’emulazione della CPU, cosi’ da poter essere riutilizzato da qualsiasi scheda. Implementa:

  • Due canali — canale A agli offset 0/1 (dati/controllo) e canale B agli offset 2/3.
  • L'insieme completo di write-register WR0–WR7 e l'insieme di read-register RR0–RR2, incluso il protocollo a due byte puntatore/comando di WR0 (si scrive un byte di selezione-registro/comando, poi il byte dati agisce sul registro selezionato).
  • Interrupt vettorizzati Z80 mode-2 con uno stack in-service a 4 livelli e priorita' daisy-chain rigorosa (special-Rx del canale A la piu' alta, fino a external/status del canale B la piu' bassa), e l'opzione status-affects-vector.
  • Due ring lock-free single-producer/single-consumer per canale, 1 KB ciascuno: un ring Tx (produttore core 1 → consumatore core 0) e un ring Rx (produttore core 0 → consumatore core 1). Gli helper di push/pop del ring sono esposti per la pompa USB, e un volatile bool* e' esposto per la linea /INT.

Quando opera su USB (anziche’ su una linea seriale reale), DCD e CTS sono tenuti asseriti affinche’ il firmware host che interroga lo stato di controllo modem non si blocchi in attesa di una portante.

Implementazione condivisa della scheda e il wrapper sottile. MZ8BIO3.c contiene l’implementazione condivisa SIOCard_* (init, reset, poll, task, pompa USB). MZ1E24.c e’ un wrapper sottile che differisce solo per la modalita’ connettore passata a SIOCard_Init()SIOCARD_MODE_BI per l’MZ-8BIO3 rispetto a SIOCARD_MODE_ST per l’MZ-1E24. La funzione di init della scheda:

  • Analizza il param "port" (predefinito 0xB0), mascherandolo a un confine di 4 porte affinche' i quattro registri SIO cadano su una base pulita.
  • Istanzia lo Z80 SIO e collega la sua linea /INT a cpu->swIntAssert — l'hook della sorgente di interrupt software nell'emulazione della CPU.
  • Installa l'handler I/O su tutte le 256 varianti di byte alto di ciascuna delle quattro porte (lo Z80 pone il registro B su A8–A15 durante l'I/O, cosi' ogni variante di byte alto deve risolvere sullo stesso handler).
  • Registra gli hook di acknowledge/RETI dell'interrupt software (swIntAckVector / swIntReti) affinche' la consegna del vettore mode-2 e l'azzeramento in-service funzionino.
  • Installa la pompa USB SIOCard_usbPump() nell'hook globale g_usbSerialPump.

In modalita’ fisica la scheda non fa nulla e restituisce 0 — la vera scheda hardware risponde sul bus host.

Bridge USB. Il canale A e’ collegato all’USB CDC 2 (VSER_CHANNEL_A) e il canale B all’USB CDC 3 (VSER_CHANNEL_B). Queste due porte CDC non hanno alcuna UART fisica di supporto; invece pollUSBtoUART() su core 0 chiama SIOCard_usbPump() per spostare byte tra le FIFO CDC e i ring dei canali SIO (host → ring Rx, ring Tx → host). La scheda espone le quattro callback standard di sub-interfaccia — SIOCard_Reset, SIOCard_PollCB, SIOCard_TaskProcessor — ed e’ registrata nell’interfaceFuncMap[] di ogni persona con supporto SIO (MZ-700, MZ-800, MZ-80B, MZ-1500) come, ad esempio: </div>

// Nell'interfaceFuncMap[] di ogni persona con supporto SIO:
{"MZ-8BIO3", false, MZ8BIO3_Init, SIOCard_Reset, SIOCard_PollCB, SIOCard_TaskProcessor},
{"MZ-1E24",  false, MZ1E24_Init,  SIOCard_Reset, SIOCard_PollCB, SIOCard_TaskProcessor},
Entrambe le schede seguono il contratto standard dei driver di interfaccia descritto sopra — sono prive di ROM (nessuna voce "rom"), installano solo handler I/O e callback, e vengono selezionate dalla persona che le elenca nel suo interfaceFuncMap[]. Sul lato configurazione web sono pubblicizzate tramite configgui.js (la mappa driverInterfaces e la tabella interfaceRomLimits, dove riportano un conteggio ROM pari a zero).

Interazione Core 0 / Core 1

I gestori dei driver vengono eseguiti su Core 1 all'interno del loop principale. Qualsiasi operazione che richiede piu' di pochi microsecondi (I/O su file, comandi UART all'ESP32, malloc) deve essere scaricata su Core 0 usando la coda inter-core.

Utilizzo della Coda Intercore

Il pattern e':
  1. Il vostro gestore (su Core 1) rileva che e' necessaria un'operazione di I/O su file o simile (ad esempio lo Z80 ha scritto un numero di settore in un registro comando del disco).
  2. Il gestore imposta un flag di stato (ad esempio diskState.pendingRead = true) e ritorna immediatamente — non esegue l'I/O.
  3. Il vostro poll_ptr (anch'esso su Core 1, chiamato ogni ~2048 cicli) controlla il flag di stato e, se impostato, invia un messaggio di richiesta in cpu->requestQueue.
  4. Core 0 riceve il messaggio, esegue l'I/O su file (ad esempio legge un settore disco dalla scheda SD) e invia il risultato in cpu->responseQueue.
  5. Il vostro task_ptr viene chiamato (su Core 1) con il risultato del task. Copia i dati del settore nella PSRAM e cancella il flag pending.
// In your I/O handler (Core 1 — must be fast):
uint8_t MyDisk_IOHandler(Z80CPU *cpu, bool read, uint16_t addr, uint8_t data)
{
    if(!read && (addr & 0xFF) == 0xFE)
    {
        // Z80 issued a read sector command
        diskState.pendingSector = data;
        diskState.pendingRead   = true;
        // Return immediately — do NOT call file I/O here
    }
    return 0;
}

// In your poll handler (Core 1 — fast check only):
uint8_t MyDisk_PollCB(Z80CPU *cpu)
{
    if(diskState.pendingRead)
    {
        // Build a request and push to Core 0
        t_intercoreMsg msg = {
            .taskId  = TASK_READ_SECTOR,
            .param1  = diskState.pendingSector,
            .dataPtr = diskState.sectorBuffer,
        };
        if(queue_try_add(&cpu->requestQueue, &msg))
            diskState.pendingRead = false;  // Request sent
    }
    return 0;
}

// In your task processor (Core 1 — called when Core 0 has completed the task):
uint8_t MyDisk_TaskProcessor(Z80CPU *cpu, enum Z80CPU_TASK_NAME task, char *param)
{
    if(task == TASK_READ_SECTOR)
    {
        // Sector data is now in diskState.sectorBuffer
        // Copy into PSRAM at the DMA transfer address
        uint32_t dmaAddr = DISK_BUFFER_BANK * MEMORY_PAGE_SIZE + diskState.dmaAddr;
        memcpy(&cpu->_z80PSRAM->RAM[dmaAddr],
               diskState.sectorBuffer, SECTOR_SIZE);

        // Signal the Z80 that data is ready (e.g. set a status flag in a FUNC register)
        diskState.statusReg |= 0x01;  // DRQ bit
    }
    return 0;
}

Errori Comuni

  • Bloccare il gestore. L'errore piu' comune. Qualsiasi chiamata a debugf, sleep_ms, fopen o qualsiasi funzione UART dall'interno di un gestore o callback poll blocchera' Core 1 e causera' un timing del bus incorreto per lo Z80 host. Spostare tutte le operazioni bloccanti su Core 0 tramite la coda di richieste. Usare plogf() al posto di debugf() per il logging nel percorso di boot che deve funzionare prima che l'USB sia disponibile (vedere Log di Debug).
  • Ordine errato dei tag di chiusura nella registrazione dei gestori. Quando si installano gestori su un intervallo usando un loop, assicurarsi che i limiti del loop usino < e non <= per l'indirizzo finale — errori off-by-one possono corrompere i gestori adiacenti.
  • Dimenticare di cancellare i gestori allo shutdown o al reset del driver. Se il vostro gestore di reset non cancella gli slot memioPtr[] o ioPtr[] che il vostro driver ha installato, quei gestori continueranno a essere chiamati dopo il reset, con stato potenzialmente obsoleto.
  • Collisione del numero di banco. Ogni banco PSRAM e' da 64KB. La configurazione JSON assegna i banchi alle regioni di memoria. Se due driver usano lo stesso numero di banco, sovrascriveranno i dati l'uno dell'altro. Usare numeri di banco univoci per ogni driver. I banchi 0–7 sono tipicamente usati dalla persona MZ-700; usare i banchi 8+ per sotto-interfacce e driver aggiuntivi.
  • MEMBANK_TYPE_FUNC senza gestore installato. Se si imposta un blocco al tipo FUNC ma non si installa un gestore memioPtr, le letture restituiranno 0x00 e le scritture verranno silenziosamente scartate. Questo e' un comportamento valido ma spesso e' un bug — installare sempre il gestore prima di impostare il tipo di blocco.
  • Nome virtualFuncMap e nome JSON non corrispondenti. La ricerca e' case-insensitive ma la stringa deve altrimenti corrispondere esattamente. Un errore di battitura in una delle due posizioni causera' il salto silenzioso del driver senza messaggio di errore. Aggiungere una chiamata temporanea a debugf in Z80CPU_getVirtualFunc() se il vostro driver non si inizializza — debugf e' una macro che puo' essere disabilitata o limitata nella frequenza nelle build di produzione, quindi non impatta il timing del bus.
  • Dimenticare di impostare reset_ptr / poll_ptr / task_ptr. Se non assegnate questi nella vostra funzione init, Core 1 non chiamera' mai le vostre funzioni di reset, poll o task. Il driver si inizializzera' correttamente ma non rispondera' al RESET ne' eseguira' alcuna manutenzione periodica.

Log di Debug

Il debug del firmware picoZ80 e' impegnativo perche' si tratta di un sistema multi-core e multi-processore: l'RP2350 esegue due core Cortex-M33 con distinte responsabilita' real-time e non-real-time, e il co-processore ESP32 gestisce tutto l'I/O di rete e storage. Il debug tradizionale in stile printf non e' immediato — l'USB potrebbe non essere disponibile durante l'avvio iniziale, il loop principale di Core 1 non puo' tollerare chiamate bloccanti, e un reset del watchdog distrugge lo stato volatile. Il firmware fornisce tre meccanismi complementari di output di debug per affrontare questi vincoli.

debugf() — Output di Debug Bufferizzato

debugf() e' la macro principale di output di debug. Funziona come printf() ma scrive in un buffer da 64KB nella PSRAM (all'indirizzo 0x117EF004) anziche' direttamente su una porta seriale. Il buffer viene svuotato su USB CDC quando il loop principale ha tempo disponibile. Un mutex (debugMutex) protegge il buffer dall'accesso concorrente da parte di entrambi i core.
// src/include/debug.h

// Primary debug output — mutex-protected, buffered in PSRAM, flushed to USB
debugf("Boot stage %d reached, PSRAM size = %d bytes\n", stage, psramSize);

// Single character / string output (also mutex-protected)
debug_putchar('.');
debug_puts("Init complete\n");
Proprieta' chiave:
  • Dimensione del buffer: 64KB (MAX_DEBUG_BUFFER_SIZE = 65536) nella PSRAM.
  • Thread-safe: protetto da mutex per l'accesso multi-core.
  • Residente in PSRAM: il buffer sopravvive ai reset del watchdog (la PSRAM mantiene i dati), ma il puntatore del buffer a 0x117EF004 deve essere rivalidato dopo il reset.
  • Non chiamare mai dai gestori di Core 1 o dai callback poll. L'acquisizione del mutex puo' bloccare, introducendo jitter nel timing del bus. Usare plogf() per i messaggi nel percorso di boot o inviare richieste di debug a Core 0 tramite la coda inter-core.

plogf() — Log di Boot Persistente in PSRAM

plogf() scrive in una regione dedicata da 4KB alla fine della PSRAM da 8MB (indirizzo 0x117FF000). A differenza di debugf(), non usa un mutex ed e' destinato esclusivamente al logging nel percorso di boot su Core 0 — per catturare i messaggi durante le fasi critiche di avvio iniziale prima che l'USB sia disponibile per il normale output di debug. Poiche' la PSRAM mantiene i suoi contenuti attraverso i reset del watchdog, questi messaggi sopravvivono a un crash e possono essere esaminati al successivo avvio riuscito.
// src/include/debug.h

// PSRAM persistent log structure (at 0x117FF000)
#define PLOG_ADDR   0x117FF000
#define PLOG_SIZE   3840          // 4KB minus 256 bytes reserved for fault diagnostics
#define PLOG_MAGIC  0x504C4F47    // "PLOG"

typedef struct {
    uint32_t  magic;              // PLOG_MAGIC if buffer contains valid data
    uint32_t  len;                // Current write position in buf[]
    char      buf[PLOG_SIZE - 8]; // Circular text buffer
} t_PsramLog;

// Write to persistent log (no mutex — Core 0 boot path only)
plogf("FSPI init: DMA TX=%d RX=%d\n", gDmaTx, gDmaRx);

// Dump captured log on next successful boot (called from main loop)
dump_plog();  // Outputs plog contents via debugf(), then clears the buffer
Pattern di utilizzo tipico: Durante il boot, chiamare plogf() a ogni traguardo critico (init PSRAM, handshake SPI, parsing della configurazione). Se il watchdog scatta, il buffer plog mantiene tutti i messaggi fino al punto di blocco. Al successivo avvio riuscito, dump_plog() stampa i messaggi catturati prima di cancellare il buffer, fornendo una traccia chiara di cosa e' successo prima del crash.

Scegliere l'Output di Debug Corretto

Macro Posizione Mutex Sopravvive al reset WDT Sicuro da Core 1 Caso d'uso
debugf() PSRAM (buffer 64KB) Si' Contenuti del buffer si'; il puntatore deve essere rivalidato No — blocchera' Output di debug generale Core 0
plogf() PSRAM (4KB alla fine) No Si' No — solo Core 0 Logging percorso di boot prima dell'USB
SWD + GDB Sonda hardware N/A N/A Si' (porte per-core) Debug live, breakpoint, ispezione

Watchdog e Tracciamento del Progresso di Boot

Il picoZ80 opera in un ambiente impegnativo: un RP2350 dual-core che comunica via SPI con un ESP32, servendo un'interfaccia bus Z80 cycle-accurate tramite PIO, il tutto gestendo 8MB di PSRAM esterna. Un blocco in qualsiasi punto durante il boot — inizializzazione PSRAM, handshake SPI, parsing della configurazione o lancio di Core 1 — lascerebbe la scheda non responsiva senza alcun output diagnostico. Il timer watchdog hardware e il sistema di tracciamento del progresso di boot sono stati progettati per rendere tali guasti recuperabili e diagnosticabili.

Timer Watchdog

Il watchdog hardware dell'RP2350 viene abilitato presto in main() con un timeout di 30 secondi:
// src/model/BaseZ80/main.c
watchdog_enable(30000, true);  // 30s timeout, pause-on-debug enabled
watchdog_update() viene chiamato a ogni traguardo di boot e durante tutto il loop principale. Le operazioni lunghe critiche (caricamento immagini floppy disk con logica di retry, trasferimenti DMA, handshake SPI ESP32) includono kick espliciti del watchdog per prevenire reset spuri durante operazioni lente legittime:
// Floppy disk load with retry logic — kick watchdog between attempts
for (int attempt = 0; attempt < 10; attempt++)
{
    watchdog_update();
    bytesXfer = ESP_readFloppyDiskFile(filename, ..., diskNo);
    if (bytesXfer > 0) break;
    watchdog_update();
    sleep_ms(500);
}

// QD disk change — kick watchdog while waiting for Core 1 hold acknowledge
z80CPU->hold = true;
for (int w = 3000; !z80CPU->holdAck && w > 0; w--)
{
    sleep_ms(1);
    if ((w % 1000) == 0) watchdog_update();
}

Registri Scratch del Watchdog

L'RP2350 fornisce otto registri scratch a 32 bit nel blocco hardware del watchdog che sopravvivono ai reset del watchdog ma vengono cancellati al reset di accensione. Il firmware picoZ80 ne usa cinque per mantenere una cronologia diagnostica completa del boot:
// src/model/BaseZ80/main.c

// Scratch register allocation
#define BOOTP_SCR_MAGIC    5   // Magic marker: 0xB00710BE
#define BOOTP_SCR_STAGE    6   // Current boot stage code
#define BOOTP_SCR_RSTCAUS  7   // Reset cause from hardware
// scratch[0-3] = boot attempt history (FIFO, most recent in [3])
// scratch[4]   = SPI diagnostic counters (packed bitfield)

#define BOOTP_MAGIC  0xB00710BE  // "BOOT-PROBE" marker

// Inline function to record current boot stage
static inline void bootStage(uint32_t stage)
{
    watchdog_hw->scratch[BOOTP_SCR_STAGE] = stage;
}

// Usage throughout boot sequence:
bootStage(BOOTP_START);         // 0x01 — entry point
// ... clock setup ...
bootStage(BOOTP_CLK_SET);       // 0x02
// ... PSRAM init ...
bootStage(BOOTP_PSRAM_INIT);    // 0x03
watchdog_update();
bootStage(BOOTP_PSRAM_OK);      // 0x04
// ... and so on through BOOTP_MAIN_LOOP (0x10)
A ogni reset del watchdog, lo stage corrente e la causa del reset vengono spostati nel FIFO della cronologia (scratch[0–3]) prima di essere sovrascritti. Questo vi da gli ultimi quattro tentativi di reset, rendendo possibile distinguere un glitch occasionale da un errore di boot ripetuto in una fase specifica.

Riferimento delle Fasi di Boot

Codice Costante Descrizione
0x01BOOTP_STARTPunto di ingresso raggiunto
0x02BOOTP_CLK_SETClock di sistema configurato (frequenza CPU, frequenza PSRAM, tensione)
0x03BOOTP_PSRAM_INITInizializzazione PSRAM avviata
0x04BOOTP_PSRAM_OKPSRAM inizializzata e testata
0x05BOOTP_STDIO_INITUSB stdio inizializzato
0x06BOOTP_PIO_INITState machine PIO caricate e avviate
0x07BOOTP_Z80_INITContesto CPU Z80 creato
0x08BOOTP_USB_INITBridge USB inizializzato
0x0ABOOTP_ESP_HS_SYNCSincronizzazione handshake SPI ESP32
0x0BBOOTP_CORE1_LAUNCHCore 1 lanciato tramite multicore_launch_core1()
0x0DBOOTP_FSPI_INITIPC binario FSPI inizializzato (canali DMA rivendicati)
0x0EBOOTP_ESP_INITLayer di comunicazione ESP32 pronto
0x10BOOTP_MAIN_LOOPLoop principale avviato — boot completato
0x11BOOTP_ML_POLL_USBLoop principale: polling USB
0x12BOOTP_ML_INTERCORELoop principale: elaborazione comandi inter-core
0x20BOOTP_IC_DEQUEUEInter-core: dequeue della richiesta
0x21BOOTP_IC_FD_LOADInter-core: caricamento immagine floppy disk
0x22BOOTP_IC_QD_LOADInter-core: caricamento immagine QuickDisk
0x23BOOTP_IC_RF_LOADInter-core: caricamento immagine RAMFILE
0x24–0x27BOOTP_IC_FILE_*Inter-core: caricamento/scrittura/risposta/completamento file
Debug di un reset del watchdog: Collegare una sonda SWD, fermare l'RP2350 e leggere i registri scratch. Se scratch[5] == 0xB00710BE, i registri contengono dati validi sul progresso del boot. Leggere scratch[6] per il codice della fase. Ad esempio, se scratch[6] == 0x0A (BOOTP_ESP_HS_SYNC), il firmware si e' bloccato durante l'handshake SPI dell'ESP32 — verificare che l'ESP32 sia programmato e in esecuzione, e controllare il cablaggio SPI.

ICE Debug Shell (dbgsh.c)

La debug shell (dbgsh.c / dbgsh.h) implementa un debugger ICE a 49 comandi su USB CDC Channel 1. Viene eseguita su Core 0 e comunica con Core 1 tramite flag condivisi nella struttura di contesto t_Z80CPU:
  • cpu->hold / cpu->holdAck — handshake di pausa/ripresa tra la shell su Core 0 e il loop di emulazione su Core 1.
  • cpu->dbgBpAddr[DBG_MAX_BP] — array di indirizzi breakpoint (8 slot, 0xFFFF = non usato). Core 1 controlla prima di ogni fetch di opcode.
  • cpu->dbgStepCount — contatore single-step. Core 1 decrementa dopo ogni istruzione e si mette automaticamente in hold a zero.
  • cpu->dbgTrace[DBG_TRACE_SZ] — buffer circolare a 512 voci che registra [31:16]=PC, [15:8]=opcode, [7:0]=F per ogni istruzione eseguita.
Pattern di implementazione chiave: la shell usa dbg_sprintf() (un printf leggero residente in RAM) per evitare stalli della flash XIP quando si stampa da Core 0 mentre Core 1 sta attivamente usando la PSRAM. L'accesso fisico a memoria e I/O viene eseguito tramite Z80CPU_readPhysicalMem() / Z80CPU_writePhysicalMem() / Z80CPU_readPhysicalIO() / Z80CPU_writePhysicalIO(), che pilotano cicli bus Z80 reali attraverso le state machine PIO.
Sistema di hook di debug: Diversi comandi (mmutrace, ipl) usano un meccanismo di callback per-driver — ogni driver persona puo' registrare il proprio gestore di trace o reset, cosi' l'output della debug shell si adatta al contesto macchina attivo senza codificare logica specifica della macchina in dbgsh.c.

Gestori di Errori e Diagnostica PSRAM

Il firmware installa gestori di fault Cortex-M33 che catturano uno snapshot diagnostico completo nella PSRAM prima di permettere al watchdog di resettare il sistema. Questo fornisce capacita' di analisi post-mortem senza richiedere una sessione di debugger live — essenziale per diagnosticare fault intermittenti in un sistema real-time multi-core.

Struttura Diagnostica PSRAM per i Fault

Gli ultimi 256 byte della PSRAM da 8MB (indirizzo 0x117FFF00) sono riservati per la diagnostica dei fault. Quando si verifica un fault, il gestore salva uno snapshot completo dei registri:
// src/fault_handlers.c

#define PSRAM_DIAG_ADDR   0x117FFF00   // Last 256 bytes of 8MB PSRAM
#define PSRAM_DIAG_MAGIC  0xFA017000   // "FAULT" marker

typedef struct {
    uint32_t  magic;       // PSRAM_DIAG_MAGIC if valid
    uint32_t  faultType;   // 1=Hard, 2=MemManage, 3=BusFault, 4=UsageFault
    uint32_t  pc;          // Program counter at fault
    uint32_t  lr;          // Link register (return address)
    uint32_t  sp;          // Stack pointer
    uint32_t  r0, r1, r2, r3, r12;  // General-purpose registers
    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;      // Which core faulted (0 or 1)
} t_PsramFaultDiag;

Implementazione dei Gestori di Fault

Ogni tipo di fault (hard fault, memory management fault, bus fault, usage fault) ha un wrapper in assembly che estrae il Main Stack Pointer (MSP) o il Process Stack Pointer (PSP) — a seconda di quale era attivo al momento del fault — e lo passa a un gestore C comune. Il gestore C:
  1. Scrive la struttura diagnostica nella PSRAM all'indirizzo 0x117FFF00 con il marcatore magic e il tipo di fault appropriati.
  2. Stampa il dump dei registri e i dettagli del fault tramite debugf() (se l'USB e' disponibile).
  3. Entra in un loop infinito (while(1)), permettendo al watchdog di attivare un reset.
Al successivo avvio riuscito, il firmware controlla 0x117FFF00 per il marcatore PSRAM_DIAG_MAGIC. Se presente, scarica le informazioni di fault salvate tramite debugf() e cancella il marcatore, fornendo una traccia post-mortem completa.
// Interpreting fault diagnostics (from GDB or from debugf output):
//
// faultType=1 (Hard Fault):
//   Check HFSR bit 30 (FORCED) — indicates escalated fault.
//   Check CFSR for the original fault type.
//
// faultType=3 (Bus Fault):
//   Check CFSR bits [15:8] for bus fault status.
//   If BFARVALID (bit 15), BFAR contains the faulting address.
//   Common cause: PSRAM SPI contention between Core 0 and Core 1.
//
// faultType=4 (Usage Fault):
//   Check CFSR bits [25:16] for usage fault status.
//   UNDEFINSTR = undefined instruction (corrupted code in Flash/PSRAM).
//   DIVBYZERO  = division by zero (if enabled).
//
// PC value: the instruction that faulted.
// LR value: the return address (caller of the faulting function).

Mappa di Memoria Diagnostica PSRAM

La parte superiore della PSRAM da 8MB e' suddivisa in tre regioni diagnostiche:
Intervallo Indirizzi Dimensione Contenuti
0x117EF004 – 0x117FEFFF 64KB Buffer di output debugf() (puntatore volatile a 0x117EF004)
0x117FF000 – 0x117FFEFF ~4KB Log di boot persistente plogf()
0x117FFF00 – 0x117FFFFF 256B Snapshot diagnostica dei fault
Tutte e tre le regioni sopravvivono ai reset del watchdog perche' la PSRAM mantiene i suoi contenuti finche' l'alimentazione e' mantenuta. Al reset di accensione, i contenuti sono indefiniti e il firmware li reinizializza controllando i marcatori magic.

Protocollo IPC Binario (FSPI v1.1)

L'RP2350 comunica con l'ESP32 tramite un link SPI a 4 fili da 50MHz usando un protocollo IPC binario (versione 1.1). Questo sostituisce un precedente protocollo basato su testo con un formato di frame binario strutturato che supporta verifica di integrita' CRC32, trasferimenti burst di settori e canali DMA pre-allocati per ridurre la latenza e migliorare l'affidabilita'.
Il firmware ESP32 supporta tre modalita' di rete selezionabili al momento della compilazione: Solo WiFi, WiFi+NCM (entrambi simultaneamente) e Solo NCM. Sono forniti file sdkconfig pre-compilati per ogni modalita' (sdkconfig.mode_wifi_only, sdkconfig.mode_wifi_and_ncm, sdkconfig.mode_ncm_only). In modalita' NCM, l'ESP32 presenta un adattatore Ethernet USB CDC-NCM con un server DHCP integrato (IP predefinito: 192.168.7.1), permettendo l'accesso all'interfaccia web senza hardware WiFi. La modalita' Solo NCM e' richiesta per le schede spedite senza certificazione FCC/RED.

Struttura del Frame

Ogni transazione IPC consiste in un header fisso da 64 byte seguito da un payload opzionale e un trailer CRC32 da 4 byte:
// 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_* (response only)
    uint8_t   seqNum;                 // 3:  Sequence number (retry detection)
    uint16_t  payloadLen;             // 4:  Payload bytes (little-endian)
    uint16_t  sectorCount;            // 6:  Sectors in burst (1–16)
    uint32_t  fileOffset;             // 8:  Byte offset in file
    uint8_t   diskNo;                 // 12: Drive number
    uint8_t   flags;                  // 13: IPCF_FLAG_*
    uint16_t  reserved;               // 14: Reserved
    char      filename[48];           // 16: Null-terminated path (48 bytes)
} t_IpcFrameHdr;                      // Total: 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 sectors per burst
#define IPCF_SECTOR_SIZE    512       // Bytes per sector
#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

Opcode dei Comandi

Opcode Nome Descrizione
0x00 IPCF_CMD_NOP Nessuna operazione (TX fittizio durante lettura full-duplex)
0x01 IPCF_CMD_RDS Lettura singolo settore da 512 byte
0x02 IPCF_CMD_WRS Scrittura singolo settore da 512 byte
0x03 IPCF_CMD_RBURST Lettura burst: 1–16 settori in una singola transazione SPI
0x04 IPCF_CMD_WBURST Scrittura burst: 1–16 settori
0x05 IPCF_CMD_RFILE Lettura file intero (suddiviso in chunk se supera il payload massimo)
0x06 IPCF_CMD_WFILE Scrittura file intero
0x07 IPCF_CMD_INF Trasferimento info versione/partizione RP2350 all’ESP32
0x08 IPCF_CMD_RFD Lettura file immagine floppy disk
0x09 IPCF_CMD_RQD Lettura file immagine QuickDisk
0x0A IPCF_CMD_RRF Lettura immagine backup RAMFILE

DMA e Integrita'

  • Canali DMA pre-allocati: I canali DMA TX e RX (gDmaTx, gDmaRx) vengono rivendicati una volta all'inizializzazione FSPI e non vengono mai rilasciati. Questo elimina l'overhead di claim/unclaim per trasferimento e previene le race condition di esaurimento canali DMA che possono verificarsi quando Core 1 sta contendendo il bus QMI.
  • Elevazione priorita' RX: Il canale DMA RX e' impostato ad ALTA PRIORITA' per prevenire l'overflow del FIFO RX SPI. Gli accessi PSRAM di Core 1 tramite il bus QMI possono stallare il fabric del bus AHB, e se il canale DMA RX e' a priorita' normale i suoi trasferimenti possono essere ritardati abbastanza a lungo da causare l'overflow del FIFO SPI.
  • Integrita' CRC32: Ogni frame e' protetto da un CRC32 IEEE 802.3 standard (polinomio 0xEDB88320, riflesso). L'ESP32 usa esp_rom_crc32_le() che produce lo stesso risultato. In caso di mismatch CRC il frame viene ritentato, usando il contatore di sequenza (seqNum) per il rilevamento dei duplicati.
  • Integrazione watchdog: Le attese DMA includono un timeout di 2 secondi con kick del watchdog ogni secondo. Se un trasferimento DMA si blocca (ad esempio a causa di un reset dell'ESP32), i canali vengono interrotti, il CS viene rilasciato e il boot continua.

Siti di Riferimento

Risorsa Link
Pagina del progetto picoZ80 /picoz80/
Manuale Utente picoZ80 /picoz80-usermanual/
Guida Tecnica picoZ80 /picoz80-technicalguide/
Pagina del progetto pico6502 /pico6502/
Datasheet RP2350 datasheets.raspberrypi.com
API Multicore Pico SDK raspberrypi.github.io/pico-sdk-doxygen
Libreria Zeta Z80 github.com/superzazu/z80
Manuale Utente CPU Zilog Z80 zilog.com
Libreria cJSON github.com/DaveGamble/cJSON

Avviso Normativo Wireless

Questo dispositivo incorpora un modulo wireless ESP32-S3-PICO-1 che trasmette nella banda ISM a 2,4 GHz, rendendolo un radiatore intenzionale ai sensi delle normative sulle radiofrequenze in tutto il mondo (incluso FCC Part 15 Subpart C negli Stati Uniti e la Direttiva sulle Apparecchiature Radio 2014/53/UE nell'Unione Europea).
Sebbene il modulo ESP32-S3-PICO-1 stesso possieda certificazioni normative preesistenti (FCC, CE e altre), tali certificazioni a livello di modulo non si estendono automaticamente a un prodotto finito che incorpora il modulo. L'esenzione per moduli pre-certificati permette ai singoli hobbisti di costruire un numero limitato di dispositivi per uso personale, sperimentale o educativo senza ottenere un'autorizzazione per apparecchiature separata.
Limitazioni Importanti
  • I dispositivi assemblati non devono essere venduti, messi in vendita, regalati o altrimenti distribuiti a terzi a meno che il prodotto finito non sia stato testato indipendentemente e gli sia stata concessa la propria autorizzazione per apparecchiature (ad esempio FCC ID, marcatura CE con valutazione di un Organismo Notificato) nella giurisdizione pertinente.
  • La costruzione di questo progetto per uso personale in quantita' limitate e' generalmente consentita ai sensi delle disposizioni per hobbisti e uso sperimentale (ad esempio FCC § 15.23), a condizione che il dispositivo non causi interferenze dannose.
  • I requisiti normativi variano da paese a paese. I costruttori al di fuori degli Stati Uniti dovrebbero consultare la propria autorita' nazionale per le radiofrequenze per le regole applicabili.
Responsabilita' del Costruttore
E' responsabilita' esclusiva del costruttore assicurarsi che qualsiasi dispositivo costruito da questi progetti sia conforme a tutte le normative sulle radiofrequenze applicabili nella propria giurisdizione. L'autore fornisce questi progetti per uso personale, educativo e hobbistico e non dichiara che un dispositivo costruito da essi soddisfi i requisiti normativi per la distribuzione commerciale.