MAVLink Serialization Architecture
1. The "Wire" Reality: Why MAVLink is Not Protobuf
MAVLink is designed for a specific hostile environment: Low-Bandwidth, High-Latency, Unreliable Serial Links (e.g., 57600 baud telemetry radios). In this domain, every byte costs airtime.
Unlike Protobuf, JSON, or XML, which prioritize schema flexibility and forward compatibility via Tag-Length-Value (TLV) structures, MAVLink prioritizes Determinism and Overhead Efficiency.
Core Philosophy: C-Struct Packing
MAVLink serialization is effectively "C-structs over the wire."
- Mechanism: The message definition (XML) is compiled into a C struct.
- Serialization: The memory content of that struct is sent directly (Little Endian).
- Parsing: The receiver casts the buffer back to the struct (or copies it).
- Cost: Zero parsing overhead (CPU) and Zero serialization overhead (Bandwidth) beyond the header.
2. Anatomy of a Packet (v2.0)
A MAVLink v2 packet is a rigid byte stream (defined in mavlink_types.h). It does not describe what it is carrying; it assumes the receiver has the same dictionary (XML definitions).
| Byte | Field | Description |
|---|---|---|
| 0 | STX |
Magic Marker (0xFD for v2). |
| 1 | LEN |
Payload Length (0-255). |
| 2 | INC_FLAGS |
Incompatibility Flags (e.g., Signing). |
| 3 | CMP_FLAGS |
Compatibility Flags. |
| 4 | SEQ |
Packet Sequence (0-255) for loss detection. |
| 5 | SYSID |
System ID (Sender, e.g., Drone). |
| 6 | COMPID |
Component ID (Sender, e.g., Autopilot vs. Camera). |
| 7-9 | MSGID |
Message ID (24-bit). The "Topic". |
| 10... | PAYLOAD |
The serialized data (up to 255 bytes). |
| N-2 | CRC |
Checksum (CRC16-MCRF4XX) of Header + Payload + Extra Byte. |
The "Extra Byte" (CRC_EXTRA)
MAVLink's unique integrity check.
- Problem: If Sender V1 sends struct
{float A}and Receiver V2 expects{float A, float B}, a blind cast is dangerous. - Solution: The CRC calculation includes a hidden "Seed" byte derived from the hash of the XML message definition.
- Result: If the Sender and Receiver have different XML definitions for
MSGID X, the CRC check fails silently. Schema enforcement is done by the checksum, not the parser.
3. Comparative Analysis: MAVLink vs. Protobuf
Why doesn't ArduPilot just use Protobuf?
| Feature | MAVLink | Protobuf |
|---|---|---|
| Encoding | Packed Binary (Fixed). A float is always 4 bytes at a known offset. | Varint + TLV. A float is a Tag + WireType + 4 bytes. |
| Parsing | O(1) / Zero-Copy. Cast buffer to struct. Extremely fast on 8-bit MCUs. | Recursive Descent. Requires CPU cycles to decode tags and reconstruct objects. |
| Bandwidth | Minimal. Header (10 bytes) + Data. No field tags. | Moderate. Every field carries a tag overhead. Varints save space for small ints but cost for large ones. |
| Schema | Rigid. Changing a field changes the CRC_EXTRA, breaking the link. |
Flexible. Unknown fields are ignored. Great for microservices, bad for safety-critical control loops. |
| Safety | Deterministic. We know exactly how many bytes ATTITUDE takes. |
Variable. Message size varies with content (e.g., zeros are compressed in proto3). Harder to schedule on fixed time-slots. |
Verdict: Protobuf is superior for Inter-Process Communication (IPC) on a Companion Computer. MAVLink is superior for Telemetry and Command & Control (C2) over a radio link.
4. ArduPilot Implementation
The rubber meets the road in the GCS_MAVLink library.
GCS_MAVLink.cpp: The central hub. It manages the routing of messages between UARTs (Serial ports) and the internal Data Bus.- Fast Channel: High-rate streams (Attitude, VFR_HUD) are often pushed into ring buffers to be drained by the UART DMA.
- Lazy Serialization: ArduPilot often checks
comm_get_txspace()before attempting to serialize. If the radio buffer is full, the packet is dropped immediately to prevent latency buildup (Backpressure).
Field Sorting (Wire Optimization)
MAVLink generators (like pymavlink) reorder fields in the struct to ensure Natural Alignment.
- All
uint64_t/double(8 bytes) come first. - Then
uint32_t/float(4 bytes). - Then
uint16_t(2 bytes). - Then
uint8_t(1 byte).
Why? To prevent the C compiler from adding "Padding Bytes" between fields to align memory access. This ensures the "Wire Format" is perfectly dense, with zero wasted bits.