PX4 MAVLink Module Deep Dive: Architecture, Multi-Interface Mechanisms & Three-Layer Multiplexing (2026)

PX4 MAVLink Module Deep Dive: Architecture, Flow & Multi-Interface Mechanisms

Analysis target: src/modules/mavlink/


1. Overview

The MAVLink module is PX4’s core communication module linking the autopilot to external ground control stations (GCS), companion computers, RC transmitters, and other MAVLink devices. It implements the complete MAVLink protocol (v1/v2) send/receive chain, supports serial, UDP, TCP and other physical links, and includes a built-in multi-instance mechanism capable of running multiple independent channels simultaneously.

Core Challenges

The MAVLink module must solve multiplexing at three levels simultaneously:

Level Problem Solution
Physical Link Handle serial, UDP, TCP links simultaneously Unified abstraction via read(fd) / write(fd); differences isolated in the driver layer
Multi-Interface Concurrency Multiple interfaces running at once with different configs Multi-Instance mode; each instance runs independently
Multi-Protocol Services Handle Mission / Parameter / FTP / Timesync sub-protocols concurrently Protocol services as independent classes, plugged in via unified handle_message() + send() interfaces

Data Flow Diagram


2. File Structure

src/modules/mavlink/
├── mavlink_main.cpp # Module entry + main loop (send thread)
├── mavlink_main.h # Mavlink class definition (3515+721 lines)
├── mavlink_receiver.cpp # Receive thread + message dispatch (3571 lines)
├── mavlink_receiver.h # MavlinkReceiver class definition
├── mavlink_messages.cpp # Stream registry + factory functions
├── mavlink_messages.h # StreamListItem template
├── mavlink_stream.cpp # MavlinkStream::update() timing logic
├── mavlink_stream.h # MavlinkStream base class
├── mavlink_rate_limiter.cpp/h # Rate limiter
├── mavlink_command_sender.cpp/h # COMMAND_ACK sending
├── mavlink_parameters.cpp/h # Parameter protocol (MAVLink ↔ PX4 params)
├── mavlink_mission.cpp/h # Mission protocol (waypoint upload/download)
├── mavlink_ftp.cpp/h # File transfer protocol
├── mavlink_timesync.cpp/h # Time sync protocol
├── mavlink_events.cpp/h # Event protocol
├── mavlink_ulog.cpp/h # ULog log streaming
├── mavlink_shell.cpp/h # NuttX Shell remote access
├── mavlink_log_handler.cpp/h # Log message handling
├── tune_publisher.cpp/h # Music playback (buzzer tunes)
├── mavlink_simple_analyzer.cpp/h # Simple signal analyzer
├── open_drone_id_translations.cpp/h # Open Drone ID protocol
├── mavlink.c # Global mavlink_system definition (sysid=1, compid=1)
├── mavlink_params.c # Parameter definitions
├── module.yaml # Auto-start config and parameter definitions
├── streams/ # 100+ MAVLink message stream implementations
│ ├── HEARTBEAT.hpp
│ ├── ATTITUDE.hpp
│ ├── BATTERY_STATUS.hpp
│ ├── GPS_RAW_INT.hpp
│ ├── SYS_STATUS.hpp
│ ├── GLOBAL_POSITION_INT.hpp
│ ├── RC_CHANNELS.hpp
│ ├── SERVO_OUTPUT_RAW.hpp
│ └── ... (100+ files)
└── mavlink_tests/ # Unit tests
├── mavlink_ftp_test.cpp
└── mavlink_tests.cpp


3. Startup Flow

3.1 Complete Startup Call Chain

3.2 Startup Parameters

Parameter Meaning Default
-b Baud rate 57600
-r Data rate (bytes/s) baud/20
-d Serial device name /dev/ttyS1
-u UDP local port
-o UDP remote port 14550
-t Target IP (UDP) 127.0.0.1
-m Mode normal
-f Enable forwarding
-n Network interface name
-p Enable broadcast
-c Multicast address
-x Enable FTP
-w Wait to send
-z Hardware flow control
-Z Force no flow control
-s Software throttling
-F Iridium frequency

4. Thread Model

Each MAVLink instance contains two threads:

Inter-Thread Synchronization

Shared Resource Protection Mechanism Description
_buf[] send buffer _send_mutex Protects concurrent multi-thread sends
Forwarded message buffer _message_buffer_mutex Protects VariableLengthRingbuffer
Radio status _radio_status_mutex Protects _rstatus, _radio_status_mult
Shell object _mavlink_shell_mutex Protects remote shell access
Instance array mavlink_module_mutex (global) Protects mavlink_module_instances[]
Event buffer mavlink_event_buffer_mutex (global) Protects shared event buffer

5. First Layer of Multiplexing: Multi-Instance

The MAVLink module uses the multi-instance mechanism to solve concurrent multi-physical-interface problems.

5.1 Instance Array

static Mavlink *mavlink_module_instances[MAVLINK_COMM_NUM_BUFFERS] {};

  • MAVLINK_COMM_NUM_BUFFERS is usually 4, maximum 7
  • Each instance occupies one slot, mapped to MAVLINK_COMM_0 ~ MAVLINK_COMM_6

5.2 Instance Registration & Channel Mapping

void Mavlink::set_instance_id() {
for (int i = 0; i < MAVLINK_COMM_NUM_BUFFERS; i++) { if (mavlink_module_instances[i] == nullptr) { mavlink_module_instances[i] = this; _instance_id = i; mavlink_instance_count++; break; } } }

The instance ID maps directly to the MAVLink library channel (MAVLINK_COMM_0 ~ MAVLINK_COMM_6).

5.3 Typical Multi-Instance Scenario

Each instance is fully independent — its own _streams linked list, its own poll() file descriptors, its own rate configuration.


6. Second Layer of Multiplexing: Physical Link Abstraction

6.1 Protocol Abstraction Enum

enum class Protocol {
SERIAL = 0,
UDP, // Enabled only when MAVLINK_UDP is compiled
};

6.2 Receive Path: Unified poll, Differentiated Read

struct pollfd fds[1] = {}; // Select different fd based on protocol type
if (_mavlink.get_protocol() == Protocol::SERIAL) {
fds[0].fd = _mavlink.get_uart_fd();
} else if (_mavlink.get_protocol() == Protocol::UDP) {
fds[0].fd = _mavlink.get_socket_fd();
}
fds[0].events = POLLIN;
while (!_should_exit.load()) {
int ret = poll(&fds[0], 1, timeout);
if (ret > 0) {
// Select different read method based on protocol type
if (_mavlink.get_protocol() == Protocol::SERIAL) {
nread = ::read(fds[0].fd, buf, sizeof(buf));
} else if (_mavlink.get_protocol() == Protocol::UDP) {
nread = recvfrom(..., buf, sizeof(buf), ...);
}
// Unified parsing and dispatch
for (ssize_t i = 0; i < nread; i++) { if (mavlink_parse_char(channel, buf[i], &msg, &status)) { handle_message(&msg); } } } }

Key point: the read and poll methods differ, but parsing and dispatch are completely unified. Physical link differences are isolated at the read / recvfrom call sites.

6.3 Send Path: Three-Layer Callbacks

Regardless of the physical link, sending goes through the MAVLink library's three-layer callbacks:


7. Send Thread — Main Loop Deep Dive

7.1 Main Loop Flow

7.2 Stream Send Mechanism

Each MAVLink message stream inherits from the MavlinkStream base class:

class MavlinkStream : public ListNode {
public:
int update(const hrt_abstime &t); // Called by main loop
virtual bool send() = 0; // Implemented by subclasses
virtual void update_data() { } // High-frequency data update
virtual bool const_rate() { return false; }
virtual unsigned get_size() = 0;
};

update() logic:

7.3 Rate Adaptation

update_rate_mult() dynamically adjusts _rate_mult (0.05 ~ 1.0) to keep total bandwidth below _datarate:


8. Receive Thread — Message Reception & Dispatch

8.1 Receive Loop

8.2 Message Dispatch — handle_message()

handle_message() uses a large switch for message routing, then dispatches messages to each protocol service via the back-chain:

8.3 Message Handling → uORB Publication

Each handle_message_xxx() function typically: decodes the MAVLink message → builds a uORB message → orb_publish().

Typical mapping relationships:

MAVLink Message Published to uORB Topic
COMMAND_LONG vehicle_command
MANUAL_CONTROL manual_control_setpoint
RC_CHANNELS_OVERRIDE rc_channels
SET_POSITION_TARGET_LOCAL_NED vehicle_command / position_setpoint_triplet
SET_ATTITUDE_TARGET vehicle_command
VISION_POSITION_ESTIMATE vehicle_visual_odometry
ODOMETRY vehicle_visual_odometry
DISTANCE_SENSOR distance_sensor
GPS_RTCM_DATA gps_rtcm_data
HEARTBEAT No publish → updates internal timeout state
PING No publish → replies PING directly

9. Third Layer of Multiplexing: Protocol Services

9.1 Protocol Service Members

In the MavlinkReceiver class, multiple protocol services exist as member objects:

class MavlinkReceiver {
MavlinkFTP _mavlink_ftp;
MavlinkLogHandler _mavlink_log_handler;
MavlinkMissionManager _mission_manager;
MavlinkParametersManager _parameters_manager;
MavlinkTimesync _mavlink_timesync;
MavlinkStatustextHandler _mavlink_statustext_handler;
TunePublisher *_tune_publisher;
};

9.2 Chain of Responsibility: handle_message() Back-Chain

Each protocol service connects to message dispatch via the chain of responsibility pattern — each handler only processes messages it cares about and skips the rest:

void MavlinkReceiver::handle_message(mavlink_message_t *msg)
{
switch (msg->msgid) {
// ... 30+ message types handled directly
}

// Back-chain: each protocol service checks and handles messages it cares about
_mission_manager.handle_message(msg); // MISSION_xxx
_parameters_manager.handle_message(msg); // PARAM_xxx

if (_mavlink.ftp_enabled()) {
_mavlink_ftp.handle_message(msg); // FILE_TRANSFER_PROTOCOL
}

_mavlink_log_handler.handle_message(msg); // LOG_xxx
_mavlink_timesync.handle_message(msg); // TIMESYNC
_mavlink.handle_message(msg); // Forward to other instances
}

Each protocol service only processes msgids it cares about and ignores the rest.

9.3 Receive Thread Periodic Polling (Send Direction)

In the receive thread's main loop, each protocol service's send() is polled every 10ms:

const hrt_abstime t = hrt_absolute_time();
if (t - last_send_update > timeout * 1000) { // timeout = 10ms
_mission_manager.check_active_mission();
_mission_manager.send();

if (_mavlink.get_mode() != MAVLINK_MODE_IRIDIUM) {
_parameters_manager.send();
_mavlink.set_sending_parameters(_parameters_manager.send_active());
}

if (_mavlink.ftp_enabled()) {
_mavlink_ftp.send();
}

_mavlink_log_handler.send();
last_send_update = t;
}

if (_tune_publisher != nullptr) {
_tune_publisher->publish_next_tune(t);
}

9.4 Protocol Service Dispatch Matrix

Protocol Service Receive Direction Send Direction Running Thread
Stream Messages (Stream) N/A stream->update(t) iteration Send thread
Command (COMMAND) handle_message_command_long/int() handleAndGetCurrentCommandAck() Send thread
Mission _mission_manager.handle_message(msg) _mission_manager.send() Receive thread (10ms)
Parameter _parameters_manager.handle_message(msg) _parameters_manager.send() Receive thread (10ms)
FTP _mavlink_ftp.handle_message(msg) _mavlink_ftp.send() Receive thread (10ms)
Log _mavlink_log_handler.handle_message(msg) _mavlink_log_handler.send() Receive thread (10ms)
Timesync _mavlink_timesync.handle_message(msg) N/A Receive thread
Events handle_message_request_event() _events.update(t) Send thread
ULog stream handle_message_logging_ack() _mavlink_ulog->handle_update() Send thread
Shell handle_message_serial_control() handleMavlinkShellOutput() Send thread
Tune handle_message_play_tune() _tune_publisher->publish_next_tune() Receive thread
Forward _mavlink.handle_message(msg) Main loop pops buffer Send thread

10. Cross-Instance Forwarding Mechanism

When messages need to be shared between different physical interfaces, forward_message() handles it.

10.1 Forwarding Flow

The forwarding mechanism lets a message received by one instance be passed to other instances and finally transmitted through their physical links. The core flow:

10.2 Forwarding Filter Conditions

void Mavlink::forward_message(const mavlink_message_t *msg, Mavlink *self)
{
// 1. No other instances → no forwarding
if (mavlink_instance_count.load() <= 1) return; // 2. Target is myself → no forwarding if (target_system_id == self->get_system_id()
&& target_component_id == self->get_component_id()) return;

// 3. Heartbeat not forwarded by default (unless forward_heartbeats_enabled)
if (msg->msgid == MAVLINK_MSG_ID_HEARTBEAT
&& !self->forward_heartbeats_enabled()) return;

// 4. Low bandwidth mode blocks specific messages
if (self->get_mode() == MAVLINK_MODE_LOW_BANDWIDTH
&& msg->msgid == MAVLINK_MSG_ID_ONBOARD_COMPUTER_STATUS) return;

// 5. Only forward to instances where the target component has appeared
for (Mavlink *inst : mavlink_module_instances) {
if (inst && inst != self && inst->get_forwarding_on()) {
if (inst->_receiver.component_was_seen(target_system_id, target_component_id)) {
inst->pass_message(msg);
}
}
}
}


11. Modes & Stream Configuration

11.1 Supported Modes

Mode Purpose Data Rate
NORMAL Full GCS telemetry (default) Medium
CUSTOM No default streams Custom
ONBOARD Companion computer high rate High
OSD OSD overlay display Low
CONFIG USB debug configuration High
IRIDIUM Iridium satellite link Very low
MINIMAL Extremely low bandwidth link Very low
EXTVISION External vision (VIO) High
EXTVISIONMIN External vision minimal Medium
GIMBAL Gimbal communication Low
ONBOARD_LOW_BANDWIDTH Onboard low bandwidth Medium
UAVIONIX ADSB transponder Low
LOW_BANDWIDTH Severely constrained link Very low
DISTANCE_SENSOR Distance sensor only Very low

11.2 Stream Registry

In mavlink_messages.cpp, the stream list is built at compile time via template metaprogramming:

static const StreamListItem streams_list[] = {
create_stream_list_item(),
create_stream_list_item(),
create_stream_list_item(),
create_stream_list_item(),
// ... 100+ streams
};

Each stream is guarded by #if defined(XXX_HPP), allowing trimming on resource-constrained targets.

11.3 Stream Lifecycle


12. Protocol Services in Detail

12.1 FTP Protocol (MavlinkFTP)

Implements remote filesystem access via MAVLink FILE_TRANSFER_PROTOCOL messages.

Supported operations: List Directory, Open File, Read/Write File, Create/Remove Directory, Rename, Calc CRC32, Burst Read.

How it works:

  • Receive: handle_message() parses payload → _process_request() → switch(opcode) dispatch
  • Send: receive thread calls send() every 10ms to handle burst transfers
  • Security: all paths are prefixed with _root_dir to prevent path escape

12.2 Mission Protocol (MavlinkMissionManager)

Implements the MAVLink waypoint/mission protocol (upload, download, clear, set current target).

State machine: IDLE → SENDLIST / GETLIST → IDLE

Key design decisions:

  • Uses DatamanClient for persistent mission item storage
  • Supports three mission types: MISSION, FENCE, RALLY
  • Uses MavlinkRateLimiter to limit speed
  • _transfer_in_progress global flag prevents concurrent transfers

12.3 Parameter Protocol (MavlinkParametersManager)

Implements the MAVLink parameter protocol (read, write, list sync).

  • send_all(): iterates all PX4 parameters, sends in batches
  • send_one(): responds to single parameter read requests
  • send_untransmitted(): sends changed-but-unsent parameters

Key design decision: parameter sending blocks until boot_complete, ensuring parameters only sync after full system initialization.

12.4 Time Sync (MavlinkTimesync)

Implements the TIMESYNC protocol to synchronize PX4 and GCS clocks. The principle is round-trip time (RTT) measurement, computing the clock offset.

12.5 Other Protocol Services

Protocol Service Function
Log protocol (MavlinkLogHandler) Handles STATUSTEXT, LOGGING_ACK
Event protocol (MavlinkEvents) Streams PX4 internal events to GCS via MAVLink (handled in send thread)
ULog stream (MavlinkULog) Streams ULog binary logs to GCS in real time (handled in send thread)
Remote Shell (MavlinkShell) NuttX Shell remote access
Tune playback (TunePublisher) Buzzer tune MAVLink control

13. Full Data Flow Overview


14. Design Pattern Analysis

14.1 Chain of Responsibility

The large switch in handle_message() plus the back-chain protocol service calls form a chain of responsibility:

MAVLink message → COMMAND handler → MISSION → PARAM → FTP → ...

Each handler only processes messages it cares about and skips the rest.

14.2 Strategy Pattern

Physical links implement the strategy pattern via the Protocol enum + conditional branches; each mode defines a different stream set and rate in configure_streams_to_default() — another strategy pattern application.

14.3 Observer Pattern — Pull Model

MAVLink streams use the pull model — the main loop periodically iterates all streams, actively pulling data from uORB. This differs from uORB's push model (publisher pushes).

Main loop: for (stream : _streams) stream->update(t)
→ sub->update() pulls latest data from uORB
→ send() → pack → write(fd)

14.4 Factory Method (Stream Creation)

create_stream_list_item() is a template factory function that generates stream creation info at compile time. configure_stream() matches by name string at runtime and calls the factory to create instances.

14.5 Command Pattern (COMMAND Handling)

handle_message_command_long() converts MAVLink COMMAND messages into vehicle_command uORB messages, executed asynchronously by the Commander module — decoupling the command sender from the executor.

14.6 Adapter Pattern (Protocol Services)

Each protocol service (MavlinkFTP, MavlinkParametersManager, etc.) acts as an adapter between the MAVLink protocol and PX4 internals, converting MAVLink protocol messages into internal operations and encoding internal state back into MAVLink messages.


15. Decoupling Analysis

✅ Well-Decoupled Areas

Dimension Implementation Rating
Physical link abstraction Unifies serial and UDP via read(fd) / write(fd) ★★★★★ Differences isolated in 2 conditional branches
Message streams vs business logic 100+ independent stream files, each encapsulating one message ★★★★★ Adding a message = creating a .hpp file
Inter-instance isolation Each instance has independent _streams list and poll() fd ★★★★★ Fully independent; bridged via forwarding
Pluggable protocol services Each service as an independent class via handle_message() + send() interfaces ★★★★ Adding a protocol = new class + declaration
Send thread vs receive thread Two independent threads communicating via uORB and message queues ★★★★ Receive never blocks send
MAVLink library Standard C library mavlink_parse_char() / mavlink_msg_xxx_pack() ★★★★★ No dependency on specific MAVLink library internals
Streaming message plugin-ization 100+ stream files, one message per file ★★★★★ Adding a message = new .hpp

⚠️ Areas for Improvement

Dimension Current State Issue
handle_message() file size 3571 lines, one file with 30+ handlers File too large, violates single responsibility principle
Main loop responsibilities Single loop handles 10+ tasks High coupling, hard to modify and maintain
Hardcoded protocol services 6 protocol services are fixed members of MavlinkReceiver Cannot dynamically load/unload at runtime
Protocol service call frequency All services polled at fixed 10ms interval Wastes CPU under low load
Receive thread overload Does both message parse/dispatch and periodic protocol service sends Could split send duties to send thread
Rate adaptation Coupled with send logic in task_main() Could extract to independent strategy class
Global instance array Static array + global mutex Limits maximum instance count
Stream configuration Hardcoded in configure_streams_to_default() Adding a new mode requires modifying this function

16. Summary

Core Architecture

The MAVLink module solves multi-interface, multi-protocol concurrency through three-layer multiplexing:

Overall Assessment

Dimension Assessment
Architecture Dual-thread (send + receive) + multi-instance; each instance runs independently
Message reception poll()parse_char() → switch dispatch → handler → uORB
Message sending Stream-based: main loop iterates streams → update() checks interval → send()write(fd)
Rate control Dynamic _rate_mult adaptation considering link congestion
Protocol services 6+ sub-protocol services (mission/param/ftp/timesync/events/ulog)
Extensibility Adding a message = new stream file + register in streams_list[]
Overall ★★★★☆ Complete functionality, clear architecture. The oversized receive dispatch switch is the main coupling point, but the plug-in stream design is a highlight

At Aomway, we build and fly PX4-based FPV platforms daily, and the MAVLink module is the invisible backbone that connects every Aomway airframe to its ground station — carrying attitude, GPS, battery, and command data over the exact mechanisms dissected above. Understanding this architecture matters far beyond academia: when you're configuring an Aomway Commander ground station, tuning stream rates for a low-bandwidth long-range link, or debugging why a companion computer sees telemetry but your RC failsafe doesn't respond, you're working with the three-layer multiplexing, rate adaptation, and protocol services this article explains. The MAVLink module's plug-in stream design is precisely why PX4 remains the most flexible open autopilot for custom FPV and commercial UAV builds. Questions about MAVLink configuration, PX4 tuning, or integrating Aomway flight systems with your ground control software? Contact us at [email protected].

Frequently Asked Questions

1. What exactly is "multi-instance" in the PX4 MAVLink module and why does it matter?

Multi-instance means the MAVLink module can run several independent instances simultaneously — each bound to a different physical link (e.g., one on a serial telemetry radio to the RC controller, another over USB to a ground station, another over UDP to a companion computer). Each instance gets its own slot in mavlink_module_instances[] (usually up to 4, max 7), its own stream list, file descriptors, and rate configuration. Instances are bridged by the forwarding mechanism, so a command received on one link can be relayed to another. This is why PX4 can talk to a GCS and a companion computer at the same time without conflict.

2. How does the three-layer multiplexing work in practice?

Layer 1 is multi-instance, solving concurrent physical interfaces (each instance owns a link). Layer 2 is the physical link abstraction — serial vs UDP are unified behind read(fd)/write(fd), so parsing and dispatch code is identical regardless of link type. Layer 3 is the protocol services: Mission, Parameter, FTP, Timesync, Events, ULog, Shell, and Tune each exist as independent classes plugged into a unified handle_message() + send() interface, implementing the chain of responsibility pattern. Together they let one module handle any combination of links, protocols, and message types without code duplication.

3. What is the difference between the send thread and the receive thread?

The send thread (in mavlink_main.cpp) runs the main loop that periodically iterates all registered streams, calls update() to check each stream's send interval, invokes send() to pack data (pulled from uORB), and writes to the file descriptor. The receive thread (in mavlink_receiver.cpp) polls the fd, feeds bytes into mavlink_parse_char(), dispatches complete messages via handle_message(), and publishes to uORB topics. The receive thread also polls protocol service send() methods every 10ms for mission/param/FTP/log responses. This split ensures incoming messages never block outgoing telemetry.

4. How does rate adaptation (_rate_mult) work?

update_rate_mult() dynamically adjusts a multiplier _rate_mult between 0.05 and 1.0 to keep total stream bandwidth under the configured _datarate (default baud/20 bytes per second). When the module detects that the link is congested — typically by monitoring radio status, link throughput, or buffer pressure — it scales down stream rates proportionally, prioritizing critical streams. This is why on a low-bandwidth link (like Iridium satellite), the module can still deliver heartbeat and critical status while throttling high-rate streams like raw IMU data. It's a practical congestion control mechanism that keeps telemetry alive on marginal links.

5. How do I add a custom MAVLink message or stream to PX4?

To add a new stream: (1) create a new .hpp file in src/modules/mavlink/streams/ that inherits from MavlinkStream, implementing send() and get_size(), and optionally update_data(); (2) register it in streams_list[] in mavlink_messages.cpp via create_stream_list_item(); (3) ensure the new message ID is defined in the MAVLink library headers (or generate custom XML definitions). For incoming messages, add a case to handle_message() in mavlink_receiver.cpp (or create a protocol service class following the existing patterns). The plugin-style stream design means most additions are isolated to a single file.

Leave a Comment

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

Scroll to Top