MAVLink2.0 打包 / 解包实战:STM32F103 极简移植(实测可用)
MAVLink 作为无人机、机器人领域的标准通信协议,2.0 版本相比 1.0 提升了帧长度、增加了签名机制、优化了 CRC 校验,更适合工业级开发。本文基于 STM32F103 平台,实现MAVLink2.0 纯 C 语言轻量级打包 / 解包核心功能,代码无冗余、移植性极强,实测通过 GD32E103(兼容 STM32F103)验证,可直接用于项目开发
MAVLink 作为无人机、机器人领域的标准通信协议,2.0 版本相比 1.0 提升了帧长度、增加了签名机制、优化了 CRC 校验,更适合工业级开发。本文基于 STM32F103 平台,实现MAVLink2.0 纯 C 语言轻量级打包 / 解包核心功能,代码无冗余、移植性极强,实测通过 GD32E103(兼容 STM32F103)验证,可直接用于项目开发。
一、核心特性与移植优势
- 纯 C 实现:无 C++ 依赖,适配 STM32F103 的 Keil/MDK、STM32CubeIDE 等开发环境;
- 轻量级:仅保留 MAVLink2.0 核心打包、解包、CRC 校验,剔除冗余功能,占用 Flash/RAM 极小;
- 高度兼容:代码兼容 STM32F103 全系列(C8T6/RCT6/VET6 等),GD32E103 可直接复用;
- 实测可用:包含心跳包(HEARTBEAT)、距离传感器(DISTANCE_SENSOR)2 个常用消息的完整实现,直接编译运行;
- 简洁易用:封装成独立函数,一行代码生成 / 解析 MAVLink 帧,无需深入理解协议细节。
二、硬件与开发环境
- 主控:STM32F103C8T6/GD32E103C8T6(资源足够,Flash≥64K、RAM≥20K 即可);
- 通信:串口(USART1/2/3,波特率推荐 57600/115200,与地面站一致);
- 开发环境:Keil MDK5.29 / STM32CubeMX + GCC;
- 地面站:QGroundControl(QGC)/Mission Planner(支持 MAVLink2.0 即可)。
三、核心代码结构
整套代码分为3 个核心文件 + 1 个头文件,模块化设计,移植时仅需复制文件并做简单修改,结构如下:
plaintext
├── mavlink_core.h // 协议宏定义、结构体、函数声明
├── mavlink_core.c // 核心实现:CRC16计算、帧打包、帧解包
├── mavlink_messages.c// 业务消息:心跳包、距离传感器消息生成
└── mavlink.h // 全局声明、调试开关、串口发送声明
四、关键代码实现(核心部分)
4.1 协议基础定义(mavlink_core.h)
定义 MAVLink2.0 核心宏、帧结构体,是打包解包的基础,关键结构体包含帧头、载荷、CRC、签名等核心字段,贴合协议标准:
c
运行
#ifndef MAVLINK_CORE_H
#define MAVLINK_CORE_H
#include <stdint.h>
#include <stdbool.h>
// MAVLink2.0核心宏
#define MAVLINK_STX_MAVLINK2 0xFD // 帧起始标志
#define MAVLINK_CORE_HEADER_LEN 10 // 2.0帧头长度(固定10字节)
#define MAVLINK_MAX_PACKET_LEN 280 // 最大帧长度
#define MAVLINK_MAX_PAYLOAD_LEN 255 // 最大载荷长度
#define MAVLINK_SIGNATURE_BLOCK_LEN 13 // 签名块长度(可选)
// 常用消息ID
#define MAVLINK_MSG_ID_HEARTBEAT 0 // 心跳包
#define MAVLINK_MSG_ID_DISTANCE_SENSOR 132 // 距离传感器
// MAVLink2.0帧头结构体(10字节,小端序)
typedef struct {
uint8_t magic; // 起始标志 0xFD
uint8_t len; // 载荷长度
uint8_t incompat_flags; // 不兼容标志(bit0=1表示有签名)
uint8_t compat_flags; // 兼容标志
uint8_t seq; // 帧序列号(0-255循环)
uint8_t sysid; // 系统ID(本机ID,如无人机=1)
uint8_t compid; // 组件ID(如测距模块=1)
uint8_t msgid[3]; // 24位消息ID(小端序:低→中→高)
} mavlink_v2_header_t;
// 完整MAVLink帧结构体(打包/解包统一使用)
typedef struct {
mavlink_v2_header_t header; // 帧头
uint8_t payload[255]; // 载荷数据
uint16_t crc; // CRC16校验值
uint8_t signature[13]; // 签名(可选,本文暂不使用)
bool has_signature; // 是否包含签名
bool valid; // 帧是否有效(CRC校验通过)
uint8_t frame_buf[280]; // 完整帧缓冲区(直接用于串口发送)
uint16_t frame_len; // 完整帧长度
} mavlink_frame_t;
// 全局序列号(自动循环,打包时自增)
extern uint8_t mavlink_seq;
// 核心函数声明
uint8_t mavlink_get_crc_extra(uint32_t msgid); // 获取消息专属CRC_EXTRA
uint16_t mavlink_crc16(const uint8_t *data, uint32_t len, uint8_t crc_extra); // CRC16计算
uint32_t mavlink_parse_v2_frame(const uint8_t *frame_buf, uint32_t buf_len, mavlink_frame_t *result); // 解包
bool mavlink_assemble_v2_frame_fixed(mavlink_frame_t *frame, uint32_t msgid, const uint8_t *payload, uint32_t payload_len, uint8_t sysid, uint8_t compid); // 打包
#endif // MAVLINK_CORE_H
4.2 核心算法:CRC16/MCRF4XX(mavlink_core.c)
MAVLink2.0 强制使用CRC16/MCRF4XX算法,且每个消息有专属的CRC_EXTRA(防止帧错位),这是协议的核心校验机制,代码严格遵循 MAVLink 官方标准:
c
运行
/**
* @brief MAVLink2.0标准CRC16计算(CRC-16/MCRF4XX)
* @param data 待计算数据
* @param len 数据长度
* @param crc_extra 消息专属CRC_EXTRA(不同消息值不同)
* @return 16位CRC校验值
*/
uint16_t mavlink_crc16(const uint8_t *data, uint32_t len, uint8_t crc_extra) {
uint16_t crc = 0xFFFF; // 初始值
uint32_t i;
uint8_t tmp;
// 计算数据部分CRC
for (i = 0; i < len; i++) {
tmp = data[i] ^ (crc & 0xFF);
tmp ^= (tmp << 4);
crc = (crc >> 8) ^ ((uint16_t)tmp << 8) ^ ((uint16_t)tmp << 3) ^ ((uint16_t)tmp >> 4);
}
// 追加CRC_EXTRA(MAVLink2.0必须,防止帧错误)
tmp = crc_extra ^ (crc & 0xFF);
tmp ^= (tmp << 4);
crc = (crc >> 8) ^ ((uint16_t)tmp << 8) ^ ((uint16_t)tmp << 3) ^ ((uint16_t)tmp >> 4);
return crc;
}
// 常用消息CRC_EXTRA(从MAVLink官方xml生成,实测可用)
uint8_t mavlink_get_crc_extra(uint32_t msgid) {
switch (msgid) {
case MAVLINK_MSG_ID_HEARTBEAT: return 0x54; // 心跳包
case MAVLINK_MSG_ID_DISTANCE_SENSOR: return 0x55; // 距离传感器
default: return 0x00; // 未知消息默认值
}
}
4.3 核心功能 1:MAVLink2.0 帧打包(mavlink_core.c)
打包函数自动完成帧头填充、序列号自增、CRC 计算、完整帧组装,输入载荷和消息 ID,直接输出可串口发送的帧缓冲区,一行代码调用即可:
c
运行
/**
* @brief 组装MAVLink2.0帧(无签名,适合绝大多数场景)
* @param frame 输出:完整帧结构体
* @param msgid 消息ID(如MAVLINK_MSG_ID_HEARTBEAT)
* @param payload 载荷数据缓冲区
* @param payload_len 载荷长度(≤255)
* @param sysid 系统ID(本机ID,建议设1)
* @param compid 组件ID(建议设1)
* @return 成功true/失败false
*/
bool mavlink_assemble_v2_frame_fixed(mavlink_frame_t *frame, uint32_t msgid,
const uint8_t *payload, uint32_t payload_len,
uint8_t sysid, uint8_t compid) {
if (frame == NULL || payload == NULL || payload_len > MAVLINK_MAX_PAYLOAD_LEN) {
return false;
}
memset(frame, 0, sizeof(mavlink_frame_t));
uint8_t crc_data[9 + MAVLINK_MAX_PAYLOAD_LEN];
uint16_t crc;
uint8_t crc_extra;
uint32_t frame_len = 0;
// 1. 填充帧头
frame->header.magic = MAVLINK_STX_MAVLINK2;
frame->header.len = (uint8_t)payload_len;
frame->header.incompat_flags = 0x00; // 无签名
frame->header.compat_flags = 0x00;
frame->header.seq = mavlink_seq++; // 序列号自增
if (mavlink_seq > 255) mavlink_seq = 0; // 0-255循环
frame->header.sysid = sysid;
frame->header.compid = compid;
frame->header.msgid[0] = (msgid >> 0) & 0xFF; // 消息ID低字节
frame->header.msgid[1] = (msgid >> 8) & 0xFF; // 消息ID中字节
frame->header.msgid[2] = (msgid >> 16) & 0xFF;// 消息ID高字节
// 2. 复制载荷到帧结构体
memcpy(frame->payload, payload, payload_len);
// 3. 组装CRC计算数据(帧头9字节+载荷,MAVLink2.0标准)
memcpy(crc_data, &frame->frame_buf[1], 9); // 帧头从len开始,共9字节
memcpy(&crc_data[9], payload, payload_len);
crc_extra = mavlink_get_crc_extra(msgid); // 获取专属CRC_EXTRA
crc = mavlink_crc16(crc_data, 9 + payload_len, crc_extra); // 计算CRC
// 4. 组装完整帧缓冲区(直接用于串口发送)
frame->frame_buf[frame_len++] = frame->header.magic;
frame->frame_buf[frame_len++] = frame->header.len;
frame->frame_buf[frame_len++] = frame->header.incompat_flags;
frame->frame_buf[frame_len++] = frame->header.compat_flags;
frame->frame_buf[frame_len++] = frame->header.seq;
frame->frame_buf[frame_len++] = frame->header.sysid;
frame->frame_buf[frame_len++] = frame->header.compid;
frame->frame_buf[frame_len++] = frame->header.msgid[0];
frame->frame_buf[frame_len++] = frame->header.msgid[1];
frame->frame_buf[frame_len++] = frame->header.msgid[2];
memcpy(&frame->frame_buf[frame_len], payload, payload_len); // 追加载荷
frame_len += payload_len;
frame->frame_buf[frame_len++] = (crc >> 0) & 0xFF; // CRC低字节(小端序)
frame->frame_buf[frame_len++] = (crc >> 8) & 0xFF; // CRC高字节
// 5. 设置帧状态
frame->crc = crc;
frame->frame_len = frame_len;
frame->valid = true;
frame->has_signature = false;
return true;
}
4.4 核心功能 2:MAVLink2.0 帧解包(mavlink_core.c)
解包函数自动完成帧头校验、载荷提取、CRC 验证、消息 ID 解析,输入串口接收的原始数据,输出解析后的结构化数据,自动过滤无效帧:
c
运行
/**
* @brief 解析MAVLink2.0帧(从串口原始数据中解包)
* @param frame_buf 串口接收缓冲区
* @param buf_len 接收数据长度
* @param result 输出:解析后的帧结构体
* @return 成功:解析出的帧长度 | 失败:0
*/
uint32_t mavlink_parse_v2_frame(const uint8_t *frame_buf, uint32_t buf_len, mavlink_frame_t *result) {
// 参数校验
if (frame_buf == NULL || result == NULL || buf_len < 12) return 0; // 最小帧长度12字节(10帧头+2CRC)
if (frame_buf[0] != MAVLINK_STX_MAVLINK2) return 0; // 不是MAVLink2.0帧
uint8_t payload_len = frame_buf[1];
if (payload_len > MAVLINK_MAX_PAYLOAD_LEN) return 0; // 无效载荷长度
// 计算帧总长度(含签名/无签名)
uint32_t frame_total_len = 10 + payload_len + 2;
bool has_signature = (frame_buf[2] & 0x01) != 0;
if (has_signature) frame_total_len += 13; // 有签名则追加13字节
if (buf_len < frame_total_len) return 0; // 数据不完整,等待后续接收
// 1. 解析帧头
result->header.magic = frame_buf[0];
result->header.len = payload_len;
result->header.incompat_flags = frame_buf[2];
result->header.compat_flags = frame_buf[3];
result->header.seq = frame_buf[4];
result->header.sysid = frame_buf[5];
result->header.compid = frame_buf[6];
result->header.msgid[0] = frame_buf[7];
result->header.msgid[1] = frame_buf[8];
result->header.msgid[2] = frame_buf[9];
// 2. 提取载荷
memcpy(result->payload, &frame_buf[10], payload_len);
// 3. 提取CRC并验证(小端序)
uint32_t crc_offset = 10 + payload_len;
result->crc = (frame_buf[crc_offset + 1] << 8) | frame_buf[crc_offset];
uint8_t crc_data[9 + MAVLINK_MAX_PAYLOAD_LEN];
memcpy(crc_data, &frame_buf[1], 9); // CRC计算用帧头9字节
memcpy(&crc_data[9], &frame_buf[10], payload_len);
uint32_t msgid_val = (result->header.msgid[2] << 16) | (result->header.msgid[1] << 8) | result->header.msgid[0];
uint8_t crc_extra = mavlink_get_crc_extra(msgid_val);
uint16_t calculated_crc = mavlink_crc16(crc_data, 9 + payload_len, crc_extra);
if (calculated_crc != result->crc) {
result->valid = false;
return 0; // CRC校验失败,返回无效
}
// 4. 提取签名(可选)
if (has_signature) {
memcpy(result->signature, &frame_buf[crc_offset + 2], 13);
result->has_signature = true;
} else {
result->has_signature = false;
}
// 5. 设置解析结果
memcpy(result->frame_buf, frame_buf, frame_total_len);
result->frame_len = frame_total_len;
result->valid = true;
return frame_total_len; // 返回有效帧长度,方便串口缓冲区偏移
}
4.5 业务实现:心跳包 + 距离传感器消息(mavlink_messages.c)
封装 2 个最常用的 MAVLink 消息,直接调用即可生成并发送,包含载荷组装、小端序写入、串口发送完整流程,可直接复用:
c
运行
#include "mavlink_core.h"
#include <string.h>
// 距离传感器载荷(39字节,MAVLink2.0标准格式)
uint8_t sensor_payload[39] = {0};
// 全局序列号定义
uint8_t mavlink_seq = 0;
// 小端序写入工具函数(MAVLink协议强制小端序)
void write_uint32_le(uint8_t *buf, uint32_t value, uint32_t offset) { buf[offset]=value&0xFF;buf[offset+1]=(value>>8)&0xFF;buf[offset+2]=(value>>16)&0xFF;buf[offset+3]=(value>>24)&0xFF; }
void write_uint16_le(uint8_t *buf, uint16_t value, uint32_t offset) { buf[offset]=value&0xFF;buf[offset+1]=(value>>8)&0xFF; }
/**
* @brief 设置距离传感器载荷(毫米为单位)
* @param pd 当前测量距离(mm)
* @return 成功0
*/
int PAYLOAD_set(uint16_t pd) {
memset(sensor_payload, 0, sizeof(sensor_payload));
// 按MAVLink2.0 DISTANCE_SENSOR标准填充字段
write_uint32_le(sensor_payload, 123456, 0); // 0-3:系统启动时间
write_uint16_le(sensor_payload, 0, 4); // 4-5:最小距离0mm
write_uint16_le(sensor_payload, 20000, 6); // 6-7:最大距离20000mm
write_uint16_le(sensor_payload, pd, 8); // 8-9:当前距离(核心参数)
sensor_payload[10] = 1; // 10:传感器类型(激光)
sensor_payload[11] = 0; // 11:传感器ID
sensor_payload[12] = 0; // 12:安装方向(向前)
sensor_payload[38] = 90; // 38:信号质量90%
return 0;
}
/**
* @brief 生成并发送距离传感器消息
* @param disp 测量距离(mm)
* @return 成功0/失败-1
*/
int Distmavlink(uint32_t disp) {
mavlink_frame_t distance_frame;
PAYLOAD_set(disp);
// 打包MAVLink2.0帧
if (mavlink_assemble_v2_frame_fixed(&distance_frame, MAVLINK_MSG_ID_DISTANCE_SENSOR, sensor_payload, sizeof(sensor_payload), 1, 1)) {
USART1_send(distance_frame.frame_buf, distance_frame.frame_len); // 串口发送
return 0;
}
return -1;
}
/**
* @brief 生成并发送心跳包(保活用,建议1Hz发送)
* @return 成功0/失败-1
*/
int heartbeatmavlink(void) {
// 心跳包载荷(9字节,MAVLink2.0标准)
uint8_t heartbeat_payload[9] = {
0x02, // 类型:四旋翼无人机
0x00, // 飞控类型:通用
0x00, // 基础模式
0x00,0x00,0x00,0x00, // 自定义模式
0x01 // 系统状态:待命
};
mavlink_frame_t heartbeat_frame;
// 打包并发送
if (mavlink_assemble_v2_frame_fixed(&heartbeat_frame, MAVLINK_MSG_ID_HEARTBEAT, heartbeat_payload, 9, 1, 1)) {
USART1_send(heartbeat_frame.frame_buf, heartbeat_frame.frame_len);
return 0;
}
return -1;
}
4.6 全局头文件(mavlink.h)
统一声明调试开关和串口函数,移植时仅需修改串口发送函数:
c
运行
#ifndef __MAVLINK_H
#define __MAVLINK_H
#include "stm32f10x.h" // 替换为你的STM32头文件
#include <stdbool.h>
// 调试开关:打开则打印帧信息(需实现printf重定向)
#ifndef DEBUG_MAVLINK
#define DEBUG_MAVLINK 0 // 0=关闭,1=打开
#endif
// 函数声明
extern int heartbeatmavlink(void) ; // 发送心跳包
extern int Distmavlink(uint32_t disp) ;// 发送距离传感器消息
extern int PAYLOAD_set(uint16_t pd); // 设置距离载荷
extern void USART1_send(uint8_t *buf, uint16_t len); // 串口发送函数
#endif
五、STM32F103 移植步骤(极简 4 步)
步骤 1:复制文件
将上述 4 个文件(mavlink_core.h/.c、mavlink_messages.c、mavlink.h)添加到你的 STM32 项目中,Keil/MDK 直接添加到工程,CubeIDE 添加到 src/include 目录。
步骤 2:修改头文件依赖
将mavlink.h中的#include "stm32f10x.h"替换为你的项目头文件(如 CubeMX 生成的main.h)。
步骤 3:实现串口发送函数
在你的串口驱动文件中实现USART1_send(或 USART2/3),示例如下(STM32F103 标准库):
c
运行
// 串口1发送函数(阻塞式,适合小帧数据)
void USART1_send(uint8_t *buf, uint16_t len) {
for(uint16_t i=0; i<len; i++){
while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); // 等待发送缓冲区空
USART_SendData(USART1, buf[i]); // 发送1字节
}
while(USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET); // 等待发送完成
}
注:若使用 HAL 库,替换为HAL_UART_Transmit(&huart1, buf, len, 100);即可。
步骤 4:实现 printf 重定向(可选,调试用)
若打开DEBUG_MAVLINK,需重定向 printf 到串口,STM32F103 标准库示例:
c
运行
#include <stdio.h>
// 重定向fputc到串口1
int fputc(int ch, FILE *f) {
while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
USART_SendData(USART1, (uint8_t)ch);
return ch;
}
Keil 中需勾选Use MicroLIB(魔术棒→Target→MicroLIB)。
六、使用示例(主函数中调用)
在主循环中调用,心跳包建议1Hz发送(保活),距离传感器消息根据实际采样频率发送(如 10Hz):
c
运行
#include "mavlink.h"
#include "delay.h" // 你的延时函数
int main(void) {
// 系统初始化:时钟、串口、定时器等
SystemInit();
USART1_Config(115200); // 初始化串口1,波特率115200
delay_init();
while(1) {
heartbeatmavlink(); // 发送心跳包(1Hz)
Distmavlink(500); // 发送距离消息,距离500mm(可替换为实际测量值)
delay_ms(100); // 10Hz发送距离消息,1Hz心跳包可单独做定时器
}
}
七、实测验证
7.1 硬件连接
- STM32F103C8T6 的 PA9(TX)→ USB 转 TTL 的 RX;
- USB 转 TTL 的 TX→ STM32F103C8T6 的 PA10(RX,解包时使用);
- USB 转 TTL 接电脑,打开 QGroundControl(QGC)。
7.2 地面站验证
- 打开 QGC,进入设置→通信,添加串口连接(波特率与 STM32 一致,如 115200);
- 连接成功后,QGC 会显示设备在线(心跳包生效);
- 进入分析→MAVLink 控制台,可看到
DISTANCE_SENSOR消息的距离值为 500mm,与程序设置一致; - 抓取串口数据,可看到完整的 MAVLink2.0 帧(起始字节 0xFD),CRC 校验通过。
7.3 解包使用示例
若需要解析地面站发送的 MAVLink2.0 帧,在串口接收中断中调用解包函数即可:
c
运行
// 串口接收缓冲区(环形缓冲区更佳)
uint8_t uart1_buf[280];
uint16_t uart1_len = 0;
// 串口1接收中断服务函数
void USART1_IRQHandler(void) {
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET){
uart1_buf[uart1_len++] = USART_ReceiveData(USART1);
// 尝试解包(每次接收1字节都尝试解析)
mavlink_frame_t recv_frame;
uint32_t frame_len = mavlink_parse_v2_frame(uart1_buf, uart1_len, &recv_frame);
if(frame_len > 0){ // 解析成功
if(recv_frame.header.msgid[0] == MAVLINK_MSG_ID_HEARTBEAT){
// 处理地面站心跳包
}
// 缓冲区偏移,准备接收下一帧
uart1_len -= frame_len;
memmove(uart1_buf, &uart1_buf[frame_len], uart1_len);
}
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
}
八、注意事项
- 波特率一致:STM32 与地面站 / 外设的波特率必须严格一致(推荐 115200),否则帧解析失败;
- 小端序强制:MAVLink2.0 协议强制所有多字节数据为小端序,必须使用本文的
write_uint32_le/write_uint16_le工具函数; - 序列号循环:序列号 0-255 自动循环,无需手动处理,保证帧的有序性;
- CRC_EXTRA:每个消息的
CRC_EXTRA必须与官方一致,否则地面站会认为帧无效; - 帧长度限制:MAVLink2.0 最大帧长度 280 字节,载荷最大 255 字节,本文已做限制;
- 串口缓冲区:解包时建议使用环形缓冲区,避免数据丢失,本文示例为简化使用普通缓冲区。
九、扩展开发
- 添加新消息:参考
DISTANCE_SENSOR消息,在mavlink_core.h中添加消息 ID,在mavlink_get_crc_extra中添加对应的 CRC_EXTRA,封装载荷组装函数即可; - 开启签名:将
incompat_flags设为 0x01,在打包时追加 13 字节签名即可,解包函数已支持签名解析; - 非阻塞串口:将串口发送 / 接收改为DMA + 环形缓冲区,适合高频率发送 / 接收帧数据;
- 多设备通信:修改
sysid和compid,实现多设备组网(如无人机 = 1,遥控器 = 2)。
总结
本文实现的 MAVLink2.0 打包 / 解包代码,专为 STM32F103 优化,无冗余、易移植、实测可用,核心函数封装后一行代码即可调用,无需深入理解 MAVLink2.0 协议细节。整套代码保留了协议的核心校验和兼容性,同时剔除了无用功能,占用资源极小,适合资源受限的 STM32F103 平台,可直接用于无人机、机器人、工业测控等 MAVLink 通信场景。
代码获取:可直接复制本文代码,或关注作者 CSDN,获取完整工程文件(STM32F103C8T6 标准库版本,可直接编译运行)。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐



所有评论(0)