18 KiB
The Commodore-key hot keys, exercised in VICE
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.mdfor the mapping onto the current source.
testReport.md left one hole in the driver's hot-key layer:
The C= + C carrier override could not be exercised.
forceCarrier()sentctrl+cfour times to each machine andcarrierOverrideFlags($E04A) read back0every time, so I cannot say that hot key works.
That is closed here. Everything below was observed on 2026-08-21 with VICE 3.7.1 (x64sc, NTSC),
one machine, ../Modem_Wars_SwiftLink.d64, driven by swiftlink/testHotkeys.py. Nothing is
inferred from the source unless it says so. The full session log is
swiftlink/testLogs/hotkeyRun.txt.
Summary of what is now verified: the Commodore key under VICE's default C64 keymap is Tab;
C= + C toggles carrierOverrideFlags $E04A bit 6 on and off; with the override on the carrier
sampler reports carrier without ever reading DCD, and it does so even when the DCD input says "no
carrier", where with the override off the same input produces "no carrier"; and C= + 2 followed by
C= + 3 reprogram the ACIA control register $DE03 to $18 (2400 baud) and back to $15 (300).
1. How you press the Commodore key under VICE - determined by experiment
The game does not use the KERNAL. scanKeyboard $0DB7 reads the CIA1 matrix itself, once per
raster IRQ, and it reports the Commodore key only when matrix row 7, column 5 is pulled low. So
the whole question is which X keysym VICE's active keymap puts at 7/5, and the only way to be sure is
to press candidates and watch a byte the module writes.
testHotkeys.py holds each candidate together with C and reads two bytes while the keys are still
down: carrierOverrideFlags $E04A, which only the C= + C hot key touches, and lastHotkeyCode
$E046, which the module writes on every new key code it sees. Every key is pressed with
xdotool keydown, held for 900 ms - about 54 of the game's once-a-frame scans at true C64 speed -
and then released, because a tap is not reliably visible to a once-a-frame scanner.
| host key held with C | $E04A before -> after |
$E046 while held |
verdict |
|---|---|---|---|
Control_L |
$00 -> $00 |
$FF |
nothing reaches the module at all |
Tab |
$00 -> $40 |
$C3 |
C= + C fired: this is the Commodore key |
Super_L |
$40 -> $40 |
$C3 |
the letter arrives as a plain key; no hot key |
(Super_L starts from $40 because the Tab row above had just turned the override on; what
matters in its row is that the value does not change.)
That is reproducible: the same three lines came out of three separate boots.
Why the earlier ctrl+c attempt could never have worked, and it is not just the tap:
- The monitor reports
KeymapIndex=0withKeymapSymFile="gtk3_sym.vkm"- index 0 is the symbolic map, sogtk3_sym.vkmis the live one. - In that file,
Tab 7 5 8200(8192= "key is (left) cbm on emulated machine") andControl_L 7 2 16392(16384= "key is (left) ctrl"). Left Control is the C64 CTRL key, not the Commodore key. Only the positional mapgtk3_pos.vkmputs C= onControl_L(Control_L 7 5 8200), and it puts CTRL on Tab. - Row 7 column 2 - CTRL - is
$FFin the game's ownkeyMatrixCodeTable$0D67, andscanKeyboardanswers$FF= "no key at all" the moment it finds that cell ($0E37 bne L_0E3E). So holding left Control does not merely fail to be C=, it blinds the whole scan. That is exactly what the$E046=$FFcolumn above shows.
The Super_L row is the control case worth having: Super_L appears in neither gtk3_sym.vkm nor
gtk3_pos.vkm, so VICE ignores it, the C arrives on its own, lastHotkeyCode becomes $C3 - and $E04A does not change,
because $E31F cpx #$80 fails. "The key got in" and "the hot key fired" are therefore two different
observations, and the sweep separates them.
Two further keys were tried in an earlier boot and are recorded for the next person:
Alt_L-$E046stayed$FF, and worse, every key sent after it was swallowed: holding Alt opens the GTK menu bar, which takes a keyboard grab. The rest of that run was junk. Do not put Alt in a candidate sweep;testHotkeys.pyno longer does.Control_R- no effect,$E046=$FF, but this was measured afterAlt_Lhad already wedged that run, so it proves nothing.
That boot's log is testLogs/hotkeyKeymapSweep.txt; its Control_L, Tab and Super_L rows match
the table above exactly.
2. C= + C toggles the override on, and off again
Taken at the "GET OPPONENT ON PHONE AND PRESS A OR O" prompt, where the module is resident
($E012 reads JMP $0DB7, the game's scanKeyboard) and the raster IRQ is already calling $E015
every frame:
| when | $E04A |
|---|---|
| before | $00 |
| after one C= + C | $40 |
| after a second C= + C | $00 |
$40 is carrierOverrideToggleMask $E04B, i.e. bit 6, and the second press clears it again, which
is what $E339 eor carrierOverrideFlags is supposed to do. The auto-repeat filter at $E312 does
not get in the way as long as the key is released between presses: with nothing held, scanKeyboard
returns $FF, and lastHotkeyCode $E046 is reloaded with $FF, so the next $C3 is a fresh code.
Note that the hot key works before the link is open (isLinkActive $E03B = $00 in all three rows
above). The override flag is set; it simply has nothing to act on yet, because the carrier sampler
returns early at $E5E7 while the link is down.
3. What the override does to the link state the game reads
The link was then opened for real - A at the answer/originate prompt, then space - and came up on
the first attempt: aciaCommandShadow $E5BE = $05, $E03B isLinkActive = $01, control
register $DE03 = $15 (8N1, internal clock, 300 baud), connectionPhase $E040 = $02. The far
end of the serial line is a TCP sink that accepts the connection and never answers, so the link stays
in byte sync; 10697 bytes came out of the emulated 6551 during the run, beginning
0d 41 54 51 30 56 31 58 31 41 0d = CR ATQ0V1X1A CR, the answer-mode Hayes string, followed by the
$00 stream of beginByteSyncPhase.
Two things about the game screen while all of this was going on, so the pictures are not
misleading: with a peer that never answers, the game puts up "PHONE TROUBLE."
(shots/hotkeys03linkOpen.png) and later "PICK UP PHONE THEN PRESS SPACE."
(shots/hotkeys05borderHeld.png), while the module itself stays in connectionPhase $02 with
isLinkActive = $01 throughout. The hot keys keep working in both of those states -
inputLockoutTimer $0B7D read $00 at every measurement, so the raster IRQ was polling the
keyboard through $E015 the whole time.
With that running, C= + C on and off again:
| state | $E03B isLinkActive |
$E03C linkStatus |
$E03D linkStatusSample |
$E5BC aciaStatusSave |
|---|---|---|---|---|
| override off | $01 |
$78 |
$78 |
$90 |
| override on (read immediately) | $01 |
$40 |
$40 |
$90 |
| override on, six seconds later | $01 |
$40 |
$40 |
$90 |
| override off again | $01 |
$78 |
$78 |
$90 |
| override off, six seconds later | $01 |
$78 |
$78 |
$90 |
Those two values are the driver's own fingerprints, from $E5F8-$E602:
$78= a real carrier sample.#$F0->SEC->ROR=$F8, minus bit 7 because the line has been quiet (no character has arrived recently, sorearmCarrierTimer$E5FFstrips it).$40= an override sample. The override branch enters the sameSEC/RORwith$80, so it produces$C0, minus the same bit 7 =$40.
So the override is not merely "carrier present" - it is visibly a different route to it, and the
round trip back to $78 shows the second keypress really cleared the flag. $E03B is not
carrier-derived and stays $01 throughout, exactly as spec_link.md says.
4. The case a null-modem cable with no DCD wire depends on
VICE cannot produce "no carrier" on its own. On a SwiftLink the modem's DCD arrives on 6551
status bit 6 (CMD swapped DCD and DSR at the chip), and that is the bit the carrier sampler
serviceCarrierAndSuspendRequest $E5D6 tests with BIT aciaStatusSave. Every status byte VICE's emulated 6551 produced in this
project - $30 with the device closed, $10, $50, $88, $90, $98 while it is open - has bit 6
clear. VICE drives bit 5, the 6551's own DCD bit, and that only tracks whether the rs232 device is
open. So in VICE the driver always sees carrier, and the override can never be the difference
between a link and no link by itself.
It can be made the difference where the driver actually reads DCD: aciaStatusSave $E5BC, the
shadow the NMI keeps and the only DCD the carrier sampler ever sees. The test breaks at $E5E9 -
the LDA carrierOverrideFlags one instruction before the DCD test - pokes $E5BC to $40 (bit 6
set = DCD deasserted = no carrier), and single steps the sampler. Both passes below used exactly
that same forced input, and $E5BC was read back as $40 after each one, so nothing refreshed it in
between.
Override on ($E04A = $40) - the DCD test is never executed:
.C:e5e9 AD 4A E0 LDA $E04A A:01
.C:e5ec 0A ASL A A:40 <- carrierOverrideFlags = $40 ...
.C:e5ed 30 09 BMI $E5F8 A:80 <- ... so bit 6 is now bit 7: branch taken,
.C:e5f8 38 SEC A:80 $E5F1 BIT aciaStatusSave is skipped entirely
.C:e5f9 6A ROR A A:80
.C:e5fa EE 45 E0 INC $E045 A:C0 <- sample = $C0: carrier, by override
.C:e5fd 10 05 BPL $E604 A:C0 <- quiet line, so bit 7 comes off
.C:e5ff 8D 45 E0 STA $E045 A:C0
.C:e602 29 7F AND #$7F A:C0
.C:e604 8D 3D E0 STA $E03D A:40 <- published: bit 6 set = carrier present
.C:e607 AD BB E5 LDA $E5BB A:40 <- on into startNextTxChar, and back out
.C:e60a D0 56 BNE $E662 A:01
.C:e662 60 RTS A:01
.C:e3ce AD 3D E0 LDA $E03D A:01
.C:e3d1 CD 3C E0 CMP $E03C A:40
.C:e3d4 F0 0D BEQ $E3E3 A:40 <- agrees with linkStatus, debounce reset
readback: $E03D = $40, $E03C = $40, $E5BC = $40.
Override off ($E04A = $00), same forced $E5BC = $40:
.C:e5ec 0A ASL A A:00
.C:e5ed 30 09 BMI $E5F8 A:00 <- not taken
.C:e5ef A9 F0 LDA #$F0 A:00
.C:e5f1 2C BC E5 BIT $E5BC A:F0 <- V := bit 6 of $40 = 1
.C:e5f4 50 02 BVC $E5F8 A:F0 <- not taken: DCD says no carrier
.C:e5f6 A9 00 LDA #$00 A:F0
.C:e5f8 38 SEC A:00
.C:e5f9 6A ROR A A:00
.C:e5fa EE 45 E0 INC $E045 A:80 <- sample = $80: no carrier
.C:e5fd 10 05 BPL $E604 A:80
.C:e5ff 8D 45 E0 STA $E045 A:80
.C:e602 29 7F AND #$7F A:80
.C:e604 8D 3D E0 STA $E03D A:00 <- published: bit 6 clear = no carrier
readback: $E03D = $00, $E03C = $78 (unchanged - pollCarrierState needs 240 disagreeing
frames before it accepts a new value), $E5BC = $40.
That is the whole point of the hot key, in two traces. Identical DCD input; with the override
off the driver publishes "no carrier" and the game would tear the session down four seconds later
(pollMenuSession $56B1 watches $E03C bit 6); with the override on it publishes "carrier" and
never looks at the line at all. A three-wire null-modem cable, where DCD is not wired and the
cartridge's pull-up already reads as carrier, does not need this - but a cable that does carry a real
DCD from a peer that is not asserting it now demonstrably does.
5. A baud hot key really reprograms the ACIA
Same session, link up, at true C64 speed:
| after | $E055 baudIndex |
$E056 liveControlByte |
$DE03 ACIA control |
|---|---|---|---|
| (before) | $00 |
$15 |
$15 |
| C= + 2 | $06 |
$18 |
$18 |
| C= + 3 | $00 |
$15 |
$15 |
$18 is entry 6 of baudEntryTable $E6FD - 2400 baud on a SwiftLink's doubled clock - and it is a
value no earlier test in this project had ever written; testReport.md recorded only $1E (the
probe pattern) and $15. C= + 3 puts it back to 300. Both the index and the register move
together, so the backwards walk of baudEntryTable at $E344 and loadBaudParameters $E353 are
both doing what section 3 of the README claims.
6. The border indicator, which is the C= key's other job
readKeyAndHandleModemHotkeys also paints VIC_BORDER from linkStatusBorderTable $E05F while
the Commodore key is held, indexed by connectionPhase*2 + carrier. With the link up in
connectionPhase $02 and linkStatus $78 (carrier) the index is 5, whose table byte is $04 =
red, with the low bit clear so it is painted only while C= is held.
A checkpoint on the module's own STA VIC_BORDER at $E30C, armed first and Tab pressed afterwards:
#1 (Stop on exec e30c)
.C:e30c 8D 20 D0 STA $D020 - A:02 X:80 Y:00 SP:de N.-.....
A = $02 is red - $04 from the table, halved by the LSR at $E305 - and X = $80 is
scanKeyboard's "the Commodore key is held" flag, which is what took the BMI at $E308. So the
paint really happens; the code that does it runs from the raster IRQ, so it repeats every frame for
as long as the key is down.
Reading $D020 is not how to see it: it was $FC, the game's own border colour, both at that
breakpoint (where the store had not executed yet) and again after the key was released, so the game
rewrites the border later in the same frame and a monitor peek at an arbitrary moment nearly always
catches the game's value. What the effect looks like on screen with the key held was not captured -
the screenshot in shots/ was taken after the key came up.
7. Three things about the rig that the next person needs
- VICE 3.7.1 has no
WarpModeresource but its monitor does have awarpcommand.testReport.mdonly triedresourceset "WarpMode"/"Warp"/"WarpModeEnabled", all rejected, and concluded every run stays in warp.warp on,warp offandwarp(query, answers "Warp mode is on./off.") all work. It matters more than it sounds, because-warpon the command line does not survive the autostart: the first run of this test, launched exactly like every run intestReport.md, sat for over eleven minutes at "loading under warp" and never reached the menu, while the same script with an explicitwarp onissued before the$0800breakpoint reached the options menu in about 50 seconds, twice in a row. VICE's own log saysAUTOSTART: aborting.a second or two in - this disk's fast loader leaves the KERNAL routines VICE is watching - and-autostart-warpturning warp off at that point is the obvious explanation, though that part is inference, not measurement. That is very probably the "90 seconds to over five and a half minutes" variationtestReport.mdrecorded.testHotkeys.pynow warps the loader explicitly and issueswarp offafterwards, so every key in this report was pressed against a machine running at true C64 speed. (Between the two traces in section 4 the emulated cycle counter advanced 73.8 M cycles = 72 s of NTSC C64 time, over a stretch of wall clock of the same order - not the hundreds of seconds warp would have produced.) - Do not stop the emulator while a key is held. A key pressed with
xdotool keydownand then left down across a monitor stop does not survive the stop. This was the difference between two otherwise identical runs: with Tab pressed first and the$E30Ccheckpoint armed during the resulting monitor stop, the checkpoint never fired; with the checkpoint armed first and Tab pressed after the machine was running again, it fired immediately (section 6). Anything that needs a key to be down while the CPU runs must arm its breakpoints first and press afterwards. Reads taken during a stop are still fine - they show what the held key already did. Alt_Lwedges the run. Holding Alt opens the GTK menu bar and takes a keyboard grab; everyxdotoolkey after that goes to the menu and the emulator sees nothing.testHotkeys.pyverifies key delivery before and after the sweep ([keys] plain C: lastHotkeyCode = $C3) so this failure mode announces itself instead of silently producing zeros.
8. What this does not say
- Still nothing on real hardware. This is VICE 3.7.1 with
-acia1mode 1 -acia1base 0xDE00 -acia1irq 1. - The "no carrier" DCD was injected, not emulated. VICE never raises 6551 status bit 6, so the
value was written into
aciaStatusSave$E5BCat a breakpoint. That is the exact byte the carrier sampler reads and the exact bit the source documents, but a real SwiftLink with a real peer dropping DCD has not been observed. - Only one machine. The peer was a TCP sink that never answers, so the link stayed in
connectionPhase$02; the override was not tested with two machines actually playing. - Only C= + C, C= + 2, C= + 3 and C= on its own were pressed. C= + H (DTR), C= + P (hang up), C= + A / C= + O, C= + RETURN and the other four baud keys are still untested.
- The disk-load suspend handshake, W65C51N boards and the
$DF00fallback remain untested, astestReport.mdalready said.
Files
| file | what it is |
|---|---|
testHotkeys.py |
the test: keymap sweep, override toggle, border checkpoint, forced-DCD traces, baud hot keys |
testLogs/hotkeyRun.txt |
the session this report cites, monitor traffic included |
testLogs/hotkeyKeymapSweep.txt |
the earlier boot whose sweep also tried Alt_L and Control_R |
shots/hotkeys01menu.png |
the title screen, five seconds after the loader finished |
shots/hotkeys02modemPrompt.png |
"GET OPPONENT ON PHONE AND..." - where the keymap sweep was done |
shots/hotkeys03linkOpen.png |
after A and space, with the link open and the module in phase 2: the game says "PHONE TROUBLE." |
shots/hotkeys04overrideOn.png |
the same screen with the carrier override on |
shots/hotkeys05borderHeld.png |
the screen the border checkpoint was taken on (the key is already released, so the border is the game's own colour) |