picoZ80 Guía del Desarrollador

Guía del Desarrollador de picoZ80

Esta guía es una referencia completa para desarrolladores que desean comprender los aspectos internos del firmware picoZ80 y escribir sus propios controladores de periféricos. Cubre la arquitectura de software completa, desde el bucle de despacho de bus del Core 1 hasta el registro de controladores, la instalación de hooks de memoria y la virtualización de E/S. El controlador del Sharp MZ-700 (MZ700.c) se utiliza a lo largo de todo el documento como un ejemplo práctico concreto.
No se asume experiencia previa con el código base de picoZ80. Cada concepto se explica desde los principios fundamentales y luego se muestra en el código fuente real. Al final de esta guía, podrá escribir un controlador completo desde cero, añadirlo al sistema de compilación, registrarlo en el framework y configurarlo mediante JSON.
Para la arquitectura de hardware, los detalles de la interfaz de bus PIO y la referencia de configuración JSON, consulte la Guía Técnica de picoZ80. Para la configuración del usuario final, consulte el Manual de Usuario de picoZ80.

Estructura del Código

Todo el código fuente reside bajo projects/tzpuPico/ dentro de la raíz del repositorio (la ubicación exacta de checkout dependerá de su sistema). El diseño a continuación muestra los archivos relevantes para el desarrollo de controladores:
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

Interfaz de Bus PIO — Cómo el Código C Controla las Máquinas de Estado

La interfaz de bus del Z80 se ejecuta completamente en hardware PIO (z80.pio), pero el código C en el Core 1 orquesta qué ciclo de bus se ejecuta y cuándo. Comprender esta interacción es importante para cualquier persona que depure temporización de bus o añada nuevos tipos de ciclo.
El mecanismo central es out exec, 16 — una instrucción PIO que extrae un valor de 16 bits del TX FIFO y lo ejecuta como una instrucción PIO. La máquina de estado z80_cycle (PIO 0 SM 2) utiliza esto en un bucle ajustado:
// 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.
El código C pre-codifica las secuencias de instrucciones para cada tipo de ciclo en tiempo de compilación. Estas se almacenan como arrays de valores uint16_t. En tiempo de ejecución, el bucle principal del Core 1 introduce la secuencia apropiada en el FIFO según la transacción de bus actual:
// 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);
Coordinación entre máquinas de estado: La SM de dirección (z80_addr) y la SM de datos (z80_data) utilizan cada una su propio flag IRQ (IRQ 0 e IRQ 1 respectivamente) para implementar un handshake productor/consumidor:
  • La SM establece su flag IRQ y se detiene en wait 0 irq N — "estoy lista, envíame datos".
  • El Core 1 introduce la dirección o los datos en el TX FIFO de la SM, y luego limpia el flag IRQ.
  • La SM se despierta, extrae los datos del FIFO y activa los pines.
Este handshake asegura que las señales del bus nunca se activen antes de que se carguen los valores correctos, y que el Core 1 nunca sobrescriba entradas del FIFO que la SM aún no ha consumido.
Flujo de la SM de datos (z80_data) para un ciclo de lectura:
  1. Establece IRQ 1 y espera — "listo para dirección/datos".
  2. El Core 1 limpia IRQ 1 después de introducir la dirección de pines (modo entrada) y un byte de datos ficticio.
  3. La SM configura las direcciones de pines a entrada (tristate), permitiendo que la memoria del host controle D0–D7.
  4. La SM espera en wait 0 irq 0 hasta que el siguiente cambio de dirección indica que el ciclo está terminando.
  5. La SM restaura las direcciones de pines a un estado conocido.
Para un ciclo de escritura, el Core 1 introduce las direcciones de pines de salida y el byte de datos real; la SM controla D0–D7 con los datos.

Tipos y Estructuras de Datos Clave

Antes de examinar el framework de controladores, es esencial comprender las estructuras de datos principales definidas en src/include/Z80CPU.h. Estas estructuras se pasan a cada función del controlador y son el medio principal por el cual un controlador interactúa con el sistema de memoria y E/S.

Constantes de Tipo de Bloque de Memoria

Cada bloque de 512 bytes en el espacio de direcciones de 64KB del Z80 tiene un tipo codificado en los 8 bits superiores de su entrada membankPtr. El tipo le indica al bucle de despacho del Core 1 cómo manejar las transacciones de bus que caen dentro de ese bloque.
// 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
El tipo determina lo que sucede en Z80CPU_readMem() y Z80CPU_writeMem():
  • PHYSICAL / PHYSICAL_VRAM / PHYSICAL_HW — el RP2350 no intercepta la transacción de bus; el hardware real en la placa host responde. Utilice esto para cualquier región donde los propios chips del host (ROM, RAM, hardware de video) deben mantener el control.
  • RAM — las lecturas y escrituras van a un banco de 64KB en la PSRAM de 8MB. Si hay una función memioPtr instalada para la dirección específica, esa función se llama en lugar de (para FUNC) o junto con el acceso a PSRAM (los handlers pueden interceptar o post-procesar). Los wait states y la sincronización T1 se pueden configurar por bloque.
  • ROM — las lecturas provienen de la PSRAM (típicamente cargada desde un archivo de imagen al arranque). Los ciclos de escritura aún llegan a cualquier handler memioPtr instalado, pero la PSRAM no se modifica — útil para registros de banking que residen en una región de dirección mapeada como ROM.
  • VRAM — las lecturas provienen de la PSRAM; las escrituras van tanto a la PSRAM como a la VRAM física del host en paralelo. Esto permite que el software mantenga una copia sombra del buffer de video mientras también actualiza la pantalla real.
  • FUNC — no hay respaldo de PSRAM. Cada lectura y escritura llama a la función instalada en memioPtr[addr]. Utilice esto para registros de hardware virtualizados, puertos de control de banking mapeados en espacio de memoria, y cualquier recurso sin RAM real detrás.
  • PTR — cada byte del bloque de 512 bytes puede apuntar independientemente a una ubicación diferente en la PSRAM o tipo de memoria. Se utiliza para manipulación de espacio de direcciones de granularidad muy fina.

Codificación de membankPtr

El array _membankPtr[] tiene 128 entradas — una por cada bloque de 512 bytes del espacio de direcciones de 64KB del Z80. Cada entrada es un único valor de 32 bits que codifica tres campos:
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)
Para establecer el tipo y banco de un bloque, se empaquetan estos tres valores en un único entero de 32 bits usando OR a nivel de bits:
// 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 es 512 bytes. Hay 128 bloques que cubren 0x0000–0xFFFF. El índice de bloque para una dirección Z80 dada es: addr / MEMORY_BLOCK_SIZE = addr >> 9.

Atributos de Memoria (t_memAttr)

Cada bloque también tiene una entrada t_memAttr que controla los wait states y la sincronización de ciclos T. Estos se almacenan en un array 2D indexado por banco y bloque:
// 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 su región de memoria respaldada por PSRAM requiere más tiempo para responder (por ejemplo, porque la función handler realiza trabajo adicional), añada wait states. Cada wait state extiende el ciclo de bus por un ciclo T del reloj del host. Las regiones RAM típicamente usan 1 wait state; las regiones ROM que sirven datos pre-cargados a menudo pueden usar 0.
tCycSync: Cuando se establece en true, la máquina de estado PIO z80_sync retrasa el acceso a PSRAM hasta el flanco ascendente T1 del ciclo de bus actual. Esto evita que las operaciones internas de PSRAM introduzcan deriva de temporización en software del host que depende de una temporización precisa por ciclo de reloj (E/S de casete, bit-banging serial, bucles de retardo). Establezca esto en true para regiones RAM/ROM a las que accederá software sensible a la temporización del host.

La Estructura PSRAM (t_Z80PSRAM)

La PSRAM externa de 8MB se mapea en una única estructura t_Z80PSRAM. Esta se asigna una vez al inicio y se referencia mediante cpu->_z80PSRAM. Es la estructura de datos más grande e importante del sistema — todo lo que el Z80 puede acceder reside aquí.
// 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;
Los cuatro sub-arrays tienen roles distintos:
  • RAM[] — almacenamiento de bytes sin procesar para todos los bancos de memoria respaldados por PSRAM. El tamaño total es 64 bancos x 64KB = 4MB. Las imágenes ROM cargadas desde la tarjeta SD o Flash se escriben aquí al arranque. Durante la ejecución del Z80, las lecturas y escrituras a bloques de tipo RAM/ROM/VRAM acceden a este array.
  • memPtr[] — utilizado solo por bloques de tipo PTR. Cada entrada es un valor completo membankPtr (codificado de la misma manera que cpu->_membankPtr[]) que redirige el acceso de un byte individual a una ubicación completamente diferente. Permite remapeo a nivel de byte dentro del espacio de 64KB.
  • memioPtr[] — la tabla de hooks de funciones de memoria. Una ranura por dirección Z80. Cuando una ranura no es NULL, el Core 1 llama a la función en esa ranura en cada acceso de memoria a esa dirección, independientemente del tipo de bloque (RAM, ROM o FUNC). Así es como los controladores interceptan o anulan ubicaciones de memoria específicas sin cambiar el tipo de bloque general.
  • ioPtr[] — la tabla de hooks de puertos de E/S. Una ranura por dirección de E/S del Z80 (puerto A0–A15 completo, aunque el Z80 usa solo A0–A7 como el número de puerto real; los bits superiores pueden llevar contexto adicional). Cuando no es NULL, la función se llama para cada instrucción IN u OUT dirigida a ese puerto. Si es NULL, el ciclo de E/S pasa al hardware físico.

La Firma del Handler MemoryFunc

Tanto las ranuras memioPtr[] como ioPtr[] contienen punteros a función del mismo tipo — MemoryFunc:
// src/include/Z80CPU.h
typedef uint8_t (*MemoryFunc)(Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
Parámetros:
  • cpu — puntero al contexto Z80CPU. Le da acceso a _membankPtr[], _z80PSRAM, y todo lo demás. Nunca modifique _membankPtr[] desde dentro de un handler de E/S que pueda ser llamado desde el bucle principal del Core 1; use la cola intercore para tales operaciones (véase Interacción entre Cores).
  • readtrue si este es un ciclo de lectura (el Z80 está leyendo); false si este es un ciclo de escritura (el Z80 está escribiendo).
  • addr — la dirección Z80 completa (0x0000–0xFFFF para memoria, 0x0000–0xFFFF para puertos de E/S). Para E/S, el Z80 usa solo los 8 bits inferiores como el número de puerto real (A0–A7); los 8 bits superiores (A8–A15) son el valor del registro B durante la instrucción.
  • data — en un ciclo de escritura, el byte que el Z80 está escribiendo. En un ciclo de lectura desde un bloque RAM/ROM, este es el valor actual en esa dirección en la PSRAM (puede usarlo o ignorarlo).
Valor de retorno:
  • En una lectura: el byte a devolver al Z80. Este es el valor que el Z80 ve en el bus de datos.
  • En una escritura: el valor de retorno generalmente no se usa para handlers de E/S. Para handlers memioPtr en bloques de tipo RAM, el valor de retorno se escribe de vuelta a la PSRAM en lugar del dato original — utilice esto para modificar o sanitizar lo que se almacena.
Importante: Las funciones handler se llaman directamente desde el bucle principal del Core 1. Se ejecutan en el Core 1 con interrupciones deshabilitadas. Deben ser cortas, deterministas y nunca deben bloquear, dormir, llamar a debugf, ni realizar ninguna operación que pueda detener el Core 1. Las operaciones de E/S de archivos y comunicación UART deben enviarse al Core 0 a través de la cola intercore.

La Estructura de Contexto Z80CPU

Cada función del controlador recibe un puntero Z80CPU *cpu. Este es el contexto maestro para toda la emulación. Los campos más relevantes para los escritores de controladores son:
// 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
};

Estructuras de Configuración de Controladores

Cuando un controlador se inicializa, recibe un puntero a una estructura t_drvConfig que fue rellenada por el parser de configuración JSON. Esto le indica al controlador qué interfaces se le han asignado, qué imágenes ROM cargar, qué remapeos de direcciones aplicar y qué parámetros ha configurado el usuario.
// 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;
Puertos base de E/S reubicables. Para que las tarjetas independientes de la máquina puedan usarse en placas personalizadas / de experimentación (véase OpenZ80), varios controladores leen su puerto base de E/S desde la entrada iomap de la interfaz en lugar de codificarlo de forma fija: el dstaddr de la entrada se convierte en la base de la tarjeta (con srcaddr como el puerto auténtico de la tarjeta), y cada una de estas tarjetas lleva una constante *_DEFAULT_BASE en su cabecera que se utiliza cuando no hay ninguna entrada iomap presente. Las tarjetas reubicables y sus valores predeterminados son 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) y Celestite (0x60/16). Las claves JSON iomap/addrmap están en minúsculas (srcaddr / dstaddr) y se parsean como números; el campo Base I/O Port de la página de Configuración de la GUI las escribe por usted.
Los campos reset_ptr, poll_ptr y task_ptr no se establecen desde JSON — los establece la función init de su controlador para que el Core 1 pueda llamar a las funciones de mantenimiento de su controlador en los momentos apropiados.

El Bucle de Despacho del Core 1

El Core 1 ejecuta un bucle infinito ajustado en Z80CPU_cpu(). Comprender lo que sucede en este bucle es fundamental para escribir controladores correctos.

Estructura del Bucle Principal

// 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);
        }
    }
}
Puntos clave:
  • El bucle ejecuta z80_run() durante 2048 ciclos por iteración. Entre iteraciones, se llaman todos los handlers de poll de los controladores. Esto significa que los handlers de poll se llaman aproximadamente cada 2048 ciclos de reloj del Z80 — a 3.5MHz eso es aproximadamente cada 585 microsegundos.
  • Los handlers de poll deben ser extremadamente cortos. Están en la ruta crítica de la emulación. Un handler de poll lento introduce jitter en la temporización del bus del Z80.
  • La función z80_run() (de la biblioteca Zeta Z80) ejecuta instrucciones Z80, haciendo callbacks a Z80CPU_readMem(), Z80CPU_writeMem(), Z80CPU_readIO() y Z80CPU_writeIO() para cada transacción de bus.

Despacho de Lectura de Memoria

Cuando el emulador Z80 realiza una lectura de memoria, se llama a Z80CPU_readMem(). Esta función es el corazón del sistema de memoria — comprenderla le dice exactamente lo que sus handlers necesitan hacer.
// 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;
}

Despacho de Escritura de Memoria

// src/Z80CPU.c (simplified and annotated)
void __func_in_RAM(Z80CPU_writeMem)(Z80CPU *cpu, uint16_t addr, uint8_t data)
{
    uint8_t  blockIdx   = addr >> 9;
    uint32_t membankptr = cpu->_membankPtr[blockIdx];
    uint8_t  memType    = (membankptr >> 24) & 0xFF;
    uint8_t  bank       = (membankptr >> 16) & 0xFF;
    uint16_t blockBase  = membankptr & 0xFFFF;
    uint32_t RAMaddr    = (bank * MEMORY_PAGE_SIZE) + blockBase;
    uint16_t blockOfs   = addr & (MEMORY_BLOCK_SIZE - 1);
    uint8_t  waitStates = cpu->_memAttr[bank][blockIdx].waitStates;

    switch(memType)
    {
        case MEMBANK_TYPE_PHYSICAL:
        case MEMBANK_TYPE_PHYSICAL_VRAM:
        case MEMBANK_TYPE_PHYSICAL_HW:
            Z80CPU_writePhysicalMem(cpu, addr, data);
            break;

        case MEMBANK_TYPE_RAM:
        case MEMBANK_TYPE_VRAM:
            if(cpu->_z80PSRAM->memioPtr[addr] != NULL)
            {
                // Handler intercepts the write — the return value replaces 'data'
                // before it is written to PSRAM (handler can sanitise or transform data)
                data = cpu->_z80PSRAM->memioPtr[addr](cpu, false, addr, data);
            }
            // Write (possibly modified) data to PSRAM
            cpu->_z80PSRAM->RAM[RAMaddr + blockOfs] = data;
            if(waitStates) Z80CPU_waitPhysicalStates(cpu, waitStates);
            break;

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

        case MEMBANK_TYPE_FUNC:
            // Pure virtual — handler only, no PSRAM
            if(cpu->_z80PSRAM->memioPtr[addr] != NULL)
                cpu->_z80PSRAM->memioPtr[addr](cpu, false, addr, data);
            break;

        case MEMBANK_TYPE_PTR:
            Z80CPU_writeMem(cpu, addr, cpu->_z80PSRAM->memPtr[addr], data);
            break;
    }
}

Despacho de Puertos de 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);
    }
}
Observe que el despacho de E/S es más simple que el despacho de memoria — no existe el concepto de tipo de bloque para puertos de E/S. Cada dirección de E/S tiene un handler en ioPtr[] o va al hardware físico. No hay distinción ROM/RAM/FUNC para E/S.
También note: el Z80 usa una dirección de 16 bits en las instrucciones de E/S — los 8 bits inferiores son el número de puerto; los 8 bits superiores llevan el contenido del registro B (durante las instrucciones IN r,(C) / OUT (C),r). Si desea un comportamiento diferente del handler dependiendo del registro B, examine el byte alto de addr. Para coincidencia simple solo por número de puerto, enmascare con addr & 0xFF.

El Framework de Controladores

El framework de controladores es el mecanismo por el cual los módulos de controladores en C se descubren, se instancian desde la configuración JSON y se conectan a los sistemas de memoria y E/S. Tiene dos niveles:
  • Controladores de nivel superior (también llamados personas) — registrados en virtualFuncMap[] en Z80CPU.c. Cada persona configura una personalidad de máquina completa: disposición de memoria, banking, puertos de E/S, y opcionalmente un conjunto de sub-interfaces (tarjetas de interfaz).
  • Controladores de interfaz — registrados en el propio interfaceFuncMap[] de la persona. Cada controlador de interfaz añade un periférico específico (disquetera, QuickDisk, expansión de RAM, sistema de archivos) a la persona.

virtualFuncMap — Registro de Controladores de Nivel Superior

El array virtualFuncMap[] en src/Z80CPU.c mapea un nombre de cadena a una función init del controlador. Cada controlador de nivel superior (persona) debe tener una entrada aquí. El nombre de cadena debe coincidir exactamente con el campo "name" en el array JSON "drivers" (sin distinción de mayúsculas/minúsculas).
// 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 función de búsqueda que recorre esta tabla por nombre es:
// 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 — Construir sobre un Controlador o BIOS Existente

El modelo OpenZ80 (src/drivers/Other/Open.c, compilado con build_tzpuPico.sh open) es el punto de partida para dos tipos de experimentador: alguien que integra el picoZ80 en una placa de su propio diseño, y alguien que pone en marcha una máquina Z80 que aún no tiene una persona dedicada. Open.c está modelado a partir de MZ700.c pero despojado de todo el hardware de la máquina: en modo PHYSICAL pasa todo el espacio de 64K de memoria y E/S a la placa real (las tarjetas de interfaz superponen sus puertos fijos); en modo VIRTUAL presenta una RAM plana de 64K en la que las ROMs a nivel de controlador se cargan secuencialmente desde 0x0000. Se registra como {"Open", Open_Init} bajo #ifdef INCLUDE_OPEN_DRIVERS, y ofrece únicamente las tarjetas de interfaz independientes de la máquina (MZ-1R12/1R18/1R23/1R37, PIO-3034, MZ-8BIO3, MZ-1E24, MZ-1E05, Celestite), cada una de las cuales puede moverse a cualquier puerto base de E/S.
1 — Comience desde el controlador más cercano
Copie el controlador de persona más cercano a su objetivo y edite su mapa de memoria, handlers de E/S y disposición de ROM. Los controladores de persona son:
  • src/drivers/Other/Open.c — la persona vanilla (la mejor base para una placa completamente nueva).
  • src/drivers/Sharp/MZ700.c, MZ80A.c, MZ80K.c, MZ80B.c, MZ800.c, MZ1500.c, MZ2000.c, MZ2200.c, MZ2500.c.
  • src/drivers/Amstrad/PCW9512.c — una máquina autónoma con un FDC uPD765 integrado.
  • src/drivers/Tatung/EinsteinTC01.c — una máquina autónoma con un FDC WD1770 integrado.
Registre la nueva persona añadiendo una línea {"MyMachine", MyMachine_Init} a virtualFuncMap[] arriba (bajo un #ifdef apropiado), y conecte su código fuente en src/CMakeLists.txt (por ejemplo, añadiéndolo a la lista pZ80_drivers_open_src) más un target de modelo mediante add_z80_model_targets(...).
2 — Reutilice o parchee las ROMs de la máquina
Las ROMs de monitor, IPL, BIOS de CP/M y arranque de disquete que cargan los controladores se mantienen como código fuente comentado en ensamblador Z80 en los proyectos complementarios RFS y TZFS, bajo sus directorios asm/ (ensamblados con el ensamblador Z80 GLASS). Tómelas como base y recompílelas o parchéelas para su máquina: </font>
Propósito Archivos fuente
ROMs de monitor RFS/asm/sp1002.asm (MZ-80K SP-1002), RFS/asm/sa1510.asm (MZ-80A SA-1510), RFS/asm/1z-013a.asm & TZFS/asm/1z-013a.asm (MZ-700), RFS/asm/mz800_iocs.asm (MZ-800 IOCS)
Cargadores de arranque (IPL) TZFS/asm/mz2000_ipl.asm (MZ-2000), TZFS/asm/mz80b_ipl.asm (MZ-80B), RFS/asm/ipl.asm
BIOS de CP/M RFS/asm/cbios.asm, RFS/asm/cpm22-bios.asm, TZFS/asm/cbios.asm, TZFS/asm/cbiosII.asm
ROMs de arranque de disquete / 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
Sistemas de archivos en ROM RFS/asm/rfs.asm (banked), TZFS/asm/tzfs.asm (banked)
Ensamble la ROM que necesite, coloque el binario resultante en la tarjeta SD y refiéralo desde una entrada rom[].loadaddr (para una tarjeta de interfaz) o, en OpenZ80 en modo virtual, como una ROM a nivel de controlador cargada en 0x0000. Tanto RFS como TZFS incluyen scripts de compilación autónomos (véanse sus Guías del Desarrollador) que producen estas imágenes ROM.

TZFS — Modos de Memoria y el Procesador de Servicio Virtual

src/drivers/Sharp/TZFS.c es un buen ejemplo práctico de un controlador que combina la emulación de modos de memoria con conmutación de bancos con un modelo de servicio entre núcleos. Implementa un monitor de bajo nivel multibanco y sistema de archivos (una mejora del MONITOR 1Z-013A) con CP/M por debajo, modelado sobre el TZFS del tranZPUter SW y su procesador de E/S virtual K64F. Se registra como una interfaz seleccionable en la persona MZ-700 (en MZ700.c, junto a — y en la práctica exclusivo con — RFS), no como una persona de nivel superior.
Conmutación de modos de memoria (puerto 0x60)
El Z80 selecciona una disposición de memoria tranZPUter escribiendo un valor de modo en el puerto de E/S 0x60; el controlador reapunta sus punteros de banco en respuesta. Los modos son TZMM_ORIG, TZMM_BOOT, TZMM_TZFS, TZMM_TZFS2, TZMM_TZFS3, TZMM_TZFS4, TZMM_CPM, TZMM_CPM2 y TZMM_COMPAT. La disposición CP/M CPM2 usa paginación del bloque 0 con granularidad de byte (0x00000x003F → bloque de vectores, 0x00400x01FF → el inicio del TPA).
Procesador de servicio virtual K64F (OUT 0x68 → Core 0)
En lugar de atender las llamadas del sistema de archivos en el núcleo del Z80, TZFS usa un procesador de servicio virtual. El Z80 ejecuta OUT (0x68), que encola un mensaje MSG_TZFS_SVCREQ al Core 0; el Core 0 lo despacha mediante TZFS_processServiceRequest. Los servicios del sistema de archivos son READDIR / NEXTDIR (bloques de directorio de 16 entradas en caché), READFILE / NEXTREADFILE, LOADFILE (consciente de la cabecera MZF y de los objetos de banco), CHANGEDIR y CLOSE. Los servicios de CP/M son LOADBDOS (recarga de CCP + BDOS en arranque en caliente), ADDSDDRIVE, READSDDRIVE y WRITESDDRIVE, más servicios de frecuencia de CPU. Como picoZ80 no tiene acceso directo a la SD, los sectores de 512 bytes de CP/M se enrutan a través del ESP32 (ESP_readSector / ESP_writeSector) contra archivos de imagen completos; las rutas de imagen por unidad provienen de las entradas param[].file del JSON de la interfaz, con la plantilla de reserva CPM/SDC16M/RAW/CPMDSK<nn>.RAW. La ROM TZFS (roms/tzfs.bin) es la salida ensamblada del TZFS/asm/tzfs.asm del proyecto complementario (su BIOS de CP/M desde TZFS/asm/cbios.asm / cpm22.asm).

Flujo de Inicialización

Después de que el RP2350 lee y analiza config.json, la inicialización de controladores ocurre en el siguiente orden:
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)
Note el orden: los controladores se inicializan primero, luego se aplican los arrays JSON "memory" y "io". Esto significa que cualquier entrada explícita en los arrays memory o io en config.json anulará lo que el controlador configuró para esas direcciones. Esto permite al usuario ajustar los valores predeterminados del controlador sin modificar el código fuente del controlador.

Callbacks del Ciclo de Vida de Controladores

Un controlador registra tres callbacks continuos almacenando punteros a función en su estructura t_drvConfig durante la inicialización. El Core 1 los llama en puntos específicos durante la ejecución:
Callback Firma Cuándo se llama Uso típico
reset_ptr uint8_t f(Z80CPU *cpu) Línea RESET del host activada; Z80 PC = 0x0000 Restaurar mapa de memoria predeterminado, limpiar estado de bancos
poll_ptr uint8_t f(Z80CPU *cpu) Cada ~2048 ciclos Z80 (en el Core 1) Verificar flags de estado, enviar solicitudes al Core 0
task_ptr uint8_t f(Z80CPU *cpu, enum Z80CPU_TASK_NAME task, char *param) En respuesta a solicitudes de tareas inter-core Manejar resultados de E/S de archivos, entrega de sectores de disco
Importante: poll_ptr se llama desde el Core 1 y debe ser rápido. Si necesita realizar E/S (cargar un sector de disco, enviar un comando UART), debe enviar un mensaje al Core 0 a través de cpu->requestQueue y retornar inmediatamente. El Core 0 realizará la E/S y devolverá el resultado a través de cpu->responseQueue, activando task_ptr en la próxima oportunidad disponible.

Ejemplo Práctico: El Controlador MZ-700

El controlador del Sharp MZ-700 (src/drivers/Sharp/MZ700.c) es el controlador persona más completo del código base. Recorrerlo en detalle muestra cada patrón que necesitará para sus propios controladores. El controlador del MZ-80A (src/drivers/Sharp/MZ80A.c) proporciona otra implementación de referencia, demostrando la emulación del Intel 8253 PIT y el mecanismo de intercambio de memoria MEMSW. El controlador del MZ-2000 (src/drivers/Sharp/MZ2000.c) es una referencia adicional, mostrando el cambio de modos de memoria BST/NST, la superposición de VRAM de caracteres y gráficos con selección de banco controlada por Z80 PIO, e integración con el FDC MB8866. Soporta tanto el modo físico (reemplazo directo del Z80 en un MZ-2000 real con detección automática de modo boot/normal) como el modo virtual (emulación completa basada en PSRAM con espejo de IPL ROM). El controlador del MZ-800 (src/drivers/Sharp/MZ800.c) es una persona de modo dual que rastrea el registro Display-Mode del GDG y reconstruye la decodificación de su mapa de memoria sobre la marcha, y demuestra el puenteo de la cadena daisy-chain de interrupciones en modo virtual (véase El Controlador MZ-800 más abajo).
Están disponibles varios módulos de emulación de periféricos reutilizables: PIT8253.c (Intel 8253 Programmable Interval Timer — los seis modos de contador, conteo BCD/binario, latch de contador, modos de lectura/carga LSB/MSB), PPI8255.c (Intel 8255 Programmable Peripheral Interface — E/S Modo 0, set/reset de bits para el Puerto C, callbacks de salida por puerto e inyección de entrada), WD1773.c (controlador de disco WD1773 — utilizado por los controladores persona Sharp MZ), WD1770.c (controlador de disco WD1770 — utilizado por la persona Tatung Einstein, soportando formatos Extended CPC DSK, D88 y DSK estándar), y uPD765.c (controlador de disco NEC uPD765 — utilizado por la persona Amstrad PCW-9512, soportando formato CPC DSK e imagen de disco físico). Estos módulos están diseñados para ser instanciados por cualquier controlador persona de máquina.

El Controlador MZ-800 (Persona de Modo Dual)

El controlador del MZ-800 (src/drivers/Sharp/MZ800.c) es un buen ejemplo de una persona cuyo mapa de memoria no es fijo sino que cambia en tiempo de ejecución bajo el control del software del host. El MZ-800 es un superconjunto del MZ-700: arranca en un modo compatible con el MZ-700 y puede cambiar a un modo MZ-800 nativo con una disposición de memoria y anchura de gráficos diferentes. El controlador rastrea el modo de la máquina y reconstruye su tabla de decodificación de bloques cada vez que cambia el modo.

Registro Display-Mode del GDG (puerto 0xCE). El controlador instala un handler ioPtr[] en el puerto 0xCE que espía las escrituras al registro Display-Mode del GDG (DMD):

  • Bit 3 selecciona el modo de la máquina — a cero = MZ-800 nativo, a uno = compatibilidad MZ-700.
  • Bit 2 selecciona la anchura de gráficos — 320 vs 640 píxeles.

Cuando una escritura cambia cualquiera de los dos bits, el controlador llama a MZ800_applyMemoryMap(), que recorre los 128 bloques de 512 bytes y reescribe las entradas _membankPtr[] sobre la marcha para reflejar la nueva disposición. Este es el mismo mecanismo de despacho rápido descrito en Codificación de membankPtr — no se requiere ninguna detención del bus porque solo se reconstruye la tabla de decodificación, no la PSRAM subyacente.

Flags de control del mapa de memoria. La disposición actual se mantiene en una pequeña estructura de estado como una máscara de bits de flags: ROM_0000 (ROM del monitor visible en 0x0000), ROM_1000 (ventana de CG-ROM en 0x1000), CGRAM_VRAM (RAM/VRAM del generador de caracteres mapeada en la ventana 0x1000), y ROM_E000 (IOCS ROM / región de hardware en 0xE000). MZ800_applyMemoryMap() deriva los tipos de bloque a partir de estos flags junto con los bits de modo MZ-700/MZ-800 y 320/640 actuales. El mapa de encendido/reset expone la ROM del monitor, la CG-ROM y la IOCS ROM. Los puertos de banking de memoria del MZ-800 (0xE0–0xE6) son manejados por el controlador y reflejados al hardware físico para que una máquina real aguas abajo se mantenga sincronizada.

Puenteo de la cadena daisy-chain de interrupciones (modo virtual). Esta es la parte más sutil del controlador y sigue el mismo patrón que el controlador del MZ-2500. En modo MZ-800 nativo, la interrupción periódica es generada por el 8253 real y alimentada a través del Z80-PIO físico, que se sitúa en la cadena daisy-chain de interrupciones. El latch in-service del PIO solo se limpia cuando observa una lectura de opcode RETI (ED 4D) en el bus real — pero en modo virtual la rutina de servicio de interrupción y su RETI se ejecutan desde la PSRAM, por lo que el PIO nunca las ve y permanecería enganchado, bloqueando todas las interrupciones posteriores. El controlador puentea esto instalando dos hooks en el núcleo Zeta:

  • MZ800_readIntAck() — instalado como cpu->_Z80.inta, el callback de reconocimiento de interrupción.
  • MZ800_retiHandler() — instalado como cpu->_Z80.reti, el callback de RETI.

Ambos llaman a mz800PhysicalReti(), que reproduce un RETI físico en el bus real: coloca los dos bytes ED 4D en SP-2 en la RAM real del host, realiza lecturas de opcode M1 de ambos bytes en el bus físico (para que la lógica daisy-chain del PIO observe el RETI y limpie su latch in-service), y luego restaura los bytes originales que desplazó. Estos hooks se instalan solo cuando la interfaz se ejecuta virtualmente (!isPhysical); en modo físico el PIO real ve el RETI real directamente y no se necesita ningún puenteo.

Reset. MZ800_Reset() restaura el mapa de memoria de encendido, emite un OUT 0xE4 para imitar el reset del gate-array, limpia el área de trabajo RFS/MZF (0x1000–0x1168), y — como el 8253 PIT no tiene pin de reset — reprograma y enmascara el 8253 cuando está en modo MZ-700 para que un contador obsoleto no pueda disparar una interrupción espuria después del reset.

Volver al Ejemplo del MZ-700

El MZ-700 es un ordenador Sharp de 8 bits de 1982 basado en el Z80A. Su mapa de memoria tiene algunas características distintivas que lo convierten en un ejemplo de aprendizaje ideal:
  • Los 4KB inferiores (0x0000–0x0FFF) son una ROM Monitor al encender, pero se pueden intercambiar por RAM mediante escrituras a puertos de E/S — el llamado "bank-switching del MZ-700".
  • La región superior (0xD000–0xFFFF) contiene Video RAM, VRAM de color y registros de hardware mapeados en memoria. Toda la región superior también se puede intercambiar por RAM mediante puertos de E/S.
  • El banking de memoria se controla mediante seis puertos de E/S (0xE0–0xE6) que intercambian bloques de entrada y salida.

Mapa de Funciones de Interfaz

En la parte superior de MZ700.c, una tabla interfaceFuncMap[] lista todas las tarjetas de interfaz de periféricos (sub-controladores) que la persona MZ-700 conoce. Este es el equivalente MZ-700 de virtualFuncMap[] — mapea nombres de interfaz (del array JSON "if") a funciones init para cada tarjeta adicional.
// src/drivers/Sharp/MZ700.c

typedef struct {
    const char  *interfaceFuncName;  // Interface name string (matches JSON "if.name")
    bool         active;             // true once this interface has been initialised
    InitFunc     init_func_ptr;      // Called during driver init when this interface appears in JSON
    ResetFunc    reset_func_ptr;     // Called on RESET
    PollFunc     poll_func_ptr;      // Called every ~2048 Z80 cycles
    TaskFunc     task_func_ptr;      // Called for inter-core task delivery
} t_InterfaceFuncMap;

static t_InterfaceFuncMap interfaceFuncMap[] = {
    {"RFS",     false, RFS_Init,    RFS_Reset,    RFS_PollCB,    RFS_TaskProcessor},
    {"MZ-1E05", false, MZ1E05_Init, MZ1E05_Reset, MZ1E05_PollCB, MZ1E05_TaskProcessor},
    {"MZ-8BFI", false, MZ8BFI_Init, MZ8BFI_Reset, MZ8BFI_PollCB, MZ8BFI_TaskProcessor},
    {"MZ-1E14", false, MZ1E14_Init, MZ1E14_Reset, MZ1E14_PollCB, MZ1E14_TaskProcessor},
    {"MZ-1E19", false, MZ1E19_Init, MZ1E19_Reset, MZ1E19_PollCB, MZ1E19_TaskProcessor},
    {"MZ-1R12", false, MZ1R12_Init, MZ1R12_Reset, MZ1R12_PollCB, MZ1R12_TaskProcessor},
    {"MZ-1R18", false, MZ1R18_Init, MZ1R18_Reset, MZ1R18_PollCB, MZ1R18_TaskProcessor},
};
static const size_t interfaceFuncMapSize =
    sizeof(interfaceFuncMap) / sizeof(interfaceFuncMap[0]);
Nota: El ejemplo anterior muestra la lista de interfaces de la persona MZ-700. Cada persona tiene su propio interfaceFuncMap[] con un subconjunto diferente de interfaces disponibles. El conjunto completo de controladores de interfaz, sus cadenas JSON "name" para el array "if", y qué personas los soportan se documenta en la tabla de Compatibilidad Persona–Interfaz de la Guía Técnica. Para ejemplos de configuración JSON del usuario final, consulte el Manual de Usuario de picoZ80.

Estructura de Estado de Banking

Dado que el estado de banking debe persistir entre transacciones de bus (una escritura a 0xE0 debe recordarse para que los accesos de memoria posteriores vayan al banco correcto), el controlador mantiene una estructura de estado estática:
// 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,
};
Las variables estáticas como esta son seguras porque solo hay una instancia de Z80CPU y el Core 1 es el único hilo que llama a las funciones handler. Si alguna vez tuviera múltiples instancias de CPU (no es el diseño actual), movería este estado dentro de t_drvConfig.

La Función Init — MZ700_Init()

MZ700_Init() sirve un doble propósito. Se llama en dos contextos diferentes, identificados por qué argumentos son NULL:
  • Modo de validación (ifName != NULL, config == NULL): Llamado por Z80CPU_configDriversFromJSON() para preguntar "¿soportas este nombre de interfaz?" Retorna 1 si sí, 0 si no. Esto permite al parser JSON validar nombres de interfaz contra el controlador antes de intentar inicializarlos.
  • Modo de configuración (ifName == NULL, config != NULL): La inicialización real — configurar el mapa de memoria, instalar hooks, configurar 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;
}

El Handler de Banking — MZ700_IO_MemoryBankPorts()

Este es el handler de E/S que gestiona los seis puertos de control de banking del MZ-700. Es llamado por Z80CPU_writeIO() cada vez que el Z80 escribe en los puertos 0xE0–0xE6. Leer de estos puertos no tiene efecto secundario (retorna 0xFF). Escribir cambia el mapa de memoria modificando las entradas de _membankPtr[] en tiempo real.
// 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
}
Este handler demuestra el patrón más importante en la escritura de controladores: modificar _membankPtr[] en respuesta a una escritura de E/S para implementar banking de memoria. Los cambios surten efecto inmediatamente — el próximo acceso de memoria del Z80 usará el nuevo mapeo.
El patrón de guardar/restaurar para la región de memoria superior (MZ700Ctrl.upmembankPtr[]) es importante: cuando intercambia regiones mapeadas a hardware por RAM, debe recordar lo que había allí para poder restaurarlo cuando el software haga el intercambio de vuelta. Simplemente asignar PHYSICAL de nuevo perdería cualquier mapeo personalizado que fue configurado por sub-controladores o la configuración JSON.

El Handler de Reset — MZ700_Reset()

Cuando el host activa RESET, el Core 1 llama al reset_ptr de cada controlador. El handler de reset del MZ-700 restaura el mapa de memoria al estado de encendido (ROM en 0x0000, VRAM y hardware en 0xD000+) y luego llama a todos los handlers de reset de interfaces activas:
// 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;
}

El Handler de 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;
}

El Procesador de Tareas — 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;
}
El patrón del procesador de tareas es consistente en todos los controladores: la persona simplemente distribuye la tarea a todas las sub-interfaces activas. Cada sub-interfaz verifica si la tarea es relevante para ella y la ignora si no lo es.

Escribir un Nuevo Controlador — Paso a Paso

Esta sección recorre cada paso necesario para crear un controlador completo desde cero. El ejemplo crea un disco RAM simple (un bloque de 64KB de PSRAM al que el Z80 accede como memoria mapeada por E/S) para ilustrar todos los patrones sin la complejidad de la emulación de hardware real.

Paso 1 — Crear los Archivos Fuente

Cree dos archivos. El archivo de cabecera declara las funciones que otros módulos llamarán; el archivo C las implementa.
// File: src/include/drivers/Sharp/MyDriver.h
#ifndef MYDRIVER_H
#define MYDRIVER_H

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

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

// Lifecycle callbacks (set by init, called by Core 1 loop)
uint8_t MyDriver_Reset(Z80CPU *cpu);
uint8_t MyDriver_PollCB(Z80CPU *cpu);
uint8_t MyDriver_TaskProcessor(Z80CPU *cpu,
                                enum Z80CPU_TASK_NAME task,
                                char *param);

// Memory / I/O handler functions
uint8_t MyDriver_MemHandler(Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
uint8_t MyDriver_IOHandler(Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);

#endif // MYDRIVER_H
// File: src/drivers/Sharp/MyDriver.c
#include <string.h>
#include <stdio.h>

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

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

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

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

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

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

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

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

// ------------------------------------------------------------------
// I/O handler: called for ports 0xC0 and 0xC1
// ------------------------------------------------------------------
uint8_t MyDriver_IOHandler(Z80CPU *cpu, bool read, uint16_t addr, uint8_t data)
{
    uint8_t port = (uint8_t)(addr & 0xFF);

    if(read)
    {
        // Port 0xC0: return status byte
        if(port == MYDRIVER_IO_STATUS)
            return MyState.enabled ? 0x01 : 0x00;
        return 0xFF;
    }
    else
    {
        // Port 0xC1: control byte
        if(port == MYDRIVER_IO_CONTROL)
        {
            MyState.controlReg = data;

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

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

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

                    MyState.enabled = true;
                }
            }
            else
            {
                // Bit 0 = 0: unmap, restore PHYSICAL pass-through
                if(MyState.enabled)
                {
                    int startBlock = 0x8000 / MEMORY_BLOCK_SIZE;
                    int endBlock   = 0xC000 / MEMORY_BLOCK_SIZE;

                    for(int idx = startBlock; idx < endBlock; idx++)
                    {
                        cpu->_membankPtr[idx] = (MEMBANK_TYPE_PHYSICAL << 24)
                                              | (0 << 16)
                                              | (idx * MEMORY_BLOCK_SIZE);
                    }
                    // Remove memioPtr handlers
                    for(uint32_t a = 0x8000; a < 0xC000; a++)
                        cpu->_z80PSRAM->memioPtr[a] = NULL;

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

// ------------------------------------------------------------------
// Reset handler: restore power-on state
// ------------------------------------------------------------------
uint8_t MyDriver_Reset(Z80CPU *cpu)
{
    // If our RAM was mapped in, restore physical pass-through
    if(MyState.enabled)
    {
        int startBlock = 0x8000 / MEMORY_BLOCK_SIZE;
        int endBlock   = 0xC000 / MEMORY_BLOCK_SIZE;
        for(int idx = startBlock; idx < endBlock; idx++)
        {
            cpu->_membankPtr[idx] = (MEMBANK_TYPE_PHYSICAL << 24)
                                  | (0 << 16)
                                  | (idx * MEMORY_BLOCK_SIZE);
        }
        for(uint32_t a = 0x8000; a < 0xC000; a++)
            cpu->_z80PSRAM->memioPtr[a] = NULL;
    }

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

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

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

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

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

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

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

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

Paso 2 — Añadir a CMakeLists.txt

Abra src/CMakeLists.txt y añada su nuevo archivo fuente a la lista de controladores 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
)
También debe añadir el directorio de su cabecera al include path si está en un subdirectorio nuevo. Para el directorio de controladores Sharp esto ya está configurado, así que no se necesita una llamada adicional a target_include_directories.

Paso 3 — Incluir la Cabecera en Z80CPU.c

Abra src/Z80CPU.c y añada un include para la cabecera de su controlador junto a los includes existentes de controladores 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

Paso 4 — Registrar en virtualFuncMap

Aún en src/Z80CPU.c, encuentre el array virtualFuncMap[] y añada su entrada. La cadena "MyDriver" es lo que el campo JSON "name" debe contener:
// 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},
#endif
#ifdef INCLUDE_AMSTRAD_DRIVERS
    {"PCW9512",  PCW9512_Init},
#endif
#ifdef INCLUDE_TATUNG_DRIVERS
    {"EinsteinTC01", EinsteinTC01_Init},
#endif
    // ADD YOUR DRIVER:
    {"MyDriver", MyDriver_Init},
};
La búsqueda no distingue entre mayúsculas y minúsculas, así que "mydriver", "MyDriver" y "MYDRIVER" en el JSON coincidirán con esta entrada.

Paso 5 — Añadir el Controlador a config.json

Añada una entrada "drivers" a su config.json en la tarjeta SD. El campo "name" debe coincidir con la cadena que registró en 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": []
          }
        ]
      }
    ]
  }
}

Paso 6 — Compilar y Probar

# 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
El sistema de compilación produce binarios de firmware de aplicación para cada objetivo de modelo — dos particiones (Partición 1 en 0x10020000, Partición 2 en 0x10520000) cada una en variantes estándar y DBGSH. Están disponibles tres objetivos de modelo:
  • BaseZ80 (pZ80-BaseZ80) — binario universal con todos los controladores (Sharp + Amstrad + Tatung). Define INCLUDE_SHARP_DRIVERS, INCLUDE_AMSTRAD_DRIVERS e INCLUDE_TATUNG_DRIVERS.
  • SharpZ80 (pZ80-SharpZ80) — solo controladores Sharp MZ. Define INCLUDE_SHARP_DRIVERS. Produce un binario de firmware más pequeño.
  • AmstradZ80 (pZ80-AmstradZ80) — solo controladores Amstrad PCW. Define INCLUDE_AMSTRAD_DRIVERS y TARGET_MODEL_AMSTRAD. Produce un binario de firmware más pequeño.
  • TatungZ80 (pZ80-TatungZ80) — solo controladores Tatung Einstein. Define INCLUDE_TATUNG_DRIVERS y TARGET_MODEL_TATUNG. Produce un binario de firmware más pequeño.
Cada objetivo de modelo tiene su propio directorio bajo src/model/ con un CMakeLists.txt dedicado, punto de entrada (main.c) y scripts de enlazador. La variante estándar omite el shell de depuración para una imagen de firmware más pequeña. La variante DBGSH añade la definición de compilación INCLUDE_DBGSH, que habilita el shell de depuración ICE completo en USB CDC Channel 1. Los nombres de firmware DBGSH se identifican por el sufijo _DBGSH.

Firmware ESP32 — Selección de Modo de Red

Antes de compilar el firmware ESP32, seleccione el modo de red deseado copiando el sdkconfig pre-construido apropiado en su lugar:
# 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
Las definiciones clave del preprocesador controladas por estas configuraciones son:
  • CONFIG_IF_WIFI_ENABLED — habilita la radio WiFi y el código AP/Client.
  • CONFIG_IF_USB_NCM_ENABLED — habilita la interfaz de red USB NCM y el servidor DHCP.
Estas se configuran mediante Kconfig (idf.py menuconfig) o los archivos sdkconfig pre-construidos listados arriba.

Patrones de Hooks de Memoria en Detalle

Esta sección describe cada patrón de hook en detalle con ejemplos completos. Estos son los bloques de construcción de toda la gestión de memoria de controladores.

Patrón 1 — Dispositivo Virtual Puro (bloque FUNC)

Utilice esto cuando quiera que una región del espacio de direcciones del Z80 sea completamente controlada por su handler, sin respaldo de PSRAM. Las lecturas y escrituras del Z80 siempre llaman a su función. Nada se almacena en 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;
    }
}

Patrón 2 — Interceptar Escrituras en una Región RAM

Utilice esto cuando quiera que una región se comporte como RAM normal (las lecturas retornan datos de PSRAM, las escrituras actualizan la PSRAM) pero también quiera ser notificado de las escrituras — por ejemplo, para espejar escrituras de video RAM a un buffer sombra o para activar una actualización de hardware. El tipo de bloque permanece como RAM; instale un handler memioPtr que post-procese la escritura.
// 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
    }
}

Patrón 3 — Capturar Escrituras en una Región ROM

Algunos hardware utilizan escrituras a direcciones mapeadas como ROM como escrituras de registro de banking (la escritura es "decodificada" por el hardware pero no modifica la ROM). El bloque permanece como ROM; las escrituras activan su handler pero la PSRAM no se modifica.
// 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;
    }
}

Patrón 4 — Handlers Dispersos (Direcciones Individuales)

No tiene que instalar handlers para un bloque completo. Puede instalar un handler en una única dirección específica dentro de un bloque RAM o ROM. El tipo de bloque controla lo que sucede con todas las demás direcciones del bloque; el handler de dirección específica anula solo esa dirección.
// 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
    }
}

Patrón 5 — Handler de Puerto de E/S

Los handlers de E/S son más simples — no hay respaldo de PSRAM para puertos de E/S. El handler se llama (si está instalado) o el ciclo de E/S va al hardware físico. No existe el concepto de tipo de bloque.
// 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;
    }
}

Añadir una Sub-Interfaz a una Persona Existente

Si su nuevo periférico es una tarjeta que se conecta a un MZ-700 (u otra persona existente), lo implementa como una sub-interfaz en lugar de un controlador de nivel superior. Este es el patrón utilizado por RFS, WD1773, QDDrive, MZ-1E05, MZ8BFI y las tarjetas de expansión de RAM.

Funciones Requeridas para una Sub-Interfaz

Una sub-interfaz necesita cuatro funciones con estas firmas:
// 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);

Registrar la Sub-Interfaz

Añada su sub-interfaz al interfaceFuncMap[] en el archivo C de la persona padre (por ejemplo, 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 cadena "MyCard" debe coincidir con el campo "name" de la entrada de interfaz en el array JSON "if" (sin distinción de mayúsculas/minúsculas):
"drivers": [
  {
    "enable": 1,
    "name":   "MZ700",
    "type":   "VIRTUAL",
    "if": [
      {
        "enable": 1,
        "name":   "MyCard",
        "type":   "VIRTUAL",
        "rom":    [],
        "addrmap": [],
        "iomap":   [],
        "param": [
          { "name": "myParam", "value": "42" }
        ]
      }
    ]
  }
]

Leer Parámetros JSON en una Sub-Interfaz

El t_drvIFConfig *ifConfig pasado a la init de su sub-interfaz contiene todos los datos configurados por JSON. Para leer un parámetro con nombre:
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;
}

Ejemplo Práctico de Sub-Interfaz: Tarjetas Serie RS-232C (Z80 SIO)

Las tarjetas serie RS-232C son un ejemplo completo y del mundo real del patrón de sub-interfaz anterior, y de una sub-interfaz que posee un módulo reutilizable de emulación de dispositivo y lo puentea a USB. Se proporcionan dos sub-interfaces de persona — MZ-8BIO3 (src/drivers/Sharp/MZ8BIO3.c) y MZ-1E24 (src/drivers/Sharp/MZ1E24.c) — ambas construidas sobre una emulación Zilog Z80 SIO/2 compartida (src/drivers/Z80SIO.c / src/include/drivers/Z80SIO.h).

La emulación Z80 SIO (Z80SIO.c). Este es un modelo Zilog Z80 SIO/2 autónomo y preciso a nivel de registro, completamente desacoplado tanto de USB como de la emulación de la CPU para que pueda ser reutilizado por cualquier tarjeta. Implementa:

  • Dos canales — el canal A en los offsets 0/1 (datos/control) y el canal B en los offsets 2/3.
  • El conjunto completo de registros de escritura WR0–WR7 y el conjunto de registros de lectura RR0–RR2, incluyendo el protocolo de puntero/comando de dos bytes de WR0 (se escribe un byte de selección de registro/comando, y luego el byte de datos se dirige al registro seleccionado).
  • Interrupciones vectorizadas en modo 2 del Z80 con una pila in-service de 4 niveles y prioridad daisy-chain estricta (Rx-especial del canal A la más alta, hasta external/status del canal B la más baja), y la opción status-affects-vector.
  • Dos anillos lock-free de único-productor/único-consumidor por canal, de 1 KB cada uno: un anillo Tx (productor Core 1 → consumidor Core 0) y un anillo Rx (productor Core 0 → consumidor Core 1). Se exponen helpers de push/pop del anillo para el bombeo USB, y se expone un volatile bool* para la línea /INT.

Al operar sobre USB (en lugar de una línea serie real), DCD y CTS se mantienen activados para que el firmware del host que consulta el estado de control del módem no se bloquee esperando una portadora.

Implementación de tarjeta compartida y el wrapper delgado. MZ8BIO3.c contiene la implementación compartida SIOCard_* (init, reset, poll, task, bombeo USB). MZ1E24.c es un wrapper delgado que difiere únicamente en el modo de conector pasado a SIOCard_Init()SIOCARD_MODE_BI para el MZ-8BIO3 frente a SIOCARD_MODE_ST para el MZ-1E24. La función init de la tarjeta:

  • Analiza el parámetro "port" (por defecto 0xB0), enmascarándolo a un límite de 4 puertos para que los cuatro registros del SIO caigan sobre una base limpia.
  • Instancia el Z80 SIO y conecta su línea /INT a cpu->swIntAssert — el hook de fuente de interrupción por software en la emulación de la CPU.
  • Instala el handler de E/S en las 256 variantes de byte alto de cada uno de los cuatro puertos (el Z80 coloca el registro B en A8–A15 durante la E/S, por lo que cada variante de byte alto debe resolverse al mismo handler).
  • Registra los hooks de reconocimiento/RETI de interrupción por software (swIntAckVector / swIntReti) para que la entrega de vector en modo 2 y la limpieza in-service funcionen.
  • Instala el bombeo USB SIOCard_usbPump() en el hook global g_usbSerialPump.

En modo físico la tarjeta no hace nada y retorna 0 — la tarjeta de hardware real responde en el bus del host.

Puente USB. El canal A se puentea a USB CDC 2 (VSER_CHANNEL_A) y el canal B a USB CDC 3 (VSER_CHANNEL_B). Estos dos puertos CDC no tienen ninguna UART física de respaldo; en su lugar pollUSBtoUART() en el Core 0 llama a SIOCard_usbPump() para trasladar bytes entre los FIFOs CDC y los anillos de canal del SIO (host → anillo Rx, anillo Tx → host). La tarjeta expone los cuatro callbacks estándar de sub-interfaz — SIOCard_Reset, SIOCard_PollCB, SIOCard_TaskProcessor — y está registrada en el interfaceFuncMap[] de cada persona con capacidad SIO (MZ-700, MZ-800, MZ-80B, MZ-1500) como, por ejemplo: </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},
Ambas tarjetas siguen el contrato estándar de controlador de interfaz descrito arriba — carecen de ROM (sin entradas "rom"), instalan solo handlers de E/S y callbacks, y son seleccionadas por la persona que las lista en su interfaceFuncMap[]. En el lado de la configuración web se anuncian a través de configgui.js (el mapa driverInterfaces y la tabla interfaceRomLimits, donde llevan un conteo de ROM de cero).

Interacción Core 0 / Core 1

Los handlers de controladores se ejecutan en el Core 1 dentro del bucle principal. Cualquier operación que tome más de unos pocos microsegundos (E/S de archivos, comandos UART al ESP32, malloc) debe descargarse al Core 0 usando la cola intercore.

Uso de la Cola Intercore

El patrón es:
  1. Su handler (en el Core 1) detecta que se necesita una operación de E/S de archivo o similar (por ejemplo, el Z80 escribió un número de sector en un registro de comando de disco).
  2. El handler establece un flag de estado (por ejemplo, diskState.pendingRead = true) y retorna inmediatamente — no realiza la E/S.
  3. Su poll_ptr (también en el Core 1, llamado cada ~2048 ciclos) verifica el flag de estado y, si está establecido, introduce un mensaje de solicitud en cpu->requestQueue.
  4. El Core 0 recibe el mensaje, realiza la E/S de archivo (por ejemplo, lee un sector de disco desde la tarjeta SD), e introduce el resultado de vuelta en cpu->responseQueue.
  5. Su task_ptr es llamado (en el Core 1) con el resultado de la tarea. Copia los datos del sector en la PSRAM y limpia el flag pendiente.
// 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;
}

Errores Comunes

  • Bloquear en un handler. El error más común. Cualquier llamada a debugf, sleep_ms, fopen, o cualquier función UART desde dentro de un handler o callback de poll detendrá el Core 1 y causará que el Z80 del host vea temporización de bus incorrecta. Mueva todas las operaciones bloqueantes al Core 0 a través de la cola de solicitudes. Use plogf() en lugar de debugf() para el registro de la ruta de arranque que debe funcionar antes de que USB esté disponible (véase Registro de Depuración).
  • Orden incorrecto de cierre en el registro de handlers. Al instalar handlers en un rango usando un bucle, asegúrese de que los límites del bucle usen < y no <= para la dirección final — los errores de uno pueden corromper handlers adyacentes.
  • Olvidar limpiar handlers al apagar o resetear el controlador. Si su handler de reset no limpia las ranuras memioPtr[] o ioPtr[] que su controlador instaló, esos handlers seguirán siendo llamados después del reset, con estado potencialmente obsoleto.
  • Colisión de número de banco. Cada banco PSRAM es de 64KB. La configuración JSON asigna bancos a regiones de memoria. Si dos controladores usan el mismo número de banco, sobrescribirán los datos del otro. Use números de banco únicos para cada controlador. Los bancos 0–7 son típicamente usados por la persona MZ-700; use bancos 8+ para sub-interfaces y controladores adicionales.
  • MEMBANK_TYPE_FUNC sin handler instalado. Si establece un bloque a tipo FUNC pero no instala un handler memioPtr, las lecturas retornarán 0x00 y las escrituras se descartarán silenciosamente. Este es un comportamiento válido pero a menudo es un bug — siempre instale el handler antes de establecer el tipo de bloque.
  • Nombre de virtualFuncMap y nombre JSON no coinciden. La búsqueda no distingue mayúsculas/minúsculas pero la cadena debe coincidir exactamente en lo demás. Un error tipográfico en cualquiera de las ubicaciones causará que el controlador sea silenciosamente omitido sin mensaje de error. Añada una llamada temporal a debugf en Z80CPU_getVirtualFunc() si su controlador no se está inicializando — debugf es una macro que se puede deshabilitar o limitar en builds de producción para que no impacte la temporización del bus.
  • Olvidar establecer reset_ptr / poll_ptr / task_ptr. Si no asigna estos en su función init, el Core 1 nunca llamará a sus funciones de reset, poll o task. El controlador se inicializará correctamente pero no responderá a RESET ni realizará ningún mantenimiento periódico.

Registro de Depuración

Depurar el firmware picoZ80 es desafiante porque es un sistema multi-core y multi-procesador: el RP2350 ejecuta dos cores Cortex-M33 con responsabilidades distintas de tiempo real y no tiempo real, y el coprocesador ESP32 maneja toda la E/S de red y almacenamiento. La depuración tradicional estilo printf no es sencilla — USB puede no estar disponible durante el arranque temprano, el bucle principal del Core 1 no puede tolerar llamadas bloqueantes, y un reset del watchdog destruye el estado volátil. El firmware proporciona tres mecanismos de salida de depuración complementarios para abordar estas restricciones.

debugf() — Salida de Depuración con Buffer

debugf() es la macro principal de salida de depuración. Funciona como printf() pero escribe en un buffer de 64KB en PSRAM (en la dirección 0x117EF004) en lugar de directamente a un puerto serial. El buffer se vacía a USB CDC cuando el bucle principal tiene tiempo libre. Un mutex (debugMutex) protege el buffer del acceso concurrente de ambos 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");
Propiedades clave:
  • Tamaño del buffer: 64KB (MAX_DEBUG_BUFFER_SIZE = 65536) en PSRAM.
  • Thread-safe: protegido por mutex para acceso multi-core.
  • Residente en PSRAM: el buffer sobrevive a resets del watchdog (la PSRAM retiene datos), pero el puntero del buffer en 0x117EF004 debe ser revalidado después del reset.
  • Nunca llame desde handlers o callbacks de poll del Core 1. La adquisición del mutex puede bloquear, introduciendo jitter en la temporización del bus. Use plogf() para mensajes de la ruta de arranque o envíe solicitudes de depuración al Core 0 a través de la cola intercore.

plogf() — Log Persistente de Arranque en PSRAM

plogf() escribe en una región dedicada de 4KB al final de la PSRAM de 8MB (dirección 0x117FF000). A diferencia de debugf(), no usa mutex y está destinada solo para el registro de la ruta de arranque del Core 0 — capturando mensajes durante las etapas críticas de arranque temprano antes de que USB esté disponible para la salida de depuración normal. Dado que la PSRAM retiene su contenido entre resets del watchdog, estos mensajes sobreviven a un fallo y pueden examinarse en el siguiente arranque exitoso.
// 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
Patrón de uso típico: Durante el arranque, llame a plogf() en cada hito crítico (init de PSRAM, handshake SPI, análisis de configuración). Si el watchdog se dispara, el buffer de plog retiene todos los mensajes hasta el punto de cuelgue. En el siguiente arranque exitoso, dump_plog() imprime los mensajes capturados antes de limpiar el buffer, proporcionando una traza clara de lo que sucedió antes del fallo.

Elegir la Salida de Depuración Correcta

Macro Ubicación Mutex Sobrevive reset WDT Seguro desde Core 1 Caso de uso
debugf() PSRAM (buffer de 64KB) Contenido del buffer sí; el puntero debe revalidarse No — bloqueará Salida de depuración general del Core 0
plogf() PSRAM (4KB al final) No No — solo Core 0 Registro de ruta de arranque antes de USB
SWD + GDB Sonda de hardware N/A N/A Sí (puertos por core) Depuración en vivo, breakpoints, inspección

Watchdog y Seguimiento de Progreso de Arranque

El picoZ80 opera en un entorno desafiante: un RP2350 de doble core comunicándose por SPI con un ESP32, atendiendo una interfaz de bus Z80 precisa por ciclo a través de PIO, todo mientras gestiona 8MB de PSRAM externa. Un cuelgue en cualquier punto durante el arranque — inicialización de PSRAM, handshake SPI, análisis de configuración, o lanzamiento del Core 1 — dejaría la placa sin respuesta y sin salida de diagnóstico. El timer watchdog de hardware y el sistema de seguimiento de progreso de arranque fueron diseñados para hacer tales fallos recuperables y diagnosticables.

Timer Watchdog

El watchdog de hardware del RP2350 se habilita temprano en main() con un timeout de 30 segundos:
// src/model/BaseZ80/main.c
watchdog_enable(30000, true);  // 30s timeout, pause-on-debug enabled
watchdog_update() se llama en cada hito de arranque y a lo largo del bucle principal. Las operaciones de larga duración críticas (cargas de imágenes de disco con lógica de reintentos, transferencias DMA, handshake SPI del ESP32) incluyen actualizaciones explícitas del watchdog para prevenir resets espurios durante operaciones lentas legítimas:
// 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();
}

Registros Scratch del Watchdog

El RP2350 proporciona ocho registros scratch de 32 bits en el bloque de hardware del watchdog que sobreviven a resets del watchdog pero se borran en reset de encendido. El firmware picoZ80 usa cinco de estos para mantener un historial completo de diagnóstico de arranque:
// 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)
En cada reset del watchdog, la etapa actual y la causa del reset se desplazan al FIFO de historial (scratch[0–3]) antes de ser sobrescritas. Esto le da los últimos cuatro intentos de reset, haciendo posible distinguir un fallo puntual de un fallo de arranque repetido en una etapa específica.

Referencia de Etapas de Arranque

Código Constante Descripción
0x01BOOTP_STARTPunto de entrada alcanzado
0x02BOOTP_CLK_SETReloj del sistema configurado (frecuencia CPU, frecuencia PSRAM, voltaje)
0x03BOOTP_PSRAM_INITInicialización de PSRAM iniciada
0x04BOOTP_PSRAM_OKPSRAM inicializada y probada
0x05BOOTP_STDIO_INITUSB stdio inicializado
0x06BOOTP_PIO_INITMáquinas de estado PIO cargadas e iniciadas
0x07BOOTP_Z80_INITContexto de CPU Z80 creado
0x08BOOTP_USB_INITPuente USB inicializado
0x0ABOOTP_ESP_HS_SYNCSincronización de handshake SPI del ESP32
0x0BBOOTP_CORE1_LAUNCHCore 1 lanzado mediante multicore_launch_core1()
0x0DBOOTP_FSPI_INITIPC binario FSPI inicializado (canales DMA reclamados)
0x0EBOOTP_ESP_INITCapa de comunicación ESP32 lista
0x10BOOTP_MAIN_LOOPBucle principal ingresado — arranque completo
0x11BOOTP_ML_POLL_USBBucle principal: sondeando USB
0x12BOOTP_ML_INTERCOREBucle principal: procesando comandos inter-core
0x20BOOTP_IC_DEQUEUEInter-core: desencolando solicitud
0x21BOOTP_IC_FD_LOADInter-core: cargando imagen de disco flexible
0x22BOOTP_IC_QD_LOADInter-core: cargando imagen de QuickDisk
0x23BOOTP_IC_RF_LOADInter-core: cargando imagen de RAMFILE
0x24–0x27BOOTP_IC_FILE_*Inter-core: carga/escritura/respuesta/finalización de archivo
Depurar un reset del watchdog: Conecte una sonda SWD, detenga el RP2350 y lea los registros scratch. Si scratch[5] == 0xB00710BE, los registros contienen datos válidos de progreso de arranque. Lea scratch[6] para el código de etapa. Por ejemplo, si scratch[6] == 0x0A (BOOTP_ESP_HS_SYNC), el firmware se colgó durante el handshake SPI del ESP32 — verifique que el ESP32 esté flasheado y ejecutándose, y compruebe el cableado SPI.

Shell de Depuración ICE (dbgsh.c)

El shell de depuración (dbgsh.c / dbgsh.h) implementa un depurador ICE de 49 comandos en USB CDC Channel 1. Se ejecuta en el Core 0 y se comunica con el Core 1 a través de flags compartidos en la estructura de contexto t_Z80CPU:
  • cpu->hold / cpu->holdAck — handshake de pausa/reanudación entre el shell del Core 0 y el bucle de emulación del Core 1.
  • cpu->dbgBpAddr[DBG_MAX_BP] — array de direcciones de breakpoint (8 ranuras, 0xFFFF = no utilizada). El Core 1 verifica antes de cada búsqueda de opcode.
  • cpu->dbgStepCount — contador de paso único. El Core 1 decrementa después de cada instrucción y se auto-detiene en cero.
  • cpu->dbgTrace[DBG_TRACE_SZ] — buffer circular de 512 entradas que registra [31:16]=PC, [15:8]=opcode, [7:0]=F para cada instrucción ejecutada.
Patrones de implementación clave: el shell usa dbg_sprintf() (un printf ligero residente en RAM) para evitar bloqueos de flash XIP al imprimir desde el Core 0 mientras el Core 1 está usando activamente la PSRAM. El acceso físico a memoria y E/S se realiza mediante Z80CPU_readPhysicalMem() / Z80CPU_writePhysicalMem() / Z80CPU_readPhysicalIO() / Z80CPU_writePhysicalIO(), que controlan ciclos de bus Z80 reales a través de las máquinas de estado PIO.
Sistema de hooks de depuración: Varios comandos (mmutrace, ipl) usan un mecanismo de callback por controlador — cada controlador persona puede registrar su propio handler de traza o reset, de modo que la salida del shell de depuración se adapta al contexto de máquina activo sin codificar lógica específica de máquina en dbgsh.c.

Manejadores de Fallos y Diagnósticos de PSRAM

El firmware instala manejadores de fallos Cortex-M33 que capturan una instantánea de diagnóstico completa en la PSRAM antes de permitir que el watchdog reinicie el sistema. Esto proporciona capacidades de análisis post-mortem sin requerir una sesión de depurador en vivo — esencial para diagnosticar fallos intermitentes en un sistema multi-core de tiempo real.

Estructura de Diagnóstico de Fallos en PSRAM

Los últimos 256 bytes de la PSRAM de 8MB (dirección 0x117FFF00) están reservados para diagnósticos de fallos. Cuando ocurre un fallo, el manejador guarda una instantánea completa de registros:
// 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;

Implementación del Manejador de Fallos

Cada tipo de fallo (hard fault, memory management fault, bus fault, usage fault) tiene un wrapper en ensamblador que extrae el Main Stack Pointer (MSP) o el Process Stack Pointer (PSP) — dependiendo de cuál estaba activo en el momento del fallo — y lo pasa a un manejador C común. El manejador C:
  1. Escribe la estructura de diagnóstico en la PSRAM en 0x117FFF00 con el marcador mágico apropiado y el tipo de fallo.
  2. Imprime el volcado de registros y los detalles del fallo mediante debugf() (si USB está disponible).
  3. Entra en un bucle infinito (while(1)), permitiendo que el watchdog active un reset.
En el siguiente arranque exitoso, el firmware verifica 0x117FFF00 en busca del marcador PSRAM_DIAG_MAGIC. Si está presente, vuelca la información de fallo guardada mediante debugf() y limpia el marcador, proporcionando una traza post-mortem completa.
// Interpreting fault diagnostics (from GDB or from debugf output):
//
// faultType=1 (Hard Fault):
//   Check HFSR bit 30 (FORCED) — indicates escalated fault.
//   Check CFSR for the original fault type.
//
// faultType=3 (Bus Fault):
//   Check CFSR bits [15:8] for bus fault status.
//   If BFARVALID (bit 15), BFAR contains the faulting address.
//   Common cause: PSRAM SPI contention between Core 0 and Core 1.
//
// faultType=4 (Usage Fault):
//   Check CFSR bits [25:16] for usage fault status.
//   UNDEFINSTR = undefined instruction (corrupted code in Flash/PSRAM).
//   DIVBYZERO  = division by zero (if enabled).
//
// PC value: the instruction that faulted.
// LR value: the return address (caller of the faulting function).

Mapa de Memoria de Diagnóstico de PSRAM

La parte superior de la PSRAM de 8MB está particionada en tres regiones de diagnóstico:
Rango de Direcciones Tamaño Contenido
0x117EF004 – 0x117FEFFF 64KB Buffer de salida de debugf() (puntero volátil en 0x117EF004)
0x117FF000 – 0x117FFEFF ~4KB Log persistente de arranque de plogf()
0x117FFF00 – 0x117FFFFF 256B Instantánea de diagnóstico de fallos
Las tres regiones sobreviven a resets del watchdog porque la PSRAM retiene su contenido mientras se mantenga la alimentación. En un reset de encendido, el contenido es indefinido y el firmware las reinicializa verificando los marcadores mágicos.

Protocolo IPC Binario (FSPI v1.1)

El RP2350 se comunica con el ESP32 a través de un enlace SPI de 4 hilos a 50MHz usando un protocolo IPC binario (versión 1.1). Este reemplaza un protocolo anterior basado en texto con un formato de trama binario estructurado que soporta verificación de integridad CRC32, transferencias de sectores en ráfaga, y canales DMA pre-asignados para latencia reducida y fiabilidad mejorada.
El firmware ESP32 soporta tres modos de red seleccionados en tiempo de compilación: Solo WiFi, WiFi+NCM (ambos simultáneamente), y Solo NCM. Se proporcionan archivos sdkconfig pre-construidos para cada modo (sdkconfig.mode_wifi_only, sdkconfig.mode_wifi_and_ncm, sdkconfig.mode_ncm_only). En modo NCM, el ESP32 presenta un adaptador Ethernet USB CDC-NCM con un servidor DHCP integrado (IP predeterminada: 192.168.7.1), habilitando acceso a la interfaz web sin hardware WiFi. El modo Solo NCM es requerido para placas enviadas sin certificación FCC/RED.

Estructura de Trama

Cada transacción IPC consiste en una cabecera fija de 64 bytes seguida de un payload opcional y un trailer CRC32 de 4 bytes:
// 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 Comando

Opcode Nombre Descripción
0x00 IPCF_CMD_NOP Sin operación (TX ficticio durante lectura full-duplex)
0x01 IPCF_CMD_RDS Leer un sector de 512 bytes
0x02 IPCF_CMD_WRS Escribir un sector de 512 bytes
0x03 IPCF_CMD_RBURST Lectura en ráfaga: 1–16 sectores en una transacción SPI
0x04 IPCF_CMD_WBURST Escritura en ráfaga: 1–16 sectores
0x05 IPCF_CMD_RFILE Leer archivo completo (fragmentado si excede el payload máximo)
0x06 IPCF_CMD_WFILE Escribir archivo completo
0x07 IPCF_CMD_INF Transferir información de versión/partición del RP2350 al ESP32
0x08 IPCF_CMD_RFD Leer archivo de imagen de disco flexible
0x09 IPCF_CMD_RQD Leer archivo de imagen QuickDisk
0x0A IPCF_CMD_RRF Leer imagen de respaldo RAMFILE

DMA e Integridad

  • Canales DMA pre-asignados: Los canales DMA TX y RX (gDmaTx, gDmaRx) se reclaman una vez en la inicialización de FSPI y nunca se liberan. Esto elimina la sobrecarga de reclamar/liberar por transferencia y previene condiciones de carrera por agotamiento de canales DMA que pueden ocurrir cuando el Core 1 contende por el bus QMI.
  • Elevación de prioridad RX: El canal DMA RX se configura con PRIORIDAD ALTA para prevenir desbordamiento del FIFO SPI RX. Los accesos a PSRAM del Core 1 a través del bus QMI pueden detener el fabric del bus AHB, y si el canal DMA RX está a prioridad normal sus transferencias pueden retrasarse lo suficiente para que el FIFO SPI se desborde.
  • Integridad CRC32: Cada trama está protegida por un CRC32 estándar IEEE 802.3 (polinomio 0xEDB88320, reflejado). El ESP32 usa esp_rom_crc32_le() que produce el mismo resultado. En caso de discrepancia de CRC, la trama se reintenta, usando el contador de secuencia (seqNum) para detección de duplicados.
  • Integración con watchdog: Las esperas de DMA incluyen un timeout de 2 segundos con actualizaciones del watchdog cada segundo. Si una transferencia DMA se cuelga (por ejemplo, debido a un reset del ESP32), los canales se abortan, CS se libera y el arranque continúa.

Sitios de Referencia

Recurso Enlace
Página del proyecto picoZ80 /picoz80/
Manual de Usuario de picoZ80 /picoz80-usermanual/
Guía Técnica de picoZ80 /picoz80-technicalguide/
Página del proyecto pico6502 /pico6502/
Hoja de datos del RP2350 datasheets.raspberrypi.com
API Multicore del Pico SDK raspberrypi.github.io/pico-sdk-doxygen
Biblioteca Zeta Z80 github.com/superzazu/z80
Manual de Usuario de CPU Zilog Z80 zilog.com
Biblioteca cJSON github.com/DaveGamble/cJSON

Aviso Regulatorio de Radiofrecuencia

Este dispositivo incorpora un módulo inalámbrico ESP32-S3-PICO-1 que transmite en la banda ISM de 2.4 GHz, convirtiéndolo en un radiador intencional bajo las regulaciones de radiofrecuencia a nivel mundial (incluyendo FCC Part 15 Subpart C en los Estados Unidos, y la Directiva de Equipos de Radio 2014/53/EU en la Unión Europea).
Aunque el módulo ESP32-S3-PICO-1 por sí mismo posee certificaciones regulatorias preexistentes (FCC, CE y otras), esas certificaciones a nivel de módulo no se extienden automáticamente a un producto terminado que incorpora el módulo. La exención de módulo pre-certificado permite a hobbyistas individuales construir un número limitado de dispositivos para uso personal, experimental o educativo sin obtener una autorización de equipo separada.
Limitaciones Importantes
  • Los dispositivos ensamblados no deben venderse, ofrecerse para la venta, regalarse ni distribuirse de ninguna otra manera a terceros a menos que el producto terminado haya sido probado independientemente y se le haya otorgado su propia autorización de equipo (por ejemplo, FCC ID, marcado CE con evaluación de un Organismo Notificado) en la jurisdicción correspondiente.
  • Construir este proyecto para uso personal en cantidades limitadas generalmente está permitido bajo las disposiciones de uso hobbyista y experimental (por ejemplo, FCC § 15.23), siempre que el dispositivo no cause interferencia perjudicial.
  • Los requisitos regulatorios varían según el país. Los constructores fuera de los Estados Unidos deben consultar su autoridad nacional de radiofrecuencia para conocer las reglas aplicables.
Responsabilidad del Constructor
Es responsabilidad exclusiva del constructor asegurar que cualquier dispositivo construido a partir de estos diseños cumpla con todas las regulaciones de radiofrecuencia aplicables en su jurisdicción. El autor proporciona estos diseños para uso personal, educativo y de hobbyista y no declara que un dispositivo construido a partir de ellos satisfaga los requisitos regulatorios para distribución comercial.