picoZ80 Guía del Desarrollador

Guía del Desarrollador de picoZ80

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

Configuración del Entorno de Desarrollo

Antes de trabajar con el código fuente de picoZ80, necesita configurar el entorno de compilación — compilador cruzado, Pico SDK y herramientas de apoyo. Se proporcionan dos scripts de configuración que automatizan todo este proceso: uno para Linux y macOS, el otro para Windows. Cada script comprueba (y opcionalmente instala) todos los paquetes necesarios, clona los repositorios, configura los scripts de compilación y crea un archivo de entorno que usted carga (source) en sesiones futuras.

Linux / macOS

Descargue y ejecute el script de configuración desde cualquier directorio. Le pedirá una ubicación de instalación (por defecto: ~/pico).
# Download the setup script
curl -O https://git.eaw.app/eaw/pico/raw/branch/master/setup_picoZ80.sh
chmod +x setup_picoZ80.sh

# Run it
./setup_picoZ80.sh
El script realiza los siguientes pasos:
  1. Detecta su SO — soporta Debian/Ubuntu, Fedora/RHEL, Arch/Manjaro y macOS (Homebrew).
  2. Instala los paquetes necesarios — cmake, make, git, python3, perl, rsync, arm-none-eabi-gcc (compilador cruzado ARM), arm-none-eabi-g++ y libnewlib. En macOS, la cadena de herramientas ARM se instala mediante brew install --cask gcc-arm-embedded.
  3. Clona los repositorios — el proyecto picoZ80 desde git.eaw.app, el pico-sdk de Raspberry Pi (rama develop), pico-examples y pico-extras. Se inicializan todos los submódulos.
  4. Configura los scripts de compilación — reescribe las rutas codificadas en build_tzpuPico.sh y get_and_build_sdk.sh para que coincidan con su ubicación de instalación. Instala la cabecera de placa de picoZ80 en el SDK y copia los archivos de importación de CMake necesarios en el proyecto.
  5. Configura Docker para las compilaciones ESP32 (opcional) — detecta Docker, lo arranca si es necesario (incluido colima en macOS) y ofrece descargar la imagen espressif/idf:release-v5.4 (~4 GB). Docker solo es necesario para compilar el firmware del coprocesador ESP32; el firmware del RP2350 se compila sin él.
  6. Crea ~/picoZ80_env.sh — un archivo de entorno de shell que establece PICO_PATH, PICO_SDK_PATH y añade el script de compilación a su PATH. También define la función auxiliar idf54, que ejecuta de forma transparente los comandos de ESP-IDF mediante Docker (preferido) o una instalación nativa de ESP-IDF (alternativa).
  7. Verifica el entorno — comprueba todas las rutas y herramientas críticas e informa de cualquier problema.
Tras la configuración, cargue el entorno y compile:
# Add this to your ~/.bashrc or ~/.zshrc for automatic loading:
source ~/picoZ80_env.sh

# Build all RP2350 targets
build_tzpuPico.sh

# Build a specific target family
build_tzpuPico.sh sharp              # Sharp MZ targets only
build_tzpuPico.sh amstrad            # Amstrad PCW targets only
build_tzpuPico.sh tatung             # Tatung Einstein targets only
build_tzpuPico.sh open               # OpenZ80 experimenter persona only
build_tzpuPico.sh base               # BaseZ80 (all drivers) only

# Build with debug symbols
build_tzpuPico.sh DEBUG

# Build everything including ESP32 firmware
build_tzpuPico.sh ALL

# Build ESP32 firmware only
build_esp32
Si ya tiene el código fuente de picoZ80 descargado y solo desea reconfigurar las rutas (por ejemplo, tras mover el directorio), vuelva a ejecutar el script de configuración y apúntelo a la instalación existente — detectará el checkout y omitirá el paso de clonado.

Windows 10 / 11

Descargue y ejecute el script de configuración de PowerShell desde un símbolo de PowerShell como Administrador. Le pedirá una ubicación de instalación (por defecto: %USERPROFILE%\pico).
# Download the setup script
Invoke-WebRequest -Uri "https://git.eaw.app/eaw/pico/raw/branch/master/setup_picoZ80_windows.ps1" -OutFile "setup_picoZ80_windows.ps1"

# Allow execution and run (requires Administrator)
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
.\setup_picoZ80_windows.ps1
El script de Windows realiza pasos similares adaptados al ecosistema Windows:
  1. Comprueba los privilegios de Administrador — la instalación de paquetes mediante winget requiere elevación.
  2. Instala winget si no está presente — lo descarga e instala desde GitHub (con las dependencias VCLibs y UI.Xaml). Si winget no puede instalarse, se proporcionan URLs de descarga manual para cada herramienta.
  3. Instala las herramientas necesarias mediante winget — Git, CMake, Ninja, Python 3, Strawberry Perl y la ARM GNU Toolchain. Cada herramienta que no pueda instalarse automáticamente se lista con su URL de descarga y notas de instalación.
  4. Clona los repositorios — igual que el script de Linux/macOS: proyecto picoZ80, Pico SDK, Pico Examples y Pico Extras con submódulos.
  5. Crea build_tzpuPico.bat — un archivo por lotes de Windows preconfigurado con sus rutas de instalación. Usa CMake + Ninja para la compilación del RP2350 y soporta los mismos filtros de objetivo que el script de Linux. Para las compilaciones ESP32, prueba primero Docker y luego recurre a una instalación nativa de ESP-IDF (detectando automáticamente el instalador de Espressif para Windows en C:\Espressif\frameworks\ o %USERPROFILE%\esp\).
  6. Entorno de compilación ESP32 (opcional) — ofrece tres opciones: Docker Desktop (instalado mediante winget), el instalador sin conexión de Espressif (descargado mediante el navegador — ~1.5 GB) u omitir. Si ya hay instalado un ESP-IDF v5.4 nativo, este paso se omite automáticamente.
  7. Crea picoZ80_env.bat — establece PICO_PATH, PICO_SDK_PATH, PICO_EXTRAS_PATH y añade el directorio de instalación al PATH. Opcionalmente los añade al PATH del sistema de forma permanente.
  8. Verifica el entorno — comprueba todas las rutas y herramientas críticas.
Tras la configuración, abra un nuevo símbolo del sistema y compile:
REM Set up the environment (or add to system PATH during setup)
picoZ80_env.bat

REM Build all RP2350 targets
build_tzpuPico.bat

REM Build a specific target family
build_tzpuPico.bat sharp
build_tzpuPico.bat amstrad

REM Build with debug symbols
build_tzpuPico.bat debug

REM Build RP2350 + ESP32 (requires Docker or native ESP-IDF)
build_tzpuPico.bat ALL

REM Build ESP32 firmware only
build_tzpuPico.bat esp32
Importante: no ejecute build_tzpuPico.bat desde un símbolo de PowerShell/CMD de ESP-IDF. La cadena de herramientas ESP-IDF anula cmake con su propio esp-cmake, que no puede compilar las herramientas anfitrión del RP2350. El archivo por lotes detecta esto y sale con un mensaje de error claro. La activación de ESP-IDF se gestiona automáticamente en el paso de compilación del ESP32 cuando es necesario.

Resumen de Requisitos Previos

La tabla siguiente resume todas las herramientas que requiere el sistema de compilación. Los scripts de configuración las instalan automáticamente cuando es posible.
Herramienta Propósito Linux macOS Windows
CMake ≥ 3.13Generador del sistema de compilaciónapt / dnf / pacmanbrewwinget (Kitware.CMake)
NinjaEjecutor de compilación (Windows)make (por defecto)make (por defecto)winget (Ninja-build.Ninja)
ARM GNU ToolchainCompilador cruzado para RP2350gcc-arm-none-eabibrew cask gcc-arm-embeddedwinget (Arm.GnuArmEmbeddedToolchain)
GitControl de versionesapt / dnf / pacmanbrewwinget (Git.Git)
Python 3Scripts del SDK, ESP-IDFapt / dnf / pacmanbrewwinget (Python.Python.3.12)
PerlScripts de versiónapt / dnf / pacmanbrewwinget (StrawberryPerl)
Docker (opcional)Compilaciones ESP32 mediante contenedorget.docker.comDocker Desktop / colimaDocker Desktop
ESP-IDF v5.4 (opcional)Compilaciones ESP32 (alternativa nativa a Docker)espressif.comespressif.comInstalador sin conexión de Espressif

Estructura del Código

Todo el código fuente reside bajo projects/tzpuPico/ dentro de la raíz del repositorio (la ubicación exacta de checkout dependerá de su sistema). El diseño a continuación muestra los archivos relevantes para el desarrollo de controladores:
tzpuPico/
├── CMakeLists.txt                          Top-level build file
├── src/
│   ├── CMakeLists.txt                      Source-level build file — add new driver files here
│   ├── Z80CPU.c                            Main Z80 emulation, bus dispatch, driver framework
│   ├── Z80CPU.h                            (legacy — included via include/)
│   ├── M6502CPU.c                          6502 parallel (same architecture)
│   ├── FSPI.c / FSPI.h                     Flash SPI interface
│   ├── ESP.c / ESP.h                       ESP32 communication layer
│   ├── psram.c / psram.h                   PSRAM allocation and management
│   ├── cJSON.c / cJSON.h                   JSON parser for config.json
│   ├── include/
│   │   ├── Z80CPU.h                        *** KEY FILE: all type definitions and macros ***
│   │   ├── dbgsh.h                         Debug shell (ICE debugger) header
│   │   └── drivers/
│   │       ├── Z80SIO.h                    Zilog Z80 SIO/2 emulation header (shared)
│   │       ├── Sharp/
│   │       │   ├── MZ.h                    MZ-series common constants
│   │       │   ├── MZ700.h                 MZ-700 driver header
│   │       │   ├── MZ80A.h                 MZ-80A driver header
│   │       │   ├── MZ2000.h                MZ-2000 driver header
│   │       │   ├── MZ2200.h                MZ-2200 driver header
│   │       │   ├── MZ80B.h                 MZ-80B driver header
│   │       │   ├── MZ2500.h                MZ-2500 driver header
│   │       │   ├── MZ800.h                 MZ-800 driver header
│   │       │   ├── MZ1500.h                MZ-1500 driver header
│   │       │   ├── MZ8BIO3.h               MZ-8BIO3 RS-232C card (Z80 SIO) header
│   │       │   ├── MZ1E24.h                MZ-1E24 RS-232C card (Z80 SIO) header
│   │       │   ├── RFS.h / TZFS.h          Filing system headers
│   │       │   ├── WD1773.h                Floppy controller header
│   │       │   ├── QDDrive.h               QuickDisk header
│   │       │   ├── MZ-1R23.h               MZ-1R23 Kanji ROM header
│   │       │   ├── MZ-1R37.h               MZ-1R37 EMM header
│   │       │   ├── PIO-3034.h              PIO-3034 EMM header
│   │       │   ├── Celestite.h             Celestite composite board header
│   │       │   ├── SASI.h                  SASI hard disk protocol header
│   │       │   ├── MZ1E30.h               MZ-1E30 SASI controller header
│   │       │   └── MZ8BFI.h                MZ-8BFI floppy interface header
│   │       ├── Amstrad/
│   │       │   ├── PCW9512.h               Amstrad PCW-9512 driver header
│   │       │   └── uPD765.h                NEC uPD765 FDC header
│   │       └── Tatung/
│   │           ├── EinsteinTC01.h           Tatung Einstein TC-01 driver header
│   │           ├── EinsteinFDC.h            Einstein FDC sub-interface header
│   │           └── WD1770.h                 WD1770 FDC header
│   ├── dbgsh.c                             Debug shell (ICE debugger) — USB CDC Channel 1
│   ├── PIT8253.c                           Intel 8253 PIT emulation module
│   ├── PPI8255.c                           Intel 8255 PPI emulation module
│   ├── drivers/
│   │   ├── Z80SIO.c                        Zilog Z80 SIO/2 emulation (shared by RS-232C cards)
│   │   └── Sharp/
│   │       ├── MZ700.c                     *** EXAMPLE DRIVER ***
│   │       ├── MZ80A.c                     MZ-80A persona driver
│   │       ├── MZ2000.c                    MZ-2000 persona driver
│   │       ├── MZ2200.c                    MZ-2200 persona driver
│   │       ├── MZ80B.c                     MZ-80B persona driver
│   │       ├── MZ2500.c                    MZ-2500 persona driver
│   │       ├── MZ800.c                     MZ-800 persona driver (dual-mode MZ-700/MZ-800)
│   │       ├── MZ1500.c                    MZ-1500 persona driver
│   │       ├── MZ8BIO3.c                   MZ-8BIO3 RS-232C card (shared SIOCard_* impl)
│   │       ├── MZ1E24.c                    MZ-1E24 RS-232C card (SIOCard_* wrapper)
│   │       ├── RFS.c                       ROM Filing System driver
│   │       ├── TZFS.c                      TranZPUter Filing System driver
│   │       ├── WD1773.c                    WD1773 floppy controller driver
│   │       ├── QDDrive.c                   QuickDisk drive driver
│   │       ├── MZ-1E05.c / MZ-1E14.c / MZ-1E19.c   Peripheral interface cards
│   │       ├── MZ8BFI.c                    MZ-8BFI / E0054PA floppy interface (MZ-2000)
│   │       ├── MZ-1R12.c / MZ-1R18.c      RAM expansion cards
│   │       ├── MZ-1R23.c                   MZ-1R23 Kanji ROM / MZ-1R24 Dictionary ROM
│   │       ├── MZ-1R37.c                   MZ-1R37 640KB EMM
│   │       ├── PIO-3034.c                  IO DATA PIO-3034 320KB EMM
│   │       ├── Celestite.c                 Celestite LAN / Memory composite board
│   │       ├── SASI.c                      SASI hard disk protocol driver
│   │       └── MZ1E30.c                    MZ-1E30 SASI hard disk controller
│   ├── drivers/
│   │   ├── Amstrad/
│   │   │   ├── PCW9512.c                   *** AMSTRAD PCW-9512 DRIVER ***
│   │   │   └── uPD765.c                    NEC uPD765 FDC driver (CPC DSK format)
│   │   └── Tatung/
│   │       ├── EinsteinTC01.c              Tatung Einstein TC-01 persona driver
│   │       ├── EinsteinFDC.c               Einstein FDC sub-interface (2 drives, DSK/D88)
│   │       └── WD1770.c                    WD1770 FDC emulation (reusable module, separate from WD1773.c)
│   │   └── Other/
│   │       └── Open.c                      OpenZ80 vanilla / experimenter persona driver
│   └── model/
│       ├── BaseZ80/                        All drivers (Sharp + Amstrad)
│       │   ├── CMakeLists.txt              Per-model build targets
│       │   ├── main.c                      Entry point (Core 0 + Core 1 launch)
│       │   ├── main_memmap_partition_1.ld  Linker script for Slot 1
│       │   └── main_memmap_partition_2.ld  Linker script for Slot 2
│       ├── SharpZ80/                       Sharp MZ drivers only (smaller binary)
│       │   ├── CMakeLists.txt
│       │   ├── main.c
│       │   ├── main_memmap_partition_1.ld
│       │   └── main_memmap_partition_2.ld
│       ├── AmstradZ80/                     Amstrad PCW drivers only (smaller binary)
│       │   ├── CMakeLists.txt
│       │   ├── main.c
│       │   ├── main_memmap_partition_1.ld
│       │   └── main_memmap_partition_2.ld
│       ├── TatungZ80/                      Tatung Einstein drivers only (smaller binary)
│       │   ├── CMakeLists.txt
│       │   ├── main.c
│       │   ├── main_memmap_partition_1.ld
│       │   └── main_memmap_partition_2.ld
│       ├── OpenZ80/                         OpenZ80 experimenter persona only (machine-agnostic cards)
│       │   ├── CMakeLists.txt
│       │   ├── main.c
│       │   ├── main_memmap_partition_1.ld
│       │   └── main_memmap_partition_2.ld
│       └── Bootloader/
└── tools/
    └── NetFileServer/
        └── netfs.py                       Network file server for MZF files over TCP

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

La interfaz de bus del Z80 se ejecuta completamente en hardware PIO (z80.pio), pero el código C en el Core 1 orquesta qué ciclo de bus se ejecuta y cuándo. Comprender esta interacción es importante para cualquier persona que depure temporización de bus o añada nuevos tipos de ciclo.
El mecanismo central es out exec, 16 — una instrucción PIO que extrae un valor de 16 bits del TX FIFO y lo ejecuta como una instrucción PIO. La máquina de estado z80_cycle (PIO 0 SM 2) utiliza esto en un bucle ajustado:
// z80_cycle SM program (PIO 0 SM 2):
//
// start_cycle:
//     wait 0 irq 6          ; Stall if BUSREQ active
//     irq set 0             ; Signal "ready"
//     wait 0 irq 0          ; Wait for C code to load address and clear IRQ 0
//     wait 1 gpio CLK       ; Sync to T1 rising edge
// cycle_exec:
//     out exec, 16           ; Pull instruction from FIFO, execute it
//     jmp cycle_exec         ; Repeat
//
// The C code pushes a sequence of encoded 16-bit PIO instructions
// into the cycle SM's TX FIFO. The sequence controls every aspect
// of the bus cycle: which control signals to assert/deassert, when
// to wait for clock edges, and when to sample data.
// The final instruction in every sequence is a JMP back to start_cycle.
El código C pre-codifica las secuencias de instrucciones para cada tipo de ciclo en tiempo de compilación. Estas se almacenan como arrays de valores uint16_t. En tiempo de ejecución, el bucle principal del Core 1 introduce la secuencia apropiada en el FIFO según la transacción de bus actual:
// Simplified Core 1 flow for a memory read cycle:

// 1. z80_cycle SM sets IRQ 0 — it is ready for a new cycle.
//    z80_addr SM sets IRQ 0 — it is ready for an address.

// 2. Core 1 resolves the address and prepares the bus transaction:
pio_sm_put(pio0, SM_ADDR, (pindirs_16 << 16) | address);  // Push to z80_addr
pio_interrupt_clear(pio0, 0);                                // Clear IRQ 0 → addr SM runs

// 3. Core 1 pushes the cycle-type instruction sequence:
pio_sm_put(pio0, SM_CYCLE, encoded_wait_clk_low);   // wait 0 gpio CLK
pio_sm_put(pio0, SM_CYCLE, encoded_set_mreq_rd);    // set pins: /MREQ low, /RD low
pio_sm_put(pio0, SM_CYCLE, encoded_wait_clk_high);  // wait 1 gpio CLK (T2)
pio_sm_put(pio0, SM_CYCLE, encoded_wait_clk_low);   // wait 0 gpio CLK (T2)
// ... wait state check, T3, data sample ...
pio_sm_put(pio0, SM_CYCLE, encoded_jmp_start);       // JMP start_cycle (end)

// 4. Core 1 reads the data byte from the RX FIFO:
uint8_t data = pio_sm_get(pio0, SM_DATA);
Coordinación entre máquinas de estado: La SM de dirección (z80_addr) y la SM de datos (z80_data) utilizan cada una su propio flag IRQ (IRQ 0 e IRQ 1 respectivamente) para implementar un handshake productor/consumidor:
  • La SM establece su flag IRQ y se detiene en wait 0 irq N — "estoy lista, envíame datos".
  • El Core 1 introduce la dirección o los datos en el TX FIFO de la SM, y luego limpia el flag IRQ.
  • La SM se despierta, extrae los datos del FIFO y activa los pines.
Este handshake asegura que las señales del bus nunca se activen antes de que se carguen los valores correctos, y que el Core 1 nunca sobrescriba entradas del FIFO que la SM aún no ha consumido.
Flujo de la SM de datos (z80_data) para un ciclo de lectura:
  1. Establece IRQ 1 y espera — "listo para dirección/datos".
  2. El Core 1 limpia IRQ 1 después de introducir la dirección de pines (modo entrada) y un byte de datos ficticio.
  3. La SM configura las direcciones de pines a entrada (tristate), permitiendo que la memoria del host controle D0–D7.
  4. La SM espera en wait 0 irq 0 hasta que el siguiente cambio de dirección indica que el ciclo está terminando.
  5. La SM restaura las direcciones de pines a un estado conocido.
Para un ciclo de escritura, el Core 1 introduce las direcciones de pines de salida y el byte de datos real; la SM controla D0–D7 con los datos.

Tipos y Estructuras de Datos Clave

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

Constantes de Tipo de Bloque de Memoria

Cada bloque de 512 bytes en el espacio de direcciones de 64KB del Z80 tiene un tipo codificado en los 8 bits superiores de su entrada membankPtr. El tipo le indica al bucle de despacho del Core 1 cómo manejar las transacciones de bus que caen dentro de ese bloque.
// src/include/Z80CPU.h

#define MEMBANK_TYPE_UNKNOWN        0x00  // Uninitialised — should never appear at runtime
#define MEMBANK_TYPE_PHYSICAL       0x01  // Pass-through: RP2350 releases bus, host hardware responds
#define MEMBANK_TYPE_PHYSICAL_VRAM  0x02  // Pass-through with wait states for host video RAM
#define MEMBANK_TYPE_PHYSICAL_HW    0x04  // Pass-through for host I/O-mapped hardware registers
#define MEMBANK_TYPE_RAM            0x08  // Read/write — backed by PSRAM bank
#define MEMBANK_TYPE_VRAM           0x10  // PSRAM video RAM — writes also mirrored to physical VRAM
#define MEMBANK_TYPE_ROM            0x20  // Read-only — backed by PSRAM bank; writes silently ignored
#define MEMBANK_TYPE_FUNC           0x40  // Virtual device — every access calls a C function handler
#define MEMBANK_TYPE_PTR            0x80  // Indirection — each byte redirects to another address
El tipo determina lo que sucede en Z80CPU_readMem() y Z80CPU_writeMem():
  • PHYSICAL / PHYSICAL_VRAM / PHYSICAL_HW — el RP2350 no intercepta la transacción de bus; el hardware real en la placa host responde. Utilice esto para cualquier región donde los propios chips del host (ROM, RAM, hardware de video) deben mantener el control.
  • RAM — las lecturas y escrituras van a un banco de 64KB en la PSRAM de 8MB. Si hay una función memioPtr instalada para la dirección específica, esa función se llama en lugar de (para FUNC) o junto con el acceso a PSRAM (los handlers pueden interceptar o post-procesar). Los wait states y la sincronización T1 se pueden configurar por bloque.
  • ROM — las lecturas provienen de la PSRAM (típicamente cargada desde un archivo de imagen al arranque). Los ciclos de escritura aún llegan a cualquier handler memioPtr instalado, pero la PSRAM no se modifica — útil para registros de banking que residen en una región de dirección mapeada como ROM.
  • VRAM — las lecturas provienen de la PSRAM; las escrituras van tanto a la PSRAM como a la VRAM física del host en paralelo. Esto permite que el software mantenga una copia sombra del buffer de video mientras también actualiza la pantalla real.
  • FUNC — no hay respaldo de PSRAM. Cada lectura y escritura llama a la función instalada en memioPtr[addr]. Utilice esto para registros de hardware virtualizados, puertos de control de banking mapeados en espacio de memoria, y cualquier recurso sin RAM real detrás.
  • PTR — cada byte del bloque de 512 bytes puede apuntar independientemente a una ubicación diferente en la PSRAM o tipo de memoria. Se utiliza para manipulación de espacio de direcciones de granularidad muy fina.

Codificación de membankPtr

El array _membankPtr[] tiene 128 entradas — una por cada bloque de 512 bytes del espacio de direcciones de 64KB del Z80. Cada entrada es un único valor de 32 bits que codifica tres campos:
Bit 31..24  =  Memory type   (MEMBANK_TYPE_xxx constant)
Bit 23..16  =  PSRAM bank    (0-63, which 64KB bank in the 8MB PSRAM)
Bit 15..0   =  Z80 address   (base address of the block within the bank)
Para establecer el tipo y banco de un bloque, se empaquetan estos tres valores en un único entero de 32 bits usando OR a nivel de bits:
// Map block idx (each block = 512 bytes) to RAM type in bank 0:
cpu->_membankPtr[idx] = (MEMBANK_TYPE_RAM  << 24)   // type in top byte
                      | (MZ700_MEMBANK_0   << 16)   // bank number
                      | (idx * MEMORY_BLOCK_SIZE);  // base address of this block

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

// Map same block to physical (pass-through):
cpu->_membankPtr[idx] = (MEMBANK_TYPE_PHYSICAL << 24)
                      | (0 << 16)
                      | (idx * MEMORY_BLOCK_SIZE);
MEMORY_BLOCK_SIZE es 512 bytes. Hay 128 bloques que cubren 0x0000–0xFFFF. El índice de bloque para una dirección Z80 dada es: addr / MEMORY_BLOCK_SIZE = addr >> 9.

Atributos de Memoria (t_memAttr)

Cada bloque también tiene una entrada t_memAttr que controla los wait states y la sincronización de ciclos T. Estos se almacenan en un array 2D indexado por banco y bloque:
// src/include/Z80CPU.h
typedef struct {
    uint8_t   waitStates;   // Number of additional T-cycle wait states inserted on access
    bool      tCycSync;     // true = sync PSRAM access to the T1 rising edge of each bus cycle
} t_memAttr;

// Access pattern:
cpu->_memAttr[bank][idx].waitStates = 1;
cpu->_memAttr[bank][idx].tCycSync   = true;
waitStates: Si su región de memoria respaldada por PSRAM requiere más tiempo para responder (por ejemplo, porque la función handler realiza trabajo adicional), añada wait states. Cada wait state extiende el ciclo de bus por un ciclo T del reloj del host. Las regiones RAM típicamente usan 1 wait state; las regiones ROM que sirven datos pre-cargados a menudo pueden usar 0.
tCycSync: Cuando se establece en true, la máquina de estado PIO z80_sync retrasa el acceso a PSRAM hasta el flanco ascendente T1 del ciclo de bus actual. Esto evita que las operaciones internas de PSRAM introduzcan deriva de temporización en software del host que depende de una temporización precisa por ciclo de reloj (E/S de casete, bit-banging serial, bucles de retardo). Establezca esto en true para regiones RAM/ROM a las que accederá software sensible a la temporización del host.

La Estructura PSRAM (t_Z80PSRAM)

La PSRAM externa de 8MB se mapea en una única estructura t_Z80PSRAM. Esta se asigna una vez al inicio y se referencia mediante cpu->_z80PSRAM. Es la estructura de datos más grande e importante del sistema — todo lo que el Z80 puede acceder reside aquí.
// src/include/Z80CPU.h
typedef struct {
    // 4MB data space: 64 banks x 64KB RAM/ROM image storage
    uint8_t     RAM[MEMORY_PAGE_BANKS * MEMORY_PAGE_SIZE];

    // 64KB per-byte redirect table (used by MEMBANK_TYPE_PTR blocks)
    uint32_t    memPtr[MEMORY_PAGE_SIZE];

    // 64KB memory address function pointer table
    // Index = Z80 address (0x0000-0xFFFF)
    // Value = NULL (no override) or pointer to a C handler function
    MemoryFunc  memioPtr[MEMORY_PAGE_SIZE];

    // 64KB I/O port function pointer table
    // Index = Z80 I/O port address (0x0000-0xFFFF; Z80 uses lower 8 bits for port)
    // Value = NULL (pass through to physical I/O) or pointer to a C handler function
    MemoryFunc  ioPtr[IO_PAGE_SIZE];
} t_Z80PSRAM;
Los cuatro sub-arrays tienen roles distintos:
  • RAM[] — almacenamiento de bytes sin procesar para todos los bancos de memoria respaldados por PSRAM. El tamaño total es 64 bancos x 64KB = 4MB. Las imágenes ROM cargadas desde la tarjeta SD o Flash se escriben aquí al arranque. Durante la ejecución del Z80, las lecturas y escrituras a bloques de tipo RAM/ROM/VRAM acceden a este array.
  • memPtr[] — utilizado solo por bloques de tipo PTR. Cada entrada es un valor completo membankPtr (codificado de la misma manera que cpu->_membankPtr[]) que redirige el acceso de un byte individual a una ubicación completamente diferente. Permite remapeo a nivel de byte dentro del espacio de 64KB.
  • memioPtr[] — la tabla de hooks de funciones de memoria. Una ranura por dirección Z80. Cuando una ranura no es NULL, el Core 1 llama a la función en esa ranura en cada acceso de memoria a esa dirección, independientemente del tipo de bloque (RAM, ROM o FUNC). Así es como los controladores interceptan o anulan ubicaciones de memoria específicas sin cambiar el tipo de bloque general.
  • ioPtr[] — la tabla de hooks de puertos de E/S. Una ranura por dirección de E/S del Z80 (puerto A0–A15 completo, aunque el Z80 usa solo A0–A7 como el número de puerto real; los bits superiores pueden llevar contexto adicional). Cuando no es NULL, la función se llama para cada instrucción IN u OUT dirigida a ese puerto. Si es NULL, el ciclo de E/S pasa al hardware físico.

La Firma del Handler MemoryFunc

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

La Estructura de Contexto Z80CPU

Cada función del controlador recibe un puntero Z80CPU *cpu. Este es el contexto maestro para toda la emulación. Los campos más relevantes para los escritores de controladores son:
// src/include/Z80CPU.h (simplified)
struct Z80CPU {
    Z80           _Z80;               // Zeta Z80 emulator state (registers, flags, PC, etc.)

    // Fast dispatch table: 128 entries, one per 512-byte block of Z80 address space
    uint32_t      _membankPtr[MEMORY_PAGE_BLOCKS];  // MEMORY_PAGE_BLOCKS = 128

    // Per-block wait state and sync attributes, indexed [bank][block]
    t_memAttr     _memAttr[MEMORY_PAGE_BANKS][MEMORY_PAGE_BLOCKS];

    // Pointer to the 8MB PSRAM structure (RAM[], memPtr[], memioPtr[], ioPtr[])
    t_Z80PSRAM   *_z80PSRAM;

    // Loaded driver configurations (from JSON parsing)
    t_drivers     _drivers;

    // Intercore communication queues (Core 1 → Core 0 and Core 0 → Core 1)
    queue_t       requestQueue;
    queue_t       responseQueue;

    bool          halt;        // Z80 is in HALT state
    bool          hold;        // Core 0 is requesting Core 1 to pause
    bool          holdAck;     // Core 1 acknowledges hold request
    bool          forceReset;  // Asynchronous reset flag
};

Estructuras de Configuración de Controladores

Cuando un controlador se inicializa, recibe un puntero a una estructura t_drvConfig que fue rellenada por el parser de configuración JSON. Esto le indica al controlador qué interfaces se le han asignado, qué imágenes ROM cargar, qué remapeos de direcciones aplicar y qué parámetros ha configurado el usuario.
// src/include/Z80CPU.h

// A single parameter key/value pair (from JSON "param" array)
typedef struct {
    const char   *name;   // Parameter name string
    const char   *value;  // Parameter value string (always a string; parse as needed)
} t_ifParam;

// A single ROM image assignment (from JSON "rom" array)
typedef struct {
    const char   *file;       // SD card path to ROM file
    uint16_t      addr;       // Z80 target address for this ROM
    uint8_t       bank;       // PSRAM bank to load into
    uint16_t      size;       // Size in bytes to load
    uint32_t      fileofs;    // Byte offset into the ROM file
    uint8_t       waitStates; // Wait states for this block
    bool          tCycSync;   // T1 sync for this block
} t_drvROMConfig;

// A single address-space remap (from JSON "addrmap" array)
typedef struct {
    uint16_t      srcaddr;    // Original Z80 address
    uint16_t      dstaddr;    // Redirected-to address
    uint16_t      size;       // Range size
    uint8_t       bank;       // PSRAM bank
    uint8_t       type;       // MEMBANK_TYPE_xxx
} t_addrReMap;

// A single I/O remap (from JSON "iomap" array)
typedef struct {
    uint16_t      srcaddr;    // I/O port address
    uint16_t      size;       // Port range size
    const char   *funcName;   // Name of handler function (looked up in memory func map)
} t_ioReMap;

// One interface block (one entry in JSON "if" array)
typedef struct t_drvIFConfig {
    const char       *name;           // Interface name (e.g. "RFS", "MZ-1E05")
    bool              isPhysical;     // Virtual or physical interface
    int               romCount;       // Number of ROM entries
    t_drvROMConfig   *romConfig;      // Array of ROM configurations
    int               addrMapCount;   // Number of address remaps
    t_addrReMap      *addrMap;        // Address remap table
    int               ioMapCount;     // Number of I/O remaps
    t_ioReMap        *ioMap;          // I/O remap table
    int               ifParamCount;   // Number of parameters
    t_ifParam        *ifParam;        // Parameter array
} t_drvIFConfig;

// Top-level driver config (one entry in JSON "drivers" array)
typedef struct t_drvConfig {
    const char       *name;           // Driver name — must match a virtualFuncMap entry
    bool              isPhysical;     // Virtual or physical driver
    int               ifCount;        // Number of interfaces
    t_drvIFConfig    *ifConfig;       // Interface array
    ResetFunc         reset_ptr;      // Reset handler set by driver init
    PollFunc          poll_ptr;       // Poll handler set by driver init
    TaskFunc          task_ptr;       // Task handler set by driver init
} t_drvConfig;
Puertos base de E/S reubicables. Para que las tarjetas independientes de la máquina puedan usarse en placas personalizadas / de experimentación (véase OpenZ80), varios controladores leen su puerto base de E/S desde la entrada iomap de la interfaz en lugar de codificarlo de forma fija: el dstaddr de la entrada se convierte en la base de la tarjeta (con srcaddr como el puerto auténtico de la tarjeta), y cada una de estas tarjetas lleva una constante *_DEFAULT_BASE en su cabecera que se utiliza cuando no hay ninguna entrada iomap presente. Las tarjetas reubicables y sus valores predeterminados son MZ-1R12 (0xF8/3), MZ-1R18 (0xEA/2), MZ-1R23 (0xB8/2), MZ-1R37 (0xAC/2), PIO-3034 (0x00/4), MZ-8BIO3 / MZ-1E24 (0xB0/4), MZ-1E05 (0xD8/7) y Celestite (0x60/16). Las claves JSON iomap/addrmap están en minúsculas (srcaddr / dstaddr) y se parsean como números; el campo Base I/O Port de la página de Configuración de la GUI las escribe por usted.
Los campos reset_ptr, poll_ptr y task_ptr no se establecen desde JSON — los establece la función init de su controlador para que el Core 1 pueda llamar a las funciones de mantenimiento de su controlador en los momentos apropiados.

El Bucle de Despacho del Core 1

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

Estructura del Bucle Principal

// src/Z80CPU.c — Core 1 entry point
void __func_in_RAM(Z80CPU_cpu)(Z80CPU *cpu)
{
    // Signal Core 0 that Core 1 is running
    multicore_fifo_push_blocking(1);

    while(1)
    {
        // --- HOLD CHECK ---
        // Core 0 can pause Core 1 (e.g. for safe config reload)
        if(cpu->hold == true)
        {
            cpu->holdAck = true;
            while(cpu->hold == true);  // Spin-wait
            cpu->holdAck = false;
        }

        // --- DRIVER POLL ---
        // Call each driver's poll handler for brief periodic housekeeping
        for(int idx = 0; idx < cpu->_drivers.drvCount; idx++)
        {
            if(cpu->_drivers.driver[idx].poll_ptr != NULL)
                cpu->_drivers.driver[idx].poll_ptr(cpu);
        }

        // --- Z80 EXECUTION ---
        // Run the Z80 emulator for 2048 clock cycles
        z80_run(&cpu->_Z80, 2048);

        // --- RESET CHECK ---
        // PIO IRQ 3 = hardware RESET asserted on the host bus
        if(cpu->forceReset || (pio_2->irq & (1u << 3)) != 0)
        {
            Z80CPU_reset(cpu);
            CLEAR_IRQ(pio_2, 3);
        }
    }
}
Puntos clave:
  • El bucle ejecuta z80_run() durante 2048 ciclos por iteración. Entre iteraciones, se llaman todos los handlers de poll de los controladores. Esto significa que los handlers de poll se llaman aproximadamente cada 2048 ciclos de reloj del Z80 — a 3.5MHz eso es aproximadamente cada 585 microsegundos.
  • Los handlers de poll deben ser extremadamente cortos. Están en la ruta crítica de la emulación. Un handler de poll lento introduce jitter en la temporización del bus del Z80.
  • La función z80_run() (de la biblioteca Zeta Z80) ejecuta instrucciones Z80, haciendo callbacks a Z80CPU_readMem(), Z80CPU_writeMem(), Z80CPU_readIO() y Z80CPU_writeIO() para cada transacción de bus.

Despacho de Lectura de Memoria

Cuando el emulador Z80 realiza una lectura de memoria, se llama a Z80CPU_readMem(). Esta función es el corazón del sistema de memoria — comprenderla le dice exactamente lo que sus handlers necesitan hacer.
// src/Z80CPU.c (simplified and annotated)
uint8_t __func_in_RAM(Z80CPU_readMem)(Z80CPU *cpu, uint16_t addr)
{
    // Step 1: Look up the 512-byte block that contains this address
    uint8_t  blockIdx   = addr >> 9;                        // addr / 512
    uint32_t membankptr = cpu->_membankPtr[blockIdx];       // 32-bit encoded entry

    // Step 2: Extract the three fields from the encoded entry
    uint8_t  memType    = (membankptr >> 24) & 0xFF;        // Top byte = type
    uint8_t  bank       = (membankptr >> 16) & 0xFF;        // Middle byte = bank
    uint16_t blockBase  = membankptr & 0xFFFF;              // Lower 16 bits = base addr

    // Step 3: Calculate the offset into the PSRAM bank for this address
    uint32_t RAMaddr    = (bank * MEMORY_PAGE_SIZE) + blockBase;
    uint16_t blockOfs   = addr & (MEMORY_BLOCK_SIZE - 1);  // addr % 512 = offset in block

    uint8_t  waitStates = cpu->_memAttr[bank][blockIdx].waitStates;
    uint8_t  data       = 0x00;

    switch(memType)
    {
        case MEMBANK_TYPE_PHYSICAL:
        case MEMBANK_TYPE_PHYSICAL_VRAM:
        case MEMBANK_TYPE_PHYSICAL_HW:
            // Pass-through: let the host hardware respond
            data = Z80CPU_readPhysicalMem(cpu, addr);
            break;

        case MEMBANK_TYPE_RAM:
        case MEMBANK_TYPE_VRAM:
        case MEMBANK_TYPE_ROM:
            if(cpu->_z80PSRAM->memioPtr[addr] != NULL)
            {
                // A handler is installed for this specific address.
                // Pass the current PSRAM value as 'data' so the handler can use or modify it.
                data = cpu->_z80PSRAM->memioPtr[addr](
                           cpu, true, addr,
                           cpu->_z80PSRAM->RAM[RAMaddr + blockOfs]);
            }
            else
            {
                // No handler — read directly from PSRAM
                data = cpu->_z80PSRAM->RAM[RAMaddr + blockOfs];
            }
            if(waitStates) Z80CPU_waitPhysicalStates(cpu, waitStates);
            break;

        case MEMBANK_TYPE_FUNC:
            // Pure virtual device — no PSRAM backing
            if(cpu->_z80PSRAM->memioPtr[addr] != NULL)
                data = cpu->_z80PSRAM->memioPtr[addr](cpu, true, addr, 0);
            break;

        case MEMBANK_TYPE_PTR:
            // Indirect — follow the per-byte pointer and recurse
            data = Z80CPU_readMem(cpu, addr, cpu->_z80PSRAM->memPtr[addr]);
            break;
    }
    return data;
}

Despacho de Escritura de Memoria

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

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

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

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

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

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

Despacho de Puertos de E/S

// src/Z80CPU.c (simplified and annotated)
uint8_t __func_in_RAM(Z80CPU_readIO)(Z80CPU *cpu, uint16_t addr)
{
    // addr = full 16-bit Z80 address during I/O instruction (A0-A15)
    // Port number = addr & 0xFF (A0-A7 = lower byte)

    if(cpu->_z80PSRAM->ioPtr[addr] != NULL)
    {
        // Virtual I/O: call handler
        return cpu->_z80PSRAM->ioPtr[addr](cpu, true, addr, 0);
    }
    else
    {
        // Physical I/O: RP2350 releases bus, host hardware responds
        return Z80CPU_readPhysicalIO(cpu, addr);
    }
}

void __func_in_RAM(Z80CPU_writeIO)(Z80CPU *cpu, uint16_t addr, uint8_t data)
{
    if(cpu->_z80PSRAM->ioPtr[addr] != NULL)
    {
        cpu->_z80PSRAM->ioPtr[addr](cpu, false, addr, data);
    }
    else
    {
        Z80CPU_writePhysicalIO(cpu, addr, data);
    }
}
Observe que el despacho de E/S es más simple que el despacho de memoria — no existe el concepto de tipo de bloque para puertos de E/S. Cada dirección de E/S tiene un handler en ioPtr[] o va al hardware físico. No hay distinción ROM/RAM/FUNC para E/S.
También note: el Z80 usa una dirección de 16 bits en las instrucciones de E/S — los 8 bits inferiores son el número de puerto; los 8 bits superiores llevan el contenido del registro B (durante las instrucciones IN r,(C) / OUT (C),r). Si desea un comportamiento diferente del handler dependiendo del registro B, examine el byte alto de addr. Para coincidencia simple solo por número de puerto, enmascare con addr & 0xFF.

El Framework de Controladores

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

virtualFuncMap — Registro de Controladores de Nivel Superior

El array virtualFuncMap[] en src/Z80CPU.c mapea un nombre de cadena a una función init del controlador. Cada controlador de nivel superior (persona) debe tener una entrada aquí. El nombre de cadena debe coincidir exactamente con el campo "name" en el array JSON "drivers" (sin distinción de mayúsculas/minúsculas).
// src/Z80CPU.c

// Type definition for a top-level driver init function
typedef uint8_t (*VirtualFunc)(Z80CPU *cpu,
                               t_FlashAppConfigHeader *appConfig,
                               t_drvConfig *config,
                               const char *ifName);

// Map entry structure
typedef struct {
    const char  *virtualFuncName;   // String name (must match JSON "name" field)
    VirtualFunc  virtual_func_ptr;  // C function to call
} t_VirtualFuncMap;

// THE REGISTRATION TABLE — add your driver here
static const t_VirtualFuncMap virtualFuncMap[] = {
#ifdef INCLUDE_SHARP_DRIVERS
    {"MZ700",   MZ700_Init},   // Sharp MZ-700 persona
    {"MZ80A",   MZ80A_Init},   // Sharp MZ-80A persona
    {"MZ2000",  MZ2000_Init},  // Sharp MZ-2000 persona
    {"MZ2200",  MZ2200_Init},  // Sharp MZ-2200 persona
    {"MZ80B",   MZ80B_Init},   // Sharp MZ-80B persona
    {"MZ2500",  MZ2500_Init},  // Sharp MZ-2500 persona
    {"MZ800",   MZ800_Init},   // Sharp MZ-800 persona (dual-mode MZ-700/MZ-800)
    {"MZ1500",  MZ1500_Init},  // Sharp MZ-1500 persona
#endif
#ifdef INCLUDE_AMSTRAD_DRIVERS
    {"PCW9512", PCW9512_Init}, // Amstrad PCW-9512 persona
#endif
#ifdef INCLUDE_TATUNG_DRIVERS
    {"EinsteinTC01", EinsteinTC01_Init}, // Tatung Einstein TC-01 persona
#endif
#ifdef INCLUDE_OPEN_DRIVERS
    {"Open",    Open_Init},   // OpenZ80 vanilla / experimenter persona
#endif
};
static const size_t virtualFuncMapSize = sizeof(virtualFuncMap) / sizeof(virtualFuncMap[0]);
La función de búsqueda que recorre esta tabla por nombre es:
// src/Z80CPU.c
VirtualFunc Z80CPU_getVirtualFunc(const char *funcName)
{
    for(size_t i = 0; i < virtualFuncMapSize; i++)
    {
        if(strncasecmp(funcName, virtualFuncMap[i].virtualFuncName,
                       strlen(virtualFuncMap[i].virtualFuncName)) == 0)
            return virtualFuncMap[i].virtual_func_ptr;
    }
    return NULL;  // Name not found — driver will be skipped
}

OpenZ80 — Construir sobre un Controlador o BIOS Existente

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

TZFS — Modos de Memoria y el Procesador de Servicio Virtual

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

Flujo de Inicialización

Después de que el RP2350 lee y analiza config.json, la inicialización de controladores ocurre en el siguiente orden:
Z80CPU_configFromJSON()
    ├── Z80CPU_configDriversFromJSON()   Parse "drivers" array from JSON
    │       For each driver entry:
    │           ├── Look up name in virtualFuncMap[]
    │           ├── Parse interface configs (ROM, addrmap, iomap, param)
    │           └── Call VirtualFunc(cpu, appConfig, &drvConfig, NULL)
    │                   ↓
    │               Driver init sets up:
    │                   ├── _membankPtr[] entries (block types and banks)
    │                   ├── _memAttr[][] entries (wait states, sync)
    │                   ├── memioPtr[] handlers (memory address hooks)
    │                   ├── ioPtr[] handlers (I/O port hooks)
    │                   ├── config->reset_ptr = MyDriver_Reset
    │                   ├── config->poll_ptr  = MyDriver_PollCB
    │                   └── config->task_ptr  = MyDriver_TaskProcessor
    │
    ├── Z80CPU_configMemoryFromJSON()    Apply "memory" array (overwrites driver defaults)
    └── Z80CPU_configIOFromJSON()        Apply "io" array (overwrites driver defaults)
Note el orden: los controladores se inicializan primero, luego se aplican los arrays JSON "memory" y "io". Esto significa que cualquier entrada explícita en los arrays memory o io en config.json anulará lo que el controlador configuró para esas direcciones. Esto permite al usuario ajustar los valores predeterminados del controlador sin modificar el código fuente del controlador.

Callbacks del Ciclo de Vida de Controladores

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

Ejemplo Práctico: El Controlador MZ-700

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

El Controlador MZ-800 (Persona de Modo Dual)

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

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

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

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

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

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

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

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

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

Notas para Desarrolladores: Arranque de Disquete desde TZFS y Retorno a TZFS en Reset

A partir de TZFS v1.8.3, el picoZ80 puede arrancar disquetes MZ-80A / MZ-700 / MZ-800 (incluyendo MZ-800 CP/M) directamente desde dentro de TZFS y volver a TZFS cuando se pulsa el interruptor RESET de hardware tras un SO arrancado desde disquete. Varios hooks cooperantes hacen que esto funcione, y vale la pena entenderlos si va a extender la ruta de arranque o añadir una nueva interfaz de disco:
  • TZFS GETBOOTDSK ahora acepta cualquier id de máquina Sharp (01/02/03) más la firma IPLPRO, pre-pagina el bloque 7 de DRAM en 0000–0FFF para los SO que cargan por debajo de 1000H (por ejemplo, MZ-800 CP/M), y entrega al programa cargado BC=0200H exactamente como el IPL nativo 9Z-504M, de modo que un cargador de 2ª etapa pueda dirigir su propia lectura de directorio.
  • Domar el 8253 antes de JP (HL). TZFS reprograma el contador 2 a ~1Hz y enmascara PC2 del 8255 (reflejando el SORES del IPL) antes de saltar al programa cargado, de modo que un contador de libre ejecución no pueda inundar el Z80 con interrupciones RST 38H y abortar un cargador que reactiva las interrupciones demasiado pronto. Esto es lo que corrige el fallo "P-CP/M80 → No system file".
  • MZ-1E05 MZ1E05_IO_A10Toggle. El handler de fetch de aceleración por hardware del FDC hace un OR de DRQ con A10 en 0xF3FE / 0xF7FE, de modo que DSKREAD de TZFS transmite un sector de arranque en el MZ-800 igual que lo hace en el MZ-80A.
  • Los modos TZMM_DSKLOAD / TZMM_DSKRUN de TZFS cargan y ejecutan un SO arrancado desde disquete en la DRAM nativa del bloque 0 exactamente como lo hace el propio IPL de la máquina.
  • Seguimiento de cgWindow del MZ-800. Un SO en modo MZ-800 que carga una fuente PCG (IN 0xE0) alcanza la CG-ROM / CG-RAM real bajo TZFS (corrigiendo el texto ilegible del disk-BASIC) mientras que juegos nativos como Flappy no se ven afectados.
  • Restauración en reset. En el reset, el controlador recarga el monitor prístino del bloque 0 + la UROM de TZFS (que el SO sobrescribió en TZMM_DSKRUN, ya que la ROM/RAM del bloque 0 son alias de la misma PSRAM) y luego arranca en frío TZFS, de modo que el interruptor de reset regresa a TZFS en lugar del monitor 1Z-013A pelado.
Tres correcciones relacionadas de RAM baja / reset apoyan la misma ruta: mapear 0x0000–0x0FFF desde el banco 7 bajo TZFS para que un programa nativo que hace LDIR de código en la RAM baja y luego cambia de modo de video vea la misma memoria en ambas rutas (la corrección general de discrepancia de banco de PSRAM de Flappy); volver a ejecutar initVideoText una vez que /RESET se libera para que el GDG vuelva a escanear y se complete la sincronización HBLK del monitor (recuperación del GDG en reset de hardware); y un retardo de estabilización de 10ms tras el reset físico para que el GDG/8255 de la placa terminen su reset de hardware antes de que el firmware reinicialice el video.

Volver al Ejemplo del MZ-700

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

Mapa de Funciones de Interfaz

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

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

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

Estructura de Estado de Banking

Dado que el estado de banking debe persistir entre transacciones de bus (una escritura a 0xE0 debe recordarse para que los accesos de memoria posteriores vayan al banco correcto), el controlador mantiene una estructura de estado estática:
// src/drivers/Sharp/MZ700.c (abbreviated)
typedef struct {
    bool      loDRAMen;     // true = lower 4KB (0x0000-0x0FFF) is mapped to RAM bank 1
    bool      hiDRAMen;     // true = upper region (0xD000-0xFFFF) is mapped to RAM bank 1
    bool      inhibit;      // true = upper bank-swap is inhibited (INHIBIT port written)

    // Saved membankPtr values for the upper region when hi DRAM is swapped in
    // (needed to restore the original mapping when hi DRAM is swapped back out)
    uint32_t  upmembankPtr[MZ700_UPPERMEM_BLOCKS];
} t_MZ700Ctrl;

static t_MZ700Ctrl MZ700Ctrl = {
    .loDRAMen = false,
    .hiDRAMen = false,
    .inhibit  = false,
};
Las variables estáticas como esta son seguras porque solo hay una instancia de Z80CPU y el Core 1 es el único hilo que llama a las funciones handler. Si alguna vez tuviera múltiples instancias de CPU (no es el diseño actual), movería este estado dentro de t_drvConfig.

La Función Init — MZ700_Init()

MZ700_Init() sirve un doble propósito. Se llama en dos contextos diferentes, identificados por qué argumentos son NULL:
  • Modo de validación (ifName != NULL, config == NULL): Llamado por Z80CPU_configDriversFromJSON() para preguntar "¿soportas este nombre de interfaz?" Retorna 1 si sí, 0 si no. Esto permite al parser JSON validar nombres de interfaz contra el controlador antes de intentar inicializarlos.
  • Modo de configuración (ifName == NULL, config != NULL): La inicialización real — configurar el mapa de memoria, instalar hooks, configurar interfaces.
// src/drivers/Sharp/MZ700.c
uint8_t MZ700_Init(Z80CPU *cpu, t_FlashAppConfigHeader *appConfig,
                   t_drvConfig *config, const char *ifName)
{
    // --- VALIDATION MODE ---
    if(ifName != NULL && config == NULL)
    {
        // Check if ifName is in our interfaceFuncMap
        for(size_t i = 0; i < interfaceFuncMapSize; i++)
        {
            if(strncasecmp(ifName, interfaceFuncMap[i].interfaceFuncName,
                           strlen(interfaceFuncMap[i].interfaceFuncName)) == 0)
                return 1;  // Yes, we support this interface
        }
        return 0;  // Unknown interface
    }

    // --- CONFIGURATION MODE ---
    if(ifName == NULL && config != NULL)
    {
        // Determine if this is a physical (pass-through) or virtual (emulated) driver
        bool isPhysical = config->isPhysical;

        // ------------------------------------------------------------------
        // STEP 1: Set up the flat memory map (membankPtr[] + memAttr[][])
        // ------------------------------------------------------------------
        // Walk all 128 blocks and assign a type/bank for each
        for(int idx = 0; idx < MEMORY_PAGE_BLOCKS; idx++)
        {
            uint32_t memType   = MEMBANK_TYPE_PHYSICAL;  // Default: pass-through
            uint8_t  bank      = MZ700_MEMBANK_0;
            uint8_t  waitStates = 0;
            bool     tCycSync  = false;

            // Blocks 0-7 = 0x0000-0x0FFF (Monitor ROM / low 4KB)
            // If not physical, these will be overridden by JSON "memory" array.
            // Leave as PHYSICAL here so that JSON can assign ROM or RAM as needed.

            // Blocks 8-103 = 0x1000-0xCFFF (main RAM)
            if(idx >= 8 && idx <= 103)
            {
                if(!isPhysical)
                {
                    memType    = MEMBANK_TYPE_RAM;
                    waitStates = 1;
                    tCycSync   = true;
                }
            }

            // Blocks 104-111 = 0xD000-0xDFFF (VRAM + colour VRAM)
            if(idx >= 104 && idx <= 111)
                memType = MEMBANK_TYPE_PHYSICAL_VRAM;

            // Blocks 112-119 = 0xE000-0xE7FF (hardware registers: PPI, timer, etc.)
            if(idx >= 112 && idx <= 119)
                memType = MEMBANK_TYPE_PHYSICAL_HW;

            // Blocks 120-127 = 0xF000-0xFFFF (FDC address space)
            // Left as PHYSICAL so physical FDC hardware can respond if installed

            // Pack into membankPtr entry
            cpu->_membankPtr[idx] = (memType    << 24)
                                  | (bank       << 16)
                                  | (idx * MEMORY_BLOCK_SIZE);

            cpu->_memAttr[bank][idx].waitStates = waitStates;
            cpu->_memAttr[bank][idx].tCycSync   = tCycSync;
        }

        // ------------------------------------------------------------------
        // STEP 2: Clear the memioPtr and ioPtr tables for our address range
        // (ensure no stale handler pointers from a previous config)
        // ------------------------------------------------------------------
        for(int idx = 0; idx < MEMORY_PAGE_SIZE; idx++)
            cpu->_z80PSRAM->memioPtr[idx] = NULL;

        for(int idx = 0; idx < IO_PAGE_SIZE; idx++)
            cpu->_z80PSRAM->ioPtr[idx] = NULL;

        // ------------------------------------------------------------------
        // STEP 3: Install I/O port handlers for banking control ports
        // ------------------------------------------------------------------
        if(!isPhysical)
        {
            // MZ-700 memory banking ports 0xE0-0xE6
            for(int port = 0xE0; port <= 0xE6; port++)
                cpu->_z80PSRAM->ioPtr[port] = (MemoryFunc)MZ700_IO_MemoryBankPorts;
        }

        // ------------------------------------------------------------------
        // STEP 4: Register lifecycle callbacks
        // ------------------------------------------------------------------
        config->reset_ptr = (ResetFunc)MZ700_Reset;
        config->poll_ptr  = (PollFunc)MZ700_PollCB;
        config->task_ptr  = (TaskFunc)MZ700_TaskProcessor;

        // ------------------------------------------------------------------
        // STEP 5: Initialise sub-interfaces listed in the JSON "if" array
        // ------------------------------------------------------------------
        for(int ifNo = 0; ifNo < config->ifCount; ifNo++)
        {
            t_drvIFConfig *ifcfg = &config->ifConfig[ifNo];

            // Find the interface in our map
            for(size_t i = 0; i < interfaceFuncMapSize; i++)
            {
                if(strncasecmp(ifcfg->name, interfaceFuncMap[i].interfaceFuncName,
                               strlen(interfaceFuncMap[i].interfaceFuncName)) == 0)
                {
                    // Call the sub-driver init function
                    interfaceFuncMap[i].init_func_ptr(cpu, appConfig, ifcfg);
                    interfaceFuncMap[i].active = true;
                    break;
                }
            }
        }
    }
    return 1;
}

El Handler de Banking — MZ700_IO_MemoryBankPorts()

Este es el handler de E/S que gestiona los seis puertos de control de banking del MZ-700. Es llamado por Z80CPU_writeIO() cada vez que el Z80 escribe en los puertos 0xE0–0xE6. Leer de estos puertos no tiene efecto secundario (retorna 0xFF). Escribir cambia el mapa de memoria modificando las entradas de _membankPtr[] en tiempo real.
// src/drivers/Sharp/MZ700.c
uint8_t MZ700_IO_MemoryBankPorts(Z80CPU *cpu, bool read, uint16_t addr, uint8_t data)
{
    uint8_t port = (uint8_t)(addr & 0xFF);  // Extract port number from Z80 address

    // Reads have no side-effect
    if(read) return 0xFF;

    // --- PORT 0xE0: Enable lower 4KB DRAM ---
    // Swaps Monitor ROM (0x0000-0x0FFF) for RAM in bank 1
    if(port == 0xE0 && !MZ700Ctrl.loDRAMen)
    {
        for(int idx = 0; idx < (0x1000 / MEMORY_BLOCK_SIZE); idx++)
        {
            cpu->_membankPtr[idx] = (MEMBANK_TYPE_RAM << 24)
                                  | (MZ700_MEMBANK_1  << 16)
                                  | (idx * MEMORY_BLOCK_SIZE);
        }
        MZ700Ctrl.loDRAMen = true;
    }

    // --- PORT 0xE1: Enable upper DRAM (0xD000-0xFFFF) ---
    // Saves the current upper membankPtr entries and replaces them with RAM in bank 1
    if(port == 0xE1 && !MZ700Ctrl.inhibit && !MZ700Ctrl.hiDRAMen)
    {
        int startBlock = 0xD000 / MEMORY_BLOCK_SIZE;
        int endBlock   = 0x10000 / MEMORY_BLOCK_SIZE;

        for(int idx = startBlock; idx < endBlock; idx++)
        {
            // Save current mapping so we can restore it later (port 0xE3)
            MZ700Ctrl.upmembankPtr[idx - startBlock] = cpu->_membankPtr[idx];

            // Replace with RAM
            cpu->_membankPtr[idx] = (MEMBANK_TYPE_RAM << 24)
                                  | (MZ700_MEMBANK_1  << 16)
                                  | (idx * MEMORY_BLOCK_SIZE);
        }
        MZ700Ctrl.hiDRAMen = true;
    }

    // --- PORT 0xE2: Restore lower ROM ---
    // Swaps bank 1 RAM back out, restoring Monitor ROM at 0x0000-0x0FFF
    if(port == 0xE2 && MZ700Ctrl.loDRAMen)
    {
        for(int idx = 0; idx < (0x1000 / MEMORY_BLOCK_SIZE); idx++)
        {
            // Restore to ROM in bank 0
            cpu->_membankPtr[idx] = (MEMBANK_TYPE_ROM << 24)
                                  | (MZ700_MEMBANK_0  << 16)
                                  | (idx * MEMORY_BLOCK_SIZE);
        }
        MZ700Ctrl.loDRAMen = false;
    }

    // --- PORT 0xE3: Restore upper hardware mapping ---
    if(port == 0xE3 && MZ700Ctrl.hiDRAMen)
    {
        int startBlock = 0xD000 / MEMORY_BLOCK_SIZE;
        int endBlock   = 0x10000 / MEMORY_BLOCK_SIZE;

        for(int idx = startBlock; idx < endBlock; idx++)
        {
            // Restore saved mapping
            cpu->_membankPtr[idx] = MZ700Ctrl.upmembankPtr[idx - startBlock];
        }
        MZ700Ctrl.hiDRAMen = false;
    }

    // --- PORT 0xE4: Inhibit upper DRAM swap ---
    if(port == 0xE4)
        MZ700Ctrl.inhibit = true;

    // --- PORT 0xE5: Enable upper inhibit (alias) ---
    if(port == 0xE5)
        MZ700Ctrl.inhibit = false;

    // PORT 0xE6: Memory protect (not implemented in this example)

    return 0;  // Return value ignored for write I/O handlers
}
Este handler demuestra el patrón más importante en la escritura de controladores: modificar _membankPtr[] en respuesta a una escritura de E/S para implementar banking de memoria. Los cambios surten efecto inmediatamente — el próximo acceso de memoria del Z80 usará el nuevo mapeo.
El patrón de guardar/restaurar para la región de memoria superior (MZ700Ctrl.upmembankPtr[]) es importante: cuando intercambia regiones mapeadas a hardware por RAM, debe recordar lo que había allí para poder restaurarlo cuando el software haga el intercambio de vuelta. Simplemente asignar PHYSICAL de nuevo perdería cualquier mapeo personalizado que fue configurado por sub-controladores o la configuración JSON.

El Handler de Reset — MZ700_Reset()

Cuando el host activa RESET, el Core 1 llama al reset_ptr de cada controlador. El handler de reset del MZ-700 restaura el mapa de memoria al estado de encendido (ROM en 0x0000, VRAM y hardware en 0xD000+) y luego llama a todos los handlers de reset de interfaces activas:
// src/drivers/Sharp/MZ700.c
uint8_t MZ700_Reset(Z80CPU *cpu)
{
    // Restore lower 4KB to ROM if it was swapped to DRAM
    if(MZ700Ctrl.loDRAMen)
    {
        for(int idx = 0; idx < (0x1000 / MEMORY_BLOCK_SIZE); idx++)
        {
            cpu->_membankPtr[idx] = (MEMBANK_TYPE_ROM << 24)
                                  | (MZ700_MEMBANK_0  << 16)
                                  | (idx * MEMORY_BLOCK_SIZE);
        }
        MZ700Ctrl.loDRAMen = false;
    }

    // Restore upper region if it was swapped to DRAM
    if(MZ700Ctrl.hiDRAMen)
    {
        int startBlock = 0xD000 / MEMORY_BLOCK_SIZE;
        int endBlock   = 0x10000 / MEMORY_BLOCK_SIZE;
        for(int idx = startBlock; idx < endBlock; idx++)
            cpu->_membankPtr[idx] = MZ700Ctrl.upmembankPtr[idx - startBlock];
        MZ700Ctrl.hiDRAMen = false;
    }
    MZ700Ctrl.inhibit = false;

    // Propagate reset to all active sub-interfaces
    for(size_t i = 0; i < interfaceFuncMapSize; i++)
    {
        if(interfaceFuncMap[i].active)
            interfaceFuncMap[i].reset_func_ptr(cpu);
    }
    return 0;
}

El Handler de Poll — MZ700_PollCB()

// src/drivers/Sharp/MZ700.c
uint8_t MZ700_PollCB(Z80CPU *cpu)
{
    // The MZ-700 persona itself has nothing to do in the poll loop —
    // all polling is delegated to active sub-interfaces
    for(size_t i = 0; i < interfaceFuncMapSize; i++)
    {
        if(interfaceFuncMap[i].active)
            interfaceFuncMap[i].poll_func_ptr(cpu);
    }
    return 0;
}

El Procesador de Tareas — MZ700_TaskProcessor()

// src/drivers/Sharp/MZ700.c
uint8_t MZ700_TaskProcessor(Z80CPU *cpu, enum Z80CPU_TASK_NAME task, char *param)
{
    // Dispatch incoming task results to the sub-interface that requested them
    for(size_t i = 0; i < interfaceFuncMapSize; i++)
    {
        if(interfaceFuncMap[i].active)
            interfaceFuncMap[i].task_func_ptr(cpu, task, param);
    }
    return 0;
}
El patrón del procesador de tareas es consistente en todos los controladores: la persona simplemente distribuye la tarea a todas las sub-interfaces activas. Cada sub-interfaz verifica si la tarea es relevante para ella y la ignora si no lo es.

Ejemplo Práctico: El Controlador Virtual CMT (Casete)

El controlador Virtual CMT (src/drivers/Sharp/CMT.c + src/include/drivers/Sharp/CMT.h) es una pletina de casete virtual a nivel de forma de onda para la serie Sharp MZ. Emula la forma de onda del casete, no el cargador, por lo que desde el punto de vista del Z80 es indistinguible de una pletina real: el LOAD del monitor, BASIC, los cargadores personalizados/turbo y SAVE funcionan todos sin cambios. La carga desde tarjeta SD sigue siendo la ruta rápida; el CMT es la ruta auténtica — tome un .mzf y cárguelo "como un casete real", o grabe en un .mzf nuevo. Es un buen estudio de un controlador que debe ser exacto en temporización frente al tiempo de reloj de pared, que re-tipifica un bloque de hardware existente sin perturbar la ruta rápida física ajustada, y que abarca ambos cores más la cola de comandos inversa de vuelta al ESP32.
Un único motor sirve a dos familias de hardware, inferidas de la persona:
  • SIMPLE (MZ-80K / MZ-80A / MZ-700 / MZ-800) — motor on/off del 8255, cinta lineal, 1200 baudios (0 ~ 504µs, 1 ~ 958µs). El MZ-800 se ajusta a ~379/964µs y lee/escribe el casete a través de los puertos de E/S D0–D3 en lugar del 8255 mapeado en memoria en E000–E003.
  • CONTROLLED (MZ-80B / MZ-2000 / MZ-2200 / MZ-2500) — transporte controlado por ordenador (PLAY/STOP/FF/REW/EJECT más búsqueda de programas APSS), 2000 baudios (~333/667µs, con una variante de ~1800 baudios).
Ambas familias comparten un único códec: un byte es un bit de arranque largo seguido de 8 bits de datos con el MSB primero; un bloque es cabecera (leader) + marca de cinta + bytes + una suma de comprobación de 16 bits (el recuento de bits a 1); un archivo es un registro de cabecera (info) seguido de un registro de datos, cada uno escrito dos veces.

Motor del Controlador y Registro

El motor expone los cuatro callbacks estándar del ciclo de vida — CMT_Init / CMT_Reset / CMT_PollCB / CMT_TaskProcessor — y se registra como una interfaz (sub-controlador) en el interfaceFuncMap[] de cada máquina compatible, exactamente igual que RFS o MZ-1E05 (véase Mapa de Funciones de Interfaz):
// In each machine's interfaceFuncMap[] (e.g. src/drivers/Sharp/MZ700.c):
static t_InterfaceFuncMap interfaceFuncMap[] = {
    {"RFS",     false, RFS_Init,    RFS_Reset,    RFS_PollCB,    RFS_TaskProcessor},
    {"CMT",     false, CMT_Init,    CMT_Reset,    CMT_PollCB,    CMT_TaskProcessor},
    // ... other interfaces ...
};
Toda la temporización se mide frente al tiempo de reloj de pared time_us_64() — el mismo enfoque de temporizador de un disparo que usa el CMT de RFS — nunca frente a los T-states emulados del Z80, de modo que la forma de onda emulada se mantiene fiel a la pletina real independientemente de cuántos ciclos ejecutó una determinada porción de z80_run(). El cursor de flancos en streaming avanza en tiempo O(1) amortizado; no hay un enorme búfer de flancos precalculado, por lo que un programa entero se transmite con una huella de memoria fija y pequeña. CMT_Init sigue el habitual contrato de doble propósito (modo de validación cuando ifName != NULL, modo de configuración cuando config != NULL) y, en modo de configuración, infiere la familia de la persona e instala los hooks apropiados descritos a continuación.

Familia Simple — Re-tipificación del Bloque de Casete del 8255

En la familia simple el casete reside dentro de la región del 8255 mapeada en memoria. El controlador re-tipifica el bloque del 8255 — 512 bytes en E000–E1FF, índice de bloque 112 — de su tipo físico a MEMBANK_TYPE_RAM e instala un único handler memioPtr de bloque completo sobre él (véase Constantes de Tipo de Bloque de Memoria e intercepción de RAM). El objetivo del handler de bloque completo es que solo se virtualizan los bits de casete del Puerto C; todo lo demás en el bloque debe comportarse exactamente como el hardware real:
  • Los registros ajenos al casete pasan directamente. El teclado (E000/E001), el 8253 (E004–E007), el joystick (E008) y sus espejos se atienden llamando a Z80CPU_readPhysicalMem() / Z80CPU_writePhysicalMem(), de modo que los chips reales del host responden igual que lo harían bajo el mapeo MEMBANK_TYPE_PHYSICAL_HW intacto.
  • Solo los bits de casete del Puerto C se superponen o se espían. En la lectura, el nivel de datos de lectura actual del casete virtual se superpone al byte devuelto por el Puerto C físico; en la escritura, se espían los bits de motor/grabación. La ruta rápida PHYSICAL_HW ajustada queda por tanto intacta para cada acceso ajeno al casete — la re-tipificación le da al controlador un hook solo donde lo necesita.
En el MZ-800 los hooks equivalentes se colocan en los puertos de E/S D0–D3 en lugar del bloque de memoria, reflejando el casete mapeado en puertos del MZ-800.

Familia Controlada — Hooks ioPtr[] en E0–E3

La familia controlada (MZ-80B / MZ-2000 / MZ-2200 / MZ-2500) controla su transporte a través de puertos de E/S, por lo que CMT instala handlers ioPtr[] en los puertos E0–E3 (véase hooks de puerto de E/S). Estos decodifican los comandos de transporte — PLAY/STOP/FF/REW/EJECT — y la búsqueda de programas APSS. Como la cinta virtual es una cola ordenada de programas (abajo), FF/REW saltan programas enteros y APSS busca el límite del programa siguiente/anterior y se detiene automáticamente allí, reflejando el comportamiento de la pletina real.

La Cola de Cinta (Parámetro de Configuración)

La cinta virtual es una cola ordenada, configurada desde la GUI, de hasta 16 archivos MZF, suministrada al controlador como el array de parámetros de la interfaz (t_ifParam, la lista JSON param[] — véase Estructuras de Configuración de Controladores): cada entrada es un par {enable, file} y la familia se infiere de la persona. La cola es editable en vivo desde la GUI web (añadir / eliminar / reordenar) mediante el nuevo panel esp32/webserver/js/cmt.js. Al final de cada programa la cola avanza automáticamente — comportamiento de cinta consecutiva — de modo que el siguiente LOAD encuentra el siguiente programa sin intervención del usuario.

E/S de SD entre Cores y Grabación

Los handlers y el callback de poll se ejecutan en el Core 1 y nunca deben tocar la tarjeta SD directamente. Todas las lecturas (obtener el siguiente MZF a transmitir) y escrituras (guardar una grabación) se publican al Core 0 a través del par cpu->requestQueue / cpu->responseQueue, usando exactamente el patrón flag-en-handler → encolar-en-poll → aplicar-en-task documentado en Interacción entre el Core 0 y el Core 1.
Grabar (save). Con el motor en marcha y el modo de grabación seleccionado, la línea de datos de escritura se muestrea y decodifica sobre la marcha: cada intervalo de semi-onda se clasifica como corto/largo, las secuencias cortas/largas se convierten en bits, los bits se ensamblan en bytes, y se valida la suma de comprobación del bloque. El controlador entonces sintetiza un MZF y se lo entrega al Core 0 para guardarlo en la tarjeta SD como:
CMT/taperecord_<name>_<YYYYMMDD_HHMMSS>.mzf
<name> es el nombre de archivo de la cabecera Sharp transcodificado del código de pantalla Sharp a ASCII y luego saneado para FAT (los caracteres ilegales → _). Cuando el reloj del ESP32 no está establecido, la marca de tiempo recurre a un número de secuencia (taperecord_<name>_0001.mzf) para evitar la colisión de fecha FAT-1980. El nombre Sharp real se almacena literalmente dentro de la cabecera MZF, de modo que el programa conserva su nombre original cuando se recarga.

Conmutador Real ↔ Virtual en Tiempo de Ejecución — CMT, Disquete y QD (Cola de Comandos Inversa)

El conmutador de pletina Virtual / Real en tiempo de ejecución se lanzó primero para el CMT y ahora cubre las tres interfaces de medios extraíbles — CMT, Disquete (FDD) y Quick Disk (QD). En la GUI web el conmutador es un elemento de menú: el del CMT está en el menú CMT, mientras que los de Disquete y QD son elementos de menú disponibles en todas las páginas. Cada modo cambia un flag del controlador en vivo sin reinicio:
  • VIRTUAL — el firmware emula la unidad (imágenes de disco respaldadas en SD o una cinta virtual). Para el CMT esto superpone los bits del casete virtual en la lectura y suprime las escrituras de motor/transporte a cualquier pletina físicamente conectada; para FDD/QD el controlador emulado responde en el bus.
  • REAL — puro paso a través a la unidad/pletina físicamente conectada: el firmware espía en solo lectura y deja que el hardware real responda. El CMT espía la línea de datos de lectura y el motor acciona la pletina real; los accesos FDD/QD llegan al controlador físico.
Una interfaz que está configurada físicamente ahora ARRANCA en modo Real, con el menú y el conmutador disponibles para poder pasarla a Virtual en vivo. Los modos de pletina se comunican de vuelta a la GUI mediante INF — los nuevos campos fddMode / qdMode se unen al modo CMT que INF ya transportaba. Disquete y QD han ganado además cada uno un botón EJECT en su menú de la GUI: una ruta vacía desmonta la unidad, de modo que el FDC informa de que no hay disco.
Cómo está cableado el conmutador. La acción de la GUI viaja por la cola de comandos inversa ESP32 → RP2350 (CP_sendCmd) — la dirección ESP32-a-RP2350 del enlace del procesador de control, el mismo mecanismo que el conmutador del CMT ha usado siempre. Una única tarea DRIVE_MODE_CHANGE lleva la petición al framework de controladores, despachada por los comandos inversos FDDM y QDDM (los análogos para disquete y QD del comando de modo del CMT). En cada cambio de modo el controlador afectado ejecuta su ruta de instalación / desinstalación de hooks — instalando sus handlers memioPtr[] / ioPtr[] cuando toma el control del bus en modo Virtual y desinstalándolos para que los accesos pasen a través en modo Real.
Paso a través del SIO del QD real con ritmo controlado. En modo Real la interfaz QD pasa los accesos al SIO del Z80 directamente a la unidad física, pero ese paso a través tiene un ritmo controlado: se impone un mínimo de recuperación de 10 µs entre accesos en los puertos F4–F7. El SIO Z80 del MZ-1F11 real necesita varios ciclos de reloj de recuperación entre accesos, de modo que el firmware retiene los ciclos de puerto consecutivos hasta ese mínimo en lugar de dejar que el emulador los emita uno tras otro.
Como el flag se conmuta en vivo, puede cargar desde una cinta virtual y luego hacer SAVE a una pletina física, o sustituir una imagen de QD Virtual por el MZ-1F11 real, sin reinicio. El protocolo CMT completo, las tablas de forma de onda y los detalles del códec están documentados en docs/VIRTUAL_CMT_DESIGN.md en el repositorio picoZ80.

Dispositivos Periféricos Virtuales — Interioridades

Un dispositivo periférico virtual es un modelo de chip instanciado a partir de la configuración, en lugar de compilado dentro de una personalidad. El marco reside en src/devices.c y src/include/devices.h; los modelos de chip en sí son controladores ordinarios bajo src/drivers/. Los usuarios declaran instancias en el array device y las cablean en el array interlink — ambos documentados en la Guía Técnica.
Antes de trabajar aquí, entienda la distinción de tres capas. Un modelo de circuito integrado (Z80DMA.c, Z80CTC.c) es una emulación pura del chip, sin ninguna opinión sobre en qué máquina se encuentra. Un dispositivo es una instancia de tal modelo declarada en la configuración, pensada para quien construye su propio sistema. Una tarjeta de interfaz es el mecanismo ya existente drivers[].if[] con pegamento específico de máquina — troceado de ROM, manejo de medios, conmutadores Real/Virtual. Estos son pares que comparten los mismos modelos de circuito integrado, no capas unos de otros; el trabajo de precisión hecho en un modelo de circuito integrado beneficia tanto al dispositivo como a la tarjeta que lo usan.

Registro de Dispositivos

Cada tipo de dispositivo tiene una fila en deviceFuncMap[], en src/devices.c. La fila nombra el chip, suministra el título y la descripción que muestra la interfaz web, declara qué core lo atiende, y apunta a los puntos de entrada del modelo y a su tabla de declaración de señales. Los nombres de dispositivo se comparan sin distinguir mayúsculas de minúsculas.
Cada dispositivo declara además sus pines en una tabla t_devSignalDecl — el nombre del pin, una etiqueta en lenguaje llano, la dirección, si es activo a nivel bajo, su nivel de reposo y si es de drenador abierto. Esta tabla es la única fuente de verdad sobre el pin: el firmware la usa para construir la tabla de señales, y tools/gen_devicecat.py la rastrea en tiempo de compilación para producir el catálogo que lee la interfaz web.

Pines Propiedad del Controlador y Dispositivos Integrados

Dos mecanismos de registro permiten que un controlador persona participe en el framework de dispositivos en lugar de simplemente coexistir con él:
  • Signal_registerPins() — pines propiedad del controlador. Un controlador puede registrar pines propios (por ejemplo, la sub-interfaz de disquete del X1 publica x1fdc.DRQ y x1fdc.INTRQ), y esos pines pasan a ser resolubles por el interlink exactamente como cualquier pin de dispositivo — un usuario puede cablear un chip virtual a ellos en el editor de Conexiones. La inicialización del controlador se ejecuta antes que la configuración de dispositivos, de modo que al volver a aplicar una configuración los pines propiedad del controlador se compactan en lugar de borrarse.
  • devicesRequestBuiltin() / devicesRequestLink() — dispositivos integrados. Un controlador puede declarar hardware que es la máquina — por ejemplo, el DMA de disquete del X1 — como dispositivo integrado. Las declaraciones se inyectan en el árbol de configuración y recorren la misma ruta de instanciación que los dispositivos declarados por el usuario, de modo que hay un único conjunto de comprobaciones de rango y mensajes, y los comandos de shell dev, dev <id> y dev trace funcionan con ellos sin cambios. Una entrada de usuario con el mismo id o dirección prevalece sobre la integrada — pero una entrada de usuario DISABLED no cuenta; de lo contrario, desmarcar un dispositivo en el editor web dejaría la máquina sin hardware alguno.
Nota sobre la compilación Amstrad
En el modelo AmstradZ80 los límites de instancias del framework de dispositivos están recortados (DEVICE_MAX = 1, con pools de señales e interlink reducidos en consecuencia) para liberar SRAM interna para la sombra de DRAM del PCW, de modo que los dispositivos periféricos virtuales no están disponibles en la práctica en ese objetivo. Todos los demás modelos conservan los límites completos.

Señales y Evaluación de la Netlist

Las redes transportan el nivel eléctrico de un pin, nunca un estado abstracto de activación. La polaridad pertenece al dispositivo — el DMA decide qué significa su sentido de listo, la salida de interrupción sabe que es activa a nivel bajo — que es lo que permite que una NOR de dos salidas de temporizador se comporte en la netlist exactamente igual que en un esquema.
La evaluación es por empuje (push). Cuando una salida cambia, se reevalúan los nodos que la consumen y se excitan sus destinos, repitiendo hasta que la netlist se estabiliza o se alcanza el tope de iteraciones. El tope existe para que un par de puertas acopladas en cruz pueda enclavarse — una oscilación genuina se notifica una vez y los niveles quedan fijados. La netlist se estabiliza una vez al final de la configuración, de modo que cada entrada arranque en el nivel que exige su expresión y no en su valor de reposo declarado.
Un dispositivo excita una salida con Signal_drive() y, para los pines bidireccionales, fija la dirección con Signal_setDir(). Un cambio de nivel entrante se entrega al dispositivo a través de su callback de señal. El Z80 PIO trata cada bit de puerto de forma independiente y es el modelo correcto a copiar para un nuevo dispositivo bidireccional.

Reparto entre Cores

El trabajo de dispositivo de marcha libre se ejecuta en el Core 0, de modo que no añade nada a la ruta crítica del Core 1. El Core 1 conserva únicamente lo que debe ser síncrono con el procesador — los handlers de lectura y escritura de registros, y el motor de transferencia del DMA, que acciona el bus y retiene al Z80. El contador/temporizador y el temporizador de intervalo son avanzados por un temporizador del Core 0, que es la razón por la que aparecen marcados como dispositivos del Core 0 en el catálogo.
Importante: el tick del Core 0 solo lo arranca el modelo BaseZ80. Bajo las compilaciones Sharp, Amstrad, Tatung y OpenZ80 los dispositivos contadores son direccionables pero nunca avanzan. Si añade un dispositivo que cuenta, asegúrese de que el tick esté funcionando en el modelo contra el que está probando, o el síntoma será un temporizador que devuelve siempre el mismo valor.
La base de tiempos del contador/temporizador es el reloj de pared, escalado por el reloj del anfitrión configurado, no la cuenta de estados T emulados. Esto es deliberado y coincide con el hardware: la entrada de reloj de un contador real es un cristal, así que sigue contando durante las ráfagas de DMA, los estados de espera y el halt.

La Cadena Daisy de Interrupciones

La prioridad proviene del cableado IEI/IEO y no de un valor de configuración, de modo que la netlist es la única descripción de la máquina. Cada tipo de dispositivo declara cuáles de sus pines son IEI e IEO en su fila de deviceFuncMap[], junto a tres puntos de entrada a los que llama la cadena: uno para informar de si el dispositivo está solicitando o en servicio, otro para reconocer y devolver el vector, y otro para manejar RETI.
El orden de la cadena se deriva de la propia netlist — un nodo que excita el IEI de un dispositivo desde el IEO de otro convierte a ese otro en su predecesor. El IEO se pone alto solo cuando el IEI está alto y el dispositivo ni interrumpe ni está siendo atendido, y en eso consiste toda la regla de prioridad; el interlink se encarga después de propagar el bloqueo cadena abajo.
El hardware externo se resuelve primero. Un chip real en el bus del anfitrión tiene pines IEI/IEO que el firmware no puede ver — no hay ningún pin de procesador libre para ellos — de modo que nunca se puede arbitrar con él. Por eso el marco de dispositivos se encadena a cualquier handler de reconocimiento de interrupción y de RETI instalado previamente, ofreciéndole el reconocimiento antes de considerar sus propios dispositivos. Sin esto, añadir cualquier dispositivo habría roto silenciosamente las tarjetas MZ-8BIO3 y MZ-1E24, que instalan sus propios handlers.
La prueba de conformidad del lado anfitrión cubre el contrato del que depende la cadena: el reconocimiento devuelve el vector programado, limpia el indicador de pendiente y establece el estado en servicio — que es lo que bloquea a los dispositivos de menor prioridad — y RETI vuelve a limpiar el estado en servicio.

Herramientas del Shell de Depuración

La variante de firmware DBGSH proporciona ocho comandos para trabajar con dispositivos, implementados en src/dbgsh_devices.inl. Cada uno acepta un identificador de instancia opcional; sin él, se usa el primer dispositivo de ese tipo.
Comando Propósito
dev Listar los dispositivos configurados con su espacio de direcciones, dirección, tamaño y ajustes de DMA. dev <id> vuelca el estado en vivo de un dispositivo; dev trace muestra el anillo de traza de E/S de dispositivos (2048 entradas), y dev trace raw imprime una línea por acceso — un formato pensado para comparar (diff) con una traza de referencia de otro emulador.
sig Listar todos los pines declarados con su dirección y nivel actual. sig set <dev.pin> <0\|1> fuerza un nivel y lo propaga por la netlist.
dma [id] [force] Volcar los registros de escritura del DMA decodificados, sus direcciones de trabajo, contadores, estado y situación. force ejecuta el motor de transferencia directamente.
ctc [id] Volcar de cada canal el byte de control, el preescalador, la constante de tiempo, el contador descendente y el estado de interrupción, y cuándo vence el siguiente evento.
pio [id] Volcar de cada puerto el modo, el registro de salida, el registro de entrada, la máscara de dirección y el estado de interrupción.
ser [id] [tx <text> \| rx] Estado del dispositivo serie (MC6850 / I8251 / Z80DART). tx inyecta caracteres en el dispositivo, rx lee lo que transmitió el invitado — un diseño serie puede ejercitarse sin ningún terminal conectado. La inyección en el DART es solo por el canal A.
rtc [id] Volcar los registros del MSM5832 decodificados en una fecha y hora legibles.
prn [id] [dump] Estado de la impresora Centronics; dump vacía la cola de impresión al shell en lugar de al canal USB.
sig set es el más útil de todos ellos al poner en marcha un diseño — le permite activar un pin a mano y observar qué hace el resto de la netlist, sin escribir una sola línea de código Z80. Para probar desde el lado del Z80, declare un Signal Port: mapea ocho cables en una única dirección de E/S, de modo que un programa de prueba pueda inyectar estímulos en la netlist y leer la respuesta.

Añadir un Nuevo Modelo de Dispositivo

El procedimiento es:
  1. Escriba el modelo del chip como un controlador ordinario bajo src/drivers/, sin dependencia de ninguna máquina en particular. Modele los registros a partir de la hoja de datos del fabricante, no de otro emulador — los emuladores existentes se desvían del comportamiento publicado con más frecuencia de lo que cabría esperar.
  2. Declare sus pines en una tabla t_devSignalDecl. Escriba las etiquetas pensando en un lector que no conoce el chip; son lo que muestra la interfaz web.
  3. Añada una fila a deviceFuncMap[] nombrando los puntos de entrada de init, lectura, escritura, señal y reinicio del modelo, y elija el core de servicio.
  4. Añada los valores por defecto de empaquetado — espacio de direcciones, tamaño de decodificación y una dirección base sugerida — a la tabla DEFAULTS en tools/gen_devicecat.py, ya que no están presentes en las tablas de C.
  5. Ejecute tools/gen_devicecat.py para regenerar esp32/webserver/js/devicecat.js, y haga commit del archivo regenerado. El catálogo es un artefacto de compilación que puede quedarse obsoleto — un dispositivo o un pin añadido sin regenerarlo no aparecerá en la interfaz web.
  6. Añada una prueba de conformidad del lado anfitrión bajo test/ si el chip tiene comportamientos no evidentes. El modelo del DMA tiene una que compila el controlador con gcc contra un espacio de direcciones simulado y comprueba los resultados contra las propias tablas de la hoja de datos.

Carencias Actuales

Áreas incompletas conocidas
  • Los dispositivos no se reinician con un reinicio del procesador. Existe una función de reinicio, pero nunca se llama; los dispositivos se inicializan una sola vez, cuando se aplica la configuración.
  • El cableado de drenador abierto no es automático. El indicador openDrain se declara por pin, pero no se consulta durante la evaluación, así que un nodo de interrupción de tipo AND cableada debe escribirse con un operador AND explícito.
  • La ruta de entrada del 8255 es destructiva a nivel de bit. Al plegar el nivel de un pin entrante dentro de un puerto se lee primero el registro de salida del puerto, que devuelve cero para un puerto configurado como entrada, de modo que excitar un bit de entrada borra los demás. La ruta de entrada bit a bit del Z80 PIO es el patrón a seguir para corregirlo.
  • Los contadores de instancias no se reinician completamente al reconfigurar. Solo se limpia el conjunto del DMA cuando se vuelve a aplicar la configuración, por lo que reconfigurar repetidamente va perdiendo ranuras de instancia.
  • Las claves mirror, refresh y trace se parsean y almacenan, pero nunca se actúa sobre ellas.
Los documentos de diseño en el repositorio picoZ80 — docs/VIRTUAL_Z80DMA_DESIGN.md, docs/VIRTUAL_Z80CTC_Z80PIO_DESIGN.md y docs/VIRTUAL_DEVICE_GUI_DESIGN.md — recogen la derivación completa, las referencias a las hojas de datos y el razonamiento tras cada decisión. Cuando un documento de diseño y el código discrepen, manda el código; varias partes de esos documentos describen trabajo que aún no se ha construido.

Escribir un Nuevo Controlador — Paso a Paso

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

Paso 1 — Crear los Archivos Fuente

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Paso 2 — Añadir a CMakeLists.txt

Abra src/CMakeLists.txt y añada su nuevo archivo fuente a la lista de controladores Sharp:
# src/CMakeLists.txt — add your file to the Sharp driver list

set(pZ80_drivers_sharp_src
    ${CMAKE_CURRENT_LIST_DIR}/drivers/Sharp/MZ700.c
    ${CMAKE_CURRENT_LIST_DIR}/drivers/Sharp/RFS.c
    ${CMAKE_CURRENT_LIST_DIR}/drivers/Sharp/WD1773.c
    ${CMAKE_CURRENT_LIST_DIR}/drivers/Sharp/QDDrive.c
    ${CMAKE_CURRENT_LIST_DIR}/drivers/Sharp/MZ-1E05.c
    ${CMAKE_CURRENT_LIST_DIR}/drivers/Sharp/MZ8BFI.c
    ${CMAKE_CURRENT_LIST_DIR}/drivers/Sharp/MZ-1E14.c
    ${CMAKE_CURRENT_LIST_DIR}/drivers/Sharp/MZ-1E19.c
    ${CMAKE_CURRENT_LIST_DIR}/drivers/Sharp/MZ-1R12.c
    ${CMAKE_CURRENT_LIST_DIR}/drivers/Sharp/MZ-1R18.c
    # ADD YOUR DRIVER HERE:
    ${CMAKE_CURRENT_LIST_DIR}/drivers/Sharp/MyDriver.c
)

# Amstrad driver list (compiled when INCLUDE_AMSTRAD_DRIVERS is set)
set(pZ80_drivers_amstrad_src
    ${CMAKE_CURRENT_LIST_DIR}/drivers/Amstrad/PCW9512.c
    ${CMAKE_CURRENT_LIST_DIR}/drivers/Amstrad/uPD765.c
)
También debe añadir el directorio de su cabecera al include path si está en un subdirectorio nuevo. Para el directorio de controladores Sharp esto ya está configurado, así que no se necesita una llamada adicional a target_include_directories.

Paso 3 — Incluir la Cabecera en Z80CPU.c

Abra src/Z80CPU.c y añada un include para la cabecera de su controlador junto a los includes existentes de controladores Sharp:
// src/Z80CPU.c — near the top with other driver includes
#ifdef INCLUDE_SHARP_DRIVERS
#include "drivers/Sharp/MZ700.h"
#include "drivers/Sharp/RFS.h"
#include "drivers/Sharp/WD1773.h"
#include "drivers/Sharp/QDDrive.h"
#include "drivers/Sharp/MZ-1E05.h"
#include "drivers/Sharp/MZ8BFI.h"
#include "drivers/Sharp/MZ-1E14.h"
#include "drivers/Sharp/MZ-1E19.h"
#include "drivers/Sharp/MZ-1R12.h"
#include "drivers/Sharp/MZ-1R18.h"
// ADD YOUR INCLUDE:
#include "drivers/Sharp/MyDriver.h"
#endif

#ifdef INCLUDE_AMSTRAD_DRIVERS
#include "drivers/Amstrad/PCW9512.h"
#include "drivers/Amstrad/uPD765.h"
#endif

Paso 4 — Registrar en virtualFuncMap

Aún en src/Z80CPU.c, encuentre el array virtualFuncMap[] y añada su entrada. La cadena "MyDriver" es lo que el campo JSON "name" debe contener:
// src/Z80CPU.c
static const t_VirtualFuncMap virtualFuncMap[] = {
#ifdef INCLUDE_SHARP_DRIVERS
    {"MZ700",    MZ700_Init},
    {"MZ80A",    MZ80A_Init},
    {"MZ2000",   MZ2000_Init},
    {"MZ2200",   MZ2200_Init},
    {"MZ80B",    MZ80B_Init},
    {"MZ2500",   MZ2500_Init},
    {"MZ800",    MZ800_Init},
    {"MZ1500",   MZ1500_Init},
#endif
#ifdef INCLUDE_AMSTRAD_DRIVERS
    {"PCW9512",  PCW9512_Init},
#endif
#ifdef INCLUDE_TATUNG_DRIVERS
    {"EinsteinTC01", EinsteinTC01_Init},
#endif
    // ADD YOUR DRIVER:
    {"MyDriver", MyDriver_Init},
};
La búsqueda no distingue entre mayúsculas y minúsculas, así que "mydriver", "MyDriver" y "MYDRIVER" en el JSON coincidirán con esta entrada.

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

Añada una entrada "drivers" a su config.json en la tarjeta SD. El campo "name" debe coincidir con la cadena que registró en virtualFuncMap[]:
{
  "rp2350": {
    "core": { "cpufreq": 300000000, "psramfreq": 133000000, "voltage": 1.10 },
    "z80": [
      {
        "memory": [
          { "enable": 1, "addr": "0x0000", "size": "0x1000",
            "type": "ROM", "bank": 0, "tcycwait": 0, "tcycsync": 0,
            "task": "", "file": "/ROM/mz700.rom", "fileofs": 0 },
          { "enable": 1, "addr": "0x1000", "size": "0xCFFF",
            "type": "RAM", "bank": 0, "tcycwait": 1, "tcycsync": 1,
            "task": "", "file": "", "fileofs": 0 }
        ],
        "io": [],
        "drivers": [
          {
            "enable": 1,
            "name":   "MZ700",
            "type":   "VIRTUAL",
            "if": []
          },
          {
            "enable": 1,
            "name":   "MyDriver",
            "type":   "VIRTUAL",
            "if": []
          }
        ]
      }
    ]
  }
}

Paso 6 — Compilar y Probar

# From the project root
./build_tzpuPico.sh DEBUG

# Firmware output:
# build/bin/model/BaseZ80/BaseZ80_0x10020000.elf   (debug ELF — use with GDB)
# build/bin/model/BaseZ80/BaseZ80_0x10020000.bin   (OTA binary)

# Flash via OTA web page, or debug directly:
openocd -f interface/cmsis-dap.cfg -f target/rp2350_tzpu.cfg -c "adapter speed 5000" &
cd build/bin/model/BaseZ80
gdb-multiarch BaseZ80_0x10020000.elf
(gdb) break MyDriver_Init
(gdb) continue
El sistema de compilación produce binarios de firmware de aplicación para cada objetivo de modelo — dos particiones (Partición 1 en 0x10020000, Partición 2 en 0x10520000) cada una en variantes estándar y DBGSH. Están disponibles cinco objetivos de modelo Z80:
  • BaseZ80 (pZ80-BaseZ80) — binario universal con todos los controladores (Sharp + Amstrad + Tatung). Define INCLUDE_SHARP_DRIVERS, INCLUDE_AMSTRAD_DRIVERS e INCLUDE_TATUNG_DRIVERS.
  • SharpZ80 (pZ80-SharpZ80) — solo controladores Sharp MZ. Define INCLUDE_SHARP_DRIVERS. Produce un binario de firmware más pequeño.
  • AmstradZ80 (pZ80-AmstradZ80) — solo controladores Amstrad PCW. Define INCLUDE_AMSTRAD_DRIVERS y TARGET_MODEL_AMSTRAD. Produce un binario de firmware más pequeño.
  • TatungZ80 (pZ80-TatungZ80) — solo controladores Tatung Einstein. Define INCLUDE_TATUNG_DRIVERS y TARGET_MODEL_TATUNG. Produce un binario de firmware más pequeño.
  • OpenZ80 (pZ80-OpenZ80) — solo la persona vainilla / de experimentación OpenZ80, con las tarjetas de interfaz independientes de la máquina. Define TARGET_MODEL_OPEN.
El mismo árbol de código fuente también compila el modelo Base6502 — el firmware de pico6502 — a partir de la capa de soporte compartida neutral respecto a la CPU; consulte la Guía del Desarrollador de pico6502. Cada objetivo de modelo tiene su propio directorio bajo src/model/ con un CMakeLists.txt dedicado, punto de entrada (main.c) y scripts de enlazador. La variante estándar omite el shell de depuración para una imagen de firmware más pequeña. La variante DBGSH añade la definición de compilación INCLUDE_DBGSH, que habilita el shell de depuración ICE completo en USB CDC Channel 1. Los nombres de firmware DBGSH se identifican por el sufijo _DBGSH.

Firmware ESP32 — Selección de Modo de Red

Antes de compilar el firmware ESP32, seleccione el modo de red deseado copiando el sdkconfig pre-construido apropiado en su lugar:
# Select networking mode before building ESP32 firmware:
cp sdkconfig.mode_ncm_only sdkconfig       # NCM only (no WiFi, FCC/RED safe)
# cp sdkconfig.mode_wifi_only sdkconfig     # WiFi only
# cp sdkconfig.mode_wifi_and_ncm sdkconfig  # Both WiFi and NCM
idf.py build
Las definiciones clave del preprocesador controladas por estas configuraciones son:
  • CONFIG_IF_WIFI_ENABLED — habilita la radio WiFi y el código AP/Client.
  • CONFIG_IF_USB_NCM_ENABLED — habilita la interfaz de red USB NCM y el servidor DHCP.
Estas se configuran mediante Kconfig (idf.py menuconfig) o los archivos sdkconfig pre-construidos listados arriba.

Patrones de Hooks de Memoria en Detalle

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

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

Utilice esto cuando quiera que una región del espacio de direcciones del Z80 sea completamente controlada por su handler, sin respaldo de PSRAM. Las lecturas y escrituras del Z80 siempre llaman a su función. Nada se almacena en la PSRAM.
// Map 0xC000-0xCFFF as a pure virtual device in bank 4
int startBlock = 0xC000 / MEMORY_BLOCK_SIZE;  // = 96
int endBlock   = 0xD000 / MEMORY_BLOCK_SIZE;  // = 104

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

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

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

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

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

Utilice esto cuando quiera que una región se comporte como RAM normal (las lecturas retornan datos de PSRAM, las escrituras actualizan la PSRAM) pero también quiera ser notificado de las escrituras — por ejemplo, para espejar escrituras de video RAM a un buffer sombra o para activar una actualización de hardware. El tipo de bloque permanece como RAM; instale un handler memioPtr que post-procese la escritura.
// Map 0xD000-0xD7FF as RAM but intercept all writes
int startBlock = 0xD000 / MEMORY_BLOCK_SIZE;
int endBlock   = 0xD800 / MEMORY_BLOCK_SIZE;

for(int idx = startBlock; idx < endBlock; idx++)
{
    cpu->_membankPtr[idx] = (MEMBANK_TYPE_RAM << 24)
                          | (MY_VRAM_BANK     << 16)
                          | (idx * MEMORY_BLOCK_SIZE);
    cpu->_memAttr[MY_VRAM_BANK][idx].waitStates = 2;
    cpu->_memAttr[MY_VRAM_BANK][idx].tCycSync   = true;
}

// Only install handler for write-intercept addresses
for(uint32_t addr = 0xD000; addr < 0xD800; addr++)
    cpu->_z80PSRAM->memioPtr[addr] = (MemoryFunc)MyVRAM_WriteIntercept;

// Handler — called by Z80CPU_writeMem() for RAM type with a handler:
// The returned value is what gets stored in PSRAM (not the original 'data').
uint8_t MyVRAM_WriteIntercept(Z80CPU *cpu, bool read, uint16_t addr, uint8_t data)
{
    if(read)
    {
        // On read: 'data' already contains the PSRAM value — just return it
        return data;
    }
    else
    {
        // On write: update our shadow copy, then return 'data' so PSRAM is also updated
        uint16_t vramOffset = addr - 0xD000;
        myVRAMShadow[vramOffset] = data;
        markDirty(vramOffset);     // e.g. signal renderer that this cell changed
        return data;  // PSRAM is written with the returned value
    }
}

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

Algunos hardware utilizan escrituras a direcciones mapeadas como ROM como escrituras de registro de banking (la escritura es "decodificada" por el hardware pero no modifica la ROM). El bloque permanece como ROM; las escrituras activan su handler pero la PSRAM no se modifica.
// ROM at 0x0000-0x0FFF, but writes to 0x0000-0x001F are banking registers
for(int idx = 0; idx < (0x1000 / MEMORY_BLOCK_SIZE); idx++)
{
    cpu->_membankPtr[idx] = (MEMBANK_TYPE_ROM << 24)
                          | (ROM_BANK         << 16)
                          | (idx * MEMORY_BLOCK_SIZE);
}

// Install write-trap handler only for addresses 0x0000-0x001F
for(uint32_t addr = 0x0000; addr < 0x0020; addr++)
    cpu->_z80PSRAM->memioPtr[addr] = (MemoryFunc)MyROM_WriteTrap;

// Handler:
uint8_t MyROM_WriteTrap(Z80CPU *cpu, bool read, uint16_t addr, uint8_t data)
{
    if(read)
    {
        // Return ROM data — 'data' already contains PSRAM value at this address
        return data;
    }
    else
    {
        // Write to ROM address — treat as banking register write
        MyBankSwitch(cpu, addr, data);
        // Return value is ignored for ROM writes (PSRAM not written)
        return data;
    }
}

Patrón 4 — Handlers Dispersos (Direcciones Individuales)

No tiene que instalar handlers para un bloque completo. Puede instalar un handler en una única dirección específica dentro de un bloque RAM o ROM. El tipo de bloque controla lo que sucede con todas las demás direcciones del bloque; el handler de dirección específica anula solo esa dirección.
// The block containing 0x1234 is configured as RAM
// We want 0x1234 specifically to call a handler on write only

cpu->_z80PSRAM->memioPtr[0x1234] = (MemoryFunc)MySpecialHandler;

uint8_t MySpecialHandler(Z80CPU *cpu, bool read, uint16_t addr, uint8_t data)
{
    if(read)
        return data;  // Normal RAM read — return PSRAM value
    else
    {
        // Special action on write to 0x1234
        triggerSomething(data);
        return data;  // Write data to PSRAM as normal
    }
}

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

Los handlers de E/S son más simples — no hay respaldo de PSRAM para puertos de E/S. El handler se llama (si está instalado) o el ciclo de E/S va al hardware físico. No existe el concepto de tipo de bloque.
// Install handler for I/O ports 0x80-0x8F (16 ports)
for(int port = 0x80; port <= 0x8F; port++)
    cpu->_z80PSRAM->ioPtr[port] = (MemoryFunc)MyIO_Handler;

uint8_t MyIO_Handler(Z80CPU *cpu, bool read, uint16_t addr, uint8_t data)
{
    uint8_t port = (uint8_t)(addr & 0xFF);   // Actual port number
    // uint8_t regB = (uint8_t)(addr >> 8);  // Register B during IN r,(C) / OUT (C),r

    if(read)
    {
        switch(port)
        {
            case 0x80: return myDevice.status;
            case 0x81: return myDevice.rxData;
            default:   return 0xFF;
        }
    }
    else
    {
        switch(port)
        {
            case 0x80: myDevice.control = data; applyControl(); break;
            case 0x82: myDevice.txData  = data; sendByte(data); break;
        }
        return 0;
    }
}

Añadir una Sub-Interfaz a una Persona Existente

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

Funciones Requeridas para una Sub-Interfaz

Una sub-interfaz necesita cuatro funciones con estas firmas:
// Called once during MZ700_Init when this interface name appears in the JSON "if" array
uint8_t MyCard_Init(Z80CPU *cpu,
                    t_FlashAppConfigHeader *appConfig,
                    t_drvIFConfig *ifConfig);   // Note: t_drvIFConfig, not t_drvConfig

// Called on RESET
uint8_t MyCard_Reset(Z80CPU *cpu);

// Called every ~2048 Z80 cycles (must be very fast)
uint8_t MyCard_PollCB(Z80CPU *cpu);

// Called for inter-core task delivery
uint8_t MyCard_TaskProcessor(Z80CPU *cpu, enum Z80CPU_TASK_NAME task, char *param);

Registrar la Sub-Interfaz

Añada su sub-interfaz al interfaceFuncMap[] en el archivo C de la persona padre (por ejemplo, MZ700.c):
// src/drivers/Sharp/MZ700.c — add to interfaceFuncMap[]
#include "drivers/Sharp/MyCard.h"  // Add this include at top of MZ700.c

static t_InterfaceFuncMap interfaceFuncMap[] = {
    {"RFS",     false, RFS_Init,    RFS_Reset,    RFS_PollCB,    RFS_TaskProcessor},
    {"MZ-1E05", false, MZ1E05_Init, MZ1E05_Reset, MZ1E05_PollCB, MZ1E05_TaskProcessor},
    // ... existing entries ...
    // ADD YOUR SUB-INTERFACE:
    {"MyCard",  false, MyCard_Init, MyCard_Reset, MyCard_PollCB, MyCard_TaskProcessor},
};
La cadena "MyCard" debe coincidir con el campo "name" de la entrada de interfaz en el array JSON "if" (sin distinción de mayúsculas/minúsculas):
"drivers": [
  {
    "enable": 1,
    "name":   "MZ700",
    "type":   "VIRTUAL",
    "if": [
      {
        "enable": 1,
        "name":   "MyCard",
        "type":   "VIRTUAL",
        "rom":    [],
        "addrmap": [],
        "iomap":   [],
        "param": [
          { "name": "myParam", "value": "42" }
        ]
      }
    ]
  }
]

Leer Parámetros JSON en una Sub-Interfaz

El t_drvIFConfig *ifConfig pasado a la init de su sub-interfaz contiene todos los datos configurados por JSON. Para leer un parámetro con nombre:
uint8_t MyCard_Init(Z80CPU *cpu,
                    t_FlashAppConfigHeader *appConfig,
                    t_drvIFConfig *ifConfig)
{
    // Read a named parameter from the "param" array
    int myParamValue = 0;

    for(int p = 0; p < ifConfig->ifParamCount; p++)
    {
        if(strcasecmp(ifConfig->ifParam[p].name, "myParam") == 0)
        {
            myParamValue = atoi(ifConfig->ifParam[p].value);
            break;
        }
    }

    // Load ROM images listed in the "rom" array
    for(int r = 0; r < ifConfig->romCount; r++)
    {
        t_drvROMConfig *rom = &ifConfig->romConfig[r];

        if(rom->file != NULL && rom->file[0] != '\0')
        {
            // Load ROM from SD card into PSRAM bank at the configured address
            uint32_t bankBase = rom->bank * MEMORY_PAGE_SIZE;
            Z80CPU_ReadROM(appConfig, rom->file, NULL, NULL, NULL,
                           &cpu->_z80PSRAM->RAM[bankBase + rom->addr],
                           rom->size, rom->fileofs);

            // Set up membankPtr for this ROM region
            int startBlock = rom->addr / MEMORY_BLOCK_SIZE;
            int endBlock   = (rom->addr + rom->size) / MEMORY_BLOCK_SIZE;
            for(int idx = startBlock; idx < endBlock; idx++)
            {
                cpu->_membankPtr[idx] = (MEMBANK_TYPE_ROM << 24)
                                      | (rom->bank        << 16)
                                      | (idx * MEMORY_BLOCK_SIZE);
                cpu->_memAttr[rom->bank][idx].waitStates = rom->waitStates;
                cpu->_memAttr[rom->bank][idx].tCycSync   = rom->tCycSync;
            }
        }
    }

    // Install I/O handlers from the "iomap" array
    for(int m = 0; m < ifConfig->ioMapCount; m++)
    {
        t_ioReMap *iomap = &ifConfig->ioMap[m];

        // Look up the named handler function
        MemoryFunc handler = Z80CPU_getMemoryFunc(iomap->funcName);
        if(handler != NULL)
        {
            for(uint32_t port = iomap->srcaddr;
                port < iomap->srcaddr + iomap->size; port++)
            {
                cpu->_z80PSRAM->ioPtr[port] = handler;
            }
        }
    }

    return 1;
}

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

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

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

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

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

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

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

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

Puente USB. El canal A se puentea a USB CDC 2 (VSER_CHANNEL_A) y el canal B a USB CDC 3 (VSER_CHANNEL_B). Estos dos puertos CDC no tienen ninguna UART física de respaldo; en su lugar pollUSBtoUART() en el Core 0 llama a SIOCard_usbPump() para trasladar bytes entre los FIFOs CDC y los anillos de canal del SIO (host → anillo Rx, anillo Tx → host). La tarjeta expone los cuatro callbacks estándar de sub-interfaz — SIOCard_Reset, SIOCard_PollCB, SIOCard_TaskProcessor — y está registrada en el interfaceFuncMap[] de cada persona con capacidad SIO (MZ-700, MZ-800, MZ-80B, MZ-1500) como, por ejemplo:

// In each SIO-capable persona's interfaceFuncMap[]:
{"MZ-8BIO3", false, MZ8BIO3_Init, SIOCard_Reset, SIOCard_PollCB, SIOCard_TaskProcessor},
{"MZ-1E24",  false, MZ1E24_Init,  SIOCard_Reset, SIOCard_PollCB, SIOCard_TaskProcessor},
Ambas tarjetas siguen el contrato estándar de controlador de interfaz descrito arriba — carecen de ROM (sin entradas "rom"), instalan solo handlers de E/S y callbacks, y son seleccionadas por la persona que las lista en su interfaceFuncMap[]. En el lado de la configuración web se anuncian a través de configgui.js (el mapa driverInterfaces y la tabla interfaceRomLimits, donde llevan un conteo de ROM de cero).

Interacción Core 0 / Core 1

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

Uso de la Cola Intercore

El patrón es:
  1. Su handler (en el Core 1) detecta que se necesita una operación de E/S de archivo o similar (por ejemplo, el Z80 escribió un número de sector en un registro de comando de disco).
  2. El handler establece un flag de estado (por ejemplo, diskState.pendingRead = true) y retorna inmediatamente — no realiza la E/S.
  3. Su poll_ptr (también en el Core 1, llamado cada ~2048 ciclos) verifica el flag de estado y, si está establecido, introduce un mensaje de solicitud en cpu->requestQueue.
  4. El Core 0 recibe el mensaje, realiza la E/S de archivo (por ejemplo, lee un sector de disco desde la tarjeta SD), e introduce el resultado de vuelta en cpu->responseQueue.
  5. Su task_ptr es llamado (en el Core 1) con el resultado de la tarea. Copia los datos del sector en la PSRAM y limpia el flag pendiente.
// In your I/O handler (Core 1 — must be fast):
uint8_t MyDisk_IOHandler(Z80CPU *cpu, bool read, uint16_t addr, uint8_t data)
{
    if(!read && (addr & 0xFF) == 0xFE)
    {
        // Z80 issued a read sector command
        diskState.pendingSector = data;
        diskState.pendingRead   = true;
        // Return immediately — do NOT call file I/O here
    }
    return 0;
}

// In your poll handler (Core 1 — fast check only):
uint8_t MyDisk_PollCB(Z80CPU *cpu)
{
    if(diskState.pendingRead)
    {
        // Build a request and push to Core 0
        t_intercoreMsg msg = {
            .taskId  = TASK_READ_SECTOR,
            .param1  = diskState.pendingSector,
            .dataPtr = diskState.sectorBuffer,
        };
        if(queue_try_add(&cpu->requestQueue, &msg))
            diskState.pendingRead = false;  // Request sent
    }
    return 0;
}

// In your task processor (Core 1 — called when Core 0 has completed the task):
uint8_t MyDisk_TaskProcessor(Z80CPU *cpu, enum Z80CPU_TASK_NAME task, char *param)
{
    if(task == TASK_READ_SECTOR)
    {
        // Sector data is now in diskState.sectorBuffer
        // Copy into PSRAM at the DMA transfer address
        uint32_t dmaAddr = DISK_BUFFER_BANK * MEMORY_PAGE_SIZE + diskState.dmaAddr;
        memcpy(&cpu->_z80PSRAM->RAM[dmaAddr],
               diskState.sectorBuffer, SECTOR_SIZE);

        // Signal the Z80 that data is ready (e.g. set a status flag in a FUNC register)
        diskState.statusReg |= 0x01;  // DRQ bit
    }
    return 0;
}

Errores Comunes

  • Bloquear en un handler. El error más común. Cualquier llamada a debugf, sleep_ms, fopen, o cualquier función UART desde dentro de un handler o callback de poll detendrá el Core 1 y causará que el Z80 del host vea temporización de bus incorrecta. Mueva todas las operaciones bloqueantes al Core 0 a través de la cola de solicitudes. Use plogf() en lugar de debugf() para el registro de la ruta de arranque que debe funcionar antes de que USB esté disponible (véase Registro de Depuración).
  • Orden incorrecto de cierre en el registro de handlers. Al instalar handlers en un rango usando un bucle, asegúrese de que los límites del bucle usen < y no <= para la dirección final — los errores de uno pueden corromper handlers adyacentes.
  • Olvidar limpiar handlers al apagar o resetear el controlador. Si su handler de reset no limpia las ranuras memioPtr[] o ioPtr[] que su controlador instaló, esos handlers seguirán siendo llamados después del reset, con estado potencialmente obsoleto.
  • Colisión de número de banco. Cada banco PSRAM es de 64KB. La configuración JSON asigna bancos a regiones de memoria. Si dos controladores usan el mismo número de banco, sobrescribirán los datos del otro. Use números de banco únicos para cada controlador. Los bancos 0–7 son típicamente usados por la persona MZ-700; use bancos 8+ para sub-interfaces y controladores adicionales.
  • MEMBANK_TYPE_FUNC sin handler instalado. Si establece un bloque a tipo FUNC pero no instala un handler memioPtr, las lecturas retornarán 0x00 y las escrituras se descartarán silenciosamente. Este es un comportamiento válido pero a menudo es un bug — siempre instale el handler antes de establecer el tipo de bloque.
  • Nombre de virtualFuncMap y nombre JSON no coinciden. La búsqueda no distingue mayúsculas/minúsculas pero la cadena debe coincidir exactamente en lo demás. Un error tipográfico en cualquiera de las ubicaciones causará que el controlador sea silenciosamente omitido sin mensaje de error. Añada una llamada temporal a debugf en Z80CPU_getVirtualFunc() si su controlador no se está inicializando — debugf es una macro que se puede deshabilitar o limitar en builds de producción para que no impacte la temporización del bus.
  • Escanear la cola de respuestas incorrectamente. cpu->responseQueue es compartida por todos los controladores. Escanee como máximo queue_get_level() entradas, vuelva a poner los mensajes que no son suyos en orden y siga adelante — nunca devuelva un mensaje ajeno y se detenga. Dos controladores que hagan eso en orden opuesto (CMT y RFS lo hacían) se bloquean mutuamente de forma permanente. Compare context con el propio puntero a la estructura de control: RFS en su día lo comparaba con la dirección de la variable puntero, nunca veía sus propias finalizaciones y colgaba todas las cargas de cinta en la persona RFS.
  • malloc en la ruta caliente del Core 1. Una asignación de 64KB toma el mutex del heap durante milisegundos y detiene el refresco de DRAM en los anfitriones refrescados mediante /RFSH — visible como una línea de log "Z80 SLOW SLICE". Preasigne los buffers en la función init del controlador (CMT ahora lo hace).
  • Olvidar establecer reset_ptr / poll_ptr / task_ptr. Si no asigna estos en su función init, el Core 1 nunca llamará a sus funciones de reset, poll o task. El controlador se inicializará correctamente pero no responderá a RESET ni realizará ningún mantenimiento periódico.

Registro de Depuración

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

debugf() — Salida de Depuración con Buffer

debugf() es la macro principal de salida de depuración. Funciona como printf() pero escribe en un buffer de 64KB en PSRAM (en la dirección 0x117EF004) en lugar de directamente a un puerto serial. El buffer se vacía a USB CDC cuando el bucle principal tiene tiempo libre. Un mutex (debugMutex) protege el buffer del acceso concurrente de ambos cores.
// src/include/debug.h

// Primary debug output — mutex-protected, buffered in PSRAM, flushed to USB
debugf("Boot stage %d reached, PSRAM size = %d bytes\n", stage, psramSize);

// Single character / string output (also mutex-protected)
debug_putchar('.');
debug_puts("Init complete\n");
Propiedades clave:
  • Tamaño del buffer: 64KB (MAX_DEBUG_BUFFER_SIZE = 65536) en PSRAM.
  • Thread-safe: protegido por mutex para acceso multi-core.
  • Residente en PSRAM: el buffer sobrevive a resets del watchdog (la PSRAM retiene datos), pero el puntero del buffer en 0x117EF004 debe ser revalidado después del reset.
  • Nunca llame desde handlers o callbacks de poll del Core 1. La adquisición del mutex puede bloquear, introduciendo jitter en la temporización del bus. Use plogf() para mensajes de la ruta de arranque o envíe solicitudes de depuración al Core 0 a través de la cola intercore.

plogf() — Log Persistente de Arranque en PSRAM

plogf() escribe en una región dedicada de 4KB al final de la PSRAM de 8MB (dirección 0x117FF000). A diferencia de debugf(), no usa mutex y está destinada solo para el registro de la ruta de arranque del Core 0 — capturando mensajes durante las etapas críticas de arranque temprano antes de que USB esté disponible para la salida de depuración normal. Dado que la PSRAM retiene su contenido entre resets del watchdog, estos mensajes sobreviven a un fallo y pueden examinarse en el siguiente arranque exitoso.
// src/include/debug.h

// PSRAM persistent log structure (at 0x117FF000)
#define PLOG_ADDR   0x117FF000
#define PLOG_SIZE   3840          // 4KB minus 256 bytes reserved for fault diagnostics
#define PLOG_MAGIC  0x504C4F47    // "PLOG"

typedef struct {
    uint32_t  magic;              // PLOG_MAGIC if buffer contains valid data
    uint32_t  len;                // Current write position in buf[]
    char      buf[PLOG_SIZE - 8]; // Circular text buffer
} t_PsramLog;

// Write to persistent log (no mutex — Core 0 boot path only)
plogf("FSPI init: DMA TX=%d RX=%d\n", gDmaTx, gDmaRx);

// Dump captured log on next successful boot (called from main loop)
dump_plog();  // Outputs plog contents via debugf(), then clears the buffer
Patrón de uso típico: Durante el arranque, llame a plogf() en cada hito crítico (init de PSRAM, handshake SPI, análisis de configuración). Si el watchdog se dispara, el buffer de plog retiene todos los mensajes hasta el punto de cuelgue. En el siguiente arranque exitoso, dump_plog() imprime los mensajes capturados antes de limpiar el buffer, proporcionando una traza clara de lo que sucedió antes del fallo.

Elegir la Salida de Depuración Correcta

Macro Ubicación Mutex Sobrevive reset WDT Seguro desde Core 1 Caso de uso
debugf() PSRAM (buffer de 64KB) 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

Monitor de Paradas de Ejecución

Una máquina que parece muerta tiene dos explicaciones muy distintas: la emulación ha dejado de ejecutar (un fallo del firmware), o el Z80 emulado sigue ejecutándose pero ejecuta código corrupto o en bucle (un fallo del lado de la máquina). El monitor de paradas de ejecución existe para separar ambas sin tocar la ruta caliente de la emulación. Un temporizador repetitivo en el Core 0 muestrea el contador emulLoopCount que el Core 1 ya mantiene — una carga de 32 bits es atómica en esta arquitectura, así que el Core 1 no paga nada — y clasifica los huecos entre incrementos. La API es Z80CPU_stallMonStart() / Z80CPU_stallMonReset() / Z80CPU_stallMonPoll(), solo en el Core 0; el poll emite desde el bucle principal del Core 0, nunca desde la interrupción del temporizador.
En el log de depuración aparecen tres tipos de informe:
  • Z80 SLOW SLICE — una porción de ejecución tardó notablemente más que la línea base en curso. La redacción "execution slower than usual, not halted" es deliberada: la máquina sigue funcionando, pero algo (el servicio de la SD, paradas de XIP, una ruta pesada de un controlador) estiró una porción.
  • Resumen de ventana de 30 segundos — recuentos de huecos agrupados en >4 ms, >10 ms y >50 ms con el peor hueco observado. Solo se emite cuando algo ha ocurrido; una máquina sana permanece en silencio.
  • Z80 FROZEN: no execution ... the machine has not — ha pasado más de un segundo sin ningún progreso. Esta es la división diagnóstica: si la ejecución se detuvo, el fallo está en el firmware; si esta línea nunca se dispara mientras la máquina parece muerta, el Z80 está ejecutando código corrupto y el fallo es del lado de la máquina.
En la compilación Amstrad el informe de congelación imprime además contexto forense de interrupciones (el último anillo de aceptación de interrupciones), que es como se localizaron varios fallos de temporización del PCW.

Watchdog y Seguimiento de Progreso de Arranque

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

Timer Watchdog

El watchdog de hardware del RP2350 se habilita temprano en main() con un timeout de 30 segundos:
// src/model/BaseZ80/main.c
watchdog_enable(30000, true);  // 30s timeout, pause-on-debug enabled
watchdog_update() se llama en cada hito de arranque y a lo largo del bucle principal. Las operaciones de larga duración críticas (cargas de imágenes de disco con lógica de reintentos, transferencias DMA, handshake SPI del ESP32) incluyen actualizaciones explícitas del watchdog para prevenir resets espurios durante operaciones lentas legítimas:
// Floppy disk load with retry logic — kick watchdog between attempts
for (int attempt = 0; attempt < 10; attempt++)
{
    watchdog_update();
    bytesXfer = ESP_readFloppyDiskFile(filename, ..., diskNo);
    if (bytesXfer > 0) break;
    watchdog_update();
    sleep_ms(500);
}

// QD disk change — kick watchdog while waiting for Core 1 hold acknowledge
z80CPU->hold = true;
for (int w = 3000; !z80CPU->holdAck && w > 0; w--)
{
    sleep_ms(1);
    if ((w % 1000) == 0) watchdog_update();
}

Registros Scratch del Watchdog

El RP2350 proporciona ocho registros scratch de 32 bits en el bloque de hardware del watchdog que sobreviven a resets del watchdog pero se borran en reset de encendido. El firmware picoZ80 usa cinco de estos para mantener un historial completo de diagnóstico de arranque:
// src/model/BaseZ80/main.c

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

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

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

// Usage throughout boot sequence:
bootStage(BOOTP_START);         // 0x01 — entry point
// ... clock setup ...
bootStage(BOOTP_CLK_SET);       // 0x02
// ... PSRAM init ...
bootStage(BOOTP_PSRAM_INIT);    // 0x03
watchdog_update();
bootStage(BOOTP_PSRAM_OK);      // 0x04
// ... and so on through BOOTP_MAIN_LOOP (0x10)
En cada reset del watchdog, la etapa actual y la causa del reset se desplazan al FIFO de historial (scratch[0–3]) antes de ser sobrescritas. Esto le da los últimos cuatro intentos de reset, haciendo posible distinguir un fallo puntual de un fallo de arranque repetido en una etapa específica.

Referencia de Etapas de Arranque

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

Shell de Depuración ICE (dbgsh.c)

El shell de depuración (dbgsh.c / dbgsh.h) implementa un depurador ICE de 63 comandos en USB CDC Channel 1 (las dos incorporaciones más recientes son disdesc, que añade una descripción de una línea a cada mnemónico desensamblado a través del hook de CPU opcional mnemonicDesc(), y esppoll on|off, que controla el sondeo periódico NOP/INF del ESP32 para la bisección de fallos). El shell es ahora neutral respecto a la CPU: un único dbgsh.c sirve tanto al picoZ80 como al pico6502, despachando las operaciones de registros, desensamblado y bus a través de una vtable t_CpuOps por CPU (src/include/cpu.h), con los comandos exclusivos del Z80 excluidos de la compilación del 6502. Se ejecuta en el Core 0 y se comunica con el Core 1 a través de flags compartidos en la estructura de contexto t_Z80CPU:
  • cpu->hold / cpu->holdAck — handshake de pausa/reanudación entre el shell del Core 0 y el bucle de emulación del Core 1.
  • cpu->dbgBpAddr[DBG_MAX_BP] — array de direcciones de breakpoint (8 ranuras, 0xFFFF = no utilizada). El Core 1 verifica antes de cada búsqueda de opcode.
  • cpu->dbgStepCount — contador de paso único. El Core 1 decrementa después de cada instrucción y se auto-detiene en cero.
  • cpu->dbgTrace[DBG_TRACE_SZ] — buffer circular de 512 entradas que registra [31:16]=PC, [15:8]=opcode, [7:0]=F para cada instrucción ejecutada.
Patrones de implementación clave: el shell usa dbg_sprintf() (un printf ligero residente en RAM) para evitar bloqueos de flash XIP al imprimir desde el Core 0 mientras el Core 1 está usando activamente la PSRAM. El acceso físico a memoria y E/S se realiza mediante Z80CPU_readPhysicalMem() / Z80CPU_writePhysicalMem() / Z80CPU_readPhysicalIO() / Z80CPU_writePhysicalIO(), que controlan ciclos de bus Z80 reales a través de las máquinas de estado PIO.
Sistema de hooks de depuración: Varios comandos (mmutrace, ipl) usan un mecanismo de callback por controlador — cada controlador persona puede registrar su propio handler de traza o reset, de modo que la salida del shell de depuración se adapta al contexto de máquina activo sin codificar lógica específica de máquina en dbgsh.c.
Sondeo del SIO del QD real — qdprobe: el comando qdprobe maneja manualmente el SIO del Quick Disk real para caracterizar una unidad MZ-1F11 físicamente conectada, complementando al qdtrace del lado de la emulación. Mantiene detenida la CPU Z80 (mediante el handshake cpu->hold / holdAck) mientras toma el bus, y go reanuda después la ejecución normal. Todo el acceso al bus se hace a través de Z80CPU_readPhysicalIO() / Z80CPU_writePhysicalIO(). Dos formas:
  • qdprobe [seconds] (por defecto 20, máximo 60) — maneja el SIO directamente (puertos F4 / F6 / F7): motor en marcha, búsqueda (hunt), y mapea las líneas de señal, informando de los eventos de sincronización a medida que ocurren. Decodifica dos bytes de estado RR0 — A = RR0 del Canal A (b3=HDST, b4=hunt, b0=Rx disponible) y B = RR0 del Canal B (b3=HOME).
  • qdprobe eng [pace_us] — reproduce la secuencia exacta del motor QD de TZFS (init, hunt, read) contra la unidad real a un ritmo entre accesos configurable (microsegundos), de modo que pueda validar la temporización del hardware real y confirmar que el mínimo de recuperación del paso a través con ritmo controlado es suficiente para una unidad dada.
Diagnósticos de configuración y memoria. Tres comandos más recientes cierran lagunas que la depuración de hardware no dejaba de poner de manifiesto:
  • cfgjson [find <text> | all] — vuelca el JSON de configuración realmente almacenado en flash, que no es necesariamente el archivo de la tarjeta SD: una aplicación fallida deja confirmada la configuración anterior. find imprime solo las líneas que coinciden con un fragmento de texto. Es la respuesta definitiva a "¿qué está ejecutando realmente la placa?".
  • memtest <addr> <len> [pat] [passes] — prueba inmediata de escritura/relectura de la memoria del anfitrión sobre ciclos de bus reales, con un histograma de errores por bit de datos.
  • memtest decay <addr> <len> <pat> <secs> — prueba de decaimiento de DRAM: rellena una región, mantiene detenida la CPU mientras el refresco corre a su cadencia normal durante los segundos indicados y luego verifica — imprimiendo cada celda invertida (dirección, bit, sentido) más un histograma por bit y un veredicto automático que distingue un único chip DRAM en decaimiento de una inanición de fila/refresco. Escrito para la investigación de la DRAM del Amstrad PCW, útil en cualquier anfitrión con RAM dinámica.

Manejadores de Fallos y Diagnósticos de PSRAM

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

Estructura de Diagnóstico de Fallos en PSRAM

Los últimos 256 bytes de la PSRAM de 8MB (dirección 0x117FFF00) están reservados para diagnósticos de fallos. Cuando ocurre un fallo, el manejador guarda una instantánea completa de registros:
// src/fault_handlers.c

#define PSRAM_DIAG_ADDR   0x117FFF00   // Last 256 bytes of 8MB PSRAM
#define PSRAM_DIAG_MAGIC  0xFA017000   // "FAULT" marker

typedef struct {
    uint32_t  magic;       // PSRAM_DIAG_MAGIC if valid
    uint32_t  faultType;   // 1=Hard, 2=MemManage, 3=BusFault, 4=UsageFault
    uint32_t  pc;          // Program counter at fault
    uint32_t  lr;          // Link register (return address)
    uint32_t  sp;          // Stack pointer
    uint32_t  r0, r1, r2, r3, r12;  // General-purpose registers
    uint32_t  psr;         // Program Status Register
    uint32_t  cfsr;        // Configurable Fault Status Register
    uint32_t  hfsr;        // Hard Fault Status Register
    uint32_t  bfar;        // Bus Fault Address Register
    uint32_t  mmfar;       // Memory Management Fault Address Register
    uint32_t  coreId;      // Which core faulted (0 or 1)
} t_PsramFaultDiag;

Implementación del Manejador de Fallos

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

Mapa de Memoria de Diagnóstico de PSRAM

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

Protocolo IPC Binario (FSPI v1.1)

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

Estructura de Trama

Cada transacción IPC consiste en una cabecera fija de 64 bytes seguida de un payload opcional y un trailer CRC32 de 4 bytes:
// src/include/ipc_protocol.h

typedef struct __attribute__((packed)) {
    uint8_t   frameType;              // 0:  IPCF_TYPE_COMMAND / RESPONSE / NOP
    uint8_t   command;                // 1:  IPCF_CMD_* opcode
    uint8_t   status;                 // 2:  IPCF_STATUS_* (response only)
    uint8_t   seqNum;                 // 3:  Sequence number (retry detection)
    uint16_t  payloadLen;             // 4:  Payload bytes (little-endian)
    uint16_t  sectorCount;            // 6:  Sectors in burst (1–16)
    uint32_t  fileOffset;             // 8:  Byte offset in file
    uint8_t   diskNo;                 // 12: Drive number
    uint8_t   flags;                  // 13: IPCF_FLAG_*
    uint16_t  reserved;               // 14: Reserved
    char      filename[48];           // 16: Null-terminated path (48 bytes)
} t_IpcFrameHdr;                      // Total: 64 bytes

// Frame layout:
// [64-byte header][0–8192 bytes payload][4-byte CRC32]

#define IPCF_HEADER_SIZE    64
#define IPCF_MAX_SECTORS    16        // Max sectors per burst
#define IPCF_SECTOR_SIZE    512       // Bytes per sector
#define IPCF_CRC_SIZE       4         // CRC32 trailer
#define IPCF_MAX_PAYLOAD    (IPCF_MAX_SECTORS * IPCF_SECTOR_SIZE)  // 8192
#define IPCF_MAX_FRAME_SIZE (IPCF_HEADER_SIZE + IPCF_MAX_PAYLOAD + IPCF_CRC_SIZE)  // 8260

Opcodes de Comando

Opcode Nombre Descripción
0x00 IPCF_CMD_NOP Sin operación (TX ficticio durante lectura full-duplex)
0x01 IPCF_CMD_RDS Leer un sector de 512 bytes
0x02 IPCF_CMD_WRS Escribir un sector de 512 bytes
0x03 IPCF_CMD_RBURST Lectura en ráfaga: 1–16 sectores en una transacción SPI
0x04 IPCF_CMD_WBURST Escritura en ráfaga: 1–16 sectores
0x05 IPCF_CMD_RFILE Leer archivo completo (fragmentado si excede el payload máximo)
0x06 IPCF_CMD_WFILE Escribir archivo completo
0x07 IPCF_CMD_INF Transferir información de versión/partición del RP2350 al ESP32
0x08 IPCF_CMD_RFD Leer archivo de imagen de disco flexible
0x09 IPCF_CMD_RQD Leer archivo de imagen QuickDisk
0x0A IPCF_CMD_RRF Leer imagen de respaldo RAMFILE
0x0B IPCF_CMD_DIR Listado de directorio
0x0C IPCF_CMD_MZFDIR Listado de directorio con formato TZFS (cabeceras MZF)
0x10–0x14 IPCF_CMD_NET_CFG … IPCF_CMD_NET_PING Configuración de red, ciclo de vida de sockets, envío, recepción, ping — consulte la Guía Técnica
0x15 IPCF_CMD_TIME Obtención del reloj de pared: carga útil de 9 bytes con los segundos de época locales en 64 bits little-endian más un flag synced. Lo llama ESP_getTime() desde devicesTimeSync() en el Core 0 cada 10 s hasta que todas las instancias de RTC MSM5832 son válidas; un firmware ESP32 más antiguo devuelve un error y el RP2350 simplemente reintenta más tarde

DMA e Integridad

  • Canales DMA pre-asignados: Los canales DMA TX y RX (gDmaTx, gDmaRx) se reclaman una vez en la inicialización de FSPI y nunca se liberan. Esto elimina la sobrecarga de reclamar/liberar por transferencia y previene condiciones de carrera por agotamiento de canales DMA que pueden ocurrir cuando el Core 1 contende por el bus QMI.
  • Elevación de prioridad RX: El canal DMA RX se configura con PRIORIDAD ALTA para prevenir desbordamiento del FIFO SPI RX. Los accesos a PSRAM del Core 1 a través del bus QMI pueden detener el fabric del bus AHB, y si el canal DMA RX está a prioridad normal sus transferencias pueden retrasarse lo suficiente para que el FIFO SPI se desborde.
  • Integridad CRC32: Cada trama está protegida por un CRC32 estándar IEEE 802.3 (polinomio 0xEDB88320, reflejado). El ESP32 usa esp_rom_crc32_le() que produce el mismo resultado. En caso de discrepancia de CRC, la trama se reintenta, usando el contador de secuencia (seqNum) para detección de duplicados.
  • Validación de opcode / número de secuencia: Cada respuesta del ESP32 devuelve el opcode del comando y su seqNum (se tolera un seqNum de 0 para firmware ESP32 más antiguo). FSPI_sendBinaryCmd() descarta y contabiliza cualquier respuesta que no coincida y luego reintenta — una respuesta tardía a un comando que ya había expirado solía desplazar en uno todas las respuestas siguientes y fue la causa raíz de una familia de fallos retardados por punteros descontrolados.
  • Ruta caliente residente en RAM: las rutinas de CRC, aborto de DMA, transferencia, envío/recepción y ESP_pollNop son __not_in_flash_func, después de que se rastrearan fallos UNDEFINSTR hasta recargas XIP corruptas mientras el Core 1 martilleaba la PSRAM. FSPI_CLK_FREQ es 60 MHz.
  • 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.

Simulador en el Anfitrión (pZ80sim)

pZ80sim ejecuta el firmware picoZ80 real y sin modificar — tanto el lado del RP2350 como el lado del ESP32 — compilado de forma nativa para un anfitrión Linux e insertado en el zócalo de CPU de una máquina Sharp emulada del Common Source Code Project (CSP). El producto es un binario ordinario de emulador CSP, por ejemplo emumz700picoz80: un MZ-700 de CSP cuyo Z80 es el picoZ80. La lógica del firmware y de los controladores, la configuración de personas, el análisis de JSON, el protocolo IPC RP2350 ↔ ESP32, la interfaz web y el shell de depuración pueden ejercitarse todos en un ciclo de editar/compilar/probar de segundos, con una visibilidad que el hardware no puede ofrecer — sin recompilar dos firmwares, sin reflashear, sin apagar y encender la máquina.
Todo lo que está por encima de la placa es código real del árbol del firmware: Z80CPU.c y el núcleo Zeta Z80 parcheado, los controladores de persona Sharp, dbgsh.c, el main.c de BaseZ80, cJSON.c y las personas distribuidas, con la flash y la PSRAM mapeadas en sus direcciones reales. La mitad ESP32 es igualmente real — las fuentes de esp32/main compiladas contra una fina capa de compatibilidad (shim) de ESP-IDF. Solo se simula la capa de hardware:
  • Shim del pico-sdk (sim/picohal/) — implementaciones para el anfitrión de las cabeceras del SDK que incluye el firmware.
  • Bus físico PIO (sim/src/bus_sim.c) — los ciclos del bus físico se convierten en accesos DEVICE de CSP a la máquina emulada.
  • Canales USB CDC (sim/src/cdc_sim.c) — cada canal se convierte en un pseudoterminal del anfitrión y en un puerto TCP.
  • Shim de ESP-IDF (sim/espidfhal/) — servidor HTTP, eventos de red, primitivas de FreeRTOS y el enlace SPI, bajo las fuentes reales del ESP32.
  • Enlace SPI virtual — tramas ipc_protocol reales viajan por un enlace SPI RP2350 ↔ ESP32 simulado, línea de handshake incluida.
  • Tarjeta SD virtual — un único directorio del anfitrión compartido por ambas mitades de procesador, exactamente como lo es una única tarjeta física.
La interfaz web genuina se sirve a cualquier navegador de la red (puerto por defecto 8088), y se exponen seis canales serie virtuales tanto como enlaces simbólicos pty bajo sim/run/ como puertos TCP — el log de arranque/depuración del RP2350, el ICE dbgsh, los dos puertos serie SIO, la consola del ESP32 y la impresora. Conéctese con minicom -D sim/run/dbgsh, screen o telnet al puerto.

Máquinas Soportadas

El lanzador sim/run.sh proporciona una entrada por máquina anfitriona emulada, cada una con su propia tarjeta SD, directorio de trabajo, puerto web y bloque de puertos serie, de modo que varias máquinas pueden ejecutarse en paralelo: MZ-80K, MZ-80A, MZ-700, MZ-800, MZ-1500, MZ-80B, MZ-2000, MZ-2200, MZ-2500, más X1, X1turbo y X1turboZ. Las entradas X1 ejecutan el módulo en modo físico — el IPL/BIOS de la máquina procede de la placa base emulada, como ocurre en el hardware real. El Amstrad PCW y el Tatung Einstein no tienen modelo de máquina en CSP, por lo que no pueden aparecer en el simulador.

Compilación y Ejecución

El simulador se compila desde dentro del árbol de fuentes de CSP, apuntando al checkout de picoZ80. Los requisitos previos son los propios de CSP (bibliotecas de desarrollo de Qt5, SDL2 y FFmpeg):
# In the Common Source Code Project tree:
cd common_source_project-fm7/source
mkdir -p build-picoz80 && cd build-picoz80
cmake -DCSP_BUILD_WITH_CXX20=ON -DBUILD_PICOZ80SIM=ON \
      -DPICOZ80_ROOT=/path/to/pico/projects/tzpuPico ..
make emumz700picoz80 -j$(nproc)

# In the picoZ80 tree — run it (MZ-700 is the default):
cd projects/tzpuPico/sim
./run.sh                 # MZ-700
./run.sh mz80a           # any machine from the table
./run.sh --help
run.sh instala las ROMs de la máquina, construye su tarjeta SD en la primera ejecución, prepara el directorio de trabajo y lanza el binario de CSP. El comportamiento se controla mediante variables de entorno: PZ80SIM_HTTP_PORT (puerto de la interfaz web), PZ80SIM_TCP_BASE (bloque de puertos serie), PZ80SIM_SD (directorio de la tarjeta SD), PZ80SIM_PERSONA (selección de persona), PZ80SIM_SOUND=1 (audio del anfitrión, desactivado por defecto), PZ80SIM_VERBOSE y PZ80SIM_TRACE.

Para Qué Sirve — y Para Qué No

El simulador encuentra errores de lógica: conmutación de bancos de memoria, máquinas de estado de controladores, análisis de configuración, secuenciación del protocolo IPC, handlers de la interfaz web, comportamiento del shell de depuración. Ya se ha ganado su lugar con defectos reales que se estaban distribuyendo en hardware — un fallo del servidor web ante cualquier URL sin extensión (un substr(find_last_of(".")) sin protección que lanzaba una excepción no capturada, provocando un pánico y el reinicio del dispositivo), y un fallo de enlazado por inline en untar.h que solo enlazaba por suerte de los ajustes del optimizador de ESP-IDF. Ambos se encontraron en el simulador, se corrigieron, y las correcciones se confirmaron en el firmware propiamente dicho.
Lo que una ejecución en verde no demuestra
El simulador no modela la temporización de las instrucciones PIO, los flancos de muestreo de /WAIT, la contención del bus, la capacidad de corriente ni la contención del bus de PSRAM/flash. Una ejecución limpia en el simulador no es evidencia sobre la temporización del bus — esa clase de trabajo pertenece al hardware real con un analizador lógico, como demuestra la investigación de temporización del Amstrad PCW.
El pico6502 tiene su propia contraparte independiente, pico6502sim, que no necesita CSP en absoluto — consulte la Guía del Desarrollador de pico6502. Todos los detalles de compilación, las tablas de máquinas y las limitaciones de ambos simuladores están documentados en sim/README.md en el repositorio.

Sitios de Referencia

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

Aviso Regulatorio de Radiofrecuencia

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