# spec_link.md - what surrounds the UART in the $E000 opponent module

Reference for the SwiftLink replacement of the bit-banged user-port UART in
`disassembly/game/modemDriverE000.s`.  Everything here was read out of the listings in
`disassembly/game/` (mainly `modemDriverE000.s` and `mainProgram0800.s`); every address quoted was
checked against them.  Nothing in this document proposes changing the layers above the UART - the
point of the exercise is that they stay bit-for-bit the same so the two builds remain wire
compatible at the same line speed.

Read `docs/overview.md` section 9 first for the shape of the module.


## 0. The layer boundary

`swiftlink/checkAbi.py` freezes three regions: `$E000-$E017` (the jump table), `$E01D-$E047`
(variables the game reads and writes) and `$EC00-$EFFF` (utilities the map generator overlay calls).
The rest is ours, but only the following is genuinely hardware specific.

| Range | Routine(s) | Bytes | Keep the entry address? |
|---|---|---|---|
| `$E534-$E53F` | `flushUartTxRing` | 12 | yes - called from `$E791` and `$E80F` |
| `$E540-$E573` | `configureUserPortLines` | 52 | yes - called from `$E2D5` and `$E387` |
| `$E574-$E57A` | `dropDtrLine` | 7 | yes - called from `$E38D` |
| `$E57B-$E585` | `installCommNmiVector` | 11 | yes - called from `$E2CC` |
| `$E586-$E592` | `restartUart` | 13 | yes - called from `$E2CF` and `$E5E1` |
| `$E593-$E5B5` | `setCiaNmiMask` | 35 | internal (only `$E590` fall-through and `$E5CA`) |
| `$E5B6-$E5C0` | `clearUartState` + its 6 inline data bytes | 11 | internal |
| `$E5C1-$E5C2` | `suspendUartIfRunning` | 2 | internal (`$E5DD`) |
| `$E5C3-$E5D5` | `stopCommNmi` | 19 | yes - called from `$E2AB` |
| `$E5D6-$E5FE` | `serviceCarrierAndSuspendRequest` | 41 | yes - called from `$E3CB` |
| `$E5FF-$E606` | `rearmCarrierTimer` | 8 | internal |
| `$E607-$E653` | `startNextTxChar` | 77 | internal (`$E604` fall-through, `$E673`) |
| `$E654-$E662` | `setBitPeriodFull` / `setBitPeriod` | 15 | internal |
| `$E663-$E684` | `nmiStartNextChar` | 34 | internal |
| `$E685-$E732` | `commNmiHandler` | 174 | internal (reached through `$FFFA/$FFFB`) |
| `$E73C-$E742` | `queueByteForTransmit` | 7 | yes - called from `$E7D2`, `$E81D`, `$E87C`, `$E92A` |

That is `$E534-$E732` plus `$E73C-$E742`, 518 bytes in all.  Six 6551 registers replace all of it,
so there will be a lot of padding.

Everything else in the module is untouched by the hardware change:

* the four ring buffers and their push/pop routines, `$E40F-$E533` (the two "UART" rings are just
  memory; only their producer and consumer change);
* the three indirect link-I/O vectors `$E3B6/$E3B9/$E3BC` and the table at `$E3BF`;
* the frame/ARQ layer `$E909-$EAB2` and its state block `$EAB7-$EB02`;
* the packet/chat layer `$E111-$E2FF`.

Three data bytes are the interface between the baud table and the UART: `$E056`, `$E057` and `$E058`
(section 4).  Their meaning is private to the module, so the SwiftLink build may redefine them.

The single most important software contract across the boundary is **`uartPendingCount` at `$E0A5`**:
the number of characters handed to the transmitter that have not physically left the machine.  Four
places above the UART read it and will misbehave if it lies:

* `$E76B` `serviceLinkTick` compares it with `frameInFlightFlag` `$E0A4` to decide that a queued
  packet has drained and a new one may be built;
* `$E82D` `continueByteSync` waits for it to reach 0 before sending the next `$00` sync byte;
* `$E860` `servicePacketTransmit` refuses to start anything new while it is 2 or more;
* `$E539` `flushUartTxRing` resets it from `txCharActive` `$E5BB`.

`queueByteForTransmit` `$E73C` increments it; the stock NMI decrements it at `$E6B0` when a character
finishes and zeroes it at `$E634` if the ring unexpectedly runs dry.  A 6551 build must keep the same
accounting, counting a byte as pending until TDRE says the transmit register has taken it.

`$E044` `txPaceCounter` is a transmitter throttle owned by the UART layer, but `beginByteSyncPhase`
reaches into it: `$E820-$E825` ORs bit 1 in so that at least two transmit slots separate the sync
bytes.  The replacement must tolerate that write (and may simply ignore the byte).


## 1. The suspend/resume handshake ($E039 request, $E03A acknowledge)

The fast loader (`$0804` and the drive code) transfers bytes with a two-bit handshake on CIA2 port A
timed against `$D012`.  It cannot survive an NMI, so before any disk access the game parks the
opponent module and waits for it to confirm.

### Game side

`suspendCommModule` `$0F5F`:

```
0F5F  pha                 ; A must survive
0F60  lda $E03B           ; isLinkActive - no link (or the trainer) means nothing to suspend
0F63  beq $0F6F
0F65  lda #$C0            ; the request code
0F67  sta $E039           ; nmiSuspendRequest
0F6A  lda $E03A           ; nmiSuspendAck ...
0F6D  bpl $0F6A           ; ... spin until bit 7 is SET
0F6F  pla
0F70  rts
```

`resumeCommModule` `$0F71` is the mirror image: it stores `$00` into `$E039` at `$0F78` and spins at
`$0F7B` until bit 7 of `$E03A` is **clear**.

Both are reached through `suspendCommForDiskAccess` `$10CB` and `resumeCommAfterDiskAccess` `$10D1`,
which also put "WORKING..." on the status line.  Callers: `$0B3D`, `$1030`, `$103E`, `$1044`,
`$1086`, `$10A8`, `$10DC` in the main program and `$7B7D`, `$7DF7`, `$7DFE`, `$7F57`, `$7F95`,
`$815E`, `$81A2`, `$81AF` in the map-generator overlay.  `waitForGameDiskInserted` `$10DC` suspends
and deliberately leaves the module suspended for its caller.

### Exact values

| Byte | Value | Meaning |
|---|---|---|
| `$E039` `nmiSuspendRequest` | `$00` | no request - run normally |
| | `$C0` | the game wants the module's interrupts off (only bit 7 is ever tested) |
| | `$80` | written by the module itself when it parks (see below) |
| `$E03A` `nmiSuspendAck` | `$00` | interrupts are running |
| | `$80` | interrupts are off; the disk may be touched |

Only bit 7 matters on both bytes.  `$E5DD` tests the request with `BMI`, and `$0F6D`/`$0F7E` test the
acknowledge with `BPL`/`BMI`.

### Module side, and where it runs

The handshake is served from `pollLinkStatus` `$E00C` -> `pollCarrierState` `$E3CB` ->
`serviceCarrierAndSuspendRequest` `$E5D6`, which the raster IRQ calls once per frame at `$112C`
(right after the `$E000` X=0 tick at `$1129`).  **The game's spin loops therefore depend on the
raster IRQ still running**; the module never answers from the store itself.  (The solo trainer
answers from its `$E000` X=0 entry instead, at `$E059-$E05C` of `trainerAiE000.s`, by copying `$E039`
straight into `$E03A`.  Either entry is acceptable - the raster IRQ calls both, in that order.)

The stock sequence at `$E5D6`:

```
E5D6  lda $E03A       ; acknowledge
E5D9  asl a           ; C = "already suspended"
E5DA  lda $E039       ; request
E5DD  bmi $E5C1       ; bit 7 set -> suspendUartIfRunning
E5DF  bcc $E5E4       ; no request and not suspended -> carry on to the carrier sample
E5E1  jsr $E586       ; no request but still suspended -> restartUart
E5E4  ...             ; carrier sampling (section 2)
```

`suspendUartIfRunning` `$E5C1` is a two-byte `BCS $E5D5` that returns at once when the suspend has
already been acknowledged, otherwise it falls into `stopCommNmi` `$E5C3`.

`stopCommNmi` does three things: writes `$FF` to `uartRestartRequest` `$E042` (which forces
`connectionPhase` `$E040` back to 0 on the next tick of the modem state machine), calls
`setCiaNmiMask` with `$7F` to disable every CIA2 interrupt source, and decrements `uartPendingCount`
`$E0A5` if a character was half way out of the transmitter, because that character will never finish.

`setCiaNmiMask` `$E593` is where the two handshake bytes are actually written:

```
E5A9  eor #$FF        ; $92/$83 -> $00, $7F -> $80
E5AB  and #$80
E5AD  sta $E039       ; nmiSuspendRequest
E5B0  sta $E03A       ; nmiSuspendAck
```

so an enable mask clears both bytes and the disable mask sets both to `$80`.  Note that the module
overwrites the game's `$C0` request with its own `$80`; that is deliberate and harmless, because on
the next frame `$E5DD` still sees bit 7 set and `$E5C1` short-circuits.

`restartUart` `$E586` is the resume path: it zeroes the six bit-level UART state bytes `$E5BB-$E5C0`,
writes 1 to `uartRestartRequest` `$E042` ("re-evaluate the connection but keep the phase"), primes
`carrierSampleTimer` `$E045` with a negative value, and falls into `setCiaNmiMask` with `$92`, which
clears both handshake bytes.  `restartUart` is also the cold-start path, called from `openCommLink`
at `$E2CF`.

### What the SwiftLink UART layer must do

**On suspend** (`$E039` bit 7 set and `$E03A` bit 7 clear):

1. Silence the ACIA completely.  The expansion-port NMI is non-maskable, so it is not enough to set
   `SEI`; the interrupt source itself must go.  Set command-register bit 1 (receiver IRQ disable) and
   leave the transmit control bits at "TX IRQ off".  Do **not** clear command bit 0 - that is DTR,
   and dropping it makes a Hayes modem hang up (section 3).
2. Read the status register once afterwards so any already-latched IRQ flag is cleared.
3. Decide what to do about the character that may be mid-transmission.  The stock code decrements
   `$E0A5` for it (`$E5CD-$E5D4`); the same reasoning applies, since after a disk load the peer will
   have to resynchronise anyway.
4. Set `uartRestartRequest` `$E042 = $FF` so the connection falls back to phase 0, exactly as
   `stopCommNmi` does.  The frame layer will then rebuild byte sync, which is the right answer
   because incoming bytes were dropped on the floor while the drive was busy.
5. Set `$E039 = $80` and `$E03A = $80`.

**On resume** (`$E039` bit 7 clear and `$E03A` bit 7 set):

1. Re-enable the receive interrupt (command bit 1 clear, bit 0 still set).
2. Throw away whatever the receiver may have latched during the load - read the data register once
   and clear the overrun/framing/parity bits by reading status.
3. Clear the module's own bit-level state, set `uartRestartRequest` `$E042 = $01`, and re-prime the
   quiet-line timer `carrierSampleTimer` `$E045` with a negative value, as `restartUart` does.
4. Set `$E039 = $00` and `$E03A = $00`.

**Do not touch CIA2 at all.**  The stock `configureUserPortLines` writes `CIA2_CRA = $51` (starts
timer A with the serial port in output mode), `CIA2_TA_LO/HI`, `CIA2_SDR`, `CIA2_DDRB = $26`,
`CIA2_PRB` and `CIA2_PRA` bit 2.  The fast loader owns CIA2 port A bits 4-7 and its own DDR; leaving
CIA2 alone removes a whole class of interference that the stock driver had to suspend around.  The
SwiftLink build still has to suspend, because the ACIA's NMI would wreck the loader's cycle timing
just as thoroughly.

**The NMI chain must be preserved.**  `installCommNmiVector` `$E57B` writes `$E685` into `$FFFA/$FFFB`
(the ROMs are banked out, so the vectors are RAM).  When the NMI was not the module's, the stock
handler restores the interrupt mask and jumps through `nmiChainVector` `$E031/$E032`, which the game
patches to `$1298` (a bare `RTI`) at `$0BF3/$0BFB`.  `$E031/$E032` are inside the frozen block, so
the SwiftLink handler must keep chaining through them - that is the path the RESTORE key takes.


## 2. Carrier detection

### Where the bit comes from in the stock driver

`serviceCarrierAndSuspendRequest` `$E5D6`, after the suspend handshake:

```
E5E4  lda $E03B        ; isLinkActive - if the link was never opened, do not sample at all
E5E7  beq $E662        ; (an RTS)
E5E9  lda $E04A        ; carrierOverrideFlags
E5EC  bmi $E604        ; bit 7 would publish A verbatim (no code in the image ever sets it)
E5EE  asl a            ; bit 6 -> bit 7
E5EF  bmi $E5F8        ; the C= + C override is on: report carrier without reading the line
E5F1  eor $DD01        ; CIA2_PRB, through $E04A as a polarity mask (normally $00)
E5F4  and #$10         ; PB4
E5F6  adc #$F0         ; PB4 high -> A=$00 C=1 ; PB4 low -> A=$F0 C=0
E5F8  sec
E5F9  ror a            ; -> $80 no carrier, $F8 carrier, $C0 override
E5FA  inc $E045        ; carrierSampleTimer
E5FD  bpl $E604        ; still positive: a character arrived recently, keep bit 7
E5FF  sta $E045        ; quiet line: park the (negative) sample here so the timer stays negative
E602  and #$7F         ; and strip the "data flowing" bit
E604  sta $E03D        ; linkStatusSample
```

**The user-port bit is `CIA2_PRB` bit 4 (PB4), user-port pin H, the standard C64 RS-232 DCD input,
and carrier means PB4 reads LOW.**  That inversion is the level converter's doing: on the user port
the RS-232 control lines arrive TTL-inverted, so an asserted (positive) DCD at the connector shows up
as a 0 at the CIA.  With no interface plugged in, PB4 floats/pulls high and the driver reports "no
carrier" - which is why the C= + C override exists.

`$E04A` `carrierOverrideFlags` doubles as a polarity mask because of the `EOR $DD01`: setting bit 4
there would invert the sense of the DCD test.  No code writes bit 4; only bit 6 is toggled, by the
C= + C hot key through `$E04B` `carrierOverrideToggleMask` = `$40`.

The three possible raw samples, before the quiet-line adjustment:

| Condition | `$E03D` |
|---|---|
| PB4 high (no carrier) | `$80` |
| PB4 low (carrier) | `$F8` |
| C= + C override active | `$C0` |

and with bit 7 stripped when the line has been quiet: `$00`, `$78`, `$40`.

### The "data is flowing" bit (bit 7)

`carrierSampleTimer` `$E045` is incremented once per frame here and zeroed by the NMI receiver at
`$E719` on every character that arrives with a good stop bit.  Starting from 0 it takes 128 frames
(about 2.1 s NTSC, 2.6 s PAL) to go negative, at which point `rearmCarrierTimer` `$E5FF` stores the
sample into it - a negative value - so it stays negative until the next good character resets it to
0.  Bit 7 of `$E03D` therefore means "a character has been received in the last ~2 seconds".

The SwiftLink build must reproduce this: zero `$E045` from the receive path each time a byte is taken
out of the ACIA's data register with no framing/parity error.

### Debouncing, and how it reaches `$E03C`

`pollCarrierState` `$E3CB` (the `$E00C` implementation):

```
E3CB  jsr $E5D6            ; suspend handshake + fresh sample
E3CE  lda $E03D            ; linkStatusSample
E3D1  cmp $E03C            ; linkStatus
E3D4  beq $E3E3            ; agrees - reset the debounce count
E3D6  inc $E043            ; carrierDebounceCount
E3D9  ldx $E043
E3DC  cpx #$F0             ; 240 consecutive disagreeing samples
E3DE  bcc $E3EA
E3E0  sta $E03C            ; accept the new value
E3E3  ldx #$00
E3E5  stx $E043
E3E8  beq $E3F2
E3EA  and #$80             ; meanwhile take only the "data flowing" bit ...
E3EC  ora $E03C            ; ... and merge it in (this can only SET bit 7, never clear it)
E3EF  sta $E03C
E3F2  rts
```

240 frames is about 4.0 s NTSC / 4.8 s PAL - deliberately slow, because a modem's DCD twitches during
negotiation.  It applies to the first acquisition too, so "WAITING FOR CONNECTION..." sits on screen
for at least four seconds after carrier appears.

Note the asymmetry at `$E3EA`: between accepted transitions the merge can only turn bit 7 on.  Bit 7
of `$E03C` therefore only ever falls when a whole new sample is accepted, i.e. after 240 frames.
`hangUpModemSetState` at `$1B4B` relies on exactly that (see below).

### The three bytes the rest of the game reads

| Byte | Written by | Read by |
|---|---|---|
| `$E03B` `isLinkActive` | `openCommLink` `$E2D2` (`INC` -> 1); cleared by `clearLinkVars` `$E2AE`; forced to 0 by the game at `$1B01` in a trainer game | `$0F60`, `$0F71`, `$1331`, `$1352`, `$1A40`, `$1A8C`, `$1B20`, `$1B38`, `$1B5F`, `$4E46`, `$795D`, and `$E13E` inside the module |
| `$E03C` `linkStatus` | `pollCarrierState` only | `$1B4B` (bit 7), `$1BB6` (bit 6), `$56B1` (bit 6), `$65BC` (bit 6); inside the module `$E2F8`, `$E622`, `$E787`, `$E7E3` |
| `$E03D` `linkStatusSample` | `serviceCarrierAndSuspendRequest` only | `$1BAB` (bit 6); inside the module `$E61F` |

`$E03B` is **not** carrier-derived.  It only says "`$E003` X=0 has been called and the UART has been
opened".  Do not make it depend on DCD.

Bit meanings, which must not change:

* `$E03C` bit 6 = carrier present (debounced).  `pollMenuSession` `$56B1` tears the whole session down
  and jumps to `disconnectFromOpponent` `$1C17` when it is clear; `waitForCarrier` `$1BB6` spins until
  it is set; the VOICE PAUSE handler `$65BC` uses it to decide whether to warn the opponent first.
* `$E03C` bit 7 = data has been flowing.  `hangUpModemSetState` `$1B4B` waits (up to `$26` ticks of
  `zp_7F`) for it to go clear after telling the player to pick up the handset.
* `$E03D` bit 6 = the *undebounced* carrier bit.  `waitForCarrier` `$1BAB` uses it to step a countdown
  that nothing ever tests, so only bit 6's value matters, not its timing.

### Reading DCD on the 6551 instead

Status register (`base+1`) bit 6 is the DCD line and **it is active low**, so:

```
        lda ACIA_STATUS         ; base+1
        and #$40                ; bit 6: DCD line
        ; Z=1 (bit clear) means carrier present
```

Two SwiftLink-specific facts, both from CMD's own SwiftLink-232 Application Notes v1.1
(<http://csbruce.com/cbm/ftp/reference/swiftlink.txt>), sections 3.2 and 4:

1. **DCD and DSR are swapped at the ACIA on the SwiftLink.**  CMD deliberately exchanged the two
   signals so the 6551's receiver (which the chip gates on its own /DCD pin) stays enabled at all
   times while the user still gets to see the modem's DCD.  The result is that **status bit 6 reflects
   the modem's DCD line and bit 5 reflects DSR**, which is the *opposite* of the plain 6551 data
   sheet and of the register summary in this project's task brief.  Use bit 6.
2. **An unwired DCD reads as carrier present.**  There are pull-up resistors on DCD, DSR and CTS, so
   "if you happen to use a cable that is missing the DCD line, the pull-up resistor will pull the line
   active, so that bit #6 in the status register would be cleared".  For a null-modem cable that
   carries only TxD, RxD and ground this is exactly the behaviour the game wants: permanent carrier,
   no need for the C= + C override at all.

Also worth knowing: a state change on DCD or DSR raises an ACIA interrupt in its own right, so the
NMI handler must be prepared for an NMI in which neither RDRF nor TDRE is set.  Reading the status
register clears the interrupt flag.

**Caution about dummy reads.**  On the 6502, `STA abs,Y` and `LDA abs,X` perform a read at the
un-carried address whenever the index crosses a page.  `loadBaudParameters` `$E353` uses
`sta $DF59,y` with Y = `$FD..$FF` to reach `$E056-$E058`; the un-carried read lands on `$DF56`.  If
the cartridge is strapped to `$DF00` and mirrors its four registers through the page, `$DF56` aliases
the command register (`$56 & 3 = 2`), which is harmless - but the same trick landing on `$DF55` would
alias the *status* register, and reading status clears the pending interrupt flag.  Avoid
absolute-indexed addressing whose un-carried address can fall inside the ACIA's page.


## 3. The modem state machine ($E756) and connection phases ($E040)

### Call path

Raster IRQ `$1129` -> `$E000` X=0 -> `commRequestDispatch` `$E111` -> `serviceCommTick` `$E12F`.
`serviceCommTick` takes the re-entrancy lock `serviceLock` `$E0A7` (which rests at 1), always calls
`runModemStateMachine` `$E756`, and only when `connectionPhase` `$E040` >= 3 **and** `$E03B` is
non-zero does it also run `receivePacketFsm` `$E14D` and `sendPacketFsm` `$E1DD`.

`runModemStateMachine` falls through into `sendNextModemCommandChar` `$E7CA` and from there into
`runLinkStateMachine` `$E7F7`, so one call walks the whole chain.

### The four phases

| `$E040` | Name | What runs | Border (see below) |
|---|---|---|---|
| 0 | idle / link down | `$E765` sees phase 0 with no restart request and returns immediately | black / yellow |
| 1 | modem command + terminal mode | `serviceModemInput` `$E799` and `sendNextModemCommandChar` `$E7CA` shuttle characters between the modem and the two host rings | light blue / blue |
| 2 | byte sync | `beginByteSyncPhase` `$E805` / `continueByteSync` `$E828` | light red / red |
| 3 | packet protocol | `runPacketPhase` `$E858`, plus the packet FSMs above | light green / green |

Phase 0 is entered by `stopCommNmi` writing `$FF` into `uartRestartRequest` `$E042`, which
`$E75D-$E762` consumes by storing 0 into `$E040`.  A request of `$01` (from `restartUart`) means
"re-evaluate but keep the phase".

### How phase 1 is entered and left

```
E77B  lda #$01
E77D  cmp $E040
E780  beq $E799        ; already in terminal mode
E782  ldx $E03F        ; modemReplyTimer
E785  bne $E791        ; a modem dialogue is under way -> enter/stay in terminal mode
E787  bit $E03C        ; debounced carrier byte
E78A  bvs $E7F7        ; V = bit 6 = carrier: skip the modem dialogue entirely
E78C  jsr $E0C3        ; popHostOutRing - no carrier: discard everything queued for the opponent
E78F  bcc $E78C
E791  jsr $E534        ; flushUartTxRing
E794  lda #$01
E796  sta $E040        ; phase 1
```

So terminal mode is entered when **either** an AT command is in flight (`$E03F` non-zero) **or** there
is no carrier.  `$E78A` is the important branch: with carrier present and no AT string pending, the
module jumps straight to `runLinkStateMachine` and never visits phase 1.

Leaving phase 1 goes through `modemReplyTimer` `$E03F`:

* `queueModemCommandString` `$E393` sets it to `$FF` (negative = "a command string is going out").
* `sendNextModemCommandChar` arms it with `$79` = 121 frames (~2.0 s NTSC) when it transmits the CR
  that terminates an AT command (`$E7D5-$E7DE`).
* `$E7E3-$E7E9`: while `$E03C` bit 6 is clear the timer is pinned at 1, so it fires on the first tick
  after carrier appears.
* When it reaches 0 the module fabricates a CR into the host input ring (`$E7F2/$E7F4`) so the chat
  line unblocks, and falls straight into `runLinkStateMachine`.

`runLinkStateMachine` `$E7F7` then dispatches: any non-zero `linkErrorCount` `$E047` restarts byte
sync whatever the phase; phase 2 continues the sync at `$E828`; phase 3 or above runs the packet
protocol; phases 0 and 1 fall through into a fresh `beginByteSyncPhase`.

### Where the Hayes strings live and how they go out

Three canned strings sit in page `$EB`, **stored back to front**, because
`queueModemCommandString` `$E393` copies them forwards into `hostOutRing+1` (`modemCommandChars`
`$E092`) and then sets the read index to the character count, and `popHostOutRing` walks that index
downwards.

| Address | Emitted as | Queued by |
|---|---|---|
| `$EBD6` `modemHangUpString` | delay `$A0`, `+`, `+`, `+`, delay `$A0`, CR, delay `$C0`, `A`, `T`, `H`, `0`, CR | `hangUpModem` `$E390`, via `hangUpStringPtrLo` `$EBFD` = `$D6` |
| `$EBE3` `modemAnswerInitString` | delay `$C0`, `ATQ0V1X1A`, CR | `openCommLink` `$E2E3` when `$E011` = 0 |
| `$EBF0` `modemDialInitString` | delay `$C0`, `ATQ0V1X1D`, CR | `openCommLink` `$E2E3` when `$E011` = 1 |

`modemInitStringPtrLoTable` `$EBFE` holds the two low bytes `$E3,$F0`; the high byte `$EB` is the
un-patched half of the self-modifying `LDA $EB00,x` at `$E3A1`.

A queued byte with bit 7 set is never transmitted: `$E7D0` parks it in `modemDelayCounter` `$EB01` as
a negative frame count, which `tickGuardDelay` `$E7C0` counts back up to zero.  That is how the Hayes
`+++` guard times are produced (`$A0` = 96 frames, `$C0` = 64 frames).

Two things worth noticing about the dial string: it is `ATD` with **no telephone number**, and the
game's prompts ("GET OPPONENT ON PHONE AND...", "PRESS A OR O AND SET MODEM.", "PRESS SPACE, WAIT,
HANGUP PHONE.") make it clear that the players are expected to establish the voice call by hand and
then hand the line to the modems.  Modem Wars never dials anybody.  So a direct-cable build loses
nothing by not dialling.

`$E048` `modemOptionFlags` (`$CC` on disk) gates all of this:

* bit 7 = "a Hayes modem is attached".  `queueModemCommandString` `$E393` tests it first and returns
  `A = $FF` without doing anything when it is clear - **no AT string is ever queued and `$E03F` is
  never set**.  `serviceModemInput` `$E79E` also discards everything received in phase 1 when it is
  clear.
* bit 6 = "watch the verbose result codes".  `checkConnectSpeed` `$E7A7` sniffs for a CR preceded by
  `'0'` (the last digit of `CONNECT 1200`; a plain `CONNECT` ends in `T` and is ignored) and switches
  to the second baud entry.

`$E048` is at `$E048`, one byte past the end of the frozen block `$E01D-$E047`, so the SwiftLink build
is free to change it.

### What a direct null-modem connection has to skip

Set `$E048` bit 7 clear (and bit 6 clear - there is no result code to sniff).  Then:

* `openCommLink` `$E2E1-$E2E3` still calls `queueModemCommandString`, but it returns immediately, so
  `$E03F` `modemReplyTimer` stays 0 and `hostOutCount` `$E02E` stays 0.
* Because `$E02E` stays 0, the chat editor at `$1357` opens straight away instead of waiting for an
  AT string to drain.
* On the first tick after `openCommLink`, `$E782` finds `$E03F` = 0 and `$E787` finds carrier, so
  `$E78A` branches to `runLinkStateMachine` and the module goes 0 -> 2 -> 3 without ever entering
  phase 1.  Nothing needs to be cut out of the state machine; it already has the path.
* `hangUpModem` `$E38D` still calls `dropDtrLine` `$E574` and then `queueModemCommandString`, which
  again does nothing.  On the ACIA the equivalent of `dropDtrLine` is clearing command bit 0, which
  also disables the receiver and all interrupts - that is the correct behaviour for "hang up", and
  `commLinkControlDispatch` `$E2A2-$E2A9` immediately follows it with `stopCommNmi`.  Note `$E2A5`
  spins on `modemReplyTimer` `$E03F` until `LSR` leaves zero, i.e. until it is 0 or 1; with no modem
  configured `$E03F` is already 0, so that loop exits at once.
* Carrier: with no DCD wire the SwiftLink's pull-up reports carrier permanently, so `$E03C` bit 6
  settles set after the 240-frame debounce and stays there.  The C= + C override is then redundant,
  but it costs nothing to leave it working for people whose cable does carry a real DCD.

The one thing a null-modem build genuinely must handle differently is **who starts talking first**.
The byte-sync handshake at `$E805` is symmetric (both ends send `$00` until they see a `$00`, then
answer `$FF` and wait for the peer's `$FF`, restarting after ten fruitless rounds), so answer and
originate are interchangeable at that level.  `$E011` `isOriginateMode` still matters, though: the
game asks the ANSWER/ORIGINATE question at `$0CBD` and stores the result at `$0CDE`, and the stock
module uses it to pick the AT string (`$E2DD`), to index `userPortIdleTable` `$E04F` (both entries are
`$26`, so it makes no difference) and to index `serialShiftPatternTable` `$E051` together with
`baudIndex`.  A SwiftLink build should keep reading `$E011` if it wants the two ends to disagree about
anything (for example, to pick different bit patterns), but nothing above the UART requires it.

### Border-colour indicator (useful while bringing the new UART up)

`readKeyAndHandleModemHotkeys` `$E2F0-$E30E` paints `VIC_BORDER` from `linkStatusBorderTable` `$E05F`
using `index = connectionPhase*2 + carrier`.  Each byte is `colour*2 + always-paint`:

| Index | Byte | Colour | Painted |
|---|---|---|---|
| 0 (phase 0, no carrier) | `$00` | black | only while C= is held |
| 1 (phase 0, carrier) | `$0E` | yellow | only while C= is held |
| 2 (phase 1, no carrier) | `$1D` | light blue | always |
| 3 (phase 1, carrier) | `$0D` | blue | always |
| 4 (phase 2, no carrier) | `$14` | light red | only while C= is held |
| 5 (phase 2, carrier) | `$04` | red | only while C= is held |
| 6 (phase 3, no carrier) | `$1A` | light green | only while C= is held |
| 7 (phase 3, carrier) | `$0A` | green | only while C= is held |

Hold the Commodore key and the border tells you which phase the link is in.  Note the whole hot-key
path (and therefore this indicator) is skipped while `inputLockoutTimer` `$0B7D` is non-zero, because
the raster IRQ only calls `pollKeyboardEvent` `$0E56` at `$11E7` when it is 0.


## 4. Baud selection

### The stock table and what reads it

`baudParameterTable` `$E059`, two 3-byte entries, indexed by `baudIndex` `$E055` (0 or 3):

```
E059  $50 $0D $01      ; entry 0: bit period $0D50 = 3408 cycles = 300.1 baud, pacing 1
E05C  $53 $03 $02      ; entry 1: bit period $0353 =  851 cycles = 1201.8 baud, pacing 2
```

(3408 and 851 cycles against the NTSC 1022727 Hz system clock.)

`loadBaudParameters` `$E353` copies three bytes from `baudParameterTable+X` into
`bitPeriodLo` `$E056`, `bitPeriodHi` `$E057` and `txPaceReload` `$E058`, using
`ldy #$FD` / `sta $DF59,y` so one register both counts and indexes (`$DF59 + $FD = $E056`).  It
returns `A = $FF`, which is what the hot-key path needs in order to swallow the key.

Three call sites, all inside the module:

| Caller | X | Why |
|---|---|---|
| `openCommLink` `$E2C9` | `baudIndex` `$E055` | cold start |
| C= + 3 / C= + 1 hot keys, falling through `$E350` | 0 / 3 | player choice |
| `switchTo1200Baud` `$E7B2` | 3 (loaded at `$E7B0`) | `CONNECT 1200` was seen on the line |

`$E056/$E057` are consumed by `setBitPeriod` `$E657` (which programs CIA2 timer A or timer B) and by
the half-bit start-bit delay at `$E6E2`; `$E058` reloads `txPaceCounter` `$E044` at `$E644`.  All
three are private to the UART layer, so the SwiftLink build may redefine them - for example as
{control-register byte, spare, spare}, or a wider table with more entries.

**The game never writes `$E055`, `$E056`, `$E057` or `$E058`, and never reads them.**  There is no
saved baud preference anywhere: `baudIndex` is reset to its disk value (`$00` = 300 baud) every time
the module is reloaded from track 34, and `clearLinkVars` does not touch it because it lies outside
`$E039-$E047`.  The only thing the game stores about the link setup is `$E011` `isOriginateMode`,
written at `$0CDE` from the A/O prompt.

`$E051` `serialShiftPatternTable` (`$27,$2F,$3F,$37`, indexed by `isOriginateMode EOR baudIndex`) and
`$E04F` `userPortIdleTable` are user-port artefacts with no SwiftLink equivalent; the shift-register
write at `$E559` goes to CIA2 SP2 (user-port pin 7), which is not part of the standard RS-232 wiring
and whose purpose is not established.  Both tables are outside the frozen region and can be reused.

### The Commodore-key hot keys already in use

`readKeyAndHandleModemHotkeys` `$E2EC` is jump-table entry `$E015` and is the game's **only** keyboard
read: `pollKeyboardEvent` starts at `$0E56` with a `JSR $E015`, and the raster IRQ calls it at `$11E7`
once a frame.
It scans through the hook at `$E012`, whose operand the game patches to `scanKeyboard` `$0DB7` at
`$0F4B/$0F50`.

Key codes are the game's own: from `keyMatrixCodeTable` `$0D67`, an ordinary key returns its ASCII
code with bit 7 set (`'A'` = `$C1`, `'3'` = `$B3`), the four function keys return `$00-$03`, control
keys return their ASCII control code with bit 7 set (RETURN = `$8D`), and `$FF` means no key.  X
returns `$80` when the Commodore key is held.

Gates before any hot key is considered:

* `$E310` `BPL` - codes below `$80` (the function keys) are handed straight to the game and can never
  be hot keys.
* `$E312-$E319` - a code equal to `lastHotkeyCode` `$E046` with the Commodore key still down is
  swallowed, which is the auto-repeat filter.
* `$E31F` - the Commodore key must be held (X = `$80`).

The complete list, in the order the code tests them:

| Code | Key | Address | Action |
|---|---|---|---|
| `$C8` | C= + H | `$E323` | `EOR` `dtrToggleMask` `$E04C` = `$24` into `CIA2_PRB`: toggle PB2 (DTR) and PB5 |
| `$C3` | C= + C | `$E332` | `EOR` `$E04B` = `$40` into `carrierOverrideFlags` `$E04A`: pretend carrier is present |
| `$B3` | C= + 3 | `$E342` | `baudIndex` `$E055` = 0, then `loadBaudParameters` with X=0: 300 baud |
| `$B1` | C= + 1 | `$E34A` | `baudIndex` `$E055` = 3, then `loadBaudParameters` with X=3: 1200 baud |
| `$8D` | C= + RETURN | `$E362` | `DEC skipModemInitString` `$E041` (0 -> `$FF`), `openCommLink` `$E2C1`, `modemReplyTimer` `$E03F` = `$8D`; returns the RETURN code to the game with X cleared, so this key is **not** swallowed |
| `$D0` | C= + P | `$E374` | `hangUpModem` `$E38D` (drop DTR, queue `+++`/`ATH0`); its tail returns `$FF` |
| `$C1` | C= + A | `$E37A` | `isOriginateMode` `$E011` = 0, then `configureUserPortLines` `$E540` |
| `$CF` | C= + O | `$E37E` | `isOriginateMode` `$E011` = 1, then `configureUserPortLines` `$E540` |

Every one of them except C= + RETURN returns `A = $FF` so the game never sees the keystroke.  So does
**every other** Commodore-key combination: the fall-through at `$E38A` returns `$FF` unconditionally.
Adding new hot keys therefore cannot break anything the game currently does with the keyboard - those
codes are already being eaten - as long as the eight codes above keep their meanings.

Free and sensible for new baud keys: C= + 2 (`$B2`), C= + 4 (`$B4`), C= + 9 (`$B9`), C= + 0 (`$B0`),
C= + 6 (`$B6`), C= + 8 (`$B8`).  Note that C= + 3 and C= + 1 are already spent on 300 and 1200, so a
"digit = speed" scheme has to work around them; mapping C= + 2 -> 2400, C= + 4 -> 4800, C= + 9 ->
9600, C= + 0 -> 19200 and C= + 8 -> 38400 collides with nothing.  Avoid `$FE` and `$FF` (the modifier
and no-key sentinels) and remember that `$B3`/`$B1` must keep working if the module is to stay usable
with a real 300/1200 modem.

### The SwiftLink baud table, and verification of the doubling claim

**Conclusion: the doubling claim is correct, and the specific values in the task brief are correct.**
Verified against two independent sources beyond the brief:

1. CMD, *SwiftLink-232 Application Notes v1.1*, section 3.4 and the control-register figure
   (<http://csbruce.com/cbm/ftp/reference/swiftlink.txt>).  "Note that our cartridge uses a
   double-speed crystal, so values given on the data sheet are doubled ... (the minimum speed is 100
   bps and the maximum speed is 38,400 bps)", followed by the full table below.
2. The cc65 SwiftLink driver `libsrc/c64/ser/c64-swlink.s`
   (<https://github.com/cc65/cc65/blob/master/libsrc/c64/ser/c64-swlink.s>), whose `BaudTable` maps
   300 -> `$05`, 600 -> `$06`, 1200 -> `$07`, 2400 -> `$08`, 3600 -> `$09`, 4800 -> `$0A`,
   7200 -> `$0B`, 9600 -> `$0C`, 19200 -> `$0E`, 38400 -> `$0F`, and marks 50/75/110/134.5/1800 as
   unavailable - exactly what a doubled table predicts (1800 is unreachable because the nominal 1800
   entry now yields 3600).

Control register (`base+3`) = `$10` (bit 4 = internal baud-rate generator, bits 6-5 = `00` = 8 data
bits, bit 7 = 0 = one stop bit) OR the rate bits:

| Bits 3-0 | Control byte | SwiftLink rate | Nominal 6551 rate |
|---|---|---|---|
| `$00` | `$10` | 16x external clock | 16x external clock |
| `$01` | `$11` | 100 | 50 |
| `$02` | `$12` | 150 | 75 |
| `$03` | `$13` | 219.84 | 109.92 |
| `$04` | `$14` | 269.16 | 134.58 |
| `$05` | `$15` | **300** | 150 |
| `$06` | `$16` | 600 | 300 |
| `$07` | `$17` | **1200** | 600 |
| `$08` | `$18` | **2400** | 1200 |
| `$09` | `$19` | 3600 | 1800 |
| `$0A` | `$1A` | **4800** | 2400 |
| `$0B` | `$1B` | 7200 | 3600 |
| `$0C` | `$1C` | **9600** | 4800 |
| `$0D` | `$1D` | 14400 | 7200 |
| `$0E` | `$1E` | **19200** | 9600 |
| `$0F` | `$1F` | **38400** | 19200 |

To stay wire compatible with the stock driver, the SwiftLink build must be able to produce 300
(`$15`) and 1200 (`$17`).  Those are the only two speeds the stock module can talk at, so a stock
machine and a SwiftLink machine can only meet there.

Bear in mind that the game's *throughput* ceiling is not the line speed.  The frame layer starts at
most one thing per raster IRQ (`$E860` refuses when `uartPendingCount` >= 2) and the packet layer
exchanges one packet per lock-step turn, so past a few thousand baud the win is latency and
retransmit cost, not bandwidth.


## 5. `$E030` - the build id

`commBuildId` `$E030` is one byte inside the frozen block `$E01D-$E047`.

| Module | Value | Source |
|---|---|---|
| `game/modemDriverE000` (track 18 s7 + track 34) | `$00` | `modemDriverE000.s` `$E030`, and byte `$030` of the rebuilt 4096-byte image |
| `game/trainerAiE000` (track 35) | `$FF` | `trainerAiE000.s` `$E030` |

Only bit 7 is ever tested.  Nothing in the module writes it; it is a constant baked into the sector
image, and it survives every `clearInlineVarBlock` because none of the cleared blocks covers `$E030`
(`clearPacketVars` stops at `$E02F`, `clearLinkVars` starts at `$E039`).

The five read sites in `mainProgram0800.s`:

| Address | Routine | What the game does |
|---|---|---|
| `$0AEA` | `startGameFromSetup` | `EOR isSoloTrainer` (`$0BA5`), `AND #$80`: if bit 7 of the wanted opponent and the resident module disagree, `$0AF1/$0AF4` load the other module off disk |
| `$0B2C` | `startGameFromSetup` | `LDA $E030` / `STA $0BA5`: the resident module's id is written **back over** the wanted flag, so `$0BA5` afterwards always describes what is actually in memory |
| `$1A9F` | `miscTabAction` (ABORT GAME) | negative: skip the `$8C`/reason-0 "I am leaving" handshake, because the trainer discards everything sent to it |
| `$1AFA` | `endGameToMainMenu` | negative: force `isLinkActive` `$E03B` to 0 and preselect main-menu item 1 (PRACTICE WITH SOLO TRAINER) instead of item 0 |
| `$1B3D` | `hangUpModemSetState` | negative: skip the "PICK UP PHONE THEN PRESS SPACE." prompt (message `$17`) and the carrier-quiet wait at `$1B4B`, and go straight to `$E003` X=3 |

**The SwiftLink module must keep `$E030 = $00`.**  It is the modem build; a `$FF` there would make the
game believe the solo trainer is resident, which would (a) make `$0AEA` reload track 34 over the top
of the SwiftLink module every time a modem game starts, (b) suppress the hang-up handshake and the
end-of-game link teardown, and (c) rewrite `$0BA5` to `$FF`, turning the game into a solo game.
`checkAbi.py` enforces this as part of the `$E01D-$E047` frozen region, but it is worth stating
explicitly because it is the one byte in that region that carries meaning rather than state.

Two related bytes in the same frozen block, for completeness:

* `$E02F` `peerCommBuildId` - the *opponent's* `$E030`, stored by `exchangePlayerNamesAndSettings`
  at `$EDAD` during the start-of-session handshake.  Both ends of a modem game therefore report `$00`
  to each other; a SwiftLink machine and a stock machine will exchange identical ids, which is what
  we want since the wire protocol is unchanged.
* `$E045-$E047` hold `$44 $54 $48` ("DTH") on disk - leftovers from the sector image, because this
  page comes from track 18 sector 7, outside the encrypted area.  They are inside the frozen region,
  so the SwiftLink image must reproduce those exact junk bytes even though `clearLinkVars` `$E034`
  zeroes them the moment the link is closed.


## 6. Checklist for the replacement UART layer

Contracts, in the order they will bite:

1. `$E030` stays `$00`; `$E01D-$E047` and `$E000-$E017` come out byte-identical, junk bytes included.
2. `$E039`/`$E03A` suspend handshake served once per frame from the `$E00C` path, with `$80`/`$00`
   bit-7 semantics; ACIA interrupts genuinely off between them; CIA2 never touched.
3. `$E031/$E032` still chained on a foreign NMI.
4. `$E03D` publishes `$80`/`$F8`-shaped samples (bit 6 = carrier, bit 7 = data flowing within ~128
   frames), driven from ACIA status bit 6 with **carrier = bit clear**; `$E045` zeroed from the
   receive path; the 240-frame debounce at `$E3CB` left exactly as it is.
5. `$E03B` set by `openCommLink`, not by carrier.
6. `$E0A5` `uartPendingCount` accurate at all times; `$E044` writable by `$E820`.
7. `queueByteForTransmit` `$E73C`, `flushUartTxRing` `$E534`, `configureUserPortLines` `$E540`,
   `dropDtrLine` `$E574`, `installCommNmiVector` `$E57B`, `restartUart` `$E586`, `stopCommNmi`
   `$E5C3` and `serviceCarrierAndSuspendRequest` `$E5D6` keep their addresses.
8. `$E048` bit 7 clear for a cable-only build (no AT strings, no result-code sniffing, phase 1 never
   entered); leave the option so a real modem can still be driven.
9. `loadBaudParameters` `$E353` keeps its three-byte stride and its `A = $FF` return, or all four of
   its call sites (`$E2C9`, `$E350` fall-through, `$E7B2`) change together.
10. New hot keys only in the free codes; `$C8 $C3 $B3 $B1 $8D $D0 $C1 $CF` keep their present
    meanings, and C= + RETURN keeps returning the key to the game rather than swallowing it.


## 7. Open questions

* **Which SwiftLink clones swap DCD and DSR?**  CMD's own cartridge does (section 2), and the
  Turbo232 is documented as SwiftLink compatible, but the modern reimplementations (GLINK232 and
  friends) advertise "modem control" as an option and do not all say which way round they wire it.
  A build that only ever sees a null-modem cable does not care, because both bits read active; a
  build that wants real DCD should probably offer a bit in `$E04A` to select which status bit to
  look at, since `$E04A` is already used as a polarity/override mask by `$E5F1`.
* **Base address.**  `$DE00` is the default, `$DF00` the strap option; CMD's own notes recommend that
  software cope with both (probe by writing the control register and reading it back).  A run-time
  probe costs a few bytes and removes a support headache, and there is plenty of room in the freed
  UART region.
* **Status-register bit 7 at high speed.**  CMD warns that bit 7 (interrupt occurred) is unreliable
  at 9600 bps and above, and their own sample NMI handler ignores it and tests RDRF/TDRE/DCD/DSR
  directly.  Worth following.
* **CTS.**  The 6551 stops transmitting on its own when CTS goes inactive, and clears TDRE while it
  does, with no way to see CTS from any register.  On a null-modem cable with RTS/CTS crossed, that
  gives a free flow-control mechanism during disk loads: raising RTS on the suspending machine (both
  transmit-control bits clear) throttles the peer's transmitter automatically.  Whether that is worth
  using, or whether it is better to let the ARQ layer clean up as it does today, is a design decision
  the driver work will have to make.
