Boot-Time Control-Panel Serial Link

The first-stage bootloader in the Table Data ROM carries its own, complete control-panel serial driver — an interrupt-driven link over serial channel 1 with a ring-buffered packet layer, a handshake, a device probe and a device classifier. It is not the CPanel_* stack documented in Control Panel Protocol: that one lives in the Program ROM and only starts running after the bootloader has handed control to the main firmware.

Keep the two apart. They speak the same command vocabulary to the same panel MCUs, but they are two independent implementations with different state machines, different RAM layouts and different failure modes. Nothing measured on one transfers to the other. The source module says this in as many words (table_data/boot_cpserial.s:9-16).

Source of truth for this page (all byte-matching disassembly, table_data ROM):

module ROM range boot-time alias contents
table_data/boot_cpserial.s 0x9FEC6E-0x9FF228 0xFFEC6E-0xFFF228 setup, handshake, probe, polling half
table_data/boot_cpserial_isr.s 0x9FF229-0x9FF2F1 0xFFF229-0xFFF2F1 the three ISRs + state dispatch table
table_data/boot_cpserial_states.s 0x9FF2F2-0x9FFB2E 0xFFF2F2-0xFFFB2E state handlers, packet codecs, ring helpers

At reset the Table Data ROM is visible at 0xE00000-0xFFFFFF, so this code executes at its boot-time alias = ROM address + 0x600000. Labels in the disassembly carry the ROM (0x9Fxxxx) address; the 0xFFxxxx form is what the CPU actually runs and what the interrupt vector table stores.

Where it sits in the boot flow

The link is not brought up unconditionally. Boot_Init gates it on a floppy being present, and only a specific device answer diverts the boot into the firmware-update UI:

Boot_Init
    │
    ├── Boot_CheckDiskPresent (0xFFEC63) — Port D bit 6, active low
    │       │
    │       └── no disk ──> skip everything below, jump to the Program ROM
    │
    ├── BootSerial_InitVectorTable[0] (0xFFEC6E) ──> BootSerial_Init (0xFFEC7E)
    │       delays · BootSerial_FullInit · handshake · BootSerial_WaitDeviceIdent
    │
    ├── Boot_ProbeExternalDevice (0xFFED0E)
    │       BootSerial_ProbeSequence + Boot_ClassifyDeviceID ──> HL = device class
    │
    ├── class != 4 ──> jump to the Program ROM (normal boot)
    │
    └── class == 4 ──> firmware-update UI (display, prompt, Boot_PerformUpdate)

BootSerial_InitVectorTable is a 4-entry .long table of boot-time entry points; only entry 0 is ever fetched in this build, and the other three point at bare ret stubs.

Hardware

SFR address role
SC1BUF / SC1CR / SC1MOD / BR1CR 0xD4-0xD7 serial channel 1
PF / PFCR / PFFC 0x3C / 0x3E / 0x3F SC1 pin setup; shadowed in RAM at (0x0F66)/(0x0F67)
PE / PECR / PEFC 0x38 / 0x3A / 0x3B PE bit 5 is the link-busy status line
INTEAB / INTES1 0xE3 / 0xEB INTA and serial-1 interrupt enables
INTCLR 0xF8 0x12 = INTA, 0x22 = INTRX1, 0x23 = INTTX1
TAMOD 0xC8 timer-A baud source select

Port F bit 6 is the line-request / line-grant signal. A frame is armed by clearing PF bit 6 with PFCR bit 6 set; the next state clears PFCR bit 6 and samples PF bit 6 again — high means the line was granted and the pending INTTX1 continues the frame, low means arbitration failed and the machine resets to idle with status bit 1 set in (0x0F6A).

Three baud settings are used, the same three values the runtime driver uses: BR1CR = 0x14 while frame bytes move, 0x24 for the idle/turnaround states and 0x28 when a frame is being armed.

The three interrupt handlers

boot_cpserial_isr.s holds the whole interrupt half:

handler boot address role
Handler_INTA 0xFFF229 external interrupt A — the panel asks to send, and paces an RX transfer
Handler_INTTX1 0xFFF2AE SC1 transmit-buffer-empty — drives the TX states
Handler_INTRX1 0xFFF2D0 SC1 receive-buffer-full — drives the RX states

Handler_INTA has two arms. When (0x0F63) is zero the link is idle: it reprograms the SC1 pins for receive, arms state 0x20 and sets the RX-active flag. When a receive is already in flight it just steps the RX progress counter (reloading it with the 0x5C ring size when it hits zero) and records status bit 6. Both arms exit through the same INTCLR triple.

Both SC1 handlers push XWA/XHL/XIY, load the state byte and jump through the dispatch table; the state handlers jump back to BootSerial_TxIsrEpilogue or BootSerial_RxIsrEpilogue, which pop and reti.

The 11-entry state machine

BootSerial_StateDispatchTable (0xFFF282, 11 × .long boot-time addresses) is indexed by the state byte at RAM (0x0F62). The state byte is a raw table offset: it holds 0x00, 0x04, … 0x28 and is advanced or retreated with inc 4 / dec 4, so the ISR needs no scaling before the lookup.

state handler what it does
0x00 BootSerial_State_Abort idle/abort: sets done-abort bit 7 in (0x0F6A)
0x04 BootSerial_State04_TxLineRequest requests the line, then samples PF bit 6; on refusal resets to idle with status bit 1
0x08 BootSerial_State08_TxFirstByte pushes the first frame byte and derives the frame byte count
0x0C BootSerial_State0C_TxByteGap inter-byte turnaround (spin-10, pin restore, dummy SC1BUF write)
0x10 BootSerial_State10_TxNextByte pushes a further byte; loops back to 0x0C or advances to 0x14
0x14 BootSerial_State14_TxTail closing turnaround, two dummy SC1BUF writes
0x18 BootSerial_State18_TxFrameDone frame complete: re-arms the next frame if ≥ 2 bytes are still pending, else goes idle
0x1C BootSerial_State_Abort abort
0x20 BootSerial_State20_RxFirstByte stores the first received byte, derives the frame count
0x24 BootSerial_State24_RxNextByte stores further bytes; on the last one clears RX-active and returns to state 0
0x28 BootSerial_State_Abort abort

Frame length rule

Both State08 (TX) and State20 (RX) derive the per-frame byte countdown at (0x0F63) from the first byte of the frame, with the same rule:

count = 2                                  if (byte & 0x3F) <  0x30
count = (byte & 0x0F) + 3                  if (byte & 0x3F) >= 0x30

This is bit-for-bit the rule the runtime driver applies to CPANEL_PACKET_BYTE_COUNT (the “STATE_0_TO_17” calculation in Control Panel Protocol). Two independent implementations, one wire format.

Rings and transfer-control blocks

There are two levels of buffering: byte-level serial rings driven by the ISR, and packet-level transfer-control blocks driven by the codecs.

buffer base size indices
RX serial ring 0x0F79 0x5C bytes tail (0x0F75), head (0x0F77)
TX serial ring 0x0FD9 0x3C bytes send index (0x0FD5), pending count (0x0FD7)
RX transfer-control block 0x988A 0x80 bytes tail at base−8, head at base−4, free-slot count at base−2
TX transfer-control block 0x9914 0x80 bytes same layout

The (base−8 / −4 / −2) displacements are signed; the free-slot word is initialised to 0x0080 by BootSerial_FullInit and stepped by the parser and encoder as they fill and drain the ring.

Link state bytes worth knowing:

address meaning
0x0F62 state byte (raw dispatch-table offset)
0x0F63 per-frame byte countdown
0x0F64 flags: bit 0 RX active, bit 1 TX pending, bit 2 RX busy, bit 4 decode-collapse sentinel, bits 7:6 link mode
0x0F6A status/abort bits (bit 0 RX overflow, bit 1 TX arbitration failed, bit 3 frame discarded, bit 7 done/abort)
0x1022+ decoded-packet buffer (also the XOR-scramble state)

RX packet classes

BootSerial_RX_ParsePackets (0xFFF6F6) drains the RX serial ring into the RX transfer-control ring. Each frame is classified by (first byte & 0x38) >> 1 — the parser shifts right by one only, so the eight entries of BootSerial_RxPacketDispatchTable (0xFFF746) are 4 bytes apart and index directly.

class handler behaviour
0, 1 BootSerial_RxPkt_TwoByteScrambled both bytes stored, plus a third byte produced by exchanging into the 0x1022 scramble buffer and XORing with its old contents (3 control-ring slots)
2 BootSerial_RxPkt_TwoByteDecode first byte stored raw, then (C, A) handed to an external decoder hook
3, 4, 5 BootSerial_RxPkt_Discard both bytes consumed and dropped; status bit 3 set
6, 7 BootSerial_RxPkt_VarLengthRun run header’s low nibble gives (header & 0x0F) + 1 payload bytes; each is either externally decoded (run-tag bit 4) or XOR-mixed like class 0/1

Parsing stops when fewer than 4 free slots remain in the control ring, or fewer than 2 bytes are available in the serial ring.

★ Class-2 packets always fail on a stock machine

BootSerial_RxPkt_TwoByteDecode does not decode anything itself. It calls BootSerial_CallExternalDecode (0xFFF817), a register-saving wrapper whose whole body is:

BootSerial_CallExternalDecode:
        push    xde
        push    xiz
        push    xix
        calr    BootStub_ReturnError        ; ROM 0x9FB80E
        pop     xix
        pop     xiz
        pop     xde
        ret

And BootStub_ReturnError in the shipped ROM is two instructions:

BootStub_ReturnError:            ; ROM 0x9FB80E — bytes 33 FF FF 0E
        ldw     hl, 0xFFFF
        ret

The caller treats HL == 0xFFFF as decode failure: it takes the already-stored first byte back out of the control ring (BootSerial_CtrlRingRetreatIX), commits the serial tail and drops the packet. So every class-2 packet is discarded on a stock KN5000 — the hook exists for a decoder that this firmware does not contain. The same is true of the decode arm of the class 6/7 variable-length handler, which calls the same wrapper. The stub sits in a bank of five one-line stubs at 0x9FB802-0x9FB811 — four returning 0, this one returning 0xFFFF. That grouping looks like the residue of optional modules that were not linked into this build, but that reading is an interpretation; what the ROM proves is only that the hook resolves to a constant-error return.

This is worth remembering before attributing any boot-time panel misbehaviour to the decode path: on real hardware that path cannot succeed.

TX packet classes

BootSerial_TX_EncodePackets (0xFFF90E) is the mirror image: it reads packets from the TX transfer-control ring at 0x9914, classifies each by (tag & 0x30) >> 4 through BootSerial_TxPacketDispatchTable (0xFFF966, 4 entries), and emits frame bytes into the TX serial ring.

class handler behaviour
0, 1, 2 BootSerial_TxPkt_TwoByte plain 2-byte frame
3 BootSerial_TxPkt_VarLengthRun header plus (header & 0x0F) + 2 payload bytes

Handshake, probe and device classification

BootSerial_FullInit (0xFFED1E) resets both transfer-control blocks, programs the port F/E pin functions (keeping PFCR/PFFC shadows in RAM), sets SC1MOD = 0x00, BR1CR = 0x14, SC1CR = 0x01, enables INTA and the SC1 interrupts, selects the timer-A baud source, sends the opening frame (0x1F, 0xDA) and falls into BootSerial_HandshakeSequence (0xFFEDD2), which sends four more frames with 3000-iteration spin delays between them.

stage frames sent
BootSerial_FullInit 1F DA
BootSerial_HandshakeSequence 1F 1A, 1D 00, DD 03, 1E 80
BootSerial_ProbeSequence (0xFFF00E) 25 01, E2 04, 20 10, E2 11
BootSerial_WaitDeviceIdent (0xFFF077) 20 0B, repeated until the answer is stable
BootSerial_ResetAndIdent (0xFFF0FE) 2B 00, EB 00, 20 10, E3 10

Every one of these command bytes also appears in the runtime driver’s command reference — same panel MCUs, same vocabulary, different code.

BootSerial_WaitDeviceIdent derives an ident code from the decoded response byte at (0x102D): bit 7 → 0x0D, else bit 6 → 0x0E, else 0x0C, and loops until two consecutive polls agree (previous value kept at (0x1042)). This is the boot-time twin of the data wheel’s up/down/neutral encoding that the runtime driver reads from segment 0x0B.

Boot_ClassifyDeviceID (0xFFEF89) then inspects four bytes of the decoded-packet buffer, in this order:

test result
(0x1036) == 0x6C class 3
(0x1023) == 0x70 class 2
(0x1038) == 0x38 class 1
(0x1028) == 0x0F class 4 — flash-update service device
otherwise class 0 (none / unknown)

Only class 4 changes the boot path: Boot_Init compares L against 4 and runs the firmware-update UI on a match. What classes 1-3 correspond to physically is not established from the ROM alone — the classifier only tells us which byte of which response distinguishes them.

Dead and unreferenced code

A noticeable fraction of this driver is unreachable in the shipped firmware. The disassembly marks each case with an explicit “Callers: NONE FOUND” after searching the table_data and maincpu ROMs for both the ROM and boot-time address forms:

  • BootSerial_ModeSwitch, BootSerial_TestLoopback, BootSerial_SendTwoBytes_Bitbang (a manual bit-banged transmitter), BootSerial_PollTX and its sync-injection block, and several thunks — factory or diagnostic code.
  • BootSerial_UnusedIsrEpilogue (0xFFF5EA): a complete ISR tail with no vector, jump or fall-through reaching it — most likely the epilogue of a removed fourth handler.
  • BootSerial_CtrlRingRetreatIX_Dup: one of two byte-identical duplicate pairs of the ring-index helpers (the RX parser calls the first pair, the TX encoder the duplicates), which looks like two compiler instantiations of the same inline function.

A shared library at the tail

The last 259 bytes of boot_cpserial_states.s (ROM 0x9FFA2C-0x9FFB2E) are not part of the serial driver at all. They are a hardware-channel + memory library that appears three times in the KN5000 firmware:

copy address peripheral base
table_data bootloader 0x9FFA2C-0x9FFB2E 0x150000
maincpu program ROM (AudioMix_*) 0xEF17F4-0xEF18F6 0x150000
sub-CPU boot ROM (INIT_TONE_GEN, TONE_GEN_WRITE, …) 0xFF84A8-0xFF85AA 0x130000

The table_data and sub-CPU copies are byte-identical except for the three base-address immediates — 259 bytes, exactly 3 differing bytes, verified by direct comparison. The register protocol is the same in all three: A = (channel << 5) | 0x10 is written to base+0 as a register-address latch, then each data byte goes to base+2 with A incremented between bytes. Factoring these into a shared module is a recorded follow-up, not yet done.

Emulation status

There is no HLE of this driver in MAME today; the existing kn5000_cpanel device targets the runtime protocol. Because the bootloader only brings the link up when a disk is present, and only diverts on device class 4, a normal emulated boot never depends on it. Anyone who does implement it should note the two traps above: the state byte is a raw table offset (not a state index), and class-2 packets can never be decoded successfully by stock firmware.


See Also