# The probe hardening and the 38400 retirement, verified > **Superseded in part.** This report records the build that retired 38400 by blanking its hot-key > code, and quotes that build's addresses and its "461 of 4096, in 32 region(s)" figure. The > retirement was later reversed: `wedgeDiagnosis.md` found the real cause (NMI re-entrancy > exhausting the stack) and `portCompatReport.md` measured 38400 as healthy once it was fixed, so > the hot key is back. The probe hardening described below is still current. See > `ADDRESS_CHANGES.md` for the addresses that have moved since. > **Note: the 38400 retirement has been reversed.** Blanking that entry's hot-key code hid a defect > rather than fixing one. `wedgeDiagnosis.md` established the defect - unbounded re-entrancy in > `commNmiHandler`, not the rate - and with the re-entrancy guard in place C= + 8 selects 38400 again > (`portCompatReport.md` sections 3 and 4, `testReport.md` section 11; `testAdopt.py` measured the key > putting `baudIndex` = 18 and the control register = `$1F`). The page-latch half of this report > stands, and section 5's "C= + 8 does nothing" is the *before* for that later measurement. Nothing > below has been rewritten: it is the record of the build of the day. Two changes went into the driver since `highRateReport.md` was written. The first had been looked at once - a re-run of `testStrap.py` on the `$DF00` strap, covering two link opens and no hot keys at all, quoted in `README.md` section 5 - and the second had not been checked at all: * **the page latch.** `aciaDetect` `$E644` used to probe `$DE00` on every call, and `loadBaudParameters` calls it on every link open **and** every baud hot key - so the old build wrote its probe pattern into `$DE03` once per open and once per rate change, on a live link, on a page that is very often some other cartridge's registers. `aciaPageLatch` `$E5BF` now records the answer, `aciaDetect` returns in five bytes while it is non-zero, and only `selectFramedLinkVectors` `$E3F3` - which `initCommModule` calls once, right after the overlay loads - puts it back to zero. The probe also became a two-pattern probe, because an empty `$DE00` reads back open bus. * **the 38400 retirement.** `baudEntryTable`'s last entry keeps its control byte `$1F`, but its hot-key code is now `$00`, which the search at `$E344` can never match because that search only runs on key codes with bit 7 set. 38400 wedged four boots out of four (`highRateReport.md` sections 5 and 6). Everything below is VICE 3.7.1 (`x64sc`), NTSC, `-acia1mode 1 -acia1irq 1`, base `$DE00` or `$DF00`, on **2026-08-22**, against `../Modem_Wars_SwiftLink.d64` built from the current source with `./build.sh`. Every emulator pair runs at **true C64 speed**: warp is used for the loader only and switched off through the monitor's own `warp` command before the options menu. As in every earlier session each run picked its own X display, its own TCP ports and its own private VICE config file, and only processes these scripts started were killed. Short answer: * **Cold detect still works on both straps.** On `$DE00` the probe writes `$1E`, reads `$1E` back, writes `$15`, reads `$15` back, and latches `$DE`. On `$DF00` it writes `$1E` to `$DE03`, reads back `$F6` - open bus - never writes the second pattern, and aims everything at `$DF00`. All six accessors at `$E551-$E56E` and `aciaPageLatch` `$E5BF` agreed on the right page on all four machines, immediately after the cold detect and again at the end of each run. * **The probe does not run again.** Across the two runs the link was opened twice per machine and six speed hot keys landed per machine - eight calls to `loadBaudParameters`, and so eight calls to `aciaDetect` - and the probe **ran once**. During the whole hot-key sweep the control register received exactly **six** writes, `$17 $18 $1A $1C $1E $15`: one per landed key, in the order the keys were pressed, with no probe pattern in front of any of them. * **Nothing goes to the other page.** On the `$DF00` strap each machine touched `$DE00-$DE03` exactly **twice in the whole session** - the single probe write and its single failed read-back, both at cold start - and **zero** times during the hot-key sweep or the 20-second census that followed. On the `$DE00` strap, `$DF00-$DF03` was touched **zero** times, ever. * **38400 is unreachable from the keyboard.** C= + 8 on a live phase-3 link, on both straps, changed neither the control register nor `baudIndex`, and the screen was pixel-identical before and after. A breakpoint on the swallow at `$E38A` caught the keystroke with `A` = `$B8` and `lastHotkeyCode` `$E046` = `$B8`, so the key does reach the hot-key layer and is answered with `$FF` = "no key". The other six rates all selected first press: `$15`, `$17`, `$18`, `$1A`, `$1C`, `$1E`. * **No regression at 1200 baud.** Two boots, four 75-second windows, 303.0 seconds of measured link time at true C64 speed; both machines reached connection phase 3 on both boots, **none of 592 one-second sampling intervals carried zero bytes**, `linkErrorCount` `$E047` read `$00` at all 24 window edges, and the transmitter-deadlock shape did not occur once. * **Two faults were found in the test rig, not in the driver**, and both are fixed. One of them was hiding every receive error: `testRealtime.py`'s tracepoint was still on `countLinkError`'s old address. Section 7. --- ## 1. The build under test ``` $ ./build.sh wrote ../Modem_Wars_SwiftLink.d64: module installed in track 18 sector 7 and track 34 sectors 1-15 $ python3 checkAbi.py build/swiftlinkDriverE000.bin ../disassembly/build/game_modemDriverE000.orig.bin bytes changed: 461 of 4096, in 32 region(s) ... ABI CHECK PASSED: every address the rest of the game depends on is unchanged ``` The bytes that carry the two changes are in the image: | Address | Bytes | What it is | |---|---|---| | `$E644` | `AD BF E5 D0 0B A9 DE 20 F2 E6 F0 04 A9 DF D0 0F` | `aciaDetect`: `LDA aciaPageLatch` / `BNE done` first, then the probe | | `$E6F2` | `20 63 E6 A9 1E 20 67 E5 49 1E D0 07 A9 15 20 67 E5 49 15 60` | `aciaProbe`: aim, write `$1E`, read back, write `$15`, read back | | `$E402` | `A9 00 8D BF E5 60` | the cold-start latch clear inside `selectFramedLinkVectors` | | `$E718` | `00 1F 00` | `baudEntryTable` index 18: control `$1F`, hot-key code **`$00`** | `scanIoAccess.py` re-run against this image still finds **exactly the six accessors and nothing else** naming an address in `$DE00-$DFFF`, in both its alignment-aware and its superset pass, and still finds the only pass-1 hits elsewhere to be the three instructions in the *stock* driver this build replaces. Three static facts about reachability, taken by scanning the whole 4096-byte image for `JSR`/`JMP` to each address: * `$E3F3` (`selectFramedLinkVectors`, the only thing that clears the latch) is reached from **one** instruction: `JSR $E3F3` at `$E2BA`, inside `initCommModule` - the `$E003` X=1 case, which the game calls once, from `$0F46`, immediately after the overlay is loaded. * `$E644` (`aciaDetect`) is reached from **one** instruction: `JSR $E644` at `$E353`, the first thing `loadBaudParameters` does. * `$E6F2` (`aciaProbe`) is reached from **one** instruction: `JSR $E6F2` at `$E64B`, inside `aciaDetect`, on the far side of the latch test. So the only way to probe the hardware at all is through the latch test, and the only way to clear the latch is a cold start. The rest of this report is that argument measured rather than read. ### How it was measured `testHarden.py` (new) boots **two** machines cross-connected through the socket null modem, so every measurement below is taken on a link that reached connection phase 3 - the game's own packet protocol - rather than on a machine talking to itself. It runs in four phases: the cold detect with both cartridge pages watched; the seven speed hot keys with the other page and the control register watched; a 20-second census of every register access; and the C= + 8 breakpoint. Unlike `testStrap.py`, which used **watchpoints** and stopped the emulator 800 times, this uses VICE **tracepoints**, which print the accessing instruction and its `A` register and let the machine run on. That matters: a watchpoint stopping the emulator on every ACIA access makes it impossible to press a hot key at true C64 speed, and it is also what let the census run for 20 seconds of live link without disturbing anything. ``` python3 testHarden.py ../Modem_Wars_SwiftLink.d64 de00 SWIFTLINK_ACIA_BASE=0xDF00 python3 testHarden.py ../Modem_Wars_SwiftLink.d64 df00 python3 testRealtime.py ../Modem_Wars_SwiftLink.d64 75 1200 hard1200 2 0 python3 testRealtime.py ../Modem_Wars_SwiftLink.d64 75 1200 hard1200b 2 0 ``` --- ## 2. Cold detect, on both straps Both machines of both pairs reached `connectionPhase` 3. These are the first accesses each machine made to either cartridge page, in order, decoded from the raw tracepoint logs. ### `$DE00` strap (`testLogs/harden.de00.A.cold.txt`; machine B is identical) ``` store $DE03 STA $DE03 at $E567 A=$1E aciaProbe pattern 1 load $DE03 LDA $DE03 at $E56B A=$1E read back through the fall-through - it matches store $DE03 STA $DE03 at $E567 A=$15 aciaProbe pattern 2 load $DE03 LDA $DE03 at $E56B A=$15 it matches too: a 6551 really is here, latch = $DE store $DE03 STA $DE03 at $E567 A=$15 loadBaudParameters: $10 OR 300-baud bits load $DE03 LDA $DE03 at $E56B A=$15 the read-back aciaSetControl falls into load $DE00 LDA $DE00 at $E55F A=$00 drain the receiver load $DE01 LDA $DE01 at $E558 A=$50 status store $DE02 STA $DE02 at $E554 A=$09 command: DTR on, RTS low, receiver IRQ on, transmit IRQ off ``` That is exactly the sequence `testReport.md` section 3 recorded, with the second probe pattern now in it. **Both probe patterns were needed and both came back**, which is the first time the second pattern has been seen to run at all. ### `$DF00` strap (`testLogs/harden.df00.A.cold.txt`; machine B is identical) ``` store $DE03 STA $DE03 at $E567 A=$1E aciaProbe pattern 1, on the default page load $DE03 LDA $DE03 at $E56B A=$F6 open bus - not $1E, so this page is not a 6551 store $DF03 STA $DF03 at $E567 A=$15 the fallback is taken: loadBaudParameters at $DF00 load $DF03 LDA $DF03 at $E56B A=$15 load $DF00 LDA $DF00 at $E55F A=$00 drain the receiver load $DF01 LDA $DF01 at $E558 A=$50 status store $DF02 STA $DF02 at $E554 A=$09 command ``` **The second pattern is never written on this strap**, because the first one already decided the question - which is the whole point of writing the cheap one first. The `$F6` the read returned is the value `probeOpenBus.py` saw in 51.7% of its 120 samples of an empty `$DE03`; nothing about it is `$1E`, and the probe fell through correctly. ### The six accessors and the latch Read back through the monitor after the cold detect, and again at the end of each run - all four machines, all eight readings: | Accessor | Instruction | Operand high byte at | `$DE00` run | `$DF00` run | |---|---|---|---|---| | `aciaCmdWrite` `$E554` | `sta $xx02` | `$E556` | `$DE` | `$DF` | | `aciaStatusRead` `$E558` | `lda $xx01` | `$E55A` | `$DE` | `$DF` | | `aciaDataRead` `$E55F` | `lda $xx00` | `$E561` | `$DE` | `$DF` | | `aciaDataWrite` `$E563` | `sta $xx00` | `$E565` | `$DE` | `$DF` | | `aciaCtrlWrite` `$E567` | `sta $xx03` | `$E569` | `$DE` | `$DF` | | `aciaCtrlRead` `$E56B` | `lda $xx03` | `$E56D` | `$DE` | `$DF` | | **`aciaPageLatch`** | - | **`$E5BF`** | **`$DE`** | **`$DF`** | The latch and the six accessors were in step in every reading, which is what makes the latch an honest stand-in for them. --- ## 3. The probe does not run again Two independent measurements say so, and neither depends on the other. ### 3.1 The link was opened twice, and the probe ran once `testHarden.openLink` offers A/O and space until an ACIA register is touched, and on both runs it took two rounds, so `openCommLink` - and therefore `loadBaudParameters`, and therefore `aciaDetect` - ran twice on every machine before the hot keys were even reached. What the control register received in that phase: | Run | Machine | Control-register writes, in order | Probe writes | Link opens | |---|---|---|---:|---:| | `$DE00` | A | `$1E $15` then `$15 $15` | 2 (one probe) | 2 | | `$DE00` | B | `$1E $15` then `$15 $15` | 2 (one probe) | 2 | | `$DF00` | A | `$1E` to `$DE03`, then `$15 $15` to `$DF03` | 1 (one probe) | 2 | | `$DF00` | B | `$1E` to `$DE03`, then `$15 $15` to `$DF03` | 1 (one probe) | 2 | The old build would have written the probe pattern once per open. This one wrote it once per cold start. ### 3.2 Seven hot keys on a live link, six control-register writes For this phase the tracepoints were narrowed to the two things the question turns on: **all four registers of the other page**, and **the control register of the strapped page**. (Data and command register traffic on the strapped page is the ordinary business of a running link - the census in section 4 counts it - and it would have buried the six accesses that matter.) The whole hot-key phase - 182 seconds of live phase-3 link between the end of the cold-detect log and the end of the sweep, seven keys pressed on each machine, C= + 1, C= + 2, C= + 4, **C= + 8**, C= + 9, C= + 0, C= + 3 - produced **twelve** accesses to that traced set on every one of the four machines, and they are the same twelve everywhere: ``` store $xx03 A=$17 load $xx03 A=$17 C= + 1 1200 store $xx03 A=$18 load $xx03 A=$18 C= + 2 2400 store $xx03 A=$1A load $xx03 A=$1A C= + 4 4800 store $xx03 A=$1C load $xx03 A=$1C C= + 9 9600 store $xx03 A=$1E load $xx03 A=$1E C= + 0 19200 store $xx03 A=$15 load $xx03 A=$15 C= + 3 300 ``` **Six writes for six landed keys, each one the rate that was asked for, in the order it was asked for, and nothing else at all** - no probe pattern in front of any of them, not one access to the other page, and nothing whatever attributable to C= + 8. The matching `load` after each `store` is not a second access to look at: `aciaSetControl` falls through a `NOP` into `aciaGetControl` by design, so every control write reads itself back, probe or not. Had the probe re-run on every hot key, as it did before the latch, this sweep would have looked quite different. On the `$DE00` strap each key would have written `$1E`, `$15` and then the rate: **eighteen** control-register writes instead of six. On the `$DF00` strap each key would have written `$1E` into `$DE03`, failed to read it back, and then written the rate into `$DF03`: six writes to a page this driver has no business touching, instead of none. **One thing in the script's own output should be ignored.** `testHarden.py` counts back-to-back `$1E`,`$15` pairs as "the probe's signature" and reported **1** on the `$DF00` run. That is a false positive and the heuristic is worthless: `$1E` is 19200's control byte and `$15` is 300's, and the last two keys in the sweep are C= + 0 followed by C= + 3. The pair it found is those two hot keys. The measurement that carries the claim is the one above - six writes, matching six keys - not that counter. --- ## 4. Nothing is written to the other page Counted the way `strapReport.md` counted it, per page, but over three separate phases of the same session rather than one 800-stop log. A tracepoint does not stop the emulator, so these are complete counts of a running machine rather than a sampled subset. ### `$DF00` strap - the case that matters | Phase | Machine | `$DE00-$DE03` | `$DF00-$DF03` | |---|---|---:|---:| | cold detect + two link opens + the Hayes string + byte sync | A | **2** | 530 | | | B | **2** | 433 | | the 182-second hot-key sweep, seven keys | A | **0** | 12 | | | B | **0** | 12 | | a 20-second census of a live phase-3 link | A | **0** | 2739 | | | B | **0** | 2444 | | **whole session** | A | **2** | 3281 | | | B | **2** | 2889 | The two accesses are the single probe write of `$1E` to `$DE03` and the single read that returned `$F6`. There is no third. The census breaks down as `$DF00` 591 loads / 662 stores (received and transmitted characters), `$DF01` 1246 loads (status reads, which are also the NMI acknowledge) and `$DF02` 240 stores (command `$09`/`$05` as the transmitter is armed and disarmed) on machine A - the ordinary traffic of a link carrying ARQ frames, and none of it on the wrong page. ### `$DE00` strap - the mirror-image check | Phase | Machine | `$DE00-$DE03` | `$DF00-$DF03` | |---|---|---:|---:| | cold detect + two link opens | A | 553 | **0** | | | B | 451 | **0** | | hot-key sweep | A | 12 | **0** | | | B | 12 | **0** | | 20-second census | A | 2721 | **0** | | | B | 2418 | **0** | Nothing on this build ever named `$DF00` when the cartridge answered at `$DE00`, which is what the six self-modified accessors being the module's only knowledge of the hardware predicts. --- ## 5. 38400 is unreachable, and the other six rates are not ### C= + 8 on a live link Pressed once on each machine, Commodore key (Tab) and `8` held down together for 600 ms - about 36 of the game's once-a-frame keyboard scans - with both machines in phase 3. State read immediately before and immediately after: | Run | Machine | before | after | |---|---|---|---| | `$DE00` | A | `baudIndex` 9, `$DE03` = `$1A`, phase 3 | `baudIndex` **9**, `$DE03` = **`$1A`**, phase 3 | | `$DE00` | B | `baudIndex` 9, `$DE03` = `$1A`, phase 3 | `baudIndex` **9**, `$DE03` = **`$1A`**, phase 3 | | `$DF00` | A | `baudIndex` 9, `$DF03` = `$1A`, phase 3 | `baudIndex` **9**, `$DF03` = **`$1A`**, phase 3 | | `$DF00` | B | `baudIndex` 9, `$DF03` = `$1A`, phase 3 | `baudIndex` **9**, `$DF03` = **`$1A`**, phase 3 | `$1F` was never written to any control register in any phase of either run, and the tracepoint on the control register recorded no access at all between the C= + 4 pair and the C= + 9 pair. The link survived it: the next hot key, C= + 9, landed normally on both machines in both runs. ### The keystroke reaches the driver and is swallowed "Nothing changed" on its own would also be what a key that never arrived looks like, so the swallow itself was caught. `$E38A` is the `LDA #$FF` that answers the game "no key" for any Commodore-key combination the hot-key layer does not recognise, and it is the only way out of that layer for C= + 8 now that the baud walk cannot match it. With a breakpoint armed there **before** the key went down (entering the monitor clears VICE's emulated key matrix, so the order matters): ``` #1 (Stop on exec e38a) 34/$022, 61/$3d .C:e38a A9 FF LDA #$FF - A:B8 X:00 Y:00 SP:e9 N.-..... 374331486 lastHotkeyCode $E046 = $B8, baudIndex $E055 = 0, bitPeriodLo $E056 = $15, $DE03 = $15 ``` `A` = `$B8` is the C= + 8 key code itself, arriving at the swallow; `$E046` = `$B8` says the hot-key layer logged it as a fresh Commodore-key code; and the rate bytes are untouched. The same breakpoint fired the same way on the `$DF00` run. So the key is seen, walked past every hot key in the chain including the baud table, and answered with `$FF` - which is what stops the game seeing it. Corroboration from the screen: `shots/hdde0003aBeforeC8.png` against `shots/hdde0004aAfterC8.png`, and `shots/hddf0003aBeforeC8.png` against `shots/hddf0004aAfterC8.png`, are **pixel-identical** (0 of 384x247 pixels differ in either pair). The game was on "OPPONENT PICKING GAME/MAP." and did not act on an `8`. ### The other six rates Every one landed on both machines, on both straps, at the first press: | Hot key | Rate | `baudIndex` `$E055` | Control register | `bitPeriodLo` `$E056` | Landed | |---|---:|---:|---|---|---| | C= + 3 | 300 | 0 | `$15` | `$15` | yes | | C= + 1 | 1200 | 3 | `$17` | `$17` | yes | | C= + 2 | 2400 | 6 | `$18` | `$18` | yes | | C= + 4 | 4800 | 9 | `$1A` | `$1A` | yes | | C= + 9 | 9600 | 12 | `$1C` | `$1C` | yes | | C= + 0 | 19200 | 15 | `$1E` | `$1E` | yes | | **C= + 8** | **38400** | **unchanged** | **unchanged** | **unchanged** | **no** | `$15`, `$17`, `$18`, `$1A`, `$1C` and `$1E` were each seen in the register with the matching `baudIndex` four times over - two machines, two straps. A note on what happened *around* the sweep, recorded because it is the honest picture: driving a pair up to 19200 and back down inside three minutes is not a healthy way to run a link, and it showed. On the `$DF00` run, one reading at 19200 caught machine A in phase 2 with `linkErrorCount` = 13 and `uartPendingCount` = `$C9`; by the next reading, before the following hot key, it was back to `linkErrorCount` = 0, `uartPendingCount` = `$00`, `txCharActive` = `$00` and a self-consistent command shadow, and the run finished with both machines linked at 300 baud and a 20-second census recording 2739 and 2444 register accesses. That is the "degrades and recovers" behaviour `highRateReport.md` section 3 measured at 9600 and 19200, not something new. Over all 56 state readings of the two runs, `linkErrorCount` was `$00` in 55 of them and the transmitter-deadlock shape - `txCharActive` set while command bit 2 is clear - occurred **zero** times. --- ## 6. No regression: 1200 baud, two boots, four windows `testRealtime.py`, 1200 baud selected with C= + 1 at the "PRESS A OR O" prompt on both machines before the link was opened, two consecutive 75-second windows per boot, nothing sent to either monitor inside a window. | Boot | Window | Wall | A speed | B speed | A -> B | B -> A | `$55` lead bytes | |---|---|---:|---:|---:|---|---|---| | 1 | 1 | 75.99 s | 100.9% | 100.5% | 3793 B = **49.91 B/s** | 3151 B = **41.46 B/s** | 697 / 714 | | 1 | 2 | 75.69 s | 101.3% | 100.9% | 3133 B = **41.39 B/s** | 3545 B = **46.84 B/s** | 778 / 727 | | 2 | 1 | 75.72 s | 101.2% | 100.8% | 2193 B = **28.96 B/s** | 2032 B = **26.84 B/s** | 631 / 705 | | 2 | 2 | 75.62 s | 101.2% | 100.8% | 3034 B = **40.12 B/s** | 2574 B = **34.04 B/s** | 530 / 494 | **303.0 seconds of measured link time, 12153 bytes one way and 11302 the other.** Every machine in every window ran at 100.5% of real time or better against its own free-running cycle counter, so this is true C64 speed and not warp. The five things this run had to answer, one at a time: * **Both machines reached phase 3, on both boots.** `waitForPhase` returned `{'A': 3, 'B': 3}` before the first window of each boot, and the `$55` counts - 494 to 778 ARQ frame lead bytes per window - say frames kept crossing for the whole of every window, which only happens in the packet phase. At the individual window edges `connectionPhase` was found at 2 as well as 3, which is the ordinary 1200-baud picture: a receive-ring overflow forces a byte-sync round, the round completes, the packet layer carries on. * **Traffic flowed both ways every second.** The relay's byte counts are sampled once a second inside each window: **592 one-second sampling intervals over the two boots - 148 per window, 74 in each direction - and not one of them carried zero bytes.** Per-interval rates ran from 11.9 B/s to 109.3 B/s, and 109.3 is 91% of the 120 B/s an 8N1 line carries at 1200 baud, so the line does still get saturated in bursts. * **`linkErrorCount` `$E047` stayed at `$00`.** All 24 state readings taken with the link up read `$00`. (The two readings taken *before* the link opened read 72 = `$48`, which is the leftover ASCII `H` the module ships with at `$E047`; `clearLinkVars` zeroes it when the link opens.) * **The transmitter never parked.** In all 24 live readings `txCharActive` `$E5BB` and `aciaCommandShadow` `$E5BE` were only ever the two self-consistent pairs `$00`/`$09` (idle) and `$01`/`$05` (a byte of ours in the chip). The fault shape - `$E5BB` set while `$E5BE` bit 2 is clear - occurred zero times, and `probeTxDeadlock` never fired, so nothing in these numbers is the test repairing the driver. `uartPendingCount` `$E0A5` read `$00`, `$01` or `$03` at every edge and was never once out of range. * **The errors that did occur were the documented ones.** The `countLinkError` tracepoint counted **22** rejected characters across the four windows (4, 4, 7, 7), and the conditional tracepoint on the `CPX #$14` inside `pushUartRxRing` returned **identical** counts (4, 4, 7, 7). Every single one was therefore a **receive-ring overflow, not a bad character** - the same fingerprint `rate1200Report.md` found for all 32 of its overflows. That the byte-sync rounds they caused completed is an inference here rather than a measurement: the `$E805`/`$E855` sync tracepoints were not armed in these windows, so what says the link recovered is that frames kept crossing, that no sampling interval carried nothing, and that `connectionPhase` was still being found at 3 at the window edges. Neither emulator logged a CPU JAM. That is the same overflow rate `rate1200Report.md` measured on the pre-hardening build, to within the noise: 32 in 452 s there, 0.071 a second, against 22 in 303 s here, 0.073 a second. Two boots is not enough to call a 3% difference anything at all; what the two runs agree on is the shape - every error a ring overflow, every overflow followed by a recovery, and the link never stopping. --- ## 7. Two faults in the test rig, found and fixed Both are in the rig, not the driver, and both would have made this report wrong. * **`testRealtime.py` was tracing an address that no longer exists.** `countLinkError` moved from `$E403` to `$E73F` when `selectFramedLinkVectors` had to grow the five bytes that clear the page latch - that move is part of the hardening being verified here. `testRealtime.py` and `testHighRates.py` still said `trace exec $e403`, and `$E403` is now the operand byte of an `LDA #$00`: the program counter never equals it, so the tracepoint can never fire. Left alone, this report would have claimed **zero** receive errors at 1200 baud with a straight face. Both scripts now trace `$e73f`, which is where the 22 overflows above came from. * **The tracepoint parsers dropped every access whose value contained a hex letter.** VICE prints the program counter in lowercase (`.C:e567`) and the register values in uppercase (`- A:1E`), and the `DISASM` pattern in `testStrap.py` - which `testHarden.py` inherited - matched `A:` only against `[0-9a-f]`. So `A:1E` did not match, the header was paired with a later line, and 58 of 553 accesses in the first log were mis-parsed, including the probe's own first write. The pattern now accepts both cases in `testStrap.py` and `testHarden.py`, and `parseTrace.py` (new) re-parses a saved log offline, which is where every number in sections 2 to 4 comes from. The raw logs were never affected - only the parsing of them. --- ## 8. What this does not say * **No real hardware, and no C64 Ultimate.** Everything here is VICE 3.7.1. Nothing has been run on a physical SwiftLink, a real cable, a real modem, or the device this exercise is aimed at. * **VICE's open bus is not the C64's.** The `$DF00` strap's probe read returned `$F6` because that is what VICE puts on the bus for an unmapped I/O read at that moment. The case the second pattern exists for - a page that answers the first pattern and not the second - has still **never been produced**, on this rig or any other, and cannot be: on this rig the first pattern decides every case. The two-pattern probe is therefore *installed and exercised on the path where both patterns match*, and unexercised on the path it was written for. * **VICE does not emulate the register mirrors**, so "nothing touched the other page" is a statement about the four bytes VICE maps. `testHarden.py` did not arm the mirror tripwire that `testStrap.py` uses on `$DE04-$DEFF` / `$DF04-$DFFF`; that tripwire recorded 0 stops in every run of `strapReport.md` section 7.3, and the static scan in section 1 above still finds no instruction in this build that could reach it. * **The C= + 8 evidence is in two halves and each is partial.** During the rate sweep the proof is negative - nothing changed - because `lastHotkeyCode` is overwritten with `$FF` by the next keyboard scan, so the snapshot cannot show that the key arrived. The breakpoint run proves the key arrives and is swallowed, but it stops the emulator to do it. Neither run watched what the *game* does with a plain `8` in a context where `8` means something, because the game was on "OPPONENT PICKING GAME/MAP." both times. * **`aciaDetect` still does not verify `$DF00`.** When `$DE00` does not answer, the accessors are aimed at `$DF00` on trust and the latch is set - so a machine with no cartridge on either page latches `$DF` and never probes again until the module is reloaded. That is deliberate (the source says so), it is unchanged by this work, and it was not tested. * **The 20-second census is 20 seconds.** It says nothing about a machine that has been linked for an hour, and the whole live-link portion of each `testHarden.py` run is about four minutes. * **Four of the twelve tracepoint logs end with one header whose disassembly line was cut off** by the end of the capture window; `parseTrace.py` warns about it. They are the `$DE00` run's two cold logs and its machine-A census, and the `$DF00` run's machine-B cold log - one access out of 553, 451, 2721 and 435 respectively. Each is counted in the per-address totals, because the header carries the address and the direction, but contributes no value byte. * **The transmit-overwrite counts are noise at this sample size.** VICE logs `ACIA: data register written although data has not been sent yet.` when the driver writes a data register that has not emptied - one transmitted character lost, recovered by the ARQ layer. The four `testHarden.py` machines logged 27, 21, 108 and 87 of them, and the four 1200-baud machines logged 32, 100, 12 and 28. Two runs of the *identical* script differed by a factor of four, and no pre-hardening run ever counted them at 1200 baud, so these numbers cannot be read as a regression or as its absence. What can be said is that no window lost the link and every receive error counted at 1200 was a ring overflow rather than a bad character. * **One boot per strap for the hardening runs, two for 1200 baud.** Nothing here is a soak test. * **No complete game was played**, at either strap or either rate. --- ## Files | File | What it is | |---|---| | `testHarden.py` | new: two machines, either strap, tracepoints on both cartridge pages; the cold detect, the seven hot keys on a live link, a 20-second access census, and the C= + 8 breakpoint | | `parseTrace.py` | new: re-parses a saved tracepoint log into an ordered access list and a per-address / per-page census | | `testRealtime.py`, `testHighRates.py` | changed: the `countLinkError` tracepoint moved from `$E403` to `$E73F` | | `testStrap.py` | changed: the register-value pattern now accepts uppercase hex | | `testLogs/harden.de00.txt`, `harden.df00.txt` | the two `testHarden.py` transcripts | | `testLogs/harden.{de00,df00}.{A,B}.{cold,hotkeys,census}.txt` | the twelve raw tracepoint logs every count above is taken from | | `testLogs/harden1200.txt`, `harden1200b.txt` | the two 1200-baud transcripts | | `testLogs/realtime.hard1200*.w{1,2}.samples.txt` / `.trace.txt` | per-second byte counts and raw tracepoint output for the four windows | | `shots/hdde00*.png`, `shots/hddf00*.png`, `shots/rthard1200*.png` | the screenshots cited above |