rig: run the RR-Net path at turbo, and measure why DHCP survives it - #205
Merged
Conversation
The physical RR-Net rig had run only at stock 1 MHz, for two stated reasons. One of them was a suspicion rather than a measurement, and it is now measured. ip65's `timer_read` returns CIA2 timer B, cascaded off timer A at 1000 cycles by `ip65/drivers/c64timer.s`. CIA timers count phi2, so if the CIAs scaled with a 48 MHz CPU the ~15 s / 12-retry DHCP budget in `ip65/ip65/dhcp.s` would collapse to ~0.3 s. CLAUDE.md's CIA1 TOD figure does not answer that — TOD is a different clock domain — so `tools/probe_cia_timer_rate.py` measures timers A/B directly against host wall clock. U64E, n=6 intervals per clock: 1023.2 ticks/wall-second at 1 MHz, 1022.9 at 48 MHz, ratio 1.000, both inside 0.05% of the 1022.7/s that NTSC phi2 predicts. The probe carries its own negative control — its loop counter scaled 24.3x across the same clocks — because without one a turbo write that silently did nothing looks identical. The probe never exports a raw timer value: timer B's inverted 16-bit up-count wraps every 1.37 s at 48 kHz, which would alias a 15 s interval towards the answer under test, so the C64 accumulates modular 16-bit deltas into a 32-bit counter and publishes it through a REQ/SERVED handshake that cannot be read torn. `TURBO_MHZ` then runs the same image at turbo, with a 1 s phase cadence so the timeline does not collapse to three points. First run: 43.1 s 'G' to CONNECTION CLOSED against 1,979 s at 1 MHz, DHCP on the automatic attempt, and the CS8900a behaved — which was the open risk. 24 of 25 checks passed; `check_tls_connected` is a sampling oracle and turbo outruns it, so it is left red and documented rather than softened, and issue #204 tracks the C64-side latch that would fix it. The default stays 1 MHz, because that is the clock the product ships at. `tests/rig_vice_https_macos.py` gains a guard against the obvious misreading: warp accelerates the CIAs too, so its 1x DHCP assist is not licensed away by a hardware-turbo result. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…argin rationale Three review fixes, none of which changes behaviour. The `tls_close` store was cited as `src/tls13.s:380` in the rig, the notes and CLAUDE.md. Verified against the source: the routine is at :379, `lda #TLS_STATE_IDLE` at :382 and `sta tls_state` at :383, so the cite is now `:379`, storing IDLE at `:382-383`. The CONNECTED store is `:303-304`, not a bare `:303`. The substantive claim is unaffected — `sta tls_last_state` appears only at :310 under `@error:`, so a clean run leaves it 0 and it is no fallback. The new clock assertion fires unconditionally, including at the default 1 MHz, so a stock re-run reports 25 checks where the first passing run reported 24. Both places that state the decomposition now say so, and both keep the first run's numbers as history. `RES.check()` call sites go 18 to 19 with it; the 15 delegated verdicts are unchanged. The probe's 1.37 s wrap figure reads like an error against the ~65 s its own result implies, because the margin is budgeted against the hypothesis being refuted rather than the one expected. Said explicitly, along with why it matters: under the scaling hypothesis a raw 16-bit read across a 15 s interval would alias *towards* "about 1000/s" and manufacture the realtime answer it was meant to test for. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ord the line-cite rule tests/README.md carried a third copy of the rig's check decomposition and was the one site the 18->19 correction missed — the same defect this branch exists to fix, a count restated in more places than it is maintained. Rather than correct it to 19, that sentence now keeps the qualitative point (not every check is a fact about the cartridge) and defers the numbers to the rig's own docstring, which is where they move when a check is added. Also recorded, in engineering-notes beside the cites it applies to: five wrong line numbers into src/tls13.s were produced today by four parties, every one by reading a symbol tool's position output as a 1-based file line. The rule is to confirm any generated line number against a 1-based read of the file before it goes into a document; citing the span rather than the instruction is the cheap second defence, and the ground-truth list of the ten `sta tls_state` sites is there so the next reader does not have to re-derive it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two hardware experiments on the U64E, plus the code that made the second one runnable and the documentation that stops both from being misread.
The question
ip65/drivers/c64timer.stimer_initprograms CIA2 timer A to 1000 cycles, continuous, and cascades it into timer B;timer_readreturns timer B. CIA timers count phi2 cycles, so a "tick" is a millisecond only if phi2 is 1 MHz.ip65/ip65/dhcp.sbudgetsMAX_DHCP_MESSAGES_SENT = 12retries with growing backoff, about 15 seconds. If the CIAs scale with a 48 MHz CPU that budget collapses ~48x to ~0.3 s and DHCP cannot complete — which is whytests/rig_vice_https_macos.pyruns DHCP at 1x, and it was one of the two reasons the physical RR-Net rig had only ever run at stock speed.CLAUDE.md's CIA1 TOD result does not settle it, and was deliberately not used. TOD is a different clock domain, fed from mains rather than phi2; that it measured 0.996 of wall rate under turbo transfers nothing to timers A and B.
Experiment 1 — the timer rate (
tools/probe_cia_timer_rate.py, new)The CIA timers are realtime. Two things make that more than a ratio:
Method notes, because the aliasing trap here points the wrong way: timer B's inverted 16-bit up-count wraps every 65536 ticks, which is 65.5 s at 1 kHz but 1.37 s at 48 kHz, so a host reading raw values across a 15 s interval would alias towards the answer "1000/s". The probe therefore never exports a raw value — the C64 accumulates the modular 16-bit difference between consecutive samples into a 32-bit counter, at a loop period of microseconds against a 1.37 s worst case. Torn host reads are excluded by a REQ/SERVED publish handshake. CIA1 TOD is published as a control and read 0.996-1.004 of wall at both clocks.
Experiment 2 — the same fetch at 48 MHz (
TURBO_MHZ=48)43.1 s from
GtoCONNECTION CLOSED, against 1,979 s at 1 MHz — 46x. Same rig, same build configuration, same boundary, real silicon on both sides, so unlike the emulator comparison this one is like-for-like.DHCP completed on the automatic attempt with zero retries and the C64 held the pinned lease read out of its own
net_local_ip— experiment 1 confirmed on the live path, since a compressed budget would have failed here first.The thing this run existed to risk did not happen: the CS8900a behaved at 48 MHz. HTTP 200 and the exact 24-byte body out of the C64's own buffer,
net_last_error$00, ClientHello with SNI on the wire, application data both ways (2 records from the C64, 8 from the host), body never in cleartext with the positive control found twice, 20 frames sourced from00:0e:3a:64:64:64and no third MAC. Capture 14,344 → 23,417 bytes across the window. PRG sha2569d33683a…, byte-identical to an independent build of the same command line.The one red check, which is left red
24/25.
check_tls_connectedfailed withtls_state_max= 5 (CERT_VERIFY). It is a sampling oracle:src/tls13.s:303-304sets tls_state = CONNECTED right after the traffic-key derivation,tls_close(:379) writes it back to IDLE at:382-383, and at 48 MHz the client Finished, GET, response and close all fit inside one poll.tls_last_stateis no fallback —sta tls_last_stateappears exactly once, at:310, under@error:.The wire settles it independently: client Finished at +41.95 s, and the GET, response and close all complete by +42.34 s — a 0.39 s window against a 1 s poll, where the 1 MHz run had 17.7 s against a 5 s poll.
tls_state_max= 5 is exactly what a 0.4 s window predicts, and the ~45x ratio between those windows matches the 46x speed-up.Softening it to make a turbo run green is the exact failure this repo has recorded three times (#158, #161, #176), so it stays red and the limitation is documented in the rig, the notes and the README. The handshake's completion at turbo is inference from converging evidence, not the direct observation the 1 MHz run had, and every place that quotes the run says so. Issue #204 tracks the C64-side high-water latch that would make it direct again; scoped after the release, because it is a change to shipped source in the TLS state machine purely for test observability.
What is in the diff
tools/probe_cia_timer_rate.py— new. Assembles its own 271-byte 6502 probe with ca65/ld65 at runtime (verified byte-identical to the image actually run), so the measurement is reproducible rather than asserted.tests/rig_ip65_rrnet_hw.py—TURBO_MHZknob (default 1), turbo set before the reset with a 3 s settle and asserted against the device's own report, 1 s phase cadence at turbo, and docstring sections recording both the retired objection and the one that still stands.Two things do change at the default, both called out in the docstring and in CLAUDE.md: the settle went
sleep(1.0)→sleep(3.0)(harmless, and in the safe direction), and the new clock assertion fires unconditionally, so a stock re-run reports 25 checks, not the first passing run's 24. The extra one sits in the host-side-precondition group and is not a cartridge check; the first run's 24/8/16/15 decomposition stands as history. The assertion earns its place by catching a device that silently ignores the clock write — the same failure the probe's negative control guards against from the other side.Probe provenance, re-verified after the final edits. The image
assemble()produces from the committed docstring is 271 bytes, sha256c1d8bd07adc90691027673a6c8382a7425cb8f7d556be407fa42f769f8cbf3ad, and compares byte-identical to the binary actually written to the U64E during experiment 1. Checked once when the tool was written and again after every subsequent edit to the file, most recently atb6320d0.tests/rig_vice_https_macos.py— guard only. Warp accelerates the whole emulated machine, CIAs included, so the hardware-turbo result does not license removing its 1x DHCP assist. This is the misreading most likely to cost someone an afternoon, so the warning lives where they would go to make it.README.md,CLAUDE.md,docs/engineering-notes.md— the result, its scope and its one weakness.Scope, unchanged except for the clock
One device, one cartridge, one local listener, the same port-only deviation from the shipped image, and still no server-name validation on ip65. This adds a second clock and nothing else. Not known: whether any of it holds on the C64 Ultimate, at 64 MHz, or on a more marginal CS8900a.
Adversarially reviewed and cleared to merge. The reviewer reproduced the probe's provenance in one command from the committed source (271 bytes, sha256
c1d8bd07…, match confirmed), independently parsed the turbo capture (94 records, 20 from00:0e:3a:64:64:64, no third MAC, clean DORA with zero retries), and confirmed from the wire that the one red check is an oracle artifact rather than a client defect: client Finished at +41.95 s with the GET, response and close all complete by +42.34 s — a 0.39 s window against a 1 s poll, where the 1 MHz run had 17.7 s against a 5 s poll. Three findings from that review are fixed inb6320d0and96cf025.