picoZ80 Guide du Développeur

Guide du Developpeur picoZ80

Ce guide est une reference complete pour les developpeurs qui souhaitent comprendre les mecanismes internes du firmware picoZ80 et ecrire leurs propres pilotes de peripheriques. Il couvre l'ensemble de l'architecture logicielle, de la boucle de dispatch du bus sur le Core 1 jusqu'a l'enregistrement des pilotes, l'installation des hooks memoire et la virtualisation des E/S. Le pilote Sharp MZ-700 (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

Tout le code source se trouve sous 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

L'interface bus Z80 fonctionne entierement dans le materiel PIO (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.
Le code C pre-encode les sequences d'instructions pour chaque type de cycle au moment de la compilation. Elles sont stockees sous forme de tableaux de valeurs 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);
Coordination entre les state machines : La SM d'adresse (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.
Ce handshake garantit que les signaux du bus ne sont jamais pilotes avant que les valeurs correctes ne soient chargees, et que le Core 1 n'ecrase jamais des entrees FIFO que la SM n'a pas encore consommees.
Le flux de la SM de donnees (z80_data) pour un cycle de lecture :
  1. Positionne IRQ 1 et attend -- "prete pour la direction/les donnees".
  2. Le Core 1 efface IRQ 1 apres avoir pousse la direction des broches (mode entree) et un octet de donnees factice.
  3. La SM configure les directions des broches en entree (tristate), permettant a la memoire hote de piloter D0-D7.
  4. La SM attend sur wait 0 irq 0 jusqu'a ce que le prochain changement d'adresse indique la fin du cycle.
  5. La SM remet les directions des broches dans un etat connu.
Pour un cycle d'ecriture, le Core 1 pousse les directions de broches en sortie et l'octet de donnees reel ; la SM pilote D0-D7 avec les donnees.

Types et Structures de Donnees Cles

Avant d'examiner le framework pilotes, il est essentiel de comprendre les structures de donnees fondamentales definies dans 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

Chaque bloc de 512 octets dans l'espace d'adressage Z80 de 64 Ko possede un type encode dans les 8 bits superieurs de son entree 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
Le type determine ce qui se passe dans 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 memioPtr est 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 memioPtr installe 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

Le tableau _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)
Pour definir le type et le banc d'un bloc, vous combinez ces trois valeurs dans un seul entier 32 bits a l'aide d'un OR bit a bit :
// Map block idx (each block = 512 bytes) to RAM type in bank 0:
cpu->_membankPtr[idx] = (MEMBANK_TYPE_RAM  << 24)   // type in top byte
                      | (MZ700_MEMBANK_0   << 16)   // bank number
                      | (idx * MEMORY_BLOCK_SIZE);  // base address of this block

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

// Map same block to physical (pass-through):
cpu->_membankPtr[idx] = (MEMBANK_TYPE_PHYSICAL << 24)
                      | (0 << 16)
                      | (idx * MEMORY_BLOCK_SIZE);
MEMORY_BLOCK_SIZE 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)

Chaque bloc possede egalement une entree 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;
waitStates : Si votre region memoire supportee par la PSRAM necessite plus de temps pour repondre (par exemple parce que la fonction gestionnaire effectue un travail supplementaire), ajoutez des wait states. Chaque wait state prolonge le cycle de bus d'un T-cycle de l'horloge hote. Les regions RAM utilisent typiquement 1 wait state ; les regions ROM qui servent des donnees pre-chargees peuvent souvent utiliser 0.
tCycSync : Lorsque positionne a 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)

Les 8 Mo de PSRAM externe sont mappes dans une seule structure 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;
Les quatre sous-tableaux ont des roles distincts :
  • 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 membankPtr complete (encodee de la meme maniere que cpu->_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

Les emplacements 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);
Parametres :
  • cpu -- pointeur vers le contexte Z80CPU. Donne acces a _membankPtr[], _z80PSRAM et 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 -- true si c'est un cycle de lecture (le Z80 lit) ; false si 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).
Valeur de retour :
  • 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 memioPtr sur 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.
Important : Les fonctions gestionnaires sont appelees directement depuis la boucle principale du Core 1. Elles s'executent sur le Core 1 avec les interruptions desactivees. Elles doivent etre courtes, deterministes et ne doivent jamais bloquer, dormir, appeler 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

Chaque fonction de pilote recoit un pointeur 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

Lorsqu'un pilote est initialise, il recoit un pointeur vers une structure 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;
Ports d'E/S de base relocalisables. Afin que les cartes independantes de la machine puissent etre utilisees sur des cartes personnalisees / d'experimentation (voir OpenZ80), plusieurs pilotes lisent leur port d'E/S de base depuis l'entree 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.
Les champs 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

Le Core 1 execute une boucle infinie serree dans 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);
        }
    }
}
Points cles :
  • 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, rappelant Z80CPU_readMem(), Z80CPU_writeMem(), Z80CPU_readIO() et Z80CPU_writeIO() pour chaque transaction bus.

Dispatch Lecture Memoire

Lorsque l'emulateur Z80 effectue une 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);
    }
}
Notez que le dispatch E/S est plus simple que le dispatch memoire -- il n'y a pas de concept de type de bloc pour les ports E/S. Chaque adresse E/S a soit un gestionnaire dans 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

Le framework pilotes est le mecanisme par lequel les modules pilotes C sont decouverts, instancies depuis la configuration JSON et connectes aux systemes memoire et E/S. Il comporte deux niveaux :
  • Pilotes de niveau superieur (egalement appeles personas) -- enregistres dans virtualFuncMap[] dans Z80CPU.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

Le tableau 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]);
La fonction de recherche qui parcourt cette table par nom est :
// 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

Le modele OpenZ80 (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.
1 -- Partir du pilote le plus proche
Copiez le pilote de persona le plus proche de votre cible et modifiez sa carte memoire, ses gestionnaires d'E/S et sa disposition de ROM. Les pilotes de persona sont :
  • 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.
Enregistrez le nouveau persona en ajoutant une ligne {"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(...).
2 -- Reutiliser ou modifier les ROM de la machine
Les ROM de moniteur, d'IPL, de BIOS CP/M et d'amorcage disquette que les pilotes chargent sont maintenues sous forme de code source assembleur Z80 commente dans les projets compagnons RFS et TZFS, dans leurs repertoires 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)
Assemblez la ROM dont vous avez besoin, placez le binaire resultant sur la carte SD, et referencez-le depuis une entree 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.
Commutation de modes memoire (port 0x60)
Le Z80 selectionne une disposition memoire tranZPUter en ecrivant une valeur de mode sur le port d'E/S 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 (0x00000x003F → bloc de vecteurs, 0x00400x01FF → le debut de la TPA).
Processeur de service K64F virtuel (OUT 0x68 → Core 0)
Plutot que de traiter les appels du systeme de fichiers sur le coeur Z80, TZFS utilise un processeur de service virtuel. Le Z80 execute 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

Apres que le RP2350 a lu et analyse 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)
Notez l'ordre : les pilotes sont initialises en premier, puis les tableaux JSON "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

Un pilote enregistre trois callbacks permanents en stockant des pointeurs de fonction dans sa structure 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
Important : 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

Le pilote Sharp 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)

Le pilote MZ-800 (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 comme cpu->_Z80.inta, le callback d'acquittement d'interruption.
  • MZ800_retiHandler() -- installe comme cpu->_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

Le MZ-700 est un ordinateur Sharp 8 bits de 1982 base sur le Z80A. Sa carte memoire possede quelques caracteristiques distinctives qui en font un exemple d'apprentissage ideal :
  • 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

En haut de 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]);
Note : L'exemple ci-dessus montre la liste d'interfaces de la persona MZ-700. Chaque persona possede son propre 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

Comme l'etat du banking doit persister entre les transactions bus (une ecriture a 0xE0 doit etre memorisee pour que les acces memoire suivants aillent vers le bon banc), le pilote maintient une structure d'etat statique :
// 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,
};
Les variables statiques comme celle-ci sont sures car il n'y a qu'une seule instance Z80CPU et le Core 1 est le seul thread qui appelle les fonctions gestionnaires. Si vous aviez un jour plusieurs instances CPU (ce qui n'est pas le cas dans la conception actuelle), vous deplacerez cet etat dans 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 par Z80CPU_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()

C'est le gestionnaire E/S qui gere les six ports de controle de banking du MZ-700. Il est appele par 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
}
Ce gestionnaire demontre le patron de conception le plus important dans l'ecriture de pilotes : modifier _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()

Quand l'hote active RESET, le Core 1 appelle le 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;
}
Le patron du processeur de taches est coherent a travers tous les pilotes : la persona distribue simplement la tache a toutes les sous-interfaces actives. Chaque sous-interface verifie si la tache la concerne et l'ignore dans le cas contraire.

Ecrire un Nouveau Pilote -- Etape par Etape

Cette section parcourt chaque etape necessaire pour creer un pilote complet a partir de zero. L'exemple cree un simple disque RAM (un bloc de 64 Ko de PSRAM auquel le Z80 accede comme memoire mappee en E/S) pour illustrer tous les patrons sans la complexite de l'emulation de materiel reel.

Etape 1 -- Creer les Fichiers Sources

Creez deux fichiers. Le fichier d'en-tete declare les fonctions que les autres modules appelleront ; le fichier C les implemente.
// 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

Ouvrez 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
)
Vous devez egalement ajouter le repertoire de votre en-tete au chemin d'inclusion s'il se trouve dans un nouveau sous-repertoire. Pour le repertoire des pilotes Sharp, cela est deja configure, donc aucun appel supplementaire a target_include_directories n'est necessaire.

Etape 3 -- Inclure l'En-tete dans Z80CPU.c

Ouvrez 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

Toujours dans 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},
};
La recherche est insensible a la casse, donc "mydriver", "MyDriver" et "MYDRIVER" dans le JSON correspondront tous a cette entree.

Etape 5 -- Ajouter le Pilote au config.json

Ajoutez une entree "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
Le systeme de compilation produit des binaires firmware applicatifs pour chaque cible de modele -- deux partitions (Partition 1 a 0x10020000, Partition 2 a 0x10520000) chacune en variante standard et DBGSH. Trois cibles de modele sont disponibles :
  • BaseZ80 (pZ80-BaseZ80) -- binaire universel avec tous les pilotes (Sharp + Amstrad + Tatung). Definit INCLUDE_SHARP_DRIVERS, INCLUDE_AMSTRAD_DRIVERS et INCLUDE_TATUNG_DRIVERS.
  • SharpZ80 (pZ80-SharpZ80) -- pilotes Sharp MZ uniquement. Definit INCLUDE_SHARP_DRIVERS. Produit un binaire firmware plus petit.
  • AmstradZ80 (pZ80-AmstradZ80) -- pilotes Amstrad PCW uniquement. Definit INCLUDE_AMSTRAD_DRIVERS et TARGET_MODEL_AMSTRAD. Produit un binaire firmware plus petit.
  • TatungZ80 (pZ80-TatungZ80) -- pilotes Tatung Einstein uniquement. Definit INCLUDE_TATUNG_DRIVERS et TARGET_MODEL_TATUNG. Produit un binaire firmware plus petit.
Chaque cible de modele possede son propre repertoire sous 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

Avant de compiler le firmware ESP32, selectionnez le mode reseau souhaite en copiant le 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
Les definitions preprocesseur cles controlees par ces configurations sont :
  • 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.
Celles-ci sont definies via Kconfig (idf.py menuconfig) ou les fichiers sdkconfig pre-construits listes ci-dessus.

Modeles de Hooks Memoire en Detail

Cette section decrit chaque modele de hook en detail avec des exemples complets. Ce sont les briques de base de toute la gestion memoire des pilotes.

Modele 1 -- Peripherique Virtuel Pur (bloc FUNC)

Utilisez ceci lorsque vous voulez qu'une region de l'espace d'adressage Z80 soit entierement controlee par votre gestionnaire, sans support PSRAM. Les lectures et ecritures du Z80 appellent toujours votre fonction. Rien n'est stocke dans la PSRAM.
// Map 0xC000-0xCFFF as a pure virtual device in bank 4
int startBlock = 0xC000 / MEMORY_BLOCK_SIZE;  // = 96
int endBlock   = 0xD000 / MEMORY_BLOCK_SIZE;  // = 104

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

// Install handler for every address in range
for(uint32_t addr = 0xC000; addr < 0xD000; addr++)
    cpu->_z80PSRAM->memioPtr[addr] = (MemoryFunc)MyVirtualDevice_Handler;

// Handler:
uint8_t MyVirtualDevice_Handler(Z80CPU *cpu, bool read, uint16_t addr, uint8_t data)
{
    uint16_t reg = addr - 0xC000;  // Register offset within the device

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

Modele 2 -- Intercepter les Ecritures vers une Region RAM

Utilisez ceci lorsque vous voulez qu'une region se comporte comme de la RAM normale (les lectures retournent les donnees PSRAM, les ecritures mettent a jour la PSRAM) mais que vous souhaitez egalement etre notifie des ecritures -- par exemple, pour dupliquer les ecritures de la video RAM vers un tampon miroir ou pour declencher une mise a jour materielle. Le type de bloc reste 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

Certains materiels utilisent les ecritures vers des adresses mappees en ROM comme ecritures de registres de banking (l'ecriture est "decodee" par le materiel mais ne modifie pas la ROM). Le bloc reste 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)

Vous n'etes pas oblige d'installer des gestionnaires pour un bloc entier. Vous pouvez installer un gestionnaire sur une seule adresse specifique au sein d'un bloc RAM ou ROM. Le type de bloc controle ce qui se passe pour toutes les autres adresses du bloc ; le gestionnaire d'adresse specifique ne redefinit que cette seule adresse.
// 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

Les gestionnaires E/S sont plus simples -- il n'y a pas de support PSRAM pour les ports E/S. Le gestionnaire est soit appele (s'il est installe) soit le cycle E/S passe au materiel physique. Il n'y a pas de concept de type de bloc.
// 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

Si votre nouveau peripherique est une carte qui se branche sur un MZ-700 (ou une autre persona existante), vous l'implementez comme une sous-interface plutot qu'un pilote de niveau superieur. C'est le patron utilise par RFS, WD1773, QDDrive, MZ-1E05, MZ8BFI et les cartes d'extension RAM.

Fonctions Requises pour une Sous-Interface

Une sous-interface necessite quatre fonctions avec ces signatures :
// 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

Ajoutez votre sous-interface au 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},
};
La chaine "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

Le 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)

Les cartes serie RS-232C constituent un exemple complet et concret du patron de sous-interface ci-dessus, ainsi que d'une sous-interface qui possede un module d'emulation de peripherique reutilisable et le relie a l'USB. Deux sous-interfaces persona sont fournies -- MZ-8BIO3 (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 global g_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},
Les deux cartes suivent le contrat standard de pilote d'interface decrit ci-dessus -- elles sont sans ROM (aucune entree "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

Les gestionnaires de pilotes s'executent sur le Core 1 a l'interieur de la boucle principale. Toute operation qui prend plus de quelques microsecondes (E/S fichier, commandes UART vers l'ESP32, malloc) doit etre deleguee au Core 0 en utilisant la file inter-core.

Utilisation de la File Inter-Core

Le patron est :
  1. 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).
  2. Le gestionnaire positionne un drapeau d'etat (par ex. diskState.pendingRead = true) et retourne immediatement -- il n'effectue pas l'E/S.
  3. 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 dans cpu->requestQueue.
  4. 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.
  5. Votre task_ptr est 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. Utilisez plogf() au lieu de debugf() 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[] ou ioPtr[] 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 debugf dans Z80CPU_getVirtualFunc() si votre pilote ne s'initialise pas -- debugf est 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

Le debogage du firmware picoZ80 est un defi car c'est un systeme multi-core et multi-processeur : le RP2350 fait tourner deux cores Cortex-M33 avec des responsabilites temps reel et non temps reel distinctes, et le co-processeur ESP32 gere toutes les E/S reseau et stockage. Le debogage traditionnel de type printf n'est pas simple -- l'USB peut ne pas etre disponible pendant le demarrage precoce, la boucle principale du Core 1 ne tolere pas les appels bloquants, et un reset watchdog detruit l'etat volatile. Le firmware fournit trois mecanismes complementaires de sortie de debogage pour repondre a ces contraintes.

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");
Proprietes cles :
  • 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 0x117EF004 doit 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
Patron d'utilisation typique : Pendant le demarrage, appelez 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

Le picoZ80 opere dans un environnement contraignant : un RP2350 double core communiquant par SPI avec un ESP32, desservant une interface bus Z80 cycle-accurate via PIO, tout en gerant 8 Mo de PSRAM externe. Un blocage a n'importe quel point du demarrage -- initialisation PSRAM, handshake SPI, analyse de la configuration ou lancement du Core 1 -- laisserait la carte sans reponse et sans sortie diagnostique. Le timer watchdog materiel et le systeme de suivi de progression du demarrage ont ete concus pour rendre de telles defaillances recuperables et diagnosticables.

Timer Watchdog

Le watchdog materiel du RP2350 est active tot dans 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

Le RP2350 fournit huit registres scratch 32 bits dans le bloc materiel watchdog qui survivent aux resets watchdog mais sont effaces lors d'un reset de mise sous tension. Le firmware picoZ80 utilise cinq d'entre eux pour maintenir un historique de diagnostic de demarrage complet :
// src/model/BaseZ80/main.c

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

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

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

// Usage throughout boot sequence:
bootStage(BOOTP_START);         // 0x01 — entry point
// ... clock setup ...
bootStage(BOOTP_CLK_SET);       // 0x02
// ... PSRAM init ...
bootStage(BOOTP_PSRAM_INIT);    // 0x03
watchdog_update();
bootStage(BOOTP_PSRAM_OK);      // 0x04
// ... and so on through BOOTP_MAIN_LOOP (0x10)
A chaque reset watchdog, l'etape courante et la cause du reset sont decalees dans le FIFO d'historique (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
0x01BOOTP_STARTPoint d'entree atteint
0x02BOOTP_CLK_SETHorloge systeme configuree (frequence CPU, frequence PSRAM, tension)
0x03BOOTP_PSRAM_INITInitialisation PSRAM demarree
0x04BOOTP_PSRAM_OKPSRAM initialisee et testee
0x05BOOTP_STDIO_INITStdio USB initialise
0x06BOOTP_PIO_INITState machines PIO chargees et demarrees
0x07BOOTP_Z80_INITContexte CPU Z80 cree
0x08BOOTP_USB_INITPont USB initialise
0x0ABOOTP_ESP_HS_SYNCSynchronisation handshake SPI ESP32
0x0BBOOTP_CORE1_LAUNCHCore 1 lance via multicore_launch_core1()
0x0DBOOTP_FSPI_INITIPC binaire FSPI initialise (canaux DMA revendiques)
0x0EBOOTP_ESP_INITCouche de communication ESP32 prete
0x10BOOTP_MAIN_LOOPBoucle principale atteinte -- demarrage termine
0x11BOOTP_ML_POLL_USBBoucle principale : scrutation USB
0x12BOOTP_ML_INTERCOREBoucle principale : traitement des commandes inter-core
0x20BOOTP_IC_DEQUEUEInter-core : retrait de requete de la file
0x21BOOTP_IC_FD_LOADInter-core : chargement d'image disquette
0x22BOOTP_IC_QD_LOADInter-core : chargement d'image QuickDisk
0x23BOOTP_IC_RF_LOADInter-core : chargement d'image RAMFILE
0x24-0x27BOOTP_IC_FILE_*Inter-core : chargement/ecriture/reponse/fin de fichier
Debogage d'un reset watchdog : Connectez une sonde SWD, arretez le RP2350 et lisez les registres scratch. Si 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)

Le shell de debogage (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]=F pour chaque instruction executee.
Patrons d'implementation cles : le shell utilise 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

Le firmware installe des gestionnaires de fautes Cortex-M33 qui capturent un snapshot de diagnostic complet dans la PSRAM avant de permettre au watchdog de reinitialiser le systeme. Cela fournit des capacites d'analyse post-mortem sans necessiter une session de debogage en direct -- essentiel pour diagnostiquer les fautes intermittentes dans un systeme temps reel multi-core.

Structure de Diagnostic de Faute en PSRAM

Les 256 derniers octets des 8 Mo de PSRAM (adresse 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

Chaque type de faute (hard fault, memory management fault, bus fault, usage fault) possede un wrapper assembleur qui extrait le Main Stack Pointer (MSP) ou le Process Stack Pointer (PSP) -- selon lequel etait actif au moment de la faute -- et le passe a un gestionnaire C commun. Le gestionnaire C :
  1. Ecrit la structure de diagnostic dans la PSRAM a l'adresse 0x117FFF00 avec le marqueur magic et le type de faute appropries.
  2. Affiche le dump des registres et les details de la faute via debugf() (si l'USB est disponible).
  3. Entre dans une boucle infinie (while(1)), permettant au watchdog de declencher un reset.
Au prochain demarrage reussi, le firmware verifie 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

Le sommet des 8 Mo de PSRAM est partitionne en trois regions de diagnostic :
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
Les trois regions survivent aux resets watchdog car la PSRAM conserve son contenu tant que l'alimentation est maintenue. Lors d'un reset de mise sous tension, le contenu est indefini et le firmware les reinitialise en verifiant les marqueurs magic.

Protocole IPC Binaire (FSPI v1.1)

Le RP2350 communique avec l'ESP32 via une liaison SPI 4 fils a 50 MHz utilisant un protocole IPC binaire (version 1.1). Celui-ci remplace un protocole textuel anterieur par un format de trame binaire structure qui supporte la verification d'integrite CRC32, les transferts de secteurs en rafale et des canaux DMA pre-alloues pour une latence reduite et une fiabilite amelioree.
Le firmware ESP32 supporte trois modes reseau selectionnes au moment de la compilation : WiFi seul, WiFi+NCM (les deux simultanement) et NCM seul. Des fichiers 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

Chaque transaction IPC consiste en un en-tete fixe de 64 octets suivi d'une charge utile optionnelle et d'un trailer CRC32 de 4 octets :
// 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 utilise esp_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

Cet appareil incorpore un module sans fil ESP32-S3-PICO-1 qui emet dans la bande ISM 2,4 GHz, ce qui en fait un emetteur intentionnel au regard des reglementations sur les radiofrequences dans le monde entier (y compris FCC Part 15 Subpart C aux Etats-Unis, et la Directive Equipements Radio 2014/53/UE dans l'Union Europeenne).
Bien que le module ESP32-S3-PICO-1 lui-meme possede des certifications reglementaires prealables (FCC, CE et autres), ces certifications au niveau du module ne s'etendent pas automatiquement a un produit fini qui incorpore le module. L'exemption de module pre-certifie permet aux hobbyistes individuels de construire un nombre limite d'appareils pour un usage personnel, experimental ou educatif sans obtenir d'autorisation d'equipement separee.
Limitations Importantes
  • 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.
Responsabilite du Constructeur
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.