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

443 lines
27 KiB
Markdown

# 1200 baud: two machines in real time, and the CONNECT 1200 auto-baud path
> **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.
1200 is the rate most likely to matter in practice. It is the other speed the stock user-port module
can produce, so it is the only place besides 300 where a SwiftLink player and an unmodified user-port
player can meet, and it is the rate a real Hayes modem's `CONNECT 1200` result code puts the driver
on by itself. Until now it had never been driven at all: `testReport.md` records that the only
control-register values the module had ever been seen to write were `$1E` (the probe), `$15` (300)
and `$18` (2400).
Everything below is VICE 3.7.1 (`x64sc`), NTSC, `-acia1mode 1 -acia1base 0xDE00 -acia1irq 1`, on
**2026-08-22**, against a disk built from the current source with `./build.sh`
(`checkAbi.py` prints `ABI CHECK PASSED`). Both emulators run at **true C64 speed** - warp is used
only for the loader and switched off through the monitor's `warp` command before the options menu -
and their own cycle counters are read at every window edge to prove it.
Short answer:
* **1200 baud works, in both directions, on both machines, for the whole of six 75-second windows
over three boots.** 452 seconds of measured link time, 13121 bytes one way and 12109 the other,
and **not one second in either direction carried zero bytes**.
* **The transmitter never parked in the old deadlock state.** Thirty state readings at the window
edges: `txCharActive` `$E5BB` and `aciaCommandShadow` `$E5BE` were only ever seen as the two
self-consistent pairs, `$00`/`$09` (idle) and `$01`/`$05` (a byte of ours in the chip). The pair
that defined the fault - `$E5BB` set while `$E5BE` bit 2 is clear - did not occur once, and the
script's deadlock probe never fired.
* **1200 is the first rate at which the 20-byte receive ring has been seen to overflow.** Thirty-two
overflows across the six windows. Every one of them was a **ring overflow, not a bad character** -
a conditional tracepoint proves that for the nineteen in the four windows it was live in, and the
other thirteen carry the same fingerprint - and every one of them was on the same machine. The link
resynchronised and carried on each time; it never wedged, and `connectionPhase` was back at 2 or 3
at every edge with `isLinkActive` `$E03B` = `$01`.
* **The auto-baud path really does select 1200.** A fake Hayes modem answered the module's AT string
with `CONNECT 1200`; the driver sniffed it and wrote ACIA control `$17`. Driving `$E7B2` directly
with X = 0, 3 and 6 gave `$15`, `$17` and `$18` - the three-byte stride of `baudEntryTable` `$E6FD`
behaving, not just the one index.
Scripts: `testRealtime.py` (unchanged in method, three small additions noted in section 6) and the
new `test1200AutoBaud.py`. Logs: `testLogs/rate1200Run1.txt`, `testLogs/rate1200Run2.txt`,
`testLogs/rate1200Run3.txt`, `testLogs/autoBaud1200Run4.txt` (and the three failed attempts that
preceded it, `autoBaud1200Run1..3.txt`). Per-second byte counts in
`testLogs/realtime.r12*.run1200.w*.samples.txt`, tracepoint output in the matching `*.trace.txt`,
screenshots `shots/rtr12*.png` and `shots/abab4*.png`.
---
## 1. How 1200 was selected, and what the chip was given
`baudIndex` `$E055` comes off the disk as 0 every time the module is loaded, so 1200 has to be asked
for. Both machines were given **C= + 1** at the "PRESS A OR O" prompt, before the link was ever
opened - the order `README.md` tells the player to use - with the Commodore key on **Tab**, which is
where VICE's default symbolic keymap puts it.
The hot key took on the first press on both machines in all three runs, and it programmed the 6551
immediately rather than waiting for the link:
| Reading, at the A/O prompt, before the link opened | A | B |
|---|---|---|
| `baudIndex` `$E055` | 3 | 3 |
| ACIA control register `$DE03` | `$17` | `$17` |
| `bitPeriodLo` `$E056` (the live control byte) | `$17` | `$17` |
| `connectionPhase` `$E040` / `isLinkActive` `$E03B` | 0 / `$00` | 0 / `$00` |
| `aciaCommandShadow` `$E5BE` | `$00` | `$00` |
`$17` is `$10` (8 data bits, one stop bit, internal baud-rate generator) OR rate bits `$7`, which on
a SwiftLink's doubled 3.6864 MHz crystal is 1200 baud. It matches `baudEntryTable+1` at `$E701` in
the built image, where the seven entries read
```
$E6FD: B3 15 00 B1 17 00 B2 18 00 B4 1A 00 B9 1C 00 B0 1E 00 B8 1F 00
C=+3 300 C=+1 1200 C=+2 2400 C=+4 4800 C=+9 9600 C=+0 19200 C=+8 38400
```
One thing worth writing down because it looks alarming in a log: at that same pre-link edge
`linkErrorCount` `$E047` reads **72** on both machines. 72 is `$48`, the `H` of the leftover junk
bytes `44 54 48` ("DTH") the module ships at `$E045-$E047`; nothing has cleared it yet because the
link has not been opened. It goes to `$00` when the link comes up and stays there.
Both machines reached `connectionPhase` 3 - the game's own packet protocol - in every run, and the
side-0/side-1 negotiation completed: one machine ends on "OPPONENT PICKING GAME/MAP." and the other
on the GAME TYPE menu. That is the same game state the 300- and 2400-baud runs in
`realtimeReport.md` were measured in, which is what makes the three rates comparable.
---
## 2. Bytes each way, per window
`conn0` is machine A (answer, the first to open its ACIA); `conn1` is machine B (originate). "speed"
is each emulator's own free-running cycle counter over the window against the wall clock - the check
that this was measured at true C64 speed and not on a machine that could not keep up.
| Run | Window | Wall | A speed | B speed | A -> B | B -> A |
|---|---|---:|---:|---:|---|---|
| 1 | 1/2 | 75.67 s | 100.9% | 100.5% | 1675 B = **22.14 B/s** | 1584 B = **20.93 B/s** |
| 1 | 2/2 | 75.58 s | 101.3% | 100.9% | 3042 B = **40.25 B/s** | 2577 B = **34.10 B/s** |
| 2 | 1/2 | 75.09 s | 101.0% | 100.6% | 773 B = **10.29 B/s** | 760 B = **10.12 B/s** |
| 2 | 2/2 | 75.36 s | 101.2% | 100.8% | 792 B = **10.51 B/s** | 802 B = **10.64 B/s** |
| 3 | 1/2 | 75.67 s | 101.0% | 100.5% | 3433 B = **45.37 B/s** | 3248 B = **42.92 B/s** |
| 3 | 2/2 | 75.14 s | 100.9% | 100.5% | 3406 B = **45.33 B/s** | 3138 B = **41.76 B/s** |
| **all six** | | **452.51 s** | | | **13121 B = 29.00 B/s** | **12109 B = 26.76 B/s** |
The 8N1 ceiling at 1200 baud is 120 bytes/s. The six-window average is 24% of it in one direction
and 22% in the other, but the average is not the interesting number, because **the line does get
saturated in bursts**: the per-second buckets run from 6 to 119 bytes/s, and 119 is 99% of what 1200
baud can carry in a second. What sets the average is the game, not the wire - the frame layer starts
at most one thing per raster IRQ and the packet layer exchanges one packet per lock-step turn.
Per-second buckets, from `testLogs/realtime.r12*.run1200.w*.samples.txt`, one line per second per
window:
| Run | Window | conn0 (A -> B) | conn1 (B -> A) | seconds at zero |
|---|---|---|---|---|
| 1 | 1/2 | 15 to 79 B/s | 14 to 100 B/s | none |
| 1 | 2/2 | 15 to 88 B/s | 13 to 88 B/s | none |
| 2 | 1/2 | 6 to 47 B/s | 6 to 69 B/s | none |
| 2 | 2/2 | 6 to 61 B/s | 6 to 84 B/s | none |
| 3 | 1/2 | 19 to 89 B/s | 18 to 99 B/s | none |
| 3 | 2/2 | 15 to 74 B/s | 13 to **119** B/s | none |
**Not one second, in either direction, in any of the six windows, carried zero bytes.** That is the
single most important line in this report: the failure mode this driver has to avoid is a machine
that stops talking, and at 1200 baud it never did, not even in the windows where the receive ring was
overflowing.
### `$55` ARQ lead bytes
The frame layer's lead byte is `$55`. These are raw occurrences in the window's byte stream, so a
`$55` that happens to be payload is counted too - the number is a floor on the frame rate, not an
exact frame count.
| Run | Window | `$55` A -> B | per second | `$55` B -> A | per second |
|---|---|---:|---:|---:|---:|
| 1 | 1/2 | 553 | 7.3 | 580 | 7.7 |
| 1 | 2/2 | 713 | 9.4 | 733 | 9.7 |
| 2 | 1/2 | 279 | 3.7 | 254 | 3.4 |
| 2 | 2/2 | 297 | 3.9 | 291 | 3.9 |
| 3 | 1/2 | 786 | 10.4 | 751 | 9.9 |
| 3 | 2/2 | 758 | 10.1 | 688 | 9.2 |
| **all six** | | **3386** | 7.5 | **3297** | 7.3 |
Every window carried real ARQ frames in both directions. For comparison, the 2400-baud windows in
`realtimeReport.md` section 8 carried about 250 `$55` per direction per window - roughly 3.4 frames a
second - so 1200 baud in this same game state is doing between one and three times as much frame
traffic as 2400 did, which is a statement about how much the two games had to say to each other in
each run rather than about the rates.
### Byte-sync floods
`beginByteSyncPhase` pours out `$00` until the peer answers, so a run of eight or more `$00` in the
stream is the signature of a link that has dropped out of the packet phase and is resynchronising.
| Run | Window | A -> B runs of >= 8 zeros / longest | B -> A runs / longest |
|---|---|---|---|
| 1 | 1/2 | 3 / 42 | 2 / 25 |
| 1 | 2/2 | 18 / 87 | 17 / 54 |
| 2 | 1/2 | 0 / 6 | 1 / 42 |
| 2 | 2/2 | 0 / 1 | 0 / 1 |
| 3 | 1/2 | 33 / 48 | 30 / 39 |
| 3 | 2/2 | 33 / 57 | 33 / 50 |
Runs 1 and 3 resynchronised repeatedly and run 2 barely at all. The resyncs track the error counts
in section 3 exactly, and every one of them ended - the longest flood in 452 seconds was 87 bytes,
under a second of line time at 1200 baud. Compare the pre-fix 2400-baud failure in
`realtimeReport.md` section 4, where one direction carried 4495 bytes of `$00` and nothing else for
an entire 75-second window and never came back.
---
## 3. Errors: all thirty-two were receive-ring overflows, all on the same machine
The NMI receiver pushes two different things through `reportLinkError` `$E3BC` into `countLinkError`
`$E403`: a character whose status shows overrun, framing or parity error, and a good character that
had nowhere to go because the 20-byte receive ring was already full. `linkErrorCount` `$E047` cannot
tell them apart and cannot even be trusted as a total, because `restartByteSync` clears it at `$E814`
- which is why it reads `$00` at every window edge in this report while errors were plainly
happening.
So a second tracepoint was added, conditional so that it fires only on the overflow case:
```
trace exec $e403 # every character the receiver threw away
trace exec $e505 if x > $13 # ... and the subset thrown away because the ring was full
```
`$E505` is the `CPX #$14` inside `pushUartRxRing`, and X was loaded with `uartRxCount` three cycles
earlier at `$E502`, so `X > $13` means the ring already held all twenty characters. (The monitor
reads condition numbers as hex - `x > 19` would have meant `X > $19` - which is why the constant is
written `$13`.) A tracepoint prints and carries on; it never stops the emulator.
Machines were told apart afterwards by the free-running cycle counter each trace line carries: the
two emulators start seconds apart and their stopwatch ranges never overlap.
| Run | Window | `$E403` total | of which ring-full `$E505` | on machine A | on machine B |
|---|---|---:|---:|---:|---:|
| 1 | 1/2 | 7 | not instrumented | 0 | 7 |
| 1 | 2/2 | 6 | not instrumented | 0 | 6 |
| 2 | 1/2 | 8 | **8** | 0 | 8 |
| 2 | 2/2 | 3 | **3** | 0 | 3 |
| 3 | 1/2 | 7 | **7** | 0 | 7 |
| 3 | 2/2 | 1 | **1** | 0 | 1 |
| **all six** | | **32** | **19 of 19, in the four windows it was live in** | **0** | **32** |
In runs 2 and 3, where both tracepoints were live, the two counts are **equal in every window**: 8
and 8, 3 and 3, 7 and 7, 1 and 1. Not one character was rejected for a bad status. Run 1 had only
the `$E403` tracepoint, but its trace lines carry the same fingerprint - `X` reads `$14` at `$E403`
on all thirteen hits, which is `uartRxCount` = 20 left in X by the ring-full exit - so those thirteen
were overflows too.
**That is the ceiling the task brief suspected, and it is real at 1200 baud, not only at 38400.** The
raw receive ring is 20 bytes, only the once-per-frame tick drains it, and `awaitWholePacket` will not
copy a packet out until the whole payload and its check byte are resident - up to 17 bytes - so the
real headroom is about three bytes. At 1200 baud a video frame is about 2.0 character times, so a
steady 1200-baud stream cannot overflow it; a burst that arrives while the tick is not draining can,
and did. There is no RTS throttling anywhere in this driver.
**What the driver does about it is the answer that matters.** A non-zero `linkErrorCount` makes
`runLinkStateMachine` restart the byte sync whatever phase it was in, and that is exactly what the
`$00` floods in section 2 are. Every one of them completed, the link came back to phase 3, and the
per-second byte counts never dropped to zero in either direction. **It degrades and recovers. It
does not wedge.**
Two honest caveats. First, **every overflow in all three runs was on machine B**, the originate side
and the second emulator to be started, and machine A recorded not a single one in 452 seconds. That
is not explained. It is not the game screen - B was on the GAME TYPE menu in runs 1 and 3 and on
"OPPONENT PICKING GAME/MAP." in run 2, and it overflowed in all three. Both machines held 100.5% or
better of real time throughout, so it is not one emulator falling behind. Second, VICE's ACIA is
byte level: it paces whole bytes at the rate the control register asks for but does not shift them
bit by bit, so the framing and parity errors a real noisy line would produce cannot appear on this
rig at all. "Zero bad characters" is a statement about this emulation, not about a real cable.
---
## 4. The transmitter: the deadlock state never occurred
`txCharActive` `$E5BB` and `aciaCommandShadow` `$E5BE` were read on both machines at the open of each
series and at the close of each window, with both emulators stopped.
| `txCharActive` | `aciaCommandShadow` | Readings | Meaning |
|---|---|---:|---|
| `$00` | `$00` | 6 | at the A/O prompt, before the ACIA was ever programmed |
| `$00` | `$09` | 10 | idle: transmit interrupt disarmed, nothing believed to be in the chip |
| `$01` | `$05` | 14 | busy: transmit interrupt armed, a byte of ours in the chip |
| **`$01`** | **`$09`** | **0** | the deadlock - **never seen** |
Thirty readings, only the two self-consistent pairs. `testRealtime.isTxDeadlocked` reported `False`
at every edge and its repair probe - which would have written `$00` over `$E5BB` from the monitor and
said so in the log - never ran, so nothing in these numbers is the test rig patching the driver up.
The full edge table, all three runs, in order (open of the series, close of window 1, close of window
2). `err` is `linkErrorCount` `$E047`, `pend` is `uartPendingCount` `$E0A5`:
| Run | Edge | A: phase / active / err / `$E5BB` / `$E5BE` / pend | B: phase / active / err / `$E5BB` / `$E5BE` / pend |
|---|---|---|---|
| 1 | before the link opened | 0 / `$00` / `$48` / `$00` / `$00` / `$00` | 0 / `$00` / `$48` / `$00` / `$00` / `$00` |
| 1 | open of the series | 3 / `$01` / `$00` / `$00` / `$09` / `$00` | 3 / `$01` / `$00` / `$00` / `$09` / `$00` |
| 1 | close of w1 | 2 / `$01` / `$00` / `$01` / `$05` / `$01` | 3 / `$01` / `$00` / `$01` / `$05` / `$03` |
| 1 | close of w2 | 3 / `$01` / `$00` / `$00` / `$09` / `$00` | 2 / `$01` / `$00` / `$01` / `$05` / `$01` |
| 2 | before the link opened | 0 / `$00` / `$48` / `$00` / `$00` / `$00` | 0 / `$00` / `$48` / `$00` / `$00` / `$00` |
| 2 | open of the series | 2 / `$01` / `$00` / `$01` / `$05` / `$01` | 3 / `$01` / `$00` / `$01` / `$05` / `$03` |
| 2 | close of w1 | 3 / `$01` / `$00` / `$00` / `$09` / `$00` | 3 / `$01` / `$00` / `$01` / `$05` / `$03` |
| 2 | close of w2 | 3 / `$01` / `$00` / `$01` / `$05` / `$01` | 3 / `$01` / `$00` / `$01` / `$05` / `$03` |
| 3 | before the link opened | 0 / `$00` / `$48` / `$00` / `$00` / `$00` | 0 / `$00` / `$48` / `$00` / `$00` / `$00` |
| 3 | open of the series | 2 / `$01` / `$00` / `$01` / `$05` / `$01` | 3 / `$01` / `$00` / `$01` / `$05` / `$02` |
| 3 | close of w1 | 3 / `$01` / `$00` / `$00` / `$09` / `$00` | 2 / `$01` / `$00` / `$01` / `$05` / `$01` |
| 3 | close of w2 | 3 / `$01` / `$00` / `$01` / `$05` / `$03` | 2 / `$01` / `$00` / `$00` / `$09` / `$00` |
`isLinkActive` `$E03B` was `$01` at every edge after the link came up and `$00` at every pre-link
edge. `linkErrorCount` `$E047` read `$00` at every edge after the link came up - which, as section 3
explains, means "no error since the last resync" and not "no errors", and is the reason the
tracepoints exist. `connectionPhase` `$E040` moves between 3 and 2 as the link drops into and out of
a byte-sync round, exactly as it did in the healthy 300-baud run of `realtimeReport.md` section 3.
`uartPendingCount` never rose above `$03`, so neither transmitter ever had a backlog.
The ACIA control register `$DE03` and `bitPeriodLo` `$E056` read `$17` and `baudIndex` `$E055` read 3
at all thirty readings on both machines. Nothing reprogrammed the rate behind the driver's back and
nothing drifted.
---
## 5. The auto-baud path: `CONNECT 1200` really does select 1200
`switchTo1200Baud` `$E7B2` is reached from `$E7B0` (`LDX #$03`) when `serviceModemInput` sees a CR
whose preceding character was `'0'` - the last digit of a verbose `CONNECT 1200`. That caller
predates the SwiftLink table, so the question is whether X = 3 still lands where the table says it
does. It was tested twice, once through the real path and once by driving the entry point directly.
Script: `test1200AutoBaud.py`. Log: `testLogs/autoBaud1200Run4.txt`.
### 5.1 Through a fake Hayes modem
One machine, its serial line wired not to a peer but to a TCP listener in the test script that
behaves like a modem far enough to answer the AT string. The emulator was left completely alone
across the critical window - the answer timer at `$E7D5` gives the reply about 121 frames, two
emulated seconds, and every monitor command stops the emulated clock.
What the fake modem saw and said, from its own timestamped log:
```
+100.757 the emulator opened the serial line
+100.787 from the C64: '\r'
+101.870 from the C64: 'A' 'T' 'Q' '0' 'V' '1' 'X' '1' 'A' (answer-mode Hayes string)
+102.201 from the C64: '\r'
+102.202 answered '\r\nCONNECT 1200\r'
+104.780 from the C64: '\x00' ... the byte-sync flood of connection phase 2
```
The `$00` flood starting 2.58 s after the AT string's CR is the 121-frame answer timer expiring and
the state machine falling out of terminal mode into byte sync, which is what should happen. What it
left behind:
| Reading | Value | |
|---|---|---|
| ACIA control register `$DE03` | **`$17`** | 8N1, internal baud generator, rate bits `$7` = **1200 baud** |
| `bitPeriodLo` `$E056` | **`$17`** | the driver's own mirror of the live control byte |
| `baudIndex` `$E055` | **0** | *unchanged* - see below |
| `lastModemChar` `$EB02` | `$20` | the space `$E7B5` substitutes for the CR after the sniff |
| `connectionPhase` `$E040` / `isLinkActive` `$E03B` | 2 / `$01` | in byte sync, link up |
| `linkErrorCount` `$E047` | `$00` | |
**`$17` with `baudIndex` still 0 can only have come from `switchTo1200Baud`.** The only three
callers of `loadBaudParameters` are `openCommLink` `$E2C9`, which loads X from `baudIndex` and would
therefore have written `$15`; the speed hot keys, which do `STX baudIndex` at `$E350` on the way in
and would have left `$E055` = 3; and `$E7B2`, which loads X immediately and touches `$E055` never.
No hot key was pressed in this run.
### 5.2 Driving `$E7B2` directly
With the emulator stopped, the ACIA was forced back to 300 baud, `$E056` cleared, `txCharActive`
`$E5BB` set to `$01` by hand, PC put on `$E7B2` and X loaded through the monitor; `n` then steps over
the `JSR` and its whole subroutine. Doing it for three indices exercises the three-byte stride
rather than the single value the sniffer uses:
| X | `$DE03` before | `$DE03` after | `$E056` after | `$E5BB` after | `$E055` after | Expected |
|---:|---|---|---|---|---|---|
| 0 | `$15` | **`$15`** | `$15` | `$00` | 0 (untouched) | `$15`, 300 baud - PASS |
| **3** | `$15` | **`$17`** | `$17` | `$00` | 0 (untouched) | **`$17`, 1200 baud - PASS** |
| 6 | `$15` | **`$18`** | `$18` | `$00` | 0 (untouched) | `$18`, 2400 baud - PASS |
So the behaviour matches the table, and matches the built image, where `baudEntryTable+1` at `$E701`
is `$17`. Two side effects of the current `loadBaudParameters` show up in that table and are worth
recording:
* **`txCharActive` is cleared.** `$E5BB` was set to `$01` before each entry and read `$00` after, which
is `aciaSetControlIdle` `$E04C` doing its job - the third of the three deadlock fix sites in
`realtimeReport.md` section 8. A `CONNECT 1200` that arrives while a character is half way out of
the chip cannot strand the flag.
* **`baudIndex` `$E055` is *not* updated.** `loadBaudParameters` writes the control register and
`bitPeriodLo` and nothing else. This is faithful to the stock driver, which behaved the same way,
but it has a consequence: `openCommLink` `$E2C6` reloads the rate from `baudIndex` whenever it
finds `isLinkActive` = 0, so a link that is **torn down and re-opened** after a `CONNECT 1200`
comes back at **300 baud**, silently. A re-open on a live link (C= + RETURN, which reaches
`openCommLink` with `isLinkActive` already set) skips `loadBaudParameters` entirely and keeps 1200.
### 5.3 What the source said about it
The comment on `$E7B0`/`$E7B2` was wrong in two ways and has been corrected - **comments only, the
assembled image is byte-for-byte identical before and after, and `checkAbi.py` still passes**:
* `$E7B0` ended `"so this caller still selects 120053 cycles = 1200 baud"`. The `53 cycles` is
wreckage from the stock driver's comment, where the number really was a CIA timer value.
* `$E7B2` said `"copy that entry into the live bit period $E056/$E057 and the pacing divisor $E058"`,
which is what the *stock* `loadBaudParameters` did. The SwiftLink one writes `$E056` and the ACIA
control register; `$E057` and `$E058` have been dead since the software UART was replaced. It now
says what it does, and records the `baudIndex` trap above.
---
## 6. What was changed in the test rig, and what went wrong in it
Three additions to `testRealtime.py`, all additive:
* each machine's tracepoint output is now kept separately (`tracedBy`), so a hit can be attributed to
one end;
* the conditional overflow tracepoint `trace exec $e505 if x > $13` is set alongside `$e403`;
* the hit counter's regular expression was wrong and is fixed. The monitor announces a hit as
`#1 (Trace exec e403)` and disassembles the line as `.C:e403 ...` - **no dollar sign on either** -
so a pattern of `C:\$e403` matches nothing. **The consequence is in the logs of runs 2 and 3:
their own printed summaries say "0 tracepoint hits" and that is false.** The counts in section 3
are from the saved `*.trace.txt` files, re-counted afterwards, and they are the real ones. Run 1's
log predates the change and counted `(Trace` instead, which was right while `$e403` was the only
tracepoint.
`test1200AutoBaud.py` is new. Three attempts failed before the one reported above, all for rig
reasons rather than driver reasons, and two of them are worth passing on:
* **`answerModemPrompts` can return before the link could possibly have opened.** It uses
"`$E5BE` is non-zero" as its test, and at true C64 speed the 1541 takes the best part of a minute to
fetch the opponent module off track 34. Until it lands, `$E000-$EFFF` still holds the previous
overlay - `$E5BB-$E5BE` read `E8 84 18 AC` in the first attempt, and `$AC` was taken for an open
link while the screen still said "WORKING...". The module's stored bytes there are all `$00`, so
the test is sound once the module is resident; the fix is to wait for residency first.
* **`$E013/$E014` is not a safe residency test either**, although `testRealtime.selectRate` uses it:
the pre-load wreckage read `B7 0D` - exactly the patched value - in the second attempt.
`test1200AutoBaud.waitForModule` waits on all sixteen bytes of the `$E000` jump table instead, and
re-offers the fire button every six polls, because in the third attempt the press that starts the
load simply did not land.
One self-inflicted loss: `./build.sh` was run to rebuild the disk while a measurement run was in
flight, which rewrites the `.d64` the running emulators have attached. That run was killed and
restarted rather than trusted, and its emulators and Xvfb displays were cleaned up by hand. The
three runs reported here all ran against a stable image.
---
## 7. What this does not show
* **VICE, not hardware.** No physical SwiftLink, no real cable, no real modem. The `CONNECT 1200`
in section 5 came from fifteen bytes written into a TCP socket by a Python script.
* **VICE's ACIA is byte level.** It cannot produce framing or parity errors, so "every error was a
ring overflow" is a result about this rig. On a real line the same tracepoints would be expected to
show bad characters as well.
* **The overflows are unexplained.** Thirty-two of them, all on machine B, none on machine A, over
three boots. What burst produces them, and why only one end sees them, was not chased - doing it
needs the NMI instrumented, which stops the emulator and destroys the timing being measured.
* **No stock user-port peer.** 1200 is where a SwiftLink player and an unmodified user-port player
would meet, and that pairing still has not been run. Both machines here were SwiftLink builds.
* **No complete game.** Same as every earlier report: the two machines reach the GAME TYPE menu and
stay there.
* **Six windows is not a long soak.** 452 seconds at 1200 baud is enough to say the deadlock did not
recur and the link recovers from overflow, and not enough to say it never will.
* **The auto-baud path was exercised once end to end**, in answer mode, on a link with no peer behind
the fake modem. The originate string `ATQ0V1X1D` and a `CONNECT 1200` arriving on a link that then
goes on to play a real game were not tried.
---
## 8. Files
| File | What it is |
|---|---|
| `test1200AutoBaud.py` | new: one machine, a fake Hayes modem that answers `CONNECT 1200`, then `$E7B2` driven directly with X = 0, 3 and 6 |
| `testLogs/rate1200Run1.txt` | run 1 - C= + 1 at the A/O prompt, two 75 s windows |
| `testLogs/rate1200Run2.txt` | run 2 - a second boot, same sequence |
| `testLogs/rate1200Run3.txt` | run 3 - a third boot, same sequence, with both tracepoints live |
| `testLogs/realtime.r12*.run1200.w*.samples.txt` | per-second byte counts, both directions, every window |
| `testLogs/realtime.r12*.run1200.w*.trace.txt` | raw `$E403` and `$E505` tracepoint output, with the cycle counter that attributes each hit to a machine |
| `testLogs/autoBaud1200Run4.txt` | the auto-baud run reported in section 5 |
| `testLogs/autoBaud1200Run1.txt`, `...Run2.txt`, `...Run3.txt` | the three rig failures described in section 6 |
| `testLogs/autoBaud1200.ab4.modem.txt` | the fake modem's own timestamped event log |
| `shots/rtr120*.png`, `shots/rtr12b0*.png`, `shots/rtr12c0*.png` | the menu, the linked pair and the end of each window, runs 1 to 3 |
| `shots/abab401Menu.png`, `shots/abab402AfterConnect.png` | the auto-baud run |