721 lines
41 KiB
Markdown
721 lines
41 KiB
Markdown
# 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.
|