FPGA Packet Headers & Vivado Binary Packaging


Overview: When streaming FPGA bitstreams or configuration binaries over serial interfaces (UART, SPI, Ethernet, PCIe), raw binary files must be wrapped with structured protocol headers. This article explains how to inspect Xilinx bitstream headers, generate formatted binary outputs in Vivado, and construct custom transport packet headers for embedded MCU/FPGA loaders.

1. Architecture of a Native Xilinx Bitstream (`.bit`)

A standard .bit file generated by Xilinx Vivado consists of a binary header section containing human-readable ASCII field tags followed by the actual FPGA configuration frames and bus sync word (0xAA995566).

graph TD subgraph "Native Xilinx .bit File Format" H1["Header Field 'a': Design Name (e.g., top_design.ncd)"] --> H2["Header Field 'b': Target Part (e.g., 7z020clg400)"] H2 --> H3["Header Field 'c': Build Date (YYYY/MM/DD)"] H3 --> H4["Header Field 'd': Build Time (HH:MM:SS)"] H4 --> H5["Header Field 'e': Payload Byte Length"] H5 --> Sync["Sync Word (0xAA995566)"] Sync --> Payload["FPGA Configuration Data Frames"] end

2. Why Custom Packet Headers Are Needed

Raw bitstreams cannot be safely transmitted over lossy or packetized channels without framing. Adding custom packet headers ensures:

graph LR subgraph "Packetized Transport Stream (Ethernet / UART / SPI)" Magic["Magic Byte (0x584C)"] --- PktId["Seq # / Chunk ID"] PktId --- Len["Payload Length"] Len --- Data["Bitstream Chunk Data"] Data --- CRC["CRC32 / Checksum"] end

3. Generating Binary Memory Formats in Vivado (`write_cfgmem`)

To strip header metadata or format binaries for SPI Flash / NOR memory, Vivado Tcl provides the write_cfgmem command:

# Run in Vivado Tcl Console or Batch Mode:
write_cfgmem -format BIN -size 16 -interface SPIx4 -loadbit "up 0x00000000 top.bit" -file top.bin -force

4. Python Script: Prepending Custom Protocol Packet Headers

Below is a Python script that parses a Vivado .bit file, extracts length & payload data, calculates CRC32, and wraps the payload into structured custom packet headers for embedded firmware loaders:

import struct
import zlib

def package_fpga_binary(input_bit_path, output_bin_path):
    MAGIC_HEADER = 0x584C494E  # "XLIN" in ASCII
    
    with open(input_bit_path, 'rb') as f:
        data = f.read()

    # Locate Sync Word (0xAA995566) in Xilinx bitstream
    sync_offset = data.find(b'\xAA\x99\x55\x66')
    if sync_offset == -1:
        raise ValueError("Valid Xilinx sync word (0xAA995566) not found!")

    raw_payload = data[sync_offset:]
    payload_len = len(raw_payload)
    crc32_val = zlib.crc32(raw_payload) & 0xFFFFFFFF

    # Construct 16-byte custom header: Magic(4B) + Length(4B) + CRC32(4B) + Reserved(4B)
    custom_header = struct.pack('>IIII', MAGIC_HEADER, payload_len, crc32_val, 0x00000000)

    with open(output_bin_path, 'wb') as out_f:
        out_f.write(custom_header + raw_payload)
        
    print(f"Packaged {payload_len} bytes with CRC32 0x{crc32_val:08X} into {output_bin_path}")

# Example usage:
# package_fpga_binary("top_design.bit", "packaged_stream.bin")

5. Comparison: Binary Packaging Approaches

Approach Header Structure Best Used For
Native .bit File ASCII Metadata + Raw Sync Frames Direct JTAG programming via Vivado / Hardware Manager
Vivado write_cfgmem Raw binary memory layout (No ASCII headers) Direct QSPI / NOR Flash memory programming
Custom Packet Header Wrapper Magic Bytes + Chunk Seq + Length + CRC32 Over-the-Air (OTA) updates, UART / Ethernet stream loaders