Skip to content

Virtual AGC Integration

The Virtual AGC project provides a cycle-accurate emulator of the Apollo Guidance Computer — the onboard computer that navigated the spacecraft to the Moon and back. gr-apollo connects to this emulator, feeding it telemetry data exactly as the real spacecraft’s telecommunications system would have, and receiving commands exactly as the real uplink would have delivered them.

This connection closes the loop: you can run the actual Apollo flight software, receive its downlink telemetry through a realistic RF signal chain, and send it commands through the DSKY interface.

The Virtual AGC (yaAGC) is Ron Burkey’s emulator of the Apollo Guidance Computer, hosted at ibiblio.org/apollo. It executes the original flight software — the actual code that guided Apollo 11 to the lunar surface — instruction by instruction, using the AGC’s native 15-bit word architecture and fixed-point arithmetic.

The emulator exposes its I/O channels over TCP sockets. Each I/O channel corresponds to a hardware interface on the real AGC: the DSKY (display/keyboard), the inertial measurement unit, the rendezvous radar, the digital autopilot, and — most relevant here — the telecommunications system.

gr-apollo’s agc_bridge module connects to yaAGC’s TCP socket and speaks its 4-byte packet protocol, acting as the telecom hardware that sits between the AGC and the radio.

yaAGC communicates using a simple binary protocol: each I/O operation is a 4-byte TCP packet. The protocol encodes a 9-bit channel number and a 15-bit value, matching the AGC’s native I/O architecture (the real AGC had 512 possible I/O channels and used 15-bit words).

Each byte carries a 2-bit signature in its upper bits that identifies its position in the packet. This makes the protocol self-synchronizing — if the TCP stream gets misaligned, the receiver can scan for valid signature sequences to recover.

block-beta
    columns 8

    block:b0:2
        b0h["00"] b0d["Channel[8:4]"]
    end
    block:b1:2
        b1h["01"] b1d["Ch[3:1] | Val[14:12]"]
    end
    block:b2:2
        b2h["10"] b2d["Value[11:6]"]
    end
    block:b3:2
        b3h["11"] b3d["Value[5:0]"]
    end

    style b0h fill:#5c3a1a,stroke:#bd7a3a
    style b1h fill:#5c3a1a,stroke:#bd7a3a
    style b2h fill:#5c3a1a,stroke:#bd7a3a
    style b3h fill:#5c3a1a,stroke:#bd7a3a
    style b0d fill:#1a3a5c,stroke:#3a7abd
    style b1d fill:#1a3a5c,stroke:#3a7abd
    style b2d fill:#1a3a5c,stroke:#3a7abd
    style b3d fill:#1a3a5c,stroke:#3a7abd

The bit-level layout of each byte:

ByteBits 7-6Bits 5-0Content
000Channel bits 8-3High channel bits
101Channel bits 2-0 (in bits 5-3), Value bits 14-12 (in bits 2-0)Channel/value boundary
210Value bits 11-6Middle value bits
311Value bits 5-0Low value bits

The encoding in protocol.py follows the original C implementation from yaAGC/SocketAPI.c:

b0 = (channel >> 3) & 0x3F # signature 00 is implicit (bits 7-6 = 0)
b1 = 0x40 | ((channel & 0x07) << 3) | ((value >> 12) & 0x07)
b2 = 0x80 | ((value >> 6) & 0x3F)
b3 = 0xC0 | (value & 0x3F)

The parser validates the signature bits before extracting the channel and value, rejecting malformed packets:

if (b0 & 0xC0) != 0x00: raise ValueError(...) # must be 00xxxxxx
if (b1 & 0xC0) != 0x40: raise ValueError(...) # must be 01xxxxxx
if (b2 & 0xC0) != 0x80: raise ValueError(...) # must be 10xxxxxx
if (b3 & 0xC0) != 0xC0: raise ValueError(...) # must be 11xxxxxx

The AGC has hundreds of I/O channels, but only four are relevant to the telecommunications system:

ChannelOctalDecimalNameDirectionPurpose
DNTM103428Downlink Telemetry 1AGC -> GroundHigh byte of AGC word (bits 14-8)
DNTM203529Downlink Telemetry 2AGC -> GroundLow byte of AGC word (bits 7-0)
OUTLINK05747Digital DownlinkAGC -> GroundAdditional digital data

The AGC writes 15-bit words to these channels. The PCM system reads channels 034 and 035 at word positions 34 and 35 of each telemetry frame, encoding the AGC’s data into the serial bit stream that reaches the ground.

In gr-apollo, this path is reversed: the pcm_demux block extracts bytes from word positions 34 and 35 of decoded frames, and the downlink_decoder reassembles them into 15-bit AGC words.

AGCBridgeClient filters incoming packets to these four channels by default (the AGC_TELECOM_CHANNELS frozenset). Other AGC channels — the DSKY display driver, IMU CDUs, jet select logic — generate traffic on the socket but are not relevant to the telecom system.

The AGC uses 15-bit words internally. The PCM telemetry system carries 8-bit bytes. Each AGC word is split across two PCM channels:

block-beta
    columns 15

    block:high:7
        h1["bit 14"] h2["13"] h3["12"] h4["11"] h5["10"] h6["9"] h7["bit 8"]
    end
    block:low:8
        l1["bit 7"] l2["6"] l3["5"] l4["4"] l5["3"] l6["2"] l7["1"] l8["bit 0"]
    end

    style high fill:#5c3a1a,stroke:#bd7a3a
    style low fill:#1a3a5c,stroke:#3a7abd
  • DNTM1 (channel 034) carries bits 14-8 in the lower 7 bits of its byte. The MSB of the byte is unused.
  • DNTM2 (channel 035) carries bits 7-0 directly.

The reassemble_agc_word() function in downlink_decoder.py performs the join:

agc_word = ((dntm1_byte & 0x7F) << 8) | dntm2_byte

At the high PCM rate (50 frames/sec), one AGC word pair arrives every 20 ms. The DownlinkEngine accumulates 400 of these words (taking approximately 8 seconds) into a complete downlink buffer before attempting to decode the contents.

The AGC organizes its downlink telemetry into “lists” — structured snapshots of internal state tailored to the current mission phase. Each list type contains a different set of navigation variables, autopilot parameters, and system status words.

The list type is identified by the lower 4 bits of the first word in each 400-word buffer:

IDList NameWhen Used
0CM Powered FlightLaunch, translunar injection, course corrections
1LM Orbital ManeuversLM engine burns in lunar orbit
2CM Coast/AlignmentTranslunar and transearth coast, IMU alignment
3LM Coast/AlignmentLM IMU alignment and coast phases
7LM Descent/AscentPowered descent to lunar surface, ascent to orbit
8LM Lunar Surface AlignmentSurface operations, pre-liftoff alignment
9CM Entry UpdateAtmospheric reentry guidance

Each list type has a defined structure specifying what each word position within the 400-word buffer represents — things like the spacecraft’s position vector, velocity, gimbal angles, fuel remaining, guidance targets, and alarm status. Fully decoding these structures requires mission-specific knowledge (the word assignments changed between Apollo missions as the flight software was updated).

Ground-to-spacecraft commanding works through the DSKY (Display and Keyboard) interface. The ground station sends DSKY keycodes via the Up-Data Link, which delivers them to AGC channel 045 (INLINK). Each keystroke triggers the UPRUPT interrupt, and the flight software processes it exactly as if an astronaut had pressed the key.

The 15-bit INLINK word encodes the keycode in bits 14-10:

Bits: 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
[--- 5-bit keycode ---] [---------- data / zeros ----------]

Key DSKY keycodes (5-bit values in octal):

KeyOctalDecimalPurpose
VERB2117Begin verb entry
NOUN3731Begin noun entry
ENTER3428Execute / Proceed
RESET2218Key release / Error reset
CLEAR3630Clear current entry
0-920, 01-07, 10-11variousDigit keys
+3226Positive sign
-3327Negative sign

A typical ground command sequence — say, requesting program P63 (Braking Phase) via Verb 37 — looks like this:

sequenceDiagram
    participant GS as Ground Station
    participant UE as uplink_encoder
    participant AB as agc_bridge
    participant AGC as yaAGC

    GS->>UE: VERB 37
    UE->>AB: (ch 045, VERB keycode)
    AB->>AGC: 4-byte packet
    Note over AGC: UPRUPT fires
    UE->>AB: (ch 045, digit 3)
    AB->>AGC: 4-byte packet
    Note over AGC: UPRUPT fires
    UE->>AB: (ch 045, digit 7)
    AB->>AGC: 4-byte packet
    Note over AGC: UPRUPT fires

    GS->>UE: ENTER
    UE->>AB: (ch 045, ENTER keycode)
    AB->>AGC: 4-byte packet
    Note over AGC: UPRUPT fires,<br/>V37 N63 executes

    AGC-->>AB: DNTM1/DNTM2 words<br/>(telemetry response)
    AB-->>GS: Downlink PDUs

Each (channel, value) pair is sent as a separate 4-byte TCP packet. The AGC processes one word per UPRUPT, so multi-word sequences (like the 4-packet V37 ENTER sequence above) are sent in order. The AGCBridgeClient handles the TCP mechanics; the UplinkEncoder handles the DSKY encoding logic.

Putting it all together, the complete round-trip path from spacecraft to ground and back:

graph TB
    subgraph Spacecraft ["Spacecraft (yaAGC Emulator)"]
        AGC["Apollo Guidance<br/>Computer"]
    end

    subgraph RF ["RF Signal Chain"]
        SIG["USB Signal<br/>(2287.5 MHz)"]
    end

    subgraph Receiver ["gr-apollo Receiver"]
        PM["pm_demod"]
        SC["subcarrier_extract"]
        BPSK["bpsk_demod"]
        FS["pcm_frame_sync"]
        DM["pcm_demux"]
        DD["downlink_decoder"]
        BR["agc_bridge"]
        UE["uplink_encoder"]
    end

    subgraph Ground ["Ground Station"]
        DISP["Telemetry Display"]
        CMD["Command Entry"]
    end

    AGC -->|"ch 034/035<br/>DNTM1/DNTM2"| SIG
    SIG --> PM --> SC --> BPSK --> FS --> DM
    DM -->|"agc_data PDU"| DD
    DD -->|"400-word snapshot"| BR
    BR <-->|"TCP port 19697<br/>4-byte packets"| AGC
    BR --> DISP
    CMD --> UE -->|"DSKY keycodes"| BR
    BR -->|"ch 045 INLINK"| AGC

    style Spacecraft fill:#1a1a2e,stroke:#3a7abd
    style RF fill:#2e1a2e,stroke:#bd3a7a
    style Receiver fill:#1a2e1a,stroke:#3abd3a
    style Ground fill:#2e2e1a,stroke:#bdbd3a

The downlink path (top to bottom) is fully implemented: complex baseband samples go in, decoded telemetry PDUs come out. The uplink path (bottom to top) sends DSKY commands through the AGC bridge to the emulator.

The AGCBridgeClient handles the TCP connection to yaAGC as a background concern. It runs a receive loop in a daemon thread, automatically reconnects with exponential backoff if the connection drops, and delivers packets via callbacks.

Connection states:

StateMeaning
disconnectedNo TCP connection. Attempting to reconnect.
connectingTCP handshake in progress.
connectedActive connection. Reading packets, ready to send.

The backoff starts at 0.5 seconds and doubles up to a maximum of 30 seconds. This means if yaAGC is not running when gr-apollo starts, the bridge will keep trying without flooding the network with connection attempts.

The GNU Radio wrapper (agc_bridge block) exposes three message ports: uplink_data (input), downlink_data (output), and status (output). The status port emits the connection state string whenever it changes, which can be connected to a QT GUI label or logged for monitoring.

While gr-apollo works with the Virtual AGC emulator, others have gotten the real hardware running. CuriousMarc (Marc Verdiell), along with Mike Stewart, Ken Shirriff, and a team of volunteers, have restored actual Apollo Guidance Computers and S-Band telecommunications equipment to operational status. Their work provides invaluable validation that the specifications gr-apollo implements match the behavior of real flight hardware.

Of particular interest: Building an Apollo transmit station with Keysight instruments shows modern Keysight signal generators and analyzers driving the same S-Band uplink chain that gr-apollo models in software. Inside the WILD Lab of CuriousMarc is a Keysight-produced tour of the lab where the restoration work happens.