Overview
The picoZ80 continues the tranZPUter theme, replacing a physical Z80 in a host or industrial computer with a faster CPU, more memory, virtual devices, networking (WiFi, BT), rapid application loading from SD card and WiFi management.
It is a custom PCB designed to drop directly into the Z80 DIP-40 CPU socket of any legacy Z80-based computer. Rather than using a discrete Z80 processor, the board hosts an RP2350B microcontroller — a dual-core 150MHz Cortex-M33 device capable of running at up to 300MHz — whose programmable I/O (PIO) state machines take full, cycle-accurate control of the Z80 address, data, and control buses.
The picoZ80 is not a simple emulator adapter. Every bus transaction is handled in real time by the RP2350's PIO engines, giving the host system exactly the same bus timing it would see from a real Z80. At the same time, the RP2350's second core and abundant on-chip SRAM, combined with 8MB of external PSRAM and 16MB of Flash, allow an almost unlimited range of capabilities to be layered on top of the raw Z80 interface — including accelerated execution, virtualised memory, ROM banking, virtual disk drives, and full machine-persona emulation.
An ESP32 co-processor provides WiFi and Bluetooth connectivity, SD-card mass storage, and a browser-based management interface. All configuration is driven from a single human-readable config.json file stored on the SD card, meaning no recompilation is required to reconfigure the board's memory map, ROM images, or driver selection.
The picoZ80 has been demonstrated running within multiple Sharp MZ machines. A set of personas are being developed for these machines, indeed for other Z80 systems in time, to provide much needed features, such as banked RAM/ROM, floppy disk emulation, QuickDisk emulation, ROM Filing System, TranZPUter Filing System, all capable of being functional simultaneously. The configuration is entirely JSON-driven, adding support for a new Z80-based host is a matter of editing a configuration file and, where new I/O behaviour is required, adding a small C driver into the codebase.
- Drop-in Z80 replacement
- Cycle-accurate PIO bus interface
- Large memory space
- ROM/RAM banking
- Virtual device framework
- Machine personas
- Floppy and QuickDisk emulation
- WiFi and web management
- Dual firmware partitions
- USB firmware update
Hardware
The picoZ80 PCB (revision 2.5) is a compact, multi-layer board designed to fit within the physical footprint of the Z80 DIP-40 package and the clearance available inside typical retro-computer cases. All logic operates at 3.3V; level shifting and drive current considerations for the 5V host bus are handled in the schematic design.
The board integrates five subsystems on a single PCB: the RP2350B processor, the Z80 bus interface, the ESP32 co-processor, the power supply, and a USB hub.
- RP2350B (Cortex-M33 dual-core)
- 16MB SPI Flash
- 8MB PSRAM (SPI)
- ESP32 co-processor
- SD card slot
- USB hub
- 3.3V power supply
The picoZ80 hardware is designed in KiCad. The current revision is v2.5. Schematic and PCB layout files are available in the project repository under kicad/PICOZ80/.
The schematic is divided into five sheets:
All RP2350B GPIO assignments, decoupling, 12MHz crystal oscillator, 16MB Flash, and 8MB PSRAM connections. The RP2350B QFN-80 package is chosen specifically for its 48-GPIO count — the full Z80 bus (16 address + 8 data + 12 control signals) plus ESP32 SPI/UART and USB signals consume virtually every available pin.

ESP32-S3-PICO-1 module, SD card interface (SPI), chip antenna, debug header, and inter-processor communication lines (FSPI bus at 50MHz, UART at 460.8kbaud). The SD card signals and inter-processor SPI/UART are clearly separated in this sheet.

The 40-pin DIP socket connections and bus interface resistor network. Address lines A0–A15, data lines D0–D7, and all Z80 control signals (MREQ, IORQ, RD, WR, M1, RFSH, BUSREQ, BUSACK, HALT, INT, NMI, WAIT, CLK, RESET) are routed through series resistors to dedicated RP2350 GPIO pins monitored by the PIO state machines.

TLV62590BV 5V-to-3.3V synchronous buck converter with input/output filtering capacitors. The converter must supply the combined load of the RP2350B at up to 300MHz, 8MB PSRAM, ESP32, and USB hub from the single 5V VCC pin of the Z80 DIP-40 socket.

CH334F USB hub controller with Mini-B connector, 12MHz crystal, and downstream ports routed to both the RP2350 (for firmware update bridging) and the ESP32 (for direct USB access on newer board revisions).

The PCB was designed as small as possible to accommodate all the necessary circuity and fit within the limits of a DIP-40 socket.
The smallest components which could be manually assembled were used, ie. 0402/0603 passive devices and 0.5mm IC pitch spacing to reduce overall size and a 6 layer stackup selected to fit all required components.
Initial designs, v2.0 and v2.1 were manually assembled with point application of solder, manual part placement and a hot-air rework station. Version 2.2 was manually assembled with a stencil and reflow oven. v2.3a and v2.5 were assembled at a PCB Fab.
Click here to view an interactive PCB component placement diagram and Bill of Materials.
Architecture
Dual-Core Design
The RP2350B's two Cortex-M33 cores are given completely separate responsibilities, communicating through an intercore message queue (queue_t).
Core 0 handles all non-real-time tasks: USB bridge and CDC serial, firmware update coordination, file I/O (relayed to the ESP32 over UART), ESP32 command dispatch (floppy/QuickDisk image changes, config reloads, version queries), partition management, and watchdog supervision. A hardware watchdog timer monitors the boot sequence and main loop, with boot progress tracked through RP2350 scratch registers that survive watchdog resets. Comprehensive fault handlers capture register state and diagnostic information to PSRAM, enabling post-reset analysis of hard faults, bus faults, and usage faults. A persistent PSRAM log (plogf) captures boot-critical messages before USB becomes available, complementing the standard debugf debug output system.
Core 1 runs the CPU emulation hot loop exclusively. It services the PIO FIFOs to process Z80 bus transactions, resolves each address against the memory map, and either passes the transaction to physical host hardware (PHYSICAL type), services it from PSRAM (RAM/ROM types), or calls a virtual device handler function (FUNC type). Latency on this path is minimised by keeping the inner loop in SRAM and using the 512KB RP2350 SRAM as a fast look-up table for memory-block pointers.
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. The Z80 firmware uses all three PIO blocks:
- PIO 0 — Address and data bus (GPIO 0–23)
- PIO 1 — Control signals and cycle execution (GPIO 16–47)
- PIO 2 — Host timing, reset, refresh, and wait states
The full set of PIO programs in z80.pio, grouped by PIO block:
| PIO | Program | Function |
|---|---|---|
| 0 | z80_addr |
Outputs 16-bit address (A0–A15) onto the bus and signals cycle start. |
| 0 | z80_data |
Drives or samples D0–D7, with tri-state control during BUSRQ. |
| 0 | z80_cycle |
Top-level bus cycle sequencer — orchestrates fetch, read, write, and I/O cycles. |
| 0 | z80_fetch |
Opcode-fetch bus cycle (M1 + MREQ + RD). |
| 1 | z80_mem_read |
Memory read bus cycle (MREQ + RD). |
| 1 | z80_mem_write |
Memory write bus cycle (MREQ + WR). |
| 1 | z80_io_read |
I/O read bus cycle (IORQ + RD). |
| 1 | z80_io_write |
I/O write bus cycle (IORQ + WR). |
| 1 | z80_busrq |
Manages BUSREQ/BUSACK, releasing /IORQ, /MREQ, /RFSH, /M1, /HALT, /WR, /RD. |
| 1 | z80_nmi |
Detects NMI assertion and signals Core 1. |
| 1 | z80_clk_sync |
Synchronises PIO state machines to the Z80 CLK signal. |
| 1 | z80_int_ack |
Handles interrupt-acknowledge cycles (M1 + IORQ). |
| 2 | z80_reset |
Monitors the host RESET line and signals Core 1 to reinitialise emulation state. |
| 2 | z80_refresh |
Drives RFSH cycles on the host bus while the RP2350 services internal memory, keeping host DRAM refreshed. |
| 2 | z80_wait |
Inserts configurable T-cycle wait states on the host bus (controlled by tcycwait). |
| 2 | z80_sync |
Detects T1 on each bus cycle and signals Core 1 via IRQ, synchronising internal memory operations to the host clock. |
State machines communicate via PIO IRQ flags rather than polling, which eliminates inter-machine latency: IRQ 0 signals address/cycle start, IRQ 1 signals the data phase, IRQ 2 indicates T1 detection, IRQ 3 signals a RESET event, IRQ 4 signals NMI, and IRQ 6 signals an active BUSRQ.
Because 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 calls.
Memory accesses are resolved through three tiers of increasing latency:
Tier 1 — RP2350 SRAM (512KB, zero wait states)
| Type | Description |
|---|---|
PHYSICAL |
Pass-through to real host hardware — the RP2350 releases the bus and lets the physical host memory respond. |
PHYSICAL_VRAM |
As PHYSICAL but with additional wait states for host video RAM timing. |
PHYSICAL_HW |
Pass-through for host hardware registers. |
RAM |
Read/write — backed by PSRAM bank. |
ROM |
Read-only — backed by PSRAM bank; write cycles are silently ignored. |
VRAM |
PSRAM-backed video RAM; write cycles are also mirrored to the physical host VRAM. |
FUNC |
Virtual device — each access triggers a C function call, enabling arbitrary I/O emulation. |
PTR |
Per-byte redirect — each byte of the 512-byte block can point to any other block or type. |
The 16MB Flash is partitioned as follows:
| Partition | Address Range | Size | Contents |
|---|---|---|---|
| Bootloader | 0x10000000–0x1001FFFF |
128KB | USB bridge, firmware update, partition selector |
| App Slot 1 | 0x10020000–0x1051FFFF |
5MB | Main Z80 firmware (partition 1) |
| App Slot 2 | 0x10520000–0x10A1FFFF |
5MB | Main Z80 firmware (partition 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, checksums, metadata |
Each configuration slot can hold up to 64 ROM images and a 64KB minified JSON configuration. The active slot is recorded in the partition table and can be switched from the web interface or by holding the appropriate button during boot.
Machine Personas
The active persona is selected via the web interface Personality page or by editing config.json.
When the firmware is built with INCLUDE_SHARP_DRIVERS, the following peripheral drivers are compiled in and can be bound to any virtual hardware persona via the JSON configuration:
- MZ700.c — Sharp MZ-700 peripheral set
- WD1773.c — Floppy disk controller
- QDDrive.c — QuickDisk drive
- RFS.c — ROM Filing System
- TZFS.c — TranZPUter Filing System (work in progress)
- MZ-1E05.c — Floppy disk interface unit
- MZ-1E14.c — QuickDisk controller with BIOS ROM (MZ-700 / MZ-800)
- MZ-1E19.c — QuickDisk controller without BIOS ROM (MZ-800 / MZ-2000 / MZ-2200 / MZ-2500)
- MZ-1R12.c — 32KB battery-backed RAM board
- MZ-1R18.c — 64KB RAM board
Additional personas will be added in due course.
Multiple personas can coexist in the JSON configuration, each associated with a different PSRAM bank. Switching persona changes the active memory map and loaded ROM images without rebooting the host.
Build Instructions
Prerequisites- CMake 3.20+
- ARM GCC toolchain
- Docker
- Python 3
- Perl
All paths are relative to a user-chosen root directory, referred to here as <root>. The build scripts use a PICO_PATH variable at the top of each script which must be updated to match this root before first use. The expected layout after setup is:
<root>/
├── get_and_build_sdk.sh # clones and builds pico-sdk and pico-examples
├── build_tzpuPico.sh # builds the RP2350 firmware (and optionally ESP32)
├── picoZ80.h.tmpl # board definition template, copied into the SDK at build time
├── pico-sdk/ # cloned by get_and_build_sdk.sh
├── pico-examples/ # cloned by get_and_build_sdk.sh
└── projects/
├── Z80/ # Zeta Z80 emulator library (cloned manually)
└── tzpuPico/ # main picoZ80/pico6502 project (cloned manually)
mkdir -p <root>/projects
cd <root>/projects
# Clone the main tzpuPico project
git clone <tzpuPico-repo-url> tzpuPico
# Clone the Zeta Z80 emulator library
git clone <zeta-repo-url> Z80
Edit the PICO_PATH variable at the top of both get_and_build_sdk.sh and build_tzpuPico.sh to point to your chosen root directory:
export PICO_PATH=/your/chosen/root/
get_and_build_sdk.sh clones the Raspberry Pi Pico SDK (develop branch) and pico-examples (master branch) into the root, initialises all submodules, then builds the SDK against the RP2350 target. Run this once before the first firmware build, and again whenever you want to update the SDK.
cd <root>
./get_and_build_sdk.sh
The script clones into <root>/pico-sdk/ and <root>/pico-examples/, then builds the SDK with:
cmake -DPICO_BOARD=pimoroni_pga2350 -DPICO_PLATFORM=rp2350-arm-s -DPICO_SDK_PATH=<root>/pico-sdk/ ..
make
build_tzpuPico.sh handles the complete RP2350 build: it copies the picoZ80.h board definition into the SDK, backs up the current version, runs CMake and make -j4, increments the version number on a successful build, and copies the resulting firmware files into projects/tzpuPico/fw/uf2/ and projects/tzpuPico/fw/bin/ with version-stamped filenames. The fw/uf2/ directory holds the Bootloader UF2 image (used for initial USB mass-storage flashing); the fw/bin/ directory holds the application partition pure binary (.bin) images used for OTA updates. Application partitions are placed at non-standard flash addresses that the UF2 format cannot express, so plain binary is used for all OTA transfers.
The script accepts an optional argument:
cd <root>
# Standard release build (RP2350 only)
./build_tzpuPico.sh
# Debug build (RP2350 only, CMAKE_BUILD_TYPE=Debug)
./build_tzpuPico.sh DEBUG
# Full build — RP2350 firmware plus ESP32 firmware via Docker
./build_tzpuPico.sh ALL
The ESP32 firmware can also be built independently using Docker. Add the following alias to your shell profile (~/.bashrc or ~/.zshrc), then invoke idf54 from the esp32/ directory:
# Add to shell profile
alias idf54='docker run --rm --privileged \
--volume /dev:/dev \
--volume /sys:/sys:ro \
--volume /dev/bus/usb:/dev/bus/usb \
-v $PWD:/project \
-w /project \
-it espressif/idf:release-v5.4 idf.py "$@"'
cd <root>/projects/tzpuPico/esp32
idf54 build
# Firmware binary: build/tzpuPico_esp32.bin
# Upload via the OTA web page (ota-esp32.htm) or flash directly via USB
Flashing
Initial RP2350 FlashThere is no physical BOOTSEL or Reset button on the picoZ80 board. Both signals are exposed on the 6-pin debug header:
| Pin 1 | Pin 2 | Pin 3 | Pin 4 | Pin 5 | Pin 6 |
|---|---|---|---|---|---|
| SWCLK | SWD | Reset RP2350 | Reset ESP32 | GND | BOOTSEL |
To enter the RP2350 bootloader mass-storage mode, use a jumper or probe on the debug header:
- Hold Pin 6 (BOOTSEL) low.
- Apply power, or assert Pin 3 (Reset RP2350) low then release it — the RP2350 begins booting.
- Release BOOTSEL promptly after power-on or reset. Holding it low beyond the initial boot moment prevents the RP2350 from accessing FlashRAM.
- Connect the picoZ80 USB port to a PC — the RP2350 enumerates as a USB mass-storage device.
- Copy
Bootloader_<version>.uf2to the mounted drive. The RP2350 self-flashes the bootloader and reboots.
All subsequent RP2350 firmware updates can be performed via the OTA web page without touching the debug header.
The ESP32 is flashed using esptool via a Python virtual environment. On newer board revisions the ESP32 appears as its own USB device; on original boards with a single USB port it was accessible only through the RP2350 acting as a USB-UART bridge. In both cases Pin 4 (Reset ESP32) on the debug header is used to hold the ESP32 in reset during the RP2350 boot sequence when required.
Set up the esptool environment once:
python3 -m venv ./venv/
source ./venv/bin/activate
cd $HOME/esptool
Then flash all four ESP32 firmware components in one command, adjusting PORT to match the device node assigned by your OS and BINPATH to the directory containing the built binaries:
PORT=/dev/tty.usbmodem141403 # adjust to your system
BINPATH=/path/to/build/output
python3 ./esptool.py \
-p ${PORT} -b 115200 \
--before default_reset --after hard_reset \
--chip esp32s3 \
write_flash \
--flash_mode dio --flash_size 4MB --flash_freq 80m \
0x0 ${BINPATH}/bootloader.bin \
0x8000 ${BINPATH}/partition-table.bin \
0x9000 ${BINPATH}/ota_data_initial.bin \
0x10000 ${BINPATH}/sd_card.bin
All subsequent ESP32 firmware updates can be performed via the OTA web page (ota-esp32.htm) without requiring esptool.
Board revision note: Original picoZ80 boards (v2.0 to v2.2) have a single USB port connected to the RP2350. On these boards the ESP32 must be programmed via the RP2350 acting as a USB-UART bridge. Newer board revisions add a second USB port connected directly to the ESP32, allowing esptool to address it independently.
Format the SD card as FAT32. Place config.json in the root directory. Create subdirectories for ROM images, disk images, and filing system trees as referenced in your configuration. Once the board is running, the SD card can also be managed entirely through the web File Manager page.
Debugging
The picoZ80 supports full source-level debugging of both RP2350 cores and the ESP32 co-processor. The RP2350 is debugged over SWD using a CMSIS-DAP probe, with OpenOCD providing a two-target GDB server (one port per core). The ESP32-S3 is debugged over its built-in USB-JTAG interface using the Xtensa toolchain GDB.
RP2350 — SWD Debuggingsudo cp /usr/local/share/openocd/scripts/target/rp2350.cfg \
/usr/local/share/openocd/scripts/target/rp2350_tzpu.cfg
Then edit rp2350_tzpu.cfg — find the target smp line inside the if {[string compare $_USE_CORE SMP] == 0} block and remove the leading #:
# Before (rp2350.cfg):
#target smp $_TARGETNAME_0 $_TARGETNAME_1
# After (rp2350_tzpu.cfg):
target smp $_TARGETNAME_0 $_TARGETNAME_1
This single change activates SMP mode so that OpenOCD registers Core 0 on GDB port 3333 and Core 1 on port 3334, allowing each core to be attached and stepped independently. Launch OpenOCD from the project root before starting GDB:
openocd -f interface/cmsis-dap.cfg -f target/rp2350_tzpu.cfg -c "adapter speed 5000"
Global GDB initialisation (~/.gdbinit)
set history save on set history filename ~/.gdb_history set history size 65536 add-auto-load-safe-path build/bin/model/BaseZ80/.gdbinit:build/bin/model/Bootloader/.gdbinitBootloader Debugging
Copy the appropriate per-core .gdbinit file to the Bootloader build directory, then launch gdb-multiarch. The .gdbinit.bootloader.3333 file connects to Core 0 (port 3333) and logs output to gdb_core0.txt; .gdbinit.bootloader.3334 connects to Core 1 (port 3334) logging to gdb_core1.txt. Open two terminals to debug both cores simultaneously:
# Terminal 1 — Core 0 cd build/bin/model/Bootloader cp ../../../../.gdbinit.bootloader.3333 .gdbinit gdb-multiarch Bootloader.elf # Terminal 2 — Core 1 cd build/bin/model/Bootloader cp ../../../../.gdbinit.bootloader.3334 .gdbinit gdb-multiarch Bootloader.elfMain Firmware Debugging
The main firmware .gdbinit files (.gdbinit.3333 and .gdbinit.3334) define a custom xac <address> <count> command that dumps memory as combined hex and ASCII output, connect to the respective GDB port, and continue execution. This is useful for inspecting PSRAM bank contents and memory-mapped device state without halting the emulation loop:
# Terminal 1 — Core 0 cd build/bin/model/BaseZ80 cp ../../../../.gdbinit.3333 .gdbinit gdb-multiarch BaseZ80_0x10020000.elf # Terminal 2 — Core 1 cd build/bin/model/BaseZ80 cp ../../../../.gdbinit.3334 .gdbinit gdb-multiarch BaseZ80_0x10020000.elf # Memory dump example (in GDB prompt): (gdb) xac 0x20000000 64ESP32 — 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 a host PC directly to the ESP32 USB port on the picoZ80 board.
Start OpenOCD using the ESP32-S3 built-in JTAG configuration:
openocd -f board/esp32s3-builtin.cfg
Then launch the Xtensa GDB pointing at the ESP32 firmware ELF (located at esp32/build/main.elf relative to the project root) and attach to the OpenOCD GDB server:
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 flashed to the device so that symbols and addresses align correctly.
Configuration (JSON)
All picoZ80 behaviour is controlled by config.json on the SD card. The RP2350 reads this file at boot via the ESP32, minifies it, and stores the result in Flash. If no SD card is present, the previously stored configuration is used. The configuration can be edited directly in the browser using the Config Editor page.
The top-level structure is:
{
"esp32": {
"core": {
"device": "Z80",
"mode": 0
},
"wifi": {
"override": 1,
"wifimode": "client",
"ssid": "MyNetwork",
"password": "MyPassword",
"ip": "192.168.1.192",
"netmask": "255.255.255.0",
"gateway": "192.168.1.1",
"dhcp": 0,
"webfs": "webfs",
"persist": 0
}
},
"rp2350": {
"core": {
"cpufreq": 300000000,
"psramfreq": 133000000,
"voltage": 1.10
},
"z80": [
{
"memory": [ ... ],
"io": [ ... ],
"drivers": [ ... ]
}
]
}
}
The esp32 top-level object configures the ESP32 co-processor. It contains two sub-objects: core and wifi.
| Key | Type | Description |
|---|---|---|
device |
string | CPU device type — tells the ESP32 which processor personality to use. Valid values: "Z80" (picoZ80), "6502" (pico6502), "6512" (pico6512). |
mode |
integer | Default boot mode: 0 = client (station) mode, 1 = Access Point mode. This value is persisted in NVS and used on next boot if the WiFi manager has not overridden it. |
The wifi object provides a mechanism to inject WiFi credentials and network settings from config.json, overriding whatever is stored in NVS. This is useful for initial provisioning or for deploying a known-good network configuration without using the web WiFi Manager. Set override to 0 to ignore the config file entirely and rely on previously persisted NVS settings.
| Key | Type | Description |
|---|---|---|
override |
0/1 | Master switch. 1 = apply all settings below; 0 = ignore this block and use persisted NVS settings. |
wifimode |
string | "ap" for Access Point mode (ESP32 creates its own network); "client" for client/station mode (ESP32 joins an existing network). |
ssid |
string | WiFi network name (SSID) to create (AP mode) or join (client mode). |
password |
string | WiFi passphrase for the SSID. |
ip |
string | Fixed IP address (e.g. "192.168.1.192"). Used in both AP and client modes when dhcp is 0. |
netmask |
string | Subnet mask (e.g. "255.255.255.0"). |
gateway |
string | Default gateway address (e.g. "192.168.1.1"). |
dhcp |
0/1 | Client mode only. 1 = obtain address via DHCP; 0 = use the fixed ip/netmask/gateway above. |
webfs |
string | Override the web filesystem root directory on the SD card (default "webfs"). Allows alternate web UI assets to be served. |
persist |
0/1 | 1 = write the resolved WiFi settings back to NVS so they survive reboots even after override is cleared; 0 = apply for this session only. |
| Key | Type | Description |
|---|---|---|
cpufreq |
integer | RP2350 system clock frequency in Hz (e.g. 300000000 for 300 MHz). |
psramfreq |
integer | PSRAM SPI clock frequency in Hz (e.g. 133000000 for 133 MHz). |
voltage |
float | RP2350 core voltage in volts (e.g. 1.10). Higher clock speeds may require higher voltage. |
The memory array defines the Z80 memory map. Each entry covers a contiguous region of the 64KB Z80 address space, rounded to 512-byte block boundaries.
| Key | Type | Description |
|---|---|---|
enable |
0/1 | Whether this entry is active. |
addr |
hex string | Start address in the Z80 address space (e.g. "0x0000"). |
size |
hex string | Size of the region (e.g. "0x2000" for 8KB). |
type |
string | Block type: PHYSICAL, PHYSICAL_VRAM, PHYSICAL_HW, RAM, ROM, VRAM, FUNC, PTR. |
bank |
integer | PSRAM bank number (0–63) for RAM/ROM/VRAM types. |
tcycwait |
integer | Number of additional T-cycle wait states to insert on access. |
tcycsync |
integer | Enable synchronisation with T1 rising edge. |
task |
string | Optional task identifier for FUNC-type blocks. |
file |
string | SD-card path to a ROM image to load into this block at boot. |
fileofs |
integer | Byte offset within the ROM image file to start reading from. |
"memory": [
{
"enable": 1,
"addr": "0x0000",
"size": "0x1000",
"type": "ROM",
"bank": 0,
"tcycwait": 0,
"tcycsync": 0,
"task": "",
"file": "/TZFS/tzfs.rom",
"fileofs": 0
},
{
"enable": 1,
"addr": "0x1000",
"size": "0xCFFF",
"type": "RAM",
"bank": 0,
"tcycwait": 0,
"tcycsync": 0,
"task": "",
"file": "",
"fileofs": 0
},
{
"enable": 1,
"addr": "0xD000",
"size": "0x1000",
"type": "PHYSICAL_VRAM",
"bank": 0,
"tcycwait": 2,
"tcycsync": 0,
"task": "",
"file": "",
"fileofs": 0
}
]
The io array maps Z80 I/O port ranges to handlers. I/O cycles are distinguished from memory cycles by the Z80 IORQ signal, which the PIO control state machine monitors.
| Key | Type | Description |
|---|---|---|
enable |
0/1 | Whether this entry is active. |
addr |
hex string | Start I/O port address (e.g. "0xE0"). |
size |
hex string | Number of ports in the range. |
type |
string | PHYSICAL (pass to host), FUNC (call C handler). |
func |
string | Handler function name for FUNC type. |
"io": [
{
"enable": 1,
"addr": "0xE0",
"size": "0x08",
"type": "FUNC",
"func": "mz700_io"
},
{
"enable": 1,
"addr": "0x00",
"size": "0xE0",
"type": "PHYSICAL"
}
]
The drivers array binds named driver instances to the Z80 context. Each driver has one or more interfaces (listed under the "if" key), each of which can load ROM images, remap address ranges, remap I/O port ranges, and receive parameter files.
| Key | Type | Description |
|---|---|---|
enable |
0/1 | Whether this driver is loaded. |
name |
string | Driver name (must match a compiled-in driver, e.g. "MZ700", "RFS", "TZFS"). |
type |
string | PHYSICAL or VIRTUAL. |
if |
array | Array of interface objects (see below). |
Interface object (if[]):
| Key | Type | Description |
|---|---|---|
enable |
0/1 | Whether this interface is active. |
name |
string | Interface instance name. |
type |
string | PHYSICAL or VIRTUAL. |
rom |
array | ROM images to load into PSRAM at boot. |
addrmap |
array | Address remapping rules for this interface. |
iomap |
array | I/O port remapping rules for this interface. |
param |
array | Parameter files passed to the driver. |
rom[] entry:
| Key | Type | Description |
|---|---|---|
enable |
0/1 | Whether this ROM entry is active. |
file |
string | SD-card path to the ROM binary. |
loadaddr |
array | Load-address descriptors (position, addr, bank, size, wait states). |
addrmap[] entry:
| Key | Type | Description |
|---|---|---|
enable |
0/1 | Whether this mapping is active. |
srcAddr |
hex string | Source address in the Z80 space. |
size |
hex string | Size of the mapped region. |
dstAddr |
hex string | Destination address after remapping. |
iomap[] entry:
| Key | Type | Description |
|---|---|---|
enable |
0/1 | Whether this I/O mapping is active. |
srcAddr |
hex string | Source I/O port. |
size |
hex string | Number of ports. |
dstAddr |
hex string | Destination port after remapping. |
16bit |
0/1 | Whether 16-bit I/O addressing is used. |
"drivers": [
{
"enable": 1,
"name": "MZ700",
"type": "PHYSICAL",
"if": [
{
"enable": 1,
"name": "main",
"type": "PHYSICAL",
"rom": [
{
"enable": 1,
"file": "/MZ700/mz700.rom",
"loadaddr": [
{
"enable": 1,
"position": 0,
"addr": "0x0000",
"bank": 0,
"size": "0x1000",
"tcycwait": 0,
"tcycsync": 0
}
]
}
],
"addrmap": [
{
"enable": 1,
"srcAddr": "0x0000",
"size": "0x1000",
"dstAddr": "0x0000"
}
],
"iomap": [
{
"enable": 1,
"srcAddr": "0xE0",
"size": "0x08",
"dstAddr": "0xE0",
"16bit": 0
}
],
"param": [
{
"enable": 1,
"file": "/config/mz700.cfg"
}
]
}
]
},
{
"enable": 1,
"name": "MZ-1E05",
"type": "PHYSICAL",
"if": [
{
"enable": 1,
"name": "fdc0",
"type": "PHYSICAL",
"rom": [],
"addrmap": [],
"iomap": [
{
"enable": 1,
"srcAddr": "0xD8",
"size": "0x04",
"dstAddr": "0xD8",
"16bit": 0
}
],
"param": [
{
"enable": 1,
"file": "/DSK/MZ700/disk0.dsk"
}
]
}
]
}
]
The following is a minimal configuration that boots an MZ-700 with ROM, 48KB RAM, host VRAM, and the WD1773 floppy controller:
{
"rp2350": {
"core": {
"cpufreq": 300000000,
"psramfreq": 133000000,
"voltage": 1.10
},
"z80": [
{
"memory": [
{ "enable":1, "addr":"0x0000", "size":"0x1000", "type":"ROM",
"bank":0, "tcycwait":0, "tcycsync":0, "task":"",
"file":"/MZ700/mz700.rom", "fileofs":0 },
{ "enable":1, "addr":"0x1000", "size":"0xCFFF", "type":"RAM",
"bank":0, "tcycwait":0, "tcycsync":0, "task":"", "file":"", "fileofs":0 },
{ "enable":1, "addr":"0xD000", "size":"0x1000", "type":"PHYSICAL_VRAM",
"bank":0, "tcycwait":2, "tcycsync":0, "task":"", "file":"", "fileofs":0 },
{ "enable":1, "addr":"0xE000", "size":"0x2000", "type":"PHYSICAL",
"bank":0, "tcycwait":0, "tcycsync":0, "task":"", "file":"", "fileofs":0 }
],
"io": [
{ "enable":1, "addr":"0xE0", "size":"0x08", "type":"FUNC", "func":"mz700_io" },
{ "enable":1, "addr":"0xD8", "size":"0x04", "type":"FUNC", "func":"wd1773_io" }
],
"drivers": [
{
"enable":1, "name":"MZ700", "type":"PHYSICAL",
"if": [{ "enable":1, "name":"main", "type":"PHYSICAL",
"rom":[], "addrmap":[], "iomap":[], "param":[] }]
},
{
"enable":1, "name":"MZ-1E05", "type":"PHYSICAL",
"if": [{ "enable":1, "name":"fdc0", "type":"PHYSICAL",
"rom":[], "addrmap":[], "iomap":[],
"param":[{ "enable":1, "file":"/DSK/MZ700/disk0.dsk" }] }]
}
]
}
]
}
}
Web Interface
The ESP32 co-processor hosts a web management interface built with Bootstrap 4. Connect to the picoZ80's WiFi network (or configure client mode to join your existing network) and navigate to http://<device-ip>/ — by default http://192.168.4.1/ in Access Point mode.
On first power-up the board starts in WiFi AP mode. Use the WiFi Manager page to configure client mode and assign a fixed IP address on your network. All seven pages share a common left-hand navigation bar giving one-click access to Status, Config Editor, File Manager, Settings (Firmware → ESP32 / RP2350, WiFi Manager), and Persona.
The landing page shows the board's live state across three panels:
- WiFi Configuration
- Version Information
- RP2350 Partitions
Two dropdown menus in the top-right navbar are available on every page:
- Actions menu
- Reboot menu

The Configuration Editor page gives full editting control over the jSON configuration file. Change the configuration using the wysiwyg editor, save as needed and click Apply to reprocess the configuration.
The SD card keeps automatic numbered backups of every saved configuration (config.json;1,
config.json;2, … with the highest number being the most recent), so it is always possible to roll back to a
previous working configuration. Edited configurations are saved back to the SD card; clicking Apply or a "Reload" menu action
then sends a reload command to the RP2350 over the ESP32–RP2350 UART, causing the RP2350 to re-parse and re-apply the new configuration
and causes the ESP32 to parse and reload it's configuration.

The File Manager provides a full web-based file browser for the SD card, providing a directory listing layout to view files on the SD card.
It is intended for general SD card maintenance: uploading ROM images, floppy disk images (DSK), QuickDisk images (QD), RAM disk images, and web filesystem updates without needing to remove the card.
Each entry has action buttons to copy, delete, download, or edit text files, and a Select File upload button at the top allows new files to be transferred from the PC. Directory navigation allows descending into subdirectories such as roms/, dsk/, qd/, and ram/.
Uploading tar or gzip files will automatically have the tar or gzip (or tar.gz) file unpacked and extracted in the current SD directory.

The Persona page configures the active machine personality independently for each of the two RP2350 firmware partitions. Each partition has its own column of radio buttons covering every supported Sharp MZ machine type:
- Basic CPU — bare Z80 emulation with no machine-specific drivers; useful for generic Z80 development.
- MZ-80A and MZ-80B — Sharp MZ-80 series (1Z-013A monitor ROM, MZ-80 keyboard, standard memory map).
- MZ-700 — Sharp MZ-700 with bank-switched VRAM, keyboard controller, and optional floppy/QuickDisk drivers.
- MZ-800 — Sharp MZ-800 with extended video modes and QuickDisk support.
- MZ-1500 — Sharp MZ-1500 with QuickDisk and optional floppy.
- MZ-2000, MZ-2200, MZ-2500 — later Sharp MZ series with high-resolution video and extended memory.
Selecting a persona and clicking Select Personae writes the corresponding pre-built config.json to the SD card (backing up the current file first) and triggers a configuration reload. Because each firmware partition can hold a different persona, the board can be switched between, for example, an MZ-700 personality on partition 1 and an MZ-80A personality on partition 2 without any SD card editing.

The ESP32 OTA page reports the full software inventory of the ESP32 before accepting a firmware upload:
- Modules panel
- ESP32 Partitions panel
- ESP32 Firmware Upload panel
- FilePack Upload panel

The RP2350 OTA page manages the two RP2350 firmware partitions:
- RP2350 Partitions panel
- RP2350 Firmware Upload panel
- RP2350 Active Partition panel

The WiFi Manager configures how the picoZ80 connects to a network. The top panel shows the currently active WiFi configuration (SSID, assigned IP, netmask, and gateway). The Configure WiFi form below it exposes all settings:
- WiFi Mode
- SSID and Password
- DHCP Mode
Settings are saved to the ESP32's NVS (non-volatile storage) by clicking Save and take effect on the next reboot.

Reference Sites
The table below contains all the sites referenced in the design and programming of the picoZ80.
| Site | Language | Description |
|---|---|---|
| RP2350 Datasheet | English | Official Raspberry Pi RP2350 technical reference and datasheet. |
| Pico SDK | English | Raspberry Pi Pico C/C++ SDK — build system and hardware abstraction used by the picoZ80 firmware. |
| Z80 CPU User Manual | English | Zilog Z80 CPU family user manual — bus timing, instruction set and signal descriptions. |
| ESP-IDF | English | Espressif IoT Development Framework used for the ESP32 co-processor firmware. |
| Sharp MZ Series | English | Community resource for Sharp MZ computer hardware, software and technical documentation. |
Manuals and Datasheets
The table below contains all the datasheets and manuals referenced in the design and programming of the picoZ80.
| Datasheet | Language | Description |
|---|---|---|
| RP2350 | English | Raspberry Pi RP2350 microcontroller datasheet. |
| ESP32-S3 | English | Espressif ESP32-S3 SoC datasheet — WiFi/BT co-processor on the picoZ80 board. |
| APS6404L PSRAM | English | 8MB SPI PSRAM datasheet — the main extended RAM used for memory banking. |
| W25Q128 Flash | English | Winbond 16MB SPI NOR Flash datasheet — stores firmware and ROM images. |
| TLV62590 | English | Texas Instruments 5V→3.3V synchronous buck converter powering the picoZ80 from the Z80 DIP-40 VCC pin. |
| CH334F | English | CH334F 4-port USB 2.0 hub controller — provides USB hub functionality for firmware updates. |
Project Preview
Demonstration Videos picoZ80 running RFS (ROM Filing System) — Demo 1The picoZ80 installed in a Sharp MZ-700, running the ROM Filing System (RFS) persona. The video demonstrates the virtual disk and memory banking capabilities of the board.
Commercial Use Restriction
The picoZ80 hardware design (schematics, PCB layout, KiCad files), firmware, and all associated software are made available for personal, educational, and non-commercial use only. No part of this design — including but not limited to the PCB artwork, bill of materials, firmware binaries, source code, or documentation — may be used, reproduced, manufactured, sold, or incorporated into any commercial product or service without the express written permission of the author (Philip D. Smart).
To request a commercial licence or discuss permitted uses, please contact the author via the eaw.app website.
Credits
The picoZ80 project builds on the work of several individuals and open-source projects. Their contributions are gratefully acknowledged.
- Manuel Sainz de Baranda y Goñi
- Raspberry Pi Ltd
- Espressif Systems
- Philip Smart
- Grok (xAI)
- Claude (Anthropic)
Licenses
The picoZ80 project is composed of several components, each covered by its own licence:
| Component | Licence |
|---|---|
| picoZ80 RP2350 firmware (PIO, C sources) | GNU General Public License v3 |
| picoZ80 ESP32 firmware and web interface | GNU General Public License v3 |
| Z80 CPU emulator library (Manuel Sainz de Baranda y Goñi) | GNU General Public License v3 |
| KiCad hardware design files (schematics, PCB, Gerbers) | Creative Commons BY-NC-SA 4.0 |
| Documentation and user guides | Creative Commons BY-NC-SA 4.0 |
| Raspberry Pi Pico SDK | BSD 3-Clause |
| ESP-IDF framework | Apache License 2.0 |
| Bootstrap 4 (web interface) | MIT License |
In short: the firmware and software you build from this project's source code are open-source under the GPL v3; the hardware designs and documentation are licensed under CC BY-NC-SA 4.0 (non-commercial use only — commercial licensing available on request); third-party libraries retain their own licences as listed above. See the LICENSE and NOTICE files in the repository for full details.
Licence Terms
Copyright © 2019–2026 Philip Smart. All rights reserved.
Hardware Designs — CC BY-NC-SA 4.0Wireless Regulatory Notice
This device incorporates an ESP32-S3-PICO-1 wireless module that transmits 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).
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