Skip to content
RetrogamingDeep Dive Published Updated 8 min readViews unavailable

Game Boy Link Cable Serial Transfer: Registers, Clocking, and Timeouts

Implement Game Boy link transfers with correct SB and SC semantics, clock ownership, CGB rates, interrupt handling, synchronization, and bounded failure recovery.

The Game Boy link port is a clocked serial interface, not a packet connection. A transfer shifts eight bits through two registers while one console supplies the clock. The same eight clock pulses send one byte in each direction, so every completed exchange is full-duplex even if a game uses only one of the received values. That compact hardware contract has consequences for games, emulators, and test tools: both participants must be ready, an externally clocked transfer can wait forever, and the cable does not provide message boundaries, acknowledgments, or error detection.

The central registers are SB at FF01 and SC at FF02. SB holds the next outgoing byte before a transfer. While clocks are running, it is also a shift register: the outgoing most-significant bit leaves first, and each incoming bit enters from the other side. After eight shifts, SB contains the byte received. Treat it as a live register during the transfer, not as an immutable transmit buffer.

Clock ownership determines whether a byte can finish

SC bit 0 selects the clock source. A value of 1 requests the internally clocked role, commonly called master; a value of 0 selects external clock, commonly called slave. SC bit 7 enables or requests a transfer. The master loads SB and writes SC with bit 7 and bit 0 set, conventionally $81. The externally clocked participant must also load SB and set bit 7, conventionally $80, before the master starts. If the peer has not prepared its next byte, the byte already in its serial register can be sent again.

DEF rSB EQU $FF01
DEF rSC EQU $FF02

; CGB/DMG-compatible normal-speed example: initiate one master byte.
; The peer must already have loaded SB and armed SC bit 7.
ld a, $A5
ldh [rSB], a
ld a, $81              ; transfer request + internal clock
ldh [rSC], a

This code only starts the exchange. It is not a complete driver: it does not wait, impose a deadline, interpret the received byte, or coordinate the next one. Keep those responsibilities in a serial state machine so game logic can continue while the cable is idle or a peer is late.

When the internal clock is selected, the documented base rate is 8192 Hz on monochrome mode. In CGB mode, SC bit 1 selects the faster clock: with that bit clear, the rates are 8192 Hz in normal speed and 16384 Hz in double speed; with it set, they are 262144 Hz and 524288 Hz respectively. The fast option is CGB-only. Do not expose it as a DMG capability merely because software can write the bit. Also do not assume an external clock follows those rates: an external source may run at another rate or with irregularly spaced pulses.

The exchange is not complete just because eight writes have elapsed in the emulated CPU. Each clock edge advances both shift registers. A correct emulator schedules the master clock, samples the peer’s outgoing bit, shifts both endpoints, and updates transfer state at the proper edge. After the eighth bit, it clears the transfer-start bit and requests the serial interrupt on each participating console. The guest can poll SC bit 7 instead, but when testing completion it should mask and inspect that bit only; other SC bits have separate meanings and model-specific read behavior.

Make transfers bounded and recoverable

An external-clock endpoint cannot make progress on its own. If the cable is absent, the peer is powered off, or no clock arrives, the transfer remains in progress indefinitely unless software defines a timeout. A production driver should therefore distinguish at least idle, armed, active, complete, and timed_out states. Associate each active transfer with a monotonic deadline, and let a frame counter, timer, or scheduler event enforce it. A raw CPU-loop count is a poor deadline because its elapsed time changes with clock mode, interrupts, and emulator scheduling.

Avoid a blocking wait that monopolizes the main loop. A game may need to keep rendering, accept input, update audio, and process a disconnect while serial hardware waits. A state machine can arm the endpoint, return to normal work, then consume the serial interrupt or check the masked start bit on a later update. On timeout, invalidate the pending transaction, report a link error to the application layer, and restore a known local state. Do not quietly treat an incomplete register value as a successful packet.

/* Scheduler-level pseudocode, not a drop-in register driver. */
void serial_begin(uint8_t tx, uint32_t now, uint32_t timeout_ticks) {
    serial.tx = tx;
    serial.deadline = now + timeout_ticks;
    serial.state = SERIAL_ACTIVE;
    SB = tx;
    SC = SERIAL_TRANSFER_ENABLE | SERIAL_INTERNAL_CLOCK;
}

void serial_update(uint32_t now) {
    if (serial.state != SERIAL_ACTIVE) return;

    if ((SC & SERIAL_TRANSFER_ENABLE) == 0) {
        serial.rx = SB;
        serial.state = SERIAL_COMPLETE;
    } else if (deadline_reached(now, serial.deadline)) {
        serial.state = SERIAL_TIMED_OUT;
        serial_abort_or_reinitialize();
    }
}

The pseudocode omits register declarations, interrupt atomicity, and platform-specific timeout arithmetic intentionally. In a real implementation, choose a wrap-safe deadline comparison for the width of the timer, synchronize access to shared state between the interrupt and main loop, and document whether timeout recovery can abort the hardware transfer or must wait for a safe reset. On an externally clocked target, a timeout is mandatory because no clock means no completion event.

Synchronize bytes, not just individual clocks

The master controls when the current byte runs, but it cannot guarantee that the external-clock endpoint has prepared the next byte immediately after completion. Insert a small, deliberate inter-byte delay or use a protocol that alternates which device supplies the clock. This gives the peer time to place its next byte in SB and arm SC bit 7. The required delay depends on the software, interrupt load, and model; do not present one universal cycle count without a measured target and workload.

The serial hardware moves bytes, not application messages. If a game exchanges records longer than one byte, define framing above SB/SC: sequence numbers, a length or fixed record size, and an integrity check where corruption matters. Specify which side initiates each byte and what each byte means. Both ends shift data at the same time, so use a documented filler byte when one direction has no payload. A value of $FF is often a reasonable idle pattern, but it is a protocol convention, not a hardware-defined “no data” marker.

Do not use a received $FF by itself as proof that a cable is disconnected. On a disconnected link, the master’s input reads high, so it can receive $FF, but valid peer data can also be $FF. Detect link health through an application-level exchange with an expected response, a sequence number, and a deadline. Treat that result as evidence about the protocol session, not as a direct electrical cable-presence signal.

Model disconnection behavior with appropriate scope

If a link is disconnected, a master begins reading ones on its input and can consequently receive $FF. If disconnection happens in the middle of a byte, the line transitions rather than necessarily changing at a perfectly clean byte boundary. Pan Docs records a pull-up interval of roughly 20 microseconds, while explicitly limiting that measurement to a CGB revision E. That is useful evidence for that tested revision, not a universal timing constant for every Game Boy-family device. An emulator or hardware test suite should keep the model and revision assumption visible instead of applying this analog observation indiscriminately.

The distinction between “no peer,” “peer sent $FF,” and “peer failed to clock” belongs partly to the application protocol. The serial register alone cannot resolve all three. Keep hardware completion and application validity as separate outcomes: the former means eight clocks completed, while the latter means the received frame passed the protocol’s checks.

Test the contract rather than a favorite game

A focused implementation test matrix should cover:

  • Master and external-clock roles, with both endpoints arming in the correct order.
  • Bit order, all-zero, all-one, alternating, and asymmetric transmit bytes in both directions.
  • Completion after exactly eight clocked bits, SC bit 7 clearing, and serial interrupt requests on both endpoints.
  • Each documented internal rate, including CGB double speed and the CGB-only fast setting.
  • No peer, a peer that powers off mid-transfer, a missing clock edge, irregular external clocks, and timeout cleanup.
  • Back-to-back bytes with and without an inter-byte preparation delay.
  • Disconnect before a byte and during a byte, with behavior tied to the tested hardware revision where timing claims are involved.

For emulator validation, compare register transitions and interrupt timing as well as final SB values. Exercise DMG and CGB modes separately, and do not infer exact electrical behavior from a software-only test. For physical testing, record console model/revision, clock mode, adapter or cable, and the conditions used to capture traces. The Mooneye suite and Pan Docs are useful references, but a passing test on one device revision is not proof that every electrical edge case is identical across the entire family.

The practical rule is simple: treat the cable as a clocked, one-byte-at-a-time peripheral. Prepare both endpoints, track ownership of the clock, preserve full-duplex semantics, wait without freezing unrelated work, and put bounded timeouts and message validation above the hardware. That separation makes the driver predictable and makes emulator discrepancies diagnosable instead of hiding them behind an instant byte copy.

Related:

Sources:

Comments