Skip to content

Phase 6 - Z80-Assisted SRAM Loading

VS Code tasks

Choose Tasks: Run Task in the Z80ROMlessSBC workspace.

Diagnostic Build task Deployment task
Phase 6 SRAM loader Build Pico - Phase 6 SRAM loader Flash Pico - Phase 6 SRAM loader
Phase 6 stepped loader Build Pico - Phase 6 stepped loader Flash Pico - Phase 6 stepped loader
Phase 6 PWM memory Build Pico - Phase 6 PWM memory Flash Pico - Phase 6 PWM memory
Phase 6 PWM 20 MHz Build Pico - Phase 6 PWM 20 MHz Flash Pico - Phase 6 PWM 20 MHz
Phase 6 one-hour 20 MHz soak Build Pico - Phase 6 one-hour 20 MHz soak Flash Pico - Phase 6 one-hour 20 MHz soak

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 5. Install: Z80 U1 and SRAM U2 together, after all socket wiring is checked. The CPU is now required for RAM access; RESET is released during injection.

Wiring - SRAM Socket

Confirm the Phase 4 address trunk and A16 bank-select wiring, then complete the SRAM address/data/fixed pin wiring, including CE# U2.22 from U4.12, WE# U2.29 from U4.9, OE# U2.24 from U3.3 and CE2 to +5 V.

Complete Z80 CLK/RESET/BUSREQ/WAIT and all control monitor taps. INT/NMI have pulls to +5 V; M1/HALT/RFSH outputs remain open. Verify pin-1 orientation, power pins, and local bypasses before insertion.

Firmware and Loader Qualification

Build z80_stage06_sram_loader; UF2: build/src/stage06_sram_dma/z80_stage06_sram_loader.uf2. Application and loader protocol.

Automatic Full 64 KiB Test

The z80_stage06_step_loader diagnostic also provides an automatic whole-range test. It requires all eight active packages, checked wiring, and loaded power rails. It uses the existing Z80-assisted loader: Pico injects instructions and supplies clocks; the Z80 alone addresses and writes SRAM. This is destructive and does not load or run CP/M.

cmake --build build --target z80_stage06_step_loader

Flash build/src/stage06_sram_dma/z80_stage06_step_loader.uf2, then open the Pico USB serial console at 115200 baud. Apply external 5 V before USB; disconnect USB before external 5 V. Never flash/run with USB alone. Before the serial console connects, the application holds RESET# LOW, CLK LOW, SRAM inhibited and both data paths disabled, without issuing clocks or accessing memory. It waits for the USB CDC console connection (DTR), not merely a plugged-in USB cable. Connecting alone only prints the banner: it still waits for your explicit start command. The startup banner is concise; type f, then press Enter once to run the complete test automatically. No further stepping commands are needed. It can also replace an in-progress manual session, starting a fresh loader.

The inclusive range is 0000-FFFF, exactly 65,536 byte addresses. An SRAM byte cannot hold 256 or larger, so values wrap:

  1. Low-byte pass: fill the entire range with address & 0xFF (00,01,...,FF,00,01,...), then read the entire range back.
  2. High-byte pass: fill the entire range with address >> 8 (256 zeros, 256 ones, and so on), then read the entire range back.

Both passes fill all memory before verification. The second pass is important because the low-byte pattern alone cannot distinguish locations 256 bytes apart. Together they check both halves of the address, but they are not a substitute for March tests, analogue margins or timing evidence. After success, SRAM contains the high-byte pattern from the second pass.

Writing has one short phase banner, not a claimed per-byte success. Verification prints exactly one result line per address, all numbers hexadecimal, using the actual sampled SRAM value:

address 0000, value 00, expected 00 - SUCCESS
address 0001, value 01, expected 01 - SUCCESS
address 0002, value 02, expected 02 - SUCCESS

Pass banners distinguish the two patterns. A successful run has 65,536 result lines per pass, 131,072 total, followed by a short success summary. Every completed, failed or aborted run also prints decimal totals:

STATS: successes=131072 failures=0

Successes count matching SRAM read-back checks across both passes, not unverified writes: a complete error-free run has 131,072 successes over 65,536 distinct addresses. Failures count a data mismatch, a read/write API error, or loader initialization failure. The test still stops at the first failure, so the failure count is at most one. An operator abort or console disconnection does not count as a failure; its totals are partial. Counters reset on each new f run.

A mismatch prints the actual value and FAIL, then stops at the first error. An API read/write error explicitly prints value ERROR rather than inventing a read-back value. Loader initialization errors are reported separately and cannot produce a success summary.

Press x to abort, without Enter. The automatic loop checks input and USB connection between byte operations, not in the middle of a clock pulse. It performs at most one byte write/read per iteration. On abort, failure, completion or console disconnection, both data paths are isolated, reset is asserted and the clock is stopped. While automatic mode is running, other commands are rejected; they do not switch or restart the test. After x, press Enter to clear the input line before f + Enter to restart.

Automatic mode uses the shared loader's 1 us half-period clocks, not the manual harness's 1 ms half-period clocks. It does not start PWM or execute from SRAM. Actual elapsed time depends on firmware overhead and USB output; these slow-cycle tests do not qualify normal operating speed.

Automatic test implementation. Host tests verify exact 0000/FFFF bounds, both fill/read passes, all output rows, wrap at 256, detection of 256-byte address aliases, first-error stops, abort boundaries and console-disconnect isolation. Host tests are not physical RAM-test evidence.

PWM Speed Screening After the Slow Test

After the slow whole-memory test passes, the separate z80_stage06_pwm_memory application can screen actual Z80 execution from SRAM at increasing PWM clock rates. It does not require CP/M, provision flash disks, or start networking. All eight active packages and the complete WAIT/I/O wiring must be fitted. The existing slow diagnostic is unchanged.

cmake --build build --target z80_stage06_pwm_memory

Flash build/src/stage06_sram_dma/z80_stage06_pwm_memory.uf2. Keep external 5 V applied before USB and power every fitted IC; USB-only flashing/running is unsupported. Open USB serial at 115200 baud with DTR. Startup holds reset and does not start clocks or memory accesses until f + Enter. Use x without Enter to abort; h + Enter displays help and s + Enter displays status. After aborting, press Enter before starting another run. Disconnecting the console stops the test safely; reconnecting does not resume it.

The test requests 1-8 MHz in 500 kHz steps, using the existing integer, 50%-duty PWM implementation. It prints requested and nominal actual frequencies; these are not measured frequencies. The 6.5 MHz request duplicates the actual 6 MHz waveform and is skipped after 6 MHz passes. There are 14 distinct actual rates at the configured 144 MHz system clock. Actual rates above 6 MHz are explicitly marked experimental. See the frequency table and timing requirements.

At each speed:

  1. Load and read back a small test program using slow assisted clocks.
  2. Execute that program from the lower 32 KiB using PWM. Fill the entire upper 32 KiB, then verify it, for each of four patterns: address low byte, address high byte, complemented low byte, complemented high byte.
  3. Reload the program into the upper 32 KiB, with a temporary JP 8000 at reset address zero. Once execution reaches the upper half, repeat the same tests across every lower-half address, including 0000. The program needs no stack and disables maskable interrupts.
  4. Repeat both halves until at least 60 seconds of execution-phase wall time have accumulated and a complete two-half cycle has finished. Assisted loading/verification time is excluded. Execution time includes the brief stepped I/O reports, not exclusively uninterrupted PWM pulses.

One complete cycle verifies 65,536 addresses against four patterns: 262,144 successful comparisons. Program fetches, branches, reads and writes run at the selected PWM speed. Only short progress/error OUT 06 reports use the existing WAIT trap and slow I/O completion. The main loop must observe a correctly ordered completed-pattern marker within five seconds; a hung CPU cannot become a timed PASS. The longest normal fill/read block is under four million T-states (about four seconds at 1 MHz), leaving a bounded progress margin.

Output is one RUN line, progress about every ten seconds, and one PASS line per speed, rather than one USB line per byte. For example:

RUN: requested=1000000Hz actual=1000000Hz minimum=60s
PASS: requested=1000000Hz actual=1000000Hz pwm_ms=... cycles=... successes=... failures=0

The test stops at the first mismatch, missing/incomplete progress, unexpected I/O, loader error, or trap-counter change. A CPU-detected mismatch includes address, actual byte and expected byte. Final statistics count completed 32 KiB verification blocks only; successful comparisons in an incomplete block are not counted. Abort/disconnection does not count as a failure. Reset is asserted, PWM stopped, SRAM inhibited, and both Pico data paths isolated on completion, failure or abort. A watchdog or power reboot during a run invalidates that run; no new run starts automatically and no unobserved interval can count toward a PASS.

This is a functional screening result, not frequency qualification. It is not a March/coupling-fault test. The two-half approach also does not establish whole-memory retention across simultaneous patterns in both halves; retain the slow full-range test as complementary alias evidence. Analogue setup/hold and write-pulse margins, clock quality, WAIT handover, and repeated cold starts still need the scope and logic-analyzer qualification procedure.

Screening implementation. Host tests independently execute the generated Z80 instructions through both halves, check exact fill/read bounds and patterns, inject last-address corruption and 256-byte aliases, and exercise duration, sweep, USB gating, abort, loader failures, protocol errors and timeouts. These tests do not prove physical timing.

20 MHz Screening

After the 1-8 MHz screening passes, the separate z80_stage06_pwm_memory_20mhz target runs the same self-validating write/read/compare test at nominally exact 20 MHz:

cmake --build build --target z80_stage06_pwm_memory_20mhz

Flash build/src/stage06_sram_dma/z80_stage06_pwm_memory_20mhz.uf2, connect USB serial with DTR, check the clock banner and send f + Enter. The target compiles its clock, CPU, SRAM and I/O-trap chain separately with Z80_CLOCK_20MHZ; normal Phase 6 targets retain their 8 MHz maximum. With RESET asserted and before USB initialization or Z80 clocks, it selects a 1200 MHz PLL_SYS VCO with post-dividers 5 and 2, giving 120 MHz clk_sys. USB retains its independent 48 MHz PLL. Integer PWM division with a period count of 6 and 50% duty produces 20 MHz.

The rate must complete at least 60 seconds of the same full-64-KiB, four-pattern workload, finishing on a complete two-half cycle. SRAM accesses have no added wait states; only short I/O reports use the existing WAIT trap. A mismatch, timeout, unexpected control, loader error, aborted run, disconnect or watchdog reboot fails the run. Immediate x abort and fail-closed reset/isolation remain available. The CPU's 20 MHz grade is not a complete-system rating; a pass is functional screening evidence, not analogue timing qualification. Phases 9 and 10 run at 20 MHz from the PIO0 clock, not this PWM chain.

One-Hour 20 MHz Arithmetic and SRAM Soak

For an overnight run, the separate z80_stage06_math_soak_14h_20mhz target uses the identical workload for at least 14 hours (50,400 seconds), stopping at the first failure and finishing only on a complete cycle. The original one-hour target remains available. See the 14-hour build and operating notes.

See the complete soak-test guide for the instruction inventory, arithmetic/flag assertions, result-port protocol, statistics and run procedure. That page includes the maintained diagnostic README directly.

z80_stage06_math_soak_20mhz is a separate destructive diagnostic, not a replacement for the full-64-KiB frequency screens. It loads one resident Z80 assembly program and independently calculated reference vectors, then executes continuously without reloading/resetting between cycles.

cmake --build build --target z80_stage06_math_soak_20mhz

UF2: build/src/stage06_sram_dma/z80_stage06_math_soak_20mhz.uf2. The existing build directory must first be configured as above. The build assembles math_soak.asm and embeds its exact bytes using the firmware build tools. The isolated 20 MHz clock/CPU/trap chain selects 120 MHz Pico clk_sys before USB initialization with Z80 reset asserted, using the same PLL and 50%-duty PWM configuration as the 20 MHz frequency screen. Normal targets remain unchanged.

The resident memory layout is:

SRAM addresses Purpose
0000-0FFF Z80 instructions and two loop-state bytes
1000-19FF 128 reference records, generated by the Pico using wider C arithmetic
1A00-1FFF Guarded stack; SP starts at 2000 and grows downward
2000-FFFF 56 KiB write/read/compare work area

Each phase performs:

  1. Nested CALL/RET with 16 recursive levels, PUSH/POP register preservation, exact stack-pointer recovery and a lower stack guard.
  2. Sixteen repetitions of 128 operand pairs: 64 boundary-value combinations and 64 deterministic pseudo-random pairs. Each pair checks ADD HL, ADC HL with carry, subtraction via SBC HL with carry clear, SBC HL with borrow, low-16-bit shift/add multiplication, and signed/unsigned ordering. Arithmetic results and documented carry, half-carry, sign, zero, overflow and subtract flags are checked where applicable; undocumented flag bits are masked out. Signed comparison accounts for sign XOR overflow.
  3. A complete fill of 2000-FFFF, followed by complete read/compare. Successive phases use low address, high address, complemented low address and complemented high address bytes. Four phases make a cycle.

The code, reference and stack regions are not swept as general data. Instruction fetches, table reads and stack accesses exercise them, but this does not claim full-64-KiB destructive coverage; retain the preceding screen. No CP/M or BDOS is installed or required.

Keep external 5 V applied with every fitted IC powered before flashing or connecting USB. Connect serial with DTR, confirm the 120 MHz supervisor banner, then send f + Enter. A connection alone never starts the test. h gives help, s gives status/statistics, and immediate x aborts. Completion requires at least 3600 seconds of execution-phase elapsed time, excluding initial image loading/verification but including brief I/O report pauses, and a completed four-phase cycle. The program is not reset between phases. There are no added memory wait states.

How the Z80 Reports Results to the Pico

The Z80 performs the calculations and comparisons; the Pico supervises the run and receives its reports. Results travel through Z80 I/O port 06h, not through a UART, CP/M/BDOS call or an SRAM mailbox. This is an I/O-space address, not SRAM address 0006h. No IN instruction is needed; an unexpected I/O read is treated as a protocol fault.

After all arithmetic, stack, loop and SRAM checks in a phase succeed, the Z80 executes the equivalent of:

ld a,040h       ; Phase 0 completed successfully
out (06h),a    ; Send the accumulator byte to the Pico

The actual program forms the marker from the current phase number. Successive phases send 40h, 41h, 42h, 43h, then repeat in that order. These bytes identify a complete successful phase, not a single arithmetic result or memory comparison.

Each OUT (06h),A is captured as follows:

  1. The Z80 drives its port address and the accumulator value onto the address and D0-D7 data buses, asserting IORQ# (I/O request) and WR# (write). The # suffix means active LOW.
  2. With Pico IO_RELEASE LOW, the HCT32 equation WAIT# = IORQ# OR IO_RELEASE asserts WAIT#, holding the I/O transaction while the Pico responds.
  3. The buffered IORQ# falling edge interrupts the Pico. The handler stops PWM, allows settling, samples the port through the LVC244 monitor and confirms that this is a write. Only A0/A1/A2/A4 are decoded (address & 0x17), so the port is seen as 06h; unused address bits, including the high byte presented by OUT (n),A, are ignored.
  4. The Pico keeps its upward data driver disabled, enables the downward SN74LVC245AN path and samples the eight data bits at 3.3 V. No raw 5 V Z80 data signal is connected directly to a Pico input.
  5. The soak's receive callback checks the decoded port and byte. An in-order success marker increments the completed-phase counter; an unexpected marker or port flags a protocol error.
  6. The trap raises IO_RELEASE and uses controlled single clock cycles until IORQ# and WR# release. It then isolates both data paths, lowers IO_RELEASE and resumes PWM. The next Z80 instruction follows normally; this reporting transaction does not reset or reload the program.

Only the short I/O reports use this hardware WAIT/stepped-completion mechanism. SRAM instructions execute at the configured PWM rate without added memory wait states.

On a failed check, the Z80 instead sends nine bytes, each using a separate OUT (06h),A, then executes HALT:

Byte position Contents
0 E1h: failure-packet marker
1 Check stage
2 Operand-vector index or diagnostic context
3, 4 Relevant address, low byte then high byte
5, 6 Actual 16-bit value, low byte then high byte
7, 8 Expected 16-bit value, low byte then high byte

The Pico assembles the packet, prints the mismatch details and fails closed with RESET asserted and PWM stopped. An incomplete packet also fails on timeout. Stages 01-0B identify arithmetic result/flag, multiplication and comparison checks; 0C-10 identify stack checks; 11-12 check vector/repetition loop bounds; 80-83 identify RAM patterns. For arithmetic failures the address identifies the reference record; for RAM failures it identifies the work address. Structural/stack vector indices are diagnostic context, not necessarily a valid operand index.

Maintained sources: Z80 report instructions, Pico report parser and statistics, and I/O trap and stepped completion.

The Pico reports progress every ten seconds and fails closed on the first CPU mismatch, malformed/out-of-order report, loader/control error or five-second progress timeout. Disconnect, reboot or abort invalidates the run, not a PASS. A completed PASS holds RESET LOW, stops PWM and isolates both Pico data paths. There is no automatic restart.

The Pico generates the USB serial PROGRESS and STATS text from the received phase markers and its own elapsed-time measurement. The host logger adds the UTC timestamp and SERIAL prefix shown in the live terminal. Millions of successful checks therefore do not require millions of OUT transactions: the Pico credits the known workload only after receiving each complete-phase success marker. It does not reread every work byte or independently recompute each arithmetic result during the soak.

STATS reports successful checks and failures, with separate SRAM-byte, arithmetic, stack and loop totals. Each completed phase credits:

  • 57,344 SRAM-byte comparisons;
  • 22,528 arithmetic assertions (2048 complete vectors, 11 assertions each);
  • five stack assertions and 17 loop-bound assertions;
  • 79,894 total successful assertions.

These are different kinds of assertions, not an instruction count. Partial phases are deliberately not credited. A first failure counts as one; abort/disconnect is not a failure count but is still an incomplete run. Progress statistics alone are not a final PASS. A one-hour pass remains functional soak evidence, not measured analogue timing qualification.

Host Validation

Host controller tests simulate the exact hour/cycle boundary, counters, serial gating and fault stops. Run the Z80 instruction execution suite in the Apple container:

cmake -S src/common/tests -B build/host-control -G Ninja
cmake --build build/host-control --target math_soak_test math_soak_14h_test
ctest --test-dir build/host-control -R '^math_soak_' --output-on-failure
python3 scripts/z80_container.py test

The emulator runs the actual image through two cycles and checks every work byte/reference-table boundary, plus injected arithmetic, flag, comparison, stack-guard and first/middle/last SRAM failures. This is host execution evidence, not an hour-long physical run.

Continuous 20 MHz Scope Diagnostic (No Z80 I/O)

For uninterrupted scope captures, use the separate z80_stage06_scope_20mhz guide. It shares the arithmetic, stack and 56 KiB SRAM workload, but replaces all Z80 port reports with a reserved SRAM mailbox. No I/O trap, periodic result reads or bank reloads pause PWM during RUN. A detected mismatch commits SRAM details and HALTs the CPU while PWM continues.

x stops execution and retrieves the mailbox with slow assisted reads. There is no live progress supervision or automatic timed PASS. A clock that keeps switching does not prove the CPU remains healthy. See the 20 MHz DHO814 setup for probe safety, measurement settings and the expected 50 ns period. Build only until another running test has completed; deployment is a separate explicit operation.

Console-Stepped Miniature Loader

For controlled bring-up with all eight active packages installed, build the separate z80_stage06_step_loader target:

cmake --build build --target z80_stage06_step_loader

UF2: build/src/stage06_sram_dma/z80_stage06_step_loader.uf2. Maintained C harness. This exercises real hardware using the same injected instructions and bus helpers as the loader, rather than simulating SRAM in Pico RAM. It mimics the mechanism used to load CP/M but does not load or execute a CP/M image. The existing automatic p/c/m application remains unchanged.

This test destructively writes 00 at 0000, 01 at 0001, and 02 at 0002 (all hexadecimal). There are no scratch writes. It is not a full-memory or complete address-bus test. The C transfer table describes each instruction and its expected effect; the serial console separates EXPECTED behavior from OBSERVED monitor inputs and actual data-bus samples.

Before flashing, check every buffer's supply, direction and enables, and the loaded rails. Apply external 5 V before USB; remove USB before external 5 V. Do not use USB alone. Use the firmware flashing procedure and open the Pico USB serial console at 115200 baud. The application waits for the console, starts in reset and does not clock the CPU until instructed. Disconnecting the console isolates both data paths, asserts reset and stops the session; reconnection requires an explicit restart.

Commands and first steps

Type one lower-case letter then Enter. CR, LF and CRLF work. Empty Enter does nothing; malformed/multiple-letter lines do not advance. Do not paste command sequences or hold down a key.

Command Action
h + Enter Print help, physical probe pin map and current expected behavior; no clocks or ownership changes
f + Enter Start the automatic full 64 KiB low/high-byte test described above
b + Enter Begin/restart with RESET# LOW, CLK LOW, SRAM inhibited and data paths isolated; no clocks
n + Enter Perform exactly one next action: either one complete clock cycle or a labelled no-clock ownership/reset action
r + Enter Repeat current instructions and observations without changing signals or clocking
x Abort immediately without Enter: isolate data paths, assert reset and inhibit SRAM output

After x, press Enter to clear the discarded line before entering b. On any reported mismatch the session stops in reset; n cannot continue from the fault. Record the first error before changing anything.

  1. Read h, then type b + Enter.
  2. Issue n + Enter six times, one reset clock at a time.
  3. The next n checks idle controls and releases reset with CLK stopped LOW; it does not generate a clock.
  4. Issue two further n commands for the existing loader's reset-exit alignment clocks. Physical alignment must still be qualified.
  5. Follow NEXT n for every subsequent action. Setup and injection actions do not clock; T1/T2/T3/T4 actions each clock once and finish LOW.

Every pulse has 1 ms LOW/HIGH half-periods and the shared clock helper's LOW settling delay. The application never starts PWM. A multimeter sees the held LOW clock between commands; it cannot prove pulse amplitude, edge quality, exact timing, or absence of short glitches.

Expected instruction and bus sequence

For each target address nn and test byte n, the Z80 executes:

LD HL,nn       ; 21 low high: set target, using injected opcode/operands
LD (HL),n      ; 36 byte: Z80 drives address/data and writes SRAM
LD A,00        ; 3E 00: clear accumulator without changing SRAM
LD A,(HL)      ; 7E: SRAM supplies the byte; Pico samples through U10

LD A,00 clears the accumulator, not a persistently driven physical data bus. The zero operand is injected during its own fetch; afterward the upward path is isolated. No extra bus-clear operation or scratch write is performed. The Pico cannot directly observe A; it observes the SRAM data bus during the subsequent read, and the Z80 is expected to latch that byte at read completion.

Held point Expected address/data ownership and controls
Injected opcode/operand after T2 Address is the injected instruction PC, not necessarily HL; U9 supplies the printed byte, U10 off, SRAM OE# HIGH
Z80 write after T2 Address is HL; Z80 supplies the test byte; CE# LOW, WE# LOW, OE# HIGH; U9 off and U10 samples the write data
SRAM read after T2 Address is HL; SRAM supplies the stored byte; CE# LOW, WE# HIGH, OE# LOW; U9 off and U10 samples the read data
Opcode T4 Refresh can put a different address on the bus; do not mistake it for a target-address failure

At the held T2 points, use r to reread instructions without changing bus ownership. The console prints full expected address and byte in hexadecimal and as A15..A0 / D7..D0 bits, and h gives exact SRAM pin numbers. For example, the write/read of address 0002 expects address bits 0000000000000010 and data bits 00000010.

Use DC volts with the black probe on common circuit GND and insulated tips on the listed pins; never bridge neighboring contacts. Power off before moving clips. HIGH/LOW listings are logic expectations, not measured voltages: compare physical readings against the exact receiver's input limits. Raw Z80 outputs are TTL; a digital HIGH indication does not establish SRAM CMOS margin. Use the scope/analyzer for timing qualification.

The console compares the U10 data sample with the expected byte during both write T2 and read T2. A write-data match is not yet proof of a committed write; the following T3 finishes it. A read match is counted only after its T3 completes. The next action never hides another clock.

After all three write/immediate-read pairs, a final console-stepped read-only retention pass reloads each HL address, clears A again and reads the stored byte without rewriting it. This can expose overwrites caused by aliases among these three addresses. A complete sequence has 182 full clock cycles, including six reset and two reset-exit clocks, and six verified read-backs. Completion asserts reset, isolates both data paths and leaves SRAM contents intact.

What the harness can observe

The Pico checks BUSACK#, IORQ#, RD# and WR# via U7, and only the address bits A0/A1/A2/A4 (mask 17). It checks those address bits at stable fetch, write and read points, not during refresh. It cannot read all sixteen address bits, MREQ#, actual SRAM CE#/OE#/WE#, or actual U9 OE#. Reported enable/inhibit values are Pico-side commands, not measurements of the downstream hardware.

Host tests decode the injected instructions into a simulated Z80/SRAM and check manual pulse counts, the exact three writes, six accumulator clears, immediate/final read-back, ownership isolation, fault stops and abort at every action. Those tests establish software sequencing, not physical reset-exit alignment, analogue levels, timing or full SRAM qualification.

Full-Memory Loader Qualification

First capture slow reset exit and injected 21/36/7E sequences with the analyzer, alongside scope measurements of CLK and SRAM controls. Confirm:

  • Six reset clocks and two reset-exit clocks align the first opcode fetch.
  • Opcode fetches have four T-states; operand reads have three.
  • Each slow clock leaves CLK LOW for at least 1 us before returning. Confirm injected data remains valid through RD# release before firmware isolates it.
  • SRAM OE stays HIGH throughout injected bytes.
  • Upward data is disabled before the CPU's memory-write cycle.
  • Readback enables SRAM only after the Pico stops driving.
  • After disabling both translators, allow at least 1 us before changing Pico data pins to outputs; verify U10 is actually high-impedance at that point.
  • SRAM write pulse, address/data setup/hold, and read sample position meet the exact Z80 and SRAM datasheets. Host tests are not this evidence.

p runs the full 64 KiB address-derived pattern and readback; c the complement; m a full March test. Each command initializes a fresh injected session and finishes with RESET asserted and the clock stopped. Save the first mismatch address/expected/actual; do not advance after an error. Start with the scope-connected current-limited supply and check loaded rail voltage and clock thresholds before long tests.

Done Command Required result
Before long tests Loaded rail voltage and clock thresholds checked
p Full 64 KiB address-derived pattern and readback pass repeatedly
c Complement pattern and readback pass repeatedly
m Full March test passes repeatedly

Pass Gate

  • Loader phase verified on real hardware.
  • All three RAM tests pass repeatedly, including addresses 0000/FFFF.
  • No overlapping data drive occurs.
  • Complete the CPU reset, fetch and bus-grant checks, including receiving-pin captures and safe grant/release fault behavior.
  • Save captures and cold-start results.

No normal-run MHz claim follows from slow injected tests alone.

Maintained Source

These complete files are included from the repository at documentation build time, not copied into this page.

Phase 6 application - src/stage06_sram_dma/main.c
#include <stdio.h>

#include "pico/stdlib.h"
#include "z80sbc/cpu.h"
#include "z80sbc/sram.h"
#include "z80sbc/supervisor.h"

int main(void) {
  z80_safe_startup();
  stdio_init_all();
  printf("\nStage 6: Z80-assisted SRAM tests; CPU and SRAM fitted\n");
  printf("p=pattern, c=complement pattern, m=March test\n");

  while (true) {
    int command = getchar_timeout_us(0);
    if (command == 'p' || command == 'c' || command == 'm') {
      if (!z80_cpu_prepare_loader()) {
        printf("FAIL: loader initialization\n");
        continue;
      }
    }
    if (command == 'p')
      printf(z80_sram_pattern_test(false) ? "PASS: pattern\n" : "FAIL\n");
    else if (command == 'c')
      printf(z80_sram_pattern_test(true) ? "PASS: complement\n" : "FAIL\n");
    else if (command == 'm')
      printf(z80_sram_march_test() ? "PASS: March\n" : "FAIL\n");
    if (command == 'p' || command == 'c' || command == 'm')
      z80_cpu_fail_closed();
    tight_loop_contents();
  }
}
Shared assisted loader and RAM tests - src/common/sram.c
#include "z80sbc/sram.h"

#include <stddef.h>
#include <stdio.h>

#include "pico/stdlib.h"
#include "z80sbc/bus.h"
#include "z80sbc/banked_transfer.h"
#include "z80sbc/banked_allocator.h"
#include "z80sbc/bank1_system.h"
#include "z80sbc/clock.h"
#include "z80sbc/pins.h"
#include "z80sbc/supervisor.h"

/* Re-read a mismatching byte: a value that changes between reads is a noisy
 * read; the same wrong value every time is a write that did not take. */
static void print_rereads(uint16_t address) {
  printf("[boot] re-read %04x:", address);
  for (unsigned i = 0; i < 4; ++i) {
    uint8_t value = 0;
    if (z80_sram_read_byte(address, &value))
      printf(" %02x", value);
  }
  printf("\n");
}

static bool report_failure(uint16_t address, uint8_t expected,
                           uint8_t actual) {
  printf("FAIL %04x expected=%02x actual=%02x\n", address, expected, actual);
  print_rereads(address);
  z80_isolate_buses();
  return false;
}

bool z80_sram_prepare_loader(void) {
  z80_isolate_buses();
  gpio_put(PIN_BOOT_READ_DISABLE, 1);
  gpio_put(PIN_BUSREQ_N, 1);
  z80_reset_with_clock_cycles(6, 1);
  z80_memory_bank_select(0);
  gpio_put(PIN_RESET_N, 1);
  z80_clock_one_cycle(1);
  z80_clock_one_cycle(1);
  return true;
}

enum { BANK_RUNTIME_BYTES = Z80_SRAM_BANK_RUNTIME_BYTES,
       BANK_LOAD_BYTES = Z80_SRAM_BANK_INIT_BYTES };

/* Offsets count initialized bytes, skipping the application heap. */
static uint16_t bank_load_address(unsigned offset) {
  return offset < BANK_RUNTIME_BYTES ? (uint16_t)offset :
      (uint16_t)(0xDE00u + offset - BANK_RUNTIME_BYTES);
}

static uint8_t bank_load_value(unsigned offset, const uint8_t *base_image) {
  if (offset < BANK_RUNTIME_BYTES) {
    switch (offset) {
      case 0: case 5: return 0xC3;
      case 1: return 0x40;
      case 2: return 0xFB;
      case 6: return 0x06;
      case 7: return 0xEE;
      default: return 0;
    }
  }
  unsigned address = 0xDE00u + offset - BANK_RUNTIME_BYTES;
  if (address < 0xE600u) return bank1_service_code[address - 0xDE00u];
  if (address < 0xEE00u) return bank1_resident_code[address - 0xE600u];
  if (address < 0xFB40u) return bank1_bdos_code[address - 0xEE00u];
  if (address >= 0xFC03u && address < 0xFC06u)
    return (const uint8_t[]){0xC3, 0x40, 0xFB}[address - 0xFC03u];
  return base_image[address];
}

static void bank_load_progress(const char *phase, unsigned completed) {
  printf("[boot] bank 1 SRAM %s: %u/%u\n", phase, completed,
         (unsigned)BANK_LOAD_BYTES);
}

void z80_sram_prepare_banked_memory(const uint8_t *base_image) {
  z80_memory_bank_set_available(false);
  z80_memory_bank_select(0);
  if (base_image == NULL) {
    printf("[boot] bank 1 SRAM skipped: no base image\n");
    return;
  }
  /* Bank 0 is already loaded and verified. Preserve its reset-vector byte
   * across the A16 probe, including the aliased-bank failure path. */
  uint8_t original = 0, actual = 0;
  if (!z80_sram_read_byte(0, &original)) {
    printf("[boot] bank 1 SRAM unavailable: cannot save bank 0 probe byte\n");
    return;
  }
  z80_sram_write_byte(0, 0x5A);
  z80_memory_bank_select(1);
  z80_sram_write_byte(0, 0xA5);
  z80_memory_bank_select(0);
  bool isolated = z80_sram_read_byte(0, &actual) && actual == 0x5A;
  if (!z80_sram_write_byte(0, original) ||
      !z80_sram_read_byte(0, &actual) || actual != original) {
    printf("[boot] bank 1 SRAM unavailable: bank 0 probe-byte restore failed\n");
    return;
  }
  if (!isolated) {
    printf("[boot] bank 1 SRAM unavailable: A16 isolation probe failed\n");
    return; /* Aliased SRAM: retain 64 KiB operation. */
  }
  z80_memory_bank_select(1);
  bool valid = false;
  /* Write the complete runtime and mirrored resident page before readback,
   * just as the base-bank loader separates its load and verify passes. */
  for (unsigned offset = 0; offset < BANK_LOAD_BYTES; ++offset) {
    if (offset % 1024u == 0)
      bank_load_progress("load", offset);
    uint16_t address = bank_load_address(offset);
    uint8_t expected = bank_load_value(offset, base_image);
    if (!z80_sram_write_byte(address, expected)) {
      printf("[boot] bank 1 SRAM load FAIL: address=%04x expected=%02x\n",
             address, expected);
      goto finish_bank;
    }
  }
  bank_load_progress("load", BANK_LOAD_BYTES);
  for (unsigned offset = 0; offset < BANK_LOAD_BYTES; ++offset) {
    if (offset % 1024u == 0)
      bank_load_progress("verify", offset);
    uint16_t address = bank_load_address(offset);
    uint8_t expected = bank_load_value(offset, base_image);
    if (!z80_sram_read_byte(address, &actual)) {
      printf("[boot] bank 1 SRAM verify FAIL: address=%04x read failed\n", address);
      goto finish_bank;
    }
    if (actual != expected) {
      printf("[boot] bank 1 SRAM verify FAIL: address=%04x expected=%02x actual=%02x\n",
             address, expected, actual);
      print_rereads(address);
      goto finish_bank;
    }
  }
  bank_load_progress("verify", BANK_LOAD_BYTES);
  valid = true;
finish_bank:
  z80_memory_bank_select(0);
  z80_memory_bank_set_available(valid);
  if (valid)
    printf("[boot] bank 1 SRAM verified: BDOS, loader and extent allocator ready\n");
  else
    printf("[boot] bank 1 SRAM disabled: load/verify failed\n");
}

static void pulse_clock(unsigned cycles) {
  for (unsigned cycle = 0; cycle < cycles; ++cycle)
    z80_clock_one_cycle(1);
}

static void inject_read(uint8_t value, bool opcode) {
  pulse_clock(1);
  gpio_put(PIN_BOOT_READ_DISABLE, 1);
  busy_wait_us_32(1);
  z80_data_bus_drive(value);
  pulse_clock(2);
  z80_data_bus_isolate();
  if (opcode)
    pulse_clock(1);
}

static void load_hl(uint16_t address) {
  inject_read(0x21, true);
  inject_read((uint8_t)address, false);
  inject_read((uint8_t)(address >> 8), false);
}

bool z80_sram_write_byte(uint16_t address, uint8_t value) {
  load_hl(address);
  inject_read(0x36, true);
  inject_read(value, false);
  pulse_clock(3);
  return true;
}

bool z80_sram_read_byte(uint16_t address, uint8_t *value) {
  if (value == NULL)
    return false;

  load_hl(address);
  inject_read(0x7E, true);
  z80_data_bus_prepare_input();
  gpio_put(PIN_BOOT_READ_DISABLE, 0);
  pulse_clock(2);
  busy_wait_us_32(1);
  *value = z80_data_bus_sample();
  pulse_clock(1);
  z80_data_bus_isolate();
  gpio_put(PIN_BOOT_READ_DISABLE, 1);
  return true;
}

bool z80_sram_load(uint16_t address, const uint8_t *data, uint32_t length) {
  if (data == NULL || length > 65536u - address)
    return false;
  for (uint32_t index = 0; index < length; ++index) {
    if (!z80_sram_write_byte((uint16_t)(address + index), data[index]))
      return false;
  }
  z80_isolate_buses();
  return true;
}

bool z80_sram_verify(uint16_t address, const uint8_t *data, uint32_t length) {
  if (data == NULL || length > 65536u - address)
    return false;
  for (uint32_t index = 0; index < length; ++index) {
    uint8_t actual = 0;
    if (!z80_sram_read_byte((uint16_t)(address + index), &actual) ||
        actual != data[index])
      return report_failure((uint16_t)(address + index), data[index], actual);
  }
  z80_isolate_buses();
  return true;
}

bool z80_sram_pattern_test(bool complement) {
  for (uint32_t address = 0; address < 65536u; ++address) {
    uint8_t expected = (uint8_t)address ^ (uint8_t)(address >> 8);
    if (complement)
      expected = (uint8_t)~expected;
    if (!z80_sram_write_byte((uint16_t)address, expected))
      return false;
  }
  for (uint32_t address = 0; address < 65536u; ++address) {
    uint8_t expected = (uint8_t)address ^ (uint8_t)(address >> 8);
    uint8_t actual = 0;
    if (complement)
      expected = (uint8_t)~expected;
    if (!z80_sram_read_byte((uint16_t)address, &actual) || actual != expected)
      return report_failure((uint16_t)address, expected, actual);
  }
  z80_isolate_buses();
  return true;
}

static bool march_read_write(uint16_t address, uint8_t expected,
                             uint8_t replacement) {
  uint8_t actual = 0;
  if (!z80_sram_read_byte(address, &actual) || actual != expected)
    return report_failure(address, expected, actual);
  return z80_sram_write_byte(address, replacement);
}

bool z80_sram_march_test(void) {
  for (uint32_t address = 0; address < 65536u; ++address) {
    if (!z80_sram_write_byte((uint16_t)address, 0x00))
      return false;
  }
  for (uint32_t address = 0; address < 65536u; ++address) {
    if (!march_read_write((uint16_t)address, 0x00, 0xFF))
      return false;
  }
  for (uint32_t address = 0; address < 65536u; ++address) {
    if (!march_read_write((uint16_t)address, 0xFF, 0x00))
      return false;
  }
  for (uint32_t address = 65536u; address-- > 0;) {
    if (!march_read_write((uint16_t)address, 0x00, 0xFF))
      return false;
  }
  for (uint32_t address = 65536u; address-- > 0;) {
    if (!march_read_write((uint16_t)address, 0xFF, 0x00))
      return false;
  }
  for (uint32_t address = 0; address < 65536u; ++address) {
    uint8_t actual = 0;
    if (!z80_sram_read_byte((uint16_t)address, &actual) || actual != 0x00)
      return report_failure((uint16_t)address, 0x00, actual);
  }
  z80_isolate_buses();
  return true;
}
Console-stepped harness - src/stage06_sram_dma/step_loader.c
#include <stdio.h>

#include "full_memory_test.h"
#include "pico/stdlib.h"
#include "z80sbc/bus.h"
#include "z80sbc/clock.h"
#include "z80sbc/cpu.h"
#include "z80sbc/pins.h"
#include "z80sbc/supervisor.h"

typedef struct {
  uint16_t address;
  uint8_t value;
} test_byte_t;

static const test_byte_t TEST_BYTES[] = {
  {0x0000, 0x00},
  {0x0001, 0x01},
  {0x0002, 0x02},
};

typedef enum {
  OPCODE,
  OPERAND,
  CPU_WRITE,
  SRAM_READ,
} transfer_kind_t;

typedef struct {
  transfer_kind_t kind;
  uint8_t byte;
  const char *description;
  const char *expected;
} transfer_t;

/* The final retention pass skips transfers 3-5; it must not rewrite SRAM. */
static const transfer_t TRANSFERS[] = {
  {OPCODE, 0x21, "LD HL,nn: opcode",
   "Z80 fetches injected 21; SRAM output inhibited"},
  {OPERAND, 0, "LD HL,nn: address LOW",
   "Z80 fetches target address low byte"},
  {OPERAND, 0, "LD HL,nn: address HIGH",
   "Z80 fetches target address high byte; HL becomes target"},
  {OPCODE, 0x36, "LD (HL),n: opcode",
   "Z80 fetches injected 36; no SRAM write yet"},
  {OPERAND, 0, "LD (HL),n: test byte",
   "Z80 fetches test byte; no SRAM write yet"},
  {CPU_WRITE, 0, "LD (HL),n: Z80 writes SRAM",
   "Z80 alone drives target address and data; SRAM OE# HIGH"},
  {OPCODE, 0x3E, "LD A,00: opcode",
   "Z80 fetches injected 3E; does not write SRAM"},
  {OPERAND, 0x00, "LD A,00: zero operand",
   "Z80 accumulator becomes 00; no separate physical data-bus clear"},
  {OPCODE, 0x7E, "LD A,(HL): opcode",
   "Z80 fetches injected 7E; HL still holds original target"},
  {SRAM_READ, 0, "LD A,(HL): Z80 reads SRAM",
   "SRAM alone drives test byte; Z80 reads it; Pico observes via U10"},
};

typedef enum {
  STOPPED,
  RESET_CLOCK,
  RELEASE_RESET,
  RESET_EXIT,
  SETUP,
  T1,
  DRIVE,
  T2,
  T3,
  T4,
  COMPLETE,
} phase_t;

enum {
  TEST_COUNT = sizeof(TEST_BYTES) / sizeof(TEST_BYTES[0]),
  TRANSFER_COUNT = sizeof(TRANSFERS) / sizeof(TRANSFERS[0]),
  STEP_HALF_PERIOD_US = 1000,
};

static phase_t phase = STOPPED;
static unsigned reset_clocks;
static unsigned exit_clocks;
static unsigned total_clocks;
static unsigned test_index;
static unsigned transfer_index;
static unsigned verified_reads;
static uint16_t injected_pc;
static bool retention;
static int pending_command;
static bool invalid_line;

static bool injected(void) {
  return TRANSFERS[transfer_index].kind == OPCODE ||
         TRANSFERS[transfer_index].kind == OPERAND;
}

static uint8_t transfer_byte(void) {
  if (transfer_index == 1)
    return (uint8_t)TEST_BYTES[test_index].address;
  if (transfer_index == 2)
    return (uint8_t)(TEST_BYTES[test_index].address >> 8);
  if (transfer_index == 4 || transfer_index == 5 || transfer_index == 9)
    return TEST_BYTES[test_index].value;
  return TRANSFERS[transfer_index].byte;
}

static void safe_stop(void) {
  z80_cpu_fail_closed();
  phase = STOPPED;
}

static void print_observed(void) {
  printf("OBSERVED via U7: BUSACK#=%u IORQ#=%u RD#=%u WR#=%u "
         "A[4,2,1,0]=%02x (mask 17 only)\n",
         (unsigned)gpio_get(PIN_BUSACK_N), (unsigned)gpio_get(PIN_IORQ_N),
         (unsigned)gpio_get(PIN_RD_N), (unsigned)gpio_get(PIN_WR_N),
         z80_port_bus_sample());
  printf("PICO commands: CLK=%u RESET#=%u inhibit=%u UP_OE#=%u DOWN_OE#=%u\n",
         (unsigned)gpio_get(PIN_CLK), (unsigned)gpio_get(PIN_RESET_N),
         (unsigned)gpio_get(PIN_BOOT_READ_DISABLE),
         (unsigned)gpio_get(PIN_DATA_UP_OE_N),
         (unsigned)gpio_get(PIN_DATA_DOWN_OE_N));
  printf("Full A0-A15, MREQ#, actual SRAM CE#/OE#/WE# and actual U9 OE#\n"
         "are NOT monitored by Pico; verify at physical pins.\n");
}

static void fail(const char *reason) {
  printf("\nFAIL: %s; clock=%u target=%04x expected=%02x\n",
         reason, total_clocks, TEST_BYTES[test_index].address,
         TEST_BYTES[test_index].value);
  print_observed();
  safe_stop();
  printf("STOPPED: both data paths off, RESET# LOW, CLK LOW, inhibit HIGH.\n"
         "Record the failure. Do not continue; fix wiring/timing then b.\n");
}

static bool check_controls(bool rd_high, bool wr_high, bool check_address) {
  bool controls_ok = gpio_get(PIN_BUSACK_N) && gpio_get(PIN_IORQ_N) &&
                     gpio_get(PIN_RD_N) == rd_high &&
                     gpio_get(PIN_WR_N) == wr_high;
  uint16_t expected_address = injected() ? injected_pc :
                                         TEST_BYTES[test_index].address;
  uint8_t actual_address = z80_port_bus_sample();
  if (!controls_ok ||
      (check_address &&
       actual_address != (expected_address & Z80_PORT_ADDRESS_MASK))) {
    printf("EXPECTED: BUSACK#=1 IORQ#=1 RD#=%u WR#=%u",
           (unsigned)rd_high, (unsigned)wr_high);
    if (check_address)
      printf(" address=%04x masked=%02x", expected_address,
             expected_address & Z80_PORT_ADDRESS_MASK);
    printf("\n");
    fail("control or monitored-address mismatch");
    return false;
  }
  return true;
}

static void print_help(void) {
  printf("\nSTAGE 6 CONSOLE-STEPPED Z80 SRAM LOADER HARNESS\n"
         "Requires ALL eight active packages fitted, powered and checked.\n"
         "Destructive test: 0000=00, 0001=01, 0002=02; no scratch writes.\n"
         "Mimics CP/M assisted loading; does NOT load or execute CP/M.\n"
         "Per byte: LD HL,nn; LD (HL),n; LD A,00; LD A,(HL).\n"
         "Then final read-only retention pass over all three addresses.\n"
         "LD A,00 clears A, NOT a held physical data-bus value.\n"
         "Its injected 00 operand drives zero only during that fetch.\n"
         "A is not directly observable; reports distinguish expectation\n"
         "from data-bus samples. Three bytes do not qualify all of SRAM.\n"
         "ONE letter then Enter (CR/LF/CRLF); no pasting or held keys:\n"
         "  b = begin/restart, held in reset with NO automatic clocks\n"
         "  f = automatic full 64 KiB test: fill/read low-byte, then high-byte\n"
         "  n = next action, at most ONE full clock cycle, ending LOW\n"
         "  r = repeat status/instructions, NO clocks or ownership changes\n"
         "  h = help; x = immediate abort, no Enter required\n"
         "No-clock actions explicitly handle RESET and data ownership.\n"
         "Each clock uses 1ms half-periods; meter sees held LOW clock,\n"
         "not its pulse amplitude. Read/write T2 stops hold valid data.\n"
         "Power: external 5 V BEFORE USB; disconnect USB BEFORE 5 V.\n"
         "Never use USB alone, bridge pins, or force a CPU output LOW.\n"
         "Use DC volts and common-GND black lead; power off to move clips.\n"
         "PROBE MAP (IC package pin numbers, not breadboard rows):\n"
         "  U1 Z80: CLK=6 RESET#=26; U2 SRAM: CE#=22 OE#=24 WE#=29.\n"
         "  U2 address: A0=12 A1=11 A2=10 A3=9 A4=8 A5=7 A6=6 A7=5\n"
         "              A8=27 A9=26 A10=23 A11=25 A12=4 A13=28 A14=3 A15=31.\n"
         "  U2 data: D0=13 D1=14 D2=15 D3=17 D4=18 D5=19 D6=20 D7=21.\n"
         "  U9 actual OE#=19 (RD# OR GP6); U10 actual OE#=19 (GP7).\n"
         "  At held write T2: CE# LOW, WE# LOW, OE# HIGH; CPU drives data.\n"
         "  At held SRAM read T2: CE# LOW, WE# HIGH, OE# LOW; SRAM drives.\n"
         "  HIGH/LOW are expected logic levels, not measured voltages.\n");
}

static void print_bits(const char *label, uint16_t value, unsigned count) {
  printf("%s", label);
  for (unsigned bit = count; bit > 0; --bit)
    printf("%u", (unsigned)((value >> (bit - 1)) & 1u));
  printf("\n");
}

static void print_status(void) {
  static const char *const PHASE_NAMES[] = {
    "STOPPED", "reset clock", "release RESET", "reset exit",
    "ownership setup", "T1", "inject after T1", "T2", "T3", "T4", "COMPLETE",
  };
  printf("\nSTATE next=%s clocks=%u verified_reads=%u/6\n",
         PHASE_NAMES[phase], total_clocks, verified_reads);
  print_observed();
  if (phase == STOPPED) {
    printf("Held in reset, data paths off. b + Enter arms the sequence.\n");
  } else if (phase == COMPLETE) {
    printf("PASS: three immediate and three final SRAM bus read-backs.\n"
           "RESET# LOW, clock stopped; test addresses retain their bytes.\n"
           "This is not full address-bus, CPU-register or timing proof.\n");
  } else if (phase == RESET_CLOCK) {
    printf("NEXT n: reset clock %u/6; RESET# stays LOW; data paths off.\n",
           reset_clocks + 1);
  } else if (phase == RELEASE_RESET) {
    printf("NEXT n: check idle controls, then release RESET# while CLK LOW;\n"
           "NO clock. Six reset clocks already completed.\n");
  } else if (phase == RESET_EXIT) {
    printf("NEXT n: reset-exit alignment clock %u/2; SRAM inhibited.\n",
           exit_clocks + 1);
  } else {
    const transfer_t *transfer = &TRANSFERS[transfer_index];
    printf("%s pair %u/3: [%04x]=%02x; injected PC=%04x\n"
           "TRANSFER: %s\nEXPECTED: %s\n",
           retention ? "FINAL RETENTION" : "WRITE / IMMEDIATE READ",
           test_index + 1, TEST_BYTES[test_index].address,
           TEST_BYTES[test_index].value, injected_pc,
           transfer->description, transfer->expected);
    if (injected())
      printf("Injected byte=%02x; fetch address=%04x, NOT necessarily HL.\n",
             transfer_byte(), injected_pc);
    switch (phase) {
    case SETUP:
      printf("NEXT n: NO clock; %s.\n",
             injected() ? "inhibit SRAM and isolate both data paths" :
             transfer->kind == CPU_WRITE ?
                 "inhibit SRAM output; configure U10 input to observe Z80 write" :
                 "isolate upward drive; configure U10 input; enable SRAM reads");
      break;
    case T1:
      printf("NEXT n: clock T1; target/fetch address and bus controls assert.\n");
      break;
    case DRIVE:
      printf("NEXT n: NO clock; check fetch controls/address, then inject %02x\n"
             "through U9. SRAM output MUST stay inhibited; U10 off.\n",
             transfer_byte());
      break;
    case T2:
      printf("NEXT n: clock T2; expected RD#=%u WR#=%u, address=%04x.\n",
             (unsigned)(transfer->kind == CPU_WRITE),
             (unsigned)(transfer->kind != CPU_WRITE),
             injected() ? injected_pc : TEST_BYTES[test_index].address);
      if (!injected())
        printf("Sample physical data bus via U10; expected=%02x.\n",
               transfer_byte());
      break;
    case T3:
      printf("HELD after T2: %s; expected bus=%02x.\n"
             "NEXT n: clock T3 to finish this transfer, then isolate data.\n",
             injected() ? "U9 supplies injected byte (command, not read-back)" :
             transfer->kind == CPU_WRITE ? "Z80 drives, SRAM write active" :
                                          "SRAM drives, Z80 read active",
             transfer_byte());
      printf("Probe SRAM A0-A15 for %04x and D0-D7 for %02x now;\n"
             "see h for physical pins. 1=HIGH, 0=LOW (logic expectations).\n",
             injected() ? injected_pc : TEST_BYTES[test_index].address,
             transfer_byte());
      print_bits("EXPECTED A15..A0 = ",
                 injected() ? injected_pc : TEST_BYTES[test_index].address, 16);
      print_bits("EXPECTED D7..D0 = ", transfer_byte(), 8);
      break;
    case T4:
      printf("NEXT n: opcode T4 refresh clock; U9/U10 off, SRAM inhibited.\n"
             "Address pins may show refresh address, NOT the test target.\n");
      break;
    default:
      break;
    }
    printf("RD#/WR# are active LOW. Full write/read address and SRAM pins\n"
           "must be checked externally; U7 sees only A0/A1/A2/A4.\n");
  }
  printf("n + Enter advances; r repeats; x stops immediately.\n");
}

static void begin_test(void) {
  safe_stop();
  reset_clocks = exit_clocks = total_clocks = 0;
  test_index = transfer_index = verified_reads = 0;
  injected_pc = 0;
  retention = false;
  phase = RESET_CLOCK;
  printf("\nARMED: existing SRAM will be overwritten ONLY at 0000/0001/0002.\n");
  print_status();
}

static void clock_once(void) {
  z80_clock_one_cycle(STEP_HALF_PERIOD_US);
  ++total_clocks;
}

static void finish_transfer(void) {
  if (injected())
    ++injected_pc;
  if (transfer_index == 7)
    printf("EXPECTED CPU A=00 after LD A,00; register not directly measured.\n");
  ++transfer_index;
  if (retention && transfer_index == 3)
    transfer_index = 6;
  if (transfer_index == TRANSFER_COUNT) {
    transfer_index = 0;
    ++test_index;
    if (test_index == TEST_COUNT) {
      test_index = 0;
      if (retention) {
        safe_stop();
        phase = COMPLETE;
        return;
      }
      retention = true;
      printf("\nAll writes done. FINAL RETENTION: reread every location;\n"
             "do NOT rewrite memory. This can expose aliases among these bytes.\n");
    }
  }
  phase = SETUP;
}

static void next_action(void) {
  if (phase == STOPPED || phase == COMPLETE) {
    printf("No advance: use b + Enter to begin/restart.\n");
    return;
  }
  switch (phase) {
  case RESET_CLOCK:
    clock_once();
    if (++reset_clocks == 6)
      phase = RELEASE_RESET;
    break;
  case RELEASE_RESET:
    if (!check_controls(true, true, false))
      return;
    gpio_put(PIN_RESET_N, 1);
    busy_wait_us_32(1);
    phase = RESET_EXIT;
    break;
  case RESET_EXIT:
    clock_once();
    if (++exit_clocks == 2)
      phase = SETUP;
    break;
  case SETUP:
    z80_data_bus_isolate();
    gpio_put(PIN_BOOT_READ_DISABLE, 1);
    if (!injected()) {
      z80_data_bus_prepare_input();
      if (TRANSFERS[transfer_index].kind == SRAM_READ)
        gpio_put(PIN_BOOT_READ_DISABLE, 0);
    }
    busy_wait_us_32(1);
    phase = T1;
    break;
  case T1:
    clock_once();
    phase = injected() ? DRIVE : T2;
    break;
  case DRIVE:
    if (!check_controls(false, true, true))
      return;
    z80_data_bus_drive(transfer_byte());
    phase = T2;
    break;
  case T2: {
    clock_once();
    bool writing = TRANSFERS[transfer_index].kind == CPU_WRITE;
    if (!check_controls(writing, !writing, true))
      return;
    if (!injected()) {
      uint8_t actual = z80_data_bus_sample();
      printf("%s BUS SAMPLE: address=%04x expected=%02x actual=%02x\n",
             writing ? "Z80 WRITE" : "SRAM READ",
             TEST_BYTES[test_index].address, transfer_byte(), actual);
      if (actual != transfer_byte()) {
        fail(writing ? "Z80 write data mismatch" : "SRAM read-back mismatch");
        return;
      }
      printf("MATCH: %s; %s\n",
             writing ? "CPU-driven write data" : "SRAM-driven read data",
             writing ? "write not complete until T3" :
                       "CPU read not complete until T3");
    }
    phase = T3;
    break;
  }
  case T3:
    clock_once();
    if (!check_controls(true, true, false))
      return;
    z80_data_bus_isolate();
    gpio_put(PIN_BOOT_READ_DISABLE, 1);
    if (TRANSFERS[transfer_index].kind == SRAM_READ)
      ++verified_reads;
    if (TRANSFERS[transfer_index].kind == OPCODE)
      phase = T4;
    else
      finish_transfer();
    break;
  case T4:
    clock_once();
    if (!check_controls(true, true, false))
      return;
    finish_transfer();
    break;
  default:
    fail("invalid step state");
    return;
  }
  print_status();
}

static void run_command(int command) {
  if (z80_full_memory_test_active()) {
    printf("BUSY: automatic full-memory test; x aborts between bytes.\n");
    return;
  }
  if (command == 'f') {
    safe_stop();
    total_clocks = verified_reads = 0;
    z80_full_memory_test_start();
  } else if (command == 'b')
    begin_test();
  else if (command == 'n')
    next_action();
  else if (command == 'r')
    print_status();
  else if (command == 'h') {
    print_help();
    print_status();
  } else
    printf("ERROR: unknown command; outputs unchanged. Use h + Enter.\n");
}

static void handle_character(int character) {
  if (character == 'x') {
    safe_stop();
    pending_command = 0;
    invalid_line = true;
    if (z80_full_memory_test_active())
      z80_full_memory_test_abort("operator");
    else
      printf("\nABORTED: RESET# LOW, CLK LOW, inhibit HIGH, data paths off.\n");
  } else if (character == '\r' || character == '\n') {
    if (invalid_line)
      printf("Line discarded. Use one command letter then Enter.\n");
    else if (pending_command)
      run_command(pending_command);
    pending_command = 0;
    invalid_line = false;
  } else if (pending_command || invalid_line) {
    invalid_line = true;
  } else {
    pending_command = character;
  }
}

int main(void) {
  z80_safe_startup();
  safe_stop();
  stdio_init_all();
  while (true) {
    while (!stdio_usb_connected())
      tight_loop_contents();
    pending_command = 0;
    invalid_line = false;
    printf("\nZ80 SRAM diagnostic: RESET# LOW; all ICs required.\n"
           "Console connected. Waiting for a command; no test has started.\n"
           "f + Enter = automatic full 64 KiB low/high-byte test; x = abort\n"
           "b + Enter = three-byte single-step test; h + Enter = full help\n"
           "Destructive test. External 5 V before USB; no USB-only power.\n");
    while (stdio_usb_connected()) {
      int character = getchar_timeout_us(0);
      if (character >= 0)
        handle_character(character);
      if (z80_full_memory_test_active()) {
        if (!stdio_usb_connected())
          break;
        z80_full_memory_test_tick();
      }
      tight_loop_contents();
    }
    safe_stop();
    z80_full_memory_test_abort("console disconnected");
  }
}
Automatic full-memory test - src/stage06_sram_dma/full_memory_test.c
#include "full_memory_test.h"

#include <stdint.h>
#include <stdio.h>

#include "z80sbc/cpu.h"
#include "z80sbc/sram.h"

enum { MEMORY_SIZE = 65536 };

static bool active;
static bool verifying;
static unsigned pattern;
static uint32_t address;
static unsigned successful_checks;
static unsigned failed_checks;

static uint8_t expected_byte(void) {
  return pattern == 0 ? (uint8_t)address : (uint8_t)(address >> 8);
}

static void stop(void) {
  z80_cpu_fail_closed();
  active = false;
}

static void print_stats(void) {
  printf("STATS: successes=%u failures=%u\n", successful_checks, failed_checks);
}

static void start_pattern(void) {
  address = 0;
  verifying = false;
  if (!z80_cpu_prepare_loader()) {
    ++failed_checks;
    stop();
    printf("FAIL: loader initialization for pass %u/2\n", pattern + 1);
    print_stats();
    return;
  }
  active = true;
  printf("pass %u/2: writing 0000-ffff, pattern=address %s byte; x aborts\n",
         pattern + 1, pattern == 0 ? "low" : "high");
}

void z80_full_memory_test_start(void) {
  stop();
  pattern = 0;
  successful_checks = failed_checks = 0;
  start_pattern();
}

bool z80_full_memory_test_active(void) {
  return active;
}

void z80_full_memory_test_abort(const char *reason) {
  if (!active)
    return;
  stop();
  printf("ABORTED: %s; RESET# LOW, SRAM inhibited, data paths off\n", reason);
  print_stats();
}

void z80_full_memory_test_tick(void) {
  if (!active)
    return;

  uint8_t expected = expected_byte();
  if (!verifying) {
    if (!z80_sram_write_byte((uint16_t)address, expected)) {
      ++failed_checks;
      stop();
      printf("address %04x, value ERROR, expected %02x - FAIL (write)\n",
             (unsigned)address, expected);
      print_stats();
      return;
    }
  } else {
    uint8_t actual;
    if (!z80_sram_read_byte((uint16_t)address, &actual)) {
      ++failed_checks;
      stop();
      printf("address %04x, value ERROR, expected %02x - FAIL (read)\n",
             (unsigned)address, expected);
      print_stats();
      return;
    }
    bool matches = actual == expected;
    if (matches)
      ++successful_checks;
    else {
      ++failed_checks;
      stop();
    }
    printf("address %04x, value %02x, expected %02x - %s\n",
           (unsigned)address, actual, expected, matches ? "SUCCESS" : "FAIL");
    if (!matches) {
      print_stats();
      return;
    }
  }

  if (++address != MEMORY_SIZE)
    return;
  if (!verifying) {
    address = 0;
    verifying = true;
    printf("pass %u/2: reading 0000-ffff\n", pattern + 1);
  } else if (pattern == 0) {
    pattern = 1;
    start_pattern();
  } else {
    stop();
    printf("SUCCESS: 65536 addresses verified in each of 2 passes; RESET# LOW\n");
    print_stats();
  }
}