Phase 9 - USB CP/M Terminal and Flash Storage¶
Tick setup items when complete and tests only after recording the measured result. Progress is saved in this browser; ticks are not automatic verification. Leave Phase 10 network-dependent tests unticked until completed there. The bring-up checkpoint is not the final storage pass gate.
Prerequisite: The Phase 8 pass gate must pass.
For building C applications with DCC, importing application disks from Altair 8800 AI Edition, and deploying them with VS Code tasks, see Related projects. Use the supplied applications for this phase's bring-up tests before adding your own.
Hardware: Keep the tested Phase 8 wiring unchanged.
Goal: Boot CP/M over USB and verify persistent disks, SRAM loading, and recovery from interrupted writes.
Build and provision from VS Code¶
Open the Z80ROMlessSBC project folder, run Tasks: Run Task from the Command Palette, then choose:
| Task | Result |
|---|---|
| Build and flash Pico - Phase 9 firmware | Builds and force-flashes firmware; preserves disks and ENV |
| Build and flash Pico - Phase 9 firmware and disks | Builds and writes the complete image; replaces disks and ENV |
Choose Diagnostic for bring-up tests or Automatic loader after those tests pass. Both tasks force-update without checking the previous firmware. Use firmware and disks for initial installation or a complete BIOS/disk refresh; use firmware only for routine updates. See task requirements.
Before deployment
Save CP/M work. Apply external +5 V before USB and keep it applied. Disk tasks overwrite selected drives without backups.
Matching Phase 9 firmware, boot package, and disks installed; host backups retained.
Use disposable disk contents for fault tests
Complete normal boot/read/write checks before injecting faults. Tests that corrupt images, overwrite all records, or interrupt flash updates can destroy files. Preserve the original images and expected old/new blocks first. Use the documented one-shot watchdog hooks; do not simulate faults by pulling individual live signal wires or shorting pins.
Fail-closed means the supervisor deliberately holds the CPU in reset or reboots into recovery instead of executing an unverified image. Capture the USB error/status first. Do not bypass verification merely to get a prompt.
USB terminal first; 20 MHz after loading
Phase 9 implements the same 0x00/0x01 terminal contract as in Phases 8
and 10, using USB CDC rather than Wi-Fi. The diagnostic firmware
requires separate load/start commands; the automatic loader below performs
both on Pico reset. In either case, slow, Z80-assisted loading and complete
readback happen before execution at 20,000,000 Hz,
50% configured duty (120 MHz / 6). The Z80 clock frequency has been
scope-measured at 20 MHz on the project prototype. This does not establish
CP/M, disk-I/O or receiving-pin timing margins. I/O traps and flash writes
still pause execution. Phase 10 inherits this tested automatic-loader
application and its clock settings; combined network operation still
requires acceptance testing.
Automatic CP/M loader¶
Both Phase 9 variants also implement
ROMLESS-SVC/1 extended ports: countdown timers,
stopwatches, uptime, utilities, onboard LED control and persistent ENV. The
LED uses Utility payload [4, 0] for off, [4, 1] for on, and [5] to read
its last successfully commanded state. Its wireless-chip driver is initialized
on demand without enabling Wi-Fi networking; USB and 20 MHz operation
are unchanged. Calendar queries explicitly
report CLOCK_UNSYNCED in this USB-only phase. No additional wiring is required;
the interrupt timer remains deferred. Check these services with the updated
Drive C applications after the terminal/disk pass gates. Software regression
tests do not replace physical timing and persistence checks.
After the bring-up tests pass, run Build and flash Pico - Phase 9 firmware
and choose Automatic loader. Switching variants requires only a firmware update when the
installed boot package and disks already match; the diagnostic variant uses
manual l/g commands.
Every Pico reset now:
- Establishes safe GPIO levels, holds the Z80 in reset and stops its clock. Launches core 1, which initializes its flash-lockout participation, the default alarm pool and USB before acknowledging readiness.
- Recovers the flash journal and validates the boot manifest and payload CRC.
- Uses Z80-assisted loading to write the complete 65,536-byte SRAM image, then independently reads back and compares every byte.
- Resets the Z80 again, enables I/O trapping and starts CP/M at 20 MHz.
- Continues terminal and disk service without waiting for a USB terminal.
Core 0 owns Z80 GPIO, loading, clock generation and I/O traps. Core 1 owns all USB endpoint access, USB interrupts/background tasks, ANSI input mapping and terminal pumping, plus runtime storage and service workers. Core 1 starts before loading, but disk/service workers are enabled only after successful load/verify and CP/M startup. The earlier single-core bring-up phases are unchanged.
USB supervisor commands cross a four-entry queue to core 0; they never manipulate Z80 GPIO from core 1. A full queue explicitly rejects the command and asks the user to retry. Core 0 publishes running/loading/stopping flags; manual-loader cancellation and USB connection state are published back from core 1. The cancellation check remains at a 256-byte loader checkpoint.
Diagnostics from either core use a separate 4 KiB bounded log ring. Core 1
drains at most 128 bytes per pass, subject to CDC room, without holding the
ring lock over USB calls. Connected overflow is reported as log_drops;
disconnected logs are discarded and not replayed. CP/M output retains its
separate 512-byte terminal ring. The SDK stdout mutex is disabled for these
targets, so core 0 never waits on USB output serialization.
CP/M USB output submits at most 64 bytes in one nonblocking CDC write per pass;
a newline flushes at once and other output within 250 us. It peeks at the terminal ring
and removes only the accepted prefix, leaving short or rejected writes queued
for a later pass. No terminal lock is held during endpoint calls, and single
interactive characters are submitted immediately without a batching delay.
USB input is one endpoint read of at most 32 bytes per pass.
USB remains serviced while stopped or draining for a stop. Flash programming
can temporarily suspend it through SDK lockout. Real enumeration, concurrent
network traffic and timing still require bench acceptance.
SRAM-resident Z80 I/O path¶
Phases 9 and 10 have one run-time I/O entry: the
PIO engine completes every
IN and OUT. OUT 07h drives A16; other OUTs are queued in the PIO1 RX FIFO and
an IN waits, with WAIT and CLK held, for core 0's reply. There is no PWM clock,
GPIO-IRQ trap, clock stop or stepped completion on core 0 in these builds. If
the PIO resources cannot be allocated, CP/M start fails closed with RESET held
LOW. Without a verified bank 1, the engine still owns the clock and I/O, but
GP8/A16 stays on SIO, so an OUT 07h cannot select an unloaded bank and
IN 07h reports 00h.
Core 0 services the FIFO from the PIO1 IRQ, linked into SRAM with
__not_in_flash_func so XIP cache misses caused by core-1 networking cannot
stretch a Z80 wait:
- The FIFO service, which calls the shared Phase 9/10
virtual_read/virtual_writedispatch directly and answersIN 07hitself. - Terminal, disk, extended service/timer and HTTP port handlers, the
service mailbox submit/poll and
z80_flash_backend_read_record. - Timer polls, including the fail-closed watchdog poll, use an inlined copy
of the SDK
timer_time_us_64loop fromz80sbc/ram_timer.h.
Bus timing is entirely in PIO. Do not reintroduce core-0 settling waits or
stepped completion for Phase 9/10 cycles; the Phase 6-8 GPIO trap keeps its
SDK busy_wait_us_32 guards, whose call overhead is part of that trap's
qualified minimum (an inlined copy made CP/M hang at its first disk read).
The fast paths also avoid avoidable work in the handlers:
- OUTs: core 0 only applies the queued port/value pair. A full RX FIFO holds the Z80 in WAIT. The fail-closed poll treats serviced cycles as progress.
- INs: core 0 only computes the reply byte and queues it; PIO drives and turns the bus around. The reply is queued before any bookkeeping, and only the speaker port consults its readiness gate.
- IRQ state: everything the handler touches is one object reached from a
single base address. The read and write totals are the only per-cycle
counters; the watchdog's progress and the
[iolat]counts derive from them. - Main loop: core 0 peeks its command and flash-service queues without their locks and takes a lock only to remove an entry, so an idle pass never masks the PIO1 IRQ.
- Core 1 idle pass: takes no cross-core lock and does no text formatting or 64-bit division. The log ring count is read atomically, the Escape timer is polled only while a lone Escape is pending, and services compare raw microsecond deadlines.
- Disk writes: the Z80 fills the shared 128-byte record directly. A single-slot mailbox publishes it to core 1; BUSY blocks every further disk port write, so the record crosses cores without copies or a queue lock. The core-0 stop/flush request masks interrupts while submitting through the same slot.
- Disk reads: CP/M reads are LBA-sequential (no sector skew), so core 1
reads ahead. After each READ, core 0 publishes a hint and core 1 copies the
next eight records into direct-mapped SRAM slots (
lba & 7, at most eight copies per service pass). A hit is a 128-byte SRAM word copy with no flash access in the trap. Each slot is a seqlock with core 1 as the only writer; if core 0 sees a slot mid-rewrite it reads flash as before, so a race costs time but never returns wrong data. Core 1 invalidates the slot for every record it writes before publishing READY, and fills through the backend read, which checks the write sector first. Core 1 remembers the hint whose window is filled, so later passes skip the slot scan until the next READ or invalidation. Misses use a word copy in SRAM, not newlibmemcpy. Ctrl-] s reports[disk] readahead hits=... misses=.... - Terminal status: ring counts are stored atomically under the lock and read lock-free by the status poll. Core 0 is the only RX consumer and TX producer, so a racing core-1 update can only make one poll conservative. Data reads and writes still take the short terminal critical section.
- Timers: countdown deadlines are whole milliseconds stored as microseconds, so status polls compare against the raw timer without a 64-bit software division.
Remaining XIP calls are rare or fatal paths: fail-closed reset, ARM-side bank selection during loading, once-per-request service payload copies and HTTP session changes. Read-ahead misses (the first record of a run, random access) and diagnostic comparison bytes are still read from flash in the trap. SRAM placement does not permit trapping during flash programming or relax BUSACK/IRQ/multicore lockout.
Build verification¶
Initialized SRAM .data (including SRAM code) is 29,672 bytes for Phase 10;
the read-ahead slots add about 1.1 KiB of BSS. Check placement and audit
SRAM-to-flash veneers rather than relying on addresses alone:
elf=build/src/stage10_websocket_terminal/z80_stage10_websocket_terminal.elf
arm-none-eabi-nm -n "$elf" |
grep -E ' (io_irq_handler|service_fifo|virtual_read|virtual_write|z80_terminal_io_read|z80_flash_disk_io_write|z80_io_port_in|z80_flash_backend_read_record)$'
arm-none-eabi-objdump -d --no-show-raw-insn "$elf" |
awk '/^2[0-9a-f]+ <[^_].*>:/{f=$2;next} /^$/{f=""} f!="" && /veneer>/{print f, $NF}'
Functions must have 0x2000... execution addresses. Only the rare paths
listed above may call a flash veneer.
Hardware comparison still required¶
No before/after trap-latency result has been recorded for these changes; hardware deployment and measurement are still required. Successful builds, placement checks and host tests do not establish a measured speedup.
Ctrl-] s prints [iolat]: core-0 cost per IN (taking the request to
queuing the reply) and per queued OUT applied, as count, average and maximum
in ns from the core-0 DWT cycle counter. The window resets at each Ctrl-] s, so press it once, run a
workload, then press it again. It excludes PIO-to-IRQ entry latency and the
Z80's own wait states; use Group C captures for the bus-level interval.
For a comparison, preserve both firmware builds and use identical clock settings, CP/M images, workload, USB/browser ownership and network load. With the safe setup in the logic-analyzer procedure, capture Group C IN and OUT separately in 16-channel, 100 MHz Buffer Mode. Separate terminal status/data accesses, disk status/data accesses, read commands, and write-record submission rather than mixing their latency distributions.
Measure IORQ falling to IO_RELEASE rising as the service-response interval, and the end of free-running CLK to its resumed free-running edges as the total Z80 clock pause; for an IN this includes the two PIO completion clocks and the held-LOW isolation interval. Neither is an exact IRQ-entry/ exit timestamp. Record sample count, sample resolution, median, tail percentile, maximum, and workload/capture length for each category, both idle and with concurrent network traffic. Check control errors, drops and trap timeouts before and after; review the original data setup/hold, isolation, PIO handover and settling guards on raw captures. The acceptance gate is a repeatable reduction in the selected latency metric with no correctness/timing regression, not an assumed improvement based on SRAM addresses alone.
Connect picocom whenever convenient, using the actual USB CDC port name:
picocom --baud 115200 --databits 8 --parity n --stopbits 1 --flow n /dev/cu.usbmodemXXXX
There are no load/start commands to type. Output produced without a connected
terminal is discarded, including the CP/M banner and A> prompt. If CP/M is
idle at its command prompt, press Return after connecting to obtain a fresh
prompt; reconnecting does not restart a running application.
While a terminal is connected during automatic boot, the supervisor reports
SRAM load and independent verification progress every 4,096 bytes, for example:
[boot] SRAM load: 32768/65536
[boot] SRAM load: 36864/65536
...
[boot] SRAM verify: 32768/65536
...
[boot] verified; RESET held LOW: 65536/65536
The last line precedes releasing reset and starting CP/M. Progress is not retained or replayed when no terminal is connected, and automatic boot never waits for USB. After CP/M starts, reconnecting preserves the current program and does not reboot the Z80 or replay progress or a supervisor menu. ANSI key mapping is the same as in the diagnostic firmware.
Verification or startup failure keeps the CPU stopped; it never runs an
unverified image or retries endlessly. A later terminal connection reports
the stopped state and retained failure details. Optional Ctrl+] then s
still shows status, and Ctrl+] then h prints supervisor help.
For a safe software restart, use Ctrl+] then x, wait for
Disk quiescent; RESET held LOW, clock stopped, then use Ctrl+] then r.
The Pico reboots and automatically reloads CP/M.
The stop confirmation also prints the full Ctrl+] command menu and marks
r (reboot), s (status), and h (help) as available while stopped.
Loading/running/testing (l/g/t) requires a Pico reboot before another run;
fault commands (0-8) require a running CPU.
This resets USB CDC too, so picocom may exit with Device not configured.
Reconnect using the current USB port name; if loading or verification is still
underway, progress appears at the next 4,096-byte boundary before the CP/M banner.
Reset is not a substitute for saving and flushing
A Pico reset reloads SRAM and discards the running CP/M program. Save work and stop/flush first. Journal recovery protects committed flash updates, not unsaved application data or disk changes still held only in RAM. Keep external +5 V applied throughout in-board USB programming.
For new/disposable storage, z80_stage09_cpm_loader_images builds the complete
matching package in build/cpm-stage09-loader/. Its full-flash image contains
the automatic loader and replaces all disk contents when programmed; do not
use it for a firmware-only update.
Controlled USB workflow¶
This section uses the original manual diagnostic firmware, not the automatic loader above.
Do not deploy this firmware while another physical test or soak is running. No probing, wiring changes or firmware replacement is required to build it. After that test finishes, follow the power and provisioning procedure. Both the Pico firmware and the boot package/Drive A system tracks must come from the same new build: the BIOS now emits startup evidence.
For an already provisioned board, selectively update the Phase 9 UF2,
z80boot.pkg, and matching drive_a_cpm63k-z80.img using Section 7's
addresses. Updating Drive A overwrites its existing files; the development
workflow does not create backups.
For disposable/new storage, the complete build/cpm-stage09/z80romless-flash.bin
contains everything. Building either target never flashes the board.
For a terminal-mapping-only firmware update on an already matching Phase 9
installation, flash only z80_stage09_flash_storage.uf2. The boot package,
CP/M disks and journal do not need to be rewritten.
Open the Pico's USB CDC port with a terminal, using your actual port name:
picocom --baud 115200 --databits 8 --parity n --stopbits 1 --flow n /dev/cu.usbmodemXXXX
Do not open a second serial owner. Disable local echo. In picocom, Ctrl+A then Ctrl+X exits the host terminal; Ctrl+] then a command controls the supervisor. Ordinary letters, digits and CP/M control keys are terminal input, not diagnostic commands.
| Sequence | Supervisor action |
|---|---|
Ctrl+] then l |
Recover journal; validate manifest/CRCs, exact 65,536-byte payload and JP FC00h; load and independently read back every SRAM byte; leave RESET LOW |
Ctrl+] then g |
Start the verified image from a fresh reset at 20 MHz |
Ctrl+] then t |
After loading, replace the SRAM entry code with a read-only Z80 disk diagnostic; compare every byte of all four drives; reboot afterward |
Ctrl+] then s |
Print run state, configured clock, BIOS evidence, disk/fatal state, queue drops and trap error counts |
Ctrl+] then x |
While running CP/M, complete/flush disk work, pause the flash worker, then hold RESET LOW and stop the clock; during the read-only diagnostic, stop directly |
Ctrl+] then r |
Reboot the Pico only while stopped; journal recovery runs on the next l |
Ctrl+] then 0 |
Disarm a fault while running |
Ctrl+] then 1-8 |
Arm a one-shot journal/flash-safe watchdog fault while running; disposable disks only |
Ctrl+] then h |
Print help |
| Ctrl+] then Ctrl+] | Send a literal Ctrl+] to CP/M |
ANSI keyboard mapping¶
Phase 9 ports the ESP32 Altair ansi_input mapper into the USB receive path.
Picocom forwards the host terminal's ANSI input sequences; the firmware
converts these to the CP/M application's WordStar-style control keys:
| Host key | ANSI input | CP/M byte |
|---|---|---|
| Up | ESC [ A |
Ctrl+E (05h) |
| Down | ESC [ B |
Ctrl+X (18h) |
| Right | ESC [ C |
Ctrl+D (04h) |
| Left | ESC [ D |
Ctrl+S (13h) |
| Insert | ESC [ 2 ~ |
Ctrl+O (0Fh) |
| Delete | ESC [ 3 ~ |
Ctrl+G (07h) |
| Page Up | ESC [ 5 ~ |
Ctrl+R (12h) |
| Page Down | ESC [ 6 ~ |
Ctrl+V (16h) |
Backspace (7Fh or 08h) |
Single byte | Ctrl+H (08h) |
A standalone Escape is delivered once after a 30 ms grace period, even when
no further serial bytes arrive. Sequences may span USB reads. Ordinary text
and other nonzero control bytes pass through; CRLF still becomes one carriage
return. As in the Altair mapper, NUL means no input, unsupported ESC [ keys
are consumed, and Escape followed by a non-[ byte forwards that byte.
This is not a general ANSI keyboard decoder: Home/End, function keys,
modified arrows and application-cursor (ESC O) sequences are not mapped.
Use normal cursor-key mode in the host terminal.
Ctrl+] supervisor commands are intercepted before mapping. Starting a supervisor command, stopping the CPU or changing the USB connection resets partial ANSI state. Terminal output and Phase 10's WebSocket keyboard handling are unchanged. Test the keys in an application configured for these control bindings; individual CP/M programs can assign different meanings.
1. Prove loading without executing CP/M¶
Type Ctrl+] then l. Loading reports recovery, manifest validation,
load and verify progress every 4 KiB. Cancellation is checked every 256 bytes:
type x (plain or after Ctrl+]) to cancel; loss of the USB connection also
cancels at the next checkpoint. Failure/cancellation asserts reset, inhibits
SRAM reads, isolates the data paths and stops the clock. The first fault includes
phase, address, expected/actual bytes and error category.
Success says 65,536 bytes loaded and independently read back; CPU not running. Check RESET LOW before proceeding. One load attempt is allowed per Pico boot; reboot after cancellation/failure or a stopped run instead of reinitializing live storage queues. Initial journal recovery uses SDK lockout with the already-running USB core.
2. Optional read-only Z80 disk qualification¶
After l, type Ctrl+] then t. The actual Z80 program selects drives
A-D and LBAs 0-4095, reads exactly 128 bytes per record using INIR, and
reports those bytes using OTIR on diagnostic port 0x06. The Pico compares
them in order with the recovered XIP flash image. A PASS requires exactly
2,097,152 matching bytes, all 16,384 records, and the final success
report on port 0x05. The test issues no disk write or flush commands.
It stops at the first mismatch, disk-reported failure, or five-minute timeout.
The byte offset in a mismatch identifies the drive (offset / 524288), LBA
within that drive ((offset % 524288) / 128) and byte within the record
(offset % 128). This is stronger than calling the C handlers directly,
but is not proof of journal durability, filesystem behavior or analogue
timing margins. Progress is printed every 64 KiB.
The diagnostic overwrites part of SRAM, so it cannot subsequently start
CP/M from that image. Use Ctrl+] r, reconnect if needed, then Ctrl+]
l to load CP/M again. Flash disk contents remain unchanged.
3. Start CP/M and prove it is alive¶
After a fresh successful l, type Ctrl+] then g. These are separate
milestones:
- The supervisor reports reset released and the configured clock.
- The Z80 BIOS reports cold entry (
OUT (05h),B0h). - The BIOS reports handoff to CCP (
OUT (05h),C0h); warm boot reports this too. - The CP/M banner and
A>prompt, followed by successful commands, prove the console and operating system are functioning.
The first three are not an automatic CP/M PASS. Missing BIOS/handoff evidence after ten seconds requests a safe stop; check that the matching BIOS package was provisioned. Read-only disk testing and a real CP/M prompt remain distinct acceptance results.
Start with DIR, DIR B:, DIR C: and DIR D:; check the distinct sentinels
listed below. Run LS and require a natural return through BIOS warm boot to
the prompt. Only then test copies/writes on disposable contents. The file
named drive_d_blank.img currently contains the documented DOCTOR.COM
sentinel; its filename does not indicate it is empty.
The shared terminal queues hold 128 input and 512 output bytes. While a terminal is connected, output remains buffered for Z80 throughput: the BIOS waits only when the output queue is full, and USB output is pumped only when the CDC transport has space. With no terminal connected, writes are discarded immediately and transmit-ready stays asserted, so console output cannot stall the application. Disconnecting clears pending output; reconnecting does not replay it. Bytes already handed to USB at disconnect are in flight and are not guaranteed. Intentional disconnected-output discards are not overflow errors. CRLF input becomes one CP/M carriage return. Actual queue overflow is counted and printed as FAIL; reduce paste rate and repeat any affected acceptance test.
To stop, use Ctrl+] then x and wait for Disk quiescent; RESET held
LOW, clock stopped. Do not remove power or request reboot before that
confirmation. A fatal storage error instead holds reset and requires reboot
recovery; preserve the error log first.
Firmware feature: On explicit l, recover any valid journal and validate the boot manifest
and CRC32 while the CPU is held reset. Then initialize the assisted loader,
release reset, inject instructions to write SRAM, and compare every byte.
Reset again before normal execution. The Pico never drives addresses or SRAM
write control; BUSACK is not required for cold loading. Follow the
injection protocol.
Once running, ports 0x10-0x14
provide command/status, drive, 16-bit LBA, and 128-byte data transfers.
Reads are synchronous XIP copies; writes use the journaled core-1
service and BUSY/READY/ERROR behavior defined in the
flash-storage architecture.
Before erasing a target sector, the backend reads back and compares the complete journal data sector and header page. A failed journal program therefore leaves the old target intact and latches a storage error. Target readback and header-clear checks remain required after programming.
Application source: main.c
enters core0.c,
which owns startup, Z80 control and the disk self-test.
status_report.c
formats the Ctrl-] s/h reports from a read-only state snapshot.
core1.c
owns USB parsing and runtime storage/service scheduling.
core_coordination.h
declares the shared atomic publication words and supervisor queue; all other
application state stays private to its owning core. Input mapping uses
the ANSI input mapper
and its interface,
with the shared disk device,
terminal queues,
flash backend, and
flash layout.
Flash Disk Image Loader (Final Phase 9 Integration)¶
Maintained source: flash_disk.h, flash_layout.h, disk_device.c, and flash_backend.c.
Loading a boot image is now a synchronous, core-0-only operation: a flash read is an ordinary memory access through the XIP-mapped pointer described in the flash-storage architecture, so bringing the initial image into SRAM needs no filesystem, blocking I/O call, or core 1 task. Only CP/M's live disk-sector writes still need to run on core 1 and cross back to core 0's foreground loop, because only they need to freeze the Z80 around a flash erase/program cycle using those same storage ownership rules.
Maintained Source¶
These complete files are included from the repository at documentation build time, not copied into this page.
Core 0 - startup, Z80 control and disk self-test
#include <stdio.h>
#include "hardware/clocks.h"
#include "hardware/flash.h"
#include "hardware/watchdog.h"
#include "hardware/sync.h"
#include "pico/multicore.h"
#include "pico/stdlib.h"
#include "pico/util/queue.h"
#include "z80sbc/clock.h"
#include "z80sbc/cpu.h"
#include "z80sbc/flash_disk.h"
#include "z80sbc/flash_layout.h"
#if PICO_ON_DEVICE
#include "pico/binary_info.h"
bi_decl(bi_program_feature(Z80_FLASH_LAYOUT_FEATURE));
#endif
#include "z80sbc/io_trap.h"
#include "z80sbc/banked_pio.h"
#include "z80sbc/io_ports.h"
#include "z80sbc/sram.h"
#include "z80sbc/supervisor.h"
#include "z80sbc/terminal.h"
#include "usb_console.h"
#include "../common/io_service.h"
#include "disk_diagnostic_image.h"
#include "core_coordination.h"
#include "status_report.h"
#ifdef Z80_STAGE10_NETWORK
#include "network_runtime.h"
#include "network_setup.h"
#include "web_terminal.h"
#include "oled_display.h"
#include "speaker_io.h"
#endif
enum {
RUN_CLOCK_HZ = 20000000,
SYSTEM_CLOCK_HZ = 120000000,
DIAGNOSTIC_BYTES = Z80_FLASH_DISK_SLOT_COUNT * Z80_FLASH_DISK_SLOT_BYTES,
};
static bool boot_attempted;
static bool image_verified;
static bool running;
static bool core1_started;
static bool runtime_started;
static bool startup_failed;
static bool loading;
#ifdef Z80_STAGE10_NETWORK
static bool config_reboot_after_stop;
static bool config_reboot_ready;
static absolute_time_t config_reboot_deadline;
static bool web_reboot_active;
static absolute_time_t web_reboot_deadline;
_Alignas(8) static uint32_t core1_network_stack[8192 / sizeof(uint32_t)];
#endif
static bool stop_requested;
static bool flush_submitted;
static bool bios_announced;
static bool ccp_announced;
static bool boot_timed_out;
static bool reset_was_watchdog;
static z80_io_trap_reset_fault_t reset_fault;
static const char *boot_failure;
static uint32_t boot_markers;
static bool flash_resuming;
static absolute_time_t boot_deadline;
static volatile bool diagnostic_active;
static volatile uint32_t diagnostic_count;
static volatile uint32_t diagnostic_result;
static uint32_t diagnostic_next_progress;
static uint32_t diagnostic_fault_offset;
static uint8_t diagnostic_expected;
static uint8_t diagnostic_actual;
static void publish_cpu_state(void) {
__atomic_store_n(&cpu_state,
(running ? CPU_RUNNING : 0) |
(stop_requested ? CPU_STOPPING : 0) |
(loading ? CPU_LOADING : 0), __ATOMIC_RELEASE);
}
static uint8_t __not_in_flash_func(virtual_read)(uint8_t port, void *context) {
(void)context;
switch (port) {
case 0x00:
case 0x01:
return z80_terminal_io_read(port);
case 0x10:
case 0x11:
case 0x12:
case 0x13:
case 0x14:
return z80_flash_disk_io_read(port);
case IO_SERVICE_COMMAND_PORT:
case IO_SERVICE_REQUEST_PORT:
case IO_SERVICE_RESPONSE_PORT:
case IO_TIMER_PORT:
return z80_io_port_in(port);
#ifdef Z80_STAGE10_NETWORK
case Z80_SPEAKER_PORT:
return z80_speaker_in();
#endif
default:
return 0xFF;
}
}
static void __not_in_flash_func(diagnostic_byte)(uint8_t value) {
uint32_t offset = diagnostic_count;
if (offset < DIAGNOSTIC_BYTES) {
const uint8_t *source =
(const uint8_t *)(XIP_BASE + Z80_FLASH_DISK_OFFSET);
if (value != source[offset] && diagnostic_result == 0) {
diagnostic_fault_offset = offset;
diagnostic_expected = source[offset];
diagnostic_actual = value;
diagnostic_result = 0xE1;
}
} else {
diagnostic_result = 0xE1;
}
diagnostic_count = offset + 1;
}
static void __not_in_flash_func(boot_report)(uint8_t value) {
if (diagnostic_active) {
if (diagnostic_result == 0)
diagnostic_result = value == 0x5A &&
diagnostic_count == DIAGNOSTIC_BYTES ? 0x5A : 0xE1;
} else if (value == Z80_BOOT_REPORT_BIOS) {
__atomic_fetch_or(&boot_markers, 1u, __ATOMIC_RELEASE);
} else if (value == Z80_BOOT_REPORT_CCP) {
__atomic_fetch_or(&boot_markers, 2u, __ATOMIC_RELEASE);
}
}
static void __not_in_flash_func(virtual_write)(uint8_t port, uint8_t value,
void *context) {
(void)context;
switch (port) {
case 0x00:
z80_terminal_io_write(port, value);
break;
case Z80_BOOT_REPORT_PORT:
boot_report(value);
break;
case 0x06:
if (diagnostic_active)
diagnostic_byte(value);
break;
case 0x10:
case 0x11:
case 0x12:
case 0x13:
case 0x14:
z80_flash_disk_io_write(port, value);
break;
case IO_SERVICE_COMMAND_PORT:
case IO_SERVICE_REQUEST_PORT:
case IO_SERVICE_RESPONSE_PORT:
case IO_TIMER_PORT:
z80_io_port_out(port, value);
break;
#ifdef Z80_STAGE10_NETWORK
case Z80_SPEAKER_PORT:
z80_speaker_out(value);
break;
#endif
default:
break;
}
}
#ifdef Z80_STAGE10_NETWORK
// The shared I2C bus (OLED and speaker) is driven only from core 0, so a
// deferred speaker IN never waits on core 1's network and flash work.
static bool i2c_updates_allowed(void) {
return running && !stop_requested && !loading &&
!__atomic_load_n(&flash_pause_request, __ATOMIC_ACQUIRE);
}
static void core0_i2c_service(bool oled_updates) {
z80_speaker_service();
z80_oled_service(oled_updates);
}
static void core0_bus_wait(void) {
core0_i2c_service(false);
z80_io_trap_drain_writes();
}
#endif
static void hold_cpu(void) {
z80_io_trap_disable();
z80_cpu_fail_closed();
running = false;
image_verified = false;
stop_requested = false;
diagnostic_active = false;
}
static const char *fault_name(z80_flash_fault_point_t point) {
static const char *const names[] = {
"none", "after journal data", "after journal header",
"after target erase", "after partial target program",
"after target verification", "after journal header clear",
"safe-execute entry failure", "safe-execute exit failure",
};
return point <= Z80_FLASH_FAULT_SAFE_EXECUTE_EXIT
? names[(unsigned int)point] : "invalid";
}
static bool observe_boot(const z80_boot_status_t *status, void *context) {
(void)context;
static const char *const phases[] = {
"journal recovery", "manifest/CRC validation", "SRAM load",
"SRAM verify", "verified; RESET held LOW",
};
if (status->completed % 4096u == 0 &&
(!Z80_STAGE09_AUTOBOOT ||
(__atomic_load_n(&usb_state, __ATOMIC_ACQUIRE) & USB_CONNECTED))) {
printf("[boot] %s: %lu/%lu\n", phases[status->phase],
(unsigned long)status->completed, (unsigned long)status->total);
}
if (Z80_STAGE09_AUTOBOOT)
return true;
return !__atomic_load_n(&load_cancelled, __ATOMIC_ACQUIRE) &&
(__atomic_load_n(&usb_state, __ATOMIC_ACQUIRE) & USB_CONNECTED);
}
static void load_image(void) {
if (boot_attempted || running || runtime_started) {
printf("\n[diag] Load already attempted; stop and reboot Pico to reload.\n");
return;
}
boot_attempted = true;
loading = true;
__atomic_store_n(&load_cancelled, 0, __ATOMIC_RELEASE);
publish_cpu_state();
if (!Z80_STAGE09_AUTOBOOT)
printf("\n[boot] Load/verify only; x cancels at a 256-byte boundary.\n");
bool loaded = z80_flash_storage_init_observed(observe_boot, NULL);
loading = false;
publish_cpu_state();
if (!loaded) {
const z80_boot_status_t *status = z80_flash_boot_status();
boot_failure = status_boot_error_name(status->error);
hold_cpu();
printf("[boot] FAIL: %s phase=%u completed=%lu address=%04x "
"expected=%02x actual=%02x; RESET held LOW\n",
status_boot_error_name(status->error), (unsigned int)status->phase,
(unsigned long)status->completed, status->address,
status->expected, status->actual);
return;
}
image_verified = true;
if (!Z80_STAGE09_AUTOBOOT)
printf("[boot] PASS: 65536 bytes loaded and independently read back; "
"CPU not running. Ctrl-] g starts CP/M at %lu MHz.\n",
(unsigned long)(RUN_CLOCK_HZ / 1000000u));
}
static void start_cpm(void) {
if (!image_verified || running || runtime_started) {
printf("\n[diag] Start rejected: first load/verify with Ctrl-] l; "
"a stopped run requires Pico reboot.\n");
return;
}
__atomic_store_n(&boot_markers, 0, __ATOMIC_RELEASE);
bios_announced = ccp_announced = false;
boot_timed_out = false;
z80_io_ports_init();
#ifdef Z80_STAGE10_NETWORK
z80_banked_pio_set_read_gate(Z80_SPEAKER_PORT, z80_speaker_read_ready);
// Core 0 waiting for BUSACK must keep finishing the speaker transfer that a
// deferred IN is waiting for, and retrying that reply.
z80_cpu_set_bus_wait_hook(core0_bus_wait);
#endif
runtime_started = true;
if (!z80_io_trap_enable(virtual_read, virtual_write, NULL) ||
!z80_cpu_release_reset_and_run(RUN_CLOCK_HZ)) {
boot_failure = "trap/clock start";
hold_cpu();
printf("[boot] FAIL: trap/clock start; RESET held LOW\n");
return;
}
running = true;
__atomic_store_n(&workers_ready, 1, __ATOMIC_RELEASE);
boot_deadline = make_timeout_time_ms(10000);
if (!Z80_STAGE09_AUTOBOOT)
printf("\n[boot] CPU released from reset; configured clock=%lu Hz. "
"Waiting for BIOS evidence, not yet a CP/M PASS.\n",
(unsigned long)z80_clock_get_hz());
}
static void start_disk_diagnostic(void) {
if (!image_verified || runtime_started || running) {
printf("\n[diag] Disk test requires Ctrl-] l and no prior run.\n");
return;
}
image_verified = false;
diagnostic_count = diagnostic_result = 0;
diagnostic_fault_offset = UINT32_MAX;
diagnostic_next_progress = 65536;
if (!z80_cpu_load_and_verify(z80_disk_diagnostic,
sizeof(z80_disk_diagnostic))) {
hold_cpu();
printf("\n[diag] FAIL: disk diagnostic load/readback; RESET held LOW\n");
return;
}
diagnostic_active = true;
if (!z80_io_trap_enable(virtual_read, virtual_write, NULL) ||
!z80_cpu_release_reset_and_run(RUN_CLOCK_HZ)) {
hold_cpu();
printf("\n[diag] FAIL: disk diagnostic start; RESET held LOW\n");
return;
}
running = true;
boot_deadline = make_timeout_time_ms(300000);
printf("\n[diag] Read-only Z80 INIR/OTIR disk test at %lu MHz: "
"all 4 drives x 4096 records x 128 bytes. No disk writes. "
"CP/M SRAM image replaced; reboot afterward.\n",
(unsigned long)(RUN_CLOCK_HZ / 1000000u));
}
static core0_status_t status_state(void) {
return (core0_status_t){
.running = running,
.boot_attempted = boot_attempted,
.image_verified = image_verified,
.bios_announced = bios_announced,
.ccp_announced = ccp_announced,
.reset_was_watchdog = reset_was_watchdog,
.reset_fault = reset_fault,
.boot_failure = boot_failure,
};
}
static void print_status(bool requested) {
core0_status_t state = status_state();
status_print(&state, requested);
}
static void print_help(void) {
core0_status_t state = status_state();
status_print_help(&state);
}
static void diagnostic_command(int command) {
if (command == 'l') {
load_image();
} else if (command == 'g') {
start_cpm();
} else if (command == 't') {
start_disk_diagnostic();
} else if (command == 's') {
print_status(true);
} else if (command == 'h') {
print_help();
} else if (command == 'x') {
if (!running || diagnostic_active) {
hold_cpu();
printf("\n[diag] RESET held LOW; reboot required before a new run.\n");
print_help();
} else {
stop_requested = true;
flush_submitted = false;
__atomic_store_n(&flash_pause_request, 1, __ATOMIC_RELEASE);
printf("\n[diag] Stop requested; waiting for disk completion/flush.\n");
}
} else if (command == 'r') {
if (running || stop_requested)
printf("\n[diag] Reboot rejected: stop and flush with Ctrl-] x first.\n");
else
watchdog_reboot(0, 0, 0);
} else if (command >= '0' && command <= '8') {
if (!running) {
printf("\n[diag] Fault command rejected: CPU is not running.\n");
return;
}
z80_flash_fault_point_t point =
(z80_flash_fault_point_t)(command - '0');
z80_flash_disk_arm_fault(point);
printf("\n[diag] Armed: %s; next disk write/flush may reboot Pico.\n",
fault_name(point));
} else {
printf("\n[diag] Unknown command; Ctrl-] h for help.\n");
}
}
static void service_control(void) {
#ifdef Z80_STAGE10_NETWORK
if (z80_web_terminal_reboot_status() == Z80_WEB_REBOOT_REQUESTED) {
if (config_reboot_after_stop || config_reboot_ready || stop_requested ||
!running || diagnostic_active || z80_flash_disk_has_fatal_error()) {
z80_web_terminal_set_reboot_status(Z80_WEB_REBOOT_REJECTED);
} else if (z80_web_terminal_begin_reboot()) {
web_reboot_active = true;
web_reboot_deadline = make_timeout_time_ms(30000);
config_reboot_after_stop = true;
diagnostic_command('x');
}
}
if (web_reboot_active &&
z80_web_terminal_reboot_status() == Z80_WEB_REBOOT_TIMEOUT) {
web_reboot_active = false;
config_reboot_after_stop = config_reboot_ready = false;
printf("\n[web] Reboot cancelled after network-side timeout; "
"orderly stop remains pending.\n");
}
if (web_reboot_active && z80_flash_disk_has_fatal_error()) {
web_reboot_active = false;
config_reboot_after_stop = config_reboot_ready = false;
z80_web_terminal_set_reboot_status(Z80_WEB_REBOOT_STORAGE_ERROR);
}
if (web_reboot_active && time_reached(web_reboot_deadline)) {
web_reboot_active = false;
config_reboot_after_stop = config_reboot_ready = false;
// Keep draining/stopping; never force a reset through an unfinished write.
z80_web_terminal_set_reboot_status(Z80_WEB_REBOOT_TIMEOUT);
printf("\n[web] Reboot timed out; automatic restart cancelled. "
"Orderly stop remains pending.\n");
}
if (config_reboot_ready && time_reached(config_reboot_deadline)) {
config_reboot_ready = false;
watchdog_reboot(0, 0, 0);
return;
}
#endif
/* Peek without the queue lock, which would mask the PIO1 IRQ on every pass.
* Core 0 is the only consumer, so a racing add is seen on the next pass. */
uint16_t command;
if (queue_get_level_unsafe(&control_commands) != 0 &&
queue_try_remove(&control_commands, &command)) {
#ifdef Z80_STAGE10_NETWORK
if (command == USB_CONFIG_REBOOT) {
if (!running || diagnostic_active || z80_flash_disk_has_fatal_error()) {
printf("\n[config] FAIL: automatic reboot rejected: CP/M is not "
"running normally or storage has failed. CPU state unchanged; "
"inspect Ctrl-] s before manual recovery.\n");
} else {
config_reboot_after_stop = true;
if (!stop_requested)
diagnostic_command('x');
}
} else if ((config_reboot_after_stop || config_reboot_ready) &&
command != 's' && command != 'h' &&
command != USB_REPORT_CONNECTION) {
printf("\n[config] Orderly reboot in progress; command rejected.\n");
} else
#endif
if (command == USB_REPORT_CONNECTION) {
if (!Z80_STAGE09_AUTOBOOT) {
print_help();
print_status(false);
} else if (!running && !loading && boot_attempted) {
printf("\n[boot] %s; RESET held LOW. Ctrl-] s for status, "
"Ctrl-] r to reboot.\n",
boot_failure != NULL ? "FAIL: automatic CP/M boot stopped"
: "CP/M stopped");
print_status(false);
}
} else if (startup_failed && command != 'r' && command != 's' &&
command != 'h') {
printf("\n[diag] Rejected: core 1 initialization failed; CPU held reset.\n");
} else {
diagnostic_command(command);
}
}
publish_cpu_state();
}
static void service_run(void) {
if (!running)
return;
if (diagnostic_active) {
uint32_t interrupts = save_and_disable_interrupts();
uint32_t count = diagnostic_count;
uint32_t result = diagnostic_result;
restore_interrupts(interrupts);
if (result != 0 || time_reached(boot_deadline)) {
hold_cpu();
printf("\n[diag] %s: disk bytes checked=%lu/%u "
"fault_offset=%08lx expected=%02x actual=%02x; "
"RESET held LOW; Ctrl-] r then load CP/M again.\n",
result == 0x5A ? "PASS" : "FAIL",
(unsigned long)count, DIAGNOSTIC_BYTES,
(unsigned long)diagnostic_fault_offset,
diagnostic_expected, diagnostic_actual);
} else if (count >= diagnostic_next_progress) {
printf("[diag] disk bytes checked=%lu/%u\n",
(unsigned long)count, DIAGNOSTIC_BYTES);
diagnostic_next_progress += 65536;
}
return;
}
uint32_t markers = __atomic_load_n(&boot_markers, __ATOMIC_ACQUIRE);
if ((markers & 1u) && !bios_announced) {
bios_announced = true;
if (!Z80_STAGE09_AUTOBOOT)
printf("\n[boot] Observed BIOS cold-entry report from Z80.\n");
}
if ((markers & 2u) && !ccp_announced) {
ccp_announced = true;
if (!Z80_STAGE09_AUTOBOOT)
printf("\n[boot] Observed BIOS handoff to CCP; "
"confirm A>, DIR and transient/warm-boot tests manually.\n");
}
if (!boot_timed_out && (!bios_announced || !ccp_announced) &&
time_reached(boot_deadline)) {
boot_failure = "BIOS/CCP evidence timeout";
printf("\n[boot] FAIL: BIOS/CCP evidence timeout; stopping safely. "
"Rebuild/provision the matching boot package if markers are absent.\n");
status_print_pio();
stop_requested = true;
boot_timed_out = true;
__atomic_store_n(&flash_pause_request, 1, __ATOMIC_RELEASE);
}
if (z80_flash_disk_has_fatal_error()) {
#ifdef Z80_STAGE10_NETWORK
if (config_reboot_after_stop) {
config_reboot_after_stop = false;
printf("\n[config] FAIL: storage error; automatic reboot cancelled.\n");
}
if (web_reboot_active) {
web_reboot_active = false;
z80_web_terminal_set_reboot_status(Z80_WEB_REBOOT_STORAGE_ERROR);
}
#endif
boot_failure = "fatal storage error";
hold_cpu();
printf("\n[diag] FAIL: fatal storage error; RESET held LOW. "
"Reboot to recover; preserve USB log.\n");
return;
}
#ifdef Z80_STAGE10_NETWORK
if (bios_announced && ccp_announced && !stop_requested &&
z80_web_terminal_reboot_status() == Z80_WEB_REBOOT_IDLE)
z80_web_terminal_set_reboot_status(Z80_WEB_REBOOT_CPM_READY);
#endif
if (!stop_requested)
return;
if (flash_resuming) {
if (!__atomic_load_n(&flash_paused, __ATOMIC_ACQUIRE)) {
flash_resuming = false;
flush_submitted = false;
__atomic_store_n(&flash_pause_request, 1, __ATOMIC_RELEASE);
}
return;
}
if (__atomic_load_n(&flash_paused, __ATOMIC_ACQUIRE)) {
// PIO completes OUTs ahead of core 0. Park the Z80 between machine cycles,
// then apply every completed OUT before deciding, so the disk state seen
// here is exactly what the Z80 has issued.
if (!z80_cpu_request_bus(500000)) {
boot_failure = "stop bus request";
hold_cpu();
printf("\n[diag] FAIL: Z80 did not release the bus for stop; "
"RESET held LOW.\n");
return;
}
uint32_t interrupts = save_and_disable_interrupts();
z80_io_trap_drain_writes();
bool quiescent = z80_flash_disk_quiescent();
#ifdef Z80_STAGE10_NETWORK
quiescent = quiescent && z80_speaker_quiescent();
#endif
if (quiescent)
hold_cpu();
restore_interrupts(interrupts);
if (!quiescent && !z80_cpu_release_bus(500000)) {
boot_failure = "stop bus release";
hold_cpu();
printf("\n[diag] FAIL: Z80 bus release after stop check; "
"RESET held LOW.\n");
return;
}
if (quiescent) {
#ifdef Z80_STAGE10_NETWORK
if (config_reboot_after_stop) {
config_reboot_after_stop = false;
config_reboot_ready = true;
config_reboot_deadline = make_timeout_time_ms(1000);
if (web_reboot_active)
z80_web_terminal_set_reboot_status(Z80_WEB_REBOOT_RESTARTING);
printf("\n[config] Disks flushed; RESET held LOW, clock stopped. "
"Rebooting in one second.\n");
} else
#endif
{
printf("\n[diag] Disk quiescent; RESET held LOW, clock stopped. "
"Ctrl-] r reboots/reloads the supervisor.\n");
print_help();
}
} else {
flash_resuming = true;
__atomic_store_n(&flash_pause_request, 0, __ATOMIC_RELEASE);
}
} else if (!flush_submitted) {
z80_flash_disk_request_flush();
flush_submitted = true;
}
}
int core0_main(void) {
z80_safe_startup();
reset_was_watchdog = watchdog_caused_reboot();
reset_fault = z80_io_trap_take_reset_fault();
set_sys_clock_pll(1200000000, 5, 2);
z80_terminal_queue_init();
queue_init(&control_commands, sizeof(uint16_t), 4);
z80_usb_console_prepare();
#ifdef Z80_STAGE10_NETWORK
z80_network_setup_init();
#endif
bool clock_ready = clock_get_hz(clk_sys) == SYSTEM_CLOCK_HZ;
#ifdef Z80_STAGE10_NETWORK
multicore_launch_core1_with_stack(core1_main, core1_network_stack,
sizeof(core1_network_stack));
#else
multicore_launch_core1(core1_main);
#endif
core1_started = true;
absolute_time_t ready_deadline = make_timeout_time_ms(1000);
while (!__atomic_load_n(&core1_ready, __ATOMIC_ACQUIRE) &&
!time_reached(ready_deadline))
tight_loop_contents();
if (!clock_ready ||
__atomic_load_n(&core1_ready, __ATOMIC_ACQUIRE) != 1) {
z80_cpu_fail_closed();
startup_failed = true;
boot_failure = !clock_ready ? "expected 120 MHz supervisor clock"
: "core 1 USB/flash-safe initialization";
while (true) {
printf("FAIL: %s; CPU held reset\n", boot_failure);
service_control();
busy_wait_us_32(1000000);
tight_loop_contents();
}
}
if (Z80_STAGE09_AUTOBOOT) {
load_image();
if (image_verified)
start_cpm();
}
publish_cpu_state();
while (true) {
if (__atomic_load_n(&workers_ready, __ATOMIC_ACQUIRE)) {
#ifdef Z80_STAGE10_NETWORK
// Before the trap poll, so a finished transfer releases its IN this pass.
core0_i2c_service(i2c_updates_allowed());
#endif
z80_flash_core0_service();
}
if (core1_started && z80_io_ports_reboot_requested()) {
hold_cpu();
watchdog_reboot(0, 0, 0);
while (true)
tight_loop_contents();
}
service_control();
service_run();
publish_cpu_state();
tight_loop_contents();
}
}
Core 0 - status and help reports
#include <stdio.h>
#include "hardware/clocks.h"
#include "z80sbc/banked_pio.h"
#include "z80sbc/clock.h"
#include "z80sbc/flash_disk.h"
#include "z80sbc/io_ports.h"
#include "z80sbc/sram.h"
#include "z80sbc/supervisor.h"
#include "z80sbc/terminal.h"
#include "usb_console.h"
#include "../common/io_service.h"
#include "core_coordination.h"
#include "status_report.h"
#ifdef Z80_STAGE10_NETWORK
#include "web_terminal.h"
#include "oled_display.h"
#include "speaker_io.h"
#endif
const char *status_boot_error_name(z80_boot_error_t error) {
static const char *const names[] = {
"none", "journal recovery",
"manifest (requires 65536 bytes and JP FC00h)",
"payload CRC", "loader controls", "SRAM write", "SRAM read",
"SRAM mismatch", "flash-safe core initialization", "cancelled",
};
return (unsigned int)error < sizeof(names) / sizeof(names[0])
? names[(unsigned int)error] : "invalid";
}
static void print_reset_status(const core0_status_t *state) {
const char *reason = "none";
if (state->reset_fault.reason == Z80_IO_TRAP_RESET_RELEASE_TIMEOUT)
reason = "release-timeout";
else if (state->reset_fault.reason == Z80_IO_TRAP_RESET_INVALID_CONTROLS)
reason = "invalid-controls";
printf("[reset] watchdog=%u io_fault=%s gpio=%08lx\n",
state->reset_was_watchdog, reason,
(unsigned long)state->reset_fault.gpio);
}
void status_print_pio(void) {
z80_banked_pio_status_t p = z80_banked_pio_status();
printf("[pio] active=%u running=%u reads=%lu writes=%lu "
"last=%s:%02x:%02x pc=%u/%u irq=%02x gpio=%08lx\n",
p.active, p.running,
(unsigned long)p.reads, (unsigned long)p.writes,
p.read ? "IN" : "OUT", p.port, p.value, p.clock_pc, p.io_pc,
p.irqs, (unsigned long)p.pins);
}
static void print_io_latency(void) {
z80_banked_pio_latency_t l = z80_banked_pio_take_latency();
if (!l.available) {
printf("[iolat] unavailable (no DWT cycle counter)\n");
return;
}
// Core-0 cost since the previous Ctrl-] s; clk_sys cycles to ns.
const uint64_t mhz = clock_get_hz(clk_sys) / 1000000u;
unsigned long long in_avg =
l.in_count ? l.in_cycles * 1000u / mhz / l.in_count : 0;
unsigned long long out_avg =
l.out_count ? l.out_cycles * 1000u / mhz / l.out_count : 0;
printf("[iolat] since last status: IN count=%lu avg=%lluns max=%lluns; "
"OUT apply count=%lu avg=%lluns max=%lluns\n",
(unsigned long)l.in_count, in_avg,
(unsigned long long)l.in_max_cycles * 1000u / mhz,
(unsigned long)l.out_count, out_avg,
(unsigned long long)l.out_max_cycles * 1000u / mhz);
}
#ifdef Z80_STAGE10_NETWORK
static void print_network_status(bool requested) {
if (requested) {
char address[16];
bool has_address = z80_io_ports_get_network_address(address, sizeof(address)) != 0;
printf("[network] browser=%u setup=%u ip=%s\n",
!!(__atomic_load_n(&usb_state, __ATOMIC_ACQUIRE) & USB_BROWSER),
!!(__atomic_load_n(&usb_state, __ATOMIC_ACQUIRE) & USB_SETUP),
has_address ? address : "unavailable");
if (has_address)
printf("[network] Terminal: http://%s/\n", address);
z80_oled_print_status();
z80_speaker_print_status();
}
z80_web_terminal_stats_t web = z80_web_terminal_get_stats();
printf("[web] flush_failures=%lu ack_timeouts=%lu pong_timeouts=%lu "
"last_disconnect=%s\n",
(unsigned long)web.flush_failures, (unsigned long)web.ack_timeouts,
(unsigned long)web.pong_timeouts,
z80_web_terminal_disconnect_name(web.last_disconnect_reason));
if (web.last_ws_close_reason != 0)
printf("[webclose] reason=%s lwip_error=%ld\n",
z80_web_terminal_ws_close_name(web.last_ws_close_reason),
(long)web.last_ws_transport_error);
printf("[webbuf] out_frames=%lu/%u peak=%lu staged=%lu/%u\n",
(unsigned long)web.window_frames, Z80_WEB_TERMINAL_OUTPUT_WINDOW,
(unsigned long)web.window_frames_peak, (unsigned long)web.staged_bytes,
Z80_WEB_TERMINAL_FRAME_BYTES);
z80_terminal_buffer_stats_t buffers = z80_terminal_get_buffer_stats();
printf("[consolebuf] rx=%lu/%lu peak=%lu tx=%lu/%lu peak=%lu\n",
(unsigned long)buffers.rx_used, (unsigned long)buffers.rx_capacity,
(unsigned long)buffers.rx_peak, (unsigned long)buffers.tx_used,
(unsigned long)buffers.tx_capacity, (unsigned long)buffers.tx_peak);
}
#endif
void status_print(const core0_status_t *state, bool requested) {
if (requested)
print_reset_status(state);
uint64_t uptime_seconds = z80_service_millis() / 1000;
printf("[system] uptime=%02llu:%02llu:%02llu\n",
(unsigned long long)(uptime_seconds / 3600),
(unsigned long long)((uptime_seconds / 60) % 60),
(unsigned long long)(uptime_seconds % 60));
#ifdef Z80_STAGE10_NETWORK
print_network_status(requested);
#endif
status_print_pio();
if (requested)
print_io_latency();
uint32_t readahead_hits, readahead_misses;
z80_flash_disk_prefetch_counts(&readahead_hits, &readahead_misses);
printf("[disk] readahead hits=%lu misses=%lu\n",
(unsigned long)readahead_hits, (unsigned long)readahead_misses);
printf("[diag] banked_sram=%02x (00=unavailable, B4/B5=ready bank 0/1)\n",
(unsigned int)z80_memory_bank_status());
const z80_boot_status_t *boot = z80_flash_boot_status();
printf("\n[diag] running=%u verified=%u clock=%lu BIOS=%u CCP_handoff=%u "
"disk=%02lx fatal=%u rx_drops=%lu tx_drops=%lu "
"trap_timeouts=%lu control_errors=%lu boot_error=%s "
"failure=%s log_drops=%lu\n",
state->running, state->image_verified,
(unsigned long)z80_clock_get_hz(),
state->bios_announced, state->ccp_announced,
(unsigned long)z80_flash_disk_status(),
z80_flash_disk_has_fatal_error(),
(unsigned long)z80_terminal_rx_drop_count(),
(unsigned long)z80_terminal_tx_drop_count(),
(unsigned long)z80_io_trap_timeout_count(),
(unsigned long)z80_io_trap_control_error_count(),
status_boot_error_name(boot->error),
state->boot_failure != NULL ? state->boot_failure : "none",
(unsigned long)z80_usb_console_log_drops());
if (boot->error != Z80_BOOT_ERROR_NONE)
printf("[boot] phase=%u completed=%lu address=%04x "
"expected=%02x actual=%02x\n",
(unsigned int)boot->phase, (unsigned long)boot->completed,
boot->address, boot->expected, boot->actual);
}
void status_print_help(const core0_status_t *state) {
#ifdef Z80_STAGE10_NETWORK
printf("\nStage 10: Phase 9 automatic CP/M loader plus Wi-Fi terminal.\n"
"Ctrl-] c=system configuration; browser owns CP/M while connected, "
"USB resumes on disconnect.\n");
#else
printf("\nStage 9: USB CP/M terminal; nominal 20 MHz after slow loading.\n");
#endif
printf("Ctrl-] then: l=load/verify only, g=run verified CP/M, s=status,\n"
"t=read-only all-drive disk test after load (reboot afterward),\n"
"x=flush then hold RESET, r=reboot when stopped, h=help,\n"
"0=disarm, 1-8=arm one-shot flash fault (DISPOSABLE DISKS ONLY).\n"
"Ctrl-] Ctrl-] sends literal Ctrl-]. Ordinary bytes go to CP/M.\n");
if (!state->running && state->boot_attempted && !state->image_verified)
printf("Stopped: Ctrl-] r=reboot, s=status, h=help.\n"
"Loading/running/testing (l/g/t) requires a Pico reboot first; "
"fault commands (0-8) require a running CPU.\n");
else if (Z80_STAGE09_AUTOBOOT)
printf("CP/M loads and starts automatically on Pico reset. "
"Ctrl-] x then Ctrl-] r safely reboots into CP/M.\n");
else
printf("First: Ctrl-] l, then Ctrl-] g; confirm BIOS banner and A>.\n");
}
Core 1 - USB and runtime workers
#include <stdio.h>
#include "pico/flash.h"
#include "pico/stdio_usb.h"
#include "pico/stdlib.h"
#include "tusb.h"
#include "z80sbc/flash_disk.h"
#include "z80sbc/io_ports.h"
#include "z80sbc/terminal.h"
#include "ansi_input.h"
#include "usb_console.h"
#include "core_coordination.h"
#ifdef Z80_STAGE10_NETWORK
#include "network_runtime.h"
#include "network_setup.h"
#endif
static bool command_pending;
static bool usb_was_connected;
static uint32_t previous_cpu_state;
static bool previous_cr;
static uint32_t reported_rx_drops;
static uint32_t reported_tx_drops;
#ifdef Z80_STAGE10_NETWORK
static bool browser_was_connected;
static bool config_reboot_queue_full;
#endif
/* USB owns the CP/M terminal unless the setup shell or a browser has it. */
static bool usb_owns_terminal(void) {
#ifdef Z80_STAGE10_NETWORK
return !z80_network_setup_active() && !z80_terminal_browser_connected();
#else
return true;
#endif
}
static void forward_terminal_byte(uint8_t value) {
if (value == 0)
return;
if (value == '\n' && previous_cr) {
previous_cr = false;
return;
}
previous_cr = value == '\r';
#ifdef Z80_STAGE10_NETWORK
z80_terminal_usb_receive(value);
#else
z80_terminal_receive(&value, 1);
#endif
}
static void receive_usb_byte(uint8_t value) {
uint32_t state = __atomic_load_n(&cpu_state, __ATOMIC_ACQUIRE);
if (command_pending) {
command_pending = false;
if (value != USB_COMMAND_ESCAPE) {
#ifdef Z80_STAGE10_NETWORK
if (value == 'c') {
if ((state & (CPU_RUNNING | CPU_STOPPING)) == CPU_RUNNING)
z80_network_setup_request();
else
printf("\n[config] Rejected: CP/M must be running, not stopping.\n");
return;
}
#endif
if (value == 'x' && (state & CPU_LOADING) && !Z80_STAGE09_AUTOBOOT)
__atomic_store_n(&load_cancelled, 1, __ATOMIC_RELEASE);
else {
uint16_t command = value;
if (!queue_try_add(&control_commands, &command))
printf("\n[diag] FAIL: supervisor command queue full; retry command.\n");
}
return;
}
} else if (value == USB_COMMAND_ESCAPE) {
ansi_input_reset();
command_pending = true;
return;
}
if (state & CPU_LOADING) {
if (value == 'x' && !Z80_STAGE09_AUTOBOOT)
__atomic_store_n(&load_cancelled, 1, __ATOMIC_RELEASE);
return;
}
#ifdef Z80_STAGE10_NETWORK
if ((state & (CPU_RUNNING | CPU_STOPPING)) == CPU_RUNNING &&
z80_network_setup_input(value))
return;
if (z80_terminal_browser_connected())
return;
#endif
if (!(state & CPU_RUNNING)) {
if (Z80_STAGE09_AUTOBOOT)
printf("\n[diag] CPU held; Ctrl-] s for status, "
"Ctrl-] r to retry automatic boot.\n");
else
printf("\n[diag] CPU held; use Ctrl-] l then Ctrl-] g.\n");
return;
}
forward_terminal_byte(
ansi_input_process(value, to_ms_since_boot(get_absolute_time())));
}
static void service_usb(void) {
bool connected = stdio_usb_connected();
uint32_t state = __atomic_load_n(&cpu_state, __ATOMIC_ACQUIRE);
if (state != previous_cpu_state) {
ansi_input_reset();
previous_cr = false;
previous_cpu_state = state;
}
#ifdef Z80_STAGE10_NETWORK
if (connected != usb_was_connected)
z80_terminal_usb_connected(connected);
bool browser_connected = z80_terminal_browser_connected();
if (browser_connected != browser_was_connected) {
ansi_input_reset();
previous_cr = false;
browser_was_connected = browser_connected;
}
z80_network_setup_usb_service(
connected, (state & (CPU_RUNNING | CPU_STOPPING)) == CPU_RUNNING);
if (z80_network_setup_reboot_requested()) {
uint16_t command = USB_CONFIG_REBOOT;
if (queue_try_add(&control_commands, &command)) {
z80_network_setup_reboot_queued();
config_reboot_queue_full = false;
} else if (!config_reboot_queue_full) {
printf("\n[config] Supervisor queue full; retrying orderly reboot.\n");
config_reboot_queue_full = true;
}
}
#else
if (connected != usb_was_connected)
z80_terminal_set_connected(connected);
#endif
__atomic_store_n(&usb_state, (connected ? USB_CONNECTED : 0)
#ifdef Z80_STAGE10_NETWORK
| (browser_connected ? USB_BROWSER : 0)
| (z80_network_setup_active() ? USB_SETUP : 0)
#endif
, __ATOMIC_RELEASE);
if (connected != usb_was_connected)
ansi_input_reset();
if (connected && !usb_was_connected) {
command_pending = false;
previous_cr = false;
uint16_t command = USB_REPORT_CONNECTION;
if (!queue_try_add(&control_commands, &command))
printf("\n[diag] FAIL: connection report queue full; Ctrl-] s for status.\n");
}
usb_was_connected = connected;
if (!connected)
return;
uint8_t input[32];
size_t received = z80_usb_console_read(input, sizeof(input));
for (size_t index = 0; index < received; ++index)
receive_usb_byte(input[index]);
/* Only a lone Escape waits on the clock; other passes skip the time read. */
if (ansi_input_escape_pending() && (state & CPU_RUNNING) &&
!command_pending && usb_owns_terminal())
forward_terminal_byte(
ansi_input_process(0, to_ms_since_boot(get_absolute_time())));
if (z80_terminal_output_pending() && usb_owns_terminal()) {
uint8_t data[64];
size_t capacity = tud_cdc_write_available();
if (capacity > sizeof(data))
capacity = sizeof(data);
size_t count = z80_terminal_peek(data, capacity);
if (count) {
size_t accepted = z80_usb_console_write(data, count);
z80_terminal_supply(data, accepted);
}
}
uint32_t rx = z80_terminal_rx_drop_count();
uint32_t tx = z80_terminal_tx_drop_count();
if (rx != reported_rx_drops || tx != reported_tx_drops) {
printf("\n[diag] FAIL: terminal overflow rx=%lu tx=%lu; "
"reduce paste/output rate and repeat acceptance.\n",
(unsigned long)rx, (unsigned long)tx);
reported_rx_drops = rx;
reported_tx_drops = tx;
}
}
static void run_workers(void) {
z80_flash_core1_service();
z80_io_ports_core1_service();
#ifdef Z80_STAGE10_NETWORK
z80_network_setup_core1_service();
#endif
}
static bool workers_quiescent(void) {
return z80_flash_disk_quiescent() && z80_io_ports_quiescent()
#ifdef Z80_STAGE10_NETWORK
&& z80_network_setup_quiescent()
#endif
;
}
/* Disk and service workers run first, then USB, then the network. A pause
* request lets the workers drain, then holds them until core 0 withdraws it. */
static void core1_service(void) {
bool active = __atomic_load_n(&workers_ready, __ATOMIC_ACQUIRE);
bool pausing =
active && __atomic_load_n(&flash_pause_request, __ATOMIC_ACQUIRE);
if (pausing) {
if (!__atomic_load_n(&flash_paused, __ATOMIC_ACQUIRE)) {
run_workers();
if (workers_quiescent())
__atomic_store_n(&flash_paused, 1, __ATOMIC_RELEASE);
}
} else if (active) {
__atomic_store_n(&flash_paused, 0, __ATOMIC_RELEASE);
run_workers();
}
z80_usb_console_service();
service_usb();
#ifdef Z80_STAGE10_NETWORK
if (pausing)
z80_network_terminal_core1_service();
else if (active)
z80_network_core1_service();
#endif
}
void core1_main(void) {
bool flash_ready = flash_safe_execute_core_init();
bool usb_ready = z80_usb_console_init();
if (!flash_ready || !usb_ready)
printf("\n[diag] FAIL: core 1 USB/flash-safe initialization.\n");
__atomic_store_n(&core1_ready, flash_ready && usb_ready ? 1u : 2u,
__ATOMIC_RELEASE);
while (true) {
if (usb_ready)
core1_service();
tight_loop_contents();
}
}
ANSI keyboard mapper - src/stage09_flash_storage/ansi_input.c
#include "ansi_input.h"
#define CTRL_KEY(ch) ((ch) & 0x1f)
enum {
KEY_STATE_NORMAL,
KEY_STATE_ESC,
KEY_STATE_ESC_BRACKET,
KEY_STATE_ESC_BRACKET_NUM,
};
static uint8_t key_state;
static uint8_t pending_key;
static uint32_t esc_start;
void ansi_input_reset(void) {
key_state = KEY_STATE_NORMAL;
pending_key = 0;
esc_start = 0;
}
bool ansi_input_escape_pending(void) { return key_state == KEY_STATE_ESC; }
uint8_t ansi_input_process(uint8_t ch, uint32_t now_ms) {
switch (key_state) {
case KEY_STATE_NORMAL:
if (ch == 0x1b) {
key_state = KEY_STATE_ESC;
esc_start = now_ms;
return 0;
}
if (ch == 0x7f || ch == 0x08)
return CTRL_KEY('H');
return ch;
case KEY_STATE_ESC:
if (ch == 0) {
if ((uint32_t)(now_ms - esc_start) >= ANSI_INPUT_ESC_GRACE_MS) {
key_state = KEY_STATE_NORMAL;
return 0x1b;
}
return 0;
}
if (ch == '[') {
key_state = KEY_STATE_ESC_BRACKET;
return 0;
}
key_state = KEY_STATE_NORMAL;
return ch;
case KEY_STATE_ESC_BRACKET:
if (ch == 0)
return 0;
switch (ch) {
case 'A':
key_state = KEY_STATE_NORMAL;
return CTRL_KEY('E');
case 'B':
key_state = KEY_STATE_NORMAL;
return CTRL_KEY('X');
case 'C':
key_state = KEY_STATE_NORMAL;
return CTRL_KEY('D');
case 'D':
key_state = KEY_STATE_NORMAL;
return CTRL_KEY('S');
case '2':
pending_key = CTRL_KEY('O');
key_state = KEY_STATE_ESC_BRACKET_NUM;
return 0;
case '3':
pending_key = CTRL_KEY('G');
key_state = KEY_STATE_ESC_BRACKET_NUM;
return 0;
case '5':
pending_key = CTRL_KEY('R');
key_state = KEY_STATE_ESC_BRACKET_NUM;
return 0;
case '6':
pending_key = CTRL_KEY('V');
key_state = KEY_STATE_ESC_BRACKET_NUM;
return 0;
default:
key_state = KEY_STATE_NORMAL;
return 0;
}
case KEY_STATE_ESC_BRACKET_NUM:
if (ch == 0)
return 0;
key_state = KEY_STATE_NORMAL;
if (ch == '~') {
uint8_t result = pending_key;
pending_key = 0;
return result;
}
pending_key = 0;
return 0;
}
ansi_input_reset();
return 0;
}
Flash geometry and boot manifest - src/common/include/z80sbc/flash_layout.h
#ifndef Z80SBC_FLASH_LAYOUT_H
#define Z80SBC_FLASH_LAYOUT_H
/* Machine-readable picotool feature; kept in sync by host layout tests. */
#define Z80_FLASH_LAYOUT_FEATURE "z80sbc-storage:1d0000:10000:1e0000:20000:1fe000:2000:200000:80000:4:80:1000"
enum {
Z80_FLASH_LINK_LIMIT = 0x1D0000u,
Z80_FLASH_JOURNAL_OFFSET = 0x1D0000u,
Z80_FLASH_JOURNAL_BYTES = 0x10000u,
Z80_FLASH_BOOT_OFFSET = 0x1E0000u,
Z80_FLASH_BOOT_BYTES = 0x20000u,
Z80_FLASH_ENV_OFFSET = 0x1FE000u,
Z80_FLASH_ENV_BYTES = 0x2000u,
Z80_FLASH_DISK_OFFSET = 0x200000u,
Z80_FLASH_DISK_SLOT_BYTES = 0x80000u,
Z80_FLASH_DISK_SLOT_COUNT = 4u,
Z80_FLASH_RECORD_BYTES = 128u,
Z80_FLASH_RECORD_COUNT = 4096u,
};
#endif
Shared USB/WebSocket terminal queues - src/common/terminal_queue.c
#include "z80sbc/terminal.h"
#include <string.h>
#include "pico/critical_section.h"
#include "pico/stdlib.h"
enum {
TERM_DATA_PORT = 0x00,
TERM_STATUS_PORT = 0x01,
TERM_RX_DEPTH = 128,
TERM_TX_DEPTH = 512,
TERM_STATUS_RX_READY = 1u << 0,
TERM_STATUS_TX_ROOM = 1u << 1,
TERM_STATUS_CLIENT = 1u << 7,
};
typedef struct {
uint8_t *data;
size_t capacity;
size_t head;
size_t count;
size_t peak;
} terminal_ring_t;
static uint8_t rx_data[TERM_RX_DEPTH];
static uint8_t tx_data[TERM_TX_DEPTH];
static terminal_ring_t rx_queue = {rx_data, TERM_RX_DEPTH, 0, 0, 0};
static terminal_ring_t tx_queue = {tx_data, TERM_TX_DEPTH, 0, 0, 0};
static critical_section_t terminal_lock;
static uint32_t client_connected;
static uint32_t browser_connected;
/* Let core 1 skip empty output polls without taking the core 0 I/O lock. */
static uint32_t tx_pending;
static bool usb_connected;
static uint32_t rx_drops;
static uint32_t tx_drops;
/* Counts change only under terminal_lock, but are stored atomically so the
* core-0 status poll can read them without the cross-core lock. */
static inline void ring_set_count(terminal_ring_t *ring, size_t count) {
__atomic_store_n(&ring->count, count, __ATOMIC_RELAXED);
}
static inline size_t ring_count(const terminal_ring_t *ring) {
return __atomic_load_n(&ring->count, __ATOMIC_RELAXED);
}
static void clear_queues(void) {
rx_queue.head = tx_queue.head = 0;
ring_set_count(&rx_queue, 0);
ring_set_count(&tx_queue, 0);
__atomic_store_n(&tx_pending, 0, __ATOMIC_RELEASE);
}
static void __not_in_flash_func(ring_add)(terminal_ring_t *ring, uint8_t value) {
size_t count = ring->count;
ring->data[(ring->head + count) & (ring->capacity - 1)] = value;
ring_set_count(ring, ++count);
if (count > ring->peak)
ring->peak = count;
}
static void __not_in_flash_func(ring_drop_oldest)(terminal_ring_t *ring) {
ring->head = (ring->head + 1) & (ring->capacity - 1);
ring_set_count(ring, ring->count - 1);
}
static size_t ring_peek(const terminal_ring_t *ring, uint8_t *data,
size_t capacity) {
size_t count = capacity < ring->count ? capacity : ring->count;
size_t first = ring->capacity - ring->head;
if (first > count)
first = count;
memcpy(data, ring->data + ring->head, first);
memcpy(data + first, ring->data, count - first);
return count;
}
static size_t ring_remove(terminal_ring_t *ring, uint8_t *data,
size_t capacity) {
size_t count = ring_peek(ring, data, capacity);
ring->head = (ring->head + count) & (ring->capacity - 1);
ring_set_count(ring, ring->count - count);
return count;
}
void z80_terminal_queue_init(void) {
if (!critical_section_is_initialized(&terminal_lock))
critical_section_init(&terminal_lock);
clear_queues();
rx_queue.peak = tx_queue.peak = 0;
__atomic_store_n(&client_connected, 0, __ATOMIC_RELEASE);
__atomic_store_n(&browser_connected, 0, __ATOMIC_RELEASE);
usb_connected = false;
__atomic_store_n(&rx_drops, 0, __ATOMIC_RELAXED);
__atomic_store_n(&tx_drops, 0, __ATOMIC_RELAXED);
}
uint8_t __not_in_flash_func(z80_terminal_io_read)(uint8_t port) {
if (port == TERM_STATUS_PORT) {
/* Lock-free: core 0 is the only RX consumer and TX producer, so a racing
* core 1 update can only make this poll conservative for one cycle. */
bool connected = z80_terminal_client_connected();
uint8_t value = 0;
if (ring_count(&rx_queue))
value |= TERM_STATUS_RX_READY;
if (!connected || ring_count(&tx_queue) < TERM_TX_DEPTH)
value |= TERM_STATUS_TX_ROOM;
if (connected)
value |= TERM_STATUS_CLIENT;
return value;
}
if (!ring_count(&rx_queue))
return 0;
critical_section_enter_blocking(&terminal_lock);
uint8_t value = 0;
if (rx_queue.count) {
value = rx_queue.data[rx_queue.head];
ring_drop_oldest(&rx_queue);
}
critical_section_exit(&terminal_lock);
return value;
}
void __not_in_flash_func(z80_terminal_io_write)(uint8_t port, uint8_t value) {
if (port != TERM_DATA_PORT)
return;
critical_section_enter_blocking(&terminal_lock);
if (z80_terminal_client_connected()) {
if (tx_queue.count < TERM_TX_DEPTH) {
bool was_empty = tx_queue.count == 0;
ring_add(&tx_queue, value);
if (was_empty)
__atomic_store_n(&tx_pending, 1, __ATOMIC_RELEASE);
} else
__atomic_fetch_add(&tx_drops, 1, __ATOMIC_RELAXED);
}
critical_section_exit(&terminal_lock);
}
void z80_terminal_receive(const uint8_t *data, size_t length) {
if (data == NULL)
return;
for (size_t index = 0; index < length; ++index) {
critical_section_enter_blocking(&terminal_lock);
if (rx_queue.count == TERM_RX_DEPTH) {
ring_drop_oldest(&rx_queue);
__atomic_fetch_add(&rx_drops, 1, __ATOMIC_RELAXED);
}
ring_add(&rx_queue, data[index] == '\n' ? '\r' : data[index]);
critical_section_exit(&terminal_lock);
}
}
size_t z80_terminal_peek(uint8_t *data, size_t capacity) {
if (data == NULL || capacity == 0 ||
!__atomic_load_n(&tx_pending, __ATOMIC_ACQUIRE))
return 0;
critical_section_enter_blocking(&terminal_lock);
size_t count = ring_peek(&tx_queue, data, capacity);
critical_section_exit(&terminal_lock);
return count;
}
size_t z80_terminal_supply(uint8_t *data, size_t capacity) {
if (data == NULL || capacity == 0 ||
!__atomic_load_n(&tx_pending, __ATOMIC_ACQUIRE))
return 0;
critical_section_enter_blocking(&terminal_lock);
size_t count = ring_remove(&tx_queue, data, capacity);
if (tx_queue.count == 0)
__atomic_store_n(&tx_pending, 0, __ATOMIC_RELEASE);
critical_section_exit(&terminal_lock);
return count;
}
bool z80_terminal_output_pending(void) {
return __atomic_load_n(&tx_pending, __ATOMIC_ACQUIRE) != 0;
}
bool z80_terminal_browser_try_receive(const uint8_t *data, size_t length) {
if (data == NULL || length == 0 || length > TERM_RX_DEPTH)
return false;
critical_section_enter_blocking(&terminal_lock);
bool accepted = z80_terminal_browser_connected() &&
TERM_RX_DEPTH - rx_queue.count >= length;
if (accepted)
for (size_t i = 0; i < length; ++i)
ring_add(&rx_queue, data[i] == '\n' ? '\r' : data[i]);
critical_section_exit(&terminal_lock);
return accepted;
}
void z80_terminal_browser_connect(void) {
critical_section_enter_blocking(&terminal_lock);
clear_queues();
__atomic_store_n(&browser_connected, 1, __ATOMIC_RELEASE);
__atomic_store_n(&client_connected, 1, __ATOMIC_RELEASE);
critical_section_exit(&terminal_lock);
}
void z80_terminal_set_connected(bool connected) {
critical_section_enter_blocking(&terminal_lock);
__atomic_store_n(&client_connected, connected, __ATOMIC_RELEASE);
if (!connected) {
tx_queue.head = 0;
ring_set_count(&tx_queue, 0);
__atomic_store_n(&tx_pending, 0, __ATOMIC_RELEASE);
}
critical_section_exit(&terminal_lock);
}
void z80_terminal_browser_disconnect(void) {
critical_section_enter_blocking(&terminal_lock);
if (z80_terminal_browser_connected()) {
__atomic_store_n(&browser_connected, 0, __ATOMIC_RELEASE);
__atomic_store_n(&client_connected, usb_connected, __ATOMIC_RELEASE);
clear_queues();
}
critical_section_exit(&terminal_lock);
}
bool z80_terminal_browser_connected(void) {
return __atomic_load_n(&browser_connected, __ATOMIC_ACQUIRE) != 0;
}
void z80_terminal_usb_connected(bool connected) {
critical_section_enter_blocking(&terminal_lock);
if (!z80_terminal_browser_connected()) {
__atomic_store_n(&client_connected, connected, __ATOMIC_RELEASE);
if (usb_connected != connected)
clear_queues();
}
usb_connected = connected;
critical_section_exit(&terminal_lock);
}
void z80_terminal_usb_receive(uint8_t value) {
critical_section_enter_blocking(&terminal_lock);
if (usb_connected && !z80_terminal_browser_connected()) {
if (rx_queue.count == TERM_RX_DEPTH) {
ring_drop_oldest(&rx_queue);
__atomic_fetch_add(&rx_drops, 1, __ATOMIC_RELAXED);
}
ring_add(&rx_queue, value == '\n' ? '\r' : value);
}
critical_section_exit(&terminal_lock);
}
size_t z80_terminal_usb_supply(uint8_t *data, size_t capacity) {
if (data == NULL || capacity == 0 ||
!__atomic_load_n(&tx_pending, __ATOMIC_ACQUIRE))
return 0;
critical_section_enter_blocking(&terminal_lock);
size_t count = 0;
if (usb_connected && !z80_terminal_browser_connected()) {
count = ring_remove(&tx_queue, data, capacity);
if (tx_queue.count == 0)
__atomic_store_n(&tx_pending, 0, __ATOMIC_RELEASE);
}
critical_section_exit(&terminal_lock);
return count;
}
uint32_t z80_terminal_rx_drop_count(void) {
return __atomic_load_n(&rx_drops, __ATOMIC_RELAXED);
}
uint32_t z80_terminal_tx_drop_count(void) {
return __atomic_load_n(&tx_drops, __ATOMIC_RELAXED);
}
z80_terminal_buffer_stats_t z80_terminal_get_buffer_stats(void) {
critical_section_enter_blocking(&terminal_lock);
z80_terminal_buffer_stats_t stats = {
(uint32_t)rx_queue.count, TERM_RX_DEPTH, (uint32_t)rx_queue.peak,
(uint32_t)tx_queue.count, TERM_TX_DEPTH, (uint32_t)tx_queue.peak,
};
critical_section_exit(&terminal_lock);
return stats;
}
bool __not_in_flash_func(z80_terminal_client_connected)(void) {
return __atomic_load_n(&client_connected, __ATOMIC_ACQUIRE) != 0;
}
Read-only all-record Z80 program - src/stage09_flash_storage/disk_diagnostic.asm
org 0000h
ld sp,0200h
xor a
ld (drive),a
next_drive:
ld a,(drive)
out (11h),a
ld de,0
next_record:
ld a,e
out (12h),a
ld a,d
out (13h),a
ld a,1
out (10h),a
wait_record:
in a,(10h)
bit 7,a
jr nz,failed
bit 1,a
jr z,wait_record
ld hl,0100h
ld bc,8014h
inir
ld hl,0100h
ld bc,8006h
otir
inc de
ld a,d
cp 10h
jr nz,next_record
ld a,(drive)
inc a
ld (drive),a
cp 4
jr nz,next_drive
ld a,05ah
out (05h),a
halt
failed:
ld a,0e1h
out (05h),a
halt
drive: db 0
Disk protocol and core-1 service - src/common/disk_device.c
#include "z80sbc/flash_disk.h"
#include "pico/stdlib.h"
#include "z80sbc/flash_layout.h"
#include "flash_backend.h"
enum {
DISK_COMMAND_STATUS_PORT = 0x10,
DISK_DRIVE_PORT = 0x11,
DISK_LBA_LOW_PORT = 0x12,
DISK_LBA_HIGH_PORT = 0x13,
DISK_DATA_PORT = 0x14,
DISK_COMMAND_CLEAR = 0,
DISK_COMMAND_READ = 1,
DISK_COMMAND_WRITE_NORMAL = 2,
DISK_COMMAND_WRITE_DIRECTORY = 3,
DISK_COMMAND_WRITE_UNALLOCATED = 4,
DISK_COMMAND_FLUSH = 5,
DISK_STATUS_READY = 1u << 0,
DISK_STATUS_DATA_READY = 1u << 1,
DISK_STATUS_DATA_ROOM = 1u << 2,
DISK_STATUS_BUSY = 1u << 3,
DISK_STATUS_ERROR = 1u << 7,
RECORD_WORDS = Z80_FLASH_RECORD_BYTES / 4,
PREFETCH_SLOTS = 8,
PREFETCH_KEY_VALID = 1u << 31,
};
_Static_assert((PREFETCH_SLOTS & (PREFETCH_SLOTS - 1)) == 0,
"prefetch slots are direct-mapped by LBA");
/* Single-slot core 0 -> core 1 mailbox. Core 0 stores BUSY, then publishes
* a nonzero pending command (release). While BUSY, core 0 ignores every data,
* address and command write, so core 1 owns disk_record and the write address
* until it clears pending and then publishes READY. A record therefore
* crosses cores without a copy or a queue lock. */
static uint32_t disk_pending;
/* Supervisor stop flush: separate from the Z80 command slot, so it can never
* replace or be rejected by a Z80 command still queued in the PIO FIFO. */
static uint32_t supervisor_flush;
static uint32_t disk_status = DISK_STATUS_READY;
static uint32_t disk_fatal_error;
static uint32_t disk_write_enabled = 1;
static uint8_t disk_drive;
static uint16_t disk_lba;
static uint8_t disk_write_drive;
static uint16_t disk_write_lba;
static uint8_t disk_write_command;
static uint16_t disk_data_index;
static union {
uint32_t words[RECORD_WORDS];
uint8_t bytes[Z80_FLASH_RECORD_BYTES];
} disk_record;
/* Read-ahead keeps sequential CP/M reads out of XIP flash in the trap.
* After each READ core 0 publishes a hint; core 1 copies the following
* records into direct-mapped SRAM slots. Each slot is a seqlock with core 1
* as the only writer: sequence is odd while it is rewritten. Core 0 copies
* a matching slot with word loads and accepts it only if the sequence did
* not change, otherwise it reads flash as before. Core 1 invalidates the
* slot for every record it writes before publishing READY, so a slot never
* bypasses newer data in the backend's write sector. */
typedef struct {
uint32_t sequence;
uint32_t key;
uint32_t words[RECORD_WORDS];
} prefetch_slot_t;
static prefetch_slot_t prefetch[PREFETCH_SLOTS];
static uint32_t prefetch_hint;
/* Core 1: the hint whose window is already filled, so idle passes skip it. */
static uint32_t prefetch_filled;
static uint32_t prefetch_hits;
static uint32_t prefetch_misses;
static inline uint32_t record_key(unsigned int drive, uint16_t lba) {
return PREFETCH_KEY_VALID | (uint32_t)drive << 16 | lba;
}
static bool __not_in_flash_func(prefetch_take)(uint32_t key, uint16_t lba) {
prefetch_slot_t *slot = &prefetch[lba & (PREFETCH_SLOTS - 1)];
uint32_t sequence = __atomic_load_n(&slot->sequence, __ATOMIC_ACQUIRE);
if ((sequence & 1u) ||
__atomic_load_n(&slot->key, __ATOMIC_RELAXED) != key)
return false;
for (unsigned int index = 0; index < RECORD_WORDS; ++index)
disk_record.words[index] =
__atomic_load_n(&slot->words[index], __ATOMIC_RELAXED);
__atomic_thread_fence(__ATOMIC_ACQUIRE);
return __atomic_load_n(&slot->sequence, __ATOMIC_RELAXED) == sequence;
}
static void prefetch_publish(prefetch_slot_t *slot, uint32_t key,
const uint32_t *words) {
uint32_t sequence = __atomic_load_n(&slot->sequence, __ATOMIC_RELAXED);
__atomic_store_n(&slot->sequence, sequence + 1, __ATOMIC_RELAXED);
__atomic_thread_fence(__ATOMIC_RELEASE);
__atomic_store_n(&slot->key, key, __ATOMIC_RELAXED);
if (words != NULL)
for (unsigned int index = 0; index < RECORD_WORDS; ++index)
__atomic_store_n(&slot->words[index], words[index], __ATOMIC_RELAXED);
__atomic_store_n(&slot->sequence, sequence + 2, __ATOMIC_RELEASE);
}
static void prefetch_invalidate(unsigned int drive, uint16_t lba) {
prefetch_slot_t *slot = &prefetch[lba & (PREFETCH_SLOTS - 1)];
if (__atomic_load_n(&slot->key, __ATOMIC_RELAXED) == record_key(drive, lba)) {
prefetch_publish(slot, 0, NULL);
prefetch_filled = 0;
}
}
/* Bounded: at most PREFETCH_SLOTS record copies per call, usually one. */
static void prefetch_service(void) {
uint32_t hint = __atomic_load_n(&prefetch_hint, __ATOMIC_ACQUIRE);
if (hint == 0 || hint == prefetch_filled)
return;
unsigned int drive = (hint >> 16) & 0xFFu;
uint32_t lba = hint & 0xFFFFu;
for (uint32_t ahead = 1; ahead <= PREFETCH_SLOTS; ++ahead) {
uint32_t next = lba + ahead;
if (next >= Z80_FLASH_RECORD_COUNT)
break;
uint32_t key = record_key(drive, (uint16_t)next);
prefetch_slot_t *slot = &prefetch[next & (PREFETCH_SLOTS - 1)];
if (__atomic_load_n(&slot->key, __ATOMIC_RELAXED) == key)
continue;
uint32_t words[RECORD_WORDS];
z80_flash_backend_read_record(drive, (uint16_t)next, (uint8_t *)words);
prefetch_publish(slot, key, words);
}
prefetch_filled = hint;
}
static uint32_t __not_in_flash_func(disk_status_load)(void) {
return __atomic_load_n(&disk_status, __ATOMIC_ACQUIRE);
}
static void __not_in_flash_func(disk_status_store)(uint32_t value) {
__atomic_store_n(&disk_status, value, __ATOMIC_RELEASE);
}
static void __not_in_flash_func(disk_submit)(uint8_t command) {
disk_status_store(DISK_STATUS_BUSY);
__atomic_store_n(&disk_pending, command, __ATOMIC_RELEASE);
}
static void __not_in_flash_func(disk_start_command)(uint8_t command) {
if (disk_status_load() & DISK_STATUS_BUSY)
return;
bool fatal =
__atomic_load_n(&disk_fatal_error, __ATOMIC_ACQUIRE) != 0;
if (command == DISK_COMMAND_CLEAR) {
disk_data_index = 0;
disk_status_store(DISK_STATUS_READY |
(fatal ? DISK_STATUS_ERROR : 0));
return;
}
if (fatal) {
disk_status_store(DISK_STATUS_READY | DISK_STATUS_ERROR);
return;
}
if (command == DISK_COMMAND_FLUSH) {
disk_submit(command);
return;
}
if (disk_drive >= Z80_FLASH_DISK_SLOT_COUNT ||
disk_lba >= Z80_FLASH_RECORD_COUNT) {
disk_status_store(DISK_STATUS_READY | DISK_STATUS_ERROR);
return;
}
disk_data_index = 0;
if (command == DISK_COMMAND_READ) {
uint32_t key = record_key(disk_drive, disk_lba);
if (prefetch_take(key, disk_lba)) {
__atomic_store_n(&prefetch_hits, prefetch_hits + 1, __ATOMIC_RELAXED);
} else {
z80_flash_backend_read_record(disk_drive, disk_lba, disk_record.bytes);
__atomic_store_n(&prefetch_misses, prefetch_misses + 1,
__ATOMIC_RELAXED);
}
__atomic_store_n(&prefetch_hint, key, __ATOMIC_RELEASE);
disk_status_store(DISK_STATUS_READY | DISK_STATUS_DATA_READY);
} else if (command >= DISK_COMMAND_WRITE_NORMAL &&
command <= DISK_COMMAND_WRITE_UNALLOCATED) {
disk_write_drive = disk_drive;
disk_write_lba = disk_lba;
disk_write_command = command;
disk_status_store(DISK_STATUS_READY | DISK_STATUS_DATA_ROOM);
} else {
disk_status_store(DISK_STATUS_READY | DISK_STATUS_ERROR);
}
}
uint8_t __not_in_flash_func(z80_flash_disk_io_read)(uint8_t port) {
if (port == DISK_COMMAND_STATUS_PORT)
return __atomic_load_n(&disk_write_enabled, __ATOMIC_ACQUIRE)
? (uint8_t)disk_status_load()
: DISK_STATUS_BUSY | DISK_STATUS_ERROR;
if (port != DISK_DATA_PORT ||
!(disk_status_load() & DISK_STATUS_DATA_READY))
return 0;
uint8_t value = disk_record.bytes[disk_data_index++];
if (disk_data_index == sizeof(disk_record.bytes))
disk_status_store(DISK_STATUS_READY);
return value;
}
void __not_in_flash_func(z80_flash_disk_io_write)(uint8_t port, uint8_t value) {
if (!__atomic_load_n(&disk_write_enabled, __ATOMIC_ACQUIRE))
return;
uint32_t status = disk_status_load();
if (port == DISK_COMMAND_STATUS_PORT) {
disk_start_command(value);
} else if (status & DISK_STATUS_BUSY) {
return;
} else if (port == DISK_DRIVE_PORT) {
disk_drive = value;
} else if (port == DISK_LBA_LOW_PORT) {
disk_lba = (uint16_t)((disk_lba & 0xFF00u) | value);
} else if (port == DISK_LBA_HIGH_PORT) {
disk_lba = (uint16_t)((disk_lba & 0x00FFu) |
((uint16_t)value << 8));
} else if (port == DISK_DATA_PORT &&
(status & DISK_STATUS_DATA_ROOM)) {
disk_record.bytes[disk_data_index++] = value;
if (disk_data_index == sizeof(disk_record.bytes))
disk_submit(disk_write_command);
}
}
void z80_flash_core1_service(void) {
if (__atomic_load_n(&disk_fatal_error, __ATOMIC_ACQUIRE))
return;
uint32_t command = __atomic_load_n(&disk_pending, __ATOMIC_ACQUIRE);
if (command != 0) {
bool success;
if (command == DISK_COMMAND_FLUSH) {
success = z80_flash_backend_flush();
} else {
prefetch_invalidate(disk_write_drive, disk_write_lba);
success = z80_flash_backend_write_record(
disk_write_drive, disk_write_lba, disk_record.bytes,
sizeof(disk_record.bytes),
(uint8_t)(command - DISK_COMMAND_WRITE_NORMAL));
}
if (!success)
__atomic_store_n(&disk_fatal_error, 1, __ATOMIC_RELEASE);
__atomic_store_n(&disk_pending, 0, __ATOMIC_RELAXED);
disk_status_store(DISK_STATUS_READY |
(success ? 0 : DISK_STATUS_ERROR));
if (!success)
return;
} else if (__atomic_load_n(&supervisor_flush, __ATOMIC_ACQUIRE)) {
bool success = z80_flash_backend_flush();
__atomic_store_n(&supervisor_flush, 0, __ATOMIC_RELEASE);
if (!success) {
__atomic_store_n(&disk_fatal_error, 1, __ATOMIC_RELEASE);
disk_status_store(DISK_STATUS_READY | DISK_STATUS_ERROR);
return;
}
} else if (z80_flash_backend_flush_due() && !z80_flash_backend_flush()) {
__atomic_store_n(&disk_fatal_error, 1, __ATOMIC_RELEASE);
disk_status_store(DISK_STATUS_READY | DISK_STATUS_ERROR);
return;
}
prefetch_service();
}
bool z80_flash_storage_init(void) {
return z80_flash_storage_init_observed(NULL, NULL);
}
bool z80_flash_storage_init_observed(z80_boot_observer_t observer,
void *context) {
__atomic_store_n(&disk_pending, 0, __ATOMIC_RELEASE);
__atomic_store_n(&supervisor_flush, 0, __ATOMIC_RELEASE);
__atomic_store_n(&prefetch_hint, 0, __ATOMIC_RELEASE);
prefetch_filled = 0;
for (unsigned int slot = 0; slot < PREFETCH_SLOTS; ++slot)
prefetch_publish(&prefetch[slot], 0, NULL);
__atomic_store_n(&prefetch_hits, 0, __ATOMIC_RELAXED);
__atomic_store_n(&prefetch_misses, 0, __ATOMIC_RELAXED);
__atomic_store_n(&disk_fatal_error, 0, __ATOMIC_RELEASE);
__atomic_store_n(&disk_write_enabled, 1, __ATOMIC_RELEASE);
disk_status_store(DISK_STATUS_READY);
return z80_flash_backend_init_observed(observer, context);
}
void z80_flash_core0_service(void) {
z80_flash_backend_core0_service();
}
uint32_t z80_flash_disk_status(void) {
return disk_status_load();
}
void z80_flash_disk_prefetch_counts(uint32_t *hits, uint32_t *misses) {
*hits = __atomic_load_n(&prefetch_hits, __ATOMIC_RELAXED);
*misses = __atomic_load_n(&prefetch_misses, __ATOMIC_RELAXED);
}
bool z80_flash_disk_has_fatal_error(void) {
return __atomic_load_n(&disk_fatal_error, __ATOMIC_ACQUIRE) != 0;
}
bool z80_flash_disk_quiescent(void) {
return !__atomic_load_n(&supervisor_flush, __ATOMIC_ACQUIRE) &&
disk_status_load() == DISK_STATUS_READY &&
z80_flash_backend_quiescent();
}
void z80_flash_disk_request_flush(void) {
__atomic_store_n(&supervisor_flush, 1, __ATOMIC_RELEASE);
}
void z80_flash_disk_set_write_enabled(bool enabled) {
__atomic_store_n(&disk_write_enabled, enabled, __ATOMIC_RELEASE);
}
void z80_flash_disk_arm_fault(z80_flash_fault_point_t point) {
z80_flash_backend_arm_fault(point);
}
Boot loading, cache, journal and bus quiescence - src/common/flash_backend.c
#include "flash_backend.h"
#include <stddef.h>
#include <string.h>
#include "hardware/flash.h"
#include "hardware/watchdog.h"
#include "pico/flash.h"
#include "pico/stdlib.h"
#include "pico/util/queue.h"
#include "z80sbc/clock.h"
#include "z80sbc/banked_transfer.h"
#include "z80sbc/cpu.h"
#include "z80sbc/flash_layout.h"
#include "z80sbc/io_trap.h"
#include "z80sbc/pins.h"
#include "z80sbc/sram.h"
#include "z80sbc/supervisor.h"
enum {
FLASH_JOURNAL_MAGIC = 0x314C4E4Au,
FLASH_JOURNAL_PAIR_BYTES = 2u * FLASH_SECTOR_SIZE,
FLASH_JOURNAL_PAIR_COUNT =
Z80_FLASH_JOURNAL_BYTES / FLASH_JOURNAL_PAIR_BYTES,
FLASH_BOOT_PAYLOAD_OFFSET = Z80_FLASH_BOOT_OFFSET + FLASH_SECTOR_SIZE,
DISK_CACHE_IDLE_FLUSH_MS = 250,
Z80_BOOT_MAGIC = 0x5442385Au,
Z80_BOOT_VERSION = 1u,
Z80_BOOT_IMAGE_BYTES = 65536u,
BOOT_PROGRESS_BYTES = 256u,
FLASH_SERVICE_ACQUIRE_BUS = 0,
FLASH_SERVICE_RELEASE_BUS,
};
_Static_assert(PICO_FLASH_SIZE_BYTES == 4u * 1024u * 1024u,
"Pico 2 W physical flash size changed");
_Static_assert(Z80_FLASH_JOURNAL_OFFSET == Z80_FLASH_LINK_LIMIT,
"journal must start at the firmware link boundary");
_Static_assert(Z80_FLASH_DISK_SLOT_BYTES ==
Z80_FLASH_RECORD_BYTES * Z80_FLASH_RECORD_COUNT,
"disk geometry does not fill its slot");
_Static_assert(FLASH_BOOT_PAYLOAD_OFFSET + 65536u <=
Z80_FLASH_ENV_OFFSET,
"boot payload exceeds its region");
_Static_assert(Z80_FLASH_ENV_OFFSET + Z80_FLASH_ENV_BYTES ==
Z80_FLASH_DISK_OFFSET,
"environment must end before disk slots");
_Static_assert(Z80_FLASH_DISK_OFFSET +
Z80_FLASH_DISK_SLOT_COUNT * Z80_FLASH_DISK_SLOT_BYTES ==
PICO_FLASH_SIZE_BYTES,
"disk slots must end at physical flash boundary");
_Static_assert(FLASH_JOURNAL_PAIR_COUNT == 8,
"journal layout changed unexpectedly");
_Static_assert(!PICO_FLASH_ASSUME_CORE1_SAFE,
"USB core must participate in flash lockout during recovery");
typedef struct {
uint32_t magic;
uint16_t version;
uint16_t header_bytes;
uint32_t image_bytes;
uint32_t image_crc32;
uint32_t header_crc32;
} boot_manifest_t;
typedef struct {
uint32_t magic;
uint32_t sequence;
uint32_t target_offset;
uint32_t data_crc32;
uint32_t header_crc32;
uint8_t reserved[FLASH_PAGE_SIZE - 20u];
} journal_header_t;
typedef struct {
uint32_t header_offset;
uint32_t data_offset;
uint32_t target_offset;
uint8_t *data;
journal_header_t header;
bool committed;
} write_params_t;
typedef struct {
uint32_t header_offset;
uint32_t target_offset;
uint8_t *data;
bool restored;
} restore_params_t;
typedef struct {
uint8_t operation;
} service_request_t;
typedef struct {
bool valid;
bool dirty;
unsigned int drive;
uint32_t block_offset;
uint8_t data[FLASH_SECTOR_SIZE];
} disk_cache_t;
_Static_assert(sizeof(boot_manifest_t) == 20,
"boot manifest must match host packer");
_Static_assert(sizeof(journal_header_t) == FLASH_PAGE_SIZE,
"journal header must occupy one flash page");
static queue_t service_request_queue;
static queue_t service_result_queue;
static disk_cache_t disk_cache;
static uint32_t journal_sequence;
static unsigned int next_journal_pair;
static absolute_time_t disk_cache_flush_deadline;
static uint32_t armed_fault;
static z80_boot_status_t boot_status;
const z80_boot_status_t *z80_flash_boot_status(void) {
return &boot_status;
}
static bool boot_failed(z80_boot_error_t error) {
boot_status.error = error;
z80_cpu_fail_closed();
return false;
}
static bool boot_progress(z80_boot_phase_t phase, uint32_t completed,
z80_boot_observer_t observer, void *context) {
boot_status.phase = phase;
boot_status.completed = completed;
if (observer != NULL && !observer(&boot_status, context))
return boot_failed(Z80_BOOT_ERROR_CANCELLED);
return true;
}
static uint32_t crc32_bytes(const uint8_t *data, size_t length) {
uint32_t crc = UINT32_MAX;
for (size_t index = 0; index < length; ++index) {
crc ^= data[index];
for (unsigned int bit = 0; bit < 8; ++bit)
crc = (crc >> 1) ^
(0xEDB88320u & (uint32_t)-(int32_t)(crc & 1u));
}
return ~crc;
}
static bool disk_block_offset_valid(uint32_t offset) {
uint32_t disk_end = Z80_FLASH_DISK_OFFSET +
Z80_FLASH_DISK_SLOT_COUNT * Z80_FLASH_DISK_SLOT_BYTES;
return offset >= Z80_FLASH_DISK_OFFSET &&
offset <= disk_end - FLASH_SECTOR_SIZE &&
(offset & (FLASH_SECTOR_SIZE - 1u)) == 0;
}
static _Noreturn void reboot_after_flash_failure(void) {
gpio_put(PIN_RESET_N, 0);
z80_isolate_buses();
z80_clock_stop();
watchdog_reboot(0, 0, 0);
while (true)
tight_loop_contents();
}
static bool consume_fault(z80_flash_fault_point_t point) {
uint32_t expected = (uint32_t)point;
return __atomic_compare_exchange_n(&armed_fault, &expected,
Z80_FLASH_FAULT_NONE, false,
__ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE);
}
static void inject_power_cut(z80_flash_fault_point_t point) {
if (consume_fault(point))
reboot_after_flash_failure();
}
static void write_callback(void *parameter) {
write_params_t *params = parameter;
flash_range_erase(params->header_offset, FLASH_JOURNAL_PAIR_BYTES);
flash_range_program(params->data_offset, params->data, FLASH_SECTOR_SIZE);
inject_power_cut(Z80_FLASH_FAULT_AFTER_JOURNAL_DATA);
flash_range_program(params->header_offset,
(const uint8_t *)¶ms->header, FLASH_PAGE_SIZE);
inject_power_cut(Z80_FLASH_FAULT_AFTER_JOURNAL_HEADER);
if (memcmp((const void *)(XIP_BASE + params->data_offset), params->data,
FLASH_SECTOR_SIZE) != 0 ||
memcmp((const void *)(XIP_BASE + params->header_offset),
¶ms->header, sizeof(params->header)) != 0)
return;
flash_range_erase(params->target_offset, FLASH_SECTOR_SIZE);
inject_power_cut(Z80_FLASH_FAULT_AFTER_TARGET_ERASE);
flash_range_program(params->target_offset, params->data,
FLASH_SECTOR_SIZE / 2u);
inject_power_cut(Z80_FLASH_FAULT_AFTER_PARTIAL_TARGET);
flash_range_program(params->target_offset + FLASH_SECTOR_SIZE / 2u,
params->data + FLASH_SECTOR_SIZE / 2u,
FLASH_SECTOR_SIZE / 2u);
params->committed =
memcmp((const void *)(XIP_BASE + params->target_offset), params->data,
FLASH_SECTOR_SIZE) == 0;
if (params->committed) {
inject_power_cut(Z80_FLASH_FAULT_AFTER_TARGET_VERIFY);
flash_range_erase(params->header_offset, FLASH_SECTOR_SIZE);
params->committed =
*(const uint32_t *)(XIP_BASE + params->header_offset) == UINT32_MAX;
if (params->committed)
inject_power_cut(Z80_FLASH_FAULT_AFTER_HEADER_CLEAR);
}
}
static void restore_callback(void *parameter) {
restore_params_t *params = parameter;
flash_range_erase(params->target_offset, FLASH_SECTOR_SIZE);
flash_range_program(params->target_offset, params->data, FLASH_SECTOR_SIZE);
params->restored =
memcmp((const void *)(XIP_BASE + params->target_offset), params->data,
FLASH_SECTOR_SIZE) == 0;
if (params->restored) {
flash_range_erase(params->header_offset, FLASH_SECTOR_SIZE);
params->restored =
*(const uint32_t *)(XIP_BASE + params->header_offset) == UINT32_MAX;
}
}
static bool recover_journal(void) {
static journal_header_t headers[FLASH_JOURNAL_PAIR_COUNT];
static bool valid[FLASH_JOURNAL_PAIR_COUNT];
static uint8_t recovery_block[FLASH_SECTOR_SIZE];
for (unsigned int pair = 0; pair < FLASH_JOURNAL_PAIR_COUNT; ++pair) {
uint32_t header_offset = Z80_FLASH_JOURNAL_OFFSET +
pair * FLASH_JOURNAL_PAIR_BYTES;
memcpy(&headers[pair], (const void *)(XIP_BASE + header_offset),
sizeof(headers[pair]));
valid[pair] =
headers[pair].magic == FLASH_JOURNAL_MAGIC &&
headers[pair].header_crc32 ==
crc32_bytes((const uint8_t *)&headers[pair],
offsetof(journal_header_t, header_crc32)) &&
disk_block_offset_valid(headers[pair].target_offset);
}
for (unsigned int recovered = 0; recovered < FLASH_JOURNAL_PAIR_COUNT;
++recovered) {
int selected = -1;
for (unsigned int pair = 0; pair < FLASH_JOURNAL_PAIR_COUNT; ++pair) {
if (valid[pair] &&
(selected < 0 || headers[pair].sequence <
headers[(unsigned int)selected].sequence))
selected = (int)pair;
}
if (selected < 0)
break;
unsigned int pair = (unsigned int)selected;
uint32_t header_offset = Z80_FLASH_JOURNAL_OFFSET +
pair * FLASH_JOURNAL_PAIR_BYTES;
uint32_t data_offset = header_offset + FLASH_SECTOR_SIZE;
memcpy(recovery_block, (const void *)(XIP_BASE + data_offset),
sizeof(recovery_block));
if (crc32_bytes(recovery_block, sizeof(recovery_block)) !=
headers[pair].data_crc32)
return false;
restore_params_t params = {
header_offset, headers[pair].target_offset, recovery_block, false,
};
int result = flash_safe_execute(restore_callback, ¶ms, 1000);
if (result != PICO_OK || !params.restored)
return false;
valid[pair] = false;
}
return true;
}
static bool load_boot_image(z80_boot_observer_t observer, void *context) {
const boot_manifest_t *manifest =
(const boot_manifest_t *)(XIP_BASE + Z80_FLASH_BOOT_OFFSET);
const uint8_t *source =
(const uint8_t *)(XIP_BASE + FLASH_BOOT_PAYLOAD_OFFSET);
if (!boot_progress(Z80_BOOT_MANIFEST, 0, observer, context))
return false;
if (manifest->magic != Z80_BOOT_MAGIC ||
manifest->version != Z80_BOOT_VERSION ||
manifest->header_bytes != sizeof(*manifest) ||
manifest->image_bytes != Z80_BOOT_IMAGE_BYTES ||
crc32_bytes((const uint8_t *)manifest,
offsetof(boot_manifest_t, header_crc32)) !=
manifest->header_crc32)
return boot_failed(Z80_BOOT_ERROR_MANIFEST);
if (crc32_bytes(source, manifest->image_bytes) != manifest->image_crc32)
return boot_failed(Z80_BOOT_ERROR_PAYLOAD_CRC);
if (source[0] != 0xC3 || source[1] != 0x00 || source[2] != 0xFC)
return boot_failed(Z80_BOOT_ERROR_MANIFEST);
if (!z80_cpu_prepare_loader())
return boot_failed(Z80_BOOT_ERROR_LOADER);
for (uint32_t address = 0; address < Z80_BOOT_IMAGE_BYTES; ++address) {
if (address % BOOT_PROGRESS_BYTES == 0 &&
!boot_progress(Z80_BOOT_LOAD, address, observer, context))
return false;
boot_status.completed = address;
boot_status.address = (uint16_t)address;
boot_status.expected = source[address];
if (!z80_sram_write_byte((uint16_t)address, source[address]))
return boot_failed(Z80_BOOT_ERROR_WRITE);
}
if (!boot_progress(Z80_BOOT_LOAD, Z80_BOOT_IMAGE_BYTES, observer, context))
return false;
for (uint32_t address = 0; address < Z80_BOOT_IMAGE_BYTES; ++address) {
if (address % BOOT_PROGRESS_BYTES == 0 &&
!boot_progress(Z80_BOOT_VERIFY, address, observer, context))
return false;
boot_status.completed = address;
boot_status.address = (uint16_t)address;
boot_status.expected = source[address];
if (!z80_sram_read_byte((uint16_t)address, &boot_status.actual))
return boot_failed(Z80_BOOT_ERROR_READ);
if (boot_status.actual != source[address])
return boot_failed(Z80_BOOT_ERROR_MISMATCH);
}
if (!boot_progress(Z80_BOOT_VERIFY, Z80_BOOT_IMAGE_BYTES, observer, context))
return false;
z80_sram_prepare_banked_memory(source);
/* The isolation probe borrows bank 0's reset-vector byte. Confirm it was
* restored before accepting the already-verified base image. */
boot_status.address = 0;
boot_status.expected = source[0];
if (!z80_sram_read_byte(0, &boot_status.actual))
return boot_failed(Z80_BOOT_ERROR_READ);
if (boot_status.actual != source[0])
return boot_failed(Z80_BOOT_ERROR_MISMATCH);
z80_cpu_fail_closed();
return true;
}
static bool request_core0(uint8_t operation) {
service_request_t request = {operation};
queue_add_blocking(&service_request_queue, &request);
bool result = false;
queue_remove_blocking(&service_result_queue, &result);
return result;
}
void z80_flash_backend_core0_service(void) {
z80_io_trap_poll();
/* Peek without the queue lock, which would mask the PIO1 IRQ on every pass.
* Core 0 is the only consumer, so a racing add is seen on the next pass. */
service_request_t request;
if (queue_get_level_unsafe(&service_request_queue) == 0 ||
!queue_try_remove(&service_request_queue, &request))
return;
bool result = false;
if (request.operation == FLASH_SERVICE_ACQUIRE_BUS) {
result = z80_cpu_request_bus(500000);
if (result)
z80_io_trap_disable();
} else if (request.operation == FLASH_SERVICE_RELEASE_BUS) {
result = z80_io_trap_rearm() && z80_cpu_release_bus(500000);
if (!result)
z80_io_trap_disable();
}
if (!queue_try_add(&service_result_queue, &result))
reboot_after_flash_failure();
}
static bool flush_disk_cache(void) {
if (!disk_cache.dirty)
return true;
uint32_t flash_offset = Z80_FLASH_DISK_OFFSET +
disk_cache.drive * Z80_FLASH_DISK_SLOT_BYTES +
disk_cache.block_offset;
if (!request_core0(FLASH_SERVICE_ACQUIRE_BUS))
return false;
unsigned int pair =
next_journal_pair++ % FLASH_JOURNAL_PAIR_COUNT;
uint32_t header_offset = Z80_FLASH_JOURNAL_OFFSET +
pair * FLASH_JOURNAL_PAIR_BYTES;
write_params_t params;
memset(¶ms, 0, sizeof(params));
params.header_offset = header_offset;
params.data_offset = header_offset + FLASH_SECTOR_SIZE;
params.target_offset = flash_offset;
params.data = disk_cache.data;
memset(¶ms.header, 0xFF, sizeof(params.header));
params.header.magic = FLASH_JOURNAL_MAGIC;
params.header.sequence = ++journal_sequence;
params.header.target_offset = flash_offset;
params.header.data_crc32 =
crc32_bytes(disk_cache.data, sizeof(disk_cache.data));
params.header.header_crc32 =
crc32_bytes((const uint8_t *)¶ms.header,
offsetof(journal_header_t, header_crc32));
int result = consume_fault(Z80_FLASH_FAULT_SAFE_EXECUTE_ENTRY)
? PICO_ERROR_TIMEOUT
: flash_safe_execute(write_callback, ¶ms, 1000);
if (consume_fault(Z80_FLASH_FAULT_SAFE_EXECUTE_EXIT))
result = PICO_ERROR_TIMEOUT;
if (result != PICO_OK)
reboot_after_flash_failure();
if (params.committed)
disk_cache.dirty = false;
bool released = request_core0(FLASH_SERVICE_RELEASE_BUS);
return released && params.committed;
}
typedef struct {
uint32_t offset;
const uint8_t *sector;
bool committed;
} env_write_params_t;
static void env_write_callback(void *parameter) {
env_write_params_t *params = parameter;
flash_range_erase(params->offset, FLASH_SECTOR_SIZE);
flash_range_program(params->offset + FLASH_PAGE_SIZE,
params->sector + FLASH_PAGE_SIZE,
FLASH_SECTOR_SIZE - FLASH_PAGE_SIZE);
if (memcmp((const void *)(XIP_BASE + params->offset + FLASH_PAGE_SIZE),
params->sector + FLASH_PAGE_SIZE,
FLASH_SECTOR_SIZE - FLASH_PAGE_SIZE) != 0)
return;
/* Publish the CRC-protected header only after the payload is verified. */
flash_range_program(params->offset, params->sector, FLASH_PAGE_SIZE);
params->committed =
memcmp((const void *)(XIP_BASE + params->offset), params->sector,
FLASH_SECTOR_SIZE) == 0;
}
bool z80_flash_backend_env_write(unsigned slot, const uint8_t *sector) {
if (slot >= 2 || sector == NULL)
return false;
if (!request_core0(FLASH_SERVICE_ACQUIRE_BUS))
return false;
env_write_params_t params = {
.offset = Z80_FLASH_ENV_OFFSET + slot * FLASH_SECTOR_SIZE,
.sector = sector,
};
if (flash_safe_execute(env_write_callback, ¶ms, 1000) != PICO_OK)
reboot_after_flash_failure();
bool released = request_core0(FLASH_SERVICE_RELEASE_BUS);
return released && params.committed;
}
static bool select_disk_cache(unsigned int drive, uint16_t lba) {
uint32_t sector_offset = (uint32_t)lba * Z80_FLASH_RECORD_BYTES;
uint32_t block_offset =
sector_offset & ~(uint32_t)(FLASH_SECTOR_SIZE - 1u);
if (disk_cache.valid && disk_cache.drive == drive &&
disk_cache.block_offset == block_offset)
return true;
if (!flush_disk_cache())
return false;
uint32_t flash_offset = Z80_FLASH_DISK_OFFSET +
drive * Z80_FLASH_DISK_SLOT_BYTES + block_offset;
memcpy(disk_cache.data, (const void *)(XIP_BASE + flash_offset),
sizeof(disk_cache.data));
disk_cache.drive = drive;
disk_cache.block_offset = block_offset;
disk_cache.valid = true;
return true;
}
/* Runs in the core-0 trap. newlib memcpy lives in XIP flash, so copy words
* here and stop GCC from turning the loop back into a memcpy call. */
#if defined(__GNUC__) && !defined(__clang__)
#define NO_MEMCPY_PATTERN __attribute__((optimize("no-tree-loop-distribute-patterns")))
#else
#define NO_MEMCPY_PATTERN
#endif
void NO_MEMCPY_PATTERN
__not_in_flash_func(z80_flash_backend_read_record)(
unsigned int drive, uint16_t lba, uint8_t *data) {
uint32_t sector_offset = (uint32_t)lba * Z80_FLASH_RECORD_BYTES;
uint32_t block_offset =
sector_offset & ~(uint32_t)(FLASH_SECTOR_SIZE - 1u);
const uint8_t *source =
disk_cache.valid && disk_cache.drive == drive &&
disk_cache.block_offset == block_offset
? disk_cache.data + (sector_offset - block_offset)
: (const uint8_t *)(XIP_BASE + Z80_FLASH_DISK_OFFSET +
drive * Z80_FLASH_DISK_SLOT_BYTES +
sector_offset);
for (unsigned int index = 0; index < Z80_FLASH_RECORD_BYTES; index += 4) {
uint32_t word;
__builtin_memcpy(&word, source + index, sizeof(word));
__builtin_memcpy(data + index, &word, sizeof(word));
}
}
bool z80_flash_backend_write_record(unsigned int drive, uint16_t lba,
const uint8_t *data, size_t length,
uint8_t write_type) {
if (drive >= Z80_FLASH_DISK_SLOT_COUNT ||
lba >= Z80_FLASH_RECORD_COUNT || data == NULL ||
length != Z80_FLASH_RECORD_BYTES || write_type > 2u ||
!select_disk_cache(drive, lba))
return false;
uint32_t within_block =
((uint32_t)lba * Z80_FLASH_RECORD_BYTES) &
(FLASH_SECTOR_SIZE - 1u);
if (memcmp(disk_cache.data + within_block, data, length) != 0) {
memcpy(disk_cache.data + within_block, data, length);
disk_cache.dirty = true;
disk_cache_flush_deadline = make_timeout_time_ms(DISK_CACHE_IDLE_FLUSH_MS);
}
return write_type == 1u ? flush_disk_cache() : true;
}
bool z80_flash_backend_flush(void) {
return flush_disk_cache();
}
bool z80_flash_backend_flush_due(void) {
return disk_cache.dirty && time_reached(disk_cache_flush_deadline);
}
bool z80_flash_backend_quiescent(void) {
return !disk_cache.dirty;
}
bool z80_flash_backend_init(void) {
return z80_flash_backend_init_observed(NULL, NULL);
}
bool z80_flash_backend_init_observed(z80_boot_observer_t observer,
void *context) {
z80_cpu_fail_closed();
memset(&boot_status, 0, sizeof(boot_status));
boot_status.total = Z80_BOOT_IMAGE_BYTES;
queue_init(&service_request_queue, sizeof(service_request_t), 4);
queue_init(&service_result_queue, sizeof(bool), 4);
if (!boot_progress(Z80_BOOT_RECOVERY, 0, observer, context))
return false;
if (!recover_journal())
return boot_failed(Z80_BOOT_ERROR_JOURNAL);
if (!load_boot_image(observer, context))
return false;
memset(&disk_cache, 0, sizeof(disk_cache));
journal_sequence = 0;
next_journal_pair = 0;
disk_cache_flush_deadline = nil_time;
__atomic_store_n(&armed_fault, Z80_FLASH_FAULT_NONE, __ATOMIC_RELEASE);
if (!flash_safe_execute_core_init())
return boot_failed(Z80_BOOT_ERROR_FLASH_INIT);
return boot_progress(Z80_BOOT_VERIFIED, Z80_BOOT_IMAGE_BYTES,
observer, context);
}
void z80_flash_backend_arm_fault(z80_flash_fault_point_t point) {
__atomic_store_n(&armed_fault, (uint32_t)point, __ATOMIC_RELEASE);
}
The Z80 BIOS sets drive and 16-bit LBA, then writes command 1 for a read.
Write commands 2, 3, and 4 mean normal, directory, and first record of a newly
allocated CP/M block respectively; command 5 explicitly flushes the cache.
A read returns 128 bytes from the data port. A write accepts exactly 128 bytes
there, then changes status to BUSY until core 1 has cached it and completed
any required journaled flush. Command 0 clears a transient protocol/queue
error when not busy; a flash or journal failure remains latched until reboot
and recovery, disabling both queued writes and background flush retries.
BUSY is published before enqueueing so immediate core-1 completion cannot
be overwritten by a late BUSY store. The BIOS polls READY/DATA_READY/DATA_ROOM/BUSY instead of
assuming Pico timing, but reads each status only once per poll and uses Z80
INIR/OTIR for payload transfer. It also waits for READY before writing a
command's drive/LBA registers. Idle flushes are serialized separately by
holding BUSACK# from before the flash operation until dirty state is cleared.
No erase, program, blocking queue, or flash-safe call runs inside
io_trap_handler().
Native CP/M auto-run¶
The native image builder assembles
autorun.asm
and replaces the Altair-only AUTORUN.COM in user 0 of generated drive A.
Build images with the firmware build tools.
Install a matching boot package and native drive A image for this feature; a
utility-only copy does not update the BIOS.
Run AUTORUN, choose 1 for cold-only, 2 for warm-only, 3 for both, or
4 to disable, and enter a printable command of up to 80 characters, such
as C:DATE. RETURN keeps the current value; Q at the option prompt quits
without saving. The configuration always belongs to drive A, even if the
utility is invoked from another drive.
Settings occupy only drive A's last reserved system record: zero-based
track 1, sector 31, LBA 63, image offset 0x1F80. The 128-byte ZAR1 record
contains a boot-mode byte, command length/text, and additive checksum. It
does not overlap boot code or CP/M files. Freshly generated drive A images
initialize it disabled, so whole-image replacement resets the setting.
The utility requests a directory-type BIOS write (C=1) for journaled
durability and verifies all 128 bytes by reading them back before reporting
success. The BIOS validates the record on cold and warm boot, reports
configuration errors rather than executing invalid data, and copies an
applicable command into the CCP input buffer. Cold-only commands do not
repeat after a transient warm-boots. Warm modes intentionally execute on
every actual BIOS warm boot.
Recovery: a pending console key bypasses an applicable auto-run command
for that boot. The key is consumed; after reaching the prompt, run
AUTORUN and select 4 to disable a repeating command. Disabled and
inapplicable modes leave pending console input alone.
The directory buffer uses the optimized BDOS padding at FC80h-FCFFh;
the builder guards against overlap, and the TPA remains unchanged. The
independent host-core tests execute the real utility and BIOS, exercise all
four modes and the recovery bypass, inject read/write/verification errors,
and compare all disk bytes to prove that configuration changes touch only
the reserved record. These are emulator checks, not physical flash or
20 MHz timing qualification.
Test plan:
- With the Z80 socket populated and the
Phase 8 gate passing, confirm
io_trap_handler()still passes every Phase 8 IN/OUT test unchanged; Phase 9 adds no new pins or rework to disturb it. - Confirm the link fails if code crosses
0x10290000, while a runtime print/static assertion still reports the physical C macro as 4 MiB. - Provision the boot package and four exact-size disk images with
picotool -v; read every region's first and last page back and compare host CRC32 values. Give every image a distinct known directory sentinel (DUMP.COMon A,CC2.COMon B,ATTNC11.COMon C, andDOCTOR.COMon D) so a valid but aliased drive mapping cannot pass. - Cold-boot with RESET# initially LOW. Require journal recovery and manifest validation before releasing reset for injected loading. Capture the Z80 writing and verifying SRAM under slow clocks without waiting for BUSACK#. After verification, require a new reset to PC zero before normal execution. Corrupt the manifest, payload, and SRAM readback separately and require each case to remain fail-closed.
- Exercise the decoded port path with real Z80
IN/OUTinstructions, not direct calls to the C handlers. Prove that0x10selects only command/status,0x11only the drive,0x12/0x13the complete LBA, and0x14only record data; also prove that terminal ports0x00/0x01cannot intercept or alias any disk access. - Exercise all 2,560 LBAs on every drive through ports
0x10-0x14, alternating the selected drive between commands. Verify exact 128-byte transfers, distinct per-drive sentinel records, no cross-drive aliasing, invalid drive/LBA rejection, command while BUSY rejection, and test-injected queue-full/error clearing behavior. After an injected flash or journal failure, require every READ/WRITE command to retain READY|ERROR until reboot recovery. - Start a write, then change the live drive and LBA registers before core 1 services its queue. Verify the completed write uses the drive, LBA, write type, and 128-byte payload captured when the command began and changes no other record.
- Boot the packaged image using the Phase 9 USB terminal and exercise the filesystem through CP/M itself:
run
DIRon A-D and require each sentinel on only its expected drive; runLSthrough natural transient exit and BIOS warm boot; run multi-extentC:ATTNC11; and usePIPto copy files across drives. Reboot and compare the affected records and directories byte-for-byte with the expected host images. - Use DSLogic Group D during a write to prove BUSREQ#/BUSACK#/CLK ownership and SRAM-control propagation. Repeat with Group C to prove the trap remains armed until BUSACK# is LOW and is armed again before BUSACK# returns HIGH, with no untrapped I/O edge in either interval. Inject an IORQ# falling edge while BUSACK# is LOW and require the handler to disable the IRQ without touching CLK or either bus.
- Add test-only power-cut hooks after journal-data program, header program, target erase, partial target program, target verification, and header clear. Reboot after every hook and require recovery to produce either the complete old block (before valid header) or the complete new block (after valid header), never a mixture.
- Repeat reads and writes with Wi-Fi absent, associating, connected, and reconnecting. Disk completion must not depend on network state, and WebSocket queue overflow must remain counted rather than block.
- Rewrite hot directory blocks repeatedly while tracking journal-pair rotation and the flash part's rated erase endurance. Treat this as a smoke test, not proof of lifetime.
- Inject safe-execute entry and exit failures. Require RESET# LOW, isolated buses, stopped CLK, and a watchdog reboot without waiting on the core-0 release queue; recovery must retain the verified old or new disk block.
Phase 9 Ctrl+] then 1 through 6 arms the six power-cut points
in test 10; Ctrl+] then 7 or 8 arms safe-execute entry and exit failure in
test 13. Each key arms one watchdog reset for the next CP/M write or flush.
Before arming, preserve host copies of the old and intended new 4 KiB block;
after reboot, read back and compare the complete block before proceeding to
the next injection point.
Bring-up checkpoint¶
Before adding the Phase 10 network transport:
- Require verified provisioning.
- Require successful journal recovery and full SRAM boot-image verification.
- Require no fatal storage status.
- Require the Phase 8 bus/I/O measurements still passing.
- Require the USB CP/M prompt, per-drive directories, transient execution and warm boot.
- Record the read-only all-record Z80 diagnostic separately from CP/M filesystem tests.
- Record network and any fault-injection cases that remain outstanding.
Proceeding to Phase 10 enables network concurrency tests; it does not mark them passed or replace the USB baseline.
Host validation versus bench evidence¶
For fresh-checkout prerequisites, native CMake configuration, emulator environment setup and real Pico builds in one place, see Building and Testing Pico C Firmware.
The control suite now executes the actual flash backend with modeled flash
erase/program operations, not just disk-device stubs. It covers exact image
shape, CRCs, cancellation, first SRAM faults, journal staging verification,
all eight watchdog fault points, recovery, cache behavior, rotation and bounds.
The Phase 9 controller tests use the real terminal queues and check framed
commands, a 20 MHz start request, backpressure, overflow, startup reports
and safe stop/flush interleavings. The automatic-loader build runs the same
terminal regressions plus the real main initialization path with USB absent,
deferred CP/M output, failed loading/startup, late failure reporting and no
automatic retry.
With the existing independent-core environment configured, the suite also executes the real packaged BIOS/CP/M through cold boot, all-drive directory reads and transient/warm boot, plus the complete read-only disk diagnostic:
cmake --build build/host-control --target flash_backend_test stage09_test stage09_autoboot_test disk_test
ctest --test-dir build/host-control \
-R '^(flash_backend_recovery|stage09_usb_cpm|stage09_autoboot_cpm|disk_protocol)$' \
--output-on-failure
python3 -m unittest discover -s src/cpm -p 'test_*.py' -v
python3 scripts/z80_container.py test --test cpm_boot_z80_test.py
Use Python 3.11 or newer for the image tests. The container execution test does not prove physical 20 MHz I/O, setup/hold margins or real flash endurance. Bench checklist items remain unticked until measured.
Pass gate¶
Final storage acceptance, including the tests completed with Phase 10:
- Boot and all four disks match host CRC32 values.
- A-D expose their distinct expected sentinel files with no aliasing.
- Real CP/M
DIR, transient execution, warm boot, multi-extent loading, and cross-drive copy complete through the BIOS. - Queued writes retain their command-time drive/LBA snapshot.
- Every fault-injection reboot recovers an intact old or new block.
- All bounds and manifest failures remain fail-closed.
- Disk service works without Wi-Fi.
- The linker protects the storage boundary.
- The required DSLogic Group C/D captures show no untrapped Z80 cycle around a flash write.