# 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 |
