pico6502 Technical Guide
Note: The pico6502 project is in early development. The hardware, PIO bus interface, memory model, and build system are fully implemented and functional. Machine persona drivers are at an early stage. This guide describes the technical architecture as it exists today.
Overview
The pico6502 technical architecture is identical to the picoZ80 in all respects except the CPU bus interface. The same RP2350B dual-core processor, PSRAM memory model, Flash layout, ESP32 co-processor, and driver framework are shared between the two products. This guide focuses on the differences specific to the 6502 and refers to the picoZ80 Technical Guide for topics that are fully shared.
Hardware Architecture
Block Diagram
┌─────────────────────────────────────────────────────────────────────────┐ │ pico6502 Board (v2.2) │ │ │ │ ┌───────────────┐ PIO 0: A0–A15, D0–D7 ┌───────────────────┐ │ │ │ │◄──────────────────────────►│ DIP-40 Interface │ │ │ │ RP2350B │ PIO 1: cycle/ctrl/irq │ (6502 socket) │ │ │ │ (300 MHz) │◄──────────────────────────►│ A0–A15, D0–D7 │ │ │ │ │ PIO 2: PHI1/PHI2 clock ──►│ PHI0/RNW/SYNC │ │ │ │ Core 0 │ │ IRQ/NMI/RDY/SO │ │ │ │ (USB/IO/ESP) │ └───────────────────┘ │ │ │ │◄──QSPI──►[ 8MB PSRAM ] │ │ │ Core 1 │◄──SPI───►[ 16MB Flash ] │ │ │ (6502 loop) │ │ │ │ │◄──FSPI──►┌───────────┐ │ │ │ │◄──UART──►│ ESP32 │◄──SPI──►[ SD Card ] │ │ └───────────────┘ │ (WiFi) │──────────[ Web Server ] │ │ └───────────┘ │ │ [ USB Hub ]──USB──►[ RP2350 USB Bridge ] │ └─────────────────────────────────────────────────────────────────────────┘
Key Components
| Component | Part | Specification |
|---|---|---|
| Main processor | RP2350B | Dual Cortex-M33, up to 300 MHz, 512KB SRAM, 48 GPIO |
| PSRAM | SPI PSRAM | 8MB, QSPI interface, up to 133 MHz |
| Flash | SPI NOR Flash | 16MB, XIP (execute-in-place) |
| Co-processor | ESP32-S3 | WiFi, SD card, web server, 460.8kbaud UART to RP2350 |
| SD card | FAT32 | ROM images, disk images, config.json, web files |
| Bus interface | Resistor network | Series resistors for 5V ↔ 3.3V level translation |
| Power supply | TLV62590BV | Synchronous buck, 5V (VCC pin) → 3.3V, all board power |
| USB | On-board hub | RP2350 USB mass-storage bridge for firmware flashing |
GPIO Assignment
The GPIO assignments below are indicative of the typical layout for the pico6502 v2.2 board. The KiCad schematic (
kicad/PICO6502/PICO6502_Schematic.pdf) is the authoritative source.
| GPIO | Signal | Direction | Notes |
|---|---|---|---|
| 0–15 | A0–A15 | Output | 6502 address bus — driven by PIO 0 m6502_addr |
| 16–23 | D0–D7 | Bidir | 6502 data bus — driven/sampled by PIO 0 m6502_data |
| 24 | PHI0 | Input | External clock input to PIO 2 |
| 25 | PHI1 | Output | Phase 1 clock output — generated by PIO 2 |
| 26 | PHI2 | Output | Phase 2 clock output — generated by PIO 2 |
| 27 | RNW | Input | Read/Not-Write — sampled by PIO 1 |
| 28 | SYNC | Output | Opcode fetch indicator — driven by PIO 1 |
| 29 | IRQ | Input | Maskable interrupt (active low) — monitored by PIO 1 |
| 30 | NMI | Input | Non-maskable interrupt (active low) — monitored by PIO 1 |
| 31 | RDY | Input | Ready signal — sampled by PIO 1 |
| 32 | SO | Input | Set Overflow — monitored by PIO 1 |
| 33 | RESET | Bidir | System reset (active low) |
| 34–39 | Flash | — | SPI NOR Flash (XIP) |
| 40–45 | PSRAM | — | QSPI PSRAM |
| 46–47 | ESP32 FSPI | — | SCLK, CS to ESP32 |
| 48–49 | ESP32 UART | — | TX/RX 460.8kbaud to ESP32 |
Firmware Architecture
Flash Memory Layout
Identical to the picoZ80 layout:
| Partition | Address Range | Size | Contents |
|---|---|---|---|
| Bootloader | 0x10000000–0x1001FFFF |
128KB | USB bridge, firmware update, partition selector |
| App Slot 1 | 0x10020000–0x1051FFFF |
5MB | Main 6502 firmware (partition 1) |
| App Slot 2 | 0x10520000–0x10A1FFFF |
5MB | Main 6502 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 |
Dual-Core Design
| Core | Responsibilities |
|---|---|
| Core 0 | USB mass-storage bridge; firmware partition management; file I/O relay to ESP32; ESP32 UART command dispatch; inter-core queue service |
| Core 1 | 6502 bus emulation hot loop — services PIO FIFOs for every bus cycle, resolves addresses against the memory map, dispatches to PSRAM / PHYSICAL / FUNC handlers, calls driver poll callbacks |
PIO Bus Interface
The 6502 bus interface is implemented in
M6502.pio across all three RP2350 PIO blocks. The key difference from the Z80 interface is the 6502's two-phase non-overlapping clock scheme: PHI0 is an external clock input; PIO 2 generates PHI1 and PHI2 from it. This ensures correct 6502 bus timing independent of Core 1 scheduling.
PIO Programs
| PIO Block | Program | State Machines | Function |
|---|---|---|---|
| PIO 0 | m6502_addr |
SM 0 | Outputs 16-bit address A0–A15; signals IRQ 0 (cycle start) |
| PIO 0 | m6502_data |
SM 1 | Drives or samples D0–D7; signals IRQ 1 (data phase); tri-state control via RNW |
| PIO 1 | m6502_cycle |
SM 0 | Top-level bus cycle sequencer; IRQ 7 = execution loop exit |
| PIO 1 | m6502_fetch |
SM 1 | Opcode-fetch cycle (SYNC asserted, PHI2 high, RNW=1) |
| PIO 1 | m6502_read |
SM 1 | Memory read cycle |
| PIO 1 | m6502_write |
SM 2 | Memory write cycle |
| PIO 1 | m6502_irq |
SM 2 | Monitors IRQ pin; signals PIO IRQ 5 on assertion |
| PIO 1 | m6502_nmi |
SM 3 | Monitors NMI pin; signals PIO IRQ 4 on assertion |
| PIO 1 | m6502_so |
SM 3 | Monitors SO (Set Overflow) pin; signals PIO IRQ 6 |
| PIO 2 | m6502_clock_6502 |
SM 0 | Generates PHI1 / PHI2 two-phase clocks from external PHI0 input |
IRQ Signal Conventions
| PIO IRQ Flag | Signal | Triggered By | Consumed By |
|---|---|---|---|
| IRQ 0 | Cycle start | m6502_addr — address valid on PHI1 |
Core 1 dispatch loop |
| IRQ 1 | Data phase | m6502_data — data bus active |
Core 1 dispatch loop |
| IRQ 4 | NMI | m6502_nmi — NMI pin low |
Core 1 NMI handler |
| IRQ 5 | IRQ | m6502_irq — IRQ pin low |
Core 1 IRQ handler |
| IRQ 6 | SO | m6502_so — SO pin high |
Core 1 SO handler |
| IRQ 7 | Loop exit | Software request | m6502_cycle — exits execution loop |
PHI1 / PHI2 Clock Generation
The 6502 requires two non-overlapping clock phases. PIO 2 runs the
m6502_clock_6502 program which accepts an external PHI0 input on a dedicated GPIO and generates PHI1 and PHI2 with the correct non-overlap timing. Dedicating an entire PIO block to clock generation means the 6502 bus timing is never affected by Core 1 activity or FIFO stalls.
Wait States and RDY
The 6502
RDY signal can be used to insert wait states — holding RDY low during a read cycle pauses the processor until RDY goes high. The pico6502 can assert RDY programmatically from Core 1 to insert wait states when accessing slower virtual devices (e.g. floppy disk emulation). The tcycwait parameter in the memory map JSON entry specifies the number of wait states to insert for a given memory region.
PIO Architecture — How 6502 Bus Cycles Are Recreated
The RP2350's Programmable I/O (PIO) subsystem recreates cycle-accurate 6502 bus timing using 10 state machines across all three PIO blocks. The architecture differs from the picoZ80 PIO in two fundamental ways: the 6502 uses a two-phase non-overlapping clock scheme (PHI1/PHI2) rather than a single clock, and it uses a single
Two-Phase Clock Generation
R/W̄ (Read/Not-Write) signal instead of separate /RD and /WR.
The 6502 requires two non-overlapping clocks — PHI1 and PHI2. An external PHI0 signal provides the reference; PIO 2 generates PHI1 and PHI2 from it. The
m6502_clock_6502 state machine (PIO 2 SM 0) implements this:
PHI0 (input): ──┐ ┌──┐ ┌──
│ │ │ │
└────────┘ └────────┘
PHI1 (output): ┌──────┐ ┌──────┐
│ │ │ │
───┘ └──────────────┘ └───
PHI2 (output): ───┐ ┌──────┐ ┌──
│ │ │ │
└──────────────┘ └──────────────┘
↑ ↑ ↑
│ │ │
PHI0 falls PHI0 rises PHI0 falls
→ brief gap → brief gap → brief gap
→ PHI1 rises → PHI1 falls
→ PHI2 rises
The SM uses
The jmp pin to detect PHI0 edges and set pins to drive both PHI1 and PHI2 with the correct non-overlap gaps. The [1] delay annotation on the first set pins instruction creates the non-overlap period — both clocks are low for a brief interval during transitions. Dedicating an entire PIO block to clock generation ensures the 6502 bus timing is never affected by FIFO stalls or Core 1 latency.
out exec, 32 Mechanism
Like the picoZ80, the pico6502 uses dynamic instruction injection via
out exec — but with a key difference: the 6502 cycle SM uses out exec, 32 (32-bit instructions) rather than the Z80's out exec, 16. This provides the full 32-bit PIO instruction encoding including side-set bits, delay values, and extended operands.
The m6502_cycle SM (PIO 1 SM 0) orchestrates all bus cycles:
// m6502_cycle SM program (PIO 1 SM 0): // // start_cycle: // wait 0 gpio CLK2 ; Sync to PHI2 falling edge (start of new cycle) // irq set 0 ; Signal "ready for new cycle" // wait 0 irq 0 ; Wait for C code to load address and clear IRQ 0 // cycle_exec: // out exec, 32 ; Pull 32-bit instruction from FIFO, execute it // jmp cycle_exec ; Repeat // cycle_end: // irq set 7 ; Flag end of execution loop // cycle_exec2: // out exec, 32 ; Continue executing injected instructions // jmp cycle_exec2 ; Loop until JMP start_cycle
The C code pre-computes 32-bit instruction sequences for each cycle type (fetch, read, write) and pushes them into the FIFO. The cycle SM pulls and executes each instruction in turn, controlling the
6502 Bus Cycle Types
SYNC and R/W̄ signals while coordinating with the address and data SMs via IRQ flags.
The 6502 has three bus cycle types, all controlled by the state of
R/W̄ and SYNC:
Opcode Fetch (m6502_fetch):
PHI1: ┌──────┐ ┌──
│ │ │
───┘ └──────────────┘
PHI2: ───┐ ┌──────┐
│ │ │
└──────────────┘ └───
A0–A15: ══╤═══ PC address ══════════╗ (valid on PHI1 rising)
R/W̄: ──┤── high (read) ─────────╢
SYNC: ──┤── high (fetch) ────────╢ (SYNC high = opcode fetch)
D0–D7: ══════════════════╤════════╗ (sampled at PHI2 falling)
opcode
Memory Read (m6502_read):
Same timing as fetch, but SYNC = low.
Memory Write (m6502_write):
PHI1: ┌──────┐ ┌──
│ │ │
───┘ └──────────────┘
PHI2: ───┐ ┌──────┐
│ │ │
└──────────────┘ └───
A0–A15: ══╤═══ address ═════════════╗ (valid on PHI1 rising)
R/W̄: ──┤── low (write) ─────────╢ (R/W̄ low = write)
SYNC: ──┤── low ─────────────────╢
D0–D7: ══════════════╤═══ data ═══╗ (driven during PHI2 high)
Key differences from the Z80 PIO implementation:
Signal Monitoring State Machines
- Single R/W̄ signal — replaces the Z80's separate
/RDand/WRsignals. Them6502_fetchandm6502_readSMs setR/W̄high;m6502_writesets it low. - SYNC signal — the 6502 asserts
SYNCduring opcode fetches to distinguish them from ordinary reads. Them6502_fetchSM setsSYNChigh;m6502_readandm6502_writeleave it low. - PHI2-based data sampling — the data bus is sampled at the PHI2 falling edge (end of the cycle), not at a specific T-state clock edge like the Z80. The
m6502_dataSM waits onwait 0 gpio CLK2to time the data capture. - RDY-based wait states — instead of the Z80's
/WAITsignal, the 6502 usesRDY. WhenRDYis low during a read cycle, the processor pauses. The fetch and read SMs check RDY viajmp pinafter each PHI2 rising edge. - No refresh cycle — the 6502 does not have a DRAM refresh cycle, so fetch cycles are simpler (no T3–T4 refresh phase).
- No I/O-specific cycles — the 6502 has no
IN/OUTinstructions. All peripheral access is via memory-mapped reads and writes.
Three dedicated SMs monitor the 6502's asynchronous input signals:
m6502_irq(PIO 1 SM 2) — monitors the/IRQpin. When/IRQgoes low, it sets PIO IRQ 5 and holds it until the pin returns high, then clears the flag. Core 1 checks IRQ 5 to decide whether to inject an interrupt vector fetch sequence.m6502_nmi(PIO 1 SM 3) — monitors/NMIand sets/clears PIO IRQ 4. The NMI handler in Core 1 pushes the appropriate bus cycle sequence to vector through$FFFA/$FFFB.m6502_so(PIO 1 SM 3) — monitors theSO(Set Overflow) pin and sets/clears PIO IRQ 6. This is used by some 6502 systems (notably the Commodore disk drive) to signal data-ready events.
jmp pin to detect the pin going active, irq set to signal Core 1, then jmp pin in a loop to detect the pin returning inactive, followed by irq clear. This converts asynchronous edge events into synchronous IRQ flags that Core 1 can poll efficiently in the hot loop.
Memory Model
The pico6502 memory model is structurally identical to the picoZ80 model with one critical difference: the 6502 has no separate I/O address space. There is no
ioPtr[] array in t_6502PSRAM and no iomap JSON section. All peripheral registers are mapped into the 64KB memory address space using FUNC-type blocks.
Tier 1 — RP2350 SRAM (Fast Dispatch Table)
128 × 32-bit entries in
_membankPtr[], one per 512-byte block of the 64KB address space. Each entry encodes the block type, PSRAM bank number, and block base address in a single 32-bit word for O(1) dispatch on every bus cycle:
// 32-bit membankPtr encoding // Bits 31–24: MEMBANK_TYPE_xxx constant // Bits 23–16: PSRAM bank number (0–63) // Bits 15–0: Block base address >> 9 (i.e. upper 7 bits of 16-bit address) #define MEMBANK_ENCODE(type, bank, base) (((type) << 24) | ((bank) << 16) | ((base) >> 9))
Tier 2 — PSRAM (8MB)
The 8MB PSRAM is divided between the RAM/ROM area and the function-pointer tables:
typedef struct {
uint8_t RAM[MAX_MEMORY_BANKS * MEMORY_PAGE_SIZE]; // 64 banks × 64KB = 4MB RAM/ROM area
MemoryFunc memPtr[MEMORY_PAGE_SIZE]; // 64K per-byte read redirect (PTR type)
MemoryFunc memioPtr[MEMORY_PAGE_SIZE]; // 64K memory-mapped handler array (FUNC type)
// Note: no ioPtr[] — 6502 has no separate I/O space
} t_6502PSRAM;
Tier 3 — Flash (16MB)
The 16MB Flash holds the bootloader, two 5MB application firmware slots, two configuration slots for ROM images and minified JSON, general configuration, and the partition table. See the Flash Memory Layout table in the Firmware Architecture section above.
Memory Block Types
| Constant | Value | Behaviour |
|---|---|---|
MEMBANK_TYPE_PHYSICAL |
0 | Pass-through to real host hardware — RP2350 releases bus |
MEMBANK_TYPE_PHYSICAL_VRAM |
1 | Host video RAM with configurable wait states |
MEMBANK_TYPE_RAM |
2 | Read/write — backed by PSRAM bank |
MEMBANK_TYPE_ROM |
3 | Read-only — backed by PSRAM bank; writes are silently discarded |
MEMBANK_TYPE_VRAM |
4 | PSRAM video RAM mirror with wait states |
MEMBANK_TYPE_FUNC |
5 | Virtual device — memioPtr[addr] handler called on every access |
MEMBANK_TYPE_PTR |
6 | Per-byte redirect — memPtr[addr] handler used for reads |
Configuration Reference
The pico6502 uses the same JSON configuration mechanism as the picoZ80. The top-level keys are
"esp32" and "rp2350". The CPU-specific section uses the key "6502" (rather than "z80"). Because the 6502 has no I/O space there is no "io" array — only a "memory" array.
esp32.core
| Key | Type | Description |
|---|---|---|
device |
string | CPU device type. Must be "6502" for the pico6502. |
mode |
integer | Default boot mode: 0 = client (station), 1 = Access Point. |
esp32.wifi
| Key | Type | Description |
|---|---|---|
override |
0/1 | 1 = apply all wifi settings from this block; 0 = use NVS settings. |
wifimode |
string | "ap" or "client". |
ssid |
string | Network name to create (AP) or join (client). |
password |
string | WiFi passphrase. |
ip |
string | Fixed IP address. |
netmask |
string | Subnet mask. |
gateway |
string | Default gateway. |
dhcp |
0/1 | Client mode only. 1 = DHCP, 0 = fixed IP. |
webfs |
string | Web filesystem root directory on SD card (default "webfs"). |
persist |
0/1 | 1 = write resolved settings back to NVS each boot. |
rp2350.core
| Key | Type | Description |
|---|---|---|
cpufreq |
integer | RP2350 system clock in Hz (e.g. 300000000). |
psramfreq |
integer | PSRAM SPI clock in Hz (e.g. 133000000). |
voltage |
float | RP2350 core voltage (e.g. 1.10). |
rp2350.6502.memory[ ]
Defines the 6502 memory map. All peripherals must appear here as
FUNC-type entries — there is no separate I/O map.
| Key | Type | Description |
|---|---|---|
enable |
0/1 | Whether this entry is active. |
addr |
hex string | Start address in the 6502 address space (e.g. "0xE000"). |
size |
hex string | Region size in bytes (e.g. "0x2000"). |
type |
string | PHYSICAL, PHYSICAL_VRAM, RAM, ROM, FUNC, or PTR. |
bank |
integer | PSRAM bank number for RAM/ROM types (0–63). |
tcycwait |
integer | Wait states to insert (RDY stretching) for this region. |
task |
string | Named task / driver handler for FUNC-type blocks. |
file |
string | SD-card path to a ROM image to load at boot (ROM type). |
rp2350.6502.drivers[ ]
Driver entries for the 6502 are simpler than those for the Z80. Each entry activates a named driver (matched to the firmware's
virtualFuncMap[] table) and optionally loads ROM images into PSRAM at boot.
| Key | Type | Description |
|---|---|---|
enable |
0/1 | Whether this driver entry is active. |
name |
string | Driver name — matched case-insensitively to virtualFuncMap[]. |
rom[] |
array | ROM images to load at boot. Each entry: enable, file, loadaddr[]. |
rom[] entry:
| Key | Type | Description |
|---|---|---|
enable |
0/1 | Whether to load this ROM image. |
file |
string | SD-card path to ROM binary. |
loadaddr[] |
array | One or more load-address descriptors: enable, position, addr, bank, size. |
Virtual Device Framework
The virtual device framework is structurally identical to the picoZ80 framework described in the picoZ80 Technical Guide. The key difference is that all virtual devices are registered in
memioPtr[] — there is no ioPtr[] equivalent for the 6502.
Handler Signatures
// Memory / peripheral handler — installed in memioPtr[addr] // Called for every access to a MEMBANK_TYPE_FUNC block typedef uint8_t (*MemoryFunc)(M6502CPU *cpu, bool read, uint16_t addr, uint8_t data); // Driver init function — registered in virtualFuncMap[] // Called twice: pass=0 (validation), pass=1 (configuration) typedef int (*VirtualFunc)(M6502CPU *cpu, t_drvConfig *drvConfig, int pass);
Writing a Peripheral Driver
To add a virtual peripheral for the pico6502:
- Create a handler function with the
MemoryFuncsignature. The handler receivesread=truefor 6502 read cycles (RNW=1) andread=falsefor write cycles (RNW=0). - Create an init function with the
VirtualFuncsignature. In the configuration pass (pass=1), install your handler incpu->_6502PSRAM->memioPtr[addr]for each address your peripheral occupies. - Register the driver name and init function in the
virtualFuncMap[]table inM6502CPU.c. - Add a
FUNC-type entry in thememoryarray ofconfig.jsonfor the address range. - Add a driver entry in
config.jsonwith the matching"name"field.
See the pico6502 Developer's Guide for a complete worked example including a VIA6522 peripheral implementation.
ESP32 Co-processor
The ESP32 co-processor is identical in function and implementation to the picoZ80 ESP32 — refer to the picoZ80 Technical Guide — ESP32 Co-processor for the full description of the SD card interface, web server structure, and RP2350–ESP32 command protocol.
SD Card
The SD card is managed exclusively by the ESP32 over SPI. The RP2350 requests file reads and writes by posting messages to the inter-core queue; Core 0 relays these requests to the ESP32 over UART and returns the results. The SD card directory layout for the pico6502 is identical to the picoZ80:
webfs/ for web files, ROM/ for ROM images, and config.json in the root.
Web Server
The same seven-page Bootstrap 4 web server runs on the ESP32. The web interface automatically detects the connected device type via the
esp32.core.device field in config.json and adjusts its CSS theme and labels accordingly (the p6502.css stylesheet applies the 6502-specific colour scheme).
Command Protocol
The RP2350–ESP32 command protocol is identical for both the picoZ80 and pico6502. Commands are exchanged over a 460.8kbaud UART link (backed by 50MHz FSPI for bulk data transfer). The protocol is defined in
ESP.c / ESP.h and is shared between both firmware variants.
SWD Debugging — RP2350
The SWD debugging setup for the pico6502 is identical to the picoZ80 — refer to the picoZ80 Technical Guide — SWD Debugging for the full description of hardware connections, the custom
rp2350_tzpu.cfg OpenOCD target, and global GDB initialisation. The differences from the picoZ80 are:
- The main firmware ELF is
build/bin/model/BaseM6502/BaseM6502_0x10020000.elf(notBaseZ80). - The
add-auto-load-safe-pathentry in~/.gdbinitmust referenceBaseM6502.
OpenOCD Setup
sudo cp rp2350_tzpu.cfg /usr/local/share/openocd/scripts/target/ openocd -f interface/cmsis-dap.cfg -f target/rp2350_tzpu.cfg -c "adapter speed 5000"
GDB Configuration
set history save on set history filename ~/.gdb_history set history size 65536 add-auto-load-safe-path build/bin/model/BaseM6502/.gdbinit:build/bin/model/Bootloader/.gdbinit
# Bootloader — Core 0 cd build/bin/model/Bootloader cp ../../../../.gdbinit.bootloader.3333 .gdbinit gdb-multiarch Bootloader.elf # Bootloader — Core 1 (separate terminal) cd build/bin/model/Bootloader cp ../../../../.gdbinit.bootloader.3334 .gdbinit gdb-multiarch Bootloader.elf
# Main firmware — Core 0 cd build/bin/model/BaseM6502 cp ../../../../.gdbinit.3333 .gdbinit gdb-multiarch BaseM6502_0x10020000.elf # Main firmware — Core 1 (separate terminal) cd build/bin/model/BaseM6502 cp ../../../../.gdbinit.3334 .gdbinit gdb-multiarch BaseM6502_0x10020000.elf
ESP32 — USB Debugging
Identical to the picoZ80 — connect USB to the ESP32 USB port and use the ESP32-S3 built-in USB-JTAG interface (no external probe required).
openocd -f board/esp32s3-builtin.cfg xtensa-esp32s3-elf-gdb esp32/build/main.elf (gdb) target extended-remote :3333
Build System
The build system is identical to the picoZ80 — CMake with Pico SDK 2.x targeting the RP2350. The pico6502 does not require the Zeta Z80 emulator library. Refer to the picoZ80 Technical Guide — Build System for the full description of prerequisites, Docker setup for ESP-IDF, and the build script. The pico6502-specific differences are noted below.
Build Targets
| Target | Output | Description |
|---|---|---|
BaseM6502 |
BaseM6502_0x10020000.uf2 |
Main 6502 firmware — Slot 1 load address |
BaseM6502 |
BaseM6502_0x10520000.uf2 |
Main 6502 firmware — Slot 2 load address |
Bootloader |
Bootloader.uf2 |
Shared bootloader (same as picoZ80) |
| ESP32 | main.bin |
ESP32 firmware (shared with picoZ80) |
Build Commands
# Set up (once) — from project root
export PICO_PATH=/path/to/pico-sdk
./get_and_build_sdk.sh
# Standard release build
./build_tzpuPico.sh
# Debug build
./build_tzpuPico.sh DEBUG
# Full build including ESP32 firmware
./build_tzpuPico.sh ALL
# ESP32 only (using Docker idf54 alias)
cd projects/tzpuPico/esp32
idf54 build
# Flash RP2350 via USB mass-storage (BOOTSEL mode)
cp build/bin/model/Bootloader/Bootloader.uf2 /media/$USER/RPI-RP2/
cp build/bin/model/BaseM6502/BaseM6502_0x10020000.uf2 /media/$USER/RPI-RP2/
# Flash ESP32 (initial, via esptool)
esptool.py --chip esp32s3 --port /dev/ttyUSB0 --baud 460800 \
write_flash 0x0 esp32/build/bootloader/bootloader.bin \
0x8000 esp32/build/partition_table/partition-table.bin \
0x10000 esp32/build/main.bin
Reference Sites
| Resource | Link |
|---|---|
| pico6502 project page | /pico6502/ |
| pico6502 User Manual | /pico6502-usermanual/ |
| pico6502 Developer’s Guide | /pico6502-developersguide/ |
| picoZ80 Technical Guide | /picoz80-technicalguide/ |
| picoZ80 project page | /picoz80/ |
| RP2350 Datasheet | datasheets.raspberrypi.com |
| Pico SDK Multicore API | raspberrypi.github.io/pico-sdk-doxygen |
| MOS 6502 Datasheet | archive.org |
| esptool documentation | docs.espressif.com |
| cJSON Library | github.com/DaveGamble/cJSON |
Wireless 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
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.
- Assembled devices 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 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.
- 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.