picoZ80 Guía del Desarrollador
Guía del Desarrollador de picoZ80
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
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
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.
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);
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.
z80_data) para un ciclo de lectura:
- Establece IRQ 1 y espera — "listo para dirección/datos".
- El Core 1 limpia IRQ 1 después de introducir la dirección de pines (modo entrada) y un byte de datos ficticio.
- La SM configura las direcciones de pines a entrada (tristate), permitiendo que la memoria del host controle D0–D7.
- La SM espera en
wait 0 irq 0hasta que el siguiente cambio de dirección indica que el ciclo está terminando. - La SM restaura las direcciones de pines a un estado conocido.
Tipos y Estructuras de Datos Clave
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
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
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
memioPtrinstalada 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
memioPtrinstalado, 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
_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)
// 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)
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;
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)
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;
- 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 quecpu->_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
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);
- 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). - read —
truesi este es un ciclo de lectura (el Z80 está leyendo);falsesi 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).
- 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
memioPtren 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.
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
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
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;
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.
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
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);
}
}
}
- 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 aZ80CPU_readMem(),Z80CPU_writeMem(),Z80CPU_readIO()yZ80CPU_writeIO()para cada transacción de bus.
Despacho de Lectura de Memoria
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);
}
}
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
- Controladores de nivel superior (también llamados personas) — registrados en
virtualFuncMap[]enZ80CPU.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
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]);
// 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
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.
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.
{"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(...).
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) |
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.
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 (0x0000–0x003F → bloque de vectores, 0x0040–0x01FF → el inicio del TPA).
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
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)
"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
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 |
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
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)
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 comocpu->_Z80.inta, el callback de reconocimiento de interrupción.MZ800_retiHandler()— instalado comocpu->_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
- 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
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]);
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
// src/drivers/Sharp/MZ700.c (abbreviated)
typedef struct {
bool loDRAMen; // true = lower 4KB (0x0000-0x0FFF) is mapped to RAM bank 1
bool hiDRAMen; // true = upper region (0xD000-0xFFFF) is mapped to RAM bank 1
bool inhibit; // true = upper bank-swap is inhibited (INHIBIT port written)
// Saved membankPtr values for the upper region when hi DRAM is swapped in
// (needed to restore the original mapping when hi DRAM is swapped back out)
uint32_t upmembankPtr[MZ700_UPPERMEM_BLOCKS];
} t_MZ700Ctrl;
static t_MZ700Ctrl MZ700Ctrl = {
.loDRAMen = false,
.hiDRAMen = false,
.inhibit = false,
};
t_drvConfig.
La 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 porZ80CPU_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()
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
}
_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()
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;
}
Escribir un Nuevo Controlador — Paso a Paso
Paso 1 — Crear los Archivos Fuente
// 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
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
)
target_include_directories.
Paso 3 — Incluir la Cabecera en Z80CPU.c
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
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},
};
"mydriver", "MyDriver" y "MYDRIVER" en el JSON coincidirán con esta entrada.
Paso 5 — Añadir el Controlador a config.json
"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
BaseZ80(pZ80-BaseZ80) — binario universal con todos los controladores (Sharp + Amstrad + Tatung). DefineINCLUDE_SHARP_DRIVERS,INCLUDE_AMSTRAD_DRIVERSeINCLUDE_TATUNG_DRIVERS.SharpZ80(pZ80-SharpZ80) — solo controladores Sharp MZ. DefineINCLUDE_SHARP_DRIVERS. Produce un binario de firmware más pequeño.AmstradZ80(pZ80-AmstradZ80) — solo controladores Amstrad PCW. DefineINCLUDE_AMSTRAD_DRIVERSyTARGET_MODEL_AMSTRAD. Produce un binario de firmware más pequeño.TatungZ80(pZ80-TatungZ80) — solo controladores Tatung Einstein. DefineINCLUDE_TATUNG_DRIVERSyTARGET_MODEL_TATUNG. Produce un binario de firmware más pequeño.
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
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
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.
idf.py menuconfig) o los archivos sdkconfig pre-construidos listados arriba.
Patrones de Hooks de Memoria en Detalle
Patrón 1 — Dispositivo Virtual Puro (bloque FUNC)
// Map 0xC000-0xCFFF as a pure virtual device in bank 4
int startBlock = 0xC000 / MEMORY_BLOCK_SIZE; // = 96
int endBlock = 0xD000 / MEMORY_BLOCK_SIZE; // = 104
for(int idx = startBlock; idx < endBlock; idx++)
{
cpu->_membankPtr[idx] = (MEMBANK_TYPE_FUNC << 24)
| (4 << 16)
| (idx * MEMORY_BLOCK_SIZE);
}
// Install handler for every address in range
for(uint32_t addr = 0xC000; addr < 0xD000; addr++)
cpu->_z80PSRAM->memioPtr[addr] = (MemoryFunc)MyVirtualDevice_Handler;
// Handler:
uint8_t MyVirtualDevice_Handler(Z80CPU *cpu, bool read, uint16_t addr, uint8_t data)
{
uint16_t reg = addr - 0xC000; // Register offset within the device
if(read)
{
switch(reg)
{
case 0x00: return myDevice.statusReg;
case 0x01: return myDevice.dataReg;
default: return 0xFF;
}
}
else
{
switch(reg)
{
case 0x01: myDevice.dataReg = data; break;
case 0x02: myDevice.controlReg = data; break;
}
return data;
}
}
Patrón 2 — Interceptar Escrituras en una Región RAM
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
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)
// 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
// 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
Funciones Requeridas para una Sub-Interfaz
// 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
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},
};
"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
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)
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 globalg_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},
"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
Uso de la Cola Intercore
- 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).
- El handler establece un flag de estado (por ejemplo,
diskState.pendingRead = true) y retorna inmediatamente — no realiza la E/S. - 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 encpu->requestQueue. - 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. - Su
task_ptres 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. Useplogf()en lugar dedebugf()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[]oioPtr[]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
debugfenZ80CPU_getVirtualFunc()si su controlador no se está inicializando —debugfes 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
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");
- 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
0x117EF004debe 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
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) | Sí | Contenido del buffer sí; el puntero debe revalidarse | No — bloqueará | Salida de depuración general del Core 0 |
plogf() |
PSRAM (4KB al final) | No | Sí | 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
Timer Watchdog
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
// src/model/BaseZ80/main.c
// Scratch register allocation
#define BOOTP_SCR_MAGIC 5 // Magic marker: 0xB00710BE
#define BOOTP_SCR_STAGE 6 // Current boot stage code
#define BOOTP_SCR_RSTCAUS 7 // Reset cause from hardware
// scratch[0-3] = boot attempt history (FIFO, most recent in [3])
// scratch[4] = SPI diagnostic counters (packed bitfield)
#define BOOTP_MAGIC 0xB00710BE // "BOOT-PROBE" marker
// Inline function to record current boot stage
static inline void bootStage(uint32_t stage)
{
watchdog_hw->scratch[BOOTP_SCR_STAGE] = stage;
}
// Usage throughout boot sequence:
bootStage(BOOTP_START); // 0x01 — entry point
// ... clock setup ...
bootStage(BOOTP_CLK_SET); // 0x02
// ... PSRAM init ...
bootStage(BOOTP_PSRAM_INIT); // 0x03
watchdog_update();
bootStage(BOOTP_PSRAM_OK); // 0x04
// ... and so on through BOOTP_MAIN_LOOP (0x10)
scratch[0–3]) 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 |
|---|---|---|
0x01 | BOOTP_START | Punto de entrada alcanzado |
0x02 | BOOTP_CLK_SET | Reloj del sistema configurado (frecuencia CPU, frecuencia PSRAM, voltaje) |
0x03 | BOOTP_PSRAM_INIT | Inicialización de PSRAM iniciada |
0x04 | BOOTP_PSRAM_OK | PSRAM inicializada y probada |
0x05 | BOOTP_STDIO_INIT | USB stdio inicializado |
0x06 | BOOTP_PIO_INIT | Máquinas de estado PIO cargadas e iniciadas |
0x07 | BOOTP_Z80_INIT | Contexto de CPU Z80 creado |
0x08 | BOOTP_USB_INIT | Puente USB inicializado |
0x0A | BOOTP_ESP_HS_SYNC | Sincronización de handshake SPI del ESP32 |
0x0B | BOOTP_CORE1_LAUNCH | Core 1 lanzado mediante multicore_launch_core1() |
0x0D | BOOTP_FSPI_INIT | IPC binario FSPI inicializado (canales DMA reclamados) |
0x0E | BOOTP_ESP_INIT | Capa de comunicación ESP32 lista |
0x10 | BOOTP_MAIN_LOOP | Bucle principal ingresado — arranque completo |
0x11 | BOOTP_ML_POLL_USB | Bucle principal: sondeando USB |
0x12 | BOOTP_ML_INTERCORE | Bucle principal: procesando comandos inter-core |
0x20 | BOOTP_IC_DEQUEUE | Inter-core: desencolando solicitud |
0x21 | BOOTP_IC_FD_LOAD | Inter-core: cargando imagen de disco flexible |
0x22 | BOOTP_IC_QD_LOAD | Inter-core: cargando imagen de QuickDisk |
0x23 | BOOTP_IC_RF_LOAD | Inter-core: cargando imagen de RAMFILE |
0x24–0x27 | BOOTP_IC_FILE_* | Inter-core: carga/escritura/respuesta/finalización de archivo |
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)
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]=Fpara cada instrucción ejecutada.
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
Estructura de Diagnóstico de Fallos en PSRAM
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
- Escribe la estructura de diagnóstico en la PSRAM en
0x117FFF00con el marcador mágico apropiado y el tipo de fallo. - Imprime el volcado de registros y los detalles del fallo mediante
debugf()(si USB está disponible). - Entra en un bucle infinito (
while(1)), permitiendo que el watchdog active un reset.
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
| 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 |
Protocolo IPC Binario (FSPI v1.1)
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
// 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 usaesp_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
- 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.
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.