# The high rates: 4800, 9600, 19200, 38400, and a rate mismatch

> **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.
>
> **Note on the recommendation: "must not be selected: 38400" has been overturned.** It was written
> from the four wedges in sections 5 and 6, on a build whose NMI handler could be re-entered without
> bound. `wedgeDiagnosis.md` then established that mechanism - 34 nested handler frames decoded off a
> live page 1, three timed stack descents, and the cycle budget behind them - and the driver now
> carries a re-entrancy guard. On that build 38400 is selectable again and did not wedge in seven
> boots, and 9600 and 19200 produce no receive errors and no lost transmit characters at all
> (`portCompatReport.md` sections 3 and 4, `testReport.md` section 11). The measurements below are
> left exactly as they were taken: they are the record of the build that had the fault.

The reason to care about the top of the baud table is not speed.  It is **hardware compatibility**: a
C64 Ultimate's SwiftLink emulation may hand this driver a much faster line than *Modem Wars* was ever
written for, and the question that matters is not how much the driver can carry but whether it
**survives** what it is handed - stays in sync, or degrades and recovers, and never wedges.

Everything here 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 in every pair 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 each
emulator's own free-running cycle counter is read at both edges of every measurement window to prove
it.  **Every window in this report ran at 100.5% of real time or better on both machines.**

Script: the new `testHighRates.py`, which reuses `testRealtime.py` (the socket null modem, the boot,
the hot-key rate selection, the stopwatch check, the transmitter-deadlock probe) and adds what this
question needs.  Logs: `testLogs/highRate4800.txt`, `highRate9600.txt`, `highRate19200.txt`,
`highRate38400.txt`, `highRate38400run2.txt`, `highRate38400run3.txt`, `highRate38400run4.txt`,
`highRateMismatch.txt`, with per-second byte counts and raw tracepoint output in
`testLogs/highRate.*.samples.txt` / `*.trace.txt` / `*.ring.txt` and screenshots in `shots/hrh*.png`.

---

## The answer, in one table

| Rate | Hot key | Control | Both reached phase 3 | Sampling intervals carrying nothing | Receive-ring overflows | Transmit characters lost | Byte-sync rounds | Outcome |
|---|---|---|---|---|---|---|---|---|
| 4800 | C= + 4 | `$1A` | yes | **none**, 0 of 450 | **0** in 20 s of tracing | 0.8% / 1.5% | ~1.0/s, 98% completed | **healthy** |
| 9600 | C= + 9 | `$1C` | yes | **none**, 0 of 442 | 8 and 41 in 20.6 s | 2.1% / 2.1% | ~1.7/s, 79% completed | **degrades, recovers** |
| 19200 | C= + 0 | `$1E` | yes | **none**, 0 of 450 | 128 and 74 in 20.4 s | 2.2% / 2.3% | ~1.6/s, 76% completed | **degrades, recovers** |
| 38400 | C= + 8 | `$1F` | 3 of 4 boots, briefly | **every interval**, one or both directions, for the whole 170-246 s of every boot | 1259 in 21 s on the stuck machine | - | it stops resyncing | **WEDGES, 4 of 4 boots**, once by crashing the CPU |
| 38400 vs 300 | - | `$1F` / `$15` | yes | **none**, 0 of 474 | **0** | 0.0% / 0.1% | 0.2-0.5/s, completions >= entries | **healthy** - but see section 7 |

**Plain recommendation.**

* **Safe to advertise: 300, 1200, 2400, 4800.**  4800 behaved the way 2400 does after the transmitter
  fix: continuous traffic in both directions every second, not one receive-ring overflow, not one bad
  character, the ring never more than a third full.
* **Document as degrading but usable: 9600 and 19200.**  Both keep the link alive and both keep the
  game moving.  Both also spend a visible part of every second re-synchronising: the 20-byte receive
  ring overflows, `linkErrorCount` forces a byte-sync round, the round completes, the packet layer
  carries on.  Nothing is lost that the ARQ layer does not recover.  What they do **not** buy is
  throughput - the extra line time goes into resync floods, not into frames.
* **Must not be selected: 38400.**  *(Overturned - see the note at the top of this file.)*  Four
  consecutive boots, four wedges, every one inside the first
  minute after the link came up, none of which recovered in the 170 to 246 seconds each was then
  watched for.  On the fourth boot VICE reported `*** Main CPU: JAM at $0007` on one machine - the
  6502 executing an illegal opcode in zero page.  It is not the old 2400-baud transmitter deadlock, and
  the driver's own self-heal cannot reach it.  Sections 5 and 6 have the stuck bytes.
* **The rate table itself is fine.**  Every hot key programmed the chip correctly, first press, every
  time: `$1A`, `$1C`, `$1E` and `$1F` all appeared in `$DE03` with the matching `baudIndex`.  38400
  fails at the protocol, not at the register.

---

## 1. What actually limits this driver, arithmetically

The raw receive ring at `$E430` holds **20 characters**, only the once-per-frame link tick drains it,
and `awaitWholePacket` `$E9D9` refuses to copy a packet body out until the whole payload **and** its
check byte are resident - up to 17 bytes - so the genuine headroom is about three bytes.  There is no
RTS throttling anywhere in the driver.  So the number that decides everything is how many characters
can arrive between two drains:

| Rate | Characters/s | Characters per video frame | A full 22-byte transmit ring emptied onto the wire takes |
|---|---:|---:|---:|
| 300 | 30 | 0.50 | 733 ms = 43.9 frames |
| 1200 | 120 | 2.01 | 183 ms = 11.0 frames |
| 2400 | 240 | 4.01 | 91.7 ms = 5.5 frames |
| 4800 | 480 | 8.02 | 45.8 ms = 2.7 frames |
| 9600 | 960 | 16.05 | 22.9 ms = 1.4 frames |
| 19200 | 1920 | 32.09 | 11.5 ms = 0.69 frames |
| 38400 | 3840 | 64.19 | 5.7 ms = 0.34 frames |

Two figures in the existing documents are wrong and are worth correcting here, because the size of
the problem depends on them.  The brief for this work said a video frame is "roughly 640 character
times" at 38400 and `spec_uart.md` hazard 2 says "about 200"; at 3840 characters a second and 59.826
frames a second it is **64.2**.  The ring is therefore about three times too small at 38400, not
thirty times - which is why 19200, at 32 characters a frame, still works.

The second column is the one that decides where the overflows start.  The frame layer fills the
22-byte **transmit** ring and the NMI empties it at line rate, so a peer's ARQ frame arrives as a
burst.  At 300 baud that burst is spread over 44 video frames and cannot overflow anything.  At 9600
it lands inside one and a half frames, at 19200 inside one, and at 38400 three of them can land
inside a single frame.

**Relay direction, and how machines are told apart.**  The null modem counts bytes per connection;
`conn0` is whichever emulator opened its serial line first, which is machine A (the answer side, the
one the script drives first).  That is an inference, the same one every earlier report in this
directory makes, and where it matters below it is corroborated by something machine-specific - each
emulator's own log, or its own monitor socket, or a rate that only one of the two was set to.

---

## 2. 4800 baud: healthy

Both machines took **C= + 4** at the "PRESS A OR O" prompt; `baudIndex` `$E055` went to 9 and the
ACIA control register `$DE03` to `$1A` on both, and both reached `connectionPhase` 3.  Four windows,
246.5 seconds of measured link time.

| Window | Wall | A speed | B speed | A -> B | B -> A | resync floods | `$55` lead bytes |
|---|---:|---:|---:|---|---|---|---|
| clean 1 | 75.19 s | 101.0% | 100.6% | 681 B = **9.06 B/s** | 687 B = **9.14 B/s** | 0 / 0 | 255 / 258 |
| clean 2 | 75.15 s | 100.9% | 100.5% | 3418 B = **45.48 B/s** | 3053 B = **40.62 B/s** | 44 / 23 | 629 / 761 |
| sync tracepoints | 76.06 s | 100.9% | 100.5% | 2764 B = 36.34 B/s | 2678 B = 35.21 B/s | 32 / 36 | 622 / 659 |
| error tracepoints | 20.06 s | 103.5% | 102.0% | 866 B = 43.18 B/s | 996 B = 49.66 B/s | 8 / 8 | 140 / 165 |

* **Not one of the 450 sampling intervals - 225 in each direction - carried zero bytes.**  The
  per-second buckets ran 6 to 12 B/s in the quiet window and 12 to 175 B/s in the busy one.
* **Not one receive error of any kind.**  The `countLinkError` `$E403` tracepoint recorded **zero**
  hits on either machine in the 20-second instrumented window, and so did the conditional ring-full
  tracepoint `trace exec $e505 if x > $13`.
* **The resyncs that do happen are the packet layer's own, not the UART's.**  With no `$E403` hits at
  all, the 44 and 23 byte-sync floods in window 2 came from `receivePacket`'s `unwindAndResync` paths
  (two `$00` in a row, an invalid length code, a bad check byte), not from a dropped character.
* **They complete.**  Tracepoints on `beginByteSyncPhase` `$E805` and on the instruction that
  finishes a sync, `sta ackPending` `$E855`, counted 78 entries / 76 completions on A and 80 / 78 on
  B in 76 seconds.
* **The ring stays nearly empty.**  `uartRxCount` `$E42D`, sampled 24 times per machine with the
  emulator stopped: A min 0, max 7, mean 1.1; B min 0, max 5, mean 0.8.  Never at the 20-byte limit.
* `connectionPhase` `$E040` was seen at both 2 and 3 on both machines, `isLinkActive` `$E03B` = `$01`
  at every edge, and `txCharActive` `$E5BB` / `aciaCommandShadow` `$E5BE` were only ever the two
  self-consistent pairs `$00`/`$09` and `$01`/`$05`.  The deadlock probe never fired.

## 3. 9600 and 19200: they degrade, and they recover

Same method, one boot each, four windows each.  These two are reported together because they differ
only in degree.

### 9600 baud (`$DE03` = `$1C`, `baudIndex` 12)

| Window | Wall | A -> B | B -> A | resync floods | `$55` | per-second range |
|---|---:|---|---|---|---|---|
| clean 1 | 75.18 s | 3122 B = **41.53 B/s** | 2734 B = **36.37 B/s** | 28 / 28 | 652 / 617 | 6-345 / 6-276 |
| clean 2 | 75.23 s | 3647 B = **48.48 B/s** | 3985 B = **52.97 B/s** | 32 / 43 | 700 / 705 | 12-200 / 12-275 |
| sync tracepoints | 75.53 s | 4313 B = 57.11 B/s | 4214 B = 55.79 B/s | 31 / 59 | 810 / 730 | 14-259 / 14-228 |
| error tracepoints | 20.61 s | 1017 B = 49.35 B/s | 884 B = 42.90 B/s | 9 / 9 | 223 / 164 | 12-68 / 13-72 |

### 19200 baud (`$DE03` = `$1E`, `baudIndex` 15)

| Window | Wall | A -> B | B -> A | resync floods | `$55` | per-second range |
|---|---:|---|---|---|---|---|
| clean 1 | 75.13 s | 4702 B = **62.59 B/s** | 5377 B = **71.57 B/s** | 72 / 47 | 717 / 980 | 12-306 / 14-377 |
| clean 2 | 75.14 s | 4885 B = **65.01 B/s** | 4406 B = **58.64 B/s** | 58 / 50 | 770 / 808 | 14-371 / 12-300 |
| sync tracepoints | 75.78 s | 4623 B = 61.00 B/s | 4014 B = 52.97 B/s | 66 / 30 | 749 / 659 | 12-237 / 12-142 |
| error tracepoints | 20.35 s | 1188 B = 58.38 B/s | 1292 B = 63.49 B/s | 21 / 10 | 207 / 312 | 18-170 / 18-217 |

**The three things that decide the verdict, at both rates:**

* **No sampling interval carried nothing.**  0 of 442 at 9600 and 0 of 450 at 19200, counting both
  directions, including inside the windows where the ring was overflowing.
* **The errors are real, and every one is a ring overflow.**  In the instrumented windows the
  unconditional `$E403` tracepoint and the conditional `$E505 if x > $13` one returned **identical
  counts**: 8 and 8 on A, 41 and 41 on B at 9600; 128 and 128 on A, 74 and 74 on B at 19200.  Not one
  character was rejected for a bad status.  That is the 20-byte ring being outrun by a burst, exactly
  as `spec_uart.md` hazard 2 predicts, and it is the same signature `rate1200Report.md` found at 1200
  - there are simply far more of them.
* **Every degradation was followed by a recovery.**  `$E805` / `$E855`: 124 entries / 101 completions
  on A and 128 / 100 on B at 9600; 100 / 87 on A and 135 / 91 on B at 19200 - roughly 1.3 to 1.8
  byte-sync rounds a second, two thirds to seven eighths of which reached `sta ackPending` and went
  back to the packet phase inside the same window (81% and 78% at 9600, 87% and 67% at 19200).  (A round that does not reach `$E855` is not a
  loss; the next one starts immediately, and `$E855` under-counts anyway, because `restartByteSync`
  `$E80F` re-enters a sync without passing `$E805`.)  `connectionPhase` was found at both 2 and 3 on
  both machines, and `uartRxCount` sampled with the emulator stopped never once read the 20-byte
  limit: A max 14 / B max 4 at 9600, A max 10 / B max 4 at 19200.

**And they buy nothing.**  The `$55` frame-lead counts, a floor on how many ARQ frames crossed the
wire, are 629-761 per window at 4800, 617-705 at 9600 and 717-980 at 19200.  The *bytes* on the wire
climb from 45 B/s to 65 B/s as the rate goes up, and what climbs with them is the `$00` byte-sync
flood.  Counting only the three 75-second windows of each run, so that the lengths are comparable:
0 to 44 floods per window at 4800, 28 to 59 at 9600, 30 to 72 at 19200.  Above 4800 the extra line
rate is spent re-synchronising, not playing.

## 4. The other degradation: the driver overwrites its own outgoing characters

VICE logs `Acia1: ACIA: data register written although data has not been sent yet.` every time the
data register is written before the previous character has left - one transmitted character lost
each time, on emulation and on a real 6551 alike.  Each emulator writes its own log, so the loss
counts below belong to a named machine with certainty; the denominators come from the relay and so
inherit the `conn0` = A inference of section 1.  `realtimeReport.md` saw a handful of these at 2400;
they scale hard with rate:

| Rate | A lost | of offered | B lost | of offered |
|---|---:|---|---:|---|
| 4800 | 132 | 16207 = **0.8%** | 226 | 14689 = **1.5%** |
| 9600 | 387 | 18829 = **2.1%** | 403 | 19054 = **2.1%** |
| 19200 | 542 | 24165 = **2.2%** | 528 | 22600 = **2.3%** |
| 38400 vs 300 | 1 | 14769 = 0.0% | 6 | 9719 = 0.1% |

(The 38400/38400 boots produced 0 to 52 apiece, but they went silent within a minute, so their totals
mean nothing.)  Every one of these is a byte the peer never sees, so it is a checksum failure and a
retransmit at the far end - which is a large part of why the resync rate in section 3 climbs the way
it does.  **How the driver comes to write a busy data register was not traced**; the documented
`commNmiHandler` re-entrancy race is the obvious suspect and nothing measured here confirms it.

## 5. 38400 baud: it wedges, four times out of four

**Four boots, four wedges, no recoveries.**  Every one happened within about a minute of the link
coming up - before the first 75-second window opened - and none showed any sign of coming back
during the 170 to 246 seconds of measurement that followed.  The four are not identical, which is
itself worth recording: this is not one reproducible bug with one signature.

| Boot | Both reached phase 3? | A -> B, per window | B -> A, per window | How it ended up |
|---|---|---|---|---|
| 1 `h384` | no (`{'A': 2, 'B': 2}` after 180 s) | 998 / 1000 / 998 / 272 B, all `$00`/`$FF`, **no `$55` at all** | **0 / 0 / 0 / 0** | A stuck in byte sync; B's game back on the plain OPTIONS menu, its ring full at 20/20 |
| 2 `h384b` | yes | **0 / 0 / 0 / 0** | **0 / 0 / 0 / 0** | both frozen in phase 3, both rings full at 20/20, total silence both ways for 245 s |
| 3 `h384c` | yes | 4494 / 4495 / 4495 / 1259 B, **every byte `$00`**, 59.83 B/s = exactly one byte per video frame | **0 / 0 / 0 / 0** | A flooding byte sync for ever; B frozen in phase 3 on "OPPONENT PICKING GAME/MAP." |
| 4 `h384d` | yes | **0 / 0 / 0** | 1000 / 1000 / 266 B of `$00`/`$FF` at 13.3 B/s | **A's CPU jammed**; B alone in byte sync for ever |

Boot 3's error window is the clearest measurement of what the line does to a stuck machine: in 20.4
seconds machine B's receiver counted **1259** rejected characters and the conditional tracepoint says
**1259** of them were the 20-byte ring being full.  `linkErrorCount` `$E047` is useless as a total
here because it is one byte that simply wraps - across boot 3's four window edges it read 208, 21,
94, 206.

## 6. The stuck bytes

`realtimeReport.md` pinned the 2400-baud fault to one byte: `txCharActive` `$E5BB` = `$01` while
`aciaCommandShadow` `$E5BE` said the transmit interrupt was off.  **This is not that fault.**  In
every 38400 wedge those two bytes were self-consistent - `$00` / `$09`, "idle, nothing in the chip" -
`testRealtime.isTxDeadlocked` reported `False` at every edge, and the repair probe that would have
written `$00` over `$E5BB` never ran.  The driver's own `healStuckTransmitter`, added for the 2400
fault, has nothing to heal here.

### 6.1 Boot 3: `uartPendingCount` out of range

Machine B, read through the monitor with the emulator stopped, six times over six seconds, every
reading identical, and unchanged across four windows spanning 245 seconds:

| Byte | Value | What it means |
|---|---|---|
| `uartPendingCount` `$E0A5` | **`$FF`** | 255 characters "queued for the UART that have not finished transmitting" |
| `uartTxCount` `$E414` | `$00` | ...while the 22-byte transmit ring is **empty** |
| `uartRxCount` `$E42D` | **`$14`** = 20 | the receive ring is full, and stays full |
| `uartRxReadIndex` / `uartRxWriteIndex` `$E42E`/`$E42F` | `$06` / `$0D` | frozen - nothing pushed, nothing popped |
| `connectionPhase` `$E040` | `3` | frozen in the packet phase |
| `isLinkActive` `$E03B` | `$01` | the link is still "up" |
| `txCharActive` `$E5BB` / `aciaCommandShadow` `$E5BE` | `$00` / `$09` | self-consistent: not the 2400 deadlock |
| `nmiSuspendRequest`/`nmiSuspendAck` `$E039`/`$E03A` | `$00` / `$00` | the disk-load suspend handshake is **not** stuck |
| `$DE03` / `$DE02` | `$1F` / `$09` | the 6551 is correctly programmed at 38400, receiver interrupt on |
| `$0BA5` | `$00` | still a modem game, not switched to solo |
| `$01` | `$35` | I/O banked in - the module is reachable |
| PC | wanders (`$41C5`, `$C4EF`, `$C4F6`) | the CPU is running game code, not spinning in the driver |

**`uartPendingCount` `$E0A5` = `$FF` with an empty transmit ring is the byte that is stuck**, and it
is exactly the failure `spec_uart.md` hazard 1 warns about: *"make sure it can reach 0.  If it never
reaches 0, `serviceLinkTick $E76E` never clears `$E0A4`/`$E0A6`, `sendPacketFsm $E26E` never
completes an exchange, and the game locks in lock-step forever."*  Machine B's screen agrees - it
sits on "OPPONENT PICKING GAME/MAP.", waiting for a peer that can never satisfy it.  Boot 2 shows the
same byte out of range on both machines at once, `$39` on A and `$79` on B, both rings full, both
ends silent.

The driver has a mechanism meant to prevent exactly this.  `startNextTxChar` `$E607` resynchronises
the count whenever it runs with the ring dry:

```
        lda     uartTxCount             ; E60C  is there really something in the ring?
        bne     L_E616                  ; E60F  yes - decide whether it may go out
        sta     uartPendingCount        ; E611  the ring is dry: A = 0 resynchronises the count
```

and the once-per-frame `$E00C` service falls into it from `$E604` on every raster IRQ the link is up.
On the wedged machine the ring **is** dry and the count is **not** zero, and it stayed that way for
245 seconds - so that resynchronisation was not running, which means the game above had stopped
calling `$E00C`.  Which of the two stopped first was not determined.

### 6.2 Boot 4: the CPU jammed

Machine A, six readings over six seconds, then two more after the repair probe - every one identical:

```
  ADDR A  X  Y  SP 00 01 NV-BDIZC
.;0007 00 01 00 4a 5e 15 00110100
```

and its own emulator log says so outright:

```
***    Main CPU: JAM at $0007
```

The program counter is in **zero page**, it does not move between samples while the cycle counter
advances by 1.38 million, and the two processor-port bytes have been overwritten: `$00` (the data
direction register) reads `$5E` instead of `$2F` and `$01` reads `$15`.  None of the other fifteen
emulator logs in this report records a JAM.

The rest of that machine at the same moment:

| Byte | Value |
|---|---|
| `uartRxCount` `$E42D`, read/write index | `$14` = 20 (full), `$10` / `$10`, frozen |
| `uartTxCount` `$E414` | `$00` - transmit ring empty |
| `uartPendingCount` `$E0A5` | `$00` |
| `$DE01` status / `$DE02` command / `$DE03` control | `$9C` / **`$05`** / `$1F` |
| `aciaCommandShadow` `$E5BE` / `txCharActive` `$E5BB` | **`$09`** / `$00` |
| `$E039` / `$E03A` | `$00` / `$00` |

The line worth staring at is the command register.  **The chip says `$05` - the transmit interrupt is
armed - while the driver's shadow says `$09`, transmit interrupt off, and `txCharActive` says nothing
of ours is in the chip.**  Status `$9C` has bit 7 (IRQ), bit 4 (TDRE) and bit 3 (RDRF) set.  A 6551
with the transmit interrupt enabled and TDRE true asserts `/IRQ` again as soon as the status read
releases it, and this driver's NMI is the only thing that reads that status.  That the CPU ended up
executing zero page with its bank register smashed is consistent with runaway NMI re-entrancy - the
race the source documents at `commNmiHandler` - but **nothing here measures the path from one to the
other**, and this shape occurred once in four boots.

### 6.3 The repair probe: nothing in the driver brings it back

`realtimeReport.md` proved its diagnosis of the 2400 deadlock by writing a single `$00` over `$E5BB`
from the monitor and watching the machine come back to life.  The same probe was run here, on boot 4:
`$00` written over `uartPendingCount` `$E0A5`, twenty seconds observed, then `$00` over `txCharActive`
`$E5BB` as well and another twenty seconds.

```
  20 s after clearing uartPendingCount $E0A5: conn0 0 bytes, conn1 288 bytes
  20 s after clearing txCharActive $E5BB too: conn0 0 bytes, conn1 294 bytes
```

`conn1` is the machine that was still talking; 288 and 294 bytes in twenty seconds is the 13.3 B/s it
was already producing, unchanged.  `conn0`, the wedged machine, produced **nothing** either time.
That is the honest result and it is a negative one: on this boot the wedged CPU had jammed, and no
value written into any driver byte can restart a jammed 6502, so the probe could not have worked.
**The boot-3 shape - `uartPendingCount` = `$FF` on a machine whose CPU is still running - is the one
worth probing, and it has not been.**  That run predates the probe, and the shape did not recur in
the boot that had it.

## 7. The rate mismatch: 38400 against 300

One machine given **C= + 8**, the other left at the disk default of 300, a link opened between them,
245.8 seconds measured.  Confirmed at every window edge: `$DE03` = `$1F` on A and `$15` on B,
`baudIndex` 18 and 0.

| Window | Wall | A (38400) -> B | B (300) -> A | overflows | `$55` | resync floods |
|---|---:|---|---|---|---|---|
| clean 1 | 75.18 s | 2274 B = **30.25 B/s** | 2226 B = **29.61 B/s** | 0 | 950 / 738 | 0 / 0 |
| clean 2 | 75.21 s | 2335 B = **31.05 B/s** | 2160 B = **28.72 B/s** | 0 | 787 / 834 | 0 / 1 |
| sync tracepoints | 75.34 s | 2701 B = 35.85 B/s | 1900 B = 25.22 B/s | 0 | 824 / 502 | 14 / 0 |
| error tracepoints | 20.04 s | 750 B = 37.43 B/s | 590 B = 29.45 B/s | **0** | 299 / 62 | 0 / 0 |

**Both ends reached `connectionPhase` 3, not one of the 474 sampling intervals carried nothing,
there was not a single receive-ring overflow or bad character at either end, and neither machine
wedged.**  It is
the healthiest run in this whole report.  `uartRxCount` sampled 24 times per machine read max 0 on
the fast side and max 2 on the slow side.  The byte-sync tracepoints counted 12 entries / 13
completions on A and 35 / 35 on B - the lowest resync rate of any pair measured here.  (Completions can exceed
entries because `restartByteSync` `$E80F` re-enters a sync without passing `$E805`.)

Two things explain that, and the second is why the result must be read carefully.

* **The pair simply ran at the slower of the two rates.**  The slow side is pinned at 28.7-29.6 B/s,
  which is 300 baud's 8N1 ceiling to within a byte, and the fast side tracks it at 30-37 B/s.  The
  fast side never actually floods, because the packet layer is lock-step - it will not send the next
  thing until the previous exchange completes, so the peer's rate paces it.  That is genuine
  backpressure, and it is worth knowing that the ARQ layer throttles a fast end for free.
* **VICE's ACIA is byte level.**  It paces whole characters at whatever rate the control register
  asks for but does not shift them bit by bit, so two ends set to different rates do **not** garble
  each other here.  On real hardware a 300-baud receiver sampling a 38400-baud line would raise
  framing and overrun errors on nearly every character; every one of those goes through
  `reportLinkError` `$E3BC` into `countLinkError` `$E403`, and a non-zero `linkErrorCount` makes
  `runLinkStateMachine` restart byte sync whatever phase it was in.  The symptom on hardware would be
  the one `README.md` section 4 already describes: a link stuck in phase 2 for ever, border red,
  never green.

So the honest statement is: **on this rig a rate mismatch does not wedge either end and does not even
hurt - it degrades to the slower rate and keeps playing.**  What it cannot tell you is whether a real
38400-baud line into a real 300-baud receiver wedges, because the error storm that would drive it
cannot be produced here.  What it does establish is that the slow side survives being handed a peer
whose timing is nothing like its own, and that neither state machine falls over on the mismatch
itself.

For the Ultimate specifically, that splits the worry in two.  If the device honours the control
register - as VICE and a real SwiftLink do - the two ends agree and this test says nothing bad
happens.  If it ignores the baud bits and runs at its own default, the failure mode is the
garbage-on-the-line case above, which this rig cannot reproduce; the mitigation is the same either
way, and it is the recommendation at the top: do not offer 38400 at all, and treat 9600 and 19200 as
degraded modes.

---

## 8. What went wrong in the rig, and what it cost

* **Two runs were lost to the module-residency trap `rate1200Report.md` section 6 documented**, and
  it is worth repeating because it bit again.  `testTwoMachines.answerModemPrompts` uses "`$E5BE` is
  non-zero" as its "the link is open" test and `testRealtime.selectRate` uses "`$E013/$E014` reads
  `B7 0D`" as its "the module is resident" test, and while the 1541 is still fetching track 34 at
  true C64 speed, `$E000-$EFFF` still holds the previous overlay.  The first 19200 and 38400 attempts
  read `$E5BB-$E5BE` = `E8 84 18 AC` on machine A and took `$AC` for an open link; that machine then
  never selected its rate and never opened a link at all.  Both runs were killed, their emulators and
  Xvfb displays cleaned up by hand, and `testHighRates.moduleResident` now waits for all sixteen
  bytes of the `$E000` jump table first - which is what `test1200AutoBaud.waitForModule` already did.
  Neither lost run is used for anything above.
* **4800 and 9600 were measured with two emulator pairs on the box at once**, as were 38400 boot 2
  and the mismatch; the rest ran alone.  This is recorded because it is the sort of thing that
  invalidates a real-time measurement - but it did not: every window in the report, parallel or not,
  reports both machines at 100.5% of real time or better against their own cycle counters.  The
  residency failures above came from the *fixed sleeps* in the boot sequence being too short when the
  host was loaded, not from the measurement windows.
* **The instrumented windows are not free, and are kept separate for that reason.**  A clean window
  carries no tracepoints at all, so its byte counts are the honest throughput; the `$E805`/`$E855`
  window and the short `$E403`/`$E505` window carry theirs.  The error windows consistently read
  102-103.5% of real time rather than 100.5-101%, which is pause-and-resume overhead landing inside a
  shorter window, not the emulator running fast.
* **`$DE03` sometimes reads `$00` and `$DE01` `$FF` on a machine that is running perfectly well.**
  That is not the chip being reprogrammed; it is the monitor reading through a CPU that happens to
  have the I/O area banked out at that instant - `readFilmByte`, `writeFilmByte` and `copyPageUnderIo`
  set `$01 = $34` for up to 2600 cycles at a time.  Two of the four 38400 boots caught a machine that
  way at every window edge, which briefly looked like a driver fault and is not one.  Any future test
  that reads the cartridge page through the monitor should read `$01` alongside it; `wedgeDump` does.

## 9. What this does not show

* **VICE, not hardware.**  No physical SwiftLink, no real cable, and above all no C64 Ultimate - the
  device this whole exercise is about is one this report has never touched.
* **VICE's ACIA is byte level**, so no framing or parity error can occur on this rig at all.  "Every
  error was a ring overflow" is a statement about this emulation; on a real line the same tracepoints
  would be expected to show bad characters too, and section 7 is limited by this more than anything
  else here.
* **One boot per rate for 4800, 9600 and 19200.**  245 seconds each is enough to say the link keeps
  moving and recovers, and not enough to say it always will.  38400 got four boots because it failed.
* **The 38400 wedge is characterised, not explained.**  What is established is which bytes are stuck
  at what values in two of the four shapes, that the driver's own `uartPendingCount` resynchronisation
  is not running while they are, that one boot ended in a CPU JAM, and that the 2400-baud repair does
  not apply.  How `uartPendingCount` leaves the range the layers above wait on was not traced - `$FF`
  is what a `dec uartPendingCount` produces from zero and the NMI has such a decrement at `$E6C9`, but
  nothing measured connects the two.  Nor was the path from `$DE02` = `$05` against a `$09` shadow to
  a jammed CPU traced.  Both need the NMI instrumented, which stops the emulator and destroys the
  timing being measured.
* **The `uartPendingCount` repair probe has not been run against the shape it was written for.**  See
  section 6.3.
* **No stock user-port peer, and no complete game.**  As in every earlier report, both machines are
  SwiftLink builds, and they reach the GAME TYPE menu and stay there.
* **Nothing was fixed.**  This report characterises; no driver source file was changed, and
  `checkAbi.py` passes on the image every run above was made against.

## 10. Files

| File | What it is |
|---|---|
| `testHighRates.py` | the test: per-machine rate selection, clean and instrumented windows, `uartRxCount` sampling, the wedge dump and the `uartPendingCount` probe |
| `testLogs/highRate4800.txt`, `highRate9600.txt`, `highRate19200.txt` | the three rates above 2400 that survive |
| `testLogs/highRate38400.txt`, `...run2.txt`, `...run3.txt`, `...run4.txt` | the four 38400 boots, in the order of section 5 |
| `testLogs/highRateMismatch.txt` | 38400 on one machine, 300 on the other |
| `testLogs/highRate.*.samples.txt` | per-second byte counts, both directions, every window |
| `testLogs/highRate.*.trace.txt` | raw tracepoint output, with the cycle counter that attributes a hit to a machine |
| `testLogs/highRate.*.ring.txt` | the `uartRxCount` / phase / transmitter samples behind sections 2, 3 and 5 |
| `shots/hrh*.png` | the menu, the linked pair and the end of every window, for each run |
