31 KiB
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.mdfor 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.mdthen 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.mdsections 3 and 4,testReport.mdsection 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,
linkErrorCountforces 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 $0007on 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,$1Eand$1Fall appeared in$DE03with the matchingbaudIndex. 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$E403tracepoint recorded zero hits on either machine in the 20-second instrumented window, and so did the conditional ring-full tracepointtrace exec $e505 if x > $13. - The resyncs that do happen are the packet layer's own, not the UART's. With no
$E403hits at all, the 44 and 23 byte-sync floods in window 2 came fromreceivePacket'sunwindAndResyncpaths (two$00in a row, an invalid length code, a bad check byte), not from a dropped character. - They complete. Tracepoints on
beginByteSyncPhase$E805and 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$E040was seen at both 2 and 3 on both machines,isLinkActive$E03B=$01at every edge, andtxCharActive$E5BB/aciaCommandShadow$E5BEwere only ever the two self-consistent pairs$00/$09and$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
$E403tracepoint and the conditional$E505 if x > $13one 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 asspec_uart.mdhazard 2 predicts, and it is the same signaturerate1200Report.mdfound 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 reachedsta ackPendingand 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$E855is not a loss; the next one starts immediately, and$E855under-counts anyway, becauserestartByteSync$E80Fre-enters a sync without passing$E805.)connectionPhasewas found at both 2 and 3 on both machines, anduartRxCountsampled 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$E3BCintocountLinkError$E403, and a non-zerolinkErrorCountmakesrunLinkStateMachinerestart byte sync whatever phase it was in. The symptom on hardware would be the oneREADME.mdsection 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.mdsection 6 documented, and it is worth repeating because it bit again.testTwoMachines.answerModemPromptsuses "$E5BEis non-zero" as its "the link is open" test andtestRealtime.selectRateuses "$E013/$E014readsB7 0D" as its "the module is resident" test, and while the 1541 is still fetching track 34 at true C64 speed,$E000-$EFFFstill holds the previous overlay. The first 19200 and 38400 attempts read$E5BB-$E5BE=E8 84 18 ACon machine A and took$ACfor 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, andtestHighRates.moduleResidentnow waits for all sixteen bytes of the$E000jump table first - which is whattest1200AutoBaud.waitForModulealready 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/$E855window and the short$E403/$E505window 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. $DE03sometimes reads$00and$DE01$FFon 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,writeFilmByteandcopyPageUnderIoset$01 = $34for 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$01alongside it;wedgeDumpdoes.
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
uartPendingCountresynchronisation is not running while they are, that one boot ended in a CPU JAM, and that the 2400-baud repair does not apply. HowuartPendingCountleaves the range the layers above wait on was not traced -$FFis what adec uartPendingCountproduces from zero and the NMI has such a decrement at$E6C9, but nothing measured connects the two. Nor was the path from$DE02=$05against a$09shadow to a jammed CPU traced. Both need the NMI instrumented, which stops the emulator and destroys the timing being measured. - The
uartPendingCountrepair 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.pypasses 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 |