この文書はまだ翻訳されていません。本文は英語のまま表示されています。
Bench report: B0 to B3, machine side closed, the part's side waiting on the build
Begun 2026-09-06 as B0's report and grown into the bench's running
report as B1, B2 and B3 got their tools before the hardware arrived.
Plan: docs/bench-plan.md, first. Nothing here has touched the part
yet: the bridge is not built. What closed is everything the four
milestones could close on the machine, each tool with a green run on a
synthesis and a mutation that goes red, and one thing B0 was going to
measure on the part turned out to be measurable on the die first,
which changed the model. The sections are in the order they were
written; the part's side of each milestone is the list at the end.
What exists
firmware/bridge/bridge.ino: the ESP32 sketch. The console's latch and clock counted by the pulse-counter peripheral (rising edges of OUT0, falling edges of the clock, 50 ns glitch filter, nothing in an interrupt), the original pad polled once a millisecond on the bridge's own lines, the 74HCT165's eight inputs written between polls, and one line per latch on USB serial at 921600:L <latch index> <byte the register held> <clocks the poll took>. Commands:MODE PASS,MODE INJECT,SET hh,AT n hh,TRIG n,RESET,STATUS. Compiles for a generic ESP32 under arduino-cli with the esp32 core 3.3.11 (281 KB). Not yet run on a board.tools/sniff.py: the serial side, commands then a stream to a file.tools/compare-logs.py: two logs latch for latch, histograms of clocks per poll, the first latch where clocks or bytes differ, and a refusal to compare a log with no polls.- The model's log:
nes-console'spad-logexample prints the same line per latch from a ROM and a script ofAT frame hhlines, andexport-testrom pad/pad-dmcwrite the polling cartridge (a strobe and eight reads every NMI, with or without a looping DMC sample at the fastest rate).
Added 2026-09-07: the head, and the script on both sides
head/headd.py is the Pi's daemon: one script (docs/script.md) played
onto the bridge, the relays and the scope, the run served back. It ran
end to end against tools/fake-bridge.py, a stand-in for the protocol
with no part behind it, and the fetched log was diffed against the
model's for the same script by tools/compare-logs.py: every latch
agreeing, the scheduled byte landing at exactly its latch on both
sides (the bridge writes it after latch n-1, the model's controller
applies it at the strobe's rise before latch n; nes @ a99b4eb,
tests/pad_log.rs). The scope half shares scope-capture's proven
dialect and adds the external-trigger single shot, untested: the scope
was off the LAN when this was written. The relays have no Pi wired.
Added 2026-09-07: the B1 tool, green on the synthesis before any capture
tools/b1-score.py runs/<stamp> rom.nes reads the run's script for
its TRIG n and the capture's .toml for the rate and the trigger's
sample (which the head reads off the scope's own preamble, so no offset
sign convention is trusted), and runs nes-console's capture-score
with the script, the latch and the trigger: the model plays the same
SET and AT lines to the first frame that completes after latch n,
the record is sliced from the trigger's sample on so the recovery's
first full frame is that frame on the part, and every flat region is
scored through the roundtrip with N6's tolerances. The recovery needs
two full frames after the slice, so the head sets the horizontal offset
to put the trigger early in the record; whether the sign is right is
the first real capture's to say, and the .toml will say it.
Its own green run (SYNTH_TRIGGER=1): six frames synthesised through
the card model, the fourth scored, the synthesis sliced from inside the
third as a trigger placed there would be, 13 of 13 regions on the bars
cartridge at every frame count tried across its luma-row step.
MUTATE_TRIGGER=1 slices one frame late and is 1 of 13 at the step
(frames 122), which is what shows the frame selection is checked and
not only the colours; the public site's boarding runs both and refuses
to board if the mutation is not red. nes @ 59d42c8.
Added 2026-09-07: the B2 classifier, green on its own synthesis
tools/b2-align.py reads the console's CPU-to-PPU alignment off a
three-channel capture (the master clock, M2, ALE) and, with sweep,
plays a power-on script through the head N times and prints the
histogram. Its two facts were measured on the dies first: the 2A03's
M2 falls on the very half-step its clk0 falls, a phi1's start, and
rises three early (high 15 of 24, the part's documented 62.5 percent
duty; v2a03-sim's m2-phase, 2a03 @ 51c9b4a), and the 2C02's ALE
rises on the very half-step pclk0 rises, a dot's start, every other dot
while rendering (v2c02-sim's ale-phase, 2,508 of 2,508 rises, 2c02
@ c3a4b9b). So the offset from an ALE rise to the next M2 fall, in
half-steps on the console's own master clock as the ruler, is the
model's cpu_phase minus ppu_phase, and mod 8 it is the class; the pair
the model runs in, (4, 3), is class 1. A capture whose offsets do not
sit on half-steps, or whose class is not clean, is refused, not
classified.
Its gate (selftest): the three channels synthesised for 72
alignments at the head's three-channel rate with the measured pin
offsets, every one read back as its class; the synthesised ALE shifted
one half-step (MUTATE) moves the class on all 24. Then the whole path
through the head: tools/fake-scope.py answers the head's SCPI with
those synthesised records, a B2 script (power off, arm three channels
on ALE's first rise, power on, capture) played three times by sweep,
each run fetched and classified as the alignment the fake was given.
The head's ARM took a channel list, a trigger source, a timebase and a
depth for it (the DS1054Z holds 6 M points at most with three channels,
so B2's window is 6 ms at 1.2 M; B1's stays 60 ms at 12 M on one).
What only the bench can say: whether the part's histogram over a hundred power-ons is flat over its classes or prefers some, which is B2's first gate, and whether the model's (4, 3) is among them.
Added 2026-09-07: the B3 tools, a planted divergence found
tools/b3.py is B3 in four verbs. record turns a run's bridge log
(MODE PASS, a hand on the original pad) into a script: MODE INJECT, SET
the first byte, an AT at every change; the firmware's schedule grew to
two thousand entries for it. replay plays that script on the part
once per latch named, each replay from RESET with one capture
triggered at its latch, and scores every capture against the model's
frame at that latch through b1-score; one capture per run, because
reading a record takes seconds while the console runs on, and the arm
comes before the trigger in the script because arming is seconds of
SCPI and a trigger set first can fire unheard, both found here. agree
sets two replays' captures at the same latches against each other, the
part against itself, region for region under B1's tolerances. bisect
finds the first latch at which a capture disagrees with the model,
assuming divergence is monotone, in about log2 of the span replays.
To test it without a part, nes-console's polling cartridge got a
variant, pad-paint, that colours its band with the byte it polled,
and capture-score writes its synthesis out as a record with the
trigger's sample beside it (SYNTH_OUT); tools/fake-scope.py --video
serves that synthesis, at the latch the fake bridge triggered at, under
the run's own script with --diverge-at N hh appended, so the "part"
plays a different byte from latch N on. With the divergence planted at
latch 200: replays at 100 and 400 agree and disagree with the model as
they should, two replays agree with each other at both, and bisect
over 0..1024 names latch 200 in eleven replays, latch 199 agreeing.
nes @ e1839eb.
One bug found by that test and fixed: the synthesis had been written out after its own trigger slice, with the trigger's sample computed from the shortened record, so a reader slicing at that sample landed two frames late, and the bisection named 198. The written record now recovers the same frame as the in-process path, anchor line for anchor line.
Added 2026-09-07: the electronics review's sheets, and what they changed
The bench's electronics review returned four schematics and a build
document (docs/bench-build-v1-v2.md, bench-v1.svg, bench-v2.svg,
logical-timing.svg, pad-adapter.svg, drawn by
tools/draw-schematics.py), with a note of what its own double-check
had amended (the LM1881's pin labels, the relay's supply on the Pi's
rail, an authored width, a build step that contradicted itself, a
decoupling count). Taken in as they came, with two things added so
they cannot drift from the rest: tools/check-sheets.py holds the v1
sheet's every C6 pin to the wiring tables and the committed SVGs to
the generator, and the build document embeds the sheets.
One firmware change the timing sheet made visible went into
bridge.ino the same day: the 165 loads while OUT0 is high, and the
eight register pins were being written one at a time, so a poll could
latch a byte half old and half new. They are now one store to the GPIO
output register, made only when OUT0 reads low before and after and
deferred to the next loop otherwise; the loop reads the counters and
logs the latch before it writes; and MUTATE ON swaps the two
counters' lines so B0's mutation is a line in a script. The fake
bridge does the same. Compiles for the C6 and the classic ESP32; not
yet run on a board.
v2 (atomic bytes over SPI, a second port, an LM1881 giving every latch its field and line) and the pad adapter are in the plan as what comes after the four milestones, with the condition that earns each.
What the die said before the part could
The gate asked for clocks per latch on the part, expecting nine where
a DMC fetch lands on a poll's read. That is a question the switch-level
2A03 can answer, so it was asked first (2a03's
joy-clock-probe, docs/n3-report.md there):
- On the read a DMC fetch lands on, /OE1 falls with the read, stays low through the halt cycles (one continuous pulse), rises during the fetch's own read of the sample, and falls again when the core re-runs the read with RDY high. Two rising edges: a 4021 shifts twice and the core takes the bit after the one it asked for. The halt cycles do not pulse it.
- Unless the sample's address has the port's low five bits ($xx16 for $4016, $xx17 for $4017): twenty aliases over two loop cadences kept /OE1 low through the fetch, and the pad is clocked once. A plain read of an alias does not assert the strobe and a fetch from one with the core idle does not either, so the strobe's high address bits come from the core's held address and its low five from the pins the DMA drives. A fact about the die's decoder.
Three changes followed, each with a test that fails without it:
- Rung 3 of the 6502 (
tinymachines/6502@ 89ae24f) re-asks its bus at every phi2 of a read held by RDY and keeps the last byte, which is what DL does;MUTATE_HELD=1keeps the first and is red. - The 2A03's rung (
tinymachines/2a03@ dbf116b) answers those re-asks from a memo while the core is held and lets one through on the re-run, unless the alias rule holds;tests/joypad.rsholds the rung's asks per instruction to the die's /OE1 pulses over two cadences, {1: 2820, 2: 6} and {1: 2245, 2: 4}, instruction for instruction;MUTATE_QUIET=1(five asks per collision) is red. - The console (
tinymachines/nes@ 2ec0fc3) logs polls; on the polling cartridge with the DMC loop the model predicts nine-read polls at 21 of 596 latches over 600 frames, recorded intests/pad_log.rs.
So the B0 gate's third bullet, the model's DMC fetch schedule beside the part's nine-clock polls, now has a prediction with an exception the documentation does not mention. The part decides.
What B0 still needs from the bench
In the plan's order, once the bridge is built per docs/wiring.md and
its "measure first" list is done:
- Clocks per latch on a game without DMC: 8 on every poll over a
minute;
tools/sniff.pythentools/compare-logs.pyagainstpad-logon the same cartridge (a dump from the shelf). - Latches per second against the scope's field count.
- Polls per frame beside the model's, by latch index.
- A game with DMC playing: the nines by latch index against the model's, which is where the alias rule is either seen or not.
- MUTATE on the bridge: the clock counter fed the latch line fails the 8-per-latch check.
None of it can be faked here, and the report says so.
Pulled at build time from nes-bench/docs/bench-report.md; the repository is the one copy.