picoZ80 Guide du Développeur
Guide du Developpeur picoZ80
MZ700.c) est utilise tout au long du document comme exemple pratique concret.
Aucune experience prealable avec la base de code picoZ80 n'est requise. Chaque concept est explique depuis les fondamentaux puis illustre dans le code source reel. A la fin de ce guide, vous serez en mesure d'ecrire un pilote complet a partir de zero, de l'ajouter au systeme de compilation, de l'enregistrer dans le framework et de le configurer via JSON.
Pour l'architecture materielle, les details de l'interface bus PIO et la reference de configuration JSON, consultez le Guide Technique picoZ80. Pour la mise en route utilisateur, consultez le Manuel Utilisateur picoZ80.
Arborescence du Code
projects/tzpuPico/ a la racine du depot (l'emplacement exact depend de votre systeme). L'arborescence ci-dessous montre les fichiers pertinents pour le developpement de pilotes :
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
Interface Bus PIO -- Comment le Code C Pilote les State Machines
z80.pio), mais le code C sur le Core 1 orchestre quel cycle de bus s'execute et quand. Comprendre cette interaction est important pour quiconque debogue le timing du bus ou ajoute de nouveaux types de cycles.
Le mecanisme central est out exec, 16 -- une instruction PIO qui extrait une valeur 16 bits du TX FIFO et l'execute comme une instruction PIO. La state machine z80_cycle (PIO 0 SM 2) l'utilise dans une boucle serree :
// 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 l'execution, la boucle principale du Core 1 pousse la sequence appropriee dans le FIFO en fonction de la transaction bus en cours :
// 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) et la SM de donnees (z80_data) utilisent chacune leur propre drapeau IRQ (IRQ 0 et IRQ 1 respectivement) pour implementer un handshake producteur/consommateur :
- La SM positionne son drapeau IRQ et se bloque sur
wait 0 irq N-- "je suis prete, envoyez-moi des donnees". - Le Core 1 pousse l'adresse ou les donnees dans le TX FIFO de la SM, puis efface le drapeau IRQ.
- La SM se reveille, extrait les donnees du FIFO et pilote les broches.
z80_data) pour un cycle de lecture :
- Positionne IRQ 1 et attend -- "prete pour la direction/les donnees".
- Le Core 1 efface IRQ 1 apres avoir pousse la direction des broches (mode entree) et un octet de donnees factice.
- La SM configure les directions des broches en entree (tristate), permettant a la memoire hote de piloter D0-D7.
- La SM attend sur
wait 0 irq 0jusqu'a ce que le prochain changement d'adresse indique la fin du cycle. - La SM remet les directions des broches dans un etat connu.
Types et Structures de Donnees Cles
src/include/Z80CPU.h. Ces structures sont passees a chaque fonction de pilote et constituent le moyen principal par lequel un pilote interagit avec le systeme memoire et E/S.
Constantes de Type de Bloc Memoire
membankPtr. Le type indique a la boucle de dispatch du Core 1 comment gerer les transactions bus qui tombent dans ce bloc.
// 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() et Z80CPU_writeMem() :
- PHYSICAL / PHYSICAL_VRAM / PHYSICAL_HW -- le RP2350 n'intercepte pas la transaction bus ; le materiel reel de la carte hote repond. Utilisez ceci pour toute region ou les propres puces de l'hote (ROM, RAM, materiel video) doivent rester en controle.
- RAM -- les lectures et ecritures vont vers un banc de 64 Ko dans la PSRAM de 8 Mo. Si une fonction
memioPtrest installee pour l'adresse specifique, cette fonction est appelee a la place de (pour FUNC) ou en complement de l'acces PSRAM (les gestionnaires peuvent intercepter ou post-traiter). Les wait states et la synchronisation T1 sont configurables par bloc. - ROM -- les lectures proviennent de la PSRAM (generalement chargee depuis un fichier image au demarrage). Les cycles d'ecriture atteignent toujours tout gestionnaire
memioPtrinstalle mais la PSRAM n'est pas modifiee -- utile pour les registres de bank switching situes dans une region d'adresses mappee en ROM. - VRAM -- les lectures viennent de la PSRAM ; les ecritures vont a la fois dans la PSRAM et vers la VRAM physique de l'hote en parallele. Cela permet au logiciel de maintenir une copie miroir du tampon video tout en mettant a jour l'ecran reel.
- FUNC -- il n'y a pas de support PSRAM. Chaque lecture et ecriture appelle la fonction installee dans
memioPtr[addr]. Utilisez ceci pour les registres materiels virtualises, les ports de controle de banking mappes en memoire, et toute ressource sans RAM reelle derriere. - PTR -- chaque octet du bloc de 512 octets peut independamment pointer vers un emplacement PSRAM different ou un type de memoire different. Utilise pour une manipulation tres fine de l'espace d'adressage.
Encodage de membankPtr
_membankPtr[] contient 128 entrees -- une par bloc de 512 octets de l'espace d'adressage Z80 de 64 Ko. Chaque entree est une seule valeur 32 bits qui encode trois champs :
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 vaut 512 octets. Il y a 128 blocs couvrant 0x0000-0xFFFF. L'index de bloc pour une adresse Z80 donnee est : addr / MEMORY_BLOCK_SIZE = addr >> 9.
Attributs Memoire (t_memAttr)
t_memAttr qui controle les wait states et la synchronisation T-cycle. Ils sont stockes dans un tableau 2D indexe par banc et bloc :
// 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 retarde l'acces PSRAM jusqu'au front montant T1 du cycle de bus en cours. Cela empeche les operations internes de la PSRAM d'introduire une derive de timing dans les logiciels hotes qui dependent d'un timing cycle-horloge precis (E/S cassette, bit-banging serie, boucles de temporisation). Positionnez a true pour les regions RAM/ROM auxquelles les logiciels sensibles au timing de l'hote accederont.
La Structure PSRAM (t_Z80PSRAM)
t_Z80PSRAM. Celle-ci est allouee une fois au demarrage et pointee par cpu->_z80PSRAM. C'est la structure de donnees la plus grande et la plus importante du systeme -- tout ce a quoi le Z80 peut acceder reside ici.
// 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[] -- stockage brut d'octets pour tous les bancs memoire supportes par la PSRAM. La taille totale est de 64 bancs x 64 Ko = 4 Mo. Les images ROM chargees depuis la carte SD ou la Flash y sont ecrites au demarrage. Pendant l'execution du Z80, les lectures et ecritures vers les blocs de type RAM/ROM/VRAM accedent a ce tableau.
- memPtr[] -- utilise uniquement par les blocs de type PTR. Chaque entree est une valeur
membankPtrcomplete (encodee de la meme maniere quecpu->_membankPtr[]) qui redirige l'acces d'un seul octet vers un emplacement completement different. Permet le remapping au niveau de l'octet dans l'espace de 64 Ko. - memioPtr[] -- la table de hooks de fonctions memoire. Un emplacement par adresse Z80. Lorsqu'un emplacement est non-NULL, le Core 1 appelle la fonction a cet emplacement a chaque acces memoire a cette adresse, quel que soit le type de bloc (RAM, ROM ou FUNC). C'est ainsi que les pilotes interceptent ou redefinissent des emplacements memoire specifiques sans changer le type de bloc global.
- ioPtr[] -- la table de hooks des ports E/S. Un emplacement par adresse E/S Z80 (port A0-A15 en largeur, bien que le Z80 n'utilise que A0-A7 comme numero de port reel ; les bits superieurs peuvent porter un contexte supplementaire). Lorsque non-NULL, la fonction est appelee pour chaque instruction IN ou OUT ciblant ce port. Si NULL, le cycle E/S passe au materiel physique.
La Signature du Gestionnaire MemoryFunc
memioPtr[] et ioPtr[] contiennent tous deux des pointeurs de fonction du meme type -- MemoryFunc :
// src/include/Z80CPU.h typedef uint8_t (*MemoryFunc)(Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
- cpu -- pointeur vers le contexte Z80CPU. Donne acces a
_membankPtr[],_z80PSRAMet tout le reste. Ne modifiez jamais_membankPtr[]depuis l'interieur d'un gestionnaire E/S qui peut etre appele depuis la boucle principale du Core 1 ; utilisez la file inter-core pour de telles operations (voir Interaction entre Cores). - read --
truesi c'est un cycle de lecture (le Z80 lit) ;falsesi c'est un cycle d'ecriture (le Z80 ecrit). - addr -- l'adresse Z80 complete (0x0000-0xFFFF pour la memoire, 0x0000-0xFFFF pour les ports E/S). Pour les E/S, le Z80 n'utilise que les 8 bits inferieurs comme numero de port reel (A0-A7) ; les 8 bits superieurs (A8-A15) sont la valeur du registre B pendant l'instruction.
- data -- en cycle d'ecriture, l'octet que le Z80 ecrit. En cycle de lecture depuis un bloc RAM/ROM, c'est la valeur actuelle a cette adresse dans la PSRAM (vous pouvez l'utiliser ou l'ignorer).
- En lecture : l'octet a retourner au Z80. C'est la valeur que le Z80 voit sur le bus de donnees.
- En ecriture : la valeur de retour est generalement inutilisee pour les gestionnaires E/S. Pour les gestionnaires
memioPtrsur les blocs de type RAM, la valeur de retour est reecrite dans la PSRAM a la place des donnees originales -- utilisez ceci pour modifier ou assainir ce qui est stocke.
debugf, ou effectuer toute operation susceptible de bloquer le Core 1. Les E/S fichier et la communication UART doivent etre postees au Core 0 via la file inter-core.
La Structure de Contexte Z80CPU
Z80CPU *cpu. C'est le contexte maitre de l'ensemble de l'emulation. Les champs les plus pertinents pour les auteurs de pilotes sont :
// 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
};
Structures de Configuration des Pilotes
t_drvConfig qui a ete remplie par l'analyseur de configuration JSON. Cela indique au pilote quelles interfaces lui ont ete attribuees, quelles images ROM charger, quels remappages d'adresses appliquer et quels parametres l'utilisateur a configures.
// 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 de l'interface plutot que de le coder en dur : le dstaddr de l'entree devient la base de la carte (avec srcaddr le port authentique de la carte), et chacune de ces cartes porte une constante *_DEFAULT_BASE dans son en-tete, utilisee lorsqu'aucune entree iomap n'est presente. Les cartes relocalisables et leurs valeurs par defaut sont 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) et Celestite (0x60/16). Les cles JSON iomap/addrmap sont en minuscules (srcaddr / dstaddr) et interpretees comme des nombres ; le champ Base I/O Port de la page Configuration GUI les ecrit pour vous.
reset_ptr, poll_ptr et task_ptr ne sont pas definis depuis le JSON -- ils sont definis par la fonction init de votre pilote afin que le Core 1 puisse appeler les fonctions de maintenance de votre pilote aux moments appropries.
La Boucle de Dispatch du Core 1
Z80CPU_cpu(). Comprendre ce qui se passe dans cette boucle est fondamental pour ecrire des pilotes corrects.
Structure de la Boucle 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);
}
}
}
- La boucle execute
z80_run()pour 2048 cycles par iteration. Entre les iterations, tous les gestionnaires poll des pilotes sont appeles. Cela signifie que les gestionnaires poll sont appeles environ toutes les 2048 cycles d'horloge Z80 -- a 3,5 MHz cela represente environ 585 microsecondes. - Les gestionnaires poll doivent etre extremement courts. Ils sont sur le chemin critique de l'emulation. Un gestionnaire poll lent introduit une gigue dans le timing du bus Z80.
- La fonction
z80_run()(de la bibliotheque Zeta Z80) execute les instructions Z80, rappelantZ80CPU_readMem(),Z80CPU_writeMem(),Z80CPU_readIO()etZ80CPU_writeIO()pour chaque transaction bus.
Dispatch Lecture Memoire
Z80CPU_readMem() est appelee. Cette fonction est le coeur du systeme memoire -- la comprendre vous dit exactement ce que vos gestionnaires doivent faire.
// 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 Ecriture Memoire
// 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 des Ports E/S
// 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[], soit elle passe au materiel physique. Il n'y a pas de distinction ROM/RAM/FUNC pour les E/S.
Notez egalement : le Z80 utilise une adresse 16 bits pour les instructions E/S -- les 8 bits inferieurs sont le numero de port ; les 8 bits superieurs contiennent le contenu du registre B (pendant les instructions IN r,(C) / OUT (C),r). Si vous souhaitez un comportement different du gestionnaire selon le registre B, examinez l'octet superieur de addr. Pour une correspondance simple sur le numero de port uniquement, masquez avec addr & 0xFF.
Le Framework Pilotes
- Pilotes de niveau superieur (egalement appeles personas) -- enregistres dans
virtualFuncMap[]dansZ80CPU.c. Chaque persona configure une personnalite machine complete : disposition memoire, banking, ports E/S, et optionnellement un ensemble de sous-interfaces (cartes d'interface). - Pilotes d'interface -- enregistres dans le propre
interfaceFuncMap[]de la persona. Chaque pilote d'interface ajoute un peripherique specifique (lecteur de disquettes, QuickDisk, extension RAM, systeme de fichiers) a la persona.
virtualFuncMap -- Enregistrement des Pilotes de Niveau Superieur
virtualFuncMap[] dans src/Z80CPU.c associe un nom sous forme de chaine a une fonction d'initialisation de pilote. Chaque pilote de niveau superieur (persona) doit avoir une entree ici. Le nom sous forme de chaine doit correspondre exactement au champ "name" dans le tableau JSON "drivers" (insensible a la casse).
// 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}, // Sharp MZ-800 persona (dual-mode 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 -- Construire a partir d'un Pilote ou d'un BIOS Existant
src/drivers/Other/Open.c, compile avec build_tzpuPico.sh open) est le point de depart pour deux types d'experimentateurs : celui qui integre le picoZ80 dans une carte de sa propre conception, et celui qui met en route une machine Z80 qui n'a pas encore de persona dedie. Open.c est calque sur MZ700.c mais depouille de tout materiel machine : en mode PHYSICAL, il transmet l'ensemble de l'espace memoire et d'E/S 64K a la carte reelle (les cartes d'interface superposent leurs ports fixes) ; en mode VIRTUAL, il presente une RAM 64K plate dans laquelle les ROM de niveau pilote sont chargees sequentiellement a partir de 0x0000. Il est enregistre comme {"Open", Open_Init} sous #ifdef INCLUDE_OPEN_DRIVERS, et n'offre que les cartes d'interface independantes de la machine (MZ-1R12/1R18/1R23/1R37, PIO-3034, MZ-8BIO3, MZ-1E24, MZ-1E05, Celestite), chacune pouvant etre deplacee vers n'importe quel port d'E/S de base.
src/drivers/Other/Open.c-- le persona vanilla (meilleure base pour une carte entierement nouvelle).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-- une machine autonome avec un FDC uPD765 integre.src/drivers/Tatung/EinsteinTC01.c-- une machine autonome avec un FDC WD1770 integre.
{"MyMachine", MyMachine_Init} au virtualFuncMap[] ci-dessus (sous un #ifdef approprie), et raccordez sa source dans src/CMakeLists.txt (p. ex. ajoutez-la a la liste pZ80_drivers_open_src) plus une cible de modele via add_z80_model_targets(...).
asm/ (assemblees avec l'assembleur GLASS Z80). Prenez-les comme base et recompilez-les ou modifiez-les pour votre machine :
</font>| Objet | Fichiers source |
|---|---|
| ROM de moniteur | 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) |
| Chargeurs d’amorcage (IPL) | TZFS/asm/mz2000_ipl.asm (MZ-2000), TZFS/asm/mz80b_ipl.asm (MZ-80B), RFS/asm/ipl.asm |
| BIOS CP/M | RFS/asm/cbios.asm, RFS/asm/cpm22-bios.asm, TZFS/asm/cbios.asm, TZFS/asm/cbiosII.asm |
| ROM d’amorcage disquette / 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 |
| Systemes de fichiers en ROM | RFS/asm/rfs.asm (en banques), TZFS/asm/tzfs.asm (en banques) |
rom[].loadaddr (pour une carte d'interface) ou, sur OpenZ80 en mode virtuel, comme une ROM de niveau pilote chargee a 0x0000. RFS et TZFS fournissent tous deux des scripts de build autonomes (voir leurs Guides du Developpeur) qui produisent ces images ROM.
TZFS — Modes Memoire et le Processeur de Service Virtuel
src/drivers/Sharp/TZFS.c est un bon exemple concret de pilote combinant l'emulation de modes memoire a commutation de bancs avec un modele de service inter-core. Il implemente un moniteur bas niveau multi-bancs et un systeme de fichiers (une amelioration du MONITOR 1Z-013A) avec CP/M en dessous, modele sur le TZFS du tranZPUter SW et son processeur d'E/S virtuel K64F. Il est enregistre comme interface selectionnable sur le persona MZ-700 (dans MZ700.c, aux cotes de — et en pratique exclusif avec — RFS), et non comme un persona de premier niveau.
0x60 ; le pilote redirige ses pointeurs de banc en reponse. Les modes sont TZMM_ORIG, TZMM_BOOT, TZMM_TZFS, TZMM_TZFS2, TZMM_TZFS3, TZMM_TZFS4, TZMM_CPM, TZMM_CPM2 et TZMM_COMPAT. La disposition CP/M CPM2 utilise une pagination du bloc 0 a granularite octet (0x0000–0x003F → bloc de vecteurs, 0x0040–0x01FF → le debut de la TPA).
OUT (0x68), ce qui met en file un message MSG_TZFS_SVCREQ vers le Core 0 ; le Core 0 le distribue via TZFS_processServiceRequest. Les services du systeme de fichiers sont READDIR / NEXTDIR (blocs de repertoire de 16 entrees mis en cache), READFILE / NEXTREADFILE, LOADFILE (compatible en-tete MZF et objet-banc), CHANGEDIR et CLOSE. Les services CP/M sont LOADBDOS (rechargement CCP + BDOS au demarrage a chaud), ADDSDDRIVE, READSDDRIVE et WRITESDDRIVE, ainsi que des services de frequence CPU. Comme le picoZ80 n'a pas d'acces SD direct, les secteurs CP/M de 512 octets sont routes via l'ESP32 (ESP_readSector / ESP_writeSector) sur des fichiers d'image complete ; les chemins d'image par lecteur proviennent des entrees param[].file du JSON d'interface, avec le modele de repli CPM/SDC16M/RAW/CPMDSK<nn>.RAW. La ROM TZFS (roms/tzfs.bin) est la sortie assemblee du TZFS/asm/tzfs.asm du projet compagnon (son BIOS CP/M depuis TZFS/asm/cbios.asm / cpm22.asm).
Flux d'Initialisation
config.json, l'initialisation des pilotes se deroule dans l'ordre suivant :
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" et "io" sont appliques. Cela signifie que toute entree explicite dans les tableaux memory ou io de config.json ecrasera ce que le pilote a configure pour ces adresses. Cela permet a l'utilisateur d'affiner les valeurs par defaut du pilote sans modifier le code source du pilote.
Callbacks du Cycle de Vie des Pilotes
t_drvConfig pendant l'initialisation. Le Core 1 les appelle a des moments specifiques de l'execution :
| Callback | Signature | Quand appele | Utilisation typique |
|---|---|---|---|
reset_ptr |
uint8_t f(Z80CPU *cpu) |
Ligne RESET hote activee ; Z80 PC = 0x0000 | Restaurer la carte memoire par defaut, reinitialiser l'etat des bancs |
poll_ptr |
uint8_t f(Z80CPU *cpu) |
Toutes les ~2048 cycles Z80 (sur le Core 1) | Verifier les drapeaux d'etat, poster des requetes au Core 0 |
task_ptr |
uint8_t f(Z80CPU *cpu, enum Z80CPU_TASK_NAME task, char *param) |
En reponse aux requetes de taches inter-core | Traiter les resultats d'E/S fichier, livraison de secteurs de disque |
poll_ptr est appele depuis le Core 1 et doit etre rapide. S'il doit effectuer des E/S (charger un secteur de disque, envoyer une commande UART), il doit poster un message au Core 0 via cpu->requestQueue et retourner immediatement. Le Core 0 effectuera les E/S et renverra le resultat via cpu->responseQueue, declenchant task_ptr a la prochaine occasion disponible.
Exemple Pratique : Le Pilote MZ-700
src/drivers/Sharp/MZ700.c) est le pilote persona le plus complet de la base de code. Le parcourir en detail montre chaque patron de conception dont vous aurez besoin pour vos propres pilotes. Le pilote MZ-80A (src/drivers/Sharp/MZ80A.c) fournit une autre implementation de reference, demontrant l'emulation du PIT Intel 8253 et le mecanisme de permutation memoire MEMSW. Le pilote MZ-2000 (src/drivers/Sharp/MZ2000.c) est une reference supplementaire, montrant la commutation de modes memoire BST/NST, la superposition de VRAM caracteres et graphiques avec selection de banc par PIO Z80, et l'integration du FDC MB8866. Il prend en charge le mode physique (remplacement direct du Z80 dans un vrai MZ-2000 avec detection automatique du mode boot/normal) et le mode virtuel (emulation complete basee sur la PSRAM avec mirroring de la ROM IPL). Le pilote MZ-800 (src/drivers/Sharp/MZ800.c) est une persona bimode qui suit le registre GDG Display-Mode et reconstruit a la volee le decodage de sa carte memoire, et illustre le pontage de la chaine d'interruptions daisy-chain en mode virtuel (voir Le Pilote MZ-800 ci-dessous).
Plusieurs modules d'emulation de peripheriques reutilisables sont disponibles : PIT8253.c (Intel 8253 Programmable Interval Timer -- les six modes de compteur, comptage BCD/binaire, verrouillage de compteur, modes de lecture/chargement LSB/MSB), PPI8255.c (Intel 8255 Programmable Peripheral Interface -- E/S Mode 0, positionnement/remise a zero de bits pour le Port C, callbacks de sortie par port et injection d'entree), WD1773.c (controleur de disquettes WD1773 -- utilise par les pilotes persona Sharp MZ), WD1770.c (controleur de disquettes WD1770 -- utilise par la persona Tatung Einstein, prenant en charge les formats Extended CPC DSK, D88 et DSK standard), et uPD765.c (controleur de disquettes NEC uPD765 -- utilise par la persona Amstrad PCW-9512, prenant en charge le format CPC DSK et l'imagerie de disques physiques). Ces modules sont concus pour etre instancies par tout pilote persona machine.
Le Pilote MZ-800 (Persona Bimode)
src/drivers/Sharp/MZ800.c) est un bon exemple de persona dont la carte memoire n'est pas fixe mais change au cours de l'execution sous le controle du logiciel hote. Le MZ-800 est un sur-ensemble du MZ-700 : il demarre dans un mode compatible MZ-700 et peut basculer vers un mode MZ-800 natif avec une disposition memoire et une largeur graphique differentes. Le pilote suit le mode machine et reconstruit sa table de decodage de blocs chaque fois que le mode change.
Registre GDG Display-Mode (port 0xCE). Le pilote installe un gestionnaire ioPtr[] sur le port 0xCE qui espionne les ecritures vers le registre GDG Display-Mode (DMD) :
- Le bit 3 selectionne le mode machine -- efface = MZ-800 natif, positionne = compatibilite MZ-700.
- Le bit 2 selectionne la largeur graphique -- 320 ou 640 pixels.
Lorsqu’une ecriture modifie l’un ou l’autre bit, le pilote appelle MZ800_applyMemoryMap(), qui parcourt les 128 blocs de 512 octets et reecrit a la volee les entrees _membankPtr[] pour refleter la nouvelle disposition. C’est le meme mecanisme de dispatch rapide decrit dans Encodage membankPtr – aucun blocage du bus n’est requis car seule la table de decodage est reconstruite, pas la PSRAM sous-jacente.
Drapeaux de controle de la carte memoire. La disposition courante est conservee dans une petite structure d’etat sous forme de masque binaire de drapeaux : ROM_0000 (ROM monitor visible a 0x0000), ROM_1000 (fenetre CG-ROM a 0x1000), CGRAM_VRAM (RAM/VRAM du generateur de caracteres mappee dans la fenetre 0x1000), et ROM_E000 (ROM IOCS / region materielle a 0xE000). MZ800_applyMemoryMap() derive les types de blocs a partir de ces drapeaux ainsi que des bits de mode MZ-700/MZ-800 et 320/640 courants. La carte de mise sous tension/reset expose la ROM monitor, la CG-ROM et la ROM IOCS. Les ports de banking memoire du MZ-800 (0xE0-0xE6) sont geres par le pilote et repercutes vers le materiel physique afin qu’une vraie machine en aval reste synchronisee.
Pontage de la chaine d’interruptions daisy-chain (mode virtuel). C’est la partie la plus subtile du pilote et elle suit le meme patron que le pilote MZ-2500. En mode MZ-800 natif, l’interruption periodique est generee par le vrai 8253 et acheminee a travers le Z80-PIO physique, qui se trouve dans la chaine d’interruptions daisy-chain. Le verrou in-service du PIO ne s’efface que lorsqu’il observe une lecture d’opcode RETI (ED 4D) sur le bus reel – mais en mode virtuel la routine de service d’interruption et son RETI s’executent depuis la PSRAM, de sorte que le PIO ne les voit jamais et resterait verrouille, bloquant toutes les interruptions suivantes. Le pilote comble ce manque en installant deux hooks dans le cœur Zeta :
MZ800_readIntAck()-- installe commecpu->_Z80.inta, le callback d'acquittement d'interruption.MZ800_retiHandler()-- installe commecpu->_Z80.reti, le callback RETI.
Les deux appellent mz800PhysicalReti(), qui rejoue un RETI physique sur le bus reel : il place les deux octets ED 4D a SP-2 dans la RAM hote reelle, effectue des lectures d’opcode M1 des deux octets sur le bus physique (de sorte que la logique daisy-chain du PIO observe le RETI et efface son verrou in-service), puis restaure les octets d’origine qu’il avait deplaces. Ces hooks ne sont installes que lorsque l’interface fonctionne en mode virtuel (!isPhysical) ; en mode physique le vrai PIO voit directement le vrai RETI et aucun pontage n’est necessaire.
Reset. MZ800_Reset() restaure la carte memoire de mise sous tension, emet un OUT 0xE4 pour imiter le reset du gate-array, efface la zone de travail RFS/MZF (0x1000-0x1168), et – parce que le PIT 8253 n’a pas de broche de reset – reprogramme et masque le 8253 en mode MZ-700 afin qu’un compteur perime ne puisse pas declencher une interruption parasite apres le reset.
Retour a l'Exemple MZ-700
- Les 4 Ko inferieurs (0x0000-0x0FFF) sont une ROM Monitor a la mise sous tension mais peuvent etre remplaces par de la RAM via des ecritures de ports E/S -- le "bank-switching MZ-700".
- La region superieure (0xD000-0xFFFF) contient la Video RAM, la VRAM couleur et des registres materiels mappes en memoire. La region superieure entiere peut egalement etre commutee vers la RAM via des ports E/S.
- Le banking memoire est controle via six ports E/S (0xE0-0xE6) qui permutent les blocs.
Table de Fonctions d'Interface
MZ700.c, une table interfaceFuncMap[] liste toutes les cartes d'interface de peripheriques (sous-pilotes) que la persona MZ-700 connait. C'est l'equivalent MZ-700 de virtualFuncMap[] -- elle associe les noms d'interface (du tableau JSON "if") aux fonctions d'initialisation de chaque carte additionnelle.
// 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[] avec un sous-ensemble different d'interfaces disponibles. L'ensemble complet des pilotes d'interface, leurs chaines JSON "name" pour le tableau "if", et quelles personas les supportent est documente dans la table Compatibilite Persona-Interface du Guide Technique. Pour des exemples de configuration JSON utilisateur, consultez le Manuel Utilisateur picoZ80.
Structure d'Etat du 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 Fonction d'Initialisation -- MZ700_Init()
MZ700_Init() sert un double objectif. Elle est appelee dans deux contextes differents, identifies par les arguments qui sont NULL :
- Mode validation (
ifName != NULL,config == NULL) : Appelee parZ80CPU_configDriversFromJSON()pour demander "supportez-vous ce nom d'interface ?" Retourne 1 si oui, 0 si non. Cela permet a l'analyseur JSON de valider les noms d'interface aupres du pilote avant d'essayer de les initialiser. - Mode configuration (
ifName == NULL,config != NULL) : La veritable initialisation -- configurer la carte memoire, installer les hooks, configurer les interfaces.
// 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;
}
Le Gestionnaire de Banking -- MZ700_IO_MemoryBankPorts()
Z80CPU_writeIO() a chaque fois que le Z80 ecrit sur les ports 0xE0-0xE6. La lecture de ces ports n'a pas d'effet secondaire (retourne 0xFF). L'ecriture modifie la carte memoire en modifiant les entrees _membankPtr[] en temps reel.
// 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[] en reponse a une ecriture E/S pour implementer le bank switching memoire. Les modifications prennent effet immediatement -- le tout prochain acces memoire du Z80 utilisera le nouveau mapping.
Le patron de sauvegarde/restauration pour la region memoire superieure (MZ700Ctrl.upmembankPtr[]) est important : quand vous remplacez des regions mappees sur du materiel par de la RAM, vous devez retenir ce qui s'y trouvait pour pouvoir le restaurer quand le logiciel repermute. Simplement reassigner PHYSICAL perdrait tous les mappings personnalises configures par les sous-pilotes ou la configuration JSON.
Le Gestionnaire de Reset -- MZ700_Reset()
reset_ptr de chaque pilote. Le gestionnaire de reset du MZ-700 restaure la carte memoire a l'etat de mise sous tension (ROM a 0x0000, VRAM et materiel a 0xD000+) puis appelle tous les gestionnaires de reset des interfaces actives :
// 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;
}
Le Gestionnaire 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;
}
Le Processeur de Taches -- 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;
}
Ecrire un Nouveau Pilote -- Etape par Etape
Etape 1 -- Creer les Fichiers Sources
// 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;
}
Etape 2 -- Ajouter au CMakeLists.txt
src/CMakeLists.txt et ajoutez votre nouveau fichier source a la liste des pilotes 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 n'est necessaire.
Etape 3 -- Inclure l'En-tete dans Z80CPU.c
src/Z80CPU.c et ajoutez un include pour l'en-tete de votre pilote a cote des includes existants des pilotes Sharp :
// 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
Etape 4 -- Enregistrer dans virtualFuncMap
src/Z80CPU.c, trouvez le tableau virtualFuncMap[] et ajoutez votre entree. La chaine "MyDriver" est ce que le champ JSON "name" doit contenir :
// 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},
{"MZ1500", MZ1500_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" et "MYDRIVER" dans le JSON correspondront tous a cette entree.
Etape 5 -- Ajouter le Pilote au config.json
"drivers" a votre config.json sur la carte SD. Le champ "name" doit correspondre a la chaine que vous avez enregistree dans 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": []
}
]
}
]
}
}
Etape 6 -- Compiler et Tester
# 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) -- binaire universel avec tous les pilotes (Sharp + Amstrad + Tatung). DefinitINCLUDE_SHARP_DRIVERS,INCLUDE_AMSTRAD_DRIVERSetINCLUDE_TATUNG_DRIVERS.SharpZ80(pZ80-SharpZ80) -- pilotes Sharp MZ uniquement. DefinitINCLUDE_SHARP_DRIVERS. Produit un binaire firmware plus petit.AmstradZ80(pZ80-AmstradZ80) -- pilotes Amstrad PCW uniquement. DefinitINCLUDE_AMSTRAD_DRIVERSetTARGET_MODEL_AMSTRAD. Produit un binaire firmware plus petit.TatungZ80(pZ80-TatungZ80) -- pilotes Tatung Einstein uniquement. DefinitINCLUDE_TATUNG_DRIVERSetTARGET_MODEL_TATUNG. Produit un binaire firmware plus petit.
src/model/ avec un CMakeLists.txt dedie, un point d'entree (main.c) et des scripts de liaison. La variante standard omet le shell de debogage pour une image firmware plus petite. La variante DBGSH ajoute la definition de compilation INCLUDE_DBGSH, qui active le shell de debogage ICE complet sur le canal USB CDC 1. Les noms de fichiers firmware DBGSH sont identifies par le suffixe _DBGSH.
Firmware ESP32 -- Selection du Mode Reseau
sdkconfig pre-construit appropriate :
# 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-- active la radio WiFi et le code AP/Client.CONFIG_IF_USB_NCM_ENABLED-- active l'interface reseau USB NCM et le serveur DHCP.
idf.py menuconfig) ou les fichiers sdkconfig pre-construits listes ci-dessus.
Modeles de Hooks Memoire en Detail
Modele 1 -- Peripherique Virtuel Pur (bloc 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;
}
}
Modele 2 -- Intercepter les Ecritures vers une Region RAM
RAM ; vous installez un gestionnaire memioPtr qui post-traite l'ecriture.
// 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
}
}
Modele 3 -- Intercepter les Ecritures vers une Region ROM
ROM ; les ecritures declenchent votre gestionnaire mais la PSRAM n'est pas modifiee.
// 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;
}
}
Modele 4 -- Gestionnaires Epars (Adresses Individuelles)
// 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
}
}
Modele 5 -- Gestionnaire de Port E/S
// 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;
}
}
Ajouter une Sous-Interface a une Persona Existante
Fonctions Requises pour une Sous-Interface
// 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);
Enregistrer la Sous-Interface
interfaceFuncMap[] dans le fichier C de la persona parente (par ex. 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" doit correspondre au champ "name" de l'entree d'interface dans le tableau JSON "if" (insensible a la casse) :
"drivers": [
{
"enable": 1,
"name": "MZ700",
"type": "VIRTUAL",
"if": [
{
"enable": 1,
"name": "MyCard",
"type": "VIRTUAL",
"rom": [],
"addrmap": [],
"iomap": [],
"param": [
{ "name": "myParam", "value": "42" }
]
}
]
}
]
Lire les Parametres JSON dans une Sous-Interface
t_drvIFConfig *ifConfig passe a l'init de votre sous-interface contient toutes les donnees configurees par JSON. Pour lire un parametre nomme :
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;
}
Exemple Pratique de Sous-Interface : Cartes Serie RS-232C (Z80 SIO)
src/drivers/Sharp/MZ8BIO3.c) et MZ-1E24 (src/drivers/Sharp/MZ1E24.c) -- toutes deux construites sur une emulation Zilog Z80 SIO/2 partagee (src/drivers/Z80SIO.c / src/include/drivers/Z80SIO.h).
L’emulation Z80 SIO (Z80SIO.c). Il s’agit d’un modele Zilog Z80 SIO/2 autonome et fidele au niveau des registres, entierement decouple a la fois de l’USB et de l’emulation CPU afin de pouvoir etre reutilise par n’importe quelle carte. Il implemente :
- Deux canaux -- le canal A aux offsets 0/1 (donnees/controle) et le canal B aux offsets 2/3.
- L'ensemble complet des registres d'ecriture WR0-WR7 et des registres de lecture RR0-RR2, y compris le protocole a deux octets pointeur/commande de WR0 (ecrire un octet de selection de registre/commande, puis l'octet de donnees cible le registre selectionne).
- Les interruptions vectorisees Z80 mode 2 avec une pile in-service a 4 niveaux et une priorite daisy-chain stricte (Rx-special du canal A la plus haute, jusqu'a external/status du canal B la plus basse), ainsi que l'option status-affects-vector.
- Deux anneaux single-producer/single-consumer sans verrou par canal, de 1 Ko chacun : un anneau Tx (producteur core 1 → consommateur core 0) et un anneau Rx (producteur core 0 → consommateur core 1). Des helpers push/pop d'anneau sont exposes pour la pompe USB, et un
volatile bool*est expose pour la ligne /INT.
Lorsqu’elle fonctionne sur USB (plutot que sur une vraie ligne serie), DCD et CTS sont maintenus actifs afin que le firmware hote qui interroge l’etat du controle modem ne se bloque pas en attendant une porteuse.
Implementation partagee de carte et le wrapper mince. MZ8BIO3.c contient l’implementation SIOCard_* partagee (init, reset, poll, task, pompe USB). MZ1E24.c est un wrapper mince qui ne differe que par le mode de connecteur passe a SIOCard_Init() – SIOCARD_MODE_BI pour le MZ-8BIO3 contre SIOCARD_MODE_ST pour le MZ-1E24. La fonction d’initialisation de la carte :
- Analyse le param
"port"(par defaut 0xB0), en le masquant sur une frontiere de 4 ports afin que les quatre registres SIO se placent sur une base propre. - Instancie le Z80 SIO et relie sa ligne /INT a
cpu->swIntAssert-- le hook de source d'interruption logicielle dans l'emulation CPU. - Installe le gestionnaire E/S sur les 256 variantes d'octet de poids fort de chacun des quatre ports (le Z80 place le registre B sur A8-A15 pendant les E/S, donc chaque variante d'octet de poids fort doit se resoudre vers le meme gestionnaire).
- Enregistre les hooks d'acquittement/RETI d'interruption logicielle (
swIntAckVector/swIntReti) afin que la livraison de vecteur mode 2 et l'effacement in-service fonctionnent. - Installe la pompe USB
SIOCard_usbPump()dans le hook globalg_usbSerialPump.
En mode physique la carte ne fait rien et retourne 0 – la vraie carte materielle repond sur le bus hote.
Pont USB. Le canal A est relie a l’USB CDC 2 (VSER_CHANNEL_A) et le canal B a l’USB CDC 3 (VSER_CHANNEL_B). Ces deux ports CDC n’ont pas d’UART physique sous-jacent ; a la place pollUSBtoUART() sur le core 0 appelle SIOCard_usbPump() pour faire transiter les octets entre les FIFO CDC et les anneaux de canal SIO (hote → anneau Rx, anneau Tx → hote). La carte expose les quatre callbacks standard de sous-interface – SIOCard_Reset, SIOCard_PollCB, SIOCard_TaskProcessor – et est enregistree dans le interfaceFuncMap[] de chaque persona compatible SIO (MZ-700, MZ-800, MZ-80B, MZ-1500) sous la forme, par exemple :
</div>
// In each SIO-capable persona's interfaceFuncMap[]:
{"MZ-8BIO3", false, MZ8BIO3_Init, SIOCard_Reset, SIOCard_PollCB, SIOCard_TaskProcessor},
{"MZ-1E24", false, MZ1E24_Init, SIOCard_Reset, SIOCard_PollCB, SIOCard_TaskProcessor},
"rom"), n'installent que des gestionnaires E/S et des callbacks, et sont selectionnees par la persona qui les liste dans son interfaceFuncMap[]. Du cote de la configuration web, elles sont annoncees via configgui.js (la table de correspondance driverInterfaces et la table interfaceRomLimits, ou elles portent un nombre de ROM egal a zero).
Interaction Core 0 / Core 1
Utilisation de la File Inter-Core
- Votre gestionnaire (sur le Core 1) detecte qu'une operation d'E/S fichier ou similaire est necessaire (par ex. le Z80 a ecrit un numero de secteur dans un registre de commande disque).
- Le gestionnaire positionne un drapeau d'etat (par ex.
diskState.pendingRead = true) et retourne immediatement -- il n'effectue pas l'E/S. - Votre
poll_ptr(egalement sur le Core 1, appele toutes les ~2048 cycles) verifie le drapeau d'etat et, s'il est positionne, pousse un message de requete danscpu->requestQueue. - Le Core 0 recoit le message, effectue l'E/S fichier (par ex. lit un secteur de disque depuis la carte SD) et pousse le resultat dans
cpu->responseQueue. - Votre
task_ptrest appele (sur le Core 1) avec le resultat de la tache. Il copie les donnees du secteur dans la PSRAM et efface le drapeau en attente.
// 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;
}
Pieges Courants
- Blocage dans un gestionnaire. L'erreur la plus courante. Tout appel a
debugf,sleep_ms,fopen, ou toute fonction UART depuis l'interieur d'un gestionnaire ou d'un callback poll bloquera le Core 1 et causera un timing de bus incorrect pour le Z80 hote. Deplacez toutes les operations bloquantes vers le Core 0 via la file de requetes. Utilisezplogf()au lieu dedebugf()pour la journalisation du chemin de demarrage qui doit fonctionner avant que l'USB ne soit disponible (voir Journalisation de Debogage). - Mauvais ordre des balises fermantes dans l'enregistrement des gestionnaires. Lors de l'installation de gestionnaires sur une plage a l'aide d'une boucle, assurez-vous que les limites de la boucle utilisent
<et non<=pour l'adresse de fin -- les erreurs d'un cran peuvent corrompre les gestionnaires adjacents. - Oublier d'effacer les gestionnaires lors de l'arret ou du reset du pilote. Si votre gestionnaire de reset n'efface pas les emplacements
memioPtr[]ouioPtr[]que votre pilote a installes, ces gestionnaires continueront d'etre appeles apres le reset, avec un etat potentiellement obsolete. - Collision de numeros de banc. Chaque banc PSRAM fait 64 Ko. La configuration JSON assigne des bancs aux regions memoire. Si deux pilotes utilisent le meme numero de banc, ils ecraseront mutuellement leurs donnees. Utilisez des numeros de banc uniques pour chaque pilote. Les bancs 0-7 sont typiquement utilises par la persona MZ-700 ; utilisez les bancs 8+ pour les sous-interfaces et les pilotes supplementaires.
- MEMBANK_TYPE_FUNC sans gestionnaire installe. Si vous definissez un bloc au type FUNC mais n'installez pas de gestionnaire
memioPtr, les lectures retourneront 0x00 et les ecritures seront silencieusement ignorees. C'est un comportement valide mais c'est souvent un bug -- installez toujours le gestionnaire avant de definir le type de bloc. - Discordance entre le nom virtualFuncMap et le nom JSON. La recherche est insensible a la casse mais la chaine doit correspondre exactement pour le reste. Une faute de frappe dans l'un ou l'autre emplacement causera le saut silencieux du pilote sans message d'erreur. Ajoutez un appel temporaire a
debugfdansZ80CPU_getVirtualFunc()si votre pilote ne s'initialise pas --debugfest une macro qui peut etre desactivee ou limitee en debit dans les builds de production afin de ne pas impacter le timing du bus. - Oublier de definir reset_ptr / poll_ptr / task_ptr. Si vous n'assignez pas ces valeurs dans votre fonction init, le Core 1 n'appellera jamais vos fonctions de reset, poll ou tache. Le pilote s'initialisera correctement mais ne repondra pas au RESET et n'effectuera aucune maintenance periodique.
Journalisation de Debogage
debugf() -- Sortie de Debogage Tamponnee
debugf() est la macro principale de sortie de debogage. Elle fonctionne comme printf() mais ecrit dans un tampon de 64 Ko en PSRAM (a l'adresse 0x117EF004) plutot que directement vers un port serie. Le tampon est vide vers USB CDC lorsque la boucle principale a du temps libre. Un mutex (debugMutex) protege le tampon contre les acces concurrents des deux cores.
// 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");
- Taille du tampon : 64 Ko (
MAX_DEBUG_BUFFER_SIZE = 65536) en PSRAM. - Thread-safe : protege par mutex pour l'acces multi-core.
- Resident en PSRAM : le tampon survit aux resets watchdog (la PSRAM conserve les donnees), mais le pointeur de tampon a
0x117EF004doit etre revalide apres un reset. - Ne jamais appeler depuis les gestionnaires ou callbacks poll du Core 1. L'acquisition du mutex peut bloquer, introduisant une gigue de timing du bus. Utilisez
plogf()pour les messages du chemin de demarrage ou postez les requetes de debogage au Core 0 via la file inter-core.
plogf() -- Journal de Demarrage Persistant en PSRAM
plogf() ecrit dans une region dediee de 4 Ko a la toute fin des 8 Mo de PSRAM (adresse 0x117FF000). Contrairement a debugf(), elle n'utilise pas de mutex et est destinee a la journalisation du chemin de demarrage du Core 0 uniquement -- capturant les messages pendant les etapes critiques de demarrage precoce avant que l'USB ne soit disponible pour la sortie de debogage normale. Comme la PSRAM conserve son contenu a travers les resets watchdog, ces messages survivent a un crash et peuvent etre examines au prochain demarrage reussi.
// 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 chaque jalon critique (init PSRAM, handshake SPI, analyse de la configuration). Si le watchdog se declenche, le tampon plog conserve tous les messages jusqu'au point de blocage. Au prochain demarrage reussi, dump_plog() affiche les messages captures avant d'effacer le tampon, donnant une trace claire de ce qui s'est passe avant le crash.
Choisir la Bonne Sortie de Debogage
| Macro | Emplacement | Mutex | Survit au reset WDT | Sur depuis le Core 1 | Cas d'utilisation |
|---|---|---|---|---|---|
debugf() |
PSRAM (tampon 64 Ko) | Oui | Contenu du tampon oui ; le pointeur doit etre revalide | Non -- bloquera | Sortie de debogage generale du Core 0 |
plogf() |
PSRAM (4 Ko a la fin) | Non | Oui | Non -- Core 0 uniquement | Journalisation du chemin de demarrage avant USB |
| SWD + GDB | Sonde materielle | N/A | N/A | Oui (ports par core) | Debogage en direct, points d'arret, inspection |
Watchdog et Suivi de Progression du Demarrage
Timer Watchdog
main() avec un timeout de 30 secondes :
// src/model/BaseZ80/main.c watchdog_enable(30000, true); // 30s timeout, pause-on-debug enabled
watchdog_update() est appele a chaque jalon de demarrage et tout au long de la boucle principale. Les operations longues critiques (chargements d'images disquettes avec logique de retry, transferts DMA, handshake SPI ESP32) incluent des relances explicites du watchdog pour eviter les resets intempestifs pendant les operations legitimement lentes :
// 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();
}
Registres Scratch du 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]) avant d'etre ecrasees. Cela vous donne les quatre derniers essais de reset, permettant de distinguer un incident ponctuel d'une defaillance de demarrage repetitive a une etape specifique.
Reference des Etapes de Demarrage
| Code | Constante | Description |
|---|---|---|
0x01 | BOOTP_START | Point d'entree atteint |
0x02 | BOOTP_CLK_SET | Horloge systeme configuree (frequence CPU, frequence PSRAM, tension) |
0x03 | BOOTP_PSRAM_INIT | Initialisation PSRAM demarree |
0x04 | BOOTP_PSRAM_OK | PSRAM initialisee et testee |
0x05 | BOOTP_STDIO_INIT | Stdio USB initialise |
0x06 | BOOTP_PIO_INIT | State machines PIO chargees et demarrees |
0x07 | BOOTP_Z80_INIT | Contexte CPU Z80 cree |
0x08 | BOOTP_USB_INIT | Pont USB initialise |
0x0A | BOOTP_ESP_HS_SYNC | Synchronisation handshake SPI ESP32 |
0x0B | BOOTP_CORE1_LAUNCH | Core 1 lance via multicore_launch_core1() |
0x0D | BOOTP_FSPI_INIT | IPC binaire FSPI initialise (canaux DMA revendiques) |
0x0E | BOOTP_ESP_INIT | Couche de communication ESP32 prete |
0x10 | BOOTP_MAIN_LOOP | Boucle principale atteinte -- demarrage termine |
0x11 | BOOTP_ML_POLL_USB | Boucle principale : scrutation USB |
0x12 | BOOTP_ML_INTERCORE | Boucle principale : traitement des commandes inter-core |
0x20 | BOOTP_IC_DEQUEUE | Inter-core : retrait de requete de la file |
0x21 | BOOTP_IC_FD_LOAD | Inter-core : chargement d'image disquette |
0x22 | BOOTP_IC_QD_LOAD | Inter-core : chargement d'image QuickDisk |
0x23 | BOOTP_IC_RF_LOAD | Inter-core : chargement d'image RAMFILE |
0x24-0x27 | BOOTP_IC_FILE_* | Inter-core : chargement/ecriture/reponse/fin de fichier |
scratch[5] == 0xB00710BE, les registres contiennent des donnees de progression de demarrage valides. Lisez scratch[6] pour le code d'etape. Par exemple, si scratch[6] == 0x0A (BOOTP_ESP_HS_SYNC), le firmware s'est bloque pendant le handshake SPI ESP32 -- verifiez que l'ESP32 est flashe et en cours d'execution, et verifiez le cablage SPI.
Shell de Debogage ICE (dbgsh.c)
dbgsh.c / dbgsh.h) implemente un debogueur ICE a 49 commandes sur le canal USB CDC 1. Il s'execute sur le Core 0 et communique avec le Core 1 via des drapeaux partages dans la structure de contexte t_Z80CPU :
cpu->hold/cpu->holdAck-- handshake pause/reprise entre le shell Core 0 et la boucle d'emulation Core 1.cpu->dbgBpAddr[DBG_MAX_BP]-- tableau d'adresses de points d'arret (8 emplacements, 0xFFFF = inutilise). Le Core 1 verifie avant chaque fetch d'opcode.cpu->dbgStepCount-- compteur de pas a pas. Le Core 1 decremente apres chaque instruction et se met automatiquement en pause a zero.cpu->dbgTrace[DBG_TRACE_SZ]-- tampon circulaire de 512 entrees enregistrant[31:16]=PC, [15:8]=opcode, [7:0]=Fpour chaque instruction executee.
dbg_sprintf() (un printf leger resident en RAM) pour eviter les blocages de Flash XIP lors de l'affichage depuis le Core 0 pendant que le Core 1 utilise activement la PSRAM. L'acces a la memoire physique et aux E/S est effectue via Z80CPU_readPhysicalMem() / Z80CPU_writePhysicalMem() / Z80CPU_readPhysicalIO() / Z80CPU_writePhysicalIO(), qui pilotent de vrais cycles de bus Z80 a travers les state machines PIO.
Systeme de hooks de debogage : Plusieurs commandes (mmutrace, ipl) utilisent un mecanisme de callback par pilote -- chaque pilote persona peut enregistrer son propre gestionnaire de trace ou de reset, de sorte que la sortie du shell de debogage s'adapte au contexte machine actif sans coder en dur la logique specifique a la machine dans dbgsh.c.
Gestionnaires de Fautes et Diagnostics PSRAM
Structure de Diagnostic de Faute en PSRAM
0x117FFF00) sont reserves pour les diagnostics de faute. Lorsqu'une faute survient, le gestionnaire sauvegarde un snapshot complet des registres :
// 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;
Implementation des Gestionnaires de Fautes
- Ecrit la structure de diagnostic dans la PSRAM a l'adresse
0x117FFF00avec le marqueur magic et le type de faute appropries. - Affiche le dump des registres et les details de la faute via
debugf()(si l'USB est disponible). - Entre dans une boucle infinie (
while(1)), permettant au watchdog de declencher un reset.
0x117FFF00 pour le marqueur PSRAM_DIAG_MAGIC. S'il est present, il affiche les informations de faute sauvegardees via debugf() et efface le marqueur, fournissant une trace post-mortem complete.
// 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).
Carte Memoire de Diagnostic PSRAM
| Plage d’adresses | Taille | Contenu |
|---|---|---|
0x117EF004 – 0x117FEFFF |
64 Ko | Tampon de sortie debugf() (pointeur volatile a 0x117EF004) |
0x117FF000 – 0x117FFEFF |
~4 Ko | Journal de demarrage persistant plogf() |
0x117FFF00 – 0x117FFFFF |
256 o | Snapshot de diagnostic de faute |
Protocole IPC Binaire (FSPI v1.1)
sdkconfig pre-construits sont fournis pour chaque mode (sdkconfig.mode_wifi_only, sdkconfig.mode_wifi_and_ncm, sdkconfig.mode_ncm_only). En mode NCM, l'ESP32 presente un adaptateur Ethernet USB CDC-NCM avec un serveur DHCP integre (IP par defaut : 192.168.7.1), permettant l'acces a l'interface web sans materiel WiFi. Le mode NCM seul est requis pour les cartes expediees sans certification FCC/RED.
Structure de Trame
// 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
Opcodes de Commande
| Opcode | Nom | Description |
|---|---|---|
0x00 |
IPCF_CMD_NOP |
No-operation (TX factice pendant la lecture full-duplex) |
0x01 |
IPCF_CMD_RDS |
Lecture d’un seul secteur de 512 octets |
0x02 |
IPCF_CMD_WRS |
Ecriture d’un seul secteur de 512 octets |
0x03 |
IPCF_CMD_RBURST |
Lecture en rafale : 1-16 secteurs en une transaction SPI |
0x04 |
IPCF_CMD_WBURST |
Ecriture en rafale : 1-16 secteurs |
0x05 |
IPCF_CMD_RFILE |
Lecture de fichier complet (decoupe si depasse la charge utile max) |
0x06 |
IPCF_CMD_WFILE |
Ecriture de fichier complet |
0x07 |
IPCF_CMD_INF |
Transfert des infos version/partition du RP2350 vers l’ESP32 |
0x08 |
IPCF_CMD_RFD |
Lecture de fichier image disquette |
0x09 |
IPCF_CMD_RQD |
Lecture de fichier image QuickDisk |
0x0A |
IPCF_CMD_RRF |
Lecture de fichier image de sauvegarde RAMFILE |
DMA et Integrite
- Canaux DMA pre-alloues : Les canaux DMA TX et RX (
gDmaTx,gDmaRx) sont revendiques une fois a l'initialisation FSPI et jamais liberes. Cela elimine le surcout de revendication/liberation par transfert et empeche les conditions de course d'epuisement des canaux DMA qui peuvent survenir lorsque le Core 1 est en contention pour le bus QMI. - Elevation de priorite RX : Le canal DMA RX est configure en HAUTE PRIORITE pour eviter le debordement du FIFO RX SPI. Les acces PSRAM du Core 1 via le bus QMI peuvent bloquer le fabric de bus AHB, et si le canal DMA RX est en priorite normale, ses transferts peuvent etre retardes suffisamment longtemps pour que le FIFO SPI deborde.
- Integrite CRC32 : Chaque trame est protegee par un CRC32 IEEE 802.3 standard (polynome
0xEDB88320, reflechi). L'ESP32 utiliseesp_rom_crc32_le()qui produit le meme resultat. En cas de discordance CRC, la trame est retransmise, en utilisant le compteur de sequence (seqNum) pour la detection de doublons. - Integration watchdog : Les attentes DMA incluent un timeout de 2 secondes avec des relances watchdog chaque seconde. Si un transfert DMA se bloque (par ex. en raison d'un reset ESP32), les canaux sont avortes, CS est libere et le demarrage continue.
Sites de Reference
| Ressource | Lien |
|---|---|
| Page du projet picoZ80 | /picoz80/ |
| Manuel Utilisateur picoZ80 | /picoz80-usermanual/ |
| Guide Technique picoZ80 | /picoz80-technicalguide/ |
| Page du projet pico6502 | /pico6502/ |
| Fiche technique RP2350 | datasheets.raspberrypi.com |
| API Multicore Pico SDK | raspberrypi.github.io/pico-sdk-doxygen |
| Bibliotheque Zeta Z80 | github.com/superzazu/z80 |
| Manuel Utilisateur CPU Zilog Z80 | zilog.com |
| Bibliotheque cJSON | github.com/DaveGamble/cJSON |
Avis Reglementaire sur les Communications Sans Fil
- Les appareils assembles ne doivent pas etre vendus, proposes a la vente, offerts ou autrement distribues a des tiers, sauf si le produit fini a ete independamment teste et a obtenu sa propre autorisation d'equipement (par ex. FCC ID, marquage CE avec evaluation par un Organisme Notifie) dans la juridiction concernee.
- La construction de ce projet pour un usage personnel en quantites limitees est generalement permise en vertu des dispositions hobbyistes et d'usage experimental (par ex. FCC § 15.23), a condition que l'appareil ne cause pas d'interference nuisible.
- Les exigences reglementaires varient selon les pays. Les constructeurs en dehors des Etats-Unis doivent consulter leur autorite nationale de radiofrequences pour les regles applicables.
Il est de la seule responsabilite du constructeur de s'assurer que tout appareil construit a partir de ces conceptions est conforme a toutes les reglementations de radiofrequences applicables dans sa juridiction. L'auteur fournit ces conceptions pour un usage personnel, educatif et hobbyiste et ne fait aucune declaration selon laquelle un appareil construit a partir de celles-ci satisfait les exigences reglementaires pour la distribution commerciale.