Skip to content
RetrogamingDeep Dive Published Updated 9 min readViews unavailable

Atari Lynx ComLynx UART: Shared-Line Framing, Timer 4, and Echo

Model ComLynx as a shared UART line with fixed framing, timer-4 baud generation, self-reception, status flags, and software-controlled turn taking.

ComLynx is not a packet network controller with hardware arbitration. The Atari Lynx exposes a UART inside Mikey and links its transmit and receive paths onto the shared ComLynx data line. Every console can hear its own transmission, there is no hardware handshake line to grant a sender the bus, and software must coordinate who talks and when. That architecture is central to understanding multiplayer timing, receive echoes, collisions, and lost bytes.

The Lynx hardware-reference project and the Atari Lynx programming guide document the UART register behavior. Together they describe the fixed frame shape, timer-4 baud generator, receive/transmit holding registers, serial status and control fields, and consequences of the shared cable wiring. The guide is a developer-authored technical reference rather than a newly published Atari service bulletin; physical timing claims should still be labeled as manual-derived unless measured on a console.

Electrical topology drives software rules

The ComLynx cable connects the Lynx’s receive and transmit lines together on the data path. A byte placed in a device’s transmitter therefore appears on the same connected line its receiver observes. This self-echo is a property of the interface, not a duplicate packet inserted by a game library. Software that expects only data from a peer must distinguish its own transmitted byte from peer data or include echo in its protocol state machine.

Multiple consoles share that one line. If two UARTs actively drive incompatible levels at once, the resulting signal can be corrupted and receivers can report a framing or parity error. The hardware does not provide CTS/RTS-style flow control or a token arbiter. A multiplayer protocol must define turn ownership, silence windows, collision recovery, and what happens when a participant resets or disappears. Do not claim that this cable implements a collision-safe Ethernet-like link.

The interface also gives no packet boundary beyond the UART character frame. A received byte is not automatically a game message. Protocol software must define message start markers or lengths, sender identities, checksums if desired, and resynchronization after malformed data. Game payload length and game-frame cadence are higher-level policies, not UART hardware features.

Fixed UART frame and bit order

The documented character frame contains a start bit, eight data bits, a parity bit, and a stop bit. Data is transmitted least-significant bit first. The start and stop bits have fixed values; a parity option selects Odd, Even, Mark, or Space behavior. There is no “no parity bit” frame format in this UART description, so every character consumes eleven bit periods. A receiver must sample each field in the expected sequence and report a framing or parity condition rather than silently dropping the error.

Because the payload is eight bits, a single frame carries one byte and costs eleven serial bit intervals before any software-level inter-frame idle time. At a fixed bit rate, throughput is consequently lower than the raw bit rate. When planning a multiplayer protocol, include the frame overhead, software turnaround time, and any self-echo bookkeeping. A queue that fills one byte per game frame may outpace the wire if the game sends multiple player states and acknowledgements.

Parity is a basic single-bit error check, not an integrity guarantee. It can detect some corruptions but not every possible multi-bit error, and it says nothing about message ordering, duplication, or stale data. If the game needs stronger integrity, it must define a checksum or sequence field at the protocol layer. Treat UART parity error, framing error, and game-packet validation as distinct signals.

Timer 4 controls transfer pacing

Mikey timer 4 is the serial timer used to set the UART bit timing. Its reload/countdown and frequency configuration determine the baud rate; peers need compatible settings or the receiver will sample the frame at the wrong point. The timer is thus part of the serial-device state, not just a generic gameplay timer that can be approximated by host time.

The programming guide derives transfer pacing from the Timer 4 configuration and the framing length. An emulator should implement the documented timer path in the same clock domain as the UART shift logic. Do not schedule a whole transmitted byte as one event at “eleven bit times from now” and skip intermediate line levels if software, another console, or a test harness can observe collisions or sampling boundaries. For debugging, log the timer underflow, each bit edge, the receiver’s sample phase, and the resulting UART flags.

All linked Lynx units need compatible serial timing. If one device runs a different timer period, the line still changes, but the receivers no longer agree on the center of each bit. This can look like wrong parity or random input when the root cause is simply inconsistent baud configuration. A useful network trace stores each unit’s timer 4 setup alongside every TX/RX event.

Holding registers, shift registers, and status

The transmitter has a holding register and a shift register. Software can write the next byte into the holding register while the current byte is already being serialized. The transmit-ready flag refers to availability of the holding register; transmit-empty indicates that both the holding and shift stages are empty. These states are not interchangeable. Writing a second byte as soon as the holding register accepts the first does not mean the line has finished sending the first.

The receiver has its own shift operation and holding register. Once a frame is assembled, the byte waits for software to read it. If another character completes before the first is consumed, the hardware reports overrun and data is lost or replaced according to the device behavior. Polling or interrupt-driven software therefore needs to service RX-ready before the next complete frame arrives. A missed receive-ready interrupt and an electrical collision are different defects and should be separately reported.

Serial interrupts can notify software when a byte arrives or the transmitter can accept another byte. Interrupts reduce polling overhead but do not create a larger FIFO or reliable packet buffer. A handler still needs bounded work: read the data register before overwrite, append to a software queue, and signal the higher-level protocol. If the game deliberately busy-waits, test its read/write cadence against the actual Timer 4 timing.

Control-register access has an important asymmetry: the value read back may report live status rather than the control bits last written. The guide recommends maintaining a software shadow when code needs to reuse its configured control value. Do not implement a read of the status/control register as “return the last write”; status flags and configuration shadow are separate data. This also affects debuggers: show the actual hardware-facing read result and a separate software-visible shadow if one exists.

Turn-taking and echo-aware message design

A robust ComLynx game protocol should assign a sender for each scheduled slot or use an explicit master/slave exchange. A sender should check that the shared line is idle if the interface exposes sufficient state, transmit its frame or packet, account for self-echo, and signal or wait for a defined turnaround before the next node speaks. The guide’s shared-line description rules out assuming that all players can send simultaneously without coordination.

Peer discovery should be conservative. A local loopback byte proves that the local UART can transmit and receive, but it does not prove that another Lynx is connected. A protocol can use a challenge/response marker and a timeout, then remove a peer after repeated missed responses. Timeouts need to be expressed in emulated serial time or game frames, not host milliseconds, so deterministic replays remain reproducible.

The receiver should track frame boundaries and byte assembly independently. A valid 11-bit UART character may belong to a truncated or stale higher-level message. Include sequence numbers or message IDs if the application protocol needs duplicate detection. Define behavior for a peer that resets mid-byte: the receiver may observe a framing error and then needs a resynchronization policy rather than treating the next arbitrary data byte as a complete game state.

A framing fixture for UART tests

The following Python function creates the bit sequence expected by a diagnostic UART test. It models the documented one-start, eight-data, one-parity, one-stop frame; it is not a replacement for Mikey register-level emulation or the ComLynx line driver.

def uart_frame(byte, parity="even"):
    if not 0 <= byte <= 0xFF:
        raise ValueError("UART payload must be one byte")
    if parity not in {"even", "odd", "mark", "space"}:
        raise ValueError("unsupported Lynx parity mode")

    data_bits = [(byte >> bit) & 1 for bit in range(8)]
    ones_odd = sum(data_bits) & 1
    parity_bit = {
        "even": ones_odd,
        "odd": 1 - ones_odd,
        "mark": 1,
        "space": 0,
    }[parity]
    return [0, *data_bits, parity_bit, 1]


frame = uart_frame(0xA5, "even")
assert len(frame) == 11
assert frame[0] == 0 and frame[-1] == 1
assert sum(frame[1:9]) % 2 == 0

This fixture checks serialization order, frame length, and parity selection. Separate tests should model receive sampling, parity/frame errors, line collisions, timer underflow, buffer flags, and loopback. For Mark and Space modes, validate against the chip documentation’s definition of the ninth bit rather than relying solely on a generic UART library’s parity enum.

Verification matrix

Start with one Lynx in loopback. Configure Timer 4 and each parity option, transmit a byte with both even and odd data-bit counts, and verify eleven bit intervals, LSB-first ordering, self-reception, TX-ready, TX-empty, RX-ready, and the read value. Check that reading a status register reports current flags instead of returning the software’s last control byte. Then send two bytes back-to-back to test holding-register handoff and receiver overrun behavior.

Next connect a second device. Give both peers matching timing and verify a request/response exchange with explicit turn ownership. Attempt simultaneous transmission in a controlled test and record the resulting line, parity/framing status, and recovery behavior. Disconnect or reset one peer during the start, data, parity, and stop portions of a frame to make sure the receiver reaches a known state again.

Test interrupt-driven and polled software paths. Delay the RX interrupt handler until just before the next byte arrives, then past the overrun boundary. Confirm a missing byte is reported as overrun rather than silently duplicated. Save states while the line is idle, mid-frame, between holding and shift registers, and with unread receive data; a restore should resume every bit edge and flag deterministically.

Finally test a multi-unit protocol with explicit addressing, timeouts, and reinitialization after one unit drops. Report the unit count and hardware/cable topology. Do not state a maximum simultaneous-player count unless the cited software and cable topology establish it; a physical ability to connect units is distinct from a particular game’s supported player count.

Acceptance criteria

A reliable ComLynx implementation models a shared physical line, self-echo, fixed UART framing, timer-4-driven bit timing, both buffering stages, status/control semantics, receive errors, and software turn-taking. It separates hardware byte transmission from the game’s message protocol and preserves all in-flight state in save states. With these details visible, multiplayer failures can be attributed to baud mismatch, buffer service, collision, frame parsing, or game policy rather than described vaguely as “link cable lag.”

Related:

Sources:

Comments