6. Architectural Operational Boundaries¶
For a visual walkthrough of how CP/M memory, the BIOS and PIO fit together, see CP/M execution and banked memory.
The three operating states¶
| State | Who controls the buses? | What you should expect |
|---|---|---|
| Injected loading | Z80 alone drives SRAM addresses and writes | RESET# is released after slow reset clocks; Pico supplies instructions with SRAM reads inhibited and verifies CPU-written RAM. |
| Normal execution | Z80 addresses SRAM directly | Both Pico data paths are isolated; ordinary memory access needs no Pico software. |
| Trapped I/O | Z80 holds its address while the Pico services one byte | HCT32 WAIT and clock stopping hold the cycle; four buffered address bits select the port. |
Runtime flash writes temporarily request the Z80 bus using BUSREQ#/BUSACK#. This is different from cold boot: a running CPU must acknowledge the request before flash programming. The Pico never drives memory addresses or writes, even during a bus grant. The detailed rules below protect that boundary.
6.1 Hardware-WAIT-Assisted Clock-Stop Trap Protocol¶
IORQ# LOW reaches HCT32 pin 4 and makes WAIT# pin 6 LOW while Pico GP9
IO_RELEASE is LOW. In Phases 6-8 the buffered IORQ# GPIO edge interrupts
core 0, which stops the PWM clock (Phases 9-10 complete every cycle in PIO; see
PIO I/O service). The fully static CMOS Z80 tolerates the stopped clock. After settling,
firmware first checks that IORQ# remains LOW, then requires exactly one of
RD#/WR# active and samples A0/A1/A2/A4 before servicing data. If IORQ# is
already HIGH, it counts an inactive interrupt and resumes the clock without
touching the data paths or IO_RELEASE. These events remain included in
control_errors so they cannot silently pass timing qualification.
For OUT, U10 receives the CPU byte; for IN, U9 supplies a preloaded reply. Only after the path is configured does GP9 rise. Firmware completes the I/O cycle with slow single clocks, observes IORQ/strobe release, disables both paths, waits at least 1 us for isolation, lowers GP9, then restores the clock. Every slow step includes a fixed 1 us LOW settling interval before the control sample, preventing an extra clock caused by propagation or input-synchronizer delay. This prevents a later memory read from re-enabling the Pico output. RD#-qualified U9 OE provides additional strobe gating, not software-independent ownership arbitration.
WAIT assertion is fixed logic, not IRQ/PIO dependent. HCT32 propagation, WAIT setup/hold, run-clock-to-SIO transitions, and data setup still require captures. The 500 ms release timeout fails closed through RESET and watchdog recovery.
6.2 Terminal I/O over Pico WebSocket¶
See I/O ports and services for the overall allocation of fixed terminal/disk ports, reserved diagnostics and proposed extensions.
The final terminal is a virtual I/O peripheral implemented by the Pico,
not a Z80-side UART. Z80 software performs ordinary IN and OUT
instructions against a small terminal port pair; the Pico intercepts
those cycles through the
Section 6.1 clock-stop trap,
then resumes the
CPU after sampling or supplying the data byte. A browser connects to the
Pico over Wi-Fi and receives the terminal stream through a WebSocket
server running on the Pico 2 W.
Wi-Fi/WebSocket is the intended final and day-one operating terminal.
Before networking is introduced, Phase 8
and Phase 9
use USB CDC over the Pico's
existing USB connector as an intermediate terminal transport. USB CDC
consumes no GPIO and appears as /dev/cu.usbmodem... on macOS. It lets
the same 0x00/0x01 virtual-port contract and bounded queues be tested
without lwIP timing in the loop. Keep diagnostics framed or on a separate
CDC interface so debug text cannot become CP/M input. Those applications
uses ordinary bytes as terminal input and reserves Ctrl-] followed by one
command byte for framed diagnostics. Phase 10
selects Wi-Fi/WebSocket exclusively while a browser is connected. USB remains
the CP/M fallback before connection and after disconnect; framed Ctrl-]
diagnostics remain available even while the browser owns CP/M. Network setup
uses Ctrl-] n and the existing persistent ENV store.
The WebSocket server must run on the Pico's other core so Wi-Fi, lwIP, HTTP serving, and WebSocket polling cannot interfere with the timing of the Z80-facing supervisor path. Core 0 owns GPIO, clock stop/resume, assisted loading, and the I/O trap. Core 1 owns USB initialization and interrupts, USB console/supervisor parsing, Wi-Fi association, the embedded terminal page, WebSocket accept/send/receive, and network polling. The cores share terminal byte queues, one immutable disk-write queue, a small request/result pair for Z80 bus ownership, and atomic terminal/disk status words; they share no raw bus GPIO ownership. Phase 10 uses the CYW43 polling architecture so network callbacks execute only on core 1. USB commands use a bounded queue to request Z80 actions on core 0; USB never directly changes bus pins. Core 0 publishes CPU state and receives published cancellation/connectivity flags instead of reading USB. All stdout uses a bounded diagnostic ring drained only by core 1. Its default alarm pool is explicitly initialized there, keeping USB background work off core 0. Core 1 starts before journal recovery and participates in flash lockout; runtime workers start only after SRAM verification. Only core 1 reads or writes the runtime ENV backend.
Use the pico-altair-8800 WebSocket console as the software pattern:
one embedded HTML terminal page, one WebSocket server, and byte queues
between the CPU-facing side and the network-facing side. The trap hook
must never call lwIP, print, sleep, wait for a browser, or block on a
queue. It may only enqueue Z80 output bytes, dequeue already-buffered
browser input bytes, and report terminal status. Core 1 may drop, drain,
or back-pressure network data; it must not hold terminal critical sections
across network or flash work or touch any Z80 bus GPIO.
The trap path uses no SDK queues. Terminal status is read lock-free from
atomically stored ring counts; because core 0 is the only receive consumer and
transmit producer, a racing core-1 update can only make one poll conservative.
A short SDK critical section serializes terminal data dequeue/enqueue and
disconnect cleanup so output cannot race with a disconnect on the other core.
Include that bounded spinlock contention in measured trap-latency
qualification.
The initial terminal decode is intentionally small:
| Port | Direction | Function |
|---|---|---|
0x00 |
Z80 OUT |
Terminal transmit byte from Z80 to host terminal |
0x00 |
Z80 IN |
Terminal receive byte from host to Z80; returns 0x00 if empty |
0x01 |
Z80 IN |
Terminal status: bit 0 = receive byte available, bit 1 = transmit-ready (queue has room or transport disconnected), bit 7 = transport connected (USB in Phase 9; browser or fallback USB in Phase 10) |
Connected terminals retain the 512-byte output buffer. If it is full, a new Z80 output write is discarded and the TX-drop counter increments; older queued characters remain unchanged and in order. The trap never waits for space. Transmit-ready remains clear until space is available, so software that polls it can still wait voluntarily before writing. This policy applies to USB and browser output alike. Without a connected terminal, output writes are discarded immediately and transmit-ready remains set, preventing CP/M console output from waiting for a client. Disconnecting clears queued output; reconnecting does not replay it. These intentional discards do not increment the terminal overflow counter. Phase 10 also clears queued input/output on browser/USB handover and rejects additional browser sessions without changing the active owner. Wi-Fi-only events leave USB-owned queues intact. The Phase 10 browser and firmware use a sequenced, acknowledged binary protocol: up to four 512-byte output frames are in flight and cumulatively acknowledged after xterm consumes them. TCP backpressure delays output without loss or reordering. One pending browser input frame waits for RX-queue space and is admitted on a later service pass rather than evicting older bytes. The terminal sends a ping every 30 seconds; the third consecutive missed pong without other browser activity releases the browser to USB, as does five minutes without output-ACK progress.
This is enough for ROM monitors, BASIC, CP/M console glue, and small diagnostics. If later software needs modem-control style signals, add more virtual status bits before adding hardware.
6.3 Onboard Flash CP/M Disk Storage¶
The Pico's 4 MiB onboard flash stores its firmware, the Z80 boot package, and four persistent read/write CP/M disks. No external storage, SPI bus, or extra GPIO is required. The linker limits Pico firmware to the first 1.8125 MiB and reserves the remainder as follows:
| Region | Flash offset | Size | Contents |
|---|---|---|---|
| Journal | 0x1D0000 |
64 KiB | Eight rotating metadata/data sector pairs |
| Boot partition | 0x1E0000 |
128 KiB | Header/image through 0x1F0FFF; ENV in final 8 KiB |
| Environment (inside boot partition) | 0x1FE000 |
8 KiB | Two alternating CRC-checked 4 KiB snapshots |
| Drive A | 0x200000 |
512 KiB | CP/M system disk |
| Drive B | 0x280000 |
512 KiB | CP/M data disk |
| Drive C | 0x300000 |
512 KiB | CP/M data disk |
| Drive D | 0x380000 |
512 KiB | CP/M data disk |
Disk format. Each disk is exactly 524,288 bytes: 128 tracks of 32
sequential 128-byte records. This project-specific format is not IBM 3740
SSSD. CMake builds the CP/M images from the 512 KiB images in
src/disks/native/ and tracks their changes, so rebuilding cannot silently
reuse stale images. The same files are used by the host emulator and the Pico
flash slots.
Build and provisioning. The z80_cpm_images target automatically creates
the boot package, four disk images, and combined
build/cpm/z80romless-flash.bin. The build does not flash the Pico. For
initial setup, put it in BOOTSEL mode and run:
cmake --build build --target z80_cpm_images -j
picotool load -v build/cpm/z80romless-flash.bin -t bin -o 0x10000000
picotool reboot
Use separate artifacts only for selective updates. The
firmware build and provisioning page gives their commands
and addresses. Full provisioning, flash_nuke.uf2, and mass erase destroy
existing disk contents and environment variables, so keep backups.
Phases 9 and 10 implement the ROMLESS-SVC/1 extended I/O protocol, including persistent environment variables. ENV uses the unused boot-partition tail, not the disk journal. Exact firmware-only and boot-package-only updates preserve it; never provision a padded whole-boot-partition image over the ENV sectors.
Runtime behavior. On boot-image initialization, the Pico recovers any interrupted disk
update, validates z80boot.pkg, copies its image to Z80 SRAM, and verifies the
copy, then resets to PC zero before execution. The BIOS transfers one 128-byte disk record at a
time. Core 0 serves reads from memory-mapped flash and queues writes to core 1,
which maintains one 4 KiB cache in RP2350 internal SRAM, separate from the
Z80's external SRAM. Directory writes flush immediately. Other writes flush
on cache eviction, an explicit BIOS request, warm boot, or 250 ms of
inactivity.
Phase 9 waits for USB commands rather than automatically executing CP/M:
Ctrl+] l loads/verifies with slow injected clocks and holds reset afterward;
Ctrl+] g starts at the scope-measured 20 MHz Z80 clock using a 120 MHz
supervisor clock.
The manifest must describe a complete 65,536-byte image beginning with
JP FC00h. BIOS reports on reserved port 0x05 distinguish cold entry
(B0h) and CCP handoff (C0h) from the act of releasing reset. A real prompt
and filesystem tests remain necessary. The separate Phase 9 automatic-loader
target and Phase 10 use automatic verified startup at 20 MHz.
A 64 KiB journal makes 4 KiB sector replacement recoverable after power loss. At startup, firmware replays complete, CRC-valid committed entries and rejects invalid or out-of-range entries. Before erasing a target, it verifies the staged journal data sector and header page by readback; afterward it verifies the complete new target and clears the header. The Phase 9 implementation guide defines the journal sequence, multicore lockout, failure handling, and tests.
Flash has finite erase endurance. The cache coalesces writes and skips clean blocks, but sustained write-heavy workloads should use replaceable or wear-levelled storage.
6.4 System Performance Envelope & Constraints¶
-
I/O Decode Width: Four buffered lines A0/A1/A2/A4, mask
0x17. A3/A5-A7 and the high byte alias; terminal 00/01 and disk 10-14 remain distinct. -
Trap Latency Profile: HCT32-generated WAIT# covers the interval from IORQ# falling until IO_RELEASE reports a configured data path. The Pico stops the static CPU clock while servicing the cycle. Measure IORQ#-to-WAIT#, WAIT# setup/hold, the final run-clock edge, and IO_RELEASE-to-WAIT# release and completion (stepped in Phases 6-8; PIO in Phases 9-10) at every claimed rate. The design remains suitable for low-rate virtual peripherals, not high-speed line tracing.
-
Clock Validation Targets:
-
Bring-Up Target: 1 MHz, qualified only after a logic-analyzer capture proves every I/O cycle stops before the Z80 samples or releases its data.
-
Qualification Range: 2 MHz – 6 MHz, tested in the increments in the frequency-qualification plan. No rate in this range is guaranteed in advance.
-
Experimental Range: Requests 6.5 MHz – 8 MHz may be attempted in requested 500 kHz steps only after 6 MHz passes. At 144 MHz
clk_sys, the 6.5 MHz request still produces 6 MHz; subsequent requests produce approximately 6.545455, 7.2, and exactly 8 MHz nominally. These rates are exploratory, not design claims, because the 55 ns SRAM and breadboard margin dominate despite the faster buffer. -
Failure Boundary: Any WAIT setup/hold failure, SRAM setup failure, malformed clock edge, or repeatable memory/I/O error ends qualification at the preceding passing step.
-
20 MHz CPU Rating: The
Z84C0020PECrating applies to the CPU, not automatically to every assembled board. The project IC set has been tested at 20 MHz, but PCB receiving-pin margins require separate evidence. A 50 ns clock period is not the complete Z80 memory-access window: compare the actual read/write edges, SRAM access (55 ns on the DIP board, 35 ns on the partial-SMT board), AHCT244/HCT32 propagation and setup/hold requirements. Changing a clock setting alone does not establish these margins. -
PCB Targets: The DIP and partial-SMT boards implement this circuit with separate inventories, measured route lengths and manufacturing packages. Each has a 20 MHz qualification target; start either PCB's bring-up at 1 MHz.
PIO I/O service¶
Phases 9 and 10 use the
PIO clock and I/O engine
as their only 20 MHz run clock and I/O entry: every IN and OUT completes in
PIO, and the stepped software sequence above is used only by the Phase 6-8
GPIO trap. OUT 07h drives A16 in PIO; other OUTs are queued for core 0, and
an IN waits, with WAIT and CLK held, for core 0's reply byte. Core 0 services
them in bus order, so an IN always observes every earlier OUT. The supervisor
stop parks the Z80 with BUSREQ#, applies queued OUTs and only then checks disk
quiescence; its flush is a separate core-1 request, never a command injected
into the Z80 disk port. If the PIO resources cannot be allocated, CP/M start
fails closed with RESET held LOW. Without a verified second SRAM bank, the
engine still runs the clock and I/O but leaves GP8/A16 on SIO, so runtime
banking reports unavailable and OUT 07h cannot switch banks. No extra wiring
is required beyond the A16 modification.