跳到主要内容MAVLink 通信协议 C++ 开发实战:环境搭建与飞控通信 | 极客日志C++算法
MAVLink 通信协议 C++ 开发实战:环境搭建与飞控通信
综述由AI生成MAVLink 是轻量级无人机通信协议。基于 C++ 实现完整通信流程,涵盖协议原理、环境搭建(头文件获取)、跨平台串口通信代码(含心跳与姿态消息收发)、编译运行及网络通信扩展。解决了嵌入式环境下 MAVLink 消息编解码、串口配置及常见问题排查,支持 Linux/Windows 移植。
念念不忘5K 浏览 前言
MAVLink(Micro Air Vehicle Link)是一款轻量级、低带宽、高可靠性的微小型无人机通信协议,由 PX4 团队主导设计,广泛应用于无人机、无人车、机器人等嵌入式系统的跨设备通信场景。其核心优势在于专为资源受限的硬件(如 MCU、小型嵌入式板卡)优化,采用二进制编码格式,相比 JSON、XML 等文本协议,传输效率提升数倍,同时支持数据校验、重传机制,能在复杂电磁环境下保证通信稳定性。
从 MAVLink 协议核心原理出发,结合 C++ 语言实现完整的通信流程,涵盖协议环境搭建、消息编解码、串口 / 网络通信、数据收发实战等核心内容,所有代码均可直接移植到 Linux/Windows 嵌入式环境,助力快速上手 MAVLink C++ 开发。
一、MAVLink 协议核心原理
1.1 协议定位与核心特点
MAVLink 是应用层协议,基于串口(UART)、CAN、TCP/UDP 等传输层实现跨设备通信,主从架构为主(地面站 / GCS)从(无人机 / 飞控)模式,支持 1 对多设备通信。核心特点:
- 轻量级:最小消息帧仅 8 字节,无额外冗余数据;
- 二进制编码:无需解析文本,编解码效率极高;
- 高可靠:内置 CRC 校验、消息序列号、系统 ID / 组件 ID 标识;
- 可扩展:支持自定义消息,兼容官方标准消息集(common.xml、ardupilotmega.xml);
- 跨平台:原生支持 C/C++、Python、MATLAB 等语言,适配 Linux/Windows/RTOS。
1.2 核心消息帧结构
MAVLink v2.0(当前主流版本,兼容 v1.0)采用固定 + 可变长度的帧结构,标准帧共 10+N 字节(N 为有效载荷长度,最大 255 字节),结构如下(从低字节到高字节):
| 字段 | 长度(字节) | 作用 |
|---|
| 帧起始符 | 1 | 固定为0xFD(v2.0),标识消息帧开始,v1.0 为0xFE |
| 有效载荷长度 | 1 | 后续数据字段(payload)的字节数,0~255 |
| 兼容标志 | 1 | v2.0 新增,向下兼容 v1.0 的标志位 |
| 不兼容标志 | 1 | v2.0 新增,不兼容 v1.0 的标志位(如启用签名) |
| 消息序列号 | 1 | 消息计数(0~255 循环),用于检测丢包 |
| 系统 ID(SID) | 1 | 设备唯一标识(如地面站 SID=1,无人机 1=2,无人机 2=3),区分多设备 |
| 组件 ID(CID) | 1 | 设备内组件标识(如飞控 CID=1,摄像头 CID=2),区分单设备多组件 |
| 消息 ID | 3 | 消息类型标识(如心跳消息 ID=0,姿态消息 ID=30),v1.0 为 1 字节 |
| 有效载荷 | N | 实际业务数据(如姿态角、速度、指令等),按协议定义的格式存储 |
| CRC 校验码 | 2 | 对帧起始符到消息 ID 的所有字段校验,检测数据传输错误 |
| 签名(可选) | 13 | 安全校验字段,用于加密通信,需手动启用 |
1.3 核心设计思想
- 无连接通信:无需建立 TCP 式的连接,直接基于传输层发送帧数据,降低通信开销;
- 设备唯一标识:通过
系统 ID+ 组件 ID实现多设备 / 多组件的精准寻址,地面站可同时管理多台无人机;
- 轻量化编解码:基于预定义的 XML 消息集生成 C/C++ 头文件,编解码仅需简单的内存拷贝和校验,无动态内存分配,适配嵌入式系统;
鲁棒性机制:消息序列号检测丢包、CRC 校验检测数据损坏、超时重传机制保证消息送达。二、开发环境搭建与依赖
MAVLink C++ 开发无复杂依赖,核心仅需 MAVLink 官方头文件(无需编译库,纯头文件实现),支持跨平台编译,以下为 Linux(Ubuntu)和 Windows 环境的搭建步骤。
2.1 核心依赖
- 编译环境:Linux(g++/cmake)、Windows(MSVC/MinGW);
- 通信依赖:Linux(
termios串口库、socket网络库)、Windows(Win32 API串口库、Winsock网络库);
- MAVLink 头文件:官方自动生成,适配指定消息集(如 common、ardupilotmega)。
2.2 MAVLink 头文件获取(两种方式)
方式 1:官方在线生成(推荐,简单快捷)
- 打开 MAVLink 在线生成工具:https://mavlink.io/en/gen/
- 选择消息集:推荐
common(通用消息集,包含心跳、姿态、指令等核心消息);
- 选择版本:
MAVLink 2.0;
- 点击
Generate生成压缩包,解压后得到mavlink文件夹(包含所有头文件,核心为mavlink.h)。
方式 2:本地 Python 生成(适合自定义消息)
- 安装 Python3:
sudo apt install python3 python3-pip(Linux)/ 官网下载(Windows);
- 克隆 MAVLink 源码:
git clone https://github.com/mavlink/mavlink.git;
- 进入工具目录:
cd mavlink/pymavlink/tools;
- 生成头文件:
python3 mavgen.py --lang C --wire-protocol 2.0 --output ../generated/mavlink/v2.0 common.xml;
- 生成的头文件位于
../generated/mavlink/v2.0目录下。
2.3 环境配置
将生成的mavlink头文件文件夹放入项目目录,在 C++ 代码中通过#include "mavlink/mavlink.h"引入即可,无需链接任何库(纯头文件实现,编解码逻辑在头文件中)。
三、C++ 实现 MAVLink 完整通信流程
MAVLink C++ 通信的核心流程为:环境初始化→消息编码→数据传输→消息解码→数据解析,以下实现跨平台串口通信(最常用的 MAVLink 通信方式,无人机飞控多通过串口与地面站通信),同时兼容 MAVLink 2.0 协议,包含心跳消息(HEARTBEAT)和姿态消息(ATTITUDE)的收发实战。
3.1 核心代码实现(含跨平台兼容)
mavlink_cpp_demo/
├── mavlink/ # MAVLink 官方头文件文件夹
│ └── mavlink.h # 核心头文件
├── MavlinkSerial.h # 串口通信封装头文件
├── MavlinkSerial.cpp # 串口通信封装实现
└── main.cpp # 主函数,收发实战
文件 1:MavlinkSerial.h(跨平台串口封装 + MAVLink 基础)
#ifndef MAVLINK_SERIAL_H
#define MAVLINK_SERIAL_H
#include <cstdint>
#include <string>
#include "mavlink/mavlink.h"
#ifdef _WIN32
#include <windows.h>
typedef HANDLE SerialHandle;
#else
#include <termios.h>
typedef int SerialHandle;
#endif
const uint8_t SYS_ID = 1;
const uint8_t COMP_ID = 1;
const uint32_t BAUDRATE = 115200;
class MavlinkSerial {
public:
MavlinkSerial();
~MavlinkSerial();
bool openSerial(const std::string& port);
void closeSerial();
bool isOpen() const { return m_isOpen; }
bool sendMavlinkMsg(const mavlink_message_t& msg);
bool recvMavlinkMsg(mavlink_message_t& msg, mavlink_status_t& status);
private:
SerialHandle m_serialHandle;
bool m_isOpen;
bool configSerial(uint32_t baudrate);
};
#endif
文件 2:MavlinkSerial.cpp(跨平台串口实现 + MAVLink 编解码)
#include "MavlinkSerial.h"
#include <cstring>
#include <unistd.h>
MavlinkSerial::MavlinkSerial() : m_serialHandle(0), m_isOpen(false) {
#ifdef _WIN32
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
#endif
}
MavlinkSerial::~MavlinkSerial() {
if (m_isOpen) {
closeSerial();
}
#ifdef _WIN32
WSACleanup();
#endif
}
bool MavlinkSerial::openSerial(const std::string& port) {
if (m_isOpen) {
return true;
}
#ifdef _WIN32
m_serialHandle = CreateFileA(port.c_str(), GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (m_serialHandle == INVALID_HANDLE_VALUE) {
return false;
}
#else
m_serialHandle = open(port.c_str(), O_RDWR | O_NOCTTY | O_NDELAY);
if (m_serialHandle < 0) {
return false;
}
fcntl(m_serialHandle, F_SETFL, 0);
#endif
if (!configSerial(BAUDRATE)) {
closeSerial();
return false;
}
m_isOpen = true;
return true;
}
void MavlinkSerial::closeSerial() {
if (!m_isOpen) {
return;
}
#ifdef _WIN32
CloseHandle(m_serialHandle);
#else
close(m_serialHandle);
#endif
m_serialHandle = 0;
m_isOpen = false;
}
bool MavlinkSerial::configSerial(uint32_t baudrate) {
#ifdef _WIN32
DCB dcbSerial = {0};
dcbSerial.DCBlength = sizeof(dcbSerial);
if (!GetCommState(m_serialHandle, &dcbSerial)) {
return false;
}
dcbSerial.BaudRate = baudrate;
dcbSerial.ByteSize = 8;
dcbSerial.Parity = NOPARITY;
dcbSerial.StopBits = ONESTOPBIT;
if (!SetCommState(m_serialHandle, &dcbSerial)) {
return false;
}
COMMTIMEOUTS timeouts = {0};
timeouts.ReadIntervalTimeout = 100;
timeouts.ReadTotalTimeoutConstant = 100;
timeouts.WriteTotalTimeoutConstant = 50;
SetCommTimeouts(m_serialHandle, &timeouts);
#else
struct termios tty;
memset(&tty, 0, sizeof(tty));
if (tcgetattr(m_serialHandle, &tty) != 0) {
return false;
}
speed_t speed;
switch (baudrate) {
case 9600: speed = B9600; break;
case 115200: speed = B115200; break;
case 230400: speed = B230400; break;
default: speed = B115200; break;
}
cfsetispeed(&tty, speed);
cfsetospeed(&tty, speed);
tty.c_cflag |= (CLOCAL | CREAD);
tty.c_cflag &= ~CSIZE;
tty.c_cflag |= CS8;
tty.c_cflag &= ~PARENB;
tty.c_cflag &= ~CSTOPB;
tty.c_cflag &= ~CRTSCTS;
tty.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
tty.c_oflag &= ~OPOST;
tty.c_cc[VMIN] = 1;
tty.c_cc[VTIME] = 10;
if (tcsetattr(m_serialHandle, TCSANOW, &tty) != 0) {
return false;
}
#endif
return true;
}
bool MavlinkSerial::sendMavlinkMsg(const mavlink_message_t& msg) {
if (!m_isOpen) {
return false;
}
uint8_t buf[MAVLINK_MAX_PACKET_LEN];
uint16_t len = mavlink_msg_to_send_buffer(buf, &msg);
#ifdef _WIN32
DWORD written = 0;
WriteFile(m_serialHandle, buf, len, &written, NULL);
return (written == len);
#else
ssize_t written = write(m_serialHandle, buf, len);
return (written == len);
#endif
}
bool MavlinkSerial::recvMavlinkMsg(mavlink_message_t& msg, mavlink_status_t& status) {
if (!m_isOpen) {
return false;
}
uint8_t ch;
ssize_t readLen = 0;
#ifdef _WIN32
DWORD read = 0;
ReadFile(m_serialHandle, &ch, 1, &read, NULL);
readLen = read;
#else
readLen = read(m_serialHandle, &ch, 1);
#endif
if (readLen != 1) {
return false;
}
if (mavlink_parse_char(MAVLINK_COMM_0, ch, &msg, &status) == MAVLINK_FRAMING_OK) {
return true;
}
return false;
}
文件 3:main.cpp(MAVLink 消息收发实战:心跳 + 姿态)
#include "MavlinkSerial.h"
#include <iostream>
#include <chrono>
#include <thread>
int main() {
MavlinkSerial mavSerial;
std::string serialPort;
#ifdef _WIN32
serialPort = "COM3";
#else
serialPort = "/dev/ttyUSB0";
#endif
if (!mavSerial.openSerial(serialPort)) {
std::cerr << "打开串口失败!请检查串口名和权限" << std::endl;
return -1;
}
std::cout << "串口打开成功,开始 MAVLink 通信..." << std::endl;
mavlink_status_t mavStatus;
mavlink_message_t mavMsg;
mavlink_heartbeat_t heartbeatMsg;
mavlink_attitude_t attitudeMsg;
uint8_t msgSeq = 0;
heartbeatMsg.type = MAV_TYPE_GCS;
heartbeatMsg.autopilot = MAV_AUTOPILOT_GENERIC;
heartbeatMsg.base_mode = 0;
heartbeatMsg.custom_mode = 0;
heartbeatMsg.system_status = MAV_STATE_STANDBY;
heartbeatMsg.mavlink_version = 2;
while (true) {
static auto lastHeartbeat = std::chrono::steady_clock::now();
auto now = std::chrono::steady_clock::now();
if (std::chrono::duration_cast<std::chrono::seconds>(now - lastHeartbeat).count() >= 1) {
mavlink_msg_heartbeat_encode(
SYS_ID,
COMP_ID,
&mavMsg,
&heartbeatMsg
);
mavMsg.seq = msgSeq++;
if (mavSerial.sendMavlinkMsg(mavMsg)) {
std::cout << "发送心跳消息成功,序列号:" << (int)(msgSeq-1) << std::endl;
} else {
std::cerr << "发送心跳消息失败" << std::endl;
}
lastHeartbeat = now;
}
static auto lastAttitude = std::chrono::steady_clock::now();
if (std::chrono::duration_cast<std::chrono::milliseconds>(now - lastAttitude).count() >= 500) {
attitudeMsg.roll = 0.05f;
attitudeMsg.pitch = -0.02f;
attitudeMsg.yaw = 0.1f;
attitudeMsg.rollspeed = 0.01f;
attitudeMsg.pitchspeed = 0.005f;
attitudeMsg.yawspeed = 0.02f;
attitudeMsg.time_boot_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch()
).count();
mavlink_msg_attitude_encode(
SYS_ID,
COMP_ID,
&mavMsg,
&attitudeMsg
);
mavMsg.seq = msgSeq++;
if (mavSerial.sendMavlinkMsg(mavMsg)) {
std::cout << "发送姿态消息成功,滚转:" << attitudeMsg.roll << "rad" << std::endl;
} else {
std::cerr << "发送姿态消息失败" << std::endl;
}
lastAttitude = now;
}
if (mavSerial.recvMavlinkMsg(mavMsg, mavStatus)) {
std::cout << "\n接收到 MAVLink 消息:" << std::endl;
std::cout << "系统 ID:" << (int)mavMsg.sysid << ",组件 ID:" << (int)mavMsg.compid << std::endl;
std::cout << "消息 ID:" << (int)mavMsg.msgid << ",序列号:" << (int)mavMsg.seq << std::endl;
switch (mavMsg.msgid) {
case MAVLINK_MSG_ID_HEARTBEAT: {
mavlink_heartbeat_t recvHeartbeat;
mavlink_msg_heartbeat_decode(&mavMsg, &recvHeartbeat);
std::cout << "消息类型:心跳" << std::endl;
std::cout << "设备类型:" << (int)recvHeartbeat.type << ",系统状态:" << (int)recvHeartbeat.system_status << std::endl;
break;
}
case MAVLINK_MSG_ID_ATTITUDE: {
mavlink_attitude_t recvAttitude;
mavlink_msg_attitude_decode(&mavMsg, &recvAttitude);
std::cout << "消息类型:姿态" << std::endl;
std::cout << "滚转:" << recvAttitude.roll << "rad,俯仰:" << recvAttitude.pitch << "rad,偏航:" << recvAttitude.yaw << "rad" << std::endl;
break;
}
default:
std::cout << "消息类型:未知(ID=" << (int)mavMsg.msgid << ")" << std::endl;
break;
}
}
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
mavSerial.closeSerial();
return 0;
}
3.2 核心 API 说明(MAVLink 编解码关键函数)
MAVLink 的编解码逻辑全部封装在官方头文件中,核心 API 为以下 4 个,是 C++ 开发的基础:
1. 消息编码:mavlink_msg_XXX_encode
- 功能:将消息结构体(如
mavlink_heartbeat_t)编码为 MAVLink 标准消息对象mavlink_message_t;
- 命名规则:
XXX为消息名(如 heartbeat、attitude),与消息结构体对应;
- 参数:
sysid(本设备系统 ID)、compid(本设备组件 ID)、msg(输出消息对象)、XXX(待编码的消息结构体);
- 示例:
mavlink_msg_heartbeat_encode(SYS_ID, COMP_ID, &mavMsg, &heartbeatMsg)。
2. 字节流转换:mavlink_msg_to_send_buffer
- 功能:MAVLink核心编码函数,将
mavlink_message_t对象转换为可传输的二进制字节流(符合 MAVLink 帧结构);
- 参数:
buf(输出字节流缓冲区,需大于MAVLINK_MAX_PACKET_LEN)、msg(待转换的 MAVLink 消息对象);
- 返回值:编码后的字节流长度(即消息帧总字节数,可直接通过串口 / 网络发送);
- 示例:
uint16_t len = mavlink_msg_to_send_buffer(buf, &mavMsg)。
3. 逐字节解码:mavlink_parse_char
- 功能:MAVLink核心解码函数,逐字节解析二进制流,自动校验帧结构、CRC、序列号,完成一帧解析后返回成功标志;
- 参数:
chan(通信通道,默认MAVLINK_COMM_0)、ch(待解析的单个字节)、msg(输出解码后的消息对象)、status(解码状态,包含解析统计);
- 返回值:
MAVLINK_FRAMING_OK表示解析完成一帧有效消息,其他值为解析中 / 错误;
- 示例:
if (mavlink_parse_char(MAVLINK_COMM_0, ch, &msg, &status) == MAVLINK_FRAMING_OK)。
4. 消息解析:mavlink_msg_XXX_decode
- 功能:将解码后的
mavlink_message_t对象解析为消息结构体,方便提取业务数据;
- 命名规则:与编码函数对应,
XXX为消息名;
- 参数:
msg(解码后的 MAVLink 消息对象)、XXX(输出消息结构体);
- 示例:
mavlink_msg_heartbeat_decode(&mavMsg, &recvHeartbeat)。
四、编译与运行
4.1 Linux(Ubuntu)编译运行
- 安装编译工具:
sudo apt install g++ cmake;
- 进入项目目录,创建编译脚本
build.sh:
#!/bin/bash
g++ -std=c++11 main.cpp MavlinkSerial.cpp -o mavlink_demo
sudo chmod 777 /dev/ttyUSB0
- 执行脚本:
chmod +x build.sh && ./build.sh;
- 运行程序:
./mavlink_demo。
4.2 Windows(MinGW)编译运行
- 安装 MinGW,配置环境变量;
- 打开 CMD,进入项目目录,执行编译命令:
g++ -std=c++11 main.cpp MavlinkSerial.cpp -o mavlink_demo.exe -lws2_32
(-lws2_32链接 Windows 网络库,必须添加);
3. 运行程序:mavlink_demo.exe(需确保串口COM3存在,根据实际修改)。
4.3 运行结果说明
程序运行后,将每秒发送 1 次心跳消息、每 500ms 发送 1 次模拟姿态消息,同时实时接收串口端的 MAVLink 消息并解析打印。若连接无人机飞控(如 PX4、ArduPilot),将能接收到飞控发送的真实心跳、姿态、GPS 等消息。
五、扩展开发:网络通信(TCP/UDP)
除了串口通信,MAVLink 也广泛用于网络通信(如地面站通过 WiFi/4G 与无人机通信),其核心编解码逻辑与串口完全一致,仅需替换数据传输层(将串口的read/write替换为网络的send/recv)。
核心扩展思路
- 基于 Linux
socket/WindowsWinsock实现 TCP/UDP 客户端 / 服务端;
- 网络数据发送:将
mavlink_msg_to_send_buffer编码后的字节流通过send/sendto发送;
- 网络数据接收:通过
recv/recvfrom读取网络字节流,再通过mavlink_parse_char逐字节解码;
- 编解码 API、消息对象
mavlink_message_t完全复用,无需修改。
六、常见问题与解决方案
6.1 串口打开失败
- 原因 1:串口名错误→解决方案:Linux 通过
dmesg | grep tty查看串口名,Windows 通过设备管理器查看;
- 原因 2:Linux 串口权限不足→解决方案:
sudo chmod 777 /dev/ttyUSB0或添加用户到 dialout 组:sudo usermod -aG dialout 用户名(重启生效);
- 原因 3:串口被其他程序占用→解决方案:关闭串口调试工具(如串口助手、QGroundControl)。
6.2 消息发送成功但接收不到
- 原因 1:波特率 / 串口配置不匹配→解决方案:确保两端波特率均为 115200、8N1(无校验、1 停止位);
- 原因 2:MAVLink 版本不兼容→解决方案:两端均使用 MAVLink 2.0(避免一端 v1.0 一端 v2.0);
- 原因 3:系统 ID / 组件 ID 配置错误→解决方案:确保通信双方的寻址 ID 正确(地面站可接收所有 ID 的消息)。
6.3 解码失败 / CRC 校验错误
- 原因 1:数据传输过程中损坏→解决方案:检查硬件连接(如串口线接触不良、WiFi 信号差);
- 原因 2:解码时未逐字节解析→解决方案:严格使用
mavlink_parse_char逐字节解析,不可批量解析;
- 原因 3:MAVLink 头文件版本不匹配→解决方案:确保通信双方使用相同消息集(如 common.xml)生成的头文件。
6.4 编译报错:MAVLink 头文件未找到
- 解决方案:检查代码中
#include "mavlink/mavlink.h"的路径,确保mavlink文件夹在项目根目录,或添加头文件路径编译:g++ -I/xxx/mavlink_dir main.cpp ...。
七、总结
本文详细讲解了 MAVLink 通信协议的核心原理和 C++ 实战实现,核心要点总结如下:
- MAVLink 是轻量级应用层协议,基于二进制编码,专为嵌入式设备优化,核心帧结构包含起始符、有效载荷、CRC 校验等字段;
- MAVLink C++ 开发无需编译库,仅需官方头文件,核心编解码通过 4 个 API 完成:
encode(结构体转消息对象)、mavlink_msg_to_send_buffer(消息对象转二进制流)、mavlink_parse_char(逐字节解码)、decode(消息对象转结构体);
- 实现了跨平台串口通信的 MAVLink 收发,兼容 Linux/Windows,可直接移植到嵌入式系统,消息收发流程为「编码→传输→解码→解析」;
- 网络通信(TCP/UDP)可直接复用编解码逻辑,仅需替换数据传输层,扩展性极强。
相关免费在线工具
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Gemini 图片去水印
基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online