picoZ80 Technical Guide

picoZ80 Technical Guide

This guide documents the picoZ80 hardware architecture, RP2350 PIO bus interface, memory model, JSON configuration reference, virtual device framework, and debugging procedures. It is intended for developers who want to understand the internals, write new drivers, port the firmware to a new host machine, or debug firmware-level issues.
For end-user setup and web interface usage, see the picoZ80 User Manual. For the project overview and build instructions see the picoZ80 project page.

Hardware Architecture

The picoZ80 integrates five subsystems on a single compact PCB designed to fit within the footprint of a DIP-40 package. All logic operates at 3.3V; the Z80 bus interface handles level translation and current drive for the 5V host bus.

System Block Diagram

┌─────────────────────────────────────────────────────────────────────────┐
│                         picoZ80 PCB                                     │
│                                                                         │
│  ┌────────────────────────────┐      ┌──────────────────────────────┐  │
│  │         RP2350B            │      │           ESP32-S3           │  │
│  │  (Cortex-M33, dual core)   │      │                              │  │
│  │                            │      │  ┌──────┐  ┌──────────────┐ │  │
│  │  Core 0: USB, file I/O,    │◄────►│  │  SD  │  │  Web Server  │ │  │
│  │          ESP32 relay       │ FSPI │  │ Card │  │  (Bootstrap) │ │  │
│  │  Core 1: Z80 bus hot loop  │ UART │  └──────┘  └──────────────┘ │  │
│  │                            │      │                              │  │
│  │  PIO 0,1,2: bus interface  │      │  WiFi ─── 802.11 b/g/n AP   │  │
│  │                            │      │           or Client mode     │  │
│  │  16MB SPI Flash            │      └──────────────────────────────┘  │
│  │  8MB PSRAM (SPI)           │                                         │
│  └────────────────────────────┘                                         │
│                │                                                         │
│       ┌────────┴────────┐                                               │
│       │ Z80 Bus Interface│                                               │
│       │ (40-pin DIP out) │                                               │
│       └────────┬────────┘                                               │
│                │ 5V bus (A0–A15, D0–D7, MREQ, IORQ, RD, WR...)         │
└────────────────┼────────────────────────────────────────────────────────┘
                 │
         ┌───────┴───────┐
         │  Host Z80     │
         │  DIP-40 socket│
         │  (legacy      │
         │   computer)   │
         └───────────────┘

Key Components

Component Device Role
Primary MCURP2350B (QFN-80)Dual Cortex-M33, 150MHz (up to 300MHz OC), 512KB SRAM, 12 PIO state machines, 48 GPIO pins
FlashW25Q128 (16MB SPI)Bootloader, dual firmware slots, config partitions
PSRAM8MB SPI PSRAM64 × 64KB RAM/ROM banks for Z80 address space
Co-processorESP32-S3-PICO-1WiFi, SD card, web server, OTA
USB hubCH334FUSB hub, firmware update bridging
Power supplyTLV62590BV5V → 3.3V synchronous buck converter

RP2350B GPIO Assignment

The RP2350B QFN-80 package provides 48 GPIO pins. The picoZ80 uses virtually every pin. The assignment is fixed in the board design and reflected in the PIO programs:
GPIO Range Signals Direction
GPIO 0–15 A0–A15 (Z80 Address Bus) Output (driven by PIO)
GPIO 16–23 D0–D7 (Z80 Data Bus) Bidirectional (PIO tri-state)
GPIO 24 MREQ Output
GPIO 25 IORQ Output
GPIO 26 RD Output
GPIO 27 WR Output
GPIO 28 M1 Output
GPIO 29 RFSH Output
GPIO 30 BUSREQ Input
GPIO 31 BUSACK Output
GPIO 32 HALT Output
GPIO 33 INT Input
GPIO 34 NMI Input
GPIO 35 WAIT Output
GPIO 36 CLK Input (host clock)
GPIO 37 RESET Input
GPIO 38–41 ESP32 FSPI (CS, CLK, MOSI, MISO) SPI
GPIO 42–43 ESP32 UART (TX, RX) UART
GPIO 44–45 PSRAM SPI SPI
GPIO 46–47 USB (D+, D–) USB

Firmware Architecture

The RP2350 firmware is built with the Raspberry Pi Pico SDK 2.x targeting the RP2350-arm-s platform. The firmware is divided into two independent executables: the Bootloader and the Application.

Flash Memory Layout

Partition Address Range Size Contents
Bootloader 0x10000000 – 0x1001FFFF 128KB USB bridge, firmware update, partition selector
App Slot 1 0x10020000 – 0x1051FFFF 5MB Z80 firmware — active application (slot 1)
App Slot 2 0x10520000 – 0x10A1FFFF 5MB Z80 firmware — active application (slot 2)
App Config 1 0x10A20000 – 0x10C9FFFF 2.5MB ROM images + minified config JSON (slot 1)
App Config 2 0x10CA0000 – 0x10F1FFFF 2.5MB ROM images + minified config JSON (slot 2)
General Config 0x10F20000 – 0x10FFEFFF 892KB Core settings, scratch space
Partition Table 0x10FFF000 – 0x11000000 4KB Active slot number, checksums, metadata

Dual-Core Responsibilities

The two Cortex-M33 cores are assigned completely separate responsibilities and communicate via an inter-core message queue (queue_t). This separation ensures that non-real-time work on Core 0 never introduces jitter into the Z80 bus transactions on Core 1.
Core Responsibilities
Core 0 USB CDC-serial bridge; firmware update coordination; file I/O (relayed to ESP32 over UART); ESP32 command dispatch (disk image changes, config reloads, version queries); partition management; inter-core message dispatch.
Core 1 Z80 bus emulation hot loop — runs exclusively. Services PIO FIFOs, resolves each bus transaction against the memory map, and dispatches to: physical host hardware (PHYSICAL), PSRAM (RAM/ROM), or virtual device handler (FUNC). Inner loop is placed in SRAM.

PIO Bus Interface

The Z80 bus interface is implemented entirely in RP2350 PIO assembly (z80.pio). The RP2350 provides three PIO blocks (PIO 0, PIO 1, PIO 2) each with four state machines — twelve state machines in total, of which the Z80 firmware uses all twelve.
PIO programs execute independently of the Cortex-M33 cores. The bus interface continues to respond deterministically even when Core 1 is occupied with PSRAM accesses or virtual device function calls. State machines communicate via PIO IRQ flags rather than polling, eliminating inter-machine latency.

PIO Program Table

PIO State Machine Program Function
0 SM 0 z80_addr Outputs the 16-bit address (A0–A15) onto the bus and signals cycle start to SM 2.
0 SM 1 z80_data Drives or samples D0–D7 with tri-state control; released during BUSRQ.
0 SM 2 z80_cycle Top-level bus cycle sequencer — orchestrates fetch, read, write, I/O, and DRAM refresh cycles.
0 SM 3 z80_fetch Opcode-fetch cycle (M1 + MREQ + RD).
1 SM 0 z80_mem_read Memory read cycle (MREQ + RD).
1 SM 1 z80_mem_write Memory write cycle (MREQ + WR).
1 SM 2 z80_io_read I/O read cycle (IORQ + RD).
1 SM 3 z80_io_write I/O write cycle (IORQ + WR).
2 SM 0 z80_busrq Manages BUSREQ/BUSACK; releases /IORQ, /MREQ, /RFSH, /M1, /HALT, /WR, /RD.
2 SM 1 z80_nmi Detects NMI assertion and signals Core 1.
2 SM 2 z80_clk_sync Synchronises PIO state machines to the host Z80 CLK signal.
2 SM 3 z80_int_ack Handles interrupt-acknowledge cycles (M1 + IORQ).

PIO IRQ Signal Conventions

Inter-state-machine communication uses PIO IRQ flags. Core 1 monitors these flags in the hot loop to take action on each bus event:
IRQ Event
IRQ 0 Address valid / cycle start — a new bus cycle has begun and A0–A15 are stable.
IRQ 1 Data phase — data bus direction has been resolved; D0–D7 should be driven or sampled.
IRQ 2 T1 detected — the rising edge of T1 on the current cycle. Used to synchronise internal operations to the host clock.
IRQ 3 RESET event — the host RESET line has been asserted. Core 1 should reinitialise emulation state.
IRQ 4 NMI detected — host NMI line asserted.
IRQ 6 BUSRQ active — host has asserted BUSREQ; PIO is releasing the bus.

Wait State Generation

The z80_wait PIO program in PIO 2 SM 0 inserts configurable T-cycle wait states on the host bus by asserting /WAIT. The number of additional wait states is controlled per memory or I/O block by the tcycwait parameter in config.json.
Wait states are necessary when the RP2350 needs additional time to complete a PSRAM access or a virtual device function call before presenting data to the host bus. The tcycsync parameter enables T1 synchronisation (z80_sync in PIO 2 SM 1), which locks the PSRAM access window to the T1 rising edge of each bus cycle, preventing timing drift in applications that depend on the host clock for precise timing (cassette, serial bit-banging).

PIO Architecture — How Bus Cycles Are Recreated

The RP2350's Programmable I/O (PIO) subsystem is the key technology that allows the picoZ80 to recreate cycle-accurate Z80 bus timing. Understanding how the PIO state machines work together is essential for anyone modifying the bus interface or debugging timing issues.

RP2350 PIO Fundamentals
Each PIO block contains four independent state machines (SMs) that execute small programs from a shared 32-instruction memory. State machines run independently of the Cortex-M33 cores at the system clock frequency (up to 300 MHz). Key PIO resources used by the picoZ80:
  • TX FIFO — a 4-entry queue from the CPU to the state machine. The C code on Core 1 pushes data (addresses, control words, injected instructions) into the FIFO; the PIO program pulls from it using out or pull.
  • RX FIFO — a 4-entry queue from the state machine to the CPU. The PIO pushes data bus samples into the FIFO using in; Core 1 reads them after each bus cycle completes.
  • IRQ flags — 8 flags (IRQ 0–7) shared across all state machines within a PIO block. Flags can also be seen across PIO blocks (IRQ 0–3 in one block map to IRQ 4–7 in adjacent blocks). State machines use irq set / irq wait / irq clear to synchronise with each other and with the C code.
  • Scratch registers X and Y — two 32-bit registers per SM used for loop counters and temporary values.
  • out exec — a special instruction that pulls a value from the TX FIFO and executes it as a PIO instruction. This is the mechanism by which the C code dynamically controls bus cycle sequences (see below).
  • set pins / out pins — drive GPIO pins directly. set uses an immediate 5-bit value; out shifts data from the output shift register (OSR) to the pins.
  • in pins — samples GPIO pins into the input shift register (ISR), then auto-pushes to the RX FIFO.
  • wait gpio — stalls the SM until a specific GPIO pin reaches a specified level. Used extensively to synchronise with the host Z80 clock signal.
  • Side-set — allows one or two GPIO pins to be driven as a side-effect of any instruction, without using an instruction cycle. The picoZ80 uses 2-bit side-set to control /RD and /WR simultaneously with other operations.
  • JMP PIN — conditional jump based on a designated GPIO pin level. Used to test /WAIT, BUSREQ, /NMI, and /RESET.

The out exec Mechanism — Dynamic Instruction Injection
The most distinctive feature of the picoZ80 PIO design is the use of out exec, 16 in the z80_cycle orchestrator state machine. This instruction pulls a 16-bit value from the TX FIFO and executes it immediately as a PIO instruction — the value is not data, it is the next instruction the state machine will run.
This mechanism allows the C code on Core 1 to control the bus cycle sequence in real time. Rather than loading a fixed PIO program for each cycle type, Core 1 pushes a sequence of pre-encoded PIO instructions into the TX FIFO, and the cycle SM executes them one by one:
// z80_cycle SM (PIO 0 SM 2) — the orchestrator
//
// .program z80_cycle
// .side_set 2 opt
// public start_cycle:
//     wait 0 irq 6             ; Pause if BUSACK is active (bus relinquished).
//     irq set 0                ; Signal "ready for new cycle".
//     wait 0 irq 0             ; Wait until C code clears IRQ 0 (address loaded).
//     wait 1 gpio Z80_PIN_CLK  ; Sync to T1 rising edge of host clock.
// cycle_exec:
//     out exec, 16             ; ← Pull next instruction from TX FIFO and execute it.
//     jmp cycle_exec           ; Loop: keep executing injected instructions.
//
// The C code pushes a sequence of encoded PIO instructions into the FIFO.
// Each instruction controls one step of the bus cycle (assert /MREQ, wait for
// clock edge, read data bus, etc.). The sequence ends with a JMP back to
// start_cycle, which restarts the orchestrator for the next bus transaction.
The C code pre-computes these instruction sequences at startup for each cycle type (fetch, memory read, memory write, I/O read, I/O write, refresh, interrupt acknowledge). During execution, Core 1 selects the appropriate pre-built sequence and pushes it into the FIFO. This approach has two critical advantages:
  • Program space efficiency — each PIO block has only 32 instruction slots. By injecting instructions dynamically, the cycle SM needs only 7 instructions of program memory to orchestrate all cycle types. The actual cycle-type programs (fetch, read, write, etc.) exist as C arrays of encoded instructions, not as resident PIO programs.
  • Flexibility — the C code can modify the injected instruction sequence at runtime to handle special cases (e.g. inserting extra wait states, skipping the refresh phase, or generating a non-standard cycle for debugging).

State Machine Coordination
The 12 state machines work as a coordinated pipeline. The following diagram shows the flow of a typical memory read cycle:
                  Core 1 (C code)               PIO State Machines
                  ─────────────                 ──────────────────
    1. Resolve address                          z80_cycle: IRQ 0 set
       from memory map                           (waiting for work)
                  │
    2. Push addr → TX FIFO ──────────────────→ z80_addr: receives addr
       Clear IRQ 0                               outputs A0–A15 on pins
                  │
    3. Push cycle instructions ──────────────→ z80_cycle: out exec, 16
       (e.g. mem_read sequence)                   executes: set /MREQ low
       into cycle SM TX FIFO                      executes: set /RD low
                  │                               executes: wait CLK edges
    4. Wait for RX FIFO ←────────────────────  z80_data: samples D0–D7
       (data byte from bus)                       pushes to RX FIFO
                  │
    5. Read data from                          z80_cycle: JMP start_cycle
       RX FIFO                                    (ready for next cycle)
                  │
    6. Dispatch to PSRAM
       or driver handler
The IRQ-based handshake ensures that the address bus is stable before control signals are asserted, and that data is sampled at the correct point in the bus cycle. The state machines never poll — they use wait 0 irq N to sleep until the relevant event occurs, consuming zero CPU cycles while waiting.

Z80 Fetch Cycle (M1 Cycle) — Step by Step
The opcode fetch is the most complex Z80 bus cycle — it combines a memory read with a refresh cycle. The z80_fetch program executes over 4 T-cycles of the host clock:
Host CLK:  ──┐  ┌──┐  ┌──┐  ┌──┐  ┌──
             │  │  │  │  │  │  │  │
             └──┘  └──┘  └──┘  └──┘
              T1    T2    T3    T4

A0–A15:    ══╤═══ PC address ══════╤═══ Refresh addr ══╗
             │                     │                    ║
/M1:       ──┘                     └────────────────────╜── (low during T1–T2, high T3–T4)
/MREQ:     ────┘              ┌────┘              ┌──── (low T1↓–T3↑, then T3↓–T4↓ for refresh)
/RD:       ────┘              ┌─────────────────────── (low T1↓–T3↑)
/RFSH:     ────────────────────┘                   ┌── (low T3↑–T4↓)
D0–D7:     ═══════════════╤═══╗                        (sampled at T3↑)
                          │   ║
                       opcode read
The PIO program implements this as follows:
  1. T1 rising edgez80_addr SM outputs the PC value onto A0–A15. z80_fetch asserts /M1 low via set pins.
  2. T1 falling edge/MREQ and /RD are asserted low (via set pins and side-set). The address is now valid and the memory system can begin responding.
  3. T2 — the SM enters a wait-state loop: it waits for CLK rising then falling edge, then checks the /WAIT pin via jmp pin. If /WAIT is low, the SM loops (adding Tw cycles). If high, it proceeds to T3.
  4. T3 rising edgein pins, 8 samples D0–D7 (the opcode byte) and pushes it into the RX FIFO. IRQ 1 is set to signal z80_data/z80_addr that the refresh address should now be output. /M1, /MREQ, and /RD are deasserted; /RFSH is asserted low.
  5. T3 falling edge/MREQ is asserted again (for the refresh row strobe).
  6. T4 — refresh continues. At the end of T4, /MREQ and /RFSH are deasserted. The cycle SM returns to start_cycle ready for the next bus transaction.
Core 1 reads the opcode from the RX FIFO and uses it to decode the instruction, determine how many subsequent memory or I/O cycles are needed, and push the appropriate instruction sequences.

Memory Read and Write Cycles
Memory read and write cycles are simpler than the fetch — they span 3 T-cycles with no refresh phase.
Memory Read:
Host CLK:  ──┐  ┌──┐  ┌──┐  ┌──
             │  │  │  │  │  │
             └──┘  └──┘  └──┘
              T1    T2    T3

A0–A15:    ══╤═══ address ═══════╗
/MREQ:     ────┘              ┌──── (low T1↓–T3↓)
/RD:       ────┘              ┌──── (low T1↓–T3↓)
D0–D7:     ═══════════════╤═══╗     (sampled at T3↓)


Memory Write:
Host CLK:  ──┐  ┌──┐  ┌──┐  ┌──
             │  │  │  │  │  │
             └──┘  └──┘  └──┘
              T1    T2    T3

A0–A15:    ══╤═══ address ═══════╗
D0–D7:     ══════╤═══ data ═════╗   (driven from T2 onwards)
/MREQ:     ────┘              ┌──── (low T1↓–T3↓)
/WR:       ──────────┘        ┌──── (low T2↓–T3↓)
For reads, the z80_mem_read SM asserts /MREQ and /RD at T1 falling edge, waits through T2 (checking /WAIT for wait states), then samples the data bus at T3 falling edge using in pins, 8. For writes, z80_mem_write asserts /MREQ at T1 falling edge, then asserts /WR at T2 falling edge after the data bus is driven by z80_data. Both deassert all control signals at the end of T3.

I/O Read and Write Cycles
Z80 I/O cycles use /IORQ instead of /MREQ and always include an automatic wait state (Tw) between T2 and T3. This is a Z80 architectural feature — the extra cycle gives slower I/O devices time to respond:
I/O Read:
Host CLK:  ──┐  ┌──┐  ┌──┐  ┌──┐  ┌──
             │  │  │  │  │  │  │  │
             └──┘  └──┘  └──┘  └──┘
              T1    T2    Tw    T3

A0–A15:    ══╤═══ port address ══════════╗
/IORQ:     ──────┘                    ┌──── (low T2↑–T3↓)
/RD:       ──────┘                    ┌──── (low T2↑–T3↓)
D0–D7:     ═══════════════════════╤═══╗     (sampled at T3↓)
The z80_io_read SM asserts /IORQ and /RD at T2 rising edge (not T1 as with memory cycles — this is the Z80 specification). The automatic Tw wait state is implemented by the same jmp pin / wait loop pattern used for memory cycles. I/O writes follow the same pattern with /WR replacing /RD.

BUSREQ / BUSACK Handling
The z80_busrq SM (PIO 2 SM 0) monitors the host /BUSREQ input pin. When /BUSREQ goes active (low), the SM:
  1. Sets IRQ 6 to signal the cycle SM that a bus request is pending.
  2. Waits for the current bus cycle to complete (wait 1 irq 0).
  3. Pulls a 32-bit control word from the TX FIFO that specifies the pin directions and values for the bus-release state — this tristates the address and data buses and asserts /BUSACK low.
  4. Spins on jmp pin until /BUSREQ goes inactive (high).
  5. Pulls a second 32-bit word to restore normal pin directions and deassert /BUSACK.
  6. Clears IRQ 6, allowing the cycle SM to resume.
The cycle SM checks IRQ 6 at the start of every cycle via wait 0 irq 6 — if the flag is set, the SM stalls until the bus request is complete. This ensures bus release happens cleanly between cycles, never mid-cycle.

Clock Synchronisation
All cycle-type SMs synchronise to the host Z80 clock using wait 1 gpio Z80_PIN_CLK (wait for rising edge) and wait 0 gpio Z80_PIN_CLK (wait for falling edge). This means:
  • The PIO programs are clock-frequency independent — they work at any host clock speed from DC to the maximum rate the RP2350 can track (limited by the PIO system clock and GPIO sampling rate).
  • The RP2350's 300 MHz PIO clock provides approximately 85 PIO cycles per Z80 T-state at 3.5 MHz, giving more than enough time to execute PIO instructions, push/pull FIFOs, and check IRQ flags between clock edges.
  • The z80_sync SM (PIO 2 SM 1) provides a T1 synchronisation IRQ that the C code uses to align PSRAM accesses with the host clock, preventing timing drift in clock-sensitive host software.
  • The z80_clk_sync SM (PIO 2 SM 2) regenerates the host clock on a separate GPIO, providing a clean clock output for external monitoring or logic analyser triggering.

Interrupt Acknowledge Cycle
The z80_int_ack SM (PIO 2 SM 3) implements the Z80 interrupt acknowledge sequence. When the C code detects an interrupt condition, it loads the int_ack program. This cycle is similar to a fetch but with key differences:
  • /M1 is asserted at T1 (like a fetch), but /IORQ is asserted instead of /MREQ at the wait state (Tw1).
  • Two automatic wait states (Tw1, Tw2) are inserted to give the interrupting device time to place a vector on the data bus.
  • The vector byte is read from D0–D7 and pushed to the RX FIFO.
  • A refresh cycle follows, identical to the fetch refresh phase.

Memory Model

Memory accesses are resolved through three tiers of increasing latency. The three-tier design ensures that the common case (PSRAM-backed RAM/ROM) is fast while allowing maximum flexibility for virtual devices and physical host pass-through.

Tier 1 — RP2350 SRAM Dispatch Table

A 128-entry array of 32-bit membankPtr values, resident in the RP2350's 512KB on-chip SRAM, provides an O(1) block-type lookup for every bus transaction. One entry covers each 512-byte block of the 64KB Z80 address space (128 × 512 = 65,536 bytes). Each entry encodes:
  • The block type (PHYSICAL, RAM, ROM, FUNC, etc.).
  • For PSRAM-backed blocks: the PSRAM bank number and offset.
  • For FUNC blocks: an index into the virtual device function pointer table.
This is the fastest path — Core 1 reads the dispatch table entry for the current address in a single SRAM access (zero wait states at 300MHz) before deciding what to do next.

Tier 2 — External PSRAM

The 8MB PSRAM is organised as:
  • 64 banks × 64KB — RAM or ROM image data for the Z80 address space.
  • 64KB memPtr — per-byte redirect pointer array for PTR-type blocks.
  • 64KB memioPtr — function pointer array for memory-mapped FUNC devices.
  • 64KB ioPtr — function pointer array for I/O port FUNC devices.
PSRAM is accessed via the RP2350's dedicated SPI peripheral with DMA. Access latency is deterministic and managed by the wait-state generator to avoid bus violations.

Tier 3 — 16MB SPI Flash

ROM images are loaded from Flash (or the SD card, via the ESP32) into PSRAM at boot. At runtime the Flash is not accessed for bus transactions — all ROM data is served from PSRAM. The Flash is used for:
  • Bootloader and application firmware.
  • Minified config.json (cached from SD card on each boot).
  • ROM images in App Config partitions (used when no SD card is present).

Memory Block Types

Type Description
PHYSICAL Pass-through — the RP2350 releases the bus and the physical host memory responds. Used for the host’s native ROM and RAM.
PHYSICAL_VRAM As PHYSICAL but with additional wait states for host video RAM timing. Suitable for MZ-700/MZ-80A VRAM regions.
PHYSICAL_HW Pass-through for host hardware registers (I/O-mapped devices in memory space).
RAM Read/write — backed by a PSRAM bank. The RP2350 services reads and writes from/to PSRAM.
ROM Read-only — backed by a PSRAM bank. Write cycles are silently ignored (the host sees normal bus timing but no data is stored).
VRAM PSRAM-backed video RAM. Write cycles are mirrored to both PSRAM and the physical host VRAM simultaneously.
FUNC Virtual device — each access triggers a C function call via the memioPtr or ioPtr function pointer table, enabling arbitrary I/O emulation.
PTR Per-byte redirect — each byte of the 512-byte block can independently point to any other block type or PSRAM location.

Configuration Reference

All picoZ80 behaviour is controlled by config.json on the SD card. The RP2350 reads and minifies this file at boot, storing the result in Flash. Subsequent boots use the Flash copy if no SD card is present.
The top-level JSON structure is:
{
  "esp32": {
    "core":  { ... },
    "wifi":  { ... }
  },
  "rp2350": {
    "core":  { ... },
    "z80":   [ { "memory": [...], "io": [...], "drivers": [...] } ]
  }
}

esp32.core

Key Type Description
device string CPU personality — "Z80" for picoZ80, "6502" for pico6502, "6512" for pico6512.
mode integer Default WiFi boot mode: 0 = client (station), 1 = Access Point.

esp32.wifi

Key Type Description
override 0/1 Master switch: 1 = apply all settings below; 0 = use persisted NVS settings.
wifimode string "ap" = Access Point mode; "client" = Station/client mode.
ssid string WiFi network name to create (AP) or join (client).
password string WiFi passphrase.
ip string Fixed IP address (e.g. "192.168.1.192").
netmask string Subnet mask (e.g. "255.255.255.0").
gateway string Default gateway (e.g. "192.168.1.1").
dhcp 0/1 Client mode: 1 = DHCP; 0 = use fixed IP settings.
webfs string Web filesystem root directory on SD card (default "webfs").
persist 0/1 1 = write resolved settings to NVS for persistence across reboots.

rp2350.core

Key Type Description
cpufreq integer RP2350 system clock in Hz (e.g. 300000000). Maximum stable frequency depends on PSRAM frequency and core voltage.
psramfreq integer PSRAM SPI clock in Hz (e.g. 133000000).
voltage float RP2350 core voltage in volts (e.g. 1.10). Higher clock speeds require higher voltage.

z80[].memory — Memory Map Entries

The memory array defines the Z80 memory map. Entries must be ordered by address. Regions must be aligned to and sized as multiples of 512 bytes. Gaps between entries are treated as PHYSICAL pass-through.
Key Type Description
enable 0/1 Whether this entry is active. Disabled entries are ignored at boot.
addr hex string Start address in the Z80 address space (e.g. "0x0000"). Must be 512-byte aligned.
size hex string Region size in bytes (e.g. "0x2000" for 8KB). Must be a multiple of 512.
type string Block type — see Memory Block Types.
bank integer PSRAM bank number (0–63) for RAM/ROM/VRAM/FUNC types.
tcycwait integer Additional T-cycle wait states to insert on each access to this region.
tcycsync 0/1 Enable T1 synchronisation for this region. Required for timing-sensitive regions.
task string Optional task identifier for FUNC-type blocks (driver binding string).
file string SD-card path to a ROM image to preload into the PSRAM bank at boot (e.g. "/ROM/mz700.rom").
fileofs integer Byte offset into the ROM image file to start reading from.

z80[].io — I/O Port Map Entries

The io array maps Z80 I/O port ranges to block types. Only PHYSICAL and FUNC types are meaningful for I/O entries.
Key Type Description
enable 0/1 Whether this I/O entry is active.
addr hex string Start I/O port address (e.g. "0xE0").
size hex string Number of consecutive ports (e.g. "0x04" for ports E0–E3).
type string PHYSICAL = pass to host; FUNC = call C handler function.
task string Driver binding string for FUNC-type entries.

z80[].drivers — Driver Instances

The drivers array instantiates virtual device drivers and binds them to memory or I/O regions. Each driver has a type (the C driver module), a name (instance identifier), and one or more interface objects that define the ROM images, address maps, I/O maps, and parameters for that driver instance.
"drivers": [
  {
    "type":   "WD1773",
    "name":   "FDC",
    "interfaces": [
      {
        "name":    "FDC_0",
        "type":    "FDC",
        "rom":     [],
        "addrmap": [],
        "iomap": [
          { "enable": 1, "addr": "0xE0", "size": "0x04", "type": "FUNC" }
        ],
        "param": [
          { "name": "tracks",   "value": "80" },
          { "name": "heads",    "value": "2" },
          { "name": "sectors",  "value": "8" },
          { "name": "image",    "value": "/DSK/disk1.dsk" }
        ]
      }
    ]
  }
]

Built-in Drivers (INCLUDE_SHARP_DRIVERS)

When the firmware is built with INCLUDE_SHARP_DRIVERS, the following driver modules are compiled in and can be instantiated via the drivers array:
Driver Type String Description
MZ700.c MZ700 Sharp MZ-700 bank switching, video, keyboard I/O
MZ80A.c MZ80A Sharp MZ-80A — monitor ROM (SA-1510), VRAM, Intel 8253 PIT emulation, 8255 PPI, MEMSW/MEMSWR memory swap (including CP/M bank switching), physical+virtual mixed mode support, and support for RFS, MZ80AFI, MZ-1E14, MZ-1E19, MZ-1R12, MZ-1R18 sub-interfaces
MZ2000.c MZ2000 Sharp MZ-2000 — BST/NST memory mode switching, character + graphics VRAM overlay, 8253 PIT, 8255 PPI, Z80 PIO, MB8866 FDC. Supports both physical mode (drop-in Z80 replacement with automatic boot/normal mode detection) and virtual mode (full PSRAM-based emulation with IPL ROM mirroring)
MZ1500.c MZ1500 Sharp MZ-1500 — MZ-700 superset with inbuilt Quick Disk drive, PCG, stereo PSG sound (SN76489AN), Z80 PIO printer interface, 8253 PIT, DIP switch MZ-700/MZ-1500 mode selection. Sub-interfaces: RFS, MZ-1E05, MZ-1E14, MZ-1E19, MZ-1R12, MZ-1R18, MZ-1R23, MZ-1R37, PIO-3034, Celestite
MZ80AFI.c MZ80AFI Sharp MZ-80A floppy interface — emulates the MZ-80A AFI floppy disk controller
WD1773.c WD1773 WD1773 FDC — 80-track, 2-head, 8-sector DSK/RAW/D88 images
QDDrive.c QDDRIVE Sharp QuickDisk drive — full Z80 SIO/2 emulation with spiral track data, motor control, and async SD card I/O
RFS.c RFS ROM Filing System — MZF loading, CP/M, BASIC from SD card
TZFS.c TZFS TranZPUter Filing System (work in progress)
MZ-1E05.c MZ1E05 Sharp MZ-1E05 floppy disk interface unit (WD1773-based)
MZ8BFI.c MZ8BFI / E0054PA MZ-2000 floppy disk interface — MB8866 FDC without driver ROM (code in IPL). D88 format support.
MZ-1E14.c MZ1E14 MZ-1E14 QuickDisk controller with BIOS ROM (MZ-700/MZ-800)
MZ-1E19.c MZ1E19 MZ-1E19 QuickDisk controller without BIOS ROM
MZ-1R12.c MZ1R12 32KB battery-backed RAM board (persisted to SD card)
MZ-1R18.c MZ1R18 64KB RAM expansion board
MZ-1R23.c MZ1R23 MZ-1R23 128KB Kanji ROM (16×16 JIS patterns) and MZ-1R24 256KB Dictionary ROM. ROM files loaded from SD card. I/O ports B8h–B9h with auto-increment read
MZ-1R37.c MZ1R37 MZ-1R37 640KB EMM (Expanded Memory Manager) — 20-bit address space with I/O port address latching
PIO-3034.c PIO3034 IO DATA PIO-3034 320KB EMM — 19-bit address counter with auto-increment data port
Celestite.c Celestite Celestite composite board — Wiznet W5100 Ethernet controller (register emulation), interrupt controller, UFM, integrated MZ-1R12 32KB CMOS RAM (expandable to 64KB), optional MZ-1R37 640KB EMM. I/O ports 60h–6Fh
PIT8253.c PIT8253 Standalone Intel 8253 PIT emulation — all six counter modes, BCD/binary, latch, LSB/MSB read/load
PPI8255.c PPI8255 Standalone Intel 8255 PPI emulation — Mode 0 I/O, bit set/reset, output callbacks, input injection

Virtual Device Framework

The FUNC block type enables arbitrary I/O emulation by calling C handler functions on each bus access. Any 512-byte block of memory or range of I/O ports can be backed by a function.

Handler Function Signatures

Memory FUNC handlers are stored in the memioPtr table in PSRAM. I/O FUNC handlers are stored in the ioPtr table. The function signatures are:
/* Memory read handler */
uint8_t mem_read_handler(uint16_t addr, void *ctx);

/* Memory write handler */
void mem_write_handler(uint16_t addr, uint8_t data, void *ctx);

/* I/O read handler */
uint8_t io_read_handler(uint8_t port, void *ctx);

/* I/O write handler */
void io_write_handler(uint8_t port, uint8_t data, void *ctx);
Handler functions are called directly from Core 1's hot loop. They must complete before the current bus cycle's wait states expire — keep handlers short and avoid any blocking operations (file I/O, UART, etc.). If a handler needs to trigger a longer operation (e.g. load a disk sector), it should post a message to Core 0 via the inter-core queue and return immediately with a status byte, deferring the actual I/O to Core 0.

Writing a New Driver

To add support for a new peripheral or host machine:
  1. Create a new .c / .h file in the src/drivers/ directory.
  2. Implement read and write handler functions matching the signatures above.
  3. Register the handler function pointers in the memioPtr or ioPtr tables during driver initialisation.
  4. Add the driver to the CMakeLists.txt build target.
  5. Add a type string entry so that the JSON configuration parser can instantiate the driver by name.
  6. Document the driver's param keys in your driver's header file.
The driver initialisation function is called once at boot, after config.json is parsed. The driver receives a pointer to its interface configuration block and should set up any internal state and register its handlers at this point.

ICE (Debug Shell)

The picoZ80 includes a built-in ICE (In-Circuit Emulator) debug shell on USB CDC Channel 1 (the second serial port enumerated when the board is connected via USB). The shell runs on Core 0 and communicates with the Core 1 emulation loop via shared flags in the Z80CPU context structure. The debug shell is only available in the DBGSH firmware variant, which is compiled with the INCLUDE_DBGSH define.

Architecture

  • Input/Output: USB CDC Channel 1 at 115200 baud. The shell prompt is dbg> . Command history (16 entries) and character echo are supported.
  • Breakpoints: Up to 8 simultaneous breakpoints stored in cpu->dbgBpAddr[]. Core 1 checks the breakpoint array before each opcode fetch; on a hit, it sets cpu->hold = true and signals Core 0 via dbgBpHit.
  • Single-step: The step command sets cpu->dbgStepCount. Core 1 decrements this counter after each instruction, holding automatically when it reaches zero. Before/after register state and the disassembled instruction are displayed for each step.
  • Execution trace: A 512-entry ring buffer (cpu->dbgTrace[]) records PC, opcode, and flags register for each executed instruction when tracing is enabled. Each 32-bit entry packs [31:16]=PC, [15:8]=opcode, [7:0]=F register.
  • Memory access: Physical memory access (dm p, wm p) drives real Z80 bus cycles via the PIO state machines. Virtual access (dm v, wm v) reads/writes PSRAM directly. Auto mode (wm without qualifier) follows the memory map. RP2350 access (dm r) reads the host microcontroller's address space with range validation.
  • Hold/Release: The hold command sets cpu->hold = true. Core 1 acknowledges via cpu->holdAck, ensuring the CPU is quiescent before the shell accesses shared state.

Command Reference

Command Syntax Description
help help List all commands
regs regs Dump all Z80 registers, flags, and cycle count
dm dm <p|f|v|r> <addr> [len] Dump memory (physical / fetch / virtual / RP2350)
cmp cmp [f] <phys> <virt> <len> Compare physical bus memory with virtual PSRAM
dis dis [p|v] [addr] [count] Disassemble Z80 code
asm asm [addr] Interactive Z80 assembler
memmap memmap [block] Show memory bank pointer table
memptr memptr [addr] Show PSRAM memPtr table
iomap iomap [port] Show I/O port handler table
status status System status (CPU freq, PSRAM, uptime)
ver ver Firmware version and partition info
drivers drivers List active drivers and interfaces
hold hold Pause CPU emulation
release release Resume CPU emulation
go go Continue (release hold, breakpoints active)
cont cont Alias for go. Continue execution (release hold, breakpoints active)
step step [n] Single-step n instructions
bp bp <addr> Set breakpoint (max 8)
bc bc <n|*> Clear breakpoint n or all
bl bl List breakpoints
wm wm [p|v] <addr> <byte>... Write to memory (physical/virtual/auto)
fill fill [p|v] <addr> <len> [w|d] <val> Fill memory with a constant value
copy copy <pv|fp|vp> <src> <len> <dst> Copy memory between physical and virtual
memtest memtest <addr> <len> [pattern] Test physical memory (write+read, write+fetch, interleaved)
in in <port> Read Z80 I/O port
out out <port> <byte> Write Z80 I/O port
trace trace <on|off|dump [n]|clear|rt|byte ...> Execution trace control; rt enables real-time trace output; byte enables byte-level tracing
verify verify <on|off> Toggle full opcode fetch verification
fwait fwait <0-4> Force extra M1 (opcode fetch) wait states; 0 = off (default)
iowait iowait <0-8> Force extra I/O cycle wait states; 0 = off (default)
corrupt corrupt [clear] Display or clear detected fetch corruptions
fdctrace fdctrace <on|off|dump> Enable/disable FDC I/O trace; dump shows last 64 operations
qdtrace qdtrace <on|off|dump> Enable/disable Quick Disk I/O trace; dump shows last 64 operations
piodbg piodbg [clear] Display RP2350 PIO hardware diagnostics (FDEBUG, FSTAT, FIFO, PCs, GPIO); clear resets sticky flags
load load <p|v> <file> <addr> [len] [ofs] Load a file from the ESP32 SD card into Z80 memory. p = physical bus, v = virtual PSRAM bank 0. file relative to /sdcard/. If len is omitted the entire file is loaded (up to 64KB); if specified, max 1MB. Optional ofs for file offset. Auto-holds CPU for physical writes. Uses PSRAM bank 63 as scratch buffer
save save <p|pf|v> <file> <addr> <len> Save Z80 memory to a file on the ESP32 SD card. p = physical bus, pf = physical fetch (M1), v = virtual PSRAM bank 0. file relative to /sdcard/. Max 64KB. Auto-holds CPU for physical reads. Periodic DRAM refresh during physical reads
dir dir [path] List files on the ESP32 SD card. Optional path is relative to /sdcard/. Shows filenames and sizes
echo echo [on|off] Toggle terminal echo
reset reset Force Z80 reset
set set <reg|flags|memmap|memptr|iomap> <idx> <val> Modify Z80 register, flags, memory map, memPtr, or I/O map entry at runtime
hist hist [n] Display command history (preserved across sessions via ESP32 NVS)
savehst savehst Force-save command history to ESP32 NVS

ESP32 Co-processor

The ESP32-S3-PICO-1 module acts as a co-processor handling all network and storage functions. It communicates with the RP2350 via two interfaces:
  • FSPI (50MHz, 4-wire SPI) — Binary IPC Protocol v1.1 — high-speed bulk data transfer (ROM images, disk sector reads/writes, config file download). The protocol uses a fixed 64-byte binary frame header with CRC32 integrity checking (replacing the earlier XOR checksum). DMA channels are pre-allocated at initialisation and never released, eliminating per-transfer claim/unclaim overhead and race conditions. Burst sector transfers allow up to 16 × 512-byte sectors (8KB) in a single SPI transaction, significantly improving floppy and QuickDisk image load times. The RX DMA channel is elevated to HIGH PRIORITY to prevent FIFO overflow caused by Core 1 PSRAM QMI bus contention.
  • UART (460.8kbaud) — command/response protocol for control messages, status queries, and short data exchanges.

Networking Modes

The ESP32 firmware supports three networking modes, selected at build time via pre-built sdkconfig files:
Mode Config File WiFi USB NCM Console FCC/RED Required
WiFi Only sdkconfig.mode_wifi_only Yes No USB Serial/JTAG Yes
WiFi + NCM sdkconfig.mode_wifi_and_ncm Yes Yes TinyUSB CDC-ACM Yes
NCM Only sdkconfig.mode_ncm_only No Yes TinyUSB CDC-ACM No
USB NCM (Network Control Model) presents a CDC-NCM Ethernet adapter on the ESP32-S3 USB OTG port (GPIO 19/20). A composite USB device exposes both a CDC-ACM serial port (for debug logging) and the NCM network interface. The built-in DHCP server assigns the host an IP address from the 192.168.7.0/24 subnet, with the picoZ80 accessible at 192.168.7.1. Lease time is 120 minutes.
In WiFi+NCM mode, the HTTP server binds to INADDR_ANY:80 and serves both interfaces simultaneously. WiFi connects asynchronously so the USB NCM interface is available immediately at power-on.
When only NCM is enabled, the WiFi radio is completely disabled, the WiFi Manager page is removed from the web interface, and the Dashboard status panel title changes from "WiFi Configuration" to "Network Configuration" (showing USB NCM status instead of SSID/WiFi details). The ESP32-S3 antenna matching network does not need to be populated on the PCB.

SD Card Interface

The ESP32 manages the SD card via its SPI interface. The SD card is mounted as FAT32 and all file access from the RP2350 is mediated by the ESP32 — the RP2350 sends file I/O commands over the FSPI/UART link and the ESP32 performs the actual FAT32 read/write operations.
The SD card is also directly accessible to the ESP32 web server, which serves files from the webfs/ directory and allows the File Manager to browse and modify the card contents via HTTP.

Web Server

The ESP32 runs an HTTP server on port 80 (no TLS — local network use only). All web assets (HTML, CSS, JavaScript) are served from the webfs/ directory on the SD card, allowing the web interface to be updated without reflashing the ESP32 firmware. The web server handles:
  • Serving static web assets from the SD card webfs/ directory.
  • REST API endpoints for JSON data (system status, config read/write, file operations).
  • OTA firmware upload endpoints for both the RP2350 and ESP32.
  • WebSocket connection for real-time Dashboard status updates.

RP2350 ↔ ESP32 Command Protocol

The RP2350 (Core 0) communicates with the ESP32 using a simple command/response protocol over the UART link. Commands are single-byte opcodes with optional payload bytes. The ESP32 acknowledges each command with a status byte followed by any response data.
Common command categories:
  • File I/O — open, read, write, close, directory listing, file stat.
  • Config — request config.json content, write updated config, reload request.
  • Disk — mount/unmount disk image, read/write sector (relayed from WD1773 and QDDrive emulation). The currently mounted floppy and QuickDisk image filenames are tracked by the ESP32 and displayed in the web interface Actions menu.
  • System — version query, reboot request, NVS read/write.
The FSPI interface is used for bulk transfers where the payload is too large for the UART (ROM image uploads, disk sector data), while the UART handles all control commands.

Watchdog and Boot Diagnostics

The RP2350 firmware uses a hardware watchdog timer to detect and recover from boot-time hangs and main-loop stalls. The watchdog is enabled early in the boot sequence with a 30-second timeout and is kicked (watchdog_update()) at each major milestone. If any boot stage or main-loop iteration takes longer than the timeout, the watchdog resets the RP2350 automatically.

Boot Progress Tracking

Boot progress is tracked using the RP2350's watchdog scratch registers, which survive watchdog resets (but not power-on resets). This allows the firmware to determine, after a watchdog reset, exactly which boot stage was reached before the hang.
Scratch Register Name Contents
scratch[0–3] Boot history Last four reset attempts — each entry encodes (attempt_count << 24) | (stage << 16) | (resetCause & 0xFFFF). Entries shift on each watchdog reset: [0]←[1]←[2]←[3]←current.
scratch[4] SPI diagnostics Packed diagnostic counters for the FSPI link: breadcrumbs, message type, T1/T3 timeout counters, bad frame count, and OK count.
scratch[5] Magic marker Set to 0xB00710BE to indicate that the scratch registers contain valid boot progress data.
scratch[6] Current stage The most recent boot stage code (see table below).
scratch[7] Reset cause The reset cause code from the hardware reset controller.
Boot stage codes progress from 0x01 (start) through to 0x10 (main loop entered). Sub-stages within the main loop (0x11–0x17) and inter-core command processing (0x20–0x27) provide fine-grained tracking:
Code Stage Description
0x01 BOOTP_START Entry point reached
0x02 BOOTP_CLK_SET System clock configured
0x03 BOOTP_PSRAM_INIT PSRAM initialisation started
0x04 BOOTP_PSRAM_OK PSRAM initialised successfully
0x05 BOOTP_STDIO_INIT USB stdio initialised
0x06 BOOTP_PIO_INIT PIO state machines loaded
0x07 BOOTP_Z80_INIT Z80 CPU context initialised
0x08 BOOTP_USB_INIT USB bridge initialised
0x0A BOOTP_ESP_HS_SYNC ESP32 SPI handshake sync
0x0B BOOTP_CORE1_LAUNCH Core 1 launched
0x0D BOOTP_FSPI_INIT FSPI binary IPC initialised
0x0E BOOTP_ESP_INIT ESP32 communication ready
0x10 BOOTP_MAIN_LOOP Main loop entered
0x11–0x17 Main loop sub-stages USB poll, inter-core, SPI NOP/CMD, tasks
0x20–0x27 Inter-core commands Floppy load, QD load, RAMFILE load, file I/O

PSRAM Persistent Log (plogf)

The last 4KB of the 8MB PSRAM (address 0x117FF000) is reserved for a persistent debug log that survives watchdog resets. The plogf() macro writes printf-style messages to this buffer during boot, before USB becomes available for normal debugf() output. On the next successful boot, the dump_plog() function outputs any captured messages to the debug console and then clears the buffer. The log uses a simple structure: a 4-byte magic marker (0x504C4F47 = "PLOG"), a 4-byte length counter, and a 3840-byte circular text buffer.

Fault Diagnostics

The firmware installs Cortex-M33 fault handlers for hard faults, memory management faults, bus faults, and usage faults. When a fault occurs, the handler saves a complete diagnostic snapshot to the last 256 bytes of PSRAM (address 0x117FFF00) with a magic marker (0xFA017000), the fault type, all relevant registers (PC, LR, SP, R0–R3, R12, PSR), the Configurable Fault Status Register (CFSR), Hard Fault Status Register (HFSR), Bus Fault Address Register (BFAR), Memory Management Fault Address Register (MMFAR), and the core ID. The handler then enters an infinite loop, allowing the watchdog to trigger a reset. On the next boot, the firmware checks for a valid fault diagnostic and outputs the captured information via debugf(), enabling post-mortem analysis without requiring a live debugger session.

Flash Configuration Clear

The RP2350 OTA update mechanism supports two additional operations beyond firmware upload:
  • Clear App Config (FW_CFGCLEAR_ID = 0xB1D7E5FA) — erases the App Config partition (ROM images and minified JSON) associated with the target firmware slot. This forces the firmware to re-read config.json from the SD card on next boot, which is necessary when the configuration schema has changed between firmware versions.
  • Clear Flash Header (FW_HDRCLEAR_ID = 0xC2E8F6AB) — resets the flash partition header to factory defaults. The bootloader configuration (partition 0) is preserved, but all application partition metadata is rebuilt from scratch. Use this when the partition table has become corrupted or when downgrading to an earlier firmware version that expects a different partition layout.
Both operations are triggered from the RP2350 OTA web page checkboxes and are executed by the bootloader during the firmware update process.

SWD Debugging — RP2350

The RP2350 supports full source-level debugging over ARM Serial Wire Debug (SWD). Connect a CMSIS-DAP compatible probe (Raspberry Pi Debug Probe, Black Magic Probe, or similar) to Pins 1 (SWCLK), 2 (SWDIO), and 5 (GND) of the debug header.

OpenOCD Setup

The picoZ80 requires a small modification to the standard OpenOCD RP2350 target script to enable SMP debugging with separate GDB ports per core:
sudo cp /usr/local/share/openocd/scripts/target/rp2350.cfg \
        /usr/local/share/openocd/scripts/target/rp2350_tzpu.cfg
Edit rp2350_tzpu.cfg — find the target smp line inside the if {[string compare $_USE_CORE SMP] == 0} block and remove the leading #:
# Before:
    #target smp $_TARGETNAME_0 $_TARGETNAME_1

# After:
    target smp $_TARGETNAME_0 $_TARGETNAME_1
This single change causes OpenOCD to register Core 0 on GDB port 3333 and Core 1 on GDB port 3334, allowing independent per-core GDB sessions. Launch OpenOCD before starting GDB:
openocd -f interface/cmsis-dap.cfg -f target/rp2350_tzpu.cfg -c "adapter speed 5000"

GDB Configuration

Add the following to ~/.gdbinit (with absolute paths matching your project location) to permit auto-loading of per-directory .gdbinit files:
set history save on
set history filename ~/.gdb_history
set history size 65536
add-auto-load-safe-path /path/to/project/build/bin/model/BaseZ80/.gdbinit
add-auto-load-safe-path /path/to/project/build/bin/model/Bootloader/.gdbinit
Debugging the Bootloader
# Terminal 1 — Core 0 (port 3333)
cd build/bin/model/Bootloader
cp ../../../../.gdbinit.bootloader.3333 .gdbinit
gdb-multiarch Bootloader.elf

# Terminal 2 — Core 1 (port 3334)
cd build/bin/model/Bootloader
cp ../../../../.gdbinit.bootloader.3334 .gdbinit
gdb-multiarch Bootloader.elf
Debugging the Main Firmware
# Terminal 1 — Core 0 (port 3333)
cd build/bin/model/BaseZ80
cp ../../../../.gdbinit.3333 .gdbinit
gdb-multiarch BaseZ80_0x10020000.elf

# Terminal 2 — Core 1 (port 3334)
cd build/bin/model/BaseZ80
cp ../../../../.gdbinit.3334 .gdbinit
gdb-multiarch BaseZ80_0x10020000.elf

# Memory dump (from GDB prompt) — hex + ASCII:
(gdb) xac 0x20000000 64
The xac <address> <count> GDB command is defined in the .gdbinit.3333 / .gdbinit.3334 files. It dumps memory as combined hex and ASCII output and is useful for inspecting PSRAM bank contents and memory-mapped device state.

ESP32 USB Debugging

The ESP32-S3 co-processor has a built-in USB-JTAG interface — no external debug probe is required. Connect a USB cable from the host PC directly to the ESP32 USB port on the picoZ80 board.
# Start OpenOCD for ESP32-S3
openocd -f board/esp32s3-builtin.cfg

# In a second terminal — launch Xtensa GDB
xtensa-esp32s3-elf-gdb esp32/build/main.elf
(gdb) target extended-remote :3333
Ensure the ELF was built from the same source revision as the firmware running on the device, so that symbols and addresses align correctly.

Build System

The picoZ80 firmware uses CMake with the Raspberry Pi Pico SDK 2.x. The build system produces the Bootloader and the Application firmware. The application is built in four variants — two partitions (Partition 1 at 0x10020000, Partition 2 at 0x10520000) each in standard and DBGSH configurations. The DBGSH variants add INCLUDE_DBGSH to the compile flags, enabling the full debug shell on USB CDC Channel 1. The ESP32 firmware is built separately using ESP-IDF v5.4, managed via Docker.

CMake Build Targets

Target Output Flash Address Notes
Bootloader Bootloader.elf, Bootloader.uf2 0x10000000  
BaseZ80_0x10020000 BaseZ80_0x10020000.elf, .bin 0x10020000 (Slot 1) Standard (no debug shell)
BaseZ80_0x10520000 BaseZ80_0x10520000.elf, .bin 0x10520000 (Slot 2) Standard (no debug shell)
BaseZ80_DBGSH_0x10020000 BaseZ80_DBGSH_0x10020000.elf, .bin 0x10020000 (Slot 1) DBGSH — includes ICE debug shell
BaseZ80_DBGSH_0x10520000 BaseZ80_DBGSH_0x10520000.elf, .bin 0x10520000 (Slot 2) DBGSH — includes ICE debug shell

Key CMake Build Flags

Flag Effect
INCLUDE_SHARP_DRIVERS Compiles in all Sharp MZ peripheral drivers (MZ700, WD1773, QDDrive, RFS, TZFS, MZ-1E05, MZ8BFI, MZ-1E14, MZ-1E19, MZ-1R12, MZ-1R18).
INCLUDE_DBGSH Compiles in the ICE debug shell on USB CDC Channel 1. Present in DBGSH build variants only.
CMAKE_BUILD_TYPE=Debug Enables debug symbols and disables optimisation. Required for source-level GDB debugging.
CMAKE_BUILD_TYPE=Release Full optimisation (-O3). Used for production firmware.

Build Commands

# First time: clone and build the SDK
./get_and_build_sdk.sh

# Standard release build (RP2350 only)
./build_tzpuPico.sh

# Debug build
./build_tzpuPico.sh DEBUG

# Full build: RP2350 + ESP32 (ESP32 built via Docker)
./build_tzpuPico.sh ALL

# ESP32 only, using the Docker idf54 alias
cd projects/tzpuPico/esp32
idf54 build
The build_tzpuPico.sh script automatically increments the version number on a successful build and copies versioned output files to fw/uf2/ (bootloader UF2) and fw/bin/ (application binary for OTA). The Bootloader UF2 is used for initial USB mass-storage flashing only. Application slot binaries use plain binary format (not UF2) because they reside at non-standard flash addresses.

ESP32 Networking Mode Selection

To switch networking mode, copy the appropriate pre-built configuration file to sdkconfig before building:
cd esp32/
cp sdkconfig.mode_ncm_only sdkconfig    # NCM only (FCC/RED safe)
# or: cp sdkconfig.mode_wifi_only sdkconfig
# or: cp sdkconfig.mode_wifi_and_ncm sdkconfig
idf.py build
idf.py flash

Reference Sites

Resource Link
picoZ80 project page /picoz80/
picoZ80 User Manual /picoz80-usermanual/
pico6502 project page /pico6502/
RP2350 Datasheet datasheets.raspberrypi.com
RP2350 PIO Reference datasheets.raspberrypi.com — Appendix B
Pico SDK Documentation raspberrypi.github.io/pico-sdk-doxygen
ESP32-S3 Technical Reference docs.espressif.com
ESP-IDF Programming Guide docs.espressif.com/esp-idf
Zilog Z80 CPU User Manual zilog.com
OpenOCD Documentation openocd.org
X (Twitter) project preview engineerswork1

Wireless Regulatory Notice

This device incorporates an ESP32-S3-PICO-1 wireless module that is capable of transmitting in the 2.4 GHz ISM band, making it an intentional radiator under radio-frequency regulations worldwide (including FCC Part 15 Subpart C in the United States, and the Radio Equipment Directive 2014/53/EU in the European Union) when WiFi firmware is installed.
As-shipped Configuration
As shipped, the picoZ80 board is flashed with the NCM-only firmware (sdkconfig.mode_ncm_only). In this configuration the WiFi radio is completely disabled and the ESP32-S3 antenna matching network components are not populated on the PCB. Because no RF transmission occurs, the device is not an intentional radiator and does not require FCC, CE/RED, or equivalent certification. It may be sold, distributed, or gifted without regulatory authorisation.
Adding WiFi
End users may populate the antenna matching network and flash WiFi-enabled firmware (sdkconfig.mode_wifi_only or sdkconfig.mode_wifi_and_ncm) for personal, experimental, or educational use. Once WiFi is enabled, the device becomes an intentional radiator and the following rules apply:
Although the ESP32-S3-PICO-1 module itself carries pre-existing regulatory certifications (FCC, CE, and others), those module-level certifications do not automatically extend to a finished product that incorporates the module. The pre-certified module exemption permits individual hobbyists to build a limited number of devices for personal, experimental, or educational use without obtaining separate equipment authorisation.
Important Limitations
  • Devices with WiFi firmware must not be sold, offered for sale, gifted, or otherwise distributed to third parties unless the finished product has been independently tested and granted its own equipment authorisation (e.g. FCC ID, CE marking with a Notified Body assessment) in the relevant jurisdiction.
  • Building this project with WiFi enabled for personal use in limited quantities is generally permitted under hobbyist and experimental-use provisions (e.g. FCC § 15.23), provided the device does not cause harmful interference.
  • Commercial sale with WiFi enabled requires full product-level FCC/RED (or equivalent) certification.
  • Regulatory requirements vary by country. Builders outside the United States should consult their national radio-frequency authority for applicable rules.
Builder’s Responsibility
It is the builder’s sole responsibility to ensure that any device constructed from these designs complies with all applicable radio-frequency regulations in their jurisdiction. The author provides these designs for personal, educational, and hobbyist use and makes no representation that a device built from them satisfies the regulatory requirements for commercial distribution.