# USB Communication Protocol Between Python GUI and FPGA via FT2232H Explained

> Learn the USB communication protocol for your Python GUI and FPGA using the FT2232H. This guide explains byte-stream protocols and packet exchange for efficient data transfer.

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

---

**The PLFM_RADAR project uses a custom byte-stream protocol over an FTDI FT2232H USB 2.0 bridge to exchange 11-byte data packets, 26-byte status packets, and 4-byte configuration commands between an onboard FPGA and a Python/PyQt GUI, with all framing and parsing logic centralized in [`9_Firmware/9_3_GUI/radar_protocol.py`](https://github.com/NawfalMotii79/PLFM_RADAR/blob/main/9_Firmware/9_3_GUI/radar_protocol.py).**

The `NawfalMotii79/PLFM_RADAR` repository implements the AERIS-10 radar system, where a single-board FPGA handles real-time signal processing and streams raw samples to a desktop application. The USB communication protocol between Python GUI and FPGA via FT2232H defines every header, footer, and payload structure required to keep the radar acquisition pipeline synchronized. Developers who want to extend the command set, integrate alternative hardware, or debug data corruption must understand the exact packet format used in [`radar_protocol.py`](https://github.com/NawfalMotii79/PLFM_RADAR/blob/main/radar_protocol.py) and its accompanying worker threads.

## Physical Layer – FT2232H Synchronous FIFO Bridge

The interface relies on an **FTDI FT2232H** dual-channel USB 2.0 Hi-Speed FIFO bridge (VID **0x0403**, PID **0x6010**). Channel A operates in **245 Synchronous FIFO** mode, providing a deterministic byte stream with minimal latency between the FPGA fabric and the host PC.

In [`9_Firmware/9_3_GUI/radar_protocol.py`](https://github.com/NawfalMotii79/PLFM_RADAR/blob/main/9_Firmware/9_3_GUI/radar_protocol.py), the `FT2232HConnection` class wraps the low-level USB session. The constructor exposes a `mock` parameter so developers can instantiate either a real hardware driver or a pure-Python test double that exposes the same `read` and `write` interface.

```python
from radar_protocol import FT2232HConnection

# Real hardware

ft = FT2232HConnection(mock=False)
ft.open(device_index=0)

# Mock for unit tests

ft_mock = FT2232HConnection(mock=True)
ft_mock.open()

```

According to the PLFM_RADAR source code, the `FT2232HConnection.__init__` and `open` methods that handle this setup are found around lines 22–55 of [`radar_protocol.py`](https://github.com/NawfalMotii79/PLFM_RADAR/blob/main/radar_protocol.py).

## Packet Format and Protocol Constants

The protocol defines three fixed-length packet types. Their layout constants are declared near the top of [`radar_protocol.py`](https://github.com/NawfalMotii79/PLFM_RADAR/blob/main/radar_protocol.py) (lines 38–45).

- **Data packets** (FPGA → Host): 11 bytes, starting with header `0xAA` and ending with footer `0x55`. The payload carries range Q (2 bytes), range I (2 bytes), Doppler I (2 bytes), Doppler Q (2 bytes), and a detection flag (1 byte).
- **Status packets** (FPGA → Host): 26 bytes, starting with header `0xBB` and ending with footer `0x55`. The payload contains six 32-bit status words in big-endian format.
- **Command packets** (Host → FPGA): 4 bytes with no header or footer. The byte layout is `{opcode, addr, value_hi, value_lo}` in big-endian order, matching the FPGA’s `usb_cmd_opcode` decoder.

The shared magic bytes and sizes are exposed as module-level constants:

```python
HEADER_BYTE = 0xAA
STATUS_HEADER_BYTE = 0xBB
FOOTER_BYTE = 0x55
DATA_PACKET_SIZE = 11
STATUS_PACKET_SIZE = 26

```

### Parsing Data Packets

`RadarProtocol.parse_data_packet` (lines 78–116) validates the header and footer, slices the unsigned 16-bit fields, and converts them to signed integers via the internal `_to_signed16` helper. The returned dictionary contains keys such as `range_i`, `range_q`, `doppler_i`, `doppler_q`, `detection`, and `frame_start`.

```python
raw = ft.read(11)  # read exactly one packet

sample = RadarProtocol.parse_data_packet(raw)

print(f"Range I/Q: ({sample['range_i']}, {sample['range_q']})")
print(f"Doppler I/Q: ({sample['doppler_i']}, {sample['doppler_q']})")
print(f"Detection flag: {sample['detection']}")

```

### Parsing Status Packets

`RadarProtocol.parse_status_packet` (lines 118–162) unpacks the six big-endian 32-bit words and maps them into a `StatusResponse` dataclass. This structure exposes operational parameters such as `radar_mode`, `stream_ctrl`, `cfar_threshold`, `agc_*` settings, and `self_test_*` flags.

```python
status = RadarProtocol.parse_status_packet(raw_bytes)
print("Current radar mode:", status.radar_mode)

```

### Building FPGA Commands

`RadarProtocol.build_command` (lines 66–75) packs a 4-byte command word from an `Opcode` enum member, an address, and a 16-bit value. The FPGA firmware decodes this word in its `usb_cmd_opcode` state machine.

```python
from radar_protocol import RadarProtocol, Opcode

# Set RADAR_MODE to 0x02 (e.g., operational state)

cmd_bytes = RadarProtocol.build_command(Opcode.RADAR_MODE, value=0x02)
ft.write(cmd_bytes)

```

## High-Level Data Flow from Raw Bytes to GUI Frames

Because the FT2232H FIFO delivers a continuous byte stream, packet boundaries must be recovered in software. `RadarProtocol.find_packet_boundaries` (lines 64–93) scans the receive buffer for start markers `0xAA` (data) or `0xBB` (status) and yields complete, validated packets.

The acquisition pipeline then follows four stages:

1. **Read**: `RadarAcquisition` (defined in [`9_Firmware/9_3_GUI/v7/hardware.py`](https://github.com/NawfalMotii79/PLFM_RADAR/blob/main/9_Firmware/9_3_GUI/v7/hardware.py)) pulls raw bytes from the bridge via `conn.read(size)`.
2. **Frame**: `find_packet_boundaries` extracts valid packets, and `RadarAcquisition._ingest_sample` assembles data samples into a **64 × 32 range-Doppler frame** represented by the `RadarFrame` class.
3. **Queue**: Completed frames are placed on a Python `queue.Queue` with a small backlog buffer (typically `maxsize=4`).
4. **Display**: `RadarDataWorker` (in [`9_Firmware/9_3_GUI/v7/workers.py`](https://github.com/NawfalMotii79/PLFM_RADAR/blob/main/9_Firmware/9_3_GUI/v7/workers.py), lines 22–28) dequeues the frame and emits the Qt signal `frameReady`, which [`GUI_V7_PyQt.py`](https://github.com/NawfalMotii79/PLFM_RADAR/blob/main/GUI_V7_PyQt.py) routes to the waterfall widget and map view.

Status packets bypass the frame builder and are forwarded through the `status_callback` path, eventually surfacing in the UI panel via the `statusReceived` signal.

The following example shows the high-level acquisition thread in action:

```python
import queue
from radar_protocol import FT2232HConnection, RadarAcquisition

conn = FT2232HConnection(mock=True)
conn.open()

frame_q = queue.Queue(maxsize=4)
acq = RadarAcquisition(
    connection=conn,
    frame_queue=frame_q,
    recorder=None,
    status_callback=lambda s: print("Status mode:", s.radar_mode)
)
acq.start()

frame = frame_q.get()  # blocks until a full frame is ready

print("Detections in frame:", frame.detection_count)
acq.stop()
acq.join()

```

For PyQt integration, `RadarDataWorker` hides the queue polling behind Qt signals:

```python
from workers import RadarDataWorker
from radar_protocol import FT2232HConnection

conn = FT2232HConnection(mock=True)
conn.open()

worker = RadarDataWorker(connection=conn)
worker.frameReady.connect(lambda f: print("New frame, detections:", f.detection_count))
worker.statusReceived.connect(lambda s: print("FPGA mode:", s.radar_mode))
worker.start()

# ... later ...

worker.stop()
worker.wait()

```

## Mock Mode for Hardware-Free Development

Both `FT2232HConnection` and the premium-board `FT601Connection` support a **mock mode** activated by `mock=True`. In this mode, `_mock_read` (lines 99–130) returns synthetic 11-byte data packets that mimic realistic target returns near range bin 20 and Doppler bin 8. This enables continuous integration tests and UI development without physical radar hardware attached.

```python
conn = FT2232HConnection(mock=True)
conn.open()

synthetic = conn.read(4096)  # returns injectable test packets

```

The mock implementation exercises the full parsing pipeline—from boundary scanning in `find_packet_boundaries` to frame assembly in `RadarAcquisition`—ensuring protocol changes are validated before hardware deployment.

## Extending the Protocol

The protocol is designed for symmetric extension on both the host and FPGA sides:

- **New opcodes**: Add entries to the `Opcode` enum in [`radar_protocol.py`](https://github.com/NawfalMotii79/PLFM_RADAR/blob/main/radar_protocol.py) and add matching decode cases in the FPGA firmware’s `usb_cmd_opcode` block.
- **New status fields**: Update `RadarProtocol.parse_status_packet` and increase `STATUS_PACKET_SIZE` only after the FPGA serializer emits the extra words.

Because packet parsing is isolated inside `RadarProtocol`, GUI code in [`workers.py`](https://github.com/NawfalMotii79/PLFM_RADAR/blob/main/workers.py) and [`GUI_V7_PyQt.py`](https://github.com/NawfalMotii79/PLFM_RADAR/blob/main/GUI_V7_PyQt.py) rarely needs changes when the underlying command set evolves.

## Summary

- The **FT2232H** bridge (VID 0x0403, PID 0x6010) provides a byte-stream USB 2.0 interface using 245 Synchronous FIFO mode on Channel A.
- [`9_Firmware/9_3_GUI/radar_protocol.py`](https://github.com/NawfalMotii79/PLFM_RADAR/blob/main/9_Firmware/9_3_GUI/radar_protocol.py) defines the complete USB communication protocol between Python GUI and FPGA via FT2232H, including classes for connection management, packet parsing, and command generation.
- **Data packets** are 11 bytes (`0xAA` header, `0x55` footer), **status packets** are 26 bytes (`0xBB` header), and **commands** are 4 bytes big-endian.
- `RadarProtocol.find_packet_boundaries` recovers packet alignment from the raw FIFO stream before `parse_data_packet` or `parse_status_packet` converts bytes into usable structures.
- `RadarAcquisition` and `RadarDataWorker` orchestrate the flow from raw bytes to fully formed `RadarFrame` objects that the PyQt GUI consumes via Qt signals.
- A built-in **mock mode** allows hardware-free unit testing and rapid UI iteration by synthesizing valid packets in pure Python.

## Frequently Asked Questions

### What USB bridge chip does PLFM_RADAR use for FPGA-to-host communication?

The project uses the **FTDI FT2232H**, a dual-channel USB 2.0 Hi-Speed FIFO bridge. Channel A is configured in 245 Synchronous FIFO mode to stream raw bytes between the FPGA signal-processing pipeline and the Python host application at high throughput with low latency.

### How does the Python GUI recover individual packets from the continuous FT2232H byte stream?

The GUI calls `conn.read(size)` inside `RadarAcquisition` and passes the resulting buffer to `RadarProtocol.find_packet_boundaries` (lines 64–93). This scanner searches for the `0xAA` data header or `0xBB` status header, validates the trailing `0x55` footer, and returns only complete packets. Each valid packet is then routed to `parse_data_packet` or `parse_status_packet` accordingly.

### Is it possible to develop and test the PLFM_RADAR interface without physical hardware?

Yes. The `FT2232HConnection` class supports `mock=True`, which activates a pure-Python `_mock_read` implementation (lines 99–130). In mock mode, the connection object synthesizes realistic 11-byte packets that pass through the same framing and parsing logic as real hardware, enabling automated tests and GUI development without an attached FT2232H board.

### Which source files connect the low-level USB protocol to the PyQt GUI?

The core protocol lives in [`9_Firmware/9_3_GUI/radar_protocol.py`](https://github.com/NawfalMotii79/PLFM_RADAR/blob/main/9_Firmware/9_3_GUI/radar_protocol.py). [`9_Firmware/9_3_GUI/v7/hardware.py`](https://github.com/NawfalMotii79/PLFM_RADAR/blob/main/9_Firmware/9_3_GUI/v7/hardware.py) provides the high-level `RadarAcquisition` façade that assembles frames. [`9_Firmware/9_3_GUI/v7/workers.py`](https://github.com/NawfalMotii79/PLFM_RADAR/blob/main/9_Firmware/9_3_GUI/v7/workers.py) contains `RadarDataWorker`, which polls the frame queue and emits Qt signals. Finally, [`9_Firmware/9_3_GUI/GUI_V7_PyQt.py`](https://github.com/NawfalMotii79/PLFM_RADAR/blob/main/9_Firmware/9_3_GUI/GUI_V7_PyQt.py) consumes those signals to update the waterfall, map, and status displays.