picoZ80 Guida dello Sviluppatore
picoZ80 Guida dello Sviluppatore
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
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
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.
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);
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.
z80_data) per un ciclo di lettura:
- Imposta IRQ 1 e attende — "pronta per direzione/dati".
- Core 1 cancella IRQ 1 dopo aver inviato la direzione dei pin (modalita' input) e un byte dati fittizio.
- La SM imposta le direzioni dei pin a input (tristate), permettendo alla memoria host di pilotare D0–D7.
- La SM attende su
wait 0 irq 0fino a quando il cambio dell'indirizzo successivo indica che il ciclo sta terminando. - La SM riporta le direzioni dei pin a uno stato noto.
Tipi e Strutture Dati Chiave
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
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
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
memioPtre' 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
memioPtrinstallato, 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
_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)
// 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)
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;
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)
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;
- 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
membankPtrcompleto (codificato allo stesso modo dicpu->_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
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);
- cpu — puntatore al contesto Z80CPU. Vi da accesso a
_membankPtr[],_z80PSRAMe 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). - read —
truese questo e' un ciclo di lettura (lo Z80 sta leggendo);falsese 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).
- 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
memioPtrsu 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.
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
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
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;
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.
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
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);
}
}
}
- 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, richiamandoZ80CPU_readMem(),Z80CPU_writeMem(),Z80CPU_readIO()eZ80CPU_writeIO()per ogni transazione bus.
Dispatch Lettura Memoria
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);
}
}
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
- Driver di primo livello (chiamati anche persona) — registrati in
virtualFuncMap[]inZ80CPU.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
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]);
// 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
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.
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.
{"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(...).
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) |
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.
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 (0x0000–0x003F → blocco dei vettori, 0x0040–0x01FF → l'inizio della TPA).
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
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)
"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
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 |
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
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')
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 comecpu->_Z80.inta, la callback di interrupt-acknowledge.MZ800_retiHandler()— installato comecpu->_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.
- 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
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]);
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
// 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,
};
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 daZ80CPU_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()
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
}
_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()
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;
}
Scrivere un Nuovo Driver — Passo per Passo
Passo 1 — Creare i File Sorgente
// 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
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
)
target_include_directories.
Passo 3 — Includere l'Header in Z80CPU.c
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
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},
};
"mydriver", "MyDriver" e "MYDRIVER" nel JSON corrisponderanno tutti a questa voce.
Passo 5 — Aggiungere il Driver a config.json
"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
BaseZ80(pZ80-BaseZ80) — binario universale con tutti i driver (Sharp + Amstrad + Tatung). DefinisceINCLUDE_SHARP_DRIVERS,INCLUDE_AMSTRAD_DRIVERSeINCLUDE_TATUNG_DRIVERS.SharpZ80(pZ80-SharpZ80) — solo driver Sharp MZ. DefinisceINCLUDE_SHARP_DRIVERS. Produce un binario firmware piu' piccolo.AmstradZ80(pZ80-AmstradZ80) — solo driver Amstrad PCW. DefinisceINCLUDE_AMSTRAD_DRIVERSeTARGET_MODEL_AMSTRAD. Produce un binario firmware piu' piccolo.TatungZ80(pZ80-TatungZ80) — solo driver Tatung Einstein. DefinisceINCLUDE_TATUNG_DRIVERSeTARGET_MODEL_TATUNG. Produce un binario firmware piu' piccolo.
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
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
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.
idf.py menuconfig) o i file sdkconfig pre-compilati elencati sopra.
Pattern di Hook Memoria in Dettaglio
Pattern 1 — Dispositivo Virtuale Puro (blocco FUNC)
// 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
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
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)
// 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
// 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
Funzioni Richieste per una Sotto-Interfaccia
// 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
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},
};
"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
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)
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 globaleg_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},
"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
Utilizzo della Coda Intercore
- 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).
- Il gestore imposta un flag di stato (ad esempio
diskState.pendingRead = true) e ritorna immediatamente — non esegue l'I/O. - 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 incpu->requestQueue. - 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. - Il vostro
task_ptrviene 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,fopeno 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. Usareplogf()al posto didebugf()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[]oioPtr[]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
debugfinZ80CPU_getVirtualFunc()se il vostro driver non si inizializza —debugfe' 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
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");
- 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
0x117EF004deve 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
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
Timer Watchdog
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
// 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)
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 |
|---|---|---|
0x01 | BOOTP_START | Punto di ingresso raggiunto |
0x02 | BOOTP_CLK_SET | Clock di sistema configurato (frequenza CPU, frequenza PSRAM, tensione) |
0x03 | BOOTP_PSRAM_INIT | Inizializzazione PSRAM avviata |
0x04 | BOOTP_PSRAM_OK | PSRAM inizializzata e testata |
0x05 | BOOTP_STDIO_INIT | USB stdio inizializzato |
0x06 | BOOTP_PIO_INIT | State machine PIO caricate e avviate |
0x07 | BOOTP_Z80_INIT | Contesto CPU Z80 creato |
0x08 | BOOTP_USB_INIT | Bridge USB inizializzato |
0x0A | BOOTP_ESP_HS_SYNC | Sincronizzazione handshake SPI ESP32 |
0x0B | BOOTP_CORE1_LAUNCH | Core 1 lanciato tramite multicore_launch_core1() |
0x0D | BOOTP_FSPI_INIT | IPC binario FSPI inizializzato (canali DMA rivendicati) |
0x0E | BOOTP_ESP_INIT | Layer di comunicazione ESP32 pronto |
0x10 | BOOTP_MAIN_LOOP | Loop principale avviato — boot completato |
0x11 | BOOTP_ML_POLL_USB | Loop principale: polling USB |
0x12 | BOOTP_ML_INTERCORE | Loop principale: elaborazione comandi inter-core |
0x20 | BOOTP_IC_DEQUEUE | Inter-core: dequeue della richiesta |
0x21 | BOOTP_IC_FD_LOAD | Inter-core: caricamento immagine floppy disk |
0x22 | BOOTP_IC_QD_LOAD | Inter-core: caricamento immagine QuickDisk |
0x23 | BOOTP_IC_RF_LOAD | Inter-core: caricamento immagine RAMFILE |
0x24–0x27 | BOOTP_IC_FILE_* | Inter-core: caricamento/scrittura/risposta/completamento file |
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)
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]=Fper ogni istruzione eseguita.
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
Struttura Diagnostica PSRAM per i Fault
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
- Scrive la struttura diagnostica nella PSRAM all'indirizzo
0x117FFF00con il marcatore magic e il tipo di fault appropriati. - Stampa il dump dei registri e i dettagli del fault tramite
debugf()(se l'USB e' disponibile). - Entra in un loop infinito (
while(1)), permettendo al watchdog di attivare un reset.
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
| 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 |
Protocollo IPC Binario (FSPI v1.1)
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
// 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 usaesp_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
- 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.
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.