You are asked to design the packet format for a new link between two chips. It must carry reads, writes and their responses. Someone suggests just sending the address and data. Within ten minutes of discussion the list of things the receiver needs to know has grown to: what kind of packet this is, how long it is, who it is for, which earlier request it answers, whether it arrived intact, and whether it is the next one in sequence.
What goes in a packet besides the payload, and where does each piece belong?
The obvious packet is pure data: address and value, nothing more. It breaks the moment the receiver needs to do anything but store it. It cannot tell a read from a write, cannot find the end of a variable-length payload, cannot match a response to its request, and cannot tell whether the bits arrived intact. Each missing piece forces either a separate wire (fine on chip, expensive off chip) or a field in the packet. Packet design is deciding which pieces the receiver needs, and where they go.
Part | Holds | Why it sits there |
|---|---|---|
Header (first) | Type, length, destination, tags, attributes | The receiver needs it before the payload: to route the packet, to know how long it is, and to decide what to do with it |
Payload | The data being moved | Opaque to the layer carrying it |
Trailer (last) | CRC or checksum; sometimes an end marker | It is computed over everything before it, so it can only be finished after the payload has gone out: putting it last lets the sender stream the packet without buffering it |
Two more choices shape every format:
Fixed or variable length. Fixed-size packets (or cells) make buffers and timing simple but waste space when messages are small. Variable-length packets fit the data but need a length field or a delimiter, and buffers sized for the largest packet.
Alignment and padding. Hardware reads fields fastest when they sit at fixed positions and on natural boundaries (a 32-bit field on a 32-bit boundary). Formats therefore pad fields and payloads to multiples of the bus width, trading a few wasted bits for much simpler logic.
DCLink, the small protocol you build through this course, starts here. Each packet is one 32-bit header flit (the unit sent per transfer) and 0 to 15 payload flits. The header's top four bits are the packet type (write, read, completion) and the next four are the number of payload flits. The receiver finds every packet boundary by reading the previous packet's length, the length-field framing of lesson 2.2. The model packs three packets into one stream, parses them back, then flips one bit in the first length field.
"""DCLink step 1: framing packets by a length field.
A packet is one 32-bit header flit followed by 0 to 15 payload flits. The header
carries the packet type in bits 31:28 and the number of payload flits in bits 27:24.
The receiver finds every packet boundary by reading the length of the one before.
"""
TYPES = {0: "WR", 1: "RD", 2: "CPL"}
def pack(ptype, payload):
assert 0 <= len(payload) <= 15
return [(ptype << 28) | (len(payload) << 24)] + list(payload)
def unpack(flits):
packets, i = [], 0
while i < len(flits):
hdr = flits[i]
ptype, n = hdr >> 28, (hdr >> 24) & 0xF
packets.append((TYPES.get(ptype, f"?{ptype}"), flits[i + 1 : i + 1 + n]))
i += 1 + n # the next header starts right after
return packets
stream = pack(0, [0xAAAA0001, 0xAAAA0002]) + pack(1, []) + pack(2, [0xBBBB0001])
print("stream:", " ".join(f"{f:08x}" for f in stream))
good = unpack(stream)
for t, p in good:
print(f" {t:3} with {len(p)} payload flits: {[f'{x:08x}' for x in p]}")
assert good == [("WR", [0xAAAA0001, 0xAAAA0002]), ("RD", []), ("CPL", [0xBBBB0001])]
bad = list(stream)
bad[0] ^= 1 << 24 # one bit flip in the first length field
print("same stream with one bit flipped in the first length field:")
for t, p in unpack(bad):
print(f" {t:3} with {len(p)} payload flits")
print("PASS: length framing recovers every packet; one bad length field misframes the rest")It prints:
stream: 02000000 aaaa0001 aaaa0002 10000000 21000000 bbbb0001
WR with 2 payload flits: ['aaaa0001', 'aaaa0002']
RD with 0 payload flits: []
CPL with 1 payload flits: ['bbbb0001']
same stream with one bit flipped in the first length field:
WR with 3 payload flits
CPL with 1 payload flits
PASS: length framing recovers every packet; one bad length field misframes the restThe good stream parses perfectly: 02000000 is a write (type 0) with 2 payload flits, 10000000 a read with none, 21000000 a completion with one. Now look at the corrupted run. One flipped bit turned the write's length from 2 into 3, so the receiver swallowed the read's header as payload, and the read disappeared without any error being reported. That is lesson 2.2's warning in miniature, and the reason DCLink will gain a CRC and sequence numbers in Part 10.
Choice | Option A | Option B |
|---|---|---|
Length | Fixed-size packets: simple buffers, wasted space for small messages | Variable length: efficient, needs a length or delimiter |
Header size | Small fixed header: low overhead, little room to grow | Larger or extensible header: room for features, more overhead on small packets |
Field placement | Natural alignment: simple, fast logic | Tightly packed: fewer bits, more shifting and muxing |
Integrity check | Trailer CRC over the whole packet | Separate header CRC so a bad header is caught before the payload arrives |
Reserved bits | None: every bit used today | Some reserved, must be zero: room for later versions (lesson 11.8) |
Protocol | Packet structure |
|---|---|
Ethernet | Preamble and start frame delimiter, then the frame (addresses, type or length, payload) and a CRC-32 frame check sequence at the end. |
PCI Express | A transaction-layer packet with a header, an optional data payload and an optional end-to-end CRC; the data link layer adds a sequence number in front and a CRC (LCRC) behind. Each layer adds its own header and trailer (lesson 0.7). |
PCI Express 6.0 | Packets are carried inside fixed 256-byte flits: 236 bytes of transaction-layer data, 6 bytes of data-link payload, 8 bytes of CRC and 6 bytes of FEC. |
A packet format puts its CRC in the header, computed over the payload, to keep all control fields together (illustrative). The sender must now compute the CRC before sending the first word, so it has to buffer the entire payload first. Latency rises by a full packet time, and the transmit buffer must hold the largest packet. The fix moved the CRC to a trailer, so the packet could stream out while the CRC was computed alongside. Field placement is not cosmetic: it decides buffering and latency.
Length framing has no check of its own: the receiver trusted the bad length, consumed the next header as payload, and every later boundary still looked plausible. Only a CRC or sequence number would expose it.
The receiver needs the type, length and destination to route and buffer the packet, and it needs them before the payload arrives.
Room for a later version to add a feature: old receivers ignore it, new ones use it, and old senders always send zero, so both stay compatible.
A packet is a header (what the receiver needs first), a payload, and a trailer (what can only be computed last, such as a CRC).
Fixed versus variable length and alignment versus packing trade simplicity against efficiency.
Length framing without a check silently misframes after one bad bit: DCLink will need a CRC.