621 lines
44 KiB
Markdown
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.
|