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