# The $DF00 strap option, exercised

> **Note: the two probe weaknesses in section 8 have since been closed.** The probe now uses two
> patterns and requires both to read back, and `aciaPageLatch` `$E5BF` latches the page so that it is
> decided once per cold start instead of on every link open and every rate change - the two of the
> three hardenings section 8 suggested. `hardenReport.md` measures both. One further change since:
> the `$DF00` fallback now *reads* that page's control register before it writes a probe pattern into
> it, so a cartridge on this strap has its own settings adopted rather than the open bus's
> (`portCompatReport.md` section 9.1, `testReport.md` section 11.6). The run below is left as it was
> taken.
>
> **Note on addresses.** This report records a run made before the probe-hardening pass, which
> relocated six internal helpers. Addresses quoted below are the ones those routines had at the
> time of the run. See `ADDRESS_CHANGES.md` for the mapping onto the current source.

A SwiftLink has a jumper that moves its 6551 from I/O1 (`$DE00`) to I/O2 (`$DF00`), and a C64
Ultimate's SwiftLink emulation can be configured either way.  The module probes `$DE00` first and
falls back to `$DF00`, but every run in `testReport.md`, `realtimeReport.md` and `hotkeyReport.md`
answered at `$DE00` on the first try, so **the second half of `aciaDetect` had never executed**.
That is the loose end this report closes.

Everything below was observed in VICE 3.7.1 (`x64sc`) on 2026-08-22, against
`../Modem_Wars_SwiftLink.d64` built from the current source with `./build.sh`
(`checkAbi.py`: 452 of 4096 bytes changed in 24 regions, `ABI CHECK PASSED`).  Nothing is inferred
from the source alone unless it says so.  As in every earlier session, each run picked its own X
display, its own TCP ports and its own VICE config file, and only processes these scripts started
were killed.

Short answer:

* **The fallback works.**  With the cartridge at `$DF00` the probe writes `$1E` to `$DE03`, reads
  back something that is not `$1E`, re-aims the six accessors at `$DF00`, and every subsequent access
  in an 800-stop watchpoint log lands on `$DF00-$DF03`.  All six accessor operands read `$DF` at run
  time and the module image on disk still says `$DE`.
* **The game behaves identically.**  Same options menu, same module load, same
  "PICK UP PHONE THEN PRESS SPACE." prompt, same `$E000` jump table, same Hayes string on the wire.
* **Two machines at `$DF00` link and carry real ARQ frames.**  Both reached connection phase 3; at
  true C64 speed 300 baud gave 29.62 and 28.99 bytes/s with 599 and 682 frame lead bytes and zero
  link errors, and 2400 baud gave 8.98 bytes/s each way with no transmitter deadlock.  A warp run got
  as far as the side-0/side-1 negotiation with a setup-overlay disk load over the live link.
* **Nothing else in the game touches the page.**  Two static passes over every image the C64 loads
  find exactly one instruction that can reach `$DF00-$DFFF`, and it is the stock driver's
  `sta $DF59,y`, which this build does not contain.  A watchpoint on `$DE04-$DEFF` and `$DF04-$DFFF`
  - the register mirrors - never fired in any run.
* **Two things about the probe are worth knowing before trusting it on hardware**, and neither is a
  regression: the read of an empty `$DE00` is open bus and is not deterministic, and the probe re-runs
  on every link open and every baud hot key.  Section 8.

---

## 1. How the rig was pointed at the other base

`x64sc` takes `-acia1base 0xDF00` and accepts it: the monitor's `io` command then reports

```
Swiftlink:
>C:df00  00 30 00 00
```

and reads of `$DE00-$DE07`, `$DF40-$DF47` and `$DFFC-$DFFF` all return open bus.  **VICE maps only
the four bytes `$DF00-$DF03`** - it does not emulate the register mirrors a real cartridge has, which
matters for how section 7's tripwire has to be read.  (`probeBankOut.py`.)

The rig learned the strap rather than having it hard-coded.  `viceHarness.py` gained

```
ACIA_BASE = int(os.environ.get("SWIFTLINK_ACIA_BASE", "0xDE00"), 16)
```

and `aciaArgs()` defaults its `base` to it; `testBoot.py`, `testModemSelect.py` and `testRealtime.py`
now build their monitor commands from `ACIA_BASE` instead of the literal `de00`.  **The default is
unchanged**, so every command line in the earlier reports still means what it meant.  A `$DF00` run
is the same command with `SWIFTLINK_ACIA_BASE=0xDF00` in front of it.

| run | command |
|---|---|
| boot | `SWIFTLINK_ACIA_BASE=0xDF00 python3 testBoot.py ../Modem_Wars_SwiftLink.d64 strapDf00Boot` |
| modem select | `SWIFTLINK_ACIA_BASE=0xDF00 python3 testModemSelect.py ../Modem_Wars_SwiftLink.d64 strapSelectDf00` |
| probe + mirrors | `SWIFTLINK_ACIA_BASE=0xDF00 python3 testStrap.py ../Modem_Wars_SwiftLink.d64 strapDf00` |
| two machines, true speed | `SWIFTLINK_ACIA_BASE=0xDF00 python3 testRealtime.py ../Modem_Wars_SwiftLink.d64 60 300 strapdf 1 1` |
| two machines, mirrors armed | `SWIFTLINK_ACIA_BASE=0xDF00 python3 testStrapPair.py ../Modem_Wars_SwiftLink.d64 150` |

`testStrap.py` is new: it watches **both** pages at once, which is what makes the fallback visible
rather than merely plausible.

---

## 2. The probe takes the fallback

`testStrap.py` arms four watchpoint pairs before the module can touch anything - `$DE00-$DE03`,
`$DF00-$DF03`, `$DE04-$DEFF`, `$DF04-$DFFF` - and then answers the modem prompts.  These are the
first six stops of the run, verbatim from `testLogs/strapDf00.hits.txt`:

```
#1 (Stop on store de03)
.C:e567  8D 03 DE    STA $DE03      - A:1E X:00 Y:24 SP:ed ..-.....  200460609
#5 (Stop on  load de03)
.C:e56b  AD 03 DE    LDA $DE03      - A:00 X:00 Y:24 SP:ed ..-...Z.  200460625
#2 (Stop on store df03)
.C:e567  8D 03 DF    STA $DF03      - A:15 X:00 Y:24 SP:ef ..-.....  200460709
#6 (Stop on  load df00)
.C:e55f  AD 00 DF    LDA $DF00      - A:00 X:00 Y:24 SP:ef ..-...Z.  200460828
#6 (Stop on  load df01)
.C:e558  AD 01 DF    LDA $DF01      - A:50 X:00 Y:24 SP:f1 ..-.....  200460841
#2 (Stop on store df02)
.C:e554  8D 02 DF    STA $DF02      - A:09 X:00 Y:24 SP:f3 ..-.....  200460975
```

Read it against `aciaDetect` `$E644`:

1. `LDA #$DE` / `JSR aciaSetPage` aims all six accessors at `$DE00`; `aciaProbe` `$E6F2` writes its
   pattern `$1E` through `aciaCtrlWrite` - and the instruction at `$E567` is disassembled as
   `STA $DE03`, so the self-modified operand really did still say `$DE`.
2. `aciaCtrlRead` at `$E56B` reads it back and gets **`$00`**.  `CMP #$1E` fails, `Z = 0`, `BEQ` is
   not taken, and `LDA #$DF` / `JSR aciaSetPage` re-aims the accessors.  **The fallback is taken.**
3. The very next access is `STA $DF03` with `A = $15` from the same instruction at `$E567` - now
   disassembled as `STA $DF03`.  That is `loadBaudParameters` writing the real control byte: 8N1,
   internal baud rate generator, 300 baud on a SwiftLink.
4. `LDA $DF00` drains the receiver, `LDA $DF01` reads the status ( `$50` ), and `STA $DF02` writes
   command `$09` - DTR on, RTS low, receiver interrupt on, transmit interrupt off.

That is exactly the opening sequence `testReport.md` section 3 recorded at `$DE00`, with one probe
failure in front of it and every register three pages higher.

The link was opened twice in that run - the script offers A and space repeatedly until the ACIA is
touched, so `configureUserPortLines` ran a second time - and the second probe read returned **`$FF`**
where the first returned `$00`.  Same decision, different floating byte; see section 8.

**Nothing else ever touched the `$DE00` page.**  Over the whole 800-stop log:

| range | stops | what they were |
|---|---:|---|
| `$DE03` store | 2 | the probe pattern `$1E`, once per link open |
| `$DE03` load | 2 | the probe read-back: `$00`, then `$FF` |
| `$DE00`, `$DE01`, `$DE02` | 0 | - |
| `$DF03` store | 2 | control `$15`, once per link open |
| `$DF02` store | 387 | command `$09` / `$05` as the transmitter is armed and disarmed |
| `$DF01` load | 203 | status reads, which are also the NMI acknowledge |
| `$DF00` store | 202 | transmitted characters |
| `$DF00` load | 2 | the receiver drain in `configureUserPortLines` |
| `$DE04-$DEFF`, `$DF04-$DFFF` | **0** | the mirrors - see section 7 |

---

## 3. The six accessors end up pointing at `$DF00`

`aciaSetPage` `$E663` patches the operand high byte of all six one-instruction accessors.  Those six
bytes are the driver's entire record of where the hardware is.  Read back through the monitor after
the link opened, on both machines of the two-machine run:

| accessor | instruction | operand high byte | on the disk | at run time |
|---|---|---|---|---|
| `aciaCmdWrite` `$E554` | `sta $xx02` command | `$E556` | `$DE` | **`$DF`** |
| `aciaStatusRead` `$E558` | `lda $xx01` status | `$E55A` | `$DE` | **`$DF`** |
| `aciaDataRead` `$E55F` | `lda $xx00` receive | `$E561` | `$DE` | **`$DF`** |
| `aciaDataWrite` `$E563` | `sta $xx00` transmit | `$E565` | `$DE` | **`$DF`** |
| `aciaCtrlWrite` `$E567` | `sta $xx03` control | `$E569` | `$DE` | **`$DF`** |
| `aciaCtrlRead` `$E56B` | `lda $xx03` control read-back | `$E56D` | `$DE` | **`$DF`** |

The raw dump, which is the same thing without the interpretation:

```
>C:e540  ae 55 e0 20  53 e3 a9 09  20 51 e5 20  5f e5 4c 58
>C:e550  e5 8d be e5  8d 02 df 60  ad 01 df 8d  bc e5 60 ad
>C:e560  00 df 60 8d  00 df 60 8d  03 df 60 ad  03 df 60 ea
```

The "on the disk" column is not an assumption: `scanIoAccess.py` decoding
`build/swiftlinkDriverE000.bin` prints `sta $DE02`, `lda $DE01`, `lda $DE00`, `sta $DE00`,
`sta $DE03`, `lda $DE03` at those six addresses, so the image is unmodified and the `$DF` bytes are
the running driver's own work.

---

## 4. The control and command registers really are written at `$DF00`

From the ordered log in section 2, and confirmed by a direct read of the chip at the end of the run
(`m df00 df03` = `00 10 05 15`):

* **control `$DF03` = `$15`** - `$10` (8 data bits, one stop bit, internal baud rate generator) OR
  the rate bits for 300 baud on a SwiftLink's doubled crystal.
* **command `$DF02` = `$09`** when the transmitter is idle (DTR on, RTS low, receiver IRQ enabled,
  transmit IRQ disabled) and **`$05`** whenever there is something to send, exactly the pattern
  `testReport.md` recorded at `$DE00`.
* **data `$DF00`** carried `$0D $41 $54 $51 $30 $56 $31 $58 $31 $41 $0D` = CR `ATQ0V1X1A` CR, the
  answer-mode Hayes string, followed by the `$00` stream of `beginByteSyncPhase`.  The TCP sink at
  the far end of the emulated serial line received 8552 bytes beginning
  `b'\rATQ0V1X1A\r\x00\x00\x00...'`, so those writes left the emulated chip.

A **baud hot key** also reaches the right page.  In the two-machine real-time run, machine B's
C= + 2 landed and the state readback showed `baudIndex=6` with **`$DF03` = `$18`** (2400 baud), i.e.
`loadBaudParameters` -> `aciaSetControlIdle` -> `aciaCtrlWrite` wrote the control register at the
strapped base.  (Machine A's key did not land that time and the script fell back to writing `$DF03`
from the monitor, which is the documented fallback in `testRealtime.setBaud`.)

---

## 5. The game reaches the modem prompts exactly as it does at `$DE00`

* **Options menu.**  `shots/strapDf00BootMenu.png` against the `$DE00` build's
  `shots/swiftlinkMenu.png`: 1551 pixels differ, every one of them inside `y = 119..126`, the single
  character row the menu highlight blinks on.  Everything else is identical.
* **The right module is loaded.**  After COMPETE WITH MODEM OPPONENT, `$E000` read
  `4c 11 e1 4c 9a e2 4c a9 e0 4c f6 e0 4c cb e3 60` - byte for byte the same jump table
  `testReport.md` section 2 recorded at `$DE00`.
* **The prompts.**  `shots/strapSelectDf0003afterOpen.png` and `shots/strapDf0003later.png` both show
  "PICK UP PHONE THEN PRESS SPACE." over the options menu.  Against the `$DE00` run's
  `shots/modemPromptPressSpace.png` each differs by 403 pixels, all inside `y = 200..206` - the status
  line's own blink row.
* **No ACIA register is touched until both prompts are answered.**  The watchpoints on all four
  ranges were armed at the options menu and the first stop came only after the A/O and space
  answers, which is the same result the `$DE00` run got.
* **The NMI vector is installed.**  `d fffa fffb` disassembles the two bytes as `85 E6`, i.e.
  `$E685` = `commNmiHandler`.

---

## 6. Two machines at `$DF00`

### At true C64 speed (`testRealtime.py`, 60-second windows)

Both machines reached **connection phase 3**, the game's own packet protocol.

| rate | direction | bytes | throughput | frame lead bytes `$55` | `countLinkError` `$E403` | receive-ring overflow `$E505` | transmitter deadlock |
|---|---|---:|---|---:|---:|---:|---|
| 300 | A -> B | 1782 | 29.62 B/s = 296 bit/s | 599 | 0 | 0 | no |
| 300 | B -> A | 1744 | 28.99 B/s = 290 bit/s | 682 | 0 | 0 | no |
| 2400 | A -> B | 540 | 8.98 B/s | 203 | 0 | 0 | no |
| 2400 | B -> A | 540 | 8.98 B/s | 203 | 0 | 0 | no |

Per second, from `testLogs/realtime.strapdf.*.samples.txt`: at 300 baud 27 to 32 bytes in each
second on conn0 and 14 to 31 on conn1, at 2400 baud 6 to 12 each way, and **no second at zero in
either window**.

The emulators' own cycle counters put both windows at 100.7% to 101.2% of real time, so this is true
C64 speed and not warp.  30 bytes/s is the whole of what 300 baud can carry at 8N1, and the 2400-baud
figures match `realtimeReport.md` section 8's post-fix `$DE00` numbers (8.94 to 8.98 B/s) to within a
byte.  The `$55` counts are the ARQ frame lead byte: these are real frames, not a byte-sync flood.
The 2400-baud rate was reached by switching on a live link, and the link came back to phase 3 after
the switch.

### In warp, with the mirror tripwire armed (`testStrapPair.py`, 150-second window)

| direction | bytes | frame lead bytes `$55` | eight-byte `$00` runs |
|---|---:|---:|---:|
| conn0 -> peer | 21642 | 5988 | 53 |
| conn1 -> peer | 20511 | 6259 | 30 |

Both machines ended in `connectionPhase = $03`, `isLinkActive = $01`, `linkErrorCount = $00`, with
all six accessors still reading `$DF`.  The tail of the relay log is frame traffic -
`55 ed 54 55 05 6b 55 05 6b ...` - lead byte, then length/control/sequence/checksum.

More useful than the byte count: **machine A reached the GAME TYPE menu and machine B showed
"OPPONENT PICKING GAME/MAP."** (`shots/pairDf0003aLater.png`, `shots/pairDf0003bLater.png`).  That is
the state the side-0/side-1 negotiation ends in, and getting there means the two agreed over the link
which of them plays which side, and that machine A loaded the setup overlay from disk **while the
link was open** - the suspend/resume handshake - with the mirror watchpoints armed throughout.

---

## 7. The hazard: does anything else reach into the `$DF00` page?

This is the question `spec_uart.md` raised (hazard 5, and the `loadBaudParameters` note): a real
SwiftLink decodes only A0 and A1 inside its page, so all 64 four-byte windows of `$DF00-$DFFF` are
the same four registers.  Any read where `address AND 3 = 0` consumes a received character and clears
RDRF; where `address AND 3 = 1` it clears the interrupt flag.  And a 6502 absolute-indexed **store**
performs a dummy **read** at the un-carried address first, so `sta $DF59,y` reads `$DF56`, `$DF57`,
`$DF58` on its way - and `$DF58 AND 3 = 0` is the data register.  That instruction is at `$E358` in
the stock module.  It is gone from this build; the question is whether anything else does it.

### 7.1 Static: two passes over every image the C64 loads, and one over the indirect pointers

`scanIoAccess.py` (new, in this directory) disassembles the code regions of all 22 loaded images -
the complement of `disassembly/build/dataMap.json`, so data tables are not decoded as instructions -
and reports every absolute or absolute-indexed instruction whose effective address, or whose dummy
read, can land in `$DE00-$DFFF`.  Whole-game result:

```
game/modemDriverE000 $E358: sta $DF59,y   reaches $DF59-$E058, dummy read in $DF00-$DFFF
game/modemDriverE000 $E657: sta $DD05,x   reaches $DD05-$DE04, dummy read in $DD00-$DDFF
game/modemDriverE000 $E65D: sta $DD04,x   reaches $DD04-$DE03, dummy read in $DD00-$DDFF
```

**All three are in the stock modem driver, which this build replaces.**  `$E358` is the known one.
The other two are `setBitPeriod` writing CIA2's timer latches with X = 0 or 2, so they reach `$DD04`
to `$DD07` in practice and their dummy read is in the CIA2 page, not the cartridge page; in this
build both addresses are a bare `RTS`.  Nothing in the main program, the overlays, the text engine,
the high-memory block, the fast loader, the trainer or the trainer overlays appears at all.

The same scan over `build/swiftlinkDriverE000.bin` finds only the six accessors, all plain absolute:

```
swiftlinkDriverE000.bin $E554: sta $DE02      $E558: lda $DE01      $E55F: lda $DE00
swiftlinkDriverE000.bin $E563: sta $DE00      $E567: sta $DE03      $E56B: lda $DE03
```

A second, alignment-independent pass decodes **every** three-byte window of every image as if it were
an instruction, which catches a misaligned entry, a self-modified operand or code hidden inside a
data region.  It adds fourteen hits and every one was checked by hand against the `.s` files:

| hit | what it really is |
|---|---|
| `boot/titleBitmapA000` x 9 | title-screen bitmap data; the file is data end to end and is never executed |
| `boot/fastLoaderC000 $C047`, `game/mainProgram0800 $08A8` | the middle of `lda $DD00` / `and #$DF` - `AD 00 DD 29 DF` read one byte late |
| `game/mainProgram0800 $0D9B` | inside the keyboard matrix code table at `$0D98`; the `$DE` is a PETSCII code |
| `game/mainProgram0800 $1E76` | the operand high byte of `sta scrollScreenStepOpcode` at `$1E74` |
| `game/modemDriverE000 $E358` | the stock `sta $DF59,y` again |

So the superset pass adds nothing: **no instruction in the shipped code of this build can name an
address in the cartridge page except the six accessors.**

### 7.2 The one place the game does address `$DF00-$DFFF` - and why it is safe

Neither static pass can see an operand that only exists at run time, and there is one:
`copyPageUnderIo` `$58B2` copies 256 bytes with `lda abs,y` / `sta abs,y` whose operands its callers
patch.  The film-buffer callers walk a whole page pointer over the film:

```
7F64  lda #$D0 / sta D_58C7    ; "the destination page walks $D000..$DF00"   (load a saved film)
816C  lda #$D0 / sta D_58C4    ; "the source page walks $D000..$DF00"        (save a film)
```

and `readFilmByte` `$5874` / `writeFilmByte` `$5893` dereference `filmPtr` `$BC/$BD`, which
`rewindFilmPtr` `$58D5` sets to `$D000` and which then walks the same 4 KB.

`scanIoAccess.py` pass 3 lists all eighteen zero-page pointers the loaded code dereferences with
`(zp),y` or `(zp,x)` and the immediate high bytes stored into each.  **`$BC/$BD` is the only one whose
high byte is ever given a value in `$D0-$DF`**, and it has exactly two indirect uses in the whole
game - `$5885` and `$58A4`, the two inside those routines.  That pass is a narrowing, not a proof: a
pointer can also be given a computed high byte, and `filmPtr` is, since it is incremented across the
buffer.  What closes it is that the two instructions which dereference it are both inside the banked-
out window below.

All four instructions - the two inside `copyPageUnderIo` and the two that dereference `filmPtr` -
run with **`$01 = $34`**, which replaces the `$D000-$DFFF` I/O block with the RAM underneath, and
every one of the three routines puts `$01` back afterwards.  That is checked in the emulator rather than
argued (`probeBankOut.py`):

```
control register written through the I/O window: $1E
same address written again with $01 = $34, read back banked out: $55
control register with the I/O area banked in again: $1E
the banked-out write reached the cartridge: no, it went to RAM
```

The write to `$DF03` with the I/O area banked out landed in RAM and the 6551's control register was
untouched, which is what a real C64's PLA does too: it does not assert /IO2 in a memory configuration
that has no I/O.  So the film system's traffic through `$DF00-$DFFF` cannot reach a `$DF00`-strapped
cartridge.

This is also the reason `DESIGN_NOTES.md` item 1 exists - the NMI handler has to bank the I/O area
back in itself, because it can fire in the middle of exactly these routines.

### 7.3 Empirical: a tripwire on the mirrors

VICE maps only `$DF00-$DF03`, so `$DE04-$DEFF` and `$DF04-$DFFF` answer nothing in the emulator -
which makes them a clean tripwire.  A watchpoint there costs nothing while it stays silent, and a
stop is an instruction reaching into the cartridge page that would be a register access on real
hardware.

| run | what it covered | mirror stops |
|---|---|---:|
| `testStrap.py` | options menu, module load, both prompts, link open, Hayes dialogue, byte sync, then 45 s more with only the mirror watch armed | **0** |
| `testStrapPair.py` machine A | boot to the GAME TYPE menu, including a setup-overlay disk load over the live link, 150 s window | **0** |
| `testStrapPair.py` machine B | boot to "OPPONENT PICKING GAME/MAP.", 150 s window | **0** |

**Verdict.**  Static and empirical agree: with the cartridge at `$DF00`, the only code that reaches
the page is the driver's six accessors, and the only other code that names addresses in it - the film
buffer - does so with the I/O area banked out, where the cartridge is not selected.

---

## 8. What this does not say, and two things worth knowing

* **No real hardware.**  Every result is VICE 3.7.1 with `-acia1mode 1 -acia1base 0xDF00
  -acia1irq 1`.  Nothing has been run on a physical SwiftLink or on a C64 Ultimate.
* **VICE does not emulate the register mirrors.**  Its SwiftLink claims four bytes, not the page.  So
  section 7.3's tripwire proves that no instruction *addresses* the mirror range; it cannot show what
  a real cartridge would *do* if one did.  The mirror behaviour itself is taken from `spec_uart.md`,
  not observed here.  The same limitation means an accidental mirror access would be harmless in this
  emulator and destructive on hardware - which is precisely why the static scan matters.
* **The probe's read of an empty `$DE00` is open bus, and it is not deterministic.**  On a C64 an
  unmapped I/O read returns the last byte the VIC put on the bus.  The driver's own two probe reads
  in one session returned `$00` and `$FF`; a memory dump later in the same session read
  `f6 f6 f6 f6` across `$DE00-$DE03`; a bare machine sitting in the KERNAL read `$00` in one run and
  `$FF` in another; and `probeOpenBus.py`, sampling `$DE03` 120 times at different points in the frame
  a few seconds after the loader finished, got:

  ```
  $F6  62 times (51.7%)     $2C  32 times (26.7%)
  $2D  20 times (16.7%)     $2E   6 times (5.0%)
  reads that would have made the probe believe an ACIA is at $DE00 ($1E): 0 of 120
  ```

  So on this screen the byte is nowhere near `$1E` - but the value is screen data the driver does not
  control, and nothing rules `$1E` out on a different screen.  If it ever reads back `$1E`, the probe
  concludes the cartridge is at `$DE00`, aims all six accessors at open bus, and the link is dead
  until something re-probes.  Note also that when the probe *does* fall back, it does not verify
  `$DF00` at all - `aciaDetect` says so in its own comment: "aim the accessors there and hope".
* **The probe re-runs on every open and every rate change.**  `aciaDetect` is the first thing
  `loadBaudParameters` `$E353` does, and the baud hot keys fall through into `loadBaudParameters` from
  `$E350`.  Two consequences on a `$DF00` machine: every speed hot key re-decides which page the
  cartridge is on, so the open-bus gamble above is taken again on a **live** link; and every open and
  every rate change writes `$1E` into `$DE03`, which is a foreign device's register if anything else
  is plugged in at `$DE00` - and something else at `$DE00` is a common reason to strap a SwiftLink to
  `$DF00` in the first place.  If that device reads back `$1E` from its own offset 3, it shadows the
  SwiftLink permanently.

  None of this is a regression and none of it was hit in any run here.  Three cheap hardenings, in
  increasing order of intrusiveness, if it is ever worth closing: probe with two different patterns
  and require both to read back; latch the page once a probe has succeeded so a live link cannot be
  re-aimed; or probe only on a cold open rather than on every rate change.
* **Only 300 and 2400 baud were exercised at `$DF00`**, matching what the earlier reports covered at
  `$DE00`.  The other five rates were not driven.
* **Only one hot key was seen to land at `$DF00`** - C= + 2 on one machine, which programmed `$DF03`
  = `$18`.  C= + A, C= + O, C= + H, C= + P, C= + C and C= + RETURN were not exercised on this strap.
* **No complete game was played**, at either base.
* **One rig failure, recorded for honesty.**  The first `testStrapPair.py` run produced a session in
  which machine A never left the options menu: `testTwoMachines.answerModemPrompts` decides the link
  is open as soon as `$E5BE` reads non-zero, and `$E5BE` is ordinary RAM until the module is loaded
  over it - it happened to hold `$AC`, so the script stopped pressing keys.  Only one machine ever
  connected to the relay and it poured out 2188 bytes of byte-sync `$00`.  That run says nothing about
  the strap and its numbers are not used here.  `testStrapPair.py` now requires two independent
  conditions instead - the module's jump table resident at `$E000` **and** the control register
  programmed - and the re-run linked first time.  `testTwoMachines.py` itself was left alone.

---

## Files

| file | what it is |
|---|---|
| `testStrap.py` | new: one machine at `$DF00`, watchpoints on both pages and both mirror ranges, ordered access log, accessor read-back |
| `testStrapPair.py` | new: two machines at `$DF00` with the mirror tripwire armed for the whole session |
| `scanIoAccess.py` | new: the three static passes of section 7, over every loaded image and any extra binary given as `path@LOADADDR` |
| `probeBankOut.py` | new: VICE's I/O map, and the banked-out write experiment of section 7.2 |
| `probeOpenBus.py` | new: what an empty `$DE00` reads back, sampled across the frame |
| `viceHarness.py` | changed: `ACIA_BASE` from `SWIFTLINK_ACIA_BASE`, default `$DE00` |
| `testBoot.py`, `testModemSelect.py`, `testRealtime.py` | changed: monitor commands built from `ACIA_BASE` instead of a literal `de00` |
| `testLogs/strapDf00.hits.txt` | the 800 watchpoint stops of section 2 |
| `testLogs/strapDf00.mirror.txt` | the mirror-only watch of section 7.3 - empty, which is the result |
| `testLogs/strapSelectDf00.hits.txt` | the modem-select run's 300 stops |
| `testLogs/strapDf00Probe.txt`, `strapSelectDf00.txt`, `strapRealtimeDf00.txt`, `strapPairDf00.txt`, `strapOpenBus.txt` | the five run transcripts |
| `testLogs/realtime.strapdf.run300.w1.samples.txt`, `realtime.strapdf.run2400.w1.samples.txt` | per-second byte counts for the two real-time windows |
| `shots/strapDf00*.png`, `shots/strapSelectDf00*.png`, `shots/pairDf00*.png`, `shots/rtstrapdf*.png` | the screenshots cited above |
