MAVLink Protocol Complete Guide Part 2: C & C# Implementation with Code Generation (2026)

1. MAVLink Setup & Code Generation

After defining custom communication messages in XML files, you can use the official toolchain to generate the underlying code libraries. In this project, the lower-level controller uses C, while the upper-level host uses C#. Generating the code libraries requires the official Python scripts and related dependencies. Below are the step-by-step instructions.


Prerequisites

  • OS: MAVLink development is cross-platform; this guide uses Windows as the example.
  • Required tools: Python 3.3+, the future package, lxml, MAVLink source code, and the pymavlink code generation toolkit.
  • Download links:

Installing Dependencies

Python Installation

Python 3.3 or higher is required. The example below uses the Python 3.13.13 offline installer.

Installation steps:

  1. Run the .exe installer with administrator privileges and follow the prompts.
  2. Make sure to check “Add Python to environment variables”.
  3. Note: Do not install Python in a directory that contains Chinese characters in the path.

MAVLink tutorial image 0
MAVLink tutorial image 1

After installation, open Windows Command Prompt and run the following to verify:

python --version

MAVLink tutorial image 2

Installing Dependency Packages

Open a terminal and run the following commands to install the required packages:

pip install future

MAVLink tutorial image 3

pip install lxml

MAVLink tutorial image 4

pip install pymavlink

MAVLink tutorial image 5

Once all dependencies are installed, download the latest MAVLink source code.

MAVLink tutorial image 6


Generating Library Code

Run the code generation tool (GUI) included in the MAVLink source.

MAVLink tutorial image 7
MAVLink tutorial image 8

Generator UI configuration:

  1. XML (Input): Select the path to your XML file. This can be a custom XML or the standard common.xml bundled with MAVLink.
  2. Out (Output): Choose the target directory for the generated library files.
  3. Language: Select the target programming language — you need to generate both C and CS (C#) separately.
  4. Protocol: Supports V1 and V2. Select 2.0 (V2).

Important: If your custom XML file uses <include> tags to reference other .xml files, they must be in the same folder. The recommended approach is to place your custom XML file inside the official source at message_definitions/v1.0/.

MAVLink tutorial image 9
MAVLink tutorial image 10

After configuring the parameters, click Generate. The complete C and C# source files will be created in the output directory.


C Library File Overview

After successful generation, the output directory contains the following key files and folders:

  • common/: Standard messages from common.xml. Contains header files with data structure definitions, pack/send functions, and parse functions.
  • minimal/: The minimal message set required for basic MAVLink communication (e.g., HEARTBEAT).
  • standard/: Official standard MAVLink message set, typically related to common vehicle commands.
  • checksum.h: CRC calculation code for packet integrity verification.
  • mavlink_conversions.h: Conversion functions between DCM, Euler angles, and quaternions.
  • mavlink_get_info.h: Interface for retrieving message metadata (name, ID, length).
  • mavlink_helpers.h: Core functions for packet construction (adding headers, computing checksums) and message parsing (receiving byte streams and extracting complete packets).
  • mavlink_sha256.h: SHA-256 implementation for V2 message signing.
  • mavlink_types.h: Fundamental structures (e.g., mavlink_message_t) and enum types.
  • protocol.h: Core macro definitions and base configuration for the protocol.
  • [custom_name]/: All dedicated pack/unpack APIs for your custom messages.

2. Lower-Level Controller: C Library Integration

2.1 Project Setup (Keil5 Example)

  1. Copy the entire MAVLink folder into your project directory.
  2. Add the C source files to the Keil project and configure the include paths in Options for Target.
  3. Include the corresponding header files in any source file that uses the MAVLink API.

MAVLink tutorial image 11
MAVLink tutorial image 12

2.2 Message Definition & Usage

MAVLink tutorial image 13

Message IDs and contents can be found in the XML file. To create custom messages, assign your own IDs (avoiding reserved and already-used numbers, staying within the allowed range).

The generated pack/unpack functions follow a consistent naming pattern: mavlink_msg_xxxx_decode and mavlink_msg_xxxx_encode.

Receiving & Parsing Packets (Unpacking)

The motor controller communicates with the host via UART. When the UART idle interrupt fires, the received data can be unpacked.

void MavlinkRecvCallback(Axis *axis, AxisDw *axis_dw, uint8_t rx_data[], uint32_t len)

Single-byte state machine parsing:
Hardware data is often “sticky” or fragmented (e.g., a 10-byte message may arrive as 3 bytes then 7 bytes). MAVLink’s built-in mavlink_parse_char function handles this elegantly:

MAVLINK_HELPER uint8_t mavlink_parse_char(
    uint8_t chan, uint8_t c,
    mavlink_message_t* r_message,
    mavlink_status_t* r_mavlink_status)
for (int i = 0; i < len; i++) {
    if (mavlink_parse_char(MAVLINK_COMM_0, rx_data[i], &msg, &status)) {
        mavlink_flag = 1;
        break;
    }
}

Parameters:

  • chan: Channel ID.
  • c: The byte to parse.
  • r_message: Output decoded message struct (NULL if decoding fails).
  • r_mavlink_status: Output channel statistics including current parse state.

Return: 0 if the packet is incomplete; 1 if successfully decoded.

Decoding the Payload:
After successful decoding, r_message contains the message ID and payload. Use a switch on msgid and call the corresponding mavlink_msg_xxxxx_decode function:

case MAVLINK_MSG_ID_ScopeConfig:
    mavlink_msg_scopeconfig_decode(&msg, &scope_config);
    mavlink_scope_config_callback(&scope_config, &kScopeObject, SCOPE_WRITE);
    mavlink_msg_scopeconfig_encode(sys_id, comp_id, &send_msg, &scope_config);
    break;

Packing & Sending Messages

To send messages to other devices, pack the data into the MAVLink V2 format in two steps:

  1. mavlink_msg_xxxxx_encode (Packing)
    • Create payload: Pack C variables (float, int32_t, etc.) into the payload area.
    • Fill header: Automatically sets sequence number, system ID, component ID, and message ID.
    • Compute checksum: Calculates and sets the CRC based on the filled data.
    • Result: Assembles a complete mavlink_message_t struct in memory.
  2. mavlink_msg_to_send_buffer (Serialization)
    • Serializes the mavlink_message_t struct into a flat byte array, handling little-endian conversion.
    • Returns the array length len. The output can be sent directly over UART.

MAVLink tutorial image 14

Read Parameter Flow

Entry: if(msg.msgid == MAVLINK_MSG_ID_READ_PARAM)

When the host wants to read data from the controller, it sends a single READ_PARAM command.

  • Step 1: Unpack the request. mavlink_msg_read_param_decode(&msg, &read_param); — determines which table the host wants to read (stored in read_param.struct_id).
  • Step 2: Fetch the data. Enter switch (read_param.struct_id) to look up the requested data. For example, if the host wants motor config (MAVLINK_MSG_ID_PmsmConfig), the program copies inductance, resistance, pole pairs, etc. from axis->pmsm_config into pmsm_config_t.
  • Step 3: Pack for response. Call mavlink_msg_pmsmconfig_encode to pack the data into send_msg for transmission.

Write Parameter Flow

Entry: else { switch (msg.msgid) ... }

If the message ID is not “read”, it’s treated as a write command.

  • Step 1: Unpack the config. Based on msgid, enter the specific branch and call the corresponding decode function to extract the new parameters into a temporary struct.
  • Step 2: Authority check & overwrite (critical). The program checks if(get_app_Comm_control_authority() == COMM_CONTROL_HOST). Only when the host has control authority are the new parameters allowed to overwrite the core variables axis->....
  • Step 3: Generate ACK. After overwriting, immediately call encode to pack the newly written parameters back into send_msg as confirmation (“write successful, current values are…”).
  • Step 4: Apply changes. After the switch, call MotorCtlParamSetUpdata(&kAxis); to recalculate motor control coefficients so the new parameters take effect immediately.

3. Upper-Level Host: C# Library Integration

3.1 Project Setup

Copy the generated .cs file (typically a single file containing all definitions and parsing classes) into the host project directory and add it as a dependency.

MAVLink tutorial image 15

Unlike C’s byte-level approach, the C# MAVLink library provides a more object-oriented, higher-level abstraction.

3.2 Initializing the Parser

All packing and stream processing operations in C# depend on the MavlinkParse class. Instantiate it before establishing communication:

// Initialize the MAVLink parsing engine
private MAVLink.MavlinkParse _mavParser = new MAVLink.MavlinkParse();

3.3 Receiving & Parsing Packets (Unpacking)

Unlike C’s manual byte-by-byte loop, C# can pass the serial port’s BaseStream directly to the parser, which automatically extracts and validates complete MAVLink frames.

Typically, a background thread reads serial data in a loop using ReadPacket():

private void ReceiveLoop()
{
    while (_isRunning && _serialPort != null && _serialPort.IsOpen)
    {
        try
        {
            // Block-read from serial port and parse a complete message
            MAVLink.MAVLinkMessage msg = _mavParser.ReadPacket(_serialPort.BaseStream);
            if (msg == null) continue;
            // Notify upper-layer business logic
            OnMessageReceived?.Invoke(msg);
        }
        catch (TimeoutException) { continue; }
        catch (System.IO.IOException) { break; } // Normal disconnect
        catch (Exception ex)
        {
            Debug.WriteLine($"[Warning] Receive parse exception: {ex.Message}");
            continue;
        }
    }
}

Decoding the Payload:

When the upper layer receives a MAVLinkMessage, use a switch on msg.msgid and cast msg.data to the corresponding struct:

private void HandleReceivedMessage(MAVLink.MAVLinkMessage msg)
{
    switch ((MAVLink.MAVLINK_MSG_ID)msg.msgid)
    {
        case MAVLink.MAVLINK_MSG_ID.AppControlWord:
            var ctrlMsg = (MAVLink.mavlink_appcontrolword_t)msg.data;
            ushort word = ctrlMsg.Controlword;
            break;
    }
}

3.4 Packing & Sending Messages

Mirroring C’s two-step encode + to_send_buffer, C# also requires constructing a struct first, then serializing via the parser:

  1. Instantiate the message data struct and assign values.
  2. Call GenerateMAVLinkPacket20() to serialize (auto-computes header and CRC).
  3. Write the byte array to the serial port.

Code example (sending an application control word):

// Thread lock prevents multi-thread send collisions
private readonly object _writeLock = new object();

public void SendAppControlWord(ushort controlWord)
{
    if (_serialPort == null || !_serialPort.IsOpen) return;
    try
    {
        // 1. Instantiate message payload struct
        var ctrlMsg = new MAVLink.mavlink_appcontrolword_t();
        ctrlMsg.Controlword = controlWord;
        ctrlMsg.Halt_running_cmd = 0;

        lock (_writeLock)
        {
            // 2. Pack struct into byte stream
            byte[] txData = _mavParser.GenerateMAVLinkPacket20(
                MAVLink.MAVLINK_MSG_ID.AppControlWord,
                ctrlMsg,
                false,
                255,
                (byte)MAVLink.MAV_COMPONENT.MAV_COMP_ID_MY_CUSTOM_HOST
            );
            // 3. Write to serial port
            _serialPort.Write(txData, 0, txData.Length);
        }
    }
    catch (Exception ex)
    {
        Debug.WriteLine($"Control word send exception: {ex.Message}");
    }
}

Key Takeaways

  • Two-step workflow: Define messages in XML → Generate C/C# code with the official toolchain.
  • C (lower controller): Use mavlink_parse_char() for byte-by-byte reception; encode + to_send_buffer for sending.
  • C# (upper host): Use MavlinkParse.ReadPacket() for frame extraction; GenerateMAVLinkPacket20() for serialization.
  • Read/Write patterns: The lower controller uses a unified READ_PARAM message for reads and message-ID-based dispatch for writes, with authority checks and ACK responses.
  • Thread safety: Always use locks on the serial port write path to prevent data corruption from concurrent sends.

FAQ

Q1: Can I use MAVLink V1 instead of V2?

Yes, but V2 is recommended. V2 supports message signing (via mavlink_sha256.h) for secure communication, and is backward-compatible with V1.

Q2: What if my custom XML references other XML files?

Place all referenced .xml files in the same directory. The recommended location is message_definitions/v1.0/ inside the MAVLink source tree.

Q3: How do I handle “sticky packets” in C?

mavlink_parse_char() is a state machine that processes one byte at a time. It automatically handles partial and concatenated packets — just feed it bytes in order.

Q4: Can I use MAVLink over TCP or UDP instead of UART?

Yes. MAVLink is transport-agnostic. The same pack/unpack functions work regardless of whether the byte stream comes from UART, TCP, UDP, or any other transport.

Q5: Why does the C# parser use a background thread?

ReadPacket() is a blocking call. Running it on the main thread would freeze the UI. A dedicated background thread ensures responsive message reception.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top