639 lines
40 KiB
Markdown
639 lines
40 KiB
Markdown
# Whatever the port is set to: adopt mode, the eight-bit guard, and every rate including 38400
|
|
|
|
> Folded into `testReport.md` as **section 11**, which carries the headline numbers; this file is the
|
|
> full record. It supersedes `highRateReport.md`'s rate recommendation and `hardenReport.md`'s 38400
|
|
> retirement, both of which now carry pointer notes.
|
|
|
|
Two earlier decisions were wrong, and this report is the measurement of their replacements.
|
|
|
|
1. **38400 was made unselectable because it wedged.** That hid a defect instead of fixing it.
|
|
`wedgeDiagnosis.md` then established the defect - unbounded NMI re-entrancy in `commNmiHandler` -
|
|
and the driver now carries a re-entrancy guard. **38400 is selectable again, and in seven boots
|
|
on this build it did not wedge once.**
|
|
2. **The driver imposed its own line settings.** `loadBaudParameters` `$E353` wrote `$10 OR rateBits`
|
|
at every link open: 8 data bits, one stop bit, internal baud generator, whatever the user's
|
|
hardware had been configured for. It now comes up in **adopt mode** and uses the control register
|
|
the hardware already holds.
|
|
|
|
**The one thing that is not negotiable is EIGHT DATA BITS**, and section 6 sets out why with the
|
|
instruction that decides it. Parity and stop bits are free; word length is not.
|
|
|
|
Everything below is VICE 3.7.1 (`x64sc`), NTSC, `-acia1 -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`). Every rate window and every adopt window ran with **two machines at true
|
|
C64 speed**: warp was read back as off through the monitor first, and each window is bracketed by both
|
|
emulators' own free-running cycle counters, the slowest of which reads **100.5% of real time**. The
|
|
guard and stack runs carry their own speed readings in section 4, and one run that was not
|
|
speed-limited at all is reported as exactly that, in section 4.3.
|
|
|
|
---
|
|
|
|
## The answers, in two tables
|
|
|
|
### 1. Every rate, both machines on the rate, 170 seconds of measured link time each
|
|
|
|
| Rate | Control | Both reached phase 3 | Sampling intervals carrying nothing | Receive-ring overflows `$E505` | Characters rejected `$E73F` | Byte-sync rounds entered / completed | Max `uartRxCount` A/B | Transmit characters lost | **Outcome** |
|
|
|---|---|---|---|---|---|---|---|---|---|
|
|
| 300 | `$15` | yes | **0 of 334** | 0 | 0 | 6 / 6 | 1 / 1 | 0 | **healthy** |
|
|
| 1200 | `$17` | yes | **0 of 334** | 2 | 2 | 8 / 6 | 1 / 14 | 0 | **healthy** (one overflow each, recovered) |
|
|
| 2400 | `$18` | yes | **0 of 334** | 0 | 0 | 0 / 1 | 6 / 0 | 0 | **healthy** |
|
|
| 4800 | `$1A` | yes | **0 of 334** | 0 | 0 | 0 / 0 | 0 / 6 | 0 | **healthy** |
|
|
| 9600 | `$1C` | yes | **0 of 334** | 0 | 0 | 0 / 0 | 0 / 3 | 0 | **healthy** |
|
|
| 19200 | `$1E` | yes | **0 of 334** | 0 | 0 | 0 / 0 | 4 / 6 | 0 | **healthy** |
|
|
| 38400 | `$1F` | yes | **0 of 334** | 0 | 0 | 0 / 0 | 1 / 3 | 0 | **healthy** |
|
|
| 38400, second boot | `$1F` | yes | **0 of 452** | 0 | 0 | 0 / 0 | 0 / 1 | 0 | **healthy**, 245.5 s |
|
|
|
|
Where the columns come from: the sampling intervals are the one-second buckets of the null modem's
|
|
byte counts, both directions, across all three windows of the boot (74 + 74 + 19 per direction). The
|
|
two error columns are tracepoint hits summed over both machines, and they only exist in the windows
|
|
that carried those tracepoints - `$E73F`/`$E505` in the 20-second window, `$E805`/`$E855` in the
|
|
75-second sync window. `uartRxCount` is the maximum of 24 samples taken per machine with the emulator
|
|
stopped. The transmit-loss column is VICE's own `data register written although data has not been
|
|
sent yet` count over the whole boot, from each emulator's log.
|
|
|
|
**38400 no longer wedges.** Nothing in this report needs the word "degrades" either: at every one of
|
|
the seven rates the link kept both directions moving in every sampling interval, the 20-byte receive
|
|
ring never reached its limit, and no machine's CPU jammed.
|
|
|
|
### 2. The port's own settings, two machines, before and after the link opens
|
|
|
|
| What the port was set to before the game touched it | What `$DE03` held after the link opened | Link |
|
|
|---|---|---|
|
|
| `$98` - 2400 baud, 8 data bits, **2 stop bits** (plus command `$69`, even parity) | **`$98`** on both machines - adopted whole | healthy, 75.2 s, 9.00 / 8.98 B/s, 0 dead seconds |
|
|
| `$B8` - 2400 baud, **7 data bits**, 2 stop bits | **`$98`** on both machines - only the word length was corrected | healthy, 75.2 s, 8.98 / 8.98 B/s, 0 dead seconds |
|
|
| `$00` - a chip nobody has configured (external clocks) | **`$15`** on both machines - the 300-baud fallback | works, 75.2 s, 10.23 / 10.22 B/s, 0 dead seconds |
|
|
|
|
The old driver would have written **`$15` into all three** of those cases and not looked at what was
|
|
there: `baudIndex` `$E055` came off the disk as 0 = 300 baud, and `loadBaudParameters` wrote
|
|
`$10 OR rateBits` at every link open. A port configured for 2400 8N2 got 300 8N1; a port configured
|
|
for anything at all got 300 8N1.
|
|
|
|
**One thing adopt mode does not cover, and it is not a defect but it is a limit: parity.** Parity
|
|
lives in the 6551's *command* register, not its control register, and the driver rewrites that
|
|
register from constants every time it arms or disarms the transmitter. Section 8 has the
|
|
measurement.
|
|
|
|
---
|
|
|
|
## 1. The build under test
|
|
|
|
```
|
|
$ ./build.sh
|
|
wrote ../Modem_Wars_SwiftLink.d64: module installed in track 18 sector 7 and track 34 sectors 1-15
|
|
$ python3 checkAbi.py build/swiftlinkDriverE000.bin ../disassembly/build/game_modemDriverE000.orig.bin
|
|
bytes changed: 474 of 4096, in 27 region(s)
|
|
$E04C-$E055 (10 bytes)
|
|
$E057-$E05E (8 bytes)
|
|
$E327-$E331 (11 bytes)
|
|
$E342-$E34F (14 bytes)
|
|
$E353-$E35E (12 bytes)
|
|
$E38E-$E38F (2 bytes)
|
|
$E3C7-$E3CA (4 bytes)
|
|
$E402-$E406 (5 bytes)
|
|
$E408-$E40E (7 bytes)
|
|
$E540-$E578 (57 bytes)
|
|
$E587-$E587 (1 bytes)
|
|
$E593-$E5BA (40 bytes)
|
|
$E5E8-$E5E8 (1 bytes)
|
|
$E5EC-$E5F7 (12 bytes)
|
|
$E607-$E61E (24 bytes)
|
|
$E620-$E620 (1 bytes)
|
|
$E622-$E636 (21 bytes)
|
|
$E638-$E63A (3 bytes)
|
|
$E63C-$E63D (2 bytes)
|
|
$E640-$E642 (3 bytes)
|
|
$E645-$E670 (44 bytes)
|
|
$E672-$E684 (19 bytes)
|
|
$E688-$E6CF (72 bytes)
|
|
$E6D1-$E714 (68 bytes)
|
|
$E716-$E732 (29 bytes)
|
|
$E73C-$E73E (3 bytes)
|
|
$E740-$E740 (1 bytes)
|
|
|
|
ABI CHECK PASSED: every address the rest of the game depends on is unchanged
|
|
```
|
|
|
|
Every changed byte is inside the UART layer or the link-control code that owns it: the ACIA
|
|
accessors and their page latch, `loadBaudParameters`, the baud hot keys, the DTR toggle that used to
|
|
poke CIA2, the NMI handler, `baudEntryTable`, and the two operands `ADDRESS_CHANGES.md` names -
|
|
`$E38E-$E38F`, the `JSR dropDtrLine` inside `hangUpModem`, and `$E3C7-$E3CA`, the error-vector pair.
|
|
The three frozen regions - the `$E000` jump table, the shared variables at `$E01D-$E047` and the
|
|
whole of `$EC00-$EFFF` that the map-generator overlay calls - are byte-identical to the stock module.
|
|
|
|
## 2. The rig, and how "true C64 speed" is established
|
|
|
|
Two `x64sc` processes, each on its own private Xvfb display with its own private `vicerc`, their
|
|
serial lines joined by a TCP null modem that counts bytes per direction (`testTwoMachines.NullModemRelay`).
|
|
The host serial rate is set to 38400 on both ends so that it can never be the thing limiting the
|
|
line; the ACIA control register is what these tests are about.
|
|
|
|
* Warp is used **only** for the loader. After `bootPastLoader` both emulators are stopped, `warp off`
|
|
is issued, and `warp` is read back - every run in this report printed `Warp mode is off.` on both
|
|
machines before anything was measured.
|
|
* Every measured window is bracketed by `stopwatch` readings taken with **both** emulators stopped, so
|
|
the two counts cover the same interval. Emulated cycles divided by 1,022,727 Hz divided by wall
|
|
seconds is the speed figure quoted throughout. 75-second windows read **100.5% to 101.6%**;
|
|
20-second windows read **102.0% to 103.6%**, which is pause-and-resume overhead landing inside a
|
|
shorter window rather than the emulator running fast.
|
|
* Nothing writes a driver byte. The only monitor writes anywhere in this report are to the ACIA's own
|
|
registers (section 5), which is what a user's hardware configuration would have left there, and the
|
|
`baudIndex` writes `testStack.py` uses to pick a rate without a keystroke.
|
|
|
|
A caution that cost a measurement. The first `testGuard.py` run in this session issued its
|
|
`warp off` to a **running** emulator rather than a stopped one, and did not read the setting back;
|
|
what came out of it was a trace whose cycle stamps span **62.3 emulated seconds inside an
|
|
eight-second read window**, i.e. the machine was running about eight times real time. The likely
|
|
mechanism is that a line sent to a running emulator is consumed by the monitor's break-in, so the
|
|
command never executed - that was not chased down, because the fix is the same either way: the script
|
|
now does what `testHighRates.py` always did (stop both machines, set it, read it back) and brackets
|
|
its window with the cycle counters, so the run says for itself. Both were verified on the re-runs.
|
|
The lost run is reported anyway, in section 4.3, because of what it accidentally measured.
|
|
|
|
## 3. Every rate, including 38400
|
|
|
|
`testHighRates.py`, one boot per rate, both machines given the same rate with the driver's own hot key
|
|
at the "PRESS A OR O" prompt, three windows per boot:
|
|
|
|
* **clean**, 75 s, no tracepoints at all, so the byte counts are honest throughput;
|
|
* **sync**, 75 s, with tracepoints on `beginByteSyncPhase` `$E805` and on `sta ackPending` `$E855`,
|
|
which count degradations and recoveries directly;
|
|
* **errors**, 20 s, with `countLinkError` `$E73F` and the conditional `$E505 if x > $13`, which
|
|
separate "a character was rejected" from "the 20-byte ring was full".
|
|
|
|
Then `uartRxCount` `$E42D`, `connectionPhase` `$E040`, `linkErrorCount` `$E047`, `txCharActive`
|
|
`$E5BB` and `aciaCommandShadow` `$E5BE` are sampled 24 times per machine with the emulator stopped.
|
|
|
|
### 3.1 What the numbers actually say
|
|
|
|
Every rate carried the same traffic: **about 9 bytes a second in each direction**, 250-odd `$55`
|
|
frame-lead bytes per 75-second window, at 1200 baud and above. That is the game, not the line - the
|
|
two machines sit at the GAME TYPE menu exchanging the lock-step ARQ frames the protocol keeps alive,
|
|
and the rate the wire runs at makes no difference to how many of them there are. It is the premise
|
|
of this whole exercise, measured: **300 baud already carries everything the game produces.**
|
|
|
|
300 baud is the exception, and in the other direction: it moved **29.6 B/s**, which is the 8N1 ceiling
|
|
of a 300-baud line to within a byte, and counted three times as many `$55` lead bytes per window as
|
|
any faster rate (871 against 253). So the slowest rate is the one working hardest. *Why* the frame
|
|
count goes up when the line slows down was not traced; the honest statement is that it did, with zero
|
|
receive errors and zero ring overflows while it happened.
|
|
|
|
| Rate | clean window, A -> B / B -> A | `$55` lead bytes | per-second range | phases seen (A / B) |
|
|
|---|---|---|---|---|
|
|
| 300 | 2228 B = 29.64 B/s / 2235 B = 29.74 B/s | 871 / 781 | 26.0-31.0 / 26.7-31.0 | [2,3] / [2,3] |
|
|
| 1200 | 729 B = 9.70 B/s / 737 B = 9.81 B/s | 268 / 254 | 5.9-26.0 / 5.9-57.0 | [2,3] / [2,3] |
|
|
| 2400 | 675 B = 8.98 B/s / 675 B = 8.98 B/s | 253 / 253 | 5.9-12.0 / 8.9-9.0 | [2,3] / [3] |
|
|
| 4800 | 699 B = 9.30 B/s / 697 B = 9.27 B/s | 258 / 261 | 5.9-24.0 / 5.9-23.0 | [3] / [2,3] |
|
|
| 9600 | 700 B = 9.31 B/s / 694 B = 9.23 B/s | 258 / 261 | 5.9-15.0 / 5.9-17.0 | [3] / [3] |
|
|
| 19200 | 675 B = 8.98 B/s / 675 B = 8.98 B/s | 253 / 253 | 8.9-9.0 / 8.9-9.0 | [2,3] / [3] |
|
|
| 38400 | 678 B = 9.02 B/s / 678 B = 9.02 B/s | 254 / 254 | 5.9-12.0 / 5.9-12.0 | [3] / [3] |
|
|
|
|
`connectionPhase` 2 appearing on one machine is a byte-sync round in progress, and every one of them
|
|
was followed by phase 3 in a later sample. At 300 and 1200 both machines were caught in one; at 9600
|
|
and 38400 neither ever was.
|
|
|
|
### 3.2 The errors, both of them
|
|
|
|
Across the seven rate boots - **1193.8 seconds of measured link time**, 1439.3 s counting the second
|
|
38400 boot - there were **two** characters rejected by the receiver and **two** receive-ring
|
|
overflows, and they are the same two events: one on each machine, both at **1200 baud**, in the
|
|
20-second instrumented window. The unconditional `$E73F`
|
|
tracepoint and the conditional `$E505 if x > $13` returned identical counts, which is what says the
|
|
character was thrown away because the 20-byte ring was full rather than because its status was bad.
|
|
Both machines carried on; neither left the packet phase because of it. That is the same signature
|
|
`rate1200Report.md` recorded, at the same rate, and 1200 is still the first rate at which a burst can
|
|
outrun the ring.
|
|
|
|
**No rate above 1200 produced a single receive error of any kind.** On the previous build, 9600 and
|
|
19200 produced 8-41 and 74-128 ring overflows in the same 20-second window.
|
|
|
|
### 3.3 The transmit-side loss is gone as well
|
|
|
|
VICE writes `Acia1: ACIA: data register written although data has not been sent yet.` into its own log
|
|
every time the driver writes the data register before the previous character has left - one
|
|
transmitted character lost each time, on emulation and on a real 6551 alike. On the previous build
|
|
that scaled with rate: 0.8% of offered characters at 4800, 2.1% at 9600, 2.2-2.3% at 19200.
|
|
|
|
**Every emulator log in this report contains zero of them**, at every rate, including both 38400
|
|
boots. `wedgeDiagnosis.md` section 4 predicted exactly that: it traced the overwrite to a pass of the
|
|
NMI handler being interrupted between `lda txCharActive` `$E6C4` and `dec uartPendingCount` `$E6C9`,
|
|
so an inner pass fed the chip and the outer pass fed it again. With the guard, no pass is ever
|
|
interrupted there.
|
|
|
|
No emulator log in this report contains `Main CPU: JAM` either.
|
|
|
|
## 4. Why 38400 no longer wedges, measured three ways
|
|
|
|
The mechanism is `nmiHandlerState` `$E5BD` and the drain loop at `nmiIdle` `$E6E4`: a second NMI that
|
|
finds a pass already running does not run the handler body, it adds one to the byte and returns - and
|
|
because it never reads the status register, `/IRQ` stays asserted and no further edge can be raised
|
|
until the running pass acknowledges. The running pass decrements the byte and goes round again
|
|
whenever the decrement leaves a note behind.
|
|
|
|
### 4.1 The stack: 200 seconds at 38400, nothing below `$01B4`
|
|
|
|
`testStack.py`, two machines at 38400, both in the packet phase, four checkpoints armed **before the
|
|
link was ever opened** and left armed for a 200-second watch. Run `pcst384b`, which measured its own
|
|
speed: **200.88 emulated seconds on A at 100.4% of real time and 200.58 on B at 100.2%.**
|
|
|
|
| Checkpoint | What it catches | Result |
|
|
|---|---|---|
|
|
| `trace exec e685 if sp < $b4` | any entry to `commNmiHandler` one frame deeper than the deepest healthy entry ever recorded | **0 hits on A, 0 hits on B** |
|
|
| `trace store $0100 $0180` | the stack descending thirteen frames past anything healthy | no hits |
|
|
| `break exec e685 if sp < $20` | the stack nearly gone | **never fired** |
|
|
| `break store $0100 $0140` | the same, from the other side | **never fired** |
|
|
|
|
At the end of the watch both machines were running game code (`PC` `$12A4` and `$12E5`) with
|
|
**`SP` = `$E9`** and, in the processor status byte, **the interrupt flag CLEAR** (`10100000` and
|
|
`00100000`) - so both raster IRQs were still alive and still calling the driver's `$E00C` tick. Page
|
|
1 from `$0100` upwards holds nothing but the flat `$00`/`$FF` pattern that was in it before the link
|
|
opened; there are no handler frames in it below `$01D0` at all.
|
|
|
|
An earlier boot with the same instrument, `pcst384`, gave the same result - 0 deep entries, no break,
|
|
`SP` `$E9` on both - but it predates the speed check added to the script, so its 200 seconds are not
|
|
independently proven to be real-time seconds. It is counted as a boot in section 4.4 and nothing else
|
|
rests on it.
|
|
|
|
On the previous build the same instrument caught three runaways in two boots, each one taking the
|
|
stack pointer from about `$B3` to `$00`-`$08` in **8.4, 11.5 and 12.5 milliseconds**, and one machine
|
|
was stopped mid-descent with 34 complete nested frames still decodable off page 1. The wedged
|
|
machines in `wedgeDiagnosis.md` section 5 were found with the interrupt flag **set** in 124 readings
|
|
out of 124, which is the state the two readings above are the opposite of.
|
|
|
|
### 4.2 The counter: it is entered, it never nests here, and it always comes back to zero
|
|
|
|
`testGuard.py` puts a tracepoint on the handler's entry `$E685` and one on `nmiLeaveNote` `$E6F0`,
|
|
the instruction a nested pass executes instead of running the body:
|
|
|
|
| Run | Rate | Window | Handler entries | Nested | `nmiHandlerState` after | `linkErrorCount` |
|
|
|---|---|---|---|---|---|---|
|
|
| `pcgd384c` A | 38400 | 91.00 emulated s (**101.1%** of real time) | 1632 | **0** | `$00` | 0 |
|
|
| `pcgd384c` B | 38400 | 90.70 emulated s (**100.7%**) | 1620 | **0** | `$00` | 0 |
|
|
| `pcgd384b` A | 38400 | 10.80 emulated s (132.7%) | 175 | **0** | `$00` | 0 |
|
|
| `pcgd384b` B | 38400 | 8.81 emulated s (108.3%) | 144 | **0** | `$00` | 0 |
|
|
| `pcgd192b` A | 19200 | 9.08 emulated s (111.2%) | 156 | **0** | `$00` | 0 |
|
|
| `pcgd192b` B | 19200 | 8.78 emulated s (107.6%) | 144 | **0** | `$00` | 0 |
|
|
|
|
Eighteen handler entries a second at both rates, which is what nine bytes a second in each direction
|
|
should produce - one `RDRF` and one `TDRE` per character. Both machines were in `connectionPhase` 3
|
|
at both edges of every window, and `nmiHandlerState` `$E5BD` read `$00` every time it was read.
|
|
|
|
The 90-second window is worth reading closely, because it says how close the margin actually is.
|
|
Every trace line carries the emulator's cycle counter, so the gap between consecutive entries can be
|
|
measured directly:
|
|
|
|
| | A | B |
|
|
|---|---|---|
|
|
| entries | 1632 | 1620 |
|
|
| gaps below 300 cycles | **817** | **1027** |
|
|
| median of those gaps | **266 cycles** | **266 cycles** |
|
|
| smallest gap seen | 224 | 222 |
|
|
| gaps below 178 cycles (one cheap pass) | **0** | **0** |
|
|
| stack pointer at entry | min `$D9`, max `$F4` | min `$D7`, max `$F0` |
|
|
|
|
**266 cycles is exactly one character time at 38400**, and half of all the gaps are that value, so
|
|
the interrupts really do arrive back to back at the rate `wedgeDiagnosis.md` priced - a burst of
|
|
consecutive characters, each one asking for its own interrupt. Against them stands a handler that
|
|
costs 175 cycles for a single half and 256-266 for a pass that services both. **The margin is
|
|
between zero and ninety cycles**, and in these 3252 entries it was never lost: no gap fell below 178,
|
|
nothing nested, and the stack pointer stayed within one frame of the main line.
|
|
|
|
That is the real shape of the fix. 38400 was never dangerous because it is fast; it was dangerous
|
|
because losing that race once corrupted `uartPendingCount` and the byte in the transmitter, which
|
|
produced a resync flood, which produced the saturation, which produced the runaway. The guard makes
|
|
losing the race cost a six-byte note instead of a nested pass, so the first link in that chain is
|
|
gone.
|
|
|
|
(The two eight-second windows read 108-133% of real time. They open immediately after a monitor
|
|
pause and VICE runs fast for a moment afterwards; the 90-second window and every window in section 3
|
|
do not show it. Nothing in this table depends on wall-clock speed anyway - entries, nesting, gaps and
|
|
the guard byte are all counted in the emulated domain.)
|
|
|
|
### 4.3 The accidental stress test, and what it is worth
|
|
|
|
The lost run described in section 2 - the one that ran eight times real time - is the only
|
|
measurement in this session in which the guard was actually **used**, and it is worth keeping for
|
|
that reason. Why it was harder on the driver was not chased down; the obvious candidate is that two
|
|
emulators running unlimited do not run at the same multiplier, so each one's bytes arrive in the
|
|
other's emulated time frame in bursts that true speed never produces. Whatever the reason, the
|
|
numbers below are the numbers, and they are the ones the guard exists for:
|
|
|
|
| Machine | Entries to `$E685` | Nested (`$E6F0`) | Emulated span | Entry rate | SP at entry | Gap between entries |
|
|
|---|---:|---:|---|---:|---|---|
|
|
| A | 3050 | **260 (8.5%)** | 62.30 s | 49/s | min `$D0`, max `$F0` | min **58 cycles**, median 540 * |
|
|
| B | 1685 | **145 (8.6%)** | 38.95 s | 43/s | min `$D6`, max `$F4` | min **58 cycles**, median 271 * |
|
|
|
|
\* median taken over the gaps below 100000 cycles, i.e. excluding the quiet stretches between bursts.
|
|
|
|
Read the two things that matter. **Second NMIs really do arrive inside a running pass** - a 58-cycle
|
|
gap against a handler that costs 175 to 266 cycles is not a near miss - and there were 405 of them.
|
|
**The stack pointer at entry never went below `$D0`**, one frame below the interrupted level and 128
|
|
bytes above the `$B4` line that the previous build crossed on its way to zero. Both machines ended
|
|
the window in the packet phase with `linkErrorCount` = 0 and `nmiHandlerState` = `$00`.
|
|
|
|
On the previous build every one of those 405 nested entries would have been a full re-entrant pass
|
|
with a status read in it.
|
|
|
|
### 4.4 Seven boots at 38400, no wedge
|
|
|
|
| Boot | What it did | Outcome |
|
|
|---|---|---|
|
|
| `pc384` | `testHighRates.py`, 170.4 s of measured windows | healthy, phase 3 in all 24 ring samples |
|
|
| `pc384b` | `testHighRates.py`, 245.5 s of measured windows | healthy, phase 3 in all 24 ring samples |
|
|
| `pcst384b` | `testStack.py`, 200.9 s at 100.4% of real time | no deep entry, no break, `I` clear at the end |
|
|
| `pcst384` | `testStack.py`, the same watch before the speed check was added | no deep entry, no break |
|
|
| `pcgd384` | `testGuard.py`, the unlimited run of section 4.3 | 405 nested passes absorbed, still linked |
|
|
| `pcgd384b` | `testGuard.py`, 8 s at true speed | 319 entries, none nested |
|
|
| `pcgd384c` | `testGuard.py`, 90 s at true speed | 3252 entries, none nested, both still in phase 3 |
|
|
|
|
The previous build wedged **four boots out of four**, every one inside the first minute, one of them
|
|
ending with `*** Main CPU: JAM at $0007`.
|
|
|
|
## 5. Adopt mode, two machines, before and after
|
|
|
|
`testAdoptPair.py`. Both machines are booted to the "PRESS A OR O" prompt with the opponent module
|
|
resident - which is the last moment at which the chip has not been touched, confirmed by
|
|
`aciaPageLatch` `$E5BF` = `$00` and the adopt entry `$E726` = `$00` on both machines - and then the
|
|
6551's control register is written from the monitor, exactly as a C64 Ultimate or a
|
|
previously-configured cartridge would have left it. Then the link is opened and measured.
|
|
|
|
### 5.1 Two stop bits at 2400 baud, and even parity in the command register
|
|
|
|
Preset: control `$98`, command `$69`.
|
|
|
|
```
|
|
$DE03 $DE02 baudIndex bitPeriodLo adoptSlot pageLatch
|
|
A at the prompt $00 $00 $15 $50 $00 $00
|
|
A after preset $98 $69 $15 $50 $00 $00
|
|
A link opened $98 $05 $15 $98 $98 $DE
|
|
A in phase 3 $98 $09 $15 $98 $98 $DE
|
|
A after 75 s $98 $09 $15 $98 $98 $DE
|
|
B at the prompt $00 $00 $15 $50 $00 $00
|
|
B after preset $98 $69 $15 $50 $00 $00
|
|
B link opened $98 $09 $15 $98 $98 $DE
|
|
B in phase 3 $98 $05 $15 $98 $98 $DE
|
|
B after 75 s $98 $09 $15 $98 $98 $DE
|
|
```
|
|
|
|
`$98` is 2400 baud, 8 data bits, **two stop bits**, internal generator - a byte the driver's own table
|
|
does not contain and would never have chosen. **It came through untouched.** `baudIndex` stayed at
|
|
`$15` = 21, the adopt entry (the disk default, so nothing had to be pressed); `aciaProbeAdopted`
|
|
`$E655` parked the adopted byte in `baudEntryTable`'s eighth entry at `$E726`; `loadBaudParameters`
|
|
wrote it to the chip and kept the live copy in `bitPeriodLo` `$E056`.
|
|
|
|
And the link works on it: **75.20 s, 677 and 675 bytes = 9.00 and 8.98 B/s, not one of the 148
|
|
one-second sampling intervals carrying nothing** (5.9-12.0 B/s one way, 8.9-9.0 the other), both
|
|
machines in `connectionPhase` 3, A and B running at 101.0% and 100.6% of real time.
|
|
|
|
The command register is the other half of that dump and section 8 is about it.
|
|
|
|
### 5.2 A note on `baudIndex` `$E055` = `$15`
|
|
|
|
Two different `$15`s meet in these dumps and it is worth separating them once. `baudIndex` `$15` is
|
|
**21 decimal**, the byte offset of the eighth entry of `baudEntryTable` - the adopt entry. Control
|
|
register `$15` is 300 baud, 8N1, internal generator. They are unrelated, and section 7 has both of
|
|
them in the same dump at the same time.
|
|
|
|
## 6. The eight-data-bit guard
|
|
|
|
Preset: control `$B8` = 2400 baud, **7 data bits**, two stop bits, internal generator.
|
|
|
|
```
|
|
$DE03 baudIndex bitPeriodLo adoptSlot pageLatch
|
|
A after preset $B8 $15 $50 $00 $00
|
|
A link opened $98 $15 $98 $98 $DE
|
|
B after preset $B8 $15 $50 $00 $00
|
|
B link opened $98 $15 $98 $98 $DE
|
|
```
|
|
|
|
`$B8` -> `$98`: **bits 6-5 went from `%01` (7 data bits) to `%00` (8 data bits), and nothing else in
|
|
the byte moved.** Bit 7 (the second stop bit) survived, bit 4 (the internal clock source) survived,
|
|
and bits 3-0 (2400 baud) survived. That is the `and #$9F` at `$E665` inside `aciaProbeAdopted`, and
|
|
it is the only place in the module where a control byte is masked - the seven fixed entries are
|
|
constants that already have those bits clear. Link: **75.15 s, 675 bytes each way = 8.98 B/s,
|
|
8.9-9.0 B/s in every one of the 148 one-second intervals, none of them empty, both machines in phase
|
|
3.**
|
|
|
|
The single-machine table in `testAdopt.py` covers the other word lengths on the same build, 46 of 46
|
|
checks passing: `$5A` (six data bits at 4800) -> `$1A`, `$3C` (seven data bits at 9600) -> `$1C`,
|
|
`$9F` (38400 with two stop bits) -> `$9F` unchanged, `$18` -> `$18` unchanged.
|
|
|
|
### Why eight data bits are mandatory
|
|
|
|
This is a statement about the source, not a measurement, and it cannot be measured on this rig -
|
|
VICE's ACIA is byte level, so it will not truncate a character to seven bits no matter what the
|
|
control register says. The argument does not need a measurement, because one instruction decides it:
|
|
|
|
```
|
|
awaitPeerSyncFf:
|
|
...
|
|
jsr receiveByteTrackZeros ; E84C look for the peer's answer
|
|
tax ; E84F
|
|
beq linkTickReturn ; E850 nothing yet
|
|
inx ; E852 was it $FF?
|
|
bne restartByteSync ; E853 anything else: start the sync over
|
|
sta ackPending ; E855 $FF received, both ends are byte aligned
|
|
```
|
|
|
|
The byte-sync handshake is `$00` one way and **`$FF`** the other, and `$FF` is tested for exactly, by
|
|
incrementing it to zero. On a seven-bit line the peer's `$FF` arrives as `$7F`, `INX` makes `$80`,
|
|
the branch is taken, and the sync restarts - for ever. The link would never leave
|
|
`connectionPhase` 2: red border, never green, which is the symptom `README.md` section 4 describes.
|
|
|
|
Two more places in the frame layer need the eighth bit and would corrupt data rather than stall it:
|
|
`computePacketChecksum` `$E937` is a full eight-bit rotate-and-add (`ASL A` / `ADC #$00` / `ADC
|
|
packetBuffer,x`), and the length code the frame layer leads with is `$55`, with the sequence bytes
|
|
that follow it taking any value at all. **Parity and stop bits cost the frame layer nothing** - they
|
|
change how a character is delivered, not what is in it - which is exactly why the mask at `$E665`
|
|
touches two bits and no others.
|
|
|
|
## 7. The unusable-value fallback
|
|
|
|
Preset: control `$00`, on both machines - what a 6551 reads after reset, and what VICE's cold ACIA
|
|
reads on this rig without any help (see the "at the prompt" rows in section 5.1, which read `$00`
|
|
before anything was written).
|
|
|
|
```
|
|
$DE03 baudIndex bitPeriodLo adoptSlot pageLatch
|
|
A after preset $00 $15 $50 $00 $00
|
|
A link opened $15 $15 $15 $15 $DE
|
|
B after preset $00 $15 $50 $00 $00
|
|
B link opened $15 $15 $15 $15 $DE
|
|
```
|
|
|
|
`(control AND $1F) < $11` means bit 4 = 0 - an external receiver clock a SwiftLink does not have - or
|
|
rate bits `%0000`, an external transmit clock, or both. `$00` is both. The driver replaced it with
|
|
`$15`: 300 baud, 8N1, internal generator.
|
|
|
|
**It did not hang.** Both machines opened the link and carried traffic for the full window: 75.16 s,
|
|
769 and 768 bytes = 10.23 and 10.22 B/s, **0 of 148 one-second intervals empty**, `linkErrorCount` =
|
|
0 in all 24 samples, `uartRxCount` never above 1.
|
|
|
|
The per-second range is the independent check that the rate really did change: **7.9 to 30.0 B/s one
|
|
way and 6.9 to 30.0 the other**, where every window at 1200 baud and above sat between 5.9 and 12.0.
|
|
30.0 B/s is the 8N1 ceiling of a 300-baud line, so the fallback is not just a byte in a register - the
|
|
wire is running at it. `connectionPhase` oscillated between 2 and 3 on both machines during the
|
|
sampling, which is what 300 baud does on this rig (the dedicated 300-baud rate run in section 3 shows
|
|
the same pattern on both machines), and traffic never stopped while it did.
|
|
|
|
The single-machine table adds the two other unusable shapes on the same build: `$0C` (9600 baud but
|
|
bit 4 clear) -> `$15`, and `$10` (internal clock but no rate bits at all) -> `$15`.
|
|
|
|
## 8. What adopt mode does not cover: parity, and the command register
|
|
|
|
In section 5.1 the command register was pre-set to `$69` - DTR on, receiver interrupt on, **parity
|
|
enabled, even** - alongside the control register. After the link opened it read `$05` and then
|
|
`$09`. The parity bits were cleared.
|
|
|
|
That is not a bug in the adopt path; it is a consequence of where a 6551 keeps parity. Bits 7-5 of
|
|
the **command** register select it, and the driver writes that register from constants at four sites:
|
|
`$E546` and `$E5A4` write `$09` (DTR on, RTS low, receiver IRQ on, transmit IRQ off), `$E5A9` writes
|
|
`$0B` (the same with the receiver IRQ off, for the disk-load suspend), and `$E63F` writes `$05` (the
|
|
transmit interrupt armed). Every one of those has bits 7-5 = `%000` = parity disabled. The one
|
|
write that is not a constant - the `C= + H` DTR toggle at `$E327`, which EORs bit 0 of
|
|
`aciaCommandShadow` - only ever carries forward what those constants left there. And the register
|
|
cannot be part-written: DTR, RTS and both interrupt enables live in the same byte, so arming the
|
|
transmitter necessarily rewrites the parity bits too.
|
|
|
|
**Consequences, stated plainly.**
|
|
|
|
* A port configured for parity will run without it. Both ends of a *Modem Wars* link run this same
|
|
driver and both clear those bits identically, so the two agree and the link works - which is what
|
|
section 5.1 measured with `$69` pre-set on both machines.
|
|
* What it would break is a link to something that is *not* this driver and does expect parity. There
|
|
is no such peer: the only other implementation is the stock user-port driver, which is 8N1 only.
|
|
* Stop bits are adopted and parity is not, so `spec_uart.md` hazard 8's "same 8N1 framing" is now only
|
|
true of the *data* bits. Two SwiftLink ends will agree on whatever the control register said; a
|
|
stock user-port peer can only ever be 8N1, so an adopted `$98` will not talk to one.
|
|
|
|
Making parity adoptable would mean keeping the user's bits 7-5 in a shadow and OR-ing them into all
|
|
four command writes. That was not done and was not measured; it is recorded here as the honest edge
|
|
of what "adopt" currently means.
|
|
|
|
## 9. No regression
|
|
|
|
### 9.1 Both straps still detect and latch
|
|
|
|
`testStrap.py`, one machine, watchpoints on `$DE00-$DE03`, `$DF00-$DF03` and both register-mirror
|
|
ranges armed before the module can touch anything.
|
|
|
|
**`$DE00` strap** - the cartridge answers on the default page:
|
|
|
|
```
|
|
load $DE03 LDA $DE03 at $E56B A=$00 aciaProbeAdopted reads the user's setting FIRST
|
|
store $DE03 STA $DE03 at $E567 A=$1E probe pattern 1
|
|
load $DE03 LDA $DE03 at $E56B A=$1E it matches
|
|
store $DE03 STA $DE03 at $E567 A=$15 probe pattern 2
|
|
load $DE03 LDA $DE03 at $E56B A=$15 it matches too - latch = $DE
|
|
store $DE03 STA $DE03 at $E567 A=$15 loadBaudParameters writes the adopted byte ($00 was
|
|
load $DE03 LDA $DE03 at $E56B A=$15 unusable, so the $15 fallback)
|
|
load $DE00 LDA $DE00 at $E55F A=$00 drain the receiver
|
|
load $DE01 LDA $DE01 at $E558 A=$50 status
|
|
store $DE02 STA $DE02 at $E554 A=$09 command
|
|
```
|
|
|
|
**`$DF00` strap** - `SWIFTLINK_ACIA_BASE=0xDF00`, nothing at `$DE00`:
|
|
|
|
```
|
|
load $DE03 LDA $DE03 at $E56B A=$F6 the adopt read on the default page: open bus
|
|
store $DE03 STA $DE03 at $E567 A=$1E probe pattern 1
|
|
load $DE03 LDA $DE03 at $E56B A=$F6 not $1E - this page is not a 6551
|
|
load $DF03 LDA $DF03 at $E56B A=$00 the fallback reads the REAL chip's setting
|
|
store $DF03 STA $DF03 at $E567 A=$1E probe pattern 1 at $DF00
|
|
load $DF03 LDA $DF03 at $E56B A=$1E it matches
|
|
store $DF03 STA $DF03 at $E567 A=$15 probe pattern 2
|
|
load $DF03 LDA $DF03 at $E56B A=$15 it matches too - latch = $DF
|
|
store $DF03 STA $DF03 at $E567 A=$15 loadBaudParameters, adopted $15 again
|
|
```
|
|
|
|
That second block is the one adopt mode changed: the `$DF00` fallback now **reads** the page before it
|
|
writes to it, so a user whose cartridge is strapped to `$DF00` gets their own settings adopted from
|
|
their own chip and not from the open bus at `$DE00`. The `$F6` the `$DE00` read returned is a value
|
|
`probeOpenBus.py` recorded in half its samples of an empty `$DE03`; it is briefly parked in the adopt
|
|
slot and then overwritten by the `$DF00` read, which is the whole reason the fallback calls the same
|
|
routine instead of only aiming at the page.
|
|
|
|
On both straps:
|
|
|
|
| | `$DE00` run | `$DF00` run |
|
|
|---|---|---|
|
|
| all six accessors' operand high bytes (`$E556`, `$E55A`, `$E561`, `$E565`, `$E569`, `$E56D`) | `$DE` | `$DF` |
|
|
| **`aciaPageLatch` `$E5BF`** | **`$DE`** | **`$DF`** |
|
|
| the cartridge page read back after the link opened | `$DE00-$DE03` = `00 10 05 15` | `$DF00-$DF03` = `00 10 09 15` |
|
|
| the *other* page read back | `$DF00-$DF03` = `F6 F6 F6 F6` (open bus) | `$DE00-$DE03` = `F6 F6 F6 F6` |
|
|
| the NMI vector `$FFFA` | `$E685` | `$E685` |
|
|
| accesses to the mirror ranges `$DE04-$DEFF` / `$DF04-$DFFF` | **0** | **0** |
|
|
| stops in a further 45 s with only the mirror tripwires armed | **0** | **0** |
|
|
| the game reached the modem prompts and opened a link | yes | yes |
|
|
|
|
One byte in those dumps needs a word of warning, because it looks alarming and is not:
|
|
`nmiHandlerState` `$E5BD` read `$01` in both strap runs. Every watchpoint in this test is on an ACIA
|
|
register, and almost every ACIA register access this driver makes is made from inside
|
|
`commNmiHandler` - so the emulator was stopped *inside a pass*, which is exactly when that byte is
|
|
supposed to be 1. The reading that says the guard byte rests at zero is the one in section 4.2,
|
|
taken with the machine stopped by nothing but a monitor pause.
|
|
|
|
### 9.2 1200 baud, two machines, still healthy
|
|
|
|
The 1200-baud row of section 3 is that run: `testHighRates.py`, one boot, both machines on **C= + 1**,
|
|
170.3 s of measured link time across three windows, both machines at 100.5-103.5% of real time.
|
|
729 and 737 bytes in the clean window (9.70 and 9.81 B/s), **0 of 334 sampling intervals carrying
|
|
nothing**, two byte-sync rounds entered on A and six on B with three completions each, and the two
|
|
receive-ring overflows of section 3.2 - one per machine - recovered from. `uartRxCount` reached 14 on
|
|
B and never the 20-byte limit. Both machines were found in `connectionPhase` 3 in most of the 24
|
|
samples and in phase 2 in the rest, which is a byte-sync round in progress; every one of them was
|
|
followed by a phase-3 reading.
|
|
|
|
## 10. What this does not show
|
|
|
|
* **VICE, not hardware.** No physical SwiftLink, no real cable, and no C64 Ultimate - the device this
|
|
whole exercise is about has still never been touched by any of it.
|
|
* **VICE's ACIA is byte level.** It paces whole characters at the rate the control register asks for
|
|
but does not shift them bit by bit, so **no framing or parity error can be produced on this rig at
|
|
all**, and a wrong word length cannot corrupt a character here. Two consequences: "zero characters
|
|
rejected at every rate above 1200" is a statement about this emulation, and the eight-data-bit
|
|
argument in section 6 is a reading of the source rather than an experiment.
|
|
* **Adopt mode is verified against VICE's control register, not against a device's idea of one.**
|
|
Everything in sections 5 to 7 assumes the hardware *presents* its configuration in the 6551 control
|
|
register the way VICE and a real SwiftLink do. If an Ultimate presents `$00` there while actually
|
|
running at some other rate, this driver will adopt 300 baud and be wrong, and nothing here can tell
|
|
you whether it does.
|
|
* **The traffic is the menu, not a game.** Both machines reach the GAME TYPE menu and stay there, as
|
|
in every earlier report in this directory. Nine bytes a second in each direction is what that
|
|
produces. A running battle may produce more, and the rates were not measured under it.
|
|
* **38400 is healthy at this traffic, not at any traffic.** Section 4.2 is explicit: at nine bytes a
|
|
second the handler is entered eighteen times a second and, in 3252 entries, never nested - but half
|
|
of those entries were one character time apart, so the margin measured there is between zero and
|
|
ninety cycles. The arithmetic that killed the old build - two interrupts per 266-cycle character
|
|
time against a 178-cycle handler - still says a *saturated* 38400 line costs more CPU than a C64
|
|
has. What the guard changes is what happens then: the receive ring overflows and the ARQ layer resynchronises, instead of the stack eating page
|
|
1. That degradation path was exercised at 1200 in this report and at 9600/19200 on the previous
|
|
build; it was never provoked at 38400 here, because nothing in this game saturates a 38400 line.
|
|
* **One boot per rate**, except 38400, which got seven. 170 seconds is enough to say a link keeps
|
|
moving; it is not enough to say it always will.
|
|
* **Parity is not adopted** (section 8), and no measurement here says what a device that really is
|
|
configured for parity would do with a driver that turns it off.
|
|
* **The `uartPendingCount` `$E0A5` repair probe never ran**, because `testHighRates.py` only runs it
|
|
when a direction carries zero bytes for a whole window, and no direction ever did.
|
|
|
|
## 11. Files
|
|
|
|
| File | What it is |
|
|
|---|---|
|
|
| `testHighRates.py` | the rate sweep: hot-key rate selection, clean and instrumented windows, ring sampling, the wedge dump |
|
|
| `testAdoptPair.py` | **new**: two machines, a pre-programmed control (and optionally command) register, before/after dumps and a measured window |
|
|
| `testAdopt.py` | the single-machine preset table, the 38400 hot key and `C= + U` |
|
|
| `testGuard.py` | entries to `commNmiHandler` and how many of them were nested; now forces warp off with both machines stopped and brackets its window with the cycle counters |
|
|
| `testStack.py` | the deep-nesting tracepoint and the stack-page checkpoints; same warp and speed fix |
|
|
| `testStrap.py` | both straps, with watchpoints on both cartridge pages and their mirrors |
|
|
| `testLogs/portCompat300.txt` ... `portCompat38400.txt` | the seven rate runs |
|
|
| `testLogs/portCompat38400run2.txt` | the second 38400 boot, four windows, 245.5 s |
|
|
| `testLogs/portCompatAdopt98.txt`, `...AdoptB8.txt`, `...Adopt00.txt` | the three two-machine adopt runs |
|
|
| `testLogs/portCompatAdoptTable.txt` | the single-machine table, 46 of 46 checks |
|
|
| `testLogs/portCompatStack38400b.txt` | the 200-second stack watch, with its own speed reading |
|
|
| `testLogs/portCompatStack38400.txt` | the same watch, one boot earlier |
|
|
| `testLogs/portCompatGuard38400b.txt`, `...Guard19200b.txt`, `...Guard38400c.txt` | the guard counts at true speed |
|
|
| `testLogs/portCompatGuard38400.txt`, `testLogs/stack.pcgd384.[AB].deep.txt` | the unlimited-speed run of section 4.3 and its raw trace |
|
|
| `testLogs/stack.pcgd384c.[AB].deep.txt` | the 90-second trace behind the inter-entry gap table in section 4.2 |
|
|
| `testLogs/portCompatStrapDe00.txt`, `...StrapDf00.txt` | the two strap runs |
|
|
| `testLogs/highRate.pc*.samples.txt` / `.trace.txt` / `.ring.txt` | per-second byte counts, raw tracepoint output and the ring samples behind section 3 |
|
|
| `shots/hrpc*.png`, `shots/apap*.png`, `shots/pcadopt*.png`, `shots/pcstrapD*.png`, `shots/stpcst384*.png` | both machines at every stage of every run |
|