Skip to content

Latest commit

 

History

History
903 lines (704 loc) · 52.4 KB

SPEC.md

File metadata and controls

903 lines (704 loc) · 52.4 KB

DRAMscope — engineering specification

⚠ The name is a placeholder. It is used consistently so it can be changed with one sed, but it should be settled before anything is published.

⚠ Every factual claim in this document must carry a source. The keyed source list and the claim-by-claim detail live in PROVENANCE.md, which also records the statements that were invented and later corrected. [S#] keys below refer to that list; [M] means measured by this project, and the measuring artefact is named with the claim.

Status: iteration 5 implemented and verified — EasyFlash delivery (boots in Ultimax; ⚠ it it starts with no working RAM and no working KERNAL — ⚠ measured on a machine with the DRAMs removed, and the launcher qualification is in G6), engine relocated to $C000, P0/P1/P2 with all sixteen address lines, P3 March B 17n, P4 March LR 14n and P5 topographical patterns over 59,648 of 65,536 bytes, plus chip naming from the failing-bit mask. Verified on an Ultimate II+ and a Kung Fu Flash as well as in VICE. Measured end to end: 58–61 M cycles, about 62 s on PAL. P6 onwards not started. Target: Commodore 64, all assemblies. Not specific to any one board or RAM replacement.


0. What this is

A memory diagnostic cartridge for the Commodore 64 that does three things existing tools do not do together:

  1. Tests beyond the standard march algorithms — linked faults, physical row/column coupling, and retention under load.
  2. Names the faulty part, rather than reporting an address and a bit.
  3. Shows a live map of the whole address space, so a fault's shape is visible — which is usually more diagnostic than its address.

What this is not

⚠ It is not a replacement for Dead Test, and it does not compete with DesTestMAX.

Dead Test is the right first move on a machine that will not boot: it is proven, it is everywhere, and it needs no working RAM. DesTestMAX and MAX-Switch are mature, thorough, and MAX-Switch's hardware mode-switching solves the 4 KB problem properly.

This tool exists to add three things to that landscape, not to displace any of it:

Covered by existing tools Added here
Stuck-at, transition, unlinked coupling ✅ March B —
Linked faults ❌ March LR
Physical row/column coupling ❌ logical addressing only Topographical patterns
Retention / marginal cells ❌ Dwell and disturb phases
Naming the failing chip ❌ Board profiles
Whole-map visual ❌ 256-page live grid

Credit to prior art belongs in the README and on the title screen, and it should be generous. See PROVENANCE.md for the rule about what may and may not be looked at.


1. Design principles

P1. The runtime is itself a check. Publish expected durations per phase. A run that completes much faster than documented is a stale build or a skipped phase, and that has already caught a real defect in the sibling project.

P2. Every gate must be able to fail. No phase ships without a fault-injection build that proves it reports the fault it claims to catch. A test that cannot fail is decoration.

⚠⚠ ONE INJECTION MECHANISM, NOT TEN. Every mutation used to be hand-written inline at its own site, and three of them damaged the thing they were meant to observe: INJECT_LR clobbered the Z flag March LR's bare r 0 branches on and reported 59,416 errors instead of 1; INJECT_ONCE loaded the pass counter over the byte just read and fired 53,294 times; INJECT_ZP pushed onto the stack page it was marching. A mutation that perturbs the program is not testing the program. There is now one hook body, a macro instantiated twice (the engine's copy cannot serve the handover, which marches $C000-$CFFF out from under it), with an explicit contract: A is the byte read, X and Y preserved, the stack balanced, and the flags set from A on every path.

⚠⚠ AND THE RESULT MUST BE PLAUSIBLE, NOT MERELY PRESENT. A mutation named "one bad byte" that reports 53,294 errors is wrong on its face, and the suite accepted it because it was only ever asked whether some cell said X. Every case must now declare errcount and redpages, and a case that omits either fails the harness rather than passing it. The screen figure must agree with the counter in memory.

⚠⚠ AND THE WHOLE SCREEN IS COMPARED. test/golden/*.txt holds the exact 25×40 screen for the clean run and every fault build. This is the strongest assertion in the suite and the one that was missing: every other check names a cell someone thought of, while this one notices any change to what the user sees. Four defects shipped that it would have caught — a lost coverage figure, a label running into its own status cell, a phase that was never marked, and a status column that forgot a fault. Regenerate deliberately with python3 test/check.py --bless; the diff shows exactly what moved.

⚠⚠ AND EVERY ASSERTION MUST BE READ. test/check.py once populated its observed values inside a conditional and then compared by iterating the observed dictionary — so a case that asserted a key the reader never filled had that assertion silently dropped. Five cases were affected and one of them was hiding a genuinely broken mutation for four commits. The comparison now iterates the expected dictionary, and every key has a reader, so an assertion that is not read is a KeyError rather than a pass.

P3. No verdict without evidence. The classifier may only state conclusions derivable from measurements the run actually made. See PROVENANCE.md, closing section.

P4. Honest coverage claims. Where a phase covers less than its name suggests — and P5 does — the documentation says so in the same place it makes the claim.

P5. Degrade visibly, not silently. If the display cannot be trusted because its own memory is faulty, the tool must say so rather than show a plausible-looking screen.

P7. One pass is a spot check; a burn-in is a test. ⚠ Carl, 2026-09-26: "the RAM test runs once only. Typically ram tests have a burn in whereby they cycle and count the number of cycles." The phases loop back to P1 rather than halting, and every result is cumulative: the map's red cells, the bad-byte count and the failing-bit mask all persist, and the liveness pulse turns red permanently once anything has failed. ⚠ Each phase used to zero its own results on entry — correct for a single run, and wrong the instant it looped, because pass two wiped pass one's findings. The counters are now cleared exactly once, in eng_start.

P8. ⚠ THE FLASH RATE IS A SAFETY REQUIREMENT, NOT A DESIGN PREFERENCE. Carl, 2026-09-26: "we don't want to risk it affecting anyone with epilepsy." WCAG 2.3.1 allows at most three flashes per second in anything occupying more than 25 % of the visual field, and the C64 border qualifies. Both liveness indicators are therefore driven by a CIA timer, not by a tick count, so the rate cannot vary with how fast a phase happens to run:

CIA2 timer A, latch $FFFF on φ2 underflows every 65,536 cycles
CIA2 timer B counts those underflows bit 3 flips every 8 decrements
One full on/off cycle 1,048,576 cycles
PAL 1.064 s → 0.94 Hz
NTSC 1.025 s → 0.98 Hz
WCAG limit 3.00 Hz — 3.2× margin

⚠ The previous tick-based version measured 1.81 Hz in P4/P5 and calculated near 2.5 Hz in P7's short passes, with nothing in the design stopping a future phase from crossing 3 Hz. That is the failure mode this replaces: not a rate that was wrong, but a rate that was unbounded.

The spinner runs at 7.5 changes/s but occupies one character cell — 0.1 % of the display, far below the 25 %-of-field threshold, and is a shape change rather than a luminance flash.

P10. Showing progress and remembering faults are different jobs. ⚠ Asked whether the phase statuses should reset between passes, the first answer here was "no, because the burn-in must not forget anything" — which conflated two separable properties. Passed phases do reset to .. at the start of each pass, so the column shows progress through the current pass; failures do not, ever. A wall of OK left over from pass 1 gives a watcher nothing, and .. truthfully means "not run yet, this pass".

P9. Nothing the burn-in has learned may be forgotten. The map's red cells, the bad-byte count, the failing-bit mask, the border pulse and the phase checklist all persist for the life of the run. ⚠ This has now been got wrong twice in the same way — once on the map (fixed by mark_page_keep) and once on the checklist, where cumulative error counts meant a phase that failed in pass 1 showed no delta in pass 2 and had its X overwritten with OK. Both were invisible to every mutation in the suite, because all of them fire on every pass. INJECT_ONCE exists to model the one thing they could not: a fault that happens once and never again.

P6. A working run must not look like a hung one. ⚠ Carl, 2026-09-26: "whilst the crt is running it is impossible to know whether it is proceeding or crashed." A run takes about a minute and the marches spend ~20 s stretches with nothing on screen changing. Every long phase therefore drives a spinner and a border pulse, and a static border from P1 onwards is itself the fault report. The harness asserts the tick counter is non-zero, because the final phase text blanks the spinner cell and the screen alone cannot prove it ever ran.


2. The two problems that shape the architecture

2.1 The screen is made of the thing under test

The VIC fetches its character matrix — 1000 bytes — from DRAM. Anything displayed therefore occupies memory that cannot simultaneously be under test. This is the same trap that caught an early version of the sibling project's test, which could not test the block it was running from.

Three facts make it tractable:

  • Colour RAM ($D800–$DBFF) is a separate 1K × 4 static chip. It is not part of the 64 KB, it is not under test, and it is always available. A display encoded in colour keeps working even over a region whose character matrix is full of test patterns.
  • The character generator is ROM, fetched by the VIC at $1000–$1FFF and $9000–$9FFF of its own address space. No DRAM required.
  • The matrix pointer moves. $D018 bits 7–4 place it on any 1 KB boundary within the current VIC bank; CIA2 $DD00 bits 0–1 select the bank. 64 possible homes.

⚠ Char ROM appears only in VIC banks 0 and 2. Banks 1 and 3 would need a character set in RAM — more DRAM, for no gain. So screen homes are restricted to $0000–$3FFF and $8000–$BFFF.

⚠ A screen home under the cartridge is free real estate. The VIC always reads RAM and never sees cartridge ROM, so a matrix at, say, $8400 displays correctly while the CPU sees cartridge ROM at $8000. Bank 2 is the natural second home.

The resulting display strategy:

Stage Display Tests
Blind border colour only screen home A, and whatever the engine needs
Visual full, matrix at home A everything except home A
Handover full, matrix moved to home B home A

Home A is in bank 0, home B in bank 2. The blind stage is about a second; everything after it is visible.

2.2 Reaching all 64 KB without needing RAM to start

A normal autostart cartridge needs the stack page, because the KERNAL's reset does JSR $FD02 to look for the CBM80 signature. A machine with a dead stack page dies before the cartridge gets control. That is not hypothetical — it is exactly what a failing board did in the sibling project, and why maxtest was written.

Ultimax mode removes every prerequisite: the cartridge supplies the reset vector at $FFFC and the 6510 runs straight from ROM with no KERNAL, no stack and no zero page. But Ultimax maps only $0000–$0FFF of RAM, which is the 4 KB ceiling every Ultimax-based tool hits.

The intended answer: start in Ultimax, then switch. The EasyFlash control register at $DE02 drives GAME/EXROM under software control, and both Kung Fu Flash and Ultimate II+ emulate EasyFlash because commercial titles depend on it. If that emulation is faithful, the tool boots without needing the KERNAL, tests low memory unaided, then switches to 8K/16K mode to reach the other 60 KB — matching MAX-Switch's capability on hardware people already own. ⚠ Confirmed on hardware with the DRAMs out; the qualification is about the launcher, not the tool — see G6.

✅ ANSWERED ON HARDWARE, 2026-09-26 — Ultimate II+ reports DE02=02 SWITCHED, CART STILL MAPPED. $02 asserts EXROM with GAME released, i.e. 8 K cartridge mode: ROML stays at $8000–$9FFF and everything else is RAM. So the tool boots in Ultimax needing no working KERNAL, proves low memory unaided, then switches and reaches the other 60 KB — on hardware people already own. ⚠ "No working RAM" is confirmed on a machine with the DRAMs removed, subject to the launcher qualification in G6. Measured with src/g1probe_roml.asm, which sweeps all eight register values rather than assuming one, so the result carries no assumption.

✅ Kung Fu Flash reports identically (2026-09-26). Two independent devices agree, so the delivery vehicle is settled.


⚠ G6 — what "needs no working RAM to start" does and does not mean · ANSWERED ON HARDWARE

Measured, 2026-09-27, on a C64 with every DRAM removed from its sockets:

Device Result What it shows
Kung Fu Flash SOLID RED SCREEN ✅ The cartridge got control, P0a found the zero-page scratch would not hold a value, and it reported and halted — exactly as designed
Ultimate II+ does not boot ⚠ Not a property of this code — see "the launcher" below

⚠ C_RED appears at exactly one place in the source — p0a_dead — so a steady red screen is unambiguous. It cannot come from anywhere else, which is what makes this a measurement and not an interpretation.

So the claim itself holds, and is now demonstrated on the worst machine there is. It boots from its own reset vector, runs registers-only with no stack and no zero page — nothing before P0a is a JSR or a PHA — and reaches a legible verdict on a machine with no memory in it at all. ⚠ That could never have been shown in VICE, which has no way to emulate a C64 with empty sockets. It took the bench.

⚠ Two things the claim never covered, and the documentation must state both.

1. The launcher is part of the dependency chain. "Needs no working RAM" is a property of the cartridge, not of the device holding it, and the two are not the same promise:

  • Kung Fu Flash boots the last cartridge you selected. That is why the red screen was seen.
  • Ultimate II+ does the same through Copy to Flash — the .crt is copied into the device's own flash and loads on restart. ⚠ Carl, 2026-09-27 [M]. An earlier revision of this section said the U2+ simply could not do it, which was wrong — it was inferred from one bench session in which the menu had been used, and never checked against the device's actual feature set.
  • ⚠ But on both devices the menu is a C64 program — it runs on the 6510, holds its state in C64 RAM and draws to the screen matrix in C64 RAM. On a machine with no working RAM you cannot reach it, so neither device can be armed after the fault appears.

⚠ So the instruction that matters is about TIMING, not about which device: arm it while the machine still works. Copy DRAMscope to flash, or select it once, on a healthy C64 — that is what makes it available later, when the machine is not healthy. A real Dead Test cartridge needs none of this because a cold reset maps it.

⚠ This corrects a real error, not a nuance. The earlier text presented Kung Fu Flash as the device that makes the no-RAM case reachable and the Ultimate II+ as one that cannot. Both can, and both share the same prerequisite. The mistake came from generalising a single bench session — the U2+ had been driven through its menu that day — into a property of the hardware, without checking what the device can actually do.

2. It cannot TEST memory that is not there — and with no RAM there is no screen. The screen matrix is DRAM, so on a machine with nothing fitted the only available output is the border, and with DEN=0 that is the whole display in one colour. A bare colour is the report in that case, by necessity rather than by choice. What the user gets is "low memory does not respond", which is true and actionable; what they do not get is a map, a bit number or a chip name, because none of those can be derived without memory to test.

⚠ A hypothesis this disproves, recorded because it drove the previous revision of this section. It was suggested that the P0 probes might falsely pass with no chip fitted: each writes a byte and immediately reads the same address back, and with nothing driving the data bus the read could return whatever the preceding cartridge-ROM instruction fetch left on it. The red screen disproves that on real hardware. The bus does not hold the written value, P0a fails as it should, and no code change is needed. The reasoning was sound and the conclusion was wrong, which is the normal outcome of reasoning about an undriven bus instead of measuring one.

⚠ Still not tested: a machine where every byte of RAM is fitted but faulty. dramscope_fall injects all eight bits bad at one byte; there is no mutation for a machine that fails every byte. The no-RAM case above is the nearest evidence and it is encouraging — the fatal path works — but it is not the same machine.

✅ G6a — the two P0a verdicts, built 2026-09-27

A steady red screen used to mean "the zero-page scratch will not hold a value", which covers both no RAM fitted and one bad byte in the scratch — two different repairs, and the first of them sends someone hunting for a faulty chip on a machine whose sockets are empty. They are now separated:

Signal Meaning Repair
Red, steady the scratch $F5–$FE will not hold a value, but something else in $0000–$0FFF does RAM is fitted; a chip is faulty
Red, flashing slowly on and off nothing in $0000–$0FFF holds a value at all No RAM fitted, or the PLA is not selecting it — there is no chip to find

How the question is asked. After P0a fails, sixteen probes — one per page of $0000–$0FFF — write $55, read it back, write $AA, read it back, and jump to the steady verdict the moment any page returns both. Two complementary patterns, so a data line stuck high or low cannot fake a response.

⚠ Registers only, no pointer, and therefore unrolled. The scratch has just failed, so zero page is unusable by definition — which rules out (mptr),y and with it any loop over pages. Sixteen unrolled probes is the price of asking the question at all; ROML has ~2.5 KB spare while the engine has 16 bytes, so it is the right half of the cartridge to spend.

⚠ Offset $80 in every page, and never $0000 or $0001 — those are the CPU's data-direction register and banking latch, not RAM, and writing a test pattern to them would change the memory map underneath the probe itself.

⚠ In Ultimax $0000–$0FFF is the only RAM there is, so there is nowhere else to ask. That is a limit of the question rather than a gap in it: on both long and short boards every chip serves every page, so a machine with memory fitted answers on all sixteen and a machine without answers on none.

Why a slow alternation and not a new colour. Every steady colour is already spoken for and two of them double as running-phase colours, so another one would add an ambiguity instead of removing one. Red going on and off cannot be confused with any steady colour, nor with a steady black "never got control", because it changes.

⚠ Timed from the raster, not from the CIA chain the running pulse uses — the engine sets those timers up, and on this machine the engine has not run and never will. It is the same frame-counting idiom P7 uses: one rule for the flash rate, not two.

⚠ THE RATE IS A SAFETY CLAIM, SO IT IS MEASURED. test/check.py brackets four intervals in emulated cycles: more than 3.4 M and less than 3.6 M, i.e. 850,000–900,000 cycles each, i.e. 0.86–0.91 s at the PAL phi2 of 985,248 Hz [S5]. A full on/off cycle is therefore 0.55–0.58 Hz against the three-flashes-per-second limit in WCAG 2.3.1 — a ≥5.2× margin, wider than the running pulse's 3.2× on purpose, because DEN=0 makes this fill the whole screen rather than just the border, which is the large-area case the standard is strictest about.

Both directions are tested, and one flag apart. dramscope_fscr defines INJECT_P0A, which flips A after the scratch store so the mismatch is certain whatever the RAM holds — the sweep then finds healthy RAM and must report steady red. dramscope_fnor adds INJECT_NORAM, which breaks the sweep as well, so nothing responds and it must report flashing red, sampled once per interval and required to alternate. ⚠ The mutation corrupts A, not the memory: skipping the store would leave the comparison reading whatever VICE powered up with, and if that were $55 the probe would pass and the mutation would test nothing.

⚠ A flashing border cannot be verified by one sample — a sample of a flashing border looks exactly like a steady one. It is sampled once per interval, four times, and the samples must alternate. And when the verdicts were deliberately folded back together to prove the check can fail, the breakpoint became unreachable and VICE ran until killed; TimeoutExpired was escaping as a traceback rather than a reported failure, so a timeout is now caught and returned as None, which mismatches and prints like any other finding. A traceback is not a report.


3. Execution model

Mode Code runs from RAM needed Reaches
Ultimax ROMH $E000–$FFFF none — registers only, no stack, no zero page $0000–$0FFF
16K ROML $8000–$9FFF engine workspace only all, banking via $01
Relocated tested RAM the region it sits in regions under the cartridge

Rules carried from the sibling project's engine, all of which were learned the hard way:

  • ⚠ Write the $01 latch before making the pins outputs. At reset the CPU port's DDR is 0 and the pins float high, reading as mode $37. Writing $00 to $00 first makes them outputs driving the latch's power-on $00 — mode $30, cartridge banked out, machine dead mid-instruction.
  • Registers and self-modifying code in any phase that tests zero page or the stack.
  • The first instruction sets a visible border colour, so "black screen" can only mean the cartridge never got control — never that it started and hung.
  • Every phase changes the border, so the last colour shown localises a hang even with no display.

4. Test phases

Ordered cheapest-and-most-diagnostic first, because a shorted data line makes every later result confusing and should be named in the first millisecond rather than inferred from a thousand march failures.

P0 — Bring-up probe · assumes no working RAM, and ⚠ verified against absent RAM (G6)

Ultimax, registers only. Proves the machine executes and that some RAM responds at all. Border-coded, readable across a room. Equivalent in role to the sibling project's maxtest.

Always runs first. Always runs, even if everything else is skipped.

P1 — Data bus integrity · instant

Walking 1s and walking 0s across D0–D7 at a single address, plus complements.

Catches stuck, shorted and open data lines immediately, and distinguishes them:

Observation Verdict
One bit never changes stuck-at on that line
Two bits always track each other shorted pair
One bit follows the last value written to another coupled / open

P2 — Address bus integrity · instant

Write a distinct signature to $0000 and to each power-of-two address ($0001, $0002, $0004 … $8000), then read all back. Any address line that is stuck, open or shorted produces aliasing, and which addresses alias identifies which line.

⚠ This is where the multiplexers show up. An and An+8 share MAn through U13/U25, so a failure that implicates both members of a pair points at the multiplexer or its 330 Ω series pack rather than at a RAM chip. That distinction is worth a great deal to whoever is holding the soldering iron.

P3 — March B, address-dependent pattern · ✅ implemented; measured 16.5–16.8 M cycles, ~17 s PAL

17n, five elements, as proven in the sibling project:

M0  ⇕(w P)
M1  ⇑(r P, w ~P, r ~P, w P, r P, w ~P)
M2  ⇑(r ~P, w P, w ~P)
M3  ⇓(r ~P, w P, w ~P, w P)
M4  ⇓(r P, w ~P, w P)

⚠ P is address-dependent: lo XOR hi XOR seed, not a fixed 0/1. March B requires only two complementary values, and it adds address-decoder coverage that a textbook fixed-pattern March B does not have: a read from the wrong address returns the wrong value.

⚠⚠ THIS SECTION USED TO SAY THE SUBSTITUTION "PRESERVES THE ALGORITHM EXACTLY". IT DOES NOT, AND G3 IN PROVENANCE.md SAYS SO — that is the very sentence G3 examined and rejected, including for March B, where it is "weaker than it looks". The correction was written into PROVENANCE.md and never carried back here, leaving the spec contradicting its own gate.

Covers: SAF, TF, AF — and AF more strongly than a fixed-pattern March B, which is the whole reason the substitution exists.

⚠ Coupling coverage is NOT claimed for P3. A CFid is sensitised only when the aggressor transitions while the victim holds a particular value; a fixed-pattern march guarantees that coincidence by construction because every cell is in the same state at the same point, and with P = lo ⊕ hi ⊕ seed they are not. G3 works through March B's M1 and concludes coverage probably survives across elements with opposite resting states — probably is not a proof. The provable unlinked-coupling and linked-fault coverage comes from P4, which is why it uses fixed $00/$FF.

⚠ THREE REGIONS ARE NOT MARCHED, AND THE MAP DISTINGUISHES THEM RATHER THAN HIDING IT: $0000–$01FF (zero page and stack, the engine uses both), $0400–$07FF (screen matrix and workspace), $0300 (the workspace page) and $C000–$CFFF (the engine). That is 23 pages, 5,888 bytes, so 59,648 of 65,536 are marched and the verdict line prints the figure. $0400–$07FF and $C000–$CFFF are probed by P0b/P0c with a single address-dependent write/verify pass — a real test, but not 17n — so they paint as + probed, never as a solid marched cell. Reaching the rest needs a second pass with the engine and display relocated.

⚠ Marched per contiguous run, not per page, so coupling faults between pages within a run are covered. The runs are simply what the exclusions leave: $02–$03, $08–$BF, $CC–$FF.

Measured runtime, by bisecting -limitcycles until the border goes green: not yet done at 16.5 M cycles, done by 16.8 M. That is 271 cycles per byte, 15.9 per operation — close to the sibling project's independently measured 15.03, which is a useful cross-check that nothing silly is happening in the inner loop.

P4 — March LR, fixed patterns · ✅ implemented; ~11 s

14n, six elements. Linked faults — two defects close enough that the write exposing one masks the other — are outside March B's guarantee, and March LR is the published answer.

Note that at 14n it is cheaper than March B, not dearer. Both run; total 31n, measured end to end at 26–28 M cycles, about 28 s on PAL.

⚠ Gate G3 ANSWERED, and not the way it was expected to go: the substitution is NOT used here. The "only two complementary values are needed" argument holds for single-cell fault classes but not for coupling faults, which are detected only when the aggressor transitions while the victim holds a particular value — a coincidence a fixed-pattern march guarantees by construction and an address-dependent one does not. So March LR runs with fixed $00/$FF, where the published linked-fault proof holds exactly as written, and March B keeps the address-dependent pattern for the decoder coverage it was introduced for. Full reasoning in PROVENANCE.md.

⚠ Review against the memory-test literature, 2026-09-27

A first-principles audit of the implementation against the published fault taxonomy and march definitions. No other tool's code, binary or disassembly was consulted — PROVENANCE.md's status line still holds.

✅ Both element sequences match the published definitions exactly. March B is 1+6+3+4+3 = 17n with M3/M4 descending; March LR is 1+2+4+2+4+1 = 14n with M1 descending — that single reversed element is the whole of LR's linked-fault property, and it is present. Nothing to fix.

⚠ FINDING 1 — P = lo ⊕ hi ⊕ seed IS INVARIANT UNDER TRANSPOSING THE ADDRESS BYTES, AND THAT IS A REAL FAULT ON THIS HARDWARE. P is symmetric in lo and hi, so P(hi,lo) = P(lo,hi). A fault that swaps which address byte becomes the DRAM row and which becomes the column maps every address (hi,lo) to the cell (lo,hi) previously used. That is a bijection — memory still stores and returns data consistently, so no march test detects it, fixed-pattern or address-dependent, and our decoder-catching pattern is blind to it too because P is unchanged.

⚠ The physical cause is specific to the C64: A0–A7/A8–A15 are multiplexed through U13/U25 (two 74LS257) on a shared select line. Invert or mis-drive that select and row and column swap wholesale.

✅ It is caught — by P7 RETENTION, and only by P7. The VIC's refresh counter drives 8 bits to generate 256 row addresses (G2, Bauer §3.13). Swap row and column and the counter now sweeps physical columns, so each physical row is refreshed once per 256 refresh cycles instead of every cycle — retention collapses from the ~2 ms refresh budget [S8] to hundreds of ms, which the 12-second dwell finds instantly. ⚠ This is written down because it is the ONLY coverage of that fault. Shorten or remove the dwell and it disappears silently, and nothing else in the design would notice.

⚠ FINDING 2 — STUCK-OPEN FAULTS (SOF) ARE NOT COVERED, AND NEITHER MARCH IS AN SOF TEST. Detecting an open access transistor requires the bit line to be holding the complement of the expected value at the moment the cell is read; a read immediately following a write to the same cell leaves the bit line holding what was just written, so the open cell returns the expected value and passes. This is a known limitation of march tests in general, not a defect in our implementation of them. Not claimed anywhere, so it is a gap rather than an over-claim — recorded so it stays a deliberate omission.

⚠ FINDING 3 — THE MARCHES RUN PER CONTIGUOUS RUN, SO THE COUPLING GUARANTEE IS PER RUN, NOT GLOBAL — and the source only ever asserted the within-run half. The across-run case is benign, for a reason worth stating rather than assuming:

  • Row-direction adjacency (adjacent offsets, ROW = A0–A7) is always within one page, so always within a run. Never affected.
  • Column-direction adjacency (same offset, adjacent pages) crosses a run boundary only where an excluded page sits between the two — $03, $04–$07, $C0–$CF. Those pages are the ones the handover marches at 9n, so the lost adjacencies are exactly the excluded pages' own neighbours, which are covered less deeply rather than not at all.

✅ A POSITIVE FINDING, recorded so nobody "optimises" it away. Because ROW = A0–A7, ascending address order walks all 256 rows of one column before moving to the next column. That is precisely the long same-column read run that DRAM sense-amplifier recovery and bit-line imbalance tests are built to produce — the marches get it for free from the C64's own address mapping, and the address-dependent P alternates the data within a column as a bonus. ⚠ Any change to the address traversal order would forfeit this, silently.

⚠ NPSF is not claimed anywhere and is not covered. Full active/passive/static neighbourhood pattern-sensitive testing needs specific neighbourhood configurations at a large constant factor per cell. P5's six patterns are a sample of neighbourhood conditions, and §2's table claims "physical row/column coupling", not NPSF. That distinction is correct as written — this note exists to keep it that way.


⚠ G7 — OPEN: EasyFlash is not the only cartridge that can leave Ultimax

Carl asked whether EasyFlash is really the only alternative. It is not, and §2.2 overstated it. The requirement is a software-settable GAME line; $DE02 is one implementation.

Cartridge Register Tag
EasyFlash $DE02 — measured on Ultimate II+ and Kung Fu Flash (G1) [M]
Final Cartridge III $DFFF, bit 4 = EXROM, bit 5 = GAME [C] c64-wiki
Action Replay / Retro Replay / MMC Replay control register in $DE00–$DE01 [A]

⚠ In hardware it is one chip. A software-controllable cartridge is at core a 74LS273 octal flip-flop, inputs on the data bus, two outputs driving GAME and EXROM, clocked from an I/O decode. A homebrew dedicated cartridge therefore needs two 8 KB ROM windows, Ultimax strapping at reset, and one latch — not an EasyFlash specifically.

Why the tool still uses exactly one register. g1probe sweeps safely because it copies itself to RAM at $0200 first — a wrong write banks the cartridge out from under the CPU and ROM-resident code dies mid-instruction, which is indistinguishable from "the register did nothing". ⚠ DRAMscope has no such luxury: it runs from ROM, in Ultimax, on a machine whose RAM may not work at all. A speculative write to an unknown register could unmap ROML mid-instruction and hang with nothing on screen. One measured register beats several guessed ones.

⚠ THIS GATE CANNOT BE CLOSED HERE, AND IS NOT A PLANNED TASK. The author owns a Kung Fu Flash and an Ultimate II+ and nothing else, so there is no Final Cartridge III or Retro Replay to measure on. Recording it as a to-do would be recording a job nobody can do.

It is also low value, which is the more honest reason to leave it. EasyFlash is the homebrew cartridge standard: anyone wanting a dedicated DRAMscope cartridge buys an EF1 or EF3 for the price of a takeaway rather than reflashing a freezer cartridge they use for something else. The two devices already measured cover the great majority of users.

If someone with the hardware wants it, the path is the one that closed G1: extend g1probe to sweep $DFFF and $DE00–$DE01 as well as $DE02, run it on the cartridge, and implement what is measured. ⚠ A lead for whoever picks it up: VICE emulates these cartridge types, and cartconv -t fc3 can build the probe as an FC3 image, so the register semantics can be exercised in emulation without owning one — though emulation agreeing is not the same as hardware agreeing, which is the whole lesson of G1.

⚠ Until someone does, no claim is made for those cartridges beyond "the hardware can do it".

⚠ Not to be resolved by reasoning. Whether a write to $DFFF is harmless on an EasyFlash, or $DE02 harmless on an FC3, depends on each cartridge's address decoding. That is measurable and must be measured — exactly the trap G6 fell into when an undriven data bus was reasoned about instead of probed.


P5 — Topographical patterns · ✅ implemented; ~34 s

A 4164 is physically 256 rows × 256 columns [S8], and the row and column addresses are the grid coordinates. Patterns structured by row-index and column-index therefore stress physically adjacent cells in a way that a logical address sweep does not.

Patterns: row/column parity checkerboard and its inverse, row stripes, column stripes, and single-cell-in-a-field (write a uniform value, flip one cell, verify the whole row and column).

✅ Gate G2 ANSWERED: ROW = A0–A7, COLUMN = A8–A15, from Bauer §3.13 — the refresh counter generates "256 DRAM row addresses" and its table puts REF7..REF0 on bits 7..0.

⚠ This inverts the intuitive reading. A DRAM row is one byte offset taken across all 256 pages, so physically row-adjacent cells are 256 bytes apart; a DRAM column is a single page, so column-adjacent cells are 1 byte apart. The patterns are built from that, not from address order.

⚠⚠ HONEST LIMIT, AND IT MUST BE STATED WHEREVER THE CLAIM IS MADE. This covers row and column adjacency as addressed. It does not model true die layout. Real 4164 dies use address scrambling, array folding and true/complement bitlines that differ by manufacturer and die revision, and that information is proprietary layout data absent from every datasheet. A C64 contains whatever chips the factory fitted or a repairer replaced, often mixed. A confidently wrong physical map is worse than none, because it claims adjacency coverage it is not delivering. So: no per-manufacturer scrambler, and no claim of one.

P6 — Retention / dwell · configurable

Write the full address-dependent pattern, let real time pass, verify.

⚠⚠ REFRESH CANNOT BE SUPPRESSED FROM SOFTWARE ON A C64, AND ANY DESIGN THAT ASSUMES OTHERWISE IS WRONG. The VIC performs 5 refresh cycles per raster line unconditionally [S2] — 78,000/s PAL, 78,600/s NTSC, computed from raster geometry [M] with no display-state term. DEN=0 suppresses badline character fetches and sprite fetches; refresh is not a badline activity and continues regardless.

The irony is that blanking does the opposite of what a refresh-starvation design wants: removing badlines gives the CPU more cycles, so the dwell loop runs faster while refresh carries on unchanged.

So this phase tests retention against a working refresh — it catches cells so leaky they fail even with refresh at specification. That is exactly the marginal chip that passes every fast march and drops bits an hour into a warm session, and nothing else catches it.

Dwell configurable: 30 s / 2 min / 10 min. The documentation should tell users to run it on a warm machine, because temperature is the other lever and it is not a software one.

P7 — Disturb · bounded, configurable

Hammer one row with repeated accesses while its neighbours hold a known pattern, then verify the neighbours. A proto-rowhammer: it stresses coupling and weak cells under access load rather than under time.

P8 — Colour RAM · instant

$D800–$DBFF, 4 bits wide, a separate static RAM chip. Needs no working DRAM, and fails often enough to matter — and when it does you get wrong colours rather than a crash, so it is routinely misdiagnosed as a VIC fault.

⚠ Mask reads to 4 bits; the upper nibble is open bus.

P6 — Zero page and the stack · ✅ implemented

Registers-only and self-modifying, 9n rather than 17n because there is no pointer to walk with and no room to write the long march out twice.

⚠⚠ Nothing in the phase may touch the stack while page $01 is under test — not JSR, not PHA, not an interrupt. Each writes into the page being marched and corrupts a cell the test has already verified. The phase is entered and left by jmp with a self-modified exit for that reason. Both traps were hit during development: the first version used jsr p6_run and hung when the return address was overwritten, and the first fault-injection used PHA and reported seven bad bits instead of one.

⚠ $0000 and $0001 cannot be reached by the CPU — the 6510 shadows them with its own data-direction register and banking port. The RAM cells underneath are real; no program running on the CPU can address them. The scan starts at $0002.


5. The classification engine

This is the feature that makes the tool useful rather than merely thorough, and it is mostly bookkeeping rather than new science. The phases above already generate the evidence; today nothing interprets it.

Rules, evaluated in order — first match wins:

Evidence Verdict
P1 fails Data line fault. Name the bits and whether stuck, shorted or coupled
P2 fails, implicating both members of an An/An+8 pair Address multiplexer or series pack — U13/U25, RP1/RP2
P2 fails on a single line Address line fault. Name the line
Every bit fails at every address Not a RAM chip. Suspect PLA/CASRAM, or no RAM fitted. ⚠ A machine with genuinely no RAM fitted never reaches this verdict — it halts at P0a long before, and since G6a it says so specifically, with a slowly flashing red screen
One bit lane fails across ≥2 distinct regions Single DRAM chip. Name it from the board profile
Failures at a regular address stride Decoder / aliasing. Name the implicated address bit
Failures confined to one page or small region Localised cell fault. Report addresses
P3/P4 pass, P6 fails Retention — leaky cells. Name the chip. Marginal, will worsen warm
P3/P4/P6 pass, P5/P7 fail Coupling. Name the bit and the neighbourhood
Colour wrong, memory clean Colour RAM chip

Board profiles

Chip naming is a lookup from data bit → designator, per motherboard assembly. Held as a data table, not code, so profiles can be added without touching the engine.

Assembly Notes
250407 8 × 4164, U9–U12 and U21–U24
250425 8 × 4164, different bank geometry
250469 and relatives ⚠ short board — TWO 41464s, 64K × 4. Each chip carries four bits, so a failing bit narrows to one of two chips and no further, and the 250407 designators are simply wrong there. Carl, 2026-09-26.

✅ 250407 is verified and implemented — D0=U21, D1=U9, D2=U22, D3=U10, D4=U23, D5=U11, D6=U24, D7=U12, from schematic 251138 via c64-ice40-ram README §2.2. The other two profiles are not, and until they are the tool must not name a chip for them.

⚠ These tables must come from Commodore schematics, verified, not from recollection. A wrong table prints a confident instruction to replace the wrong chip, which is the worst output this tool could produce. Until a profile is verified, report the bit and say the profile is unavailable.

The tool should offer a profile selector and default to reporting bits only.

⚠ Until a selector exists, a named chip carries the short-board caveat on screen — 41464? D0-D3 IS ONE CHIP - SEE SHEET. — printed in place of the last line, because it matters most when nothing is wrong and this matters most when the tool is telling someone which part to desolder.


6. Display design

+--------------------------------+-------------+
|  page map   16 x 16            | D0 . . . .  |
|  row = high nibble             | D1 . . . .  |
|  col = low  nibble             | ...         |
|  each cell = one 256-byte page | D7 . . . .  |
|                                |             |
|                                | PHASE  4/9  |
|                                | PASS      2 |
|                                | ERRORS    0 |
|                                | TIME   0:47 |
+--------------------------------+-------------+
| verdict / error log                          |
+----------------------------------------------+
  • The 16 × 16 grid is the address space. Row is the high nibble of the page number, column the low nibble; top-left is $0000, bottom-right is $FF00. A fault's shape — a stripe, a quadrant, a scatter — is visible at a glance and is usually more diagnostic than its address.
  • State by glyph and colour, never colour alone: . untested, - testing, solid pass, X fail. Colour-blind readers and monochrome monitors both exist.
  • ⚠ The colour layer must carry the map on its own. Colour RAM is a different chip, so if the matrix region itself develops a fault the glyphs go to garbage while the colours stay correct. Design for that degradation deliberately — and per principle P5, say on screen that the display is suspect rather than showing a plausible-looking lie.
  • No KERNAL, no BASIC. Direct writes, screen codes not PETSCII.

7. Runtime budget

Measured baseline from the sibling project: 255.6 cycles per byte for 17n March B, i.e. 15.0 cycles per operation, on a PAL machine at 0.985 MHz [S5].

Phase Ops Estimate
P0 probe — instant
P1 data bus ~64 instant
P2 address bus ~34 instant
P3 March B 17n = 1.11 M ~17 s
P4 March LR 14n = 0.92 M ~14 s
P5 topographical ~12n ~12 s
P6 dwell 2n + T T + 2 s
P7 disturb bounded configurable
P8 colour RAM 1 KB instant
Full run, no dwell ~45 s
Full run, 60 s dwell ~1 min 50 s

Per principle P1, publish these. A run that greens in 20 s has skipped something.


8a. Verification across the family

⚠ Carl, 2026-09-26: "it really needs a thorough simulation test, on various C64 variants." Until then the tool had run on exactly two machines — VICE's default PAL C64 and one Ultimate II+ / Kung Fu Flash — and several things it depends on are not constant across the family: raster geometry (P7 waits on line $80), φ2 frequency (the dwell and the flash rate), and the CIA revision (which now is the flash rate).

test/models.sh runs a complete pass on every model VICE emulates, against both CIAs:

Models c64, c64c, c64old, ntsc, newntsc, oldntsc, drean (PAL-N), c64gs, pet64
CIA 6526 and 8521
Assertion identical outcome — GREEN, 0 errors, map row 0 **#*****########, PASSES 0001

Two cases earn their place beyond the sweep:

  • ⚠ A KERNAL of 8 KB of $FF. The cartridge boots in Ultimax and supplies its own reset vector, so it should never execute a KERNAL byte. That is a claim, and this is what tests it — a normal autostart cartridge cannot survive it, because it needs the KERNAL's own JSR $FD02 to be handed control.
  • ⚠ The MAX Machine. 2 KB of RAM and no $C000, so the engine has nowhere to live. It must report a fatal border code and hold it, not hang. It reports ORANGE — the check that it failed to leave Ultimax — which is correct and legible.

⚠ Several models name a KERNAL image not installed here, and VICE refuses to initialise without one. The stock ROM is substituted so the run can proceed; the cartridge never executes it, and what those models actually change — the VIC-II and CIA revisions — is unaffected. ⚠ A machine VICE cannot start is reported as SKIPPED, never as a failure. Reporting a missing ROM as a defect in the cartridge would be a lie, and the harness checks for it explicitly.

⚠⚠ WHAT SIMULATION CANNOT COVER. VICE models the machine, not the DRAM arrangement. A short board's two 41464s and a long board's eight 4164s are indistinguishable to it, so chip naming can only ever be verified on real hardware — and the short-board caveat on screen exists precisely because no amount of simulation will catch that error.

8. Verification

⚠ A RAM test that cannot fail is worthless, and this is not a hypothetical concern — an early version of the sibling project's own test could not test the block it ran from, and that was only discovered by mutation-testing it.

Every phase ships with:

  1. A fault-injection build that deliberately breaks the thing the phase claims to catch — a stuck bit, an aliased address line, a cell that decays after N frames, a coupled pair.
  2. A headless VICE harness that runs both builds and asserts clean-passes and fault-reports. Pattern already established in the sibling project's diag/test.sh.
  3. A recorded expected runtime, checked by the harness.

⚠ Debian's VICE is the +dfsg build with Commodore's ROMs stripped. The harness must supply stub ROMs, as the sibling project does. Note the caveat: a stub KERNAL is not a real one, so anything depending on genuine KERNAL behaviour cannot be verified this way.

The classifier needs its own fault injection. It is the component most likely to be confidently wrong, and a wrong verdict is worse than no verdict. Every rule in §5 needs a synthetic failure set that triggers it and a neighbouring set that does not.


9. Gates — things that must be answered before the code they block

Gate Question Blocks Tag
G1 ✅ CLOSED. Ultimate II+ and Kung Fu Flash both: $DE02 = $02 (2026-09-26, measured on both). — [M]
G5 ✅ CLOSED for Assy 250407 — the bit→designator table is confirmed from schematic 251138. Other assemblies remain [A]. — [D]
G2 ✅ ANSWERED: row = A0–A7, column = A8–A15 (Bauer §3.13). — [C]
G3 ⚠ ANSWERED: no, not provably. March LR therefore uses fixed patterns. See PROVENANCE.md. — [M]
G4 In Ultimax, do VIC fetches in $3000–$3FFF come from cartridge ROMH? Screen home selection [A]
G6 ✅ ANSWERED on hardware, 2026-09-27: it starts and reports with every DRAM removed — solid red from p0a_dead on Kung Fu Flash, which boots the remembered cartridge with no menu. ⚠ The Ultimate II+ can do the same via Copy to Flash; on the day, its menu had been used. Both devices must be armed beforehand, from a working machine. — [M]
G6a ✅ BUILT 2026-09-27 — red now distinguishes "scratch bad, RAM fitted" (steady) from "nothing responds anywhere, no RAM fitted" (slow flash). Both directions tested, flash rate measured at 0.55–0.58 Hz. — [M]
G5b ✅ CLOSED 2026-09-27 — all five assemblies held and corroborated. 326298, 250407, 250425, 250466, 250469, each confirmed by two independent sources agreeing on all eight bits; the 250466 from Commodore schematic 252278 after its opencbm entry proved wrong. Table and corroboration status in PROVENANCE.md, checked against the cartridge by test/chart.py. ⚠ Renamed from a second G5, which duplicated the closed gate above it. — [D][C]

G1 first. It is cheap to test and it decides whether this is one binary or two.


10. Implementation order

Each step is intended to leave something that works.

# Step Leaves
1 Project skeleton, this spec, PROVENANCE.md ✅ done
2 Resolve G1 on real KFF and U2+ hardware ✅ closed — both devices, $DE02=$02
3 Engine skeleton, P0/P1/P2, display framework, VICE harness ✅ done — already a useful tool, bus faults named in under a second
3b EasyFlash delivery, engine relocated, A15 closed ✅ done — no working RAM needed to start, ⚠ verified with the DRAMs out (G6); the launcher must map the cartridge without a menu
4 P3 March B + bad-byte count + failing-bit mask ✅ done — parity with existing tools, plus shape
4b Chip naming from the bit mask, Assy 250407 ✅ done — and it refuses to name when all 8 bits fail
5 P4 March LR, fixed patterns (G3 answered first) ✅ done
6 P5 topographical patterns (G2 answered first) ✅ done
6b P6 dwell / retention ⬜ next
6c The remaining classifier rules in §5 (stride, region, mux pairing) ⬜
5 Board profiles (G5) → chip naming the headline feature
6 P4 March LR (G3 first) linked faults
7 G2, then P5 topographical physical coupling
8 P6 dwell, P7 disturb marginal chips
9 P8 colour RAM the commonly-misdiagnosed chip
10 Docs, credits, licence, release something the community can use

⚠ Step 3 is the first release candidate. A tool that instantly names a shorted data line or a dead address line, with a live map, is already worth having even with no march test at all. Do not hold the release for step 8.


11. Open decisions for Carl

  • The name. DRAMscope is a placeholder used consistently throughout.
  • Licence. Needs choosing before anything is public. The provenance position is independent of the licence, but a permissive licence makes the "not derived from" claim easier for others to rely on.
  • Scope of board support. 250407 only at first, or profiles from the start?
  • Should this engine ever be reused elsewhere? It is written to run from cartridge ROM on an unmodified C64, and any other home for it would be a deliberate decision rather than a drift.