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 MCU | RP2350B (QFN-80) | Dual Cortex-M33, 150MHz (up to 300MHz OC), 512KB SRAM, 12 PIO state machines, 48 GPIO pins |
| Flash | W25Q128 (16MB SPI) | Bootloader, dual firmware slots, config partitions |
| PSRAM | 8MB SPI PSRAM | 64 × 64KB RAM/ROM banks for Z80 address space |
| Co-processor | ESP32-S3-PICO-1 | WiFi, SD card, web server, OTA |
| USB hub | CH334F | USB hub, firmware update bridging |
| Power supply | TLV62590BV | 5V → 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:
The - 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
outorpull. - 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 clearto 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.setuses an immediate 5-bit value;outshifts 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
/RDand/WRsimultaneously with other operations. - JMP PIN — conditional jump based on a designated GPIO pin level. Used to test
/WAIT,BUSREQ,/NMI, and/RESET.
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:
State Machine Coordination
- 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).
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
Z80 Fetch Cycle (M1 Cycle) — Step by Step
wait 0 irq N to sleep until the relevant event occurs, consuming zero CPU cycles while waiting.
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:
Memory Read and Write Cycles
- T1 rising edge —
z80_addrSM outputs the PC value onto A0–A15.z80_fetchasserts/M1low viaset pins. - T1 falling edge —
/MREQand/RDare asserted low (viaset pinsand side-set). The address is now valid and the memory system can begin responding. - T2 — the SM enters a wait-state loop: it waits for CLK rising then falling edge, then checks the
/WAITpin viajmp pin. If/WAITis low, the SM loops (adding Tw cycles). If high, it proceeds to T3. - T3 rising edge —
in pins, 8samples D0–D7 (the opcode byte) and pushes it into the RX FIFO. IRQ 1 is set to signalz80_data/z80_addrthat the refresh address should now be output./M1,/MREQ, and/RDare deasserted;/RFSHis asserted low. - T3 falling edge —
/MREQis asserted again (for the refresh row strobe). - T4 — refresh continues. At the end of T4,
/MREQand/RFSHare deasserted. The cycle SM returns tostart_cycleready for the next bus transaction.
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
I/O Read and Write Cycles
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.
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
BUSREQ / BUSACK Handling
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.
The
Clock Synchronisation
z80_busrq SM (PIO 2 SM 0) monitors the host /BUSREQ input pin. When /BUSREQ goes active (low), the SM:
- Sets IRQ 6 to signal the cycle SM that a bus request is pending.
- Waits for the current bus cycle to complete (
wait 1 irq 0). - 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
/BUSACKlow. - Spins on
jmp pinuntil/BUSREQgoes inactive (high). - Pulls a second 32-bit word to restore normal pin directions and deassert
/BUSACK. - Clears IRQ 6, allowing the cycle SM to resume.
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.
All cycle-type SMs synchronise to the host Z80 clock using
Interrupt Acknowledge Cycle
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_syncSM (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_syncSM (PIO 2 SM 2) regenerates the host clock on a separate GPIO, providing a clean clock output for external monitoring or logic analyser triggering.
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:
/M1is asserted at T1 (like a fetch), but/IORQis asserted instead of/MREQat 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.
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.
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:
- Create a new
.c/.hfile in thesrc/drivers/directory. - Implement read and write handler functions matching the signatures above.
- Register the handler function pointers in the
memioPtrorioPtrtables during driver initialisation. - Add the driver to the CMakeLists.txt build target.
- Add a type string entry so that the JSON configuration parser can instantiate the driver by name.
- Document the driver's
paramkeys in your driver's header file.
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 setscpu->hold = trueand signals Core 0 viadbgBpHit. - Single-step: The
stepcommand setscpu->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 (wmwithout qualifier) follows the memory map. RP2350 access (dm r) reads the host microcontroller's address space with range validation. - Hold/Release: The
holdcommand setscpu->hold = true. Core 1 acknowledges viacpu->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.
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-readconfig.jsonfrom 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.
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/.gdbinitDebugging 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.elfDebugging 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 (
End users may populate the antenna matching network and flash WiFi-enabled firmware (
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.
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 WiFiEnd 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.
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.