modemwars/swiftlink/strapReport.md
2026-08-23 02:09:40 -05:00

445 lines
27 KiB
Markdown

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