PROTOCOLPACKETCCSDS 133.0-B-2

Space Packets

An envelope for meaning on a shared channel.

A CCSDS network-layer protocol data unit carrying a 6-octet primary header (11-bit APID, 14-bit sequence count, 16-bit length-minus-one) followed by an optional secondary header and a variable user data field. Identifies, orders, and delimits application data on a shared space link; reliability and routing live elsewhere.

Imagine a spacecraft with twelve onboard processes.

One watches battery voltage. One tracks temperature. One estimates attitude. One controls reaction wheels. One collects images. One records faults.

Every second, each of them produces data.

Now imagine there is only one downlink. Not twelve downlinks. One. The spacecraft cannot send twelve independent streams to Earth — everything has to be mixed together before transmission. By the time the data reaches the ground it looks something like this:

8A 4C 02 11 ... 7D 90 00 0A ... 1F 22 A1 07 ...

Just bytes. Some came from the thermal system. Some came from the attitude system. Some came from the camera. The ground has a problem: it needs to separate that mixed stream back into the individual conversations that produced it. The thermal engineer wants temperatures. The attitude engineer wants orientation estimates. The payload team wants imagery. The fault monitor wants event logs. The spacecraft sent one stream; the ground needs many.

That is the problem the Space Packet Protocol solves.

Its job is surprisingly modest. It does not know what a temperature is. It does not know what a battery is. It does not know what an image is. It simply answers four questions about every chunk of bytes that crosses the link: who produced these bytes, what order were they produced in, how many bytes belong together, and is this packet complete or part of something larger? Everything else lives elsewhere.

This is the first important idea. The Space Packet Protocol is not a telemetry format, not a database schema, not a measurement description language. It is a typed envelope moving through a shared system. The protocol carries identity, order, and size. Meaning comes later. That distinction is the reason the protocol survived for forty years.

To see why, let’s follow a single packet from a reaction wheel controller on the spacecraft to an engineer sitting at a console on Earth.

Who is talking

The reaction wheel controller writes a few bytes — its current speed, its temperature, a status flag. Before it puts those bytes on the link, it has to label them, because otherwise the ground cannot tell whose bytes they are.

So the protocol gives every onboard process a number. The reaction wheel controller has one. The thermal monitor has another. The imager has a third. The number is called an APID — Application Process Identifier — and it is eleven bits wide, which means 2048 possible producers on the spacecraft.

That number goes in the header of every packet the process sends. Nothing else identifies the source. No name, no string, no type. Just eleven bits, because every bit in the header is paid for, on every packet, forever, and eleven was enough.

NASA had been using eight. CCSDS stretched it to eleven, because that was the largest field the existing flight software could absorb without major rewrites. A number agreed in a committee room in 1986 — and still uncrowded forty years later.

APID field
demux · 3 streams
spacecraft →→ ground
thermalAPID 0x010
wheelsAPID 0x041
imagerAPID 0x0A3
thermalAPID 0x010
wheelsAPID 0x041
imagerAPID 0x0A3
DEMUXread APID
Each producer stamps its packets with an APID. The downlink carries one byte stream; the demultiplexer on the ground reads the APID on every packet and routes it back to the right channel — three sources demuxed from one wire.

The field that isn’t there

Notice what is not in the header. The packet does not say where it is going. There is no destination field.

This is not an oversight. The spacecraft sends to one place: the ground. The ground sends to one place: the spacecraft. The link is point-to-point — both ends already know each other. A destination field would carry the same value in every packet forever. Bytes that say nothing. So the protocol drops it.

What it keeps is the source, because that is the only ambiguity the receiver has to resolve. Earth-based protocols carry both source and destination because they route through many hops. Space Packets do not route. They cross one gap. The address is whoever sent it; the recipient is whoever is listening.

Order

The reaction wheel controller writes a packet every hundred milliseconds. Sometimes one of those packets is lost — a frame drops, a scheduler misses a deadline, the receiver gets corrupted bytes and discards them. The ground needs to know.

So every packet carries a counter. The producer increments it once per packet; the ground compares each new value to what came before. If the new value is two higher than expected, one packet was lost. If it is fifty higher, fifty were. The counter is fourteen bits wide and wraps at 16384.

Now the real question: whose counter?

The naive choice is one counter shared by every onboard process. One number for the whole link. Try it.

Counter scope
awaiting HK
producers →→ HK gap detector
HKAPID 0x020
PayloadAPID 0x080
0x020#10x080#10x080#20x080#30x080#40x080#50x080#60x080#70x020#2
HK GAP DETECTOR
last HK
verdict
Each APID owns its own counter. HK's two packets are stamped #1 and #2; the payload burst is invisible to HK's gap detector. Same fourteen bits in the header, same wire format — the scoping change costs nothing and decouples every producer from every other.

A bursty producer — say, the imager — fires off eight packets in a row, and the shared counter jumps by eight. The next packet from the slow thermal monitor arrives with a counter value far above its previous one. The gap detector fires: forty-three packets missing! But nothing was lost. The counter just belongs to somebody else.

Per-source counters cost exactly the same number of bits — fourteen, in the same field — and they remove the false alarm entirely. Each producer has its own monotonic count, and a burst on one stream is invisible to another.

This is the recurring architectural move in space systems. Per-source independence instead of shared state. Virtual channels do it at the link layer. Custody does it in DTN. The Space Packet sequence counter does it here. It costs nothing extra and decouples everything.

Size

The reaction wheel packet carries a few bytes. The image packet carries thousands. The protocol has to handle both, so every header carries a length field, sixteen bits wide. The smallest detail of this field is also the strangest: it encodes length minus one. A value of 0 means a one-byte payload, 1 means two bytes, and so on up to 65536 bytes total.

Why minus one? Try it.

Stream
A desynced · B in lock
shared byte stream →→ two parsers, same bytes

Bytes on the wire · LEN cells outlined

03A1A2A302B1B200C1C2C3C402D1D2
A · byte-countdesync

LEN = payload bytes

pkt#1 · 3Bpkt#2 · 2B✗ LEN=00 · scan…

saw LEN=0 → zero-length packet is illegal → desync

B · length − 1in lock

LEN = payload bytes − 1

pkt#1 · 4Bpkt#3 · 1Bpkt#4 · 3B

saw LEN=0 → read it as 1-byte payload → continue

One 0x00dropped into a LEN cell. Parser A reads it as a zero-length packet — forbidden — and has to desync and scan for the next valid boundary, losing the corrupted packet. Parser B reads it as “one byte follows,” consumes one byte, and stays in lock. The minus-one encoding outlaws the useless case and survives the most common corruption pattern in the same gesture.

Two parsers, same byte stream. One reads the length field as a plain byte count. The other subtracts one. Now drop a 0x00 into a length field — all-zeros is by far the most common form of bit corruption.

The plain-count parser sees “zero bytes follow.” Forbidden. It has to desync and scan for the next valid boundary. The minus-one parser sees “one byte follows.” Reads one byte. Carries on. Stays in lock.

One off-by-one outlaws the useless case — a zero-length packet — and survives the most common corruption pattern. Two benefits for one bit of cleverness.

Whole or part

Most of the time, one packet is one complete thing. The reaction wheel packet is a single record; the temperature packet is a single sample. But sometimes a packet is not complete — an image is too big for one packet and has to be split across several. The protocol needs to tell the ground: this is segment N of something larger, here is the beginning, here is the end.

Two bits do this.

Scenario
1 delivered · 0 in buffer
incoming packets →→ reassembler · two booleans

queue

01firstIMG-A1
00continuationIMG-A2
00continuationIMG-A3
10lastIMG-A4

reassembler

HIGHreset buffer
LOWdeliver buffer
reading01

buffer

(empty)

delivered units

IMG-A1
Four packets reassemble into one image. 01 resets the buffer, 00 appends, 10delivers — every packet runs the same two boolean checks. No special case for “is this the start” or “is this the end”: the bits answer both.

Stare at the four values:

01  first segment
00  continuation
10  last segment
11  standalone

Now stare at them differently. The high bit means “starts a new unit.” The low bit means “ends a unit.” A standalone packet — one packet, one complete unit — is encoded as 11 because it is simultaneously its first and last segment.

The reassembler does two boolean checks on every packet. High bit set? Reset the buffer. Low bit set? Deliver the buffer. No special case for standalone, no special case for unsegmented; the most common case (one packet, one unit) falls out for free from the segmentation logic. The committee that designed this in 1986 could have picked any four values. They picked the one where the state machine has no edge cases.

The escape hatch

The header is six bytes. Forty-eight bits. That covers identity (version, type, APID), order (sequence count, segmentation), and size (length). Nothing else.

No timestamp. No service ID. No CRC. No mission tag. No format version. No subsystem ID. The committee considered all of those — many agencies wanted them — but none made it into the primary header. And yet every mission also needs at least some of them.

So the header carries one final bit: the secondary header flag. If set, another header follows the primary one, and its format is mission-defined. PUS specifies one layout. NASA missions use others. CCSDS itself does not care, as long as the bit is set when extra header is present.

This is not a beautiful design. It is a political truce. The committee could not agree on what belonged in the primary header, so they put the disagreement behind a single bit and shipped. That bit is the reason the protocol survived. Every mission could extend it without forking it. The wire format stayed the same. The differences moved into local agreements and dictionaries on the ground.

A field with two lives

The first three bits of the header are a version number. Three bits, eight possible versions, and every Space Packet ever flown has used version 000. In any other protocol that would be the end of the story — a forward-compatibility hook the committee left for itself and never used.

But Space Packets had a second life.

Decades later, CCSDS needed to carry payloads that did not fit the Space Packet shape: IP packets, DTN bundles, things with their own addressing. They could have extended Space Packets. They didn’t. They invented a separate format — the Encapsulation Packet — with a completely different header, and they marked it with version 111.

Now the same downlink can carry both, interleaved.

Version field
routed · 4 sp / 3 ep
incoming frames →→ demux on first 3 bits
linkopaque frames
000HK · APID 0x020111IPv6 · ssh→relay000Img · APID 0x080000AOCS · APID 0x010111DTN bundle000HK · APID 0x020111IPv6 · NTP
ROUTERread 3 bits
000Space Packet parser4 frames
111Encapsulation parser3 frames
The version field was meant as a forward-compatibility hook nobody expected to use. In practice, it became the discriminator between two coexisting protocols sharing the same downlink. Three bits — read before anything else — fan each frame to the right parser.

Every frame the link layer delivers starts with three bits. 000 means Space Packet — route to the Space Packet parser. 111 means Encapsulation Packet — route to the Encapsulation parser. A field intended as a forward-compatibility hook became an in-band multiplexer between two coexisting protocols. The forty-year-old field is doing a job nobody designed it to do.

Silence is not allowed

The link layer wants a continuous stream of bytes. Modulators do not like to start and stop. Frame synchronisers want a constant rhythm. The receiver on Earth tracks the phase of the carrier, and if the signal vanishes for a few seconds, lock is lost — the next real packet costs several seconds of re-acquisition.

But the spacecraft does not always have something to say. The thermal monitor samples once per second; between samples, there is nothing real to send. Two bad options sit on the table. Stop transmitting, and the receiver loses lock. Lie and emit a fake telemetry packet, and the ground tries to decode it — alarms fire on noise.

The protocol’s answer is the idle packet: a packet with a reserved APID, 0x7FF, all bits set, that says I am fill, ignore me. It has a payload, the payload is arbitrary, and the ground demuxes the packet, sees the reserved APID, increments a fill counter, and drops it. The link stays busy. The decoder stays in lock. Nothing real is corrupted.

Between bursts
lock 100%
downlink wire →→ receiver lock

continuous byte stream · last 56 ticks

warming up…

real packet idle 0x7FF silence

0 real0 idle0 silentt = 0

receiver

100500
100%carrier lock
Between real packets the protocol emits an idle packet with reserved APID 0x7FF. The ground demultiplexer sees the reserved APID and drops it. The bit stream stays continuous, the receiver stays in lock, and nothing real is corrupted.

The same APID field that identifies producers also reserves one slot to mean this packet is nothing. One eleven-bit field, doing two jobs. Every protocol that sits above a synchronous channel ends up needing some version of this.

All six bytes, all at once

Here is what the reaction wheel controller writes, every hundred milliseconds:

Version   3 bits   000
Type      1 bit    0       (telemetry, downlink)
Sec.hdr   1 bit    0       (no secondary header)
APID     11 bits   0x041   (this controller's ID)
Seg.flag  2 bits   11      (standalone)
Seq.cnt  14 bits   incremented every packet
Length   16 bits   payload size − 1

Forty-eight bits. Six bytes. Followed by the actual payload — the wheel’s speed, temperature, status. Then the next packet from the thermal monitor. Then a packet from the imager. Then an idle filler. Then another reaction wheel packet. All interleaved on the same downlink.

Downlink
no gaps · all streams in lock
producers →→ demux by APID
DEMUXread APID
AOCS0x10
HK0x20
Imager0x80
Three producers share one link. Each stamps its packets with an APID and its own sequence counter. The link layer interleaves them blindly; the ground demuxes by APID into three independent streams. Drop a packet to see the gap appear in only one stream — the rest stay in lock.

The link layer carries opaque bytes; it does not know who sent what. On the ground, the demultiplexer reads the first three bits, finds the APID, looks up the packet structure in a dictionary, and delivers the bytes to the right consumer. The consumer reads its sequence counter, checks for gaps, parses its payload. Producers do not know about each other. Consumers do not know about each other. The link layer does not know about any of them. The APID is the entire glue.

The dictionary is the protagonist

The header carries the APID. It does not say what the APID means. It does not say that APID 0x041 is the reaction wheel controller, or that byte 4 of the payload is the wheel speed, in revolutions per minute, as a two-byte unsigned integer. That information lives elsewhere — in the dictionary.

The dictionary lives on the ground. XTCE, EDS, ESA’s MIB, or a mission-specific spreadsheet. It maps each APID to a packet layout, a set of calibrations, alarm thresholds, plotting hints, archive policies. The packet is just the carrier. The meaning lives in the dictionary.

If you ever feel like you want to put more in the Space Packet header, you are usually trying to skip the dictionary. Maintain the dictionary. The protocol’s smallness depends on it.

What aged well, and what didn’t

The Space Packet Protocol shipped in the mid-1980s, was standardised by CCSDS in 1987, and remains the universal payload of the link layer across every major space agency four decades later. Almost no other protocol from that era has aged this well.

What aged well: source-only addressing held, because point-to-point links remained the default for thirty years. The eleven-bit APID has never run out. Variable length was right — fixed-size packets would have wasted enormous fractions of the link. The secondary-header escape hatch absorbed every mission-specific extension without forking the format. And the dictionary-on-the-ground architecture turned out to be a deep alignment with how telemetry is actually consumed.

What aged poorly was the point-to-point assumption. The moment space became a network — relay orbiters, constellations, the Interplanetary Internet — the missing destination became a real problem. The fix was not to break Space Packets, which would have orphaned thirty years of mission software, but to wrap them, in Encapsulation Packets and Bundle Protocol payloads. The 65-kilobyte maximum size is the other mild bruise: generous in 1987, tight today, when a single hyperspectral line might be megabytes. The fix, again, is not to widen the field but to push large data through CFDP, which segments transparently and lets Space Packets carry the segments.

The protocol survived by refusing to grow. Everything it did not do got pushed up the stack. Everything it did do, it did with six bytes.

This protocol lives inside

The first signal