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

621 lines
44 KiB
Markdown

# spec_uart.md - the hardware contract of the stock Modem Wars UART layer
> **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.
What this file is: the exact, verified behaviour of every routine in `disassembly/game/modemDriverE000.s`
that touches the C64's hardware, written so the same routines can be rebuilt against a 6551 ACIA on a
SwiftLink cartridge without any code above them noticing.
Every address, instruction boundary and caller below was checked against the listing
`disassembly/game/modemDriverE000.s` and `disassembly/XREF.txt`. Where the shipped comments in the
listing are wrong, this file says so and gives the correct behaviour.
## 0. Conventions
* "Range" gives the first byte of the routine and, after the dash, **the address of the byte after its
last** - so `$E534-$E540` means the routine occupies `$E534..$E53F` inclusive and the next routine
starts at `$E540`. A replacement must fit in exactly that span or leave the tail padded.
* "Callers" lists the address of the JSR/JMP/branch, not the routine name only, so each can be checked.
* Flag names are the 6502's: N Z C V.
* "the frame tick" means the raster IRQ at `$1129`/`$112C`, which calls jump-table entry `$E000` with
X=0 and then entry `$E00C`, in that order, once per frame.
## 1. Where the seam actually is
Everything above the UART layer reaches the hardware through a very small number of doors. These are
the complete set; if the replacement honours all of them, nothing above changes:
| Door | Direction | Used by |
|---|---|---|
| `queueByteForTransmit` `$E73C` | byte out | `$E7D2`, `$E81D`, `$E87C`, `$E92A` |
| `popUartRxRing` `$E4D2` (via thunk `$E739`) | byte in | `$E743`, `$E799`, `$E80A`, `$E9E9` |
| `flushUartTxRing` `$E534` | discard queued output | `$E791`, `$E80F` |
| `uartPendingCount` `$E0A5` | "how many bytes do I still owe the wire" | `$E1EC`, `$E26E`, `$E628`, `$E76B`, `$E82D`, `$E860` |
| `linkStatus` `$E03C` / `linkStatusSample` `$E03D` | carrier + activity | `$E622`, `$E787`, `$E7E3`, and the game at `$1B4B`, `$1BAB`, `$1BB6`, `$56B1`, `$65BC` |
| `reportLinkError` `$E3BC` -> `countLinkError` `$E403` | "that character was bad" | NMI only (`$E724`) |
| `nmiSuspendRequest`/`nmiSuspendAck` `$E039`/`$E03A` | disk-load handshake | game at `$0F67`/`$0F6A` and `$0F78`/`$0F7B` |
| `ciaIcrShadow` `$E033` | mask the game restores after banking I/O out | game at `$588C`, `$58AB`, `$58D0` |
| `$E003` X=0/1/2/3 and the hot keys | open, init, stop, hang up, baud, DTR | `$E29A` dispatcher |
No code outside the module references anything in `$E048-$EBFF` of the modem driver. (A scan of
`XREF.txt` for external references into that window returns only `trainerSetupEC00` and `droneAiEE00`
entries, which belong to the *trainer* overlay that shares those addresses, not to this module.) So
the whole UART layer is free to be rewritten as long as the doors above keep their addresses and
semantics, and the three frozen regions - `$E000-$E017`, `$E01D-$E047`, `$EC00-$EFFF` - come out
byte-identical.
Note that the frozen constraint on `$E01D-$E047` is on the **disk image bytes**, not on runtime
meaning: `$E039-$E047` is zeroed by `clearLinkVars` at run time anyway, and `$E045`/`$E046`/`$E047`
ship as the leftover ASCII `D` `T` `H` from the sector image. A replacement may give those bytes new
runtime meanings; it may not change what is stored in the file.
## 2. Hardware the stock UART layer owns
Every CIA2 access in the whole module, with the address of the instruction:
| Register | Read at | Written at | Role |
|---|---|---|---|
| `CIA2_PRA $DD00` | `$E56D`, `$E66E`, `$E6BC` | `$E570`, `$E6C5` | PA2 = TXD. Always read-modify-write, because PA0/PA1 are the VIC bank select |
| `CIA2_PRB $DD01` | `$E32A`, `$E5F1`, `$E6F1` | `$E32D`, `$E568`, `$E577` | PB0 = RXD, PB1 = RTS, PB2 = DTR, PB4 = DCD, PB5 = spare output |
| `CIA2_DDRB $DD03` | - | `$E55F` | `$26`: PB1, PB2, PB5 out |
| `CIA2_TA_LO/HI $DD04/$DD05` | - | `$E547`/`$E542`, `$E65D`/`$E657` (X=0) | receive bit clock latch |
| `CIA2_TB_LO/HI $DD06/$DD07` | `$E68D`, `$E695` (hi) | `$E65D`/`$E657` (X=2) | transmit bit clock latch |
| `CIA2_SDR $DD0C` | - | `$E559` | one fixed pattern shifted out of SP2 (user-port pin 7); purpose unknown |
| `CIA2_ICR $DD0D` | `$E596`, `$E690`, `$E69C` | `$E59B`, `$E67C`, `$E68A`, `$E6CA`, `$E702` | NMI source mask / flags |
| `CIA2_CRA $DD0E` | - | `$E54C`, `$E59E`, `$E6E9`, `$E729` | timer A control |
| `CIA2_CRB $DD0F` | `$E607` | `$E651`, `$E5A1`, `$E6AD` | timer B control |
Nothing in the module touches `CIA2_DDRA`; PA2 is already an output because the boot loader and fast
loader set DDRA. A 6551 driver should touch **none** of the above except `CIA2_ICR`, and there only
to keep it disabled (see section 5).
## 3. Routine contracts
### `$E353 loadBaudParameters` - range `$E353-$E362`
Copies the three-byte baud entry `baudParameterTable+X` into the live bit-period and pacing bytes.
* **In:** `X` = 0 (300 baud) or 3 (1200 baud). `Y` is loaded internally.
* **Reads:** `$E059+X`, `$E059+X+1`, `$E059+X+2`.
* **Writes:** `$E056 bitPeriodLo`, `$E057 bitPeriodHi`, `$E058 txPaceReload`.
* **Out:** `A = $FF` (N=1, Z=0), `X` = X_in+3, `Y = $00`, C unchanged, memory as above.
The `A = $FF` return value is load bearing: the C=+3 / C=+1 hot keys fall into this routine and use
its return as the "key swallowed" answer to the game.
* **Callers:** JSR `$E2C9` (`openCommLink`), JSR `$E7B2` (`switchTo1200Baud`, on a `CONNECT 1200`
result code); **fall-through from `$E350`** (`STX baudIndex`) on the C=+3 and C=+1 hot keys.
`XREF.txt` lists only the two JSRs because the third path is a fall-through.
* **Trap for a 6551 build:** the loop stores with `STA $DF59,Y` (`$E358`), abusing `$DF59+$FD = $E056`
so one register both counts and indexes. `STA abs,Y` always performs a dummy **read** at
`(base AND $FF00) OR ((baseLo+Y) AND $FF)`, i.e. at `$DF56`, `$DF57` and `$DF58`. On a SwiftLink
strapped to `$DF00`, `$DF58 AND 3 = 0` is the **data register**: that dummy read silently consumes a
received byte and clears RDRF. This is the only instruction in the entire game that names an
address in the `$DE00`/`$DF00` I/O windows, so rewriting these three stores as plain absolute STAs
removes the whole class of problem.
### `$E534 flushUartTxRing` - range `$E534-$E540`
Throws away everything queued for transmission and resynchronises the pending count with reality.
* **In:** the UART transmit ring, `txCharActive $E5BB`.
* **Reads:** `uartTxCount $E414`, `uartTxReadIndex $E415`, `uartTxRing $E417..$E42C`, `$E5BB`.
* **Writes:** `$E414` to 0, `$E415` walked down to match, `uartPendingCount $E0A5` = `$E5BB`
(1 if a character is still being shifted out, 0 if not).
* **Out:** `A` = the value written to `$E0A5`, `X` = clobbered, `C = 1` (left set by the final empty
`popUartTxRing`), N/Z from the `LDA txCharActive`.
* **Callers:** JSR `$E791` (`enterTerminalMode`), JSR `$E80F` (`restartByteSync`); self-loop branch at
`$E537`.
* **6551 note:** the "1 if a character is still shifting" rule exists so the packet layer's
`$E0A5`-reaches-zero test stays honest across a flush. With a 6551 the equivalent is "1 if a byte
has been written to the data register whose TDRE has not come back yet, else 0".
### `$E540 configureUserPortLines` - range `$E540-$E574`
Programs CIA2 for the user-port serial link and asserts the modem control lines.
* **In:** `isOriginateMode $E011`, `baudIndex $E055`, `userPortDdrbValue $E04D` (`$26`),
`userPortIdleTable $E04F` (both entries `$26`), `serialShiftPatternTable $E051` (`$27 $2F $3F $37`).
* **Writes, in order:** `CIA2_TA_HI = 0`, `CIA2_TA_LO = 3` (latch `$0003`), `CIA2_CRA = $51`
(timer A continuous, serial port in output mode), `CIA2_SDR` = pattern selected by
`(3 AND isOriginateMode) EOR baudIndex`, `CIA2_DDRB = $26`, `CIA2_PRB = $26` (RTS, DTR, PB5 all
asserted), `CIA2_PRA` |= `$04` (TXD to the idle mark level, read-modify-write so the VIC bank bits
survive).
* **Out:** `A` = the value written to `CIA2_PRA`, `X = isOriginateMode`, `Y` unchanged, N/Z from the
`ORA CIA2_PRA`, C unchanged.
* **Callers:** JSR `$E2D5` (`openCommLink`, on every open including a re-open), JSR `$E387` (the
C=+A / C=+O answer/originate hot keys).
* **Its tail `$E56B-$E573` is a separate entry:** `setCiaNmiMask` ends with `JMP $E56B`. Any
replacement must keep a valid three-instruction "raise TXD / RTS" tail at `$E56B` or repoint that
JMP.
* **6551 equivalent:** program the control register (`$10 OR rateBits`, 8N1, internal clock) and the
command register (`$09` = DTR on, receiver IRQ enabled, RTS low, no transmit IRQ; or `$0D` when the
transmit interrupt is wanted). The `CIA2_SDR` write, the timer A latch and the `CIA2_PRA` bit are
all dead.
### `$E574 dropDtrLine` - range `$E574-$E57B`
* **In:** `userPortDtrDropped $E04E` = `$22`.
* **Writes:** `CIA2_PRB = $22` - PB2 (DTR) low, PB1 (RTS) and PB5 still high. Dropping DTR is what
makes a Hayes modem let go of the line.
* **Out:** `A = $22` (N=0, Z=0), X/Y/C unchanged.
* **Callers:** JSR `$E38D` (`hangUpModem`, itself reached from `$E2A2` for `$E003` X>=3 and from the
C=+P hot key at `$E376`).
* **6551 equivalent:** write the command register with bit 0 = 0. Note that on the 6551 clearing bit 0
also disables the receiver and all ACIA interrupts, which is the desired behaviour here.
### `$E57B installCommNmiVector` - range `$E57B-$E586`
* **In:** none.
* **Writes:** `$FFFA = $85`, `$FFFB = $E6` - i.e. the RAM NMI vector points at `commNmiHandler $E685`.
The KERNAL ROM is banked out (`$01 = $35`), so `$FFFA/$FFFB` really is RAM.
* **Out:** `A = $85`, `X = $E6`, N=1 Z=0 (from `LDX #$E6`), C unchanged.
* **Callers:** JSR `$E2CC` (`openCommLink`, only on a cold open when `isLinkActive` was 0).
* **6551 note:** the constants `$85`/`$E6` are the only thing that has to change if the handler moves,
but it must not move: nothing else does, and keeping it at `$E685` keeps this routine two bytes.
### `$E586 restartUart` - range `$E586-$E593` (falls into `setCiaNmiMask`)
(Re)starts the software UART.
* **In:** none.
* **Calls:** `clearUartState $E5B6`, which zeroes `$E5BB-$E5C0` and returns here normally.
* **Writes:** `$E5BB-$E5C0 = 0`; `uartRestartRequest $E042 = $01` ("re-evaluate the connection", as
opposed to `$FF` which forces phase 0); `carrierSampleTimer $E045 = $92`.
* **Falls through into `setCiaNmiMask` with `A = $92`**, which arms the CIA2 FLAG line (the RXD
start-bit edge) and timer B, and as a side effect writes `$00` to both `nmiSuspendRequest $E039` and
`nmiSuspendAck $E03A`.
* **Out:** as `setCiaNmiMask` - `A` = the `CIA2_PRA` value, `X = 0`.
* **Callers:** JSR `$E2CF` (`openCommLink`), JSR `$E5E1` (`serviceCarrierAndSuspendRequest`, the resume
half of the disk-load handshake).
* **Load-bearing side effects for the layer above:** `$E042 = 1` and `$E039/$E03A = 0`. A 6551 build
must still produce exactly those, because `runModemStateMachine $E758` consumes `$E042` and the
game's `resumeCommModule $0F7B` spins until `$E03A` bit 7 is clear.
### `$E593 setCiaNmiMask` - range `$E593-$E5B6`
The single point where the module's interrupt sources are armed or disarmed.
* **In:** `A` = a CIA2 ICR command byte. Only three values are ever passed: `$92` (set FLAG + timer B),
`$83` (set timer A + timer B - written by the NMI into the shadow, not through here) and `$7F`
(clear every source).
* **Reads:** `CIA2_ICR` (the `BIT` at `$E596` clears any pending flags), `ciaIcrShadow $E033`.
* **Writes:** `ciaIcrShadow $E033 = A`; `CIA2_ICR = A`; `CIA2_CRA = 0` and `CIA2_CRB = 0` (stopping
both timers); then `nmiSuspendRequest $E039` and `nmiSuspendAck $E03A` both set to
`(A EOR $FF) AND $80` - which is `$00` for `$92`/`$83` and `$80` for `$7F`.
* **Retry loop:** `CMP ciaIcrShadow / BCC $E593`. If the NMI overwrote the shadow with a *larger* mask
while we were storing (`$83` or `$92` over a `$7F`), the whole routine repeats. This is a real race,
not decoration.
* **Out:** exits through `JMP $E56B`, so `A` = the value written to `CIA2_PRA`, `X = 0`, C set by the
final `AND #$80`, N/Z from the `ORA CIA2_PRA`.
* **Callers:** JSR `$E5CA` (`stopCommNmi`), fall-through from `restartUart $E590`, self-branch `$E5A7`.
* **6551 equivalent:** "enable" writes the command register with the receive interrupt on; "disable"
writes it with bit 1 set (receiver IRQ disabled) and bits 3-2 = 00. The `$E039`/`$E03A` and `$E033`
writes must be kept exactly as they are - see section 5 for why `$E033` matters even to a 6551 build.
### `$E5B6 clearUartState` - range `$E5B6-$E5C1` (code `$E5B6-$E5BA`, data `$E5BB-$E5C0`)
`LDX #$06 / JSR clearInlineVarBlock`, followed by the six-byte block it clears. `clearInlineVarBlock`
eats its own return address and plants it in a self-modified `STA abs,X`, so the `RTS` returns to
`clearUartState`'s caller: from the caller's point of view this behaves as an ordinary subroutine.
* **In:** none.
* **Writes:** `$E5BB-$E5C0 = 0`.
* **Out:** `A = 0`, `X = 0`, Z=1 N=0, C unchanged; control returns to `restartUart`.
* **Callers:** JSR `$E586` (`restartUart`) only.
* **6551 note:** the six bytes are pure bit-bang state (section 4). A 6551 build still wants a routine
here that zeroes whatever it keeps in that space, because `restartUart` calls it and `flushUartTxRing`
reads `$E5BB`.
### `$E5C1 suspendUartIfRunning` - range `$E5C1-$E5C3`
Two bytes: `BCS $E5D5`. It sits between the UART state block and `stopCommNmi` so the suspend path can
fall into the stop path.
* **In:** `C` = bit 7 of `nmiSuspendAck`, shifted out by the `ASL` at `$E5D9`.
* **Out:** if C=1 (already suspended) it branches to the `RTS` at `$E5D5` inside `stopCommNmi` and
nothing happens; if C=0 it falls into `stopCommNmi`.
* **Callers:** branch `BMI $E5DD` in `serviceCarrierAndSuspendRequest`. Never called with JSR.
### `$E5C3 stopCommNmi` - range `$E5C3-$E5D6`
Tears the UART down.
* **In:** `txCharActive $E5BB`.
* **Writes:** `uartRestartRequest $E042 = $FF` (forces `connectionPhase` back to 0 when the state
machine next runs); calls `setCiaNmiMask` with `$7F`, so `CIA2_ICR` is cleared, both timers stopped,
`$E033 = $7F` and `$E039 = $E03A = $80`; then, if a character was half-way out,
`DEC uartPendingCount $E0A5` because that character will never finish.
* **Out:** `A` = 0 or `$E5BB`'s value, `X = 0`, RTS at `$E5D5`.
* **Callers:** JSR `$E2AB` (`commLinkControlDispatch`, reached for `$E003` X=2 and, after the hang-up
string has drained, X>=3), fall-through from `suspendUartIfRunning`.
* **Ordering that matters:** `$E042 = $FF` is written *before* the mask is cleared, and the
`uartPendingCount` fix-up happens *after*. A 6551 build must keep all three effects, because the
disk-load suspend goes through this same path and the game spins on `$E03A`.
### `$E607 startNextTxChar` - body `$E607-$E654`, exits at the `RTS $E662`
Starts shifting the next character out. It is reached two ways: from the NMI (`JSR $E673`) and by
**fall-through from `$E604`**, the tail of the carrier sampler - so it is retried once per frame from
jump-table entry `$E00C` as well as from the NMI. That frame-rate retry is not optional: when the
transmitter goes idle, timer B stops, no more transmit NMIs arrive, and the frame tick is the only
thing that can start the machine again.
Guards, in order:
1. `LDA CIA2_CRB / ORA txCharActive / LSR` - give up if timer B's START bit is set or a character is
already in flight.
2. If `connectionPhase $E040 == 1` (modem command / terminal mode), skip guards 3 and 4 entirely:
terminal-mode characters go out unpaced and without carrier.
3. `DEC txPaceCounter $E044`; give up unless it reached 0. On the frame it reaches 0 it is reloaded
with 1 so the attempt repeats next frame.
4. `LDA linkStatusSample $E03D / AND linkStatus $E03C / ASL` - give up unless bit 6 (carrier) is set in
both. The module refuses to transmit into a dead line.
5. `LDA uartPendingCount $E0A5` - give up if zero.
6. `JSR popUartTxRing`; if the ring was empty although the count said otherwise, force `$E0A5 = 0` and
give up (this is the self-healing path for a lost decrement).
Then: `INC txCharActive`, `STA txShiftRegister`, `txBitCountReload = 9`,
`txPaceCounter = txPaceReload $E058`, program timer B with a deliberately short first interval
(`A = $01` as the high byte, `bitPeriodLo` as the low byte) and `CIA2_CRB = $11` (force load + start,
continuous), then **fall through into `setBitPeriodFull`** so every following underflow is a whole bit
time. The `RTS` at `$E662` is the common exit for all six give-up paths and for the success path.
* **Out:** `A = $11` on the success path, `A`/flags meaningless on the give-up paths (every one of them
branches to `$E662`). `X = 2` on success.
* **Callers:** JSR `$E673` (`nmiStartNextChar`), fall-through from `$E606`.
* **6551 equivalent:** most of this evaporates. What must survive is the *policy*: do not transmit
unless `$E0A5` is non-zero and, outside phase 1, only with carrier. Guard 4 is the reason a
null-modem player needs the C=+C carrier override (`$E04A` bit 6) - or, with a 6551, a real DCD from
the other end's DTR. Guard 3 (frame pacing) becomes pointless at 9600+ and can be reduced to a
no-op, but the byte `$E044` must still tolerate `beginByteSyncPhase` forcing bit 1 into it at
`$E820-$E825`.
### `$E654 setBitPeriodFull` - range `$E654-$E657`
Three bytes: `LDA bitPeriodHi $E057`, then falls into `setBitPeriod`.
* **In:** `X` = 0 (timer A) or 2 (timer B).
* **Callers:** JSR `$E6EC` (the NMI, immediately after the half-bit start-bit delay), fall-through from
`startNextTxChar $E653`.
### `$E657 setBitPeriod` - range `$E657-$E663`
* **In:** `A` = high byte of the interval, `X` = 0 or 2, `bitPeriodLo $E056`.
* **Writes:** `CIA2_TA_HI,X` then `CIA2_TA_LO,X`, i.e. `$DD05`/`$DD04` for X=0 and `$DD07`/`$DD06` for
X=2. The low byte is always the full `bitPeriodLo`; only the high byte varies (the caller passes
`$01` for the short first transmit interval and `bitPeriodHi >> 1` for the half-bit start-bit delay).
* **Out:** `A = $11` - the "force load + start" byte the caller stores into `CIA2_CRA` or `CIA2_CRB`.
`X` unchanged, N=0 Z=0.
* **Callers:** JSR `$E64E` (`startNextTxChar`), JSR `$E6E6` (the NMI's start-bit path), fall-through
from `setBitPeriodFull`.
* **6551 note:** entirely dead. Baud is a control-register write, once, at open time.
### `$E663 nmiStartNextChar` - range `$E663-$E679`
The timer B path of the NMI once `txBitCounter` has gone negative.
* **In:** `txCharActive $E5BB`, `txBitCountReload $E5BC`, `CIA2_PRA`.
* **If a character is active:** reload `txBitCounter $E5BD` from `$E5BC` (9), then read `CIA2_PRA` and
branch to `$E6C3` to clear bit 2 - the start bit. The `BNE $E671` is an unconditional branch in
practice because `CIA2_PRA` can never read 0 (the VIC bank bits are in it).
* **If not:** `JSR startNextTxChar`, then `CLC / BCC $E6C8` into the receive half.
* **Out:** falls into the shared NMI tail; nothing returns to a caller.
* **Callers:** branch `BMI $E6A7` in the NMI only.
### `$E685 commNmiHandler` - range `$E685-$E733`, plus the "not mine" exit at `$E679-$E685`
The module's NMI handler. See section 5 for the dispatch and chaining rules, which the replacement
must copy exactly. Structure:
| Address | What happens |
|---|---|
| `$E685-$E687` | `PHA / TXA / PHA` - A then X saved. **Y is never saved and never used.** |
| `$E688-$E68C` | `CIA2_ICR = $7F` - mask every CIA2 source while we work |
| `$E68D-$E68F` | sample `CIA2_TB_HI` |
| `$E690-$E692` | read `CIA2_ICR` (which also clears its flags) |
| `$E693-$E694` | `BPL $E679` - **bit 7 clear means this NMI was not CIA2's** |
| `$E695-$E69B` | if timer B's high byte fell between the two reads, OR in the timer B flag by hand |
| `$E69C-$E69E` | `nmiMergeIcrFlags`: OR in anything that arrived meanwhile |
| `$E69F-$E6C7` | transmit half: `DEC txBitCounter`; negative -> `nmiStartNextChar`; zero -> stop timer B with `CIA2_CRB = $09`, `DEC uartPendingCount $E0A5`, clear `txCharActive`; then `SEC / ROR txShiftRegister` and drive PA2 with the bit |
| `$E6C8-$E6D7` | `CIA2_ICR = $82` (re-enable timer B); recover the flag byte, `AND ciaIcrShadow`, `LSR` -> C = timer A, bit 3 = the FLAG line |
| `$E6D8-$E6F0` | FLAG line = an RXD start-bit edge: `ciaIcrShadow = $83`, `rxBitCounter = $83` (any negative value), arm timer A at half a bit time, start it, then reload the latch with a whole bit time |
| `$E6F1-$E6FE` | timer A tick: sample `CIA2_PRB` bit 0, `DEC rxBitCounter`; negative = start bit, zero = stop bit, else `ROR rxShiftRegister` |
| `$E6FF-$E708` | shared exit: `CIA2_ICR = ciaIcrShadow`, `PLA / TAX / PLA / RTI` |
| `$E709-$E711` | start bit must be low; if high -> `reportLinkError`. Otherwise `rxBitCounter = 9` |
| `$E712-$E723` | stop bit must be high; if low -> `reportLinkError`. Otherwise clear the stall counter `$EB00` and `carrierSampleTimer $E045`, then `pushUartRxRing` |
| `$E724-$E726` | `JSR reportLinkError` - framing error, bad stop bit, or receive-ring overflow (C=1 from `pushUartRxRing`) |
| `$E727-$E732` | stop timer A (`CIA2_CRA = 0`), `ciaIcrShadow = $92` (back to waiting for a start bit), branch to the shared exit |
* **Reads:** `CIA2_ICR`, `CIA2_TB_HI`, `CIA2_PRA`, `CIA2_PRB`, `ciaIcrShadow $E033`, `$E5BB-$E5C0`.
* **Writes:** `CIA2_ICR`, `CIA2_CRA`, `CIA2_CRB`, `CIA2_PRA` bit 2, `$E5BB-$E5C0`, `uartPendingCount
$E0A5`, `ciaIcrShadow $E033`, `carrierSampleTimer $E045`, the stall counter `$EB00`, and the UART
receive ring `$E42D/$E42F/$E430..$E443` through `pushUartRxRing`.
* **Callers:** the CPU NMI vector, once `installCommNmiVector` has written `$FFFA/$FFFB`.
* **A correction to the shipped comment:** the listing calls `CIA2_CRB = $09` at `$E6AD` "stop timer B".
`$09` is `START | RUNMODE(one-shot)`: it runs one more bit period - the stop bit - and then the CIA
clears the START bit itself. That final underflow is the interrupt that drives `txBitCounter`
negative and reaches `nmiStartNextChar`, which is how back-to-back characters are chained without
waiting for the frame tick.
### `$E69C nmiMergeIcrFlags` - range `$E69C-$E69F`
Three bytes: `ORA CIA2_ICR`. The tail of the timer B race workaround.
* **In:** `A` = the interrupt flags read so far.
* **Out:** `A` = those flags plus anything that arrived between the handler's two reads.
* **Callers:** fall-through from `$E69A` in this build. (`XREF.txt` shows a `call:E444` for this
address, but that entry belongs to `trainerAiE000`, a different overlay at the same address.)
### `$E73C queueByteForTransmit` - range `$E73C-$E743`
The single door for outbound bytes.
* **In:** `A` = byte to transmit.
* **Calls:** `pushUartTxRing $E51B`, which silently drops the byte when the 22-byte ring already holds
22 and returns C=1 in that case.
* **Writes:** `uartTxRing $E417`, `uartTxCount $E414`, `uartTxWriteIndex $E416`; then
`INC uartPendingCount $E0A5` - **unconditionally, even when the push overflowed.** The design relies
on `startNextTxChar $E632` and `flushUartTxRing $E53C` to resynchronise `$E0A5` when it drifts.
* **Out:** `A` preserved (callers such as `sendNextModemCommandChar $E7D5` test what they just sent),
`X` clobbered, `Y` unchanged, `C` = 0 if the byte was stored and 1 if it was dropped (no caller looks).
* **Callers:** JSR `$E7D2` (`sendNextModemCommandChar`), `$E81D` (`queueSyncByte`), `$E87C`
(`queueRawSyncByte`), `$E92A` (`transmitPacketLoop` inside `sendPacket`).
* **6551 note:** `A` preserved and `X` clobbered are both relied on - `transmitPacketLoop` parks its
index in `$EAFE` across the call precisely because X does not survive.
### Addendum: hardware-touching code outside the requested list
Three more places touch the hardware and must be reimplemented or neutralised, even though they were
not in the list:
* **`$E5D6 serviceCarrierAndSuspendRequest` (range `$E5D6-$E5FF`) and `$E5FF rearmCarrierTimer`
(range `$E5FF-$E607`).** This is where carrier detect is actually read:
`EOR CIA2_PRB / AND #$10 / ADC #$F0` at `$E5F1-$E5F7` turns PB4 into A=`$F0` C=0 when DCD is low
(carrier) and A=`$00` C=1 when it is high, then `SEC / ROR` produces `$F8` (carrier) or `$80` (no
carrier) in `linkStatusSample $E03D`. Bit 6 is the carrier flag, bit 7 the "data is flowing" flag.
`INC carrierSampleTimer $E045 / BPL` keeps bit 7 only while a character has arrived within the last
~127 frames; otherwise `rearmCarrierTimer` re-parks the sample in `$E045` (so the timer stays pinned
negative) and strips bit 7. It also runs the suspend handshake at `$E5D6-$E5E3` and falls straight
into `startNextTxChar`. A 6551 build reads DCD from status bit 5 instead of `CIA2_PRB` bit 4 and
must produce the identical `$80`/`$F8` sample values, because `pollCarrierState $E3CB` debounces them
against `linkStatus` over 240 frames and the game reads both bytes.
* **`$E327-$E331`, inside `readKeyAndHandleModemHotkeys`.** The C=+H hot key does
`LDA dtrToggleMask $E04C / EOR CIA2_PRB / STA CIA2_PRB`, toggling PB2 (DTR) and PB5 directly. On a
6551 this becomes a toggle of command-register bit 0.
* **The four raw ring primitives `$E4D2 popUartRxRing`, `$E4EA popUartTxRing`, `$E502 pushUartRxRing`,
`$E51B pushUartTxRing`.** Not hardware, but owned by this layer and called from the NMI. Their
contracts (C=1 = empty / overflow, A = byte, X clobbered) are relied on by `receiveByteTrackZeros
$E743`, `beginByteSyncPhase $E80A`, `receivePacket $E9E9` and `runModemStateMachine $E799`.
## 4. Module RAM owned by the UART layer
`Frozen?` means the byte is inside `$E01D-$E047` and so its **disk value** must not change, whatever
its runtime meaning becomes.
| Address | Name | Meaning in the stock driver | Frozen? | Needed by a 6551 build |
|---|---|---|---|---|
| `$E033` | `ciaIcrShadow` | The CIA2 ICR command byte the module currently wants: `$92` idle, `$83` while receiving a character, `$7F` when suspended. Written at `$E593`, `$E6DA`, `$E72E`; read at `$E5A4`, `$E679`, `$E6CE`, `$E6FF` - **and by the main program at `$5889`, `$58A8`, `$58CD`**, which stores it back into `CIA2_ICR` after banking the I/O area out | yes (disk value `$90`) | **Keep, and pin it to `$7F` at run time.** It is the only byte of this layer the game outside the module reads. If it holds `$92`, the game will re-enable the CIA2 FLAG NMI after every film-buffer access |
| `$E039` | `nmiSuspendRequest` | `$C0` from the game before a disk load, `0` after; also written `$00`/`$80` by `setCiaNmiMask` | yes | Keep, unchanged semantics |
| `$E03A` | `nmiSuspendAck` | `$80` when the module's interrupt really is off, `0` when running. The game **spins** on this at `$0F6A` and `$0F7B` | yes | Keep, unchanged semantics |
| `$E03C` | `linkStatus` | Debounced: bit 6 carrier, bit 7 "characters still arriving". Read by the game at `$1B4B` (waits for bit 7 to clear before hanging up), `$1BB6`, `$56B1`, `$65BC` | yes | Keep, unchanged semantics |
| `$E03D` | `linkStatusSample` | The raw per-frame sample: `$80` no carrier, `$F8` carrier, bit 7 = data flowing. Read by the game at `$1BAB` | yes | Keep; source DCD from ACIA status bit 5 |
| `$E042` | `uartRestartRequest` | `1` = re-evaluate the connection, `$FF` = force `connectionPhase` to 0 | yes | Keep, unchanged semantics |
| `$E043` | `carrierDebounceCount` | Consecutive frames the sample disagreed with `linkStatus`; 240 accepts the change | yes | Keep (it lives in `pollCarrierState`, above the seam) |
| `$E044` | `txPaceCounter` | Frames between transmitted characters. Decremented in `startNextTxChar $E617`, reloaded at `$E61C`/`$E647`; `beginByteSyncPhase $E820-$E825` forces bit 1 on to space the sync bytes | yes | **Effectively dead.** The byte must survive because `$E820` writes it, but the pacing gate can become a no-op |
| `$E045` | `carrierSampleTimer` | Zeroed by the NMI (`$E719`) on every good character; `INC`ed once per frame; while it is negative the link counts as quiet. This is what produces bit 7 of `linkStatus` | yes | **Keep.** Same job, driven by "a byte arrived" instead of "a stop bit was good" |
| `$E047` | `linkErrorCount` | Bumped by `countLinkError $E403` from the NMI receiver; non-zero makes `runLinkStateMachine $E7FD` restart the byte sync | yes | Keep. Feed it from the ACIA's framing/parity/overrun status bits |
| `$E04A` | `carrierOverrideFlags` | Bit 6 = "pretend carrier is present" (the C=+C hot key), for a direct cable with no DCD | no | Keep - it is the only way to play over a three-wire null-modem lead |
| `$E04B` | `carrierOverrideToggleMask` | `$40`, the bit C=+C toggles | no | Keep |
| `$E04C` | `dtrToggleMask` | `$24` = the `CIA2_PRB` bits C=+H toggles (PB2 DTR + PB5) | no | Repurpose as the command-register DTR bit |
| `$E04D` | `userPortDdrbValue` | `$26` - DDRB pattern | no | Dead |
| `$E04E` | `userPortDtrDropped` | `$22` - PRB with DTR low | no | Repurpose as the "DTR dropped" command byte |
| `$E04F-$E050` | `userPortIdleTable` | Two entries, both `$26` - PRB idle pattern by answer/originate | no | Dead (or repurpose as the idle command byte) |
| `$E051-$E054` | `serialShiftPatternTable` | `$27 $2F $3F $37` - written to `CIA2_SDR`; purpose never established | no | Dead |
| `$E055` | `baudIndex` | 0 = 300, 3 = 1200; also the stride into `baudParameterTable` | no | Keep as the index; the table it selects changes meaning |
| `$E056` | `bitPeriodLo` | Low byte of the CIA timer latch for one bit time | no | **Dead as a timer latch.** Natural replacement: the 6551 control-register byte |
| `$E057` | `bitPeriodHi` | High byte; `$0D50` = 3408 cycles = 300.1 baud, `$0353` = 851 = 1201.8 baud on NTSC | no | Dead |
| `$E058` | `txPaceReload` | Frames between transmitted characters: 1 at 300, 2 at 1200 | no | Dead (see `$E044`) |
| `$E059-$E05E` | `baudParameterTable` | Two 3-byte entries `{periodLo, periodHi, pacing}` | no | Repurpose as `{controlByte, spare, spare}` per speed |
| `$E0A4` | `frameInFlightFlag` | The `$E0A5` value the frame layer saw when it queued a frame; `serviceLinkTick $E76E` compares the two | no (protocol block) | Keep, untouched - it is above the seam |
| **`$E0A5`** | `uartPendingCount` | **Characters queued for the UART that have not finished transmitting.** `INC` at `$E73F`; `DEC` at `$E6B0` (character finished) and `$E5D2` (character abandoned); forced to 0 at `$E634`; set to `txCharActive` at `$E53C` | no (protocol block) | **Keep - this is the single most important byte of the whole seam.** Everything above waits on it: `$E1EC`, `$E26E`, `$E628`, `$E76B`, `$E82D`, `$E860` |
| `$E0A6` | `packetInFlightCount` | The byte count of the frame the packet layer handed over | no | Keep untouched. **Structural constraint:** `sendPacketFsm` addresses it as `uartPendingCount,X` with `X = inFlightSlotIndex $E0A8 = 1` at `$E1EC`, `$E265` and `$E26E`, so `$E0A5` and `$E0A6` must stay adjacent, in that order. (`XREF.txt` misses the write at `$E265` because it is indexed, and the listing's claim that `$E0A6` is only ever written with 0 is wrong for the same reason.) |
| `$E414` | `uartTxCount` | Bytes in the 22-byte transmit ring | no | Keep - the ring is the natural 6551 output queue |
| `$E415`/`$E416` | `uartTxReadIndex` / `uartTxWriteIndex` | Descending indices | no | Keep |
| `$E417-$E42C` | `uartTxRing` | 22-byte descending circular buffer | no | Keep. It must hold a whole packet: `sendPacket $E922-$E934` queues `packetLength+1` bytes in one burst, gated only by `$E860`'s "fewer than 2 pending" test |
| `$E42D` | `uartRxCount` | Bytes in the 20-byte receive ring | no | Keep |
| `$E42E`/`$E42F` | `uartRxReadIndex` / `uartRxWriteIndex` | Descending indices | no | Keep |
| `$E430-$E443` | `uartRxRing` | 20-byte descending circular buffer, filled only by the NMI at `$E71F` | no | Keep. **20 bytes is the whole receive budget between frame ticks** - see the hazards in section 7 |
| `$E5BB` | `txCharActive` | Non-zero while a character is being shifted out. Read at `$E539`, `$E5CD`, `$E60A`, `$E663`; written at `$E639`, `$E6B5` | no | **Keep**, redefined as "a byte is sitting in the ACIA transmit register waiting for TDRE". `flushUartTxRing` and `stopCommNmi` both depend on it |
| `$E5BC` | `txBitCountReload` | 9 = 8 data bits plus the stop bit | no | Dead |
| `$E5BD` | `txBitCounter` | Bits left in the character being sent | no | Dead |
| `$E5BE` | `txShiftRegister` | The character being shifted out, LSB first | no | Dead |
| `$E5BF` | `rxBitCounter` | Negative = awaiting the start-bit sample, 9..1 = data bits, 0 = stop bit | no | Dead |
| `$E5C0` | `rxShiftRegister` | The character being shifted in | no | Dead |
| `$EB00` | stall counter | Zeroed by the NMI at `$E716` on every good character; `noteIdlePoll $E997` counts dead service ticks and forces a resync at 16 | no (ARQ block) | **Keep the NMI's zeroing of it.** It is the only other thing the receive path owes the layer above |
Summary for a 6551 build: `$E5BC-$E5C0` (five of the six software-UART bytes), `$E056`, `$E057`,
`$E058`, `$E04D`, `$E04F-$E054` and the pacing behaviour of `$E044` all become dead. Everything else
in the table stays, and `$E0A5`, `$E5BB`, `$E045`, `$E03C`, `$E03D`, `$E039`, `$E03A`, `$E033`, `$E047`
and `$EB00` must behave exactly as they do now.
## 5. How the NMI decides an interrupt is not its own, and how it chains
This is the part the replacement has to copy instruction for instruction in spirit, because the C64's
`/NMI` line is a wired-OR of the RESTORE key, CIA2's `/IRQ` and the expansion port's `/NMI`, and the
game installs a handler of its own behind this module.
**The test.** At `$E690` the handler does `LDA CIA2_ICR`. On a 6526, bit 7 of the interrupt data
register is set when any *enabled* source has fired since the register was last read, and reading it
clears all the flags. `BPL $E679` at `$E693` therefore means exactly "no enabled CIA2 source fired -
this NMI belongs to someone else". Note the ordering: `CIA2_ICR` is written with `$7F` first
(`$E688-$E68C`) to mask every source while the handler works, then `CIA2_TB_HI` is sampled, then the
register is read. The mask write does not clear pending flags; only the read does.
**The chain.** `$E679-$E684`, which sits immediately *before* the handler entry:
```
E679 LDA ciaIcrShadow ; $E033 - the mask this module wants
E67C STA CIA2_ICR ; undo the $7F we wrote at $E68A
E67F PLA
E680 TAX ; restore X
E681 PLA ; restore A
E682 JMP (nmiChainVector) ; $E031/$E032
```
Four properties the replacement must preserve:
1. **A and X are restored before the chain, and Y is never touched at all.** The handler saves A then
X (`PHA / TXA / PHA` at `$E685-$E687`) and both exits pop them in the reverse order. Y is neither
saved nor used anywhere in the handler, so the chained handler inherits the interrupted Y.
2. **The chain is a `JMP (abs)`, not a `JSR` and not an `RTI`.** The CPU's NMI stack frame (P, PCH,
PCL) is still intact and untouched, so the chained handler owns it and ends with its own `RTI`.
3. **The vector is `$E031/$E032`, inside the frozen block.** On disk it is `$E010`, a bare `RTI`
opcode inside the jump table's constant pool. `initInterruptVectors` at `$0BF3`/`$0BFB` patches it
to `$1298`, which is `irqRti`, a bare `RTI` in the main program. A replacement must read the vector
from `$E031/$E032` at run time and must not assume `$1298`.
4. **The module's own interrupt mask is restored on both exits** - `$E67C` on the chain path and
`$E702` on the normal path - from `ciaIcrShadow $E033`.
**The 6551 version of the same test.** Bit 7 of the ACIA status register at `base+1` is the ACIA's
IRQ flag, and reading the status register clears it - the direct analogue of reading `CIA2_ICR`. So:
```
PHA
TXA
PHA
LDA base+1 ; status; reading it clears the ACIA IRQ flag
BPL notMine ; bit 7 clear - not the ACIA's interrupt
... service TDRE (bit 4), RDRF (bit 3), and the error bits 2/1/0 ...
LDA $E033
STA CIA2_ICR ; keep CIA2 masked exactly as the stock code does
PLA
TAX
PLA
RTI
notMine:
LDA $E033
STA CIA2_ICR
PLA
TAX
PLA
JMP ($E031)
```
with two additions that the stock design did not need:
* **Do not write `$7F` to `CIA2_ICR` on entry and then restore `$E033`** unless `$E033` is itself `$7F`.
Simplest correct policy: set `$E033 = $7F` once at open time and never change it; then both the
handler and the main program's `$588C`/`$58AB`/`$58D0` restores keep CIA2 permanently silent, and the
only NMI source left is the cartridge.
* **Bank the I/O area in yourself.** `readFilmByte $5874`, `writeFilmByte $5893` and `copyPageUnderIo
$58B2` run with `SEI` and `$01 = $34`, which removes `$D000-$DFFF` - including the SwiftLink - from
the address space. `SEI` does not mask NMI, and writing `$7F` to `CIA2_ICR` (which is what those
routines do to silence the stock driver) has no effect on an expansion-port NMI. `copyPageUnderIo`
holds that state for a 256-byte copy loop, roughly 2600 cycles, which at 38400 baud is about ten
character times. The replacement handler must therefore save `$01`, force it to `$35`, do its work,
and restore `$01` before either exit. This is not optional: `$EE4A loadFilmStartSnapshot`, in the
frozen tail of this very module, calls `copyPageUnderIo` twice.
**One thing the stock handler does that is easy to miss:** on every successfully received character it
zeroes both `carrierSampleTimer $E045` (`$E719`) and the stall counter `$EB00` (`$E716`). The first
feeds bit 7 of `linkStatus`, which the game waits on at `$1B4B`; the second is what stops
`noteIdlePoll $E997` from tearing the connection down after sixteen dead service ticks. A 6551
receive path that forgets either one will produce a link that hangs up on itself.
## 6. The SwiftLink baud rate doubling - verified
**Claim under test:** SwiftLink fits a 3.6864 MHz crystal, twice what the 6551 expects, so every rate
in the control register's low nibble comes out doubled.
**Conclusion: the claim is correct**, and the mapping given in the task brief is right.
Second source: cc65's own SwiftLink serial driver, `libsrc/c64/ser/c64-swlink.s`, whose `BaudTable`
maps the requested rate to the control-register value the hardware needs:
| Requested | cc65 control value | 6551 nominal rate for that value | doubled |
|---|---|---|---|
| 150 | `$02` | 75 | 150 |
| 300 | `$05` | 150 | 300 |
| 600 | `$06` | 300 | 600 |
| 1200 | `$07` | 600 | 1200 |
| 2400 | `$08` | 1200 | 2400 |
| 3600 | `$09` | 1800 | 3600 |
| 4800 | `$0A` | 2400 | 4800 |
| 7200 | `$0B` | 3600 | 7200 |
| 9600 | `$0C` | 4800 | 9600 |
| 19200 | `$0E` | 9600 | 19200 |
| 38400 | `$0F` | 19200 | 38400 |
Every row is exactly 2x the nominal 6551 rate, with no exceptions, which is only consistent with a
doubled reference clock. (`$0D`, nominal 7200, would give 14400; cc65 has no constant for it and
leaves it unused - that is the only gap, and it is a gap in cc65's table, not in the hardware.)
Corroborating hardware documentation: the GLINK232 and Turbo232 cartridges both state that they use a
3.6864 MHz oscillator, "double the standard for a 6551", specifically in order to match the SwiftLink
baud rate table.
So: **control byte = `$10 OR rateBits`** (bit 4 = internal baud rate generator, bits 6-5 = 00 = 8 data
bits, bit 7 = 0 = 1 stop bit), with `$05` = 300, `$07` = 1200, `$08` = 2400, `$0C` = 9600, `$0E` =
19200, `$0F` = 38400.
Two caveats worth carrying into the implementation:
* Some SwiftLinks were rebuilt with the standard 1.8432 MHz crystal (cc65's header calls these the
"hacked" SwiftLink). On those the table is the plain 6551 one and every rate is half of the above.
If the driver ends up with a user-visible speed setting, it is worth documenting which crystal the
numbers assume.
* The doubling is a property of the crystal, not of the chip, so it applies identically to a Turbo232
in SwiftLink-compatible mode. Turbo232's extra rates come from a separate register and are not
reachable through the control register's low nibble.
## 7. Hazards checklist for the 6551 implementation
> **Two notes from later measurement.** Hazard 2's "at 38400 baud a frame is about 200 character
> times" is wrong: at 3840 characters a second and 59.826 frames a second it is **64.2**
> (`highRateReport.md` section 1). The conclusion the hazard draws is unaffected - the 20-byte ring is
> still outrun - but the problem is three times smaller than the sentence implies, which is why 19200
> survives. Hazard 8's "same 8N1 framing" is now true only of the *data* bits: the driver adopts the
> port's own rate, stop bits and clock source, forcing eight data bits and nothing else, so two
> SwiftLink ends agree on whatever their control registers hold while a stock user-port peer can only
> ever be 8N1 (`portCompatReport.md` sections 5 to 8, `testReport.md` sections 11.4 and 11.5). Parity
> is not adopted, because it lives in the command register the driver rewrites from constants. The
> text below is left as it was written.
1. **`$E0A5` is the heartbeat.** Increment it in `queueByteForTransmit`, decrement it when the ACIA
takes the byte, and 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. Note that on a 6551, "TDRE came back" means the *holding* register is free, one
byte ahead of the wire - harmless here, since nothing above measures real wire time.
2. **The receive ring is 20 bytes, only the frame tick drains it, and 17 of those bytes can be spoken
for at once.** `awaitWholePacket $E9D9` does `CPX uartRxCount / BCS countStalledPoll`: it refuses to
copy a packet body until the *whole* payload plus its check byte is already sitting in the ring.
The length-code search at `$E9CC` yields a payload length of 1..16, so up to 17 bytes must be
resident simultaneously and only 3 bytes of the 20-byte ring are genuine headroom. At 300 baud a
frame holds at most one character; at 38400 baud a frame is about 200 character times. Either the
driver must throttle (drop RTS by writing the command register with bits 3-2 = 00) or the ring has
to be enlarged - and it cannot be enlarged in place without moving `$E444` onwards. Practical
answer: keep RTS handshaking honest, and only advertise rates the frame-tick drain rate can sustain.
3. **Overrun, framing and parity (status bits 2, 1, 0) must reach `reportLinkError $E3BC`.** That is
the only thing that sets `linkErrorCount $E047`, and that is the only thing that makes
`runLinkStateMachine $E7FD` restart the byte sync after a disk load has eaten part of a packet.
4. **The disk-load suspend really must stop the interrupt.** The fast loader at `$0804` is
cycle-exact. `$E039`/`$E03A` is the handshake; `stopCommNmi` is the routine; the 6551 equivalent is
a command-register write with receiver IRQ disabled. Bytes arriving during the load are lost, and
the ARQ layer is expected to recover - that is how the stock driver behaves too.
5. **`STA $DF59,Y` at `$E358` must go.** See `loadBaudParameters` above: its dummy read hits
`$DF58` = the data register on a `$DF00`-strapped SwiftLink.
6. **Do not touch `CIA2_PRA`.** PA0/PA1 are the VIC bank select and PA3-PA5 are the serial bus. The
stock code only ever read-modify-writes bit 2; a 6551 build should leave the register alone entirely
and make `$E56B` (the tail `setCiaNmiMask` jumps to) a plain RTS or a harmless equivalent.
7. **`$E033` must end up holding `$7F` at run time** while keeping its `$90` disk byte, so that the
main program's post-film-access restores at `$588C`, `$58AB` and `$58D0` do not re-arm a CIA2 NMI
source behind the driver's back.
8. **Wire compatibility.** Nothing above the seam changes, so the module stays byte-for-byte
compatible with the stock driver on the wire at the same line rate: same 8N1 framing, same `$00`/
`$FF` byte-sync handshake, same length/control/sequence/checksum frame, same retransmit rules. Two
machines can only talk if both ends use the same speed, because the speed is not negotiated (the
only automatic change is the `CONNECT 1200` sniff at `$E7A9`, which is a Hayes-modem behaviour).
9. **W65C51N replicas.** Real SwiftLinks use a 6551 whose TDRE bit works. Some modern rebuilds use a
WDC W65C51N, on which TDRE reads as permanently set; a driver that polls TDRE will overrun the
transmitter on those boards and needs a timed delay per character instead. Worth a note in the
final driver's comments even if the target is a genuine cartridge or VICE's emulation.