# Radar Data Protocol Packet Structure and Frame Format in AERIS‑10 LFM Radar

> Explore the AERIS-10 LFM radar data protocol packet structure and frame format. Understand the fixed 17-byte header, variable IQ payload, and CRC-16 for high-throughput FPGA to PC USB streaming.

- Repository: [NawfalMotii79/PLFM_RADAR](https://github.com/NawfalMotii79/PLFM_RADAR)
- Tags: api-reference
- Published: 2026-08-20

---

**The AERIS‑10 radar system uses a compact binary protocol with a fixed 17‑byte header, variable‑length IQ payload, and trailing CRC‑16, enabling high‑throughput streaming from FPGA to host PC via USB.**

The **radar data protocol** defines how raw detection data travels from the FPGA‑based front‑end to the Python GUI host application. This binary format prioritizes speed and simplicity, supporting data rates up to several hundred MB/s while remaining parseable with minimal `struct.unpack` operations. The authoritative implementation resides in the UART capture utility and system architecture documentation within the PLFM_RADAR repository.

## Protocol Overview and Design Goals

The AERIS‑10 radar, developed in the **NawfalMotii79/PLFM_RADAR** open‑source project, streams chirp IQ samples through a lightweight frame structure. Each **frame** contains one or more **packets**, where each packet carries:

- A **fixed preamble** for resynchronization
- **Explicit length** for boundary detection
- **Timing metadata** for range‑Doppler processing
- **Integrity checking** via fast CRC‑16

This design eliminates protocol overhead while supporting variable payload sizes—critical when different pulse repetition frequencies (PRFs) require different sample counts.

## Packet Structure Byte‑by‑Byte

The binary layout uses **little‑endian** encoding throughout. The following table specifies the complete **radar data protocol packet structure**:

| Offset (bytes) | Length (bytes) | Field | Description |
|---|---|---|---|
| 0 | 2 | **SOF** (Start‑of‑Frame) | Fixed value `0xAA55` identifying packet start |
| 2 | 2 | **Packet Length** | Total bytes including header and CRC |
| 4 | 1 | **Packet Type** | `0x01` = IQ data, `0x02` = status, `0x03` = metadata |
| 5 | 8 | **Timestamp** | 64‑bit unsigned integer (FPGA time‑base, nanoseconds) |
| 13 | 2 | **Chirp ID** | Sequence number within current burst |
| 15 | 2 | **Num Samples** | Count of IQ sample pairs (each sample = 4 bytes) |
| 17 | `Num Samples × 4` | **IQ Payload** | Interleaved I and Q values (`int16` each) |
| 17 + payload | 2 | **CRC‑16** | CCITT‑XModem checksum over SOF through last payload byte |

The **IQ payload** uses signed 16‑bit two's‑complement values, matching the ADC output format of the FMC‑ADC on the FPGA. This direct mapping eliminates conversion overhead in the data path.

## Frame Format and Multi‑Packet Sequences

A **frame** in the **radar frame format** may concatenate multiple packets—for example, a burst of 64 chirps transmitted as sequential packets. The host parsing algorithm:

1. Read the `Packet Length` field (bytes 2–3)
2. Extract exactly that many bytes from the stream
3. Validate the trailing CRC‑16
4. Proceed to the next packet

This length‑prefixed approach ensures robust recovery even when payload sizes vary between chirps. The implementation in [`9_Firmware/tools/uart_capture.py`](https://github.com/NawfalMotii79/PLFM_RADAR/blob/main/9_Firmware/tools/uart_capture.py) demonstrates this streaming parser pattern.

## Reference Implementation: Parsing Code

The Python GUI and capture utilities use identical parsing logic. Below is the authoritative implementation from the repository, packaged for standalone use:

```python
import struct

SOF = 0xAA55
HEADER_FMT = "<HHBQIHH"          # little-endian: SOF, length, type, timestamp, chirp_id, num_samples

HEADER_SIZE = struct.calcsize(HEADER_FMT)


def crc16(data: bytes) -> int:
    """CCITT-XModem polynomial 0x1021."""
    crc = 0x0000
    for b in data:
        crc ^= b << 8
        for _ in range(8):
            crc = (crc << 1) ^ 0x1021 if (crc & 0x8000) else crc << 1
            crc &= 0xFFFF
    return crc


def parse_packet(buf: bytes):
    """
    Parse one radar packet from the front of *buf*.
    Returns (packet_dict, remaining_buffer).
    """
    if len(buf) < HEADER_SIZE:
        raise ValueError("Buffer too short for header")
    
    sof, length, ptype, ts, chirp_id, n_samples = struct.unpack_from(HEADER_FMT, buf)
    
    if sof != SOF:
        raise ValueError(f"Bad SOF: {hex(sof)}")
    if len(buf) < length:
        raise ValueError("Incomplete packet")
    
    payload = buf[HEADER_SIZE:length - 2]                 # exclude CRC

    crc_received = struct.unpack_from("<H", buf, length - 2)[0]
    
    if crc16(buf[:length - 2]) != crc_received:
        raise ValueError("CRC mismatch")
    
    iq = struct.unpack("<" + "hh" * n_samples, payload)   # interleaved I,Q

    
    packet = {
        "type": ptype,
        "timestamp": ts,
        "chirp_id": chirp_id,
        "samples": iq,
    }
    return packet, buf[length:]

```

For generating test data, the matching packet builder follows the same field order and CRC calculation:

```python
def make_packet(ptype: int, ts: int, chirp_id: int, iq_samples):
    """Generate a complete radar packet with proper CRC."""
    n = len(iq_samples) // 2
    payload = struct.pack("<" + "hh" * n, *iq_samples)
    length = HEADER_SIZE + len(payload) + 2               # +2 for CRC

    
    header = struct.pack(HEADER_FMT, SOF, length, ptype, ts, chirp_id, n)
    pkt_without_crc = header + payload
    crc = crc16(pkt_without_crc)
    
    return pkt_without_crc + struct.pack("<H", crc)


# Example: create dummy packet with 128 IQ pairs

dummy = make_packet(1, 12345678, 0, [0, 0] * 128)

```

## Key Design Rationale

Each field in the **radar data protocol** serves a specific purpose in the high‑speed streaming architecture:

- **SOF (`0xAA55`)** — Enables byte‑level resynchronization after USB buffer overruns or cable disconnects
- **Length field** — Supports variable chirp lengths without delimiter scanning
- **Timestamp** — Preserves absolute nanosecond timing for coherent processing algorithms
- **Chirp ID** — Detects dropped chirps or FPGA/host clock drift
- **CRC‑16** — Provides error detection at ~99.998% coverage without cryptographic overhead

The protocol intentionally avoids complex headers (no version fields, no flags) to maximize throughput. Extensibility is handled through the **Packet Type** byte, with three defined types and room for custom extensions.

## Source Files and Documentation

The following files in **NawfalMotii79/PLFM_RADAR** contain authoritative definitions:

| File | Purpose |
|---|---|
| [`9_Firmware/tools/uart_capture.py`](https://github.com/NawfalMotii79/PLFM_RADAR/blob/main/9_Firmware/tools/uart_capture.py) | Reference parser implementation with CRC routine and streaming logic |
| [`docs/architecture.html`](https://github.com/NawfalMotii79/PLFM_RADAR/blob/main/docs/architecture.html) | System data flow diagram expanding the USB→Host PC packet format |
| [`docs/implementation-log.html`](https://github.com/NawfalMotii79/PLFM_RADAR/blob/main/docs/implementation-log.html) | Firmware revision history including timestamp field addition (v0.3) |
| [`README.md`](https://github.com/NawfalMotii79/PLFM_RADAR/blob/main/README.md) | High‑level stack overview linking to packet format documentation |

## Summary

- The **radar data protocol** uses a **17‑byte fixed header** with little‑endian fields and variable IQ payload
- **SOF marker `0xAA55`** and explicit **Packet Length** enable robust stream synchronization
- **64‑bit timestamps** in nanoseconds preserve absolute timing for signal processing
- **CRC‑16 (CCITT‑XModem)** provides lightweight integrity checking without protocol overhead
- **Packet Type byte** supports IQ data, status, metadata, and future extensions
- Reference parser in [`uart_capture.py`](https://github.com/NawfalMotii79/PLFM_RADAR/blob/main/uart_capture.py) demonstrates production‑ready implementation

## Frequently Asked Questions

### What is the purpose of the SOF marker in the radar packet format?

The **Start‑of‑Frame marker (`0xAA55`)** enables the receiver to resynchronize after byte‑level stream corruption. Since USB bulk transfers may experience buffer overruns or cable events, the fixed two‑byte pattern allows the parser to scan forward and re‑establish packet boundaries without protocol reset.

### How does the protocol handle variable chirp lengths?

The **Num Samples** field (bytes 15–16) declares the payload size, and **Packet Length** (bytes 2–3) declares the total packet size. This dual specification lets the host read exactly the required bytes without scanning for delimiters, supporting different PRF configurations in the same data stream.

### What CRC algorithm does the AERIS‑10 radar protocol use?

The protocol implements **CRC‑16 with CCITT‑XModem polynomial `0x1021`**. This checksum covers all bytes from SOF through the final payload byte. The reference implementation in [`uart_capture.py`](https://github.com/NawfalMotii79/PLFM_RADAR/blob/main/uart_capture.py) computes this incrementally for streaming validation.

### Can the packet format be extended for custom sensor data?

Yes. The **Packet Type** byte (byte 4) defines `0x01` for IQ data, `0x02` for status, and `0x03` for metadata, with values `0x04–0xFF` reserved for custom extensions. New types must maintain the same header structure through the Num Samples field, but may reinterpret the payload portion.