MAVLink2.0 Packing/Unpacking Practice: Minimal Transplantation for STM32F103 (Tested & Usable)
MAVLink, the standard communication protocol for UAVs and robotics, has been upgraded to version 2.0 with extended frame length, added signature mechanism, and optimized CRC verification, making it more suitable for industrial-grade development. This article implements the lightweight pure-C packing/unpacking core functions of MAVLink2.0 based on the STM32F103 platform. The code is redundant-free and highly portable, verified on GD32E103 (STM32F103-compatible), and can be directly used in project development.
I. Core Features & Transplantation Advantages
- Pure C Implementation: No C++ dependencies, compatible with development environments such as Keil/MDK and STM32CubeIDE for STM32F103;
- Lightweight: Only retains MAVLink2.0 core packing, unpacking and CRC verification, removes redundant functions, and occupies minimal Flash/RAM;
- High Compatibility: The code is compatible with all STM32F103 series (C8T6/RCT6/VET6, etc.), and can be directly reused on GD32E103;
- Tested & Usable: Includes complete implementation of 2 commonly used messages (HEARTBEAT, DISTANCE_SENSOR), which can be compiled and run directly;
- Simple & Easy to Use: Encapsulated into independent functions, generate/parse MAVLink frames with one line of code without in-depth understanding of protocol details.
II. Hardware & Development Environment
- MCU: STM32F103C8T6/GD32E103C8T6 (sufficient resources, Flash≥64K, RAM≥20K is enough);
- Communication: UART (USART1/2/3, baud rate recommended 57600/115200, consistent with ground station);
- Development Environment: Keil MDK5.29 / STM32CubeMX + GCC;
- Ground Station: QGroundControl (QGC)/Mission Planner (supports MAVLink2.0).
III. Core Code Structure
The entire code consists of 3 core files + 1 header file with modular design. For transplantation, just copy the files and make simple modifications. The structure is as follows:
plaintext
├── mavlink_core.h // Protocol macro definitions, structures, function declarations
├── mavlink_core.c // Core implementation: CRC16 calculation, frame packing, frame unpacking
├── mavlink_messages.c// Business messages: heartbeat packet, distance sensor message generation
└── mavlink.h // Global declarations, debug switch, UART transmission declaration
IV. Key Code Implementation (Core Part)
4.1 Protocol Basic Definitions (mavlink_core.h)
Define MAVLink2.0 core macros and frame structures, the foundation of packing and unpacking. The key structure contains core fields such as frame header, payload, CRC, signature, which conform to the protocol standard:
c
运行
#ifndef MAVLINK_CORE_H
#define MAVLINK_CORE_H
#include <stdint.h>
#include <stdbool.h>
// MAVLink2.0 core macros
#define MAVLINK_STX_MAVLINK2 0xFD // Frame start flag
#define MAVLINK_CORE_HEADER_LEN 10 // 2.0 frame header length (fixed 10 bytes)
#define MAVLINK_MAX_PACKET_LEN 280 // Maximum frame length
#define MAVLINK_MAX_PAYLOAD_LEN 255 // Maximum payload length
#define MAVLINK_SIGNATURE_BLOCK_LEN 13 // Signature block length (optional)
// Common message IDs
#define MAVLINK_MSG_ID_HEARTBEAT 0 // Heartbeat packet
#define MAVLINK_MSG_ID_DISTANCE_SENSOR 132 // Distance sensor
// MAVLink2.0 frame header structure (10 bytes, little-endian)
typedef struct {
uint8_t magic; // Start flag 0xFD
uint8_t len; // Payload length
uint8_t incompat_flags; // Incompatibility flags (bit0=1 means with signature)
uint8_t compat_flags; // Compatibility flags
uint8_t seq; // Frame sequence number (0-255 cycle)
uint8_t sysid; // System ID (local ID, e.g., UAV=1)
uint8_t compid; // Component ID (e.g., ranging module=1)
uint8_t msgid[3]; // 24-bit message ID (little-endian: low→middle→high)
} mavlink_v2_header_t;
// Complete MAVLink frame structure (unified for packing/unpacking)
typedef struct {
mavlink_v2_header_t header; // Frame header
uint8_t payload[255]; // Payload data
uint16_t crc; // CRC16 check value
uint8_t signature[13]; // Signature (optional, not used in this article)
bool has_signature; // Whether to contain signature
bool valid; // Whether the frame is valid (CRC verification passed)
uint8_t frame_buf[280]; // Complete frame buffer (directly used for UART transmission)
uint16_t frame_len; // Complete frame length
} mavlink_frame_t;
// Global sequence number (automatic cycle, increment on packing)
extern uint8_t mavlink_seq;
// Core function declarations
uint8_t mavlink_get_crc_extra(uint32_t msgid); // Get message-specific CRC_EXTRA
uint16_t mavlink_crc16(const uint8_t *data, uint32_t len, uint8_t crc_extra); // CRC16 calculation
uint32_t mavlink_parse_v2_frame(const uint8_t *frame_buf, uint32_t buf_len, mavlink_frame_t *result); // Unpacking
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); // Packing
#endif // MAVLINK_CORE_H
4.2 Core Algorithm: CRC16/MCRF4XX (mavlink_core.c)
MAVLink2.0 enforces the CRC16/MCRF4XX algorithm, and each message has a dedicated CRC_EXTRA (to prevent frame misalignment), which is the core verification mechanism of the protocol. The code strictly follows the MAVLink official standard:
c
运行
/**
* @brief MAVLink2.0 standard CRC16 calculation (CRC-16/MCRF4XX)
* @param data Data to be calculated
* @param len Data length
* @param crc_extra Message-specific CRC_EXTRA (different values for different messages)
* @return 16-bit CRC check value
*/
uint16_t mavlink_crc16(const uint8_t *data, uint32_t len, uint8_t crc_extra) {
uint16_t crc = 0xFFFF; // Initial value
uint32_t i;
uint8_t tmp;
// Calculate CRC for data part
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);
}
// Append CRC_EXTRA (mandatory for MAVLink2.0 to prevent frame errors)
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;
}
// Common message CRC_EXTRA (generated from MAVLink official xml, tested usable)
uint8_t mavlink_get_crc_extra(uint32_t msgid) {
switch (msgid) {
case MAVLINK_MSG_ID_HEARTBEAT: return 0x54; // Heartbeat packet
case MAVLINK_MSG_ID_DISTANCE_SENSOR: return 0x55; // Distance sensor
default: return 0x00; // Default value for unknown messages
}
}
4.3 Core Function 1: MAVLink2.0 Frame Packing (mavlink_core.c)
The packing function automatically completes frame header filling, sequence number increment, CRC calculation, and complete frame assembly. Input the payload and message ID, and directly output the frame buffer for UART transmission, which can be called with one line of code:
c
运行
/**
* @brief Assemble MAVLink2.0 frame (no signature, suitable for most scenarios)
* @param frame Output: complete frame structure
* @param msgid Message ID (e.g., MAVLINK_MSG_ID_HEARTBEAT)
* @param payload Payload data buffer
* @param payload_len Payload length (≤255)
* @param sysid System ID (local ID, recommended 1)
* @param compid Component ID (recommended 1)
* @return Success true / Failure 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. Fill frame header
frame->header.magic = MAVLINK_STX_MAVLINK2;
frame->header.len = (uint8_t)payload_len;
frame->header.incompat_flags = 0x00; // No signature
frame->header.compat_flags = 0x00;
frame->header.seq = mavlink_seq++; // Increment sequence number
if (mavlink_seq > 255) mavlink_seq = 0; // 0-255 cycle
frame->header.sysid = sysid;
frame->header.compid = compid;
frame->header.msgid[0] = (msgid >> 0) & 0xFF; // Message ID low byte
frame->header.msgid[1] = (msgid >> 8) & 0xFF; // Message ID middle byte
frame->header.msgid[2] = (msgid >> 16) & 0xFF;// Message ID high byte
// 2. Copy payload to frame structure
memcpy(frame->payload, payload, payload_len);
// 3. Assemble CRC calculation data (9-byte frame header + payload, MAVLink2.0 standard)
memcpy(crc_data, &frame->frame_buf[1], 9); // Frame header starts from len, 9 bytes total
memcpy(&crc_data[9], payload, payload_len);
crc_extra = mavlink_get_crc_extra(msgid); // Get dedicated CRC_EXTRA
crc = mavlink_crc16(crc_data, 9 + payload_len, crc_extra); // Calculate CRC
// 4. Assemble complete frame buffer (directly used for UART transmission)
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); // Append payload
frame_len += payload_len;
frame->frame_buf[frame_len++] = (crc >> 0) & 0xFF; // CRC low byte (little-endian)
frame->frame_buf[frame_len++] = (crc >> 8) & 0xFF; // CRC high byte
// 5. Set frame status
frame->crc = crc;
frame->frame_len = frame_len;
frame->valid = true;
frame->has_signature = false;
return true;
}
4.4 Core Function 2: MAVLink2.0 Frame Unpacking (mavlink_core.c)
The unpacking function automatically completes frame header verification, payload extraction, CRC verification, and message ID parsing. Input the original UART received data, output the parsed structured data, and automatically filter invalid frames:
c
运行
/**
* @brief Parse MAVLink2.0 frame (unpack from original UART data)
* @param frame_buf UART receive buffer
* @param buf_len Received data length
* @param result Output: parsed frame structure
* @return Success: parsed frame length | Failure: 0
*/
uint32_t mavlink_parse_v2_frame(const uint8_t *frame_buf, uint32_t buf_len, mavlink_frame_t *result) {
// Parameter verification
if (frame_buf == NULL || result == NULL || buf_len < 12) return 0; // Minimum frame length 12 bytes (10-byte header + 2-byte CRC)
if (frame_buf[0] != MAVLINK_STX_MAVLINK2) return 0; // Not a MAVLink2.0 frame
uint8_t payload_len = frame_buf[1];
if (payload_len > MAVLINK_MAX_PAYLOAD_LEN) return 0; // Invalid payload length
// Calculate total frame length (with/without signature)
uint32_t frame_total_len = 10 + payload_len + 2;
bool has_signature = (frame_buf[2] & 0x01) != 0;
if (has_signature) frame_total_len += 13; // Append 13 bytes if with signature
if (buf_len < frame_total_len) return 0; // Incomplete data, wait for subsequent reception
// 1. Parse frame header
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. Extract payload
memcpy(result->payload, &frame_buf[10], payload_len);
// 3. Extract CRC and verify (little-endian)
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); // 9-byte frame header for CRC calculation
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 verification failed, return invalid
}
// 4. Extract signature (optional)
if (has_signature) {
memcpy(result->signature, &frame_buf[crc_offset + 2], 13);
result->has_signature = true;
} else {
result->has_signature = false;
}
// 5. Set parsing result
memcpy(result->frame_buf, frame_buf, frame_total_len);
result->frame_len = frame_total_len;
result->valid = true;
return frame_total_len; // Return valid frame length for UART buffer offset
}
4.5 Business Implementation: Heartbeat + Distance Sensor Messages (mavlink_messages.c)
Encapsulate 2 of the most commonly used MAVLink messages, which can be called directly to generate and send, including the complete process of payload assembly, little-endian writing, and UART transmission, and can be directly reused:
c
运行
#include "mavlink_core.h"
#include <string.h>
// Distance sensor payload (39 bytes, MAVLink2.0 standard format)
uint8_t sensor_payload[39] = {0};
// Global sequence number definition
uint8_t mavlink_seq = 0;
// Little-endian writing utility functions (MAVLink protocol enforces little-endian)
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 Set distance sensor payload (in millimeters)
* @param pd Current measured distance (mm)
* @return Success 0
*/
int PAYLOAD_set(uint16_t pd) {
memset(sensor_payload, 0, sizeof(sensor_payload));
// Fill fields according to MAVLink2.0 DISTANCE_SENSOR standard
write_uint32_le(sensor_payload, 123456, 0); // 0-3: System boot time
write_uint16_le(sensor_payload, 0, 4); // 4-5: Minimum distance 0mm
write_uint16_le(sensor_payload, 20000, 6); // 6-7: Maximum distance 20000mm
write_uint16_le(sensor_payload, pd, 8); // 8-9: Current distance (core parameter)
sensor_payload[10] = 1; // 10: Sensor type (laser)
sensor_payload[11] = 0; // 11: Sensor ID
sensor_payload[12] = 0; // 12: Mounting orientation (forward)
sensor_payload[38] = 90; // 38: Signal quality 90%
return 0;
}
/**
* @brief Generate and send distance sensor message
* @param disp Measured distance (mm)
* @return Success 0 / Failure -1
*/
int Distmavlink(uint32_t disp) {
mavlink_frame_t distance_frame;
PAYLOAD_set(disp);
// Pack MAVLink2.0 frame
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); // UART transmission
return 0;
}
return -1;
}
/**
* @brief Generate and send heartbeat packet (for keep-alive, recommended 1Hz transmission)
* @return Success 0 / Failure -1
*/
int heartbeatmavlink(void) {
// Heartbeat packet payload (9 bytes, MAVLink2.0 standard)
uint8_t heartbeat_payload[9] = {
0x02, // Type: Quadrotor UAV
0x00, // Flight controller type: Generic
0x00, // Base mode
0x00,0x00,0x00,0x00, // Custom mode
0x01 // System status: Standby
};
mavlink_frame_t heartbeat_frame;
// Pack and send
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 Global Header File (mavlink.h)
Unified declaration of debug switch and UART functions, only need to modify the UART transmission function during transplantation:
c
运行
#ifndef __MAVLINK_H
#define __MAVLINK_H
#include "stm32f10x.h" // Replace with your STM32 header file
#include <stdbool.h>
// Debug switch: Enable to print frame information (need to implement printf redirection)
#ifndef DEBUG_MAVLINK
#define DEBUG_MAVLINK 0 // 0=Disable, 1=Enable
#endif
// Function declarations
extern int heartbeatmavlink(void) ; // Send heartbeat packet
extern int Distmavlink(uint32_t disp) ;// Send distance sensor message
extern int PAYLOAD_set(uint16_t pd); // Set distance payload
extern void USART1_send(uint8_t *buf, uint16_t len); // UART transmission function
#endif
V. Minimal 4-Step Transplantation for STM32F103
Step 1: Copy Files
Add the above 4 files (mavlink_core.h/.c, mavlink_messages.c, mavlink.h) to your STM32 project. Add directly to the project in Keil/MDK, and add to the src/include directory in CubeIDE.
Step 2: Modify Header File Dependencies
Replace #include "stm32f10x.h" in mavlink.h with your project header file (e.g., main.h generated by CubeMX).
Step 3: Implement UART Transmission Function
Implement USART1_send (or USART2/3) in your UART driver file, example as follows (STM32F103 standard library):
c
运行
// UART1 transmission function (blocking, suitable for small frame data)
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); // Wait for transmit buffer empty
USART_SendData(USART1, buf[i]); // Transmit 1 byte
}
while(USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET); // Wait for transmission complete
}
Note: If using HAL library, replace with HAL_UART_Transmit(&huart1, buf, len, 100);.
Step 4: Implement printf Redirection (Optional, for Debug)
If DEBUG_MAVLINK is enabled, redirect printf to UART, STM32F103 standard library example:
c
运行
#include <stdio.h>
// Redirect fputc to UART1
int fputc(int ch, FILE *f) {
while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
USART_SendData(USART1, (uint8_t)ch);
return ch;
}
Check Use MicroLIB in Keil (Magic Wand → Target → MicroLIB).
VI. Usage Example (Called in Main Function)
Call in the main loop, the heartbeat packet is recommended to be sent at 1Hz (keep-alive), and the distance sensor message is sent according to the actual sampling frequency (e.g., 10Hz):
c
运行
#include "mavlink.h"
#include "delay.h" // Your delay function
int main(void) {
// System initialization: clock, UART, timer, etc.
SystemInit();
USART1_Config(115200); // Initialize UART1, baud rate 115200
delay_init();
while(1) {
heartbeatmavlink(); // Send heartbeat packet (1Hz)
Distmavlink(500); // Send distance message, distance 500mm (replace with actual measured value)
delay_ms(100); // Send distance message at 10Hz, heartbeat packet can be done with a separate timer
}
}
VII. Test Verification
7.1 Hardware Connection
- PA9 (TX) of STM32F103C8T6 → RX of USB-to-TTL;
- TX of USB-to-TTL → PA10 (RX of STM32F103C8T6, used for unpacking);
- Connect USB-to-TTL to computer and open QGroundControl (QGC).
7.2 Ground Station Verification
- Open QGC, enter Settings→Communications, add a serial port connection (baud rate consistent with STM32, e.g., 115200);
- After successful connection, QGC will display Device Online (heartbeat packet takes effect);
- Enter Analyze→MAVLink Console, you can see the distance value of the
DISTANCE_SENSORmessage is 500mm, consistent with the program setting; - Capture UART data, you can see the complete MAVLink2.0 frame (start byte 0xFD) with passed CRC verification.
7.3 Unpacking Usage Example
If you need to parse the MAVLink2.0 frame sent by the ground station, call the unpacking function in the UART receive interrupt:
c
运行
// UART receive buffer (circular buffer is better)
uint8_t uart1_buf[280];
uint16_t uart1_len = 0;
// UART1 receive interrupt service function
void USART1_IRQHandler(void) {
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET){
uart1_buf[uart1_len++] = USART_ReceiveData(USART1);
// Try to unpack (attempt parsing every time 1 byte is received)
mavlink_frame_t recv_frame;
uint32_t frame_len = mavlink_parse_v2_frame(uart1_buf, uart1_len, &recv_frame);
if(frame_len > 0){ // Parsing successful
if(recv_frame.header.msgid[0] == MAVLINK_MSG_ID_HEARTBEAT){
// Process ground station heartbeat packet
}
// Buffer offset, prepare to receive next frame
uart1_len -= frame_len;
memmove(uart1_buf, &uart1_buf[frame_len], uart1_len);
}
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
}
VIII. Notes
- Baud Rate Consistency: The baud rate of STM32 must be strictly consistent with that of the ground station/peripheral (115200 recommended), otherwise frame parsing will fail;
- Little-Endian Enforcement: MAVLink2.0 protocol enforces little-endian for all multi-byte data, must use the
write_uint32_le/write_uint16_leutility functions in this article; - Sequence Number Cycle: The sequence number cycles automatically from 0 to 255 without manual processing to ensure frame order;
- CRC_EXTRA: The
CRC_EXTRAof each message must be consistent with the official one, otherwise the ground station will consider the frame invalid; - Frame Length Limit: MAVLink2.0 has a maximum frame length of 280 bytes and a maximum payload of 255 bytes, which have been limited in this article;
- UART Buffer: A circular buffer is recommended for unpacking to avoid data loss, a normal buffer is used in the example for simplification.
IX. Extended Development
- Add New Messages: Refer to the
DISTANCE_SENSORmessage, add the message ID inmavlink_core.h, add the corresponding CRC_EXTRA inmavlink_get_crc_extra, and encapsulate the payload assembly function; - Enable Signature: Set
incompat_flagsto 0x01, append 13-byte signature during packing, the unpacking function already supports signature parsing; - Non-Blocking UART: Change UART transmission/reception to DMA + circular buffer for high-frequency frame data transmission/reception;
- Multi-Device Communication: Modify
sysidandcompidto realize multi-device networking (e.g., UAV=1, remote controller=2).
X. Summary
The MAVLink2.0 packing/unpacking code implemented in this article is optimized for STM32F103, with no redundancy, easy transplantation, and tested usability. The core functions can be called with one line of code after encapsulation without in-depth understanding of MAVLink2.0 protocol details. The entire code retains the core verification and compatibility of the protocol, and removes useless functions at the same time, occupying minimal resources. It is suitable for the resource-constrained STM32F103 platform and can be directly used in MAVLink communication scenarios such as UAVs, robots, and industrial measurement and control.
Code Acquisition: You can directly copy the code in this article, or follow the author's CSDN to obtain the complete project file (STM32F103C8T6 standard library version, which can be compiled and run directly).
更多推荐


所有评论(0)