Phase 8 - Bidirectional I/O and USB Terminal Qualification¶
VS Code tasks¶
Choose Tasks: Run Task in the Z80ROMlessSBC workspace.
| Diagnostic | Build task | Deployment task |
|---|---|---|
| Phase 8 virtual I/O | Build Pico - Phase 8 virtual I/O | Flash Pico - Phase 8 virtual I/O |
Deployment tasks force-update without checking the previous firmware phase. Complete this phase's hardware prerequisites and save work before flashing. See Pico tools.
Tick setup items when complete and tests only after recording the measured result. Progress is saved in this browser; ticks are not automatic verification.
Prerequisite: Phase 6, including its bus-grant and reset/fetch measurements. Install: No further chips.
Build z80_stage08_virtual_io; UF2 lives in
build/src/stage08_virtual_io/.
Application
and trap implementation.
What This Phase Adds¶
Phase 6 owns comprehensive CPU/arithmetic/stack/SRAM tests. Its soak's result reports exercise OUT to port 06h only; an IN is treated as an error. Those results do not qualify the bidirectional I/O or terminal service.
This phase retains the existing z80_stage08_virtual_io firmware at nominal
1 MHz, adding:
- IN replies: the Pico drives data to the Z80 during a trapped I/O read.
- OUT/IN address decoding: selected raw ports and their masked aliases.
- USB terminal round-trip: host input, status polling, Z80 IN, Z80 OUT and returned USB characters, with receive/transmit queue and drop counters.
- Repeated boot/I/O integration: 100 software reset/load/verify/self-test cycles; these are not 100 physical cold power cycles.
- Analogue timing and fault evidence: WAIT, stepped release, input data setup/hold, both actual data-buffer enables and invalid-control recovery.
The RAM/USB checker uses a single working byte at 0100h while exchanging
terminal traffic. Its image is loaded and verified across 64 KiB before
execution, but its runtime checker is not a full-memory sweep or a
replacement for Phase 6's SRAM tests.
Likewise, its one-hour command is an I/O/USB integration run, not another
arithmetic/SRAM qualification. A Phase 6 pass at the scope-measured 20 MHz does not
qualify IN or terminal operation at that frequency.
On boot this firmware starts the RAM/USB checker automatically. Deploy only after another running diagnostic has finished and its results are saved.
Synchronous I/O Trap Handler (Phase 8)¶
Raw IORQ# immediately asserts WAIT through U3 while GP9 IO_RELEASE is LOW. The buffered falling edge interrupts core 0, which stops PWM, samples the four port bits, and requires exactly one of RD#/WR# LOW. OUT enables the downward path; IN preloads a reply and enables the upward path.
Only after the data path is ready does GP9 rise. Firmware completes the I/O cycle using slow single clocks, waits for IORQ and the active strobe to rise, disables both paths, waits at least 1 us for isolation, lowers GP9 to rearm WAIT, then restarts PWM. Each clock step includes at least 1 us of LOW settling after the falling edge before polling the buffered controls. PWM-to-SIO handover preloads the LOW latch and extends a frozen HIGH by at least 1 us before selecting SIO. This guard is not repeated on already-SIO steps; capture both stopped phases to rule out short pulses. A stuck strobe times out after 500 ms and causes fail-closed reset/watchdog recovery. U3 also gates upward OE with RD; no SPI or address-driver operation occurs.
Shared bus access uses one GPIO snapshot for port/data sampling and masked SIO writes for data preload and direction changes. Data GPIO pads are initialized at safe startup rather than on every OUT. All isolation, preload-before-drive and settling guards above remain in force.
Register access and SRAM-resident hardware path¶
The trap prepares stopped-SIO clock mode once, then uses a pulse-only helper for each release clock. Each pulse retains its requested LOW and HIGH waits and the final 1 us LOW settling interval. General loader/reset callers still use the self-preparing single-cycle API; the prepared-step API is restricted to the core-0 bus owner, with no intervening clock ownership or mode change.
Each release check samples IORQ and the active RD/WR strobe together in one GPIO snapshot. Both must be HIGH before completion, even when they release on different clocks. Both data-buffer disable bits are set together using the SDK masked GPIO operation, without touching unrelated outputs. The SDK can use the RP2350 GPIO coprocessor; raw SIO-register replacements are not required.
The trap handler/release loop, clock stop/resume/preparation/prepared pulse, and shared data/port bus helpers use the SDK SRAM code-placement attribute. This removes instruction fetches from flash for those selected routines, not for the complete call chain: SDK timing/mux helpers, Phase 8 application callbacks and failure handling can still execute from flash. Phases 9/10 place their complete PIO1 FIFO service chain and port handlers in SRAM; see the placement checks and limits. This is not permission to execute during flash programming; the existing BUSACK, IRQ and multicore flash-lockout contracts remain mandatory.
Native tests verify constant preparation work for one to three release clocks, independently releasing controls, both frozen PWM phases, unchanged settling intervals, data patterns and fail-closed faults. SDK symbol and disassembly checks verify SRAM addresses, the pulse-only body and one masked buffer-isolation operation. These establish work counts, placement and correctness, not measured throughput or proof that XIP-cache contention caused a slowdown. Hardware qualification still requires before/after trap-duration and I/O-throughput measurements, including concurrent network traffic.
Decode is address & 0x17: 00/01 terminal and 10-14 disk ports retain
their contracts. A3/A5-A7 and high-byte aliases are intentional. A trap while
BUSACK LOW is ignored and IRQ disabled until the grant-release path rearms it.
The Phase 8 diagnostic result port is 06, which survives the mask. The
self-test still emits raw ports 00/01/55/AA/FF to exercise aliases: OUT checks
the masked port and the full transmitted byte; IN expects
(raw_port & 0x17) ^ 0xA5. Both successful completion and the RAM-checker's
E1 error report use 06; FE would decode as 16, not as FE.
Application read/write hooks must not print, sleep, touch networking, wait for a queue, or initiate another bus operation. Pico queue operations use bounded spinlock critical sections, not lock-free access.
USB Diagnostic Commands¶
Ordinary console bytes go to the Z80. Ctrl-] followed by one command byte
selects framed supervisor diagnostics; configure the serial terminal to pass
this escape through. Use Ctrl-] s for clock and fault-counter status, and
the command list below for terminal, port and repeated-boot tests.
Do not treat supervisor diagnostics as CP/M input.
| Input | Action in the existing firmware |
|---|---|
| Ordinary bytes | Queue terminal input; running checker reads with IN and echoes with OUT |
Ctrl-] s |
Print configured clock and boot/verify/RAM/trap/queue counters |
Ctrl-] r |
Reload/verify and restart the 1 MHz RAM/USB checker |
Ctrl-] p |
Run the selected-port OUT/IN self-test |
Ctrl-] b |
Run 100 reset/load/verify/port-test cycles |
Ctrl-] h |
Run the one-hour RAM/USB integration check |
Ctrl-] x |
Disable trapping, assert reset, isolate data paths and stop the clock |
Ctrl-] Ctrl-] |
Queue a literal 1Dh terminal byte |
The built-in port test selects raw ports 00h/01h/55h/AAh/FFh; it does not
exhaust every data byte or port alias. Record which patterns were actually
tested. The running checker also emits A-Z heartbeat bytes, so distinguish
those from echoed host input when inspecting the terminal.
Test Plan¶
Use I/O ports and services as the allocation reference. Its extended services are implemented in Phases 9/10, not in this Phase 8 application, and are not required by these tests.
Capture IORQ, WAIT, CLK, RD/WR and both OEs. Confirm WAIT asserts before the CPU could complete the I/O cycle, and the stepped release isolates before the next memory fetch. Include both stopped clock phases described above.
Check IN replies and OUT bytes at every bit pattern,
all supported ports, and intentional aliases.
Run sustained USB echo and repeated boot/verify/RAM tests; counters must not increase.
Use isolated test-only inputs to exercise neither/both strobes asserted, stuck IORQ/RD/WR, and BUSACK races. Do not short CPU-driven outputs. Faults must leave RESET LOW, both OEs HIGH, boot inhibit HIGH, and clock stopped before watchdog reboot.
Repeat after cold power cycles.
Pass Gate¶
- Correct terminal bytes and aliases.
- No RAM corruption.
- No enable overlap.
- Bounded WAIT release.
- Zero unexpected control/timeout/readback failures.
- Save analog and digital evidence before adding flash/network concurrency.
Maintained Source¶
These complete files are included from the repository at documentation build time, not copied into this page.
Phase 8 application - src/stage08_virtual_io/main.c
#include <stdio.h>
#include <string.h>
#include "pico/stdio_usb.h"
#include "pico/stdlib.h"
#include "pico/util/queue.h"
#include "z80sbc/bus.h"
#include "z80sbc/clock.h"
#include "z80sbc/cpu.h"
#include "z80sbc/io_trap.h"
#include "z80sbc/sram.h"
#include "z80sbc/supervisor.h"
#include "z80sbc/test_progress.h"
enum {
TERMINAL_DATA_PORT = 0x00,
TERMINAL_STATUS_PORT = 0x01,
TEST_RESULT_PORT = 0x06,
TERMINAL_RX_DEPTH = 128,
TERMINAL_TX_DEPTH = 512,
TERMINAL_STATUS_RX_READY = 1u << 0,
TERMINAL_STATUS_TX_ROOM = 1u << 1,
TERMINAL_STATUS_CONNECTED = 1u << 7,
USB_COMMAND_ESCAPE = 0x1D,
IO_MODE_RAM_CHECKER = 1,
IO_MODE_SELF_TEST = 2,
AUTOMATED_BOOT_CYCLES = 100,
};
_Static_assert((TEST_RESULT_PORT & Z80_PORT_ADDRESS_MASK) == TEST_RESULT_PORT,
"test result port must survive the hardware address mask");
static const uint8_t RAM_CHECK_PROGRAM[] = {
0x31, 0xFE, 0xFF, 0x21, 0x00, 0x01, 0x36, 0x00,
0x1E, 0x00, 0x16, 0x41, 0x1C, 0x34, 0x7E, 0xBB,
0xC2, 0x33, 0x00, 0x7A, 0xD3, 0x00, 0xDB, 0x01,
0xE6, 0x01, 0x28, 0x04, 0xDB, 0x00, 0xD3, 0x00,
0x14, 0x7A, 0xFE, 0x5B, 0x20, 0x02, 0x16, 0x41,
0x01, 0xFF, 0xFF, 0x0B, 0x78, 0xB1, 0x20, 0xFB,
0xC3, 0x0C, 0x00, 0x3E, 0xE1, 0xD3, TEST_RESULT_PORT, 0x76,
};
static const uint8_t SELF_TEST_PORTS[] = {0x00, 0x01, 0x55, 0xAA, 0xFF};
static queue_t terminal_rx_queue;
static queue_t terminal_tx_queue;
static uint8_t full_test_image[65536];
static uint8_t self_test_image[128];
static size_t self_test_image_length;
static uint32_t io_mode;
static uint32_t usb_connected;
static uint32_t usb_rx_drops;
static uint32_t usb_tx_drops;
static uint32_t boot_attempts;
static uint32_t dma_failures;
static uint32_t readback_failures;
static uint32_t ram_failures;
static uint32_t ram_heartbeats;
static z80_test_progress_t hour_progress;
static uint32_t self_test_write_index;
static uint32_t self_test_errors;
static uint32_t self_test_complete;
static bool self_test_pending;
static absolute_time_t self_test_deadline;
static bool boot_cycle_active;
static unsigned int boot_cycles_remaining;
static unsigned int boot_cycles_passed;
static bool hour_test_active;
static absolute_time_t hour_test_started;
static absolute_time_t hour_test_deadline;
static uint32_t hour_trap_timeout_baseline;
static uint32_t hour_control_error_baseline;
static bool usb_command_pending;
static bool usb_was_connected;
static void clear_queue(queue_t *queue) {
uint8_t discard;
while (queue_try_remove(queue, &discard)) {}
}
static void clear_terminal_queues(void) {
clear_queue(&terminal_rx_queue);
clear_queue(&terminal_tx_queue);
}
static void terminal_receive(uint8_t value) {
if (!queue_try_add(&terminal_rx_queue, &value)) {
uint8_t discard;
if (queue_try_remove(&terminal_rx_queue, &discard))
__atomic_fetch_add(&usb_rx_drops, 1, __ATOMIC_RELAXED);
if (!queue_try_add(&terminal_rx_queue, &value))
__atomic_fetch_add(&usb_rx_drops, 1, __ATOMIC_RELAXED);
}
}
static uint8_t virtual_read(uint8_t port, void *context) {
(void)context;
if (__atomic_load_n(&io_mode, __ATOMIC_ACQUIRE) == IO_MODE_SELF_TEST)
return (uint8_t)(port ^ 0xA5u);
if (port == TERMINAL_DATA_PORT) {
uint8_t value = 0;
queue_try_remove(&terminal_rx_queue, &value);
return value;
}
if (port != TERMINAL_STATUS_PORT)
return 0xFF;
uint8_t status = 0;
if (queue_get_level(&terminal_rx_queue) != 0)
status |= TERMINAL_STATUS_RX_READY;
if (queue_get_level(&terminal_tx_queue) < TERMINAL_TX_DEPTH)
status |= TERMINAL_STATUS_TX_ROOM;
if (__atomic_load_n(&usb_connected, __ATOMIC_ACQUIRE))
status |= TERMINAL_STATUS_CONNECTED;
return status;
}
static void virtual_write(uint8_t port, uint8_t value, void *context) {
(void)context;
if (__atomic_load_n(&io_mode, __ATOMIC_ACQUIRE) == IO_MODE_SELF_TEST) {
if (port == TEST_RESULT_PORT) {
bool passed = value == 0x5A &&
__atomic_load_n(&self_test_write_index,
__ATOMIC_RELAXED) ==
sizeof(SELF_TEST_PORTS) &&
__atomic_load_n(&self_test_errors,
__ATOMIC_RELAXED) == 0;
if (!passed)
__atomic_fetch_add(&self_test_errors, 1, __ATOMIC_RELAXED);
__atomic_store_n(&self_test_complete, 1, __ATOMIC_RELEASE);
return;
}
uint32_t index = __atomic_load_n(&self_test_write_index,
__ATOMIC_RELAXED);
if (index >= sizeof(SELF_TEST_PORTS) ||
port != (SELF_TEST_PORTS[index] & Z80_PORT_ADDRESS_MASK) ||
value != SELF_TEST_PORTS[index]) {
__atomic_fetch_add(&self_test_errors, 1, __ATOMIC_RELAXED);
return;
}
__atomic_store_n(&self_test_write_index, index + 1, __ATOMIC_RELAXED);
return;
}
if (port == TEST_RESULT_PORT && value == 0xE1) {
__atomic_fetch_add(&ram_failures, 1, __ATOMIC_RELAXED);
return;
}
if (port == TERMINAL_DATA_PORT) {
__atomic_fetch_add(&ram_heartbeats, 1, __ATOMIC_RELAXED);
if (!queue_try_add(&terminal_tx_queue, &value))
__atomic_fetch_add(&usb_tx_drops, 1, __ATOMIC_RELAXED);
}
}
static bool load_and_start(const uint8_t *program, size_t length,
uint32_t mode) {
for (uint32_t address = 0; address < sizeof(full_test_image); ++address)
full_test_image[address] =
(uint8_t)address ^ (uint8_t)(address >> 8);
memcpy(full_test_image, program, length);
z80_io_trap_disable();
z80_cpu_fail_closed();
__atomic_fetch_add(&boot_attempts, 1, __ATOMIC_RELAXED);
if (!z80_cpu_prepare_loader() ||
!z80_sram_load(0, full_test_image, sizeof(full_test_image))) {
__atomic_fetch_add(&dma_failures, 1, __ATOMIC_RELAXED);
return false;
}
if (!z80_sram_verify(0, full_test_image, sizeof(full_test_image))) {
__atomic_fetch_add(&readback_failures, 1, __ATOMIC_RELAXED);
return false;
}
clear_terminal_queues();
__atomic_store_n(&io_mode, mode, __ATOMIC_RELEASE);
return z80_io_trap_enable(virtual_read, virtual_write, NULL) &&
z80_cpu_release_reset_and_run(1000000);
}
static bool start_ram_checker(void) {
return load_and_start(RAM_CHECK_PROGRAM, sizeof(RAM_CHECK_PROGRAM),
IO_MODE_RAM_CHECKER);
}
static void emit_self_test_byte(uint8_t value) {
self_test_image[self_test_image_length++] = value;
}
static void build_self_test_image(void) {
size_t failure_patches[sizeof(SELF_TEST_PORTS)];
self_test_image_length = 0;
for (size_t index = 0; index < sizeof(SELF_TEST_PORTS); ++index) {
uint8_t port = SELF_TEST_PORTS[index];
emit_self_test_byte(0x3E);
emit_self_test_byte(port);
emit_self_test_byte(0xD3);
emit_self_test_byte(port);
}
for (size_t index = 0; index < sizeof(SELF_TEST_PORTS); ++index) {
uint8_t port = SELF_TEST_PORTS[index];
uint16_t address = (uint16_t)(0x0200u + index);
emit_self_test_byte(0xDB);
emit_self_test_byte(port);
emit_self_test_byte(0x32);
emit_self_test_byte((uint8_t)address);
emit_self_test_byte((uint8_t)(address >> 8));
emit_self_test_byte(0x3A);
emit_self_test_byte((uint8_t)address);
emit_self_test_byte((uint8_t)(address >> 8));
emit_self_test_byte(0xFE);
emit_self_test_byte((uint8_t)((port & Z80_PORT_ADDRESS_MASK) ^ 0xA5u));
emit_self_test_byte(0xC2);
failure_patches[index] = self_test_image_length;
emit_self_test_byte(0x00);
emit_self_test_byte(0x00);
}
emit_self_test_byte(0x3E);
emit_self_test_byte(0x5A);
emit_self_test_byte(0xD3);
emit_self_test_byte(TEST_RESULT_PORT);
emit_self_test_byte(0x76);
uint16_t failure_address = (uint16_t)self_test_image_length;
emit_self_test_byte(0x3E);
emit_self_test_byte(0xE1);
emit_self_test_byte(0xD3);
emit_self_test_byte(TEST_RESULT_PORT);
emit_self_test_byte(0x76);
for (size_t index = 0; index < sizeof(SELF_TEST_PORTS); ++index) {
self_test_image[failure_patches[index]] = (uint8_t)failure_address;
self_test_image[failure_patches[index] + 1] =
(uint8_t)(failure_address >> 8);
}
}
static bool start_self_test(void) {
__atomic_store_n(&self_test_write_index, 0, __ATOMIC_RELAXED);
__atomic_store_n(&self_test_errors, 0, __ATOMIC_RELAXED);
__atomic_store_n(&self_test_complete, 0, __ATOMIC_RELEASE);
bool started = load_and_start(self_test_image, self_test_image_length,
IO_MODE_SELF_TEST);
self_test_pending = started;
if (started)
self_test_deadline = make_timeout_time_ms(500);
return started;
}
static void print_status(void) {
int64_t elapsed_us = hour_test_active
? absolute_time_diff_us(hour_test_started,
get_absolute_time())
: 0;
printf("\n[diag] stage=8 clock_requested=1000000 clock_actual=%lu "
"boots=%lu dma_fail=%lu verify_fail=%lu ram_fail=%lu "
"trap_timeout=%lu control_error=%lu rx_drop=%lu tx_drop=%lu "
"hour_seconds=%llu\n",
(unsigned long)z80_clock_get_hz(),
(unsigned long)__atomic_load_n(&boot_attempts, __ATOMIC_RELAXED),
(unsigned long)__atomic_load_n(&dma_failures, __ATOMIC_RELAXED),
(unsigned long)__atomic_load_n(&readback_failures, __ATOMIC_RELAXED),
(unsigned long)__atomic_load_n(&ram_failures, __ATOMIC_RELAXED),
(unsigned long)z80_io_trap_timeout_count(),
(unsigned long)z80_io_trap_control_error_count(),
(unsigned long)__atomic_load_n(&usb_rx_drops, __ATOMIC_RELAXED),
(unsigned long)__atomic_load_n(&usb_tx_drops, __ATOMIC_RELAXED),
(unsigned long long)(elapsed_us > 0 ? elapsed_us / 1000000 : 0));
}
static void process_command(uint8_t command) {
if (command == USB_COMMAND_ESCAPE) {
terminal_receive(command);
} else if (command == 's') {
print_status();
} else if (command == 'r') {
boot_cycle_active = false;
hour_test_active = false;
printf(start_ram_checker() ? "\n[diag] RAM/USB checker started\n"
: "\n[diag] FAIL: RAM/USB checker\n");
} else if (command == 'p') {
boot_cycle_active = false;
hour_test_active = false;
printf(start_self_test() ? "\n[diag] port self-test started\n"
: "\n[diag] FAIL: port self-test start\n");
} else if (command == 'b') {
hour_test_active = false;
boot_cycle_active = true;
boot_cycles_remaining = AUTOMATED_BOOT_CYCLES;
boot_cycles_passed = 0;
bool started = start_self_test();
if (!started)
boot_cycle_active = false;
printf(started ? "\n[diag] 100 boot cycles started\n"
: "\n[diag] FAIL: boot-cycle start\n");
} else if (command == 'h') {
boot_cycle_active = false;
__atomic_store_n(&ram_failures, 0, __ATOMIC_RELAXED);
hour_trap_timeout_baseline = z80_io_trap_timeout_count();
hour_control_error_baseline = z80_io_trap_control_error_count();
hour_test_active = start_ram_checker();
if (hour_test_active) {
hour_test_started = get_absolute_time();
hour_test_deadline = make_timeout_time_ms(60u * 60u * 1000u);
z80_test_progress_start(
&hour_progress, __atomic_load_n(&ram_heartbeats, __ATOMIC_RELAXED));
}
printf(hour_test_active ? "\n[diag] one-hour RAM/USB test started\n"
: "\n[diag] FAIL: one-hour test start\n");
} else if (command == 'x') {
boot_cycle_active = false;
hour_test_active = false;
z80_io_trap_disable();
z80_cpu_fail_closed();
printf("\n[diag] CPU held in reset\n");
}
}
static void service_usb(void) {
bool connected = stdio_usb_connected();
__atomic_store_n(&usb_connected, connected, __ATOMIC_RELEASE);
if (usb_was_connected && !connected) {
clear_terminal_queues();
usb_command_pending = false;
}
usb_was_connected = connected;
int input;
while ((input = getchar_timeout_us(0)) >= 0) {
uint8_t value = (uint8_t)input;
if (usb_command_pending) {
usb_command_pending = false;
process_command(value);
} else if (value == USB_COMMAND_ESCAPE) {
usb_command_pending = true;
} else if (__atomic_load_n(&io_mode, __ATOMIC_ACQUIRE) ==
IO_MODE_RAM_CHECKER) {
terminal_receive(value);
}
}
if (!connected)
return;
for (unsigned int sent = 0; sent < 32; ++sent) {
uint8_t value;
if (!queue_try_remove(&terminal_tx_queue, &value))
break;
putchar_raw(value);
}
}
static void service_self_test(void) {
if (!self_test_pending)
return;
bool complete = __atomic_load_n(&self_test_complete, __ATOMIC_ACQUIRE);
bool failed = __atomic_load_n(&self_test_errors, __ATOMIC_RELAXED) != 0;
if (!complete && !failed && !time_reached(self_test_deadline))
return;
self_test_pending = false;
if (!complete || failed) {
boot_cycle_active = false;
printf("\n[diag] FAIL: port self-test errors=%lu\n",
(unsigned long)__atomic_load_n(&self_test_errors,
__ATOMIC_RELAXED));
return;
}
if (!boot_cycle_active) {
printf("\n[diag] PASS: OUT/IN ports and SRAM stores\n");
return;
}
++boot_cycles_passed;
if (--boot_cycles_remaining == 0) {
boot_cycle_active = false;
printf("\n[diag] PASS: %u reset-held boot/IO cycles\n",
boot_cycles_passed);
return;
}
if (!start_self_test()) {
boot_cycle_active = false;
printf("\n[diag] FAIL: boot cycle %u start\n", boot_cycles_passed + 1);
}
}
static void service_hour_test(void) {
if (!hour_test_active)
return;
if (!z80_test_progress_poll(
&hour_progress, __atomic_load_n(&ram_heartbeats, __ATOMIC_RELAXED))) {
hour_test_active = false;
printf("\n[diag] FAIL: one-hour RAM/USB test: heartbeat timeout\n");
return;
}
if (!time_reached(hour_test_deadline))
return;
hour_test_active = false;
bool passed = hour_progress.observed &&
__atomic_load_n(&ram_failures, __ATOMIC_RELAXED) == 0 &&
z80_io_trap_timeout_count() == hour_trap_timeout_baseline &&
z80_io_trap_control_error_count() ==
hour_control_error_baseline;
printf(passed ? "\n[diag] PASS: one-hour RAM/USB test\n"
: "\n[diag] FAIL: one-hour RAM/USB test\n");
}
int main(void) {
z80_safe_startup();
stdio_init_all();
queue_init(&terminal_rx_queue, sizeof(uint8_t), TERMINAL_RX_DEPTH);
queue_init(&terminal_tx_queue, sizeof(uint8_t), TERMINAL_TX_DEPTH);
build_self_test_image();
printf("\n[diag] Stage 8: virtual-ROM, USB CDC, and I/O trap\n");
printf("[diag] normal bytes=terminal input; Ctrl-] then "
"s=status r=restart p=port-test b=100-boots h=one-hour x=stop\n");
printf(start_ram_checker() ? "[diag] PASS: RAM/USB checker at 1MHz\n"
: "[diag] FAIL: boot/trap initialization\n");
while (true) {
service_usb();
service_self_test();
service_hour_test();
tight_loop_contents();
}
}
Shared synchronous I/O trap - src/common/io_trap.c
#include "z80sbc/io_trap.h"
#include "hardware/watchdog.h"
#include "pico/stdlib.h"
#include "z80sbc/clock.h"
#include "z80sbc/cpu.h"
#include "z80sbc/pins.h"
#if defined(Z80_BANKED_PIO)
#include "z80sbc/banked_pio.h"
#else
#include "z80sbc/bus.h"
#endif
static const uint32_t RESET_FAULT_MAGIC = 0x5A384954;
static z80_io_read_handler_t application_read;
static z80_io_write_handler_t application_write;
static void *application_context;
static uint32_t trap_timeouts;
static uint32_t control_errors;
#if !defined(Z80_BANKED_PIO)
static uint32_t inactive_irqs;
#endif
z80_io_trap_reset_fault_t z80_io_trap_take_reset_fault(void) {
uint32_t magic = watchdog_hw->scratch[0];
uint32_t reason = watchdog_hw->scratch[1];
uint32_t gpio = watchdog_hw->scratch[2];
uint32_t check = watchdog_hw->scratch[3];
watchdog_hw->scratch[0] = 0;
if (!watchdog_caused_reboot() || magic != RESET_FAULT_MAGIC ||
check != (magic ^ reason ^ gpio) ||
reason < Z80_IO_TRAP_RESET_RELEASE_TIMEOUT ||
reason > Z80_IO_TRAP_RESET_INVALID_CONTROLS)
return (z80_io_trap_reset_fault_t){0};
return (z80_io_trap_reset_fault_t){
.reason = (z80_io_trap_reset_reason_t)reason,
.gpio = gpio,
};
}
static _Noreturn void trap_fail_closed(z80_io_trap_reset_reason_t reason,
uint32_t controls) {
// Scratch 0-3 survive watchdog reset; the SDK reserves 4-7 for reboot.
watchdog_hw->scratch[0] = 0;
watchdog_hw->scratch[1] = reason;
watchdog_hw->scratch[2] = controls;
watchdog_hw->scratch[3] = RESET_FAULT_MAGIC ^ reason ^ controls;
watchdog_hw->scratch[0] = RESET_FAULT_MAGIC;
z80_io_trap_disable();
z80_cpu_fail_closed();
z80_reset_with_clock_cycles(3, 1);
watchdog_reboot(0, 0, 0);
while (true)
tight_loop_contents();
}
#if !defined(Z80_BANKED_PIO)
enum { TRAP_RELEASE_TIMEOUT_US = 500000 };
static void __not_in_flash_func(resume_and_wait_for_release)(uint control_pin) {
uint32_t released_mask = (1u << PIN_IORQ_N) | (1u << control_pin);
gpio_put(PIN_IO_RELEASE, 1);
uint32_t controls = gpio_get_all();
if ((controls & released_mask) != released_mask) {
/* Rare: the cycle needs stepping. Only this path reads the timer. */
absolute_time_t deadline = make_timeout_time_us(TRAP_RELEASE_TIMEOUT_US);
z80_clock_prepare_stepping();
do {
if (time_reached(deadline)) {
__atomic_fetch_add(&trap_timeouts, 1, __ATOMIC_RELAXED);
trap_fail_closed(Z80_IO_TRAP_RESET_RELEASE_TIMEOUT, gpio_get_all());
}
z80_clock_step_prepared(1);
controls = gpio_get_all();
} while ((controls & released_mask) != released_mask);
}
z80_data_bus_isolate();
busy_wait_us_32(1);
gpio_put(PIN_IO_RELEASE, 0);
z80_clock_resume();
}
/* The entry path stops CLK and retains the HIGH guard before dispatch. */
static void __not_in_flash_func(service_stopped_io)(void) {
uint32_t controls = gpio_get_all();
if (controls & (1u << PIN_IORQ_N)) {
// A latched edge is not an active I/O cycle once IORQ# has released.
__atomic_fetch_add(&inactive_irqs, 1, __ATOMIC_RELAXED);
__atomic_fetch_add(&control_errors, 1, __ATOMIC_RELAXED);
z80_clock_resume();
return;
}
bool is_read = (controls & (1u << PIN_RD_N)) == 0;
bool is_write = (controls & (1u << PIN_WR_N)) == 0;
if (is_read == is_write) {
__atomic_fetch_add(&control_errors, 1, __ATOMIC_RELAXED);
trap_fail_closed(Z80_IO_TRAP_RESET_INVALID_CONTROLS, controls);
}
uint8_t port = z80_port_bus_sample();
if (is_write) {
z80_data_bus_prepare_input();
uint8_t value = z80_data_bus_sample();
/* Runtime banking requires the PIO backend; never switch banks here. */
if (port != 0x07 && application_write != NULL)
application_write(port, value, application_context);
resume_and_wait_for_release(PIN_WR_N);
return;
}
uint8_t value = 0; /* Runtime banking requires the PIO backend. */
if (port != 0x07)
value = application_read == NULL ? 0xFF
: application_read(port, application_context);
z80_data_bus_drive(value);
resume_and_wait_for_release(PIN_RD_N);
}
#endif
#if defined(Z80_BANKED_PIO)
/* PIO1 completes every cycle and calls the application handlers directly
* from its IRQ, in bus order. */
static uint8_t __not_in_flash_func(unhandled_read)(uint8_t port, void *context) {
(void)port;
(void)context;
return 0xFF;
}
static void __not_in_flash_func(unhandled_write)(uint8_t port, uint8_t value,
void *context) {
(void)port;
(void)value;
(void)context;
}
static void apply_pio_fault(uint32_t controls) {
__atomic_fetch_add(&control_errors, 1, __ATOMIC_RELAXED);
trap_fail_closed(Z80_IO_TRAP_RESET_INVALID_CONTROLS, controls);
}
bool z80_io_trap_rearm(void) {
gpio_init(PIN_IORQ_N);
gpio_set_dir(PIN_IORQ_N, GPIO_IN);
gpio_disable_pulls(PIN_IORQ_N);
return z80_banked_pio_rearm(
application_read != NULL ? application_read : unhandled_read,
application_write != NULL ? application_write : unhandled_write,
application_context, apply_pio_fault);
}
void z80_io_trap_drain_writes(void) {
z80_banked_pio_drain();
}
void z80_io_trap_disable(void) {
z80_banked_pio_disable();
}
void z80_io_trap_poll(void) {
if (!z80_banked_pio_poll()) {
__atomic_fetch_add(&trap_timeouts, 1, __ATOMIC_RELAXED);
trap_fail_closed(Z80_IO_TRAP_RESET_RELEASE_TIMEOUT, gpio_get_all());
}
}
#else
static void __not_in_flash_func(io_trap_handler)(uint gpio, uint32_t events) {
(void)events;
if (gpio != PIN_IORQ_N)
return;
if (!gpio_get(PIN_BUSACK_N)) {
gpio_set_irq_enabled(PIN_IORQ_N, GPIO_IRQ_EDGE_FALL, false);
return;
}
z80_clock_stop();
busy_wait_us_32(1);
service_stopped_io();
}
bool z80_io_trap_rearm(void) {
gpio_init(PIN_IORQ_N);
gpio_set_dir(PIN_IORQ_N, GPIO_IN);
gpio_disable_pulls(PIN_IORQ_N);
gpio_acknowledge_irq(PIN_IORQ_N, GPIO_IRQ_EDGE_FALL);
gpio_set_irq_enabled_with_callback(PIN_IORQ_N, GPIO_IRQ_EDGE_FALL, true,
io_trap_handler);
return true;
}
void z80_io_trap_disable(void) {
gpio_set_irq_enabled(PIN_IORQ_N, GPIO_IRQ_EDGE_FALL, false);
gpio_acknowledge_irq(PIN_IORQ_N, GPIO_IRQ_EDGE_FALL);
}
void z80_io_trap_poll(void) {}
void z80_io_trap_drain_writes(void) {}
#endif
bool z80_io_trap_enable(z80_io_read_handler_t read_handler,
z80_io_write_handler_t write_handler,
void *context) {
application_read = read_handler;
application_write = write_handler;
application_context = context;
return z80_io_trap_rearm();
}
uint32_t z80_io_trap_timeout_count(void) {
return __atomic_load_n(&trap_timeouts, __ATOMIC_RELAXED);
}
uint32_t z80_io_trap_control_error_count(void) {
return __atomic_load_n(&control_errors, __ATOMIC_RELAXED);
}
#if !defined(Z80_BANKED_PIO)
uint32_t z80_io_trap_inactive_irq_count(void) {
return __atomic_load_n(&inactive_irqs, __ATOMIC_RELAXED);
}
#endif