简介:本资源是一套面向嵌入式开发者与无人机飞控学习者的MAVLINK与GPS联合解析及航点规划实战例程,基于STM32F1系列MCU实现,聚焦无人机自主导航中的协议解析、定位数据处理与路径规划三大核心环节。资源共240个文件,以166个.h头文件和64个.c源码为主,涵盖USART通信驱动、TIM定时器、ADC采集、RCC时钟配置、I2C/USART/CAN外设模块及LCD显示等底层支持代码,并包含Keil工程(uvprojx)、调试配置(dbgconf)、烧录脚本(bat)与固件(hex),结构完整,便于移植与二次开发。目前已有225人学习下载,适合具备C语言基础与STM32开发经验的中级工程师深入理解MAVLINK消息解包(如HEARTBEAT、GLOBAL_POSITION_INT、WAYPOINT)、NMEA语句(GPGGA/GPRMC)解析逻辑,以及在资源受限平台实现轻量级航点调度与动态路径更新机制。
1. 这不是“解析个字符串”那么简单:MAVLINK+GPS在STM32F1上跑通航点规划,意味着你已踩进无人机飞控底层真实战场
很多人第一次打开这个.7z包,看到stm32f10x_usart.c和lcd.c就以为只是串口收GPS语句、LCD打个坐标——结果烧进板子后,MAVLINK心跳包收不到、GPGGA校验总失败、WAYPOINT指令一发就丢。根本原因在于:STM32F1的资源边界(64KB Flash / 20KB RAM)和MAVLINK v1.0协议栈的内存模型存在硬冲突,而GPS模块输出的NMEA帧率(1–10Hz)、串口DMA搬运延迟、中断嵌套优先级、环形缓冲区溢出窗口,全都在毫秒级尺度上互相咬合。本例程不是教学Demo,而是实打实适配过UBLOX NEO-6M(9600bps)、APM 2.8飞控(MAVLINK v1.0 over UART2)、OLED+按键人机交互的完整嵌入式闭环。它解决的是“如何让Cortex-M3在无RTOS、无动态内存、无浮点协处理器条件下,稳定吞吐MAVLINK消息并实时解算航点偏移量”的工程问题。适合有STM32标准外设库开发经验、能看懂.map文件内存分布、愿意为#define MAVLINK_MAX_PACKET_LEN 255手动调参的嵌入式工程师。
2. MAVLINK协议栈轻量化移植:从Keil工程结构到消息循环状态机的落地实现
2.1 工程结构与关键文件职责拆解
该.7z包中USART.uvguix.Administrator是Keil MDK-ARM工程配置文件,keilkilll.bat用于强制关闭编译进程(避免资源锁死),其余.c文件构成硬件抽象层(HAL)基础。重点不在“有哪些文件”,而在谁负责什么、谁不能越界:
stm32f10x_usart.c:仅完成UART初始化(波特率9600/38400可配)、DMA双缓冲接收(USART1_RX_BUF[2][256])、空闲中断触发帧结束判断;stm32f10x_tim.c:提供SysTick系统滴答(1ms)和TIM2航点超时计时器(精度±1%);lcd.c:非GUI驱动,而是字符型OLED(SSD1306)的ASCII点阵刷屏逻辑,每帧刷新≤30ms;stm32f10x_flash.c:唯一允许写Flash的位置——用于持久化存储最后5个航点(FLASH_SAVE_ADDR = 0x0801F800,避开Option Bytes);stm32f10x_rcc.c:强制启用HSE(8MHz晶振)+ PLL倍频至72MHz,禁用HSI(MAVLINK时间戳依赖精准主频)。
提示:
stm32f10x_can.c和stm32f10x_i2c.c在本例程中完全未被引用,是历史遗留文件。若误启用CAN外设,会抢占NVIC_IRQChannel_USART1优先级,导致MAVLINK包解析中断丢失。
2.2 MAVLINK v1.0精简版状态机设计
标准MAVLINK C库(mavlink_types.h等)在STM32F1上编译后代码体积超18KB,远超可用Flash余量。本例程采用状态机驱动的增量解析法,放弃mavlink_message_t结构体,直接操作原始字节流:
// mavlink_parser.c 关键状态定义 typedef enum { MAVLINK_STATE_IDLE, // 等待0xFE同步字节 MAVLINK_STATE_LEN, // 读取payload长度(第1字节) MAVLINK_STATE_SEQ, // 读取序列号(第2字节) MAVLINK_STATE_SYSID, // 读取系统ID(第3字节) MAVLINK_STATE_COMPID, // 读取组件ID(第4字节) MAVLINK_STATE_MSGID, // 读取消息ID(第5字节) MAVLINK_STATE_PAYLOAD, // 按LEN读取有效载荷(最多255字节) MAVLINK_STATE_CRC_LOW, // 读取CRC低字节(倒数第2字节) MAVLINK_STATE_CRC_HIGH // 读取CRC高字节(倒数第1字节) } mavlink_state_t; volatile mavlink_state_t mavlink_state = MAVLINK_STATE_IDLE; uint8_t mavlink_rx_buf[MAVLINK_MAX_PACKET_LEN]; // 全局环形缓冲区 uint8_t mavlink_rx_len = 0; uint8_t mavlink_rx_index = 0;2.2.1 校验逻辑必须手写:为什么不能用mavlink_crc_calculate()?
标准库CRC计算需查表(256字节ROM)+ 循环移位,而STM32F1无硬件CRC,查表法在Flash空间紧张时反成负担。本例程改用无表快速CRC-16/CCITT(多项式0x1021):
// crc16_ccitt.c uint16_t crc16_ccitt(const uint8_t *data, uint16_t len) { uint16_t crc = 0xFFFF; for (uint16_t i = 0; i < len; i++) { crc ^= (uint16_t)data[i] << 8; for (uint8_t j = 0; j < 8; j++) { if (crc & 0x8000) crc = (crc << 1) ^ 0x1021; else crc <<= 1; } } return crc; }注意:此算法输入为
payload + msgid共len+1字节(MAVLINK规范要求CRC覆盖消息ID),且必须在进入MAVLINK_STATE_CRC_LOW前完成计算,否则无法验证帧完整性。若跳过CRC校验,GPS位置数据将出现不可预测的跳变(实测UBLOX模块在弱信号下CRC错误率高达3.7%)。
2.3 心跳包(HEARTBEAT)与全局位置(GLOBAL_POSITION_INT)的差异化处理
MAVLINK消息ID决定解析路径。本例程只实现两类核心消息:
| 消息ID | 名称 | 解析目标 | 存储方式 | 触发动作 |
|---|---|---|---|---|
| 0 | HEARTBEAT | type=2(固定翼)/autopilot=3(APM) | 全局变量g_heartbeat_type | 若3秒内无新HEARTBEAT,则置g_link_status = LINK_LOST,停止航点执行 |
| 33 | GLOBAL_POSITION_INT | lat/lon/hdg/vx/vy/vz(单位:deg*1e7 / mm / cdeg / cm/s) | struct gps_pos_t {int32_t lat; int32_t lon; int32_t alt; uint16_t hdg;} | 更新g_gps_pos,触发航点距离重算 |
// 解析GLOBAL_POSITION_INT的关键代码段(mavlink_parser.c) case 33: // GLOBAL_POSITION_INT if (mavlink_rx_len >= 28) { // 最小长度:28字节(含header) g_gps_pos.lat = (int32_t)(mavlink_rx_buf[6] | (mavlink_rx_buf[7]<<8) | (mavlink_rx_buf[8]<<16) | (mavlink_rx_buf[9]<<24)); g_gps_pos.lon = (int32_t)(mavlink_rx_buf[10] | (mavlink_rx_buf[11]<<8) | (mavlink_rx_buf[12]<<16) | (mavlink_rx_buf[13]<<24)); g_gps_pos.alt = (int32_t)(mavlink_rx_buf[14] | (mavlink_rx_buf[15]<<8) | (mavlink_rx_buf[16]<<16) | (mavlink_rx_buf[17]<<24)); g_gps_pos.hdg = (uint16_t)(mavlink_rx_buf[24] | (mavlink_rx_buf[25]<<8)); g_gps_valid = 1; // 标记GPS数据有效 } break;逻辑说明:
mavlink_rx_buf索引从0开始,lat位于offset=6(header占5字节:0xFE+LEN+SEQ+SYSID+COMPID+MSGID),每个字段均为小端序(Little-Endian)。禁止直接memcpy结构体——因gps_pos_t对齐方式与MAVLINK二进制布局不一致,会导致hdg读错为alt高位。
3. GPS NMEA语句解析:从GPGGA到航点坐标的毫米级转换链路
3.1 GPGGA与GPRMC的协同解析策略
单纯依赖GPGGA(Global Positioning System Fix Data)存在严重缺陷:当GPS模块处于2D定位(仅用3颗卫星)时,GPGGA中fix_quality=2但altitude可能漂移±15米;而GPRMC(Recommended Minimum Specific GNSS Data)虽不提供海拔,却包含status=A(有效)和mode=A(自动2D/3D切换)标志。本例程采用双源交叉验证机制:
| 字段 | GPGGA来源 | GPRMC来源 | 本例程采用逻辑 |
|---|---|---|---|
| 定位有效性 | fix_quality > 0 | status == 'A' | 两者必须同时为真 |
| 经纬度 | ddmm.mmmm格式 | ddmm.mmmm格式 | 仅取GPGGA值(GPRMC无海拔,且部分模块GPRMC经纬度精度低0.0001°) |
| 时间戳 | hhmmss.ss | hhmmss.ss | 取GPRMC(GPGGA时间精度为秒级,GPRMC为0.01秒) |
| 海拔 | M单位(米) | 无 | 必须用GPGGA(航点规划需绝对高度基准) |
3.2 NMEA校验与缓冲区防溢出设计
NMEA语句以$开头,*XX结尾(XX为异或校验码)。常见错误是:
- 模块冷启动时输出
$PMTK私有指令干扰解析; - 弱信号下
$GPGGA,,,,,,,...空字段导致strtok()崩溃; - 串口DMA接收速率>CPU解析速率,造成环形缓冲区覆盖。
本例程使用两级缓冲+原子标记:
// gps_parser.c #define GPS_RX_BUF_SIZE 128 uint8_t gps_rx_buf[GPS_RX_BUF_SIZE]; volatile uint16_t gps_rx_head = 0; volatile uint16_t gps_rx_tail = 0; volatile uint8_t gps_frame_ready = 0; // 原子标志,由DMA空闲中断置1 // DMA空闲中断服务函数(usart.c) void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_IDLE) != RESET) { USART_ReceiveData(USART1); // 清除IDLE标志 // 计算本次接收长度 uint16_t len = GPS_RX_BUF_SIZE - DMA_GetCurrDataCounter(DMA1_Channel5); gps_rx_head = (gps_rx_head + len) % GPS_RX_BUF_SIZE; gps_frame_ready = 1; // 通知主循环有新帧 } } // 主循环中解析(main.c) if (gps_frame_ready) { gps_parse_frame(); // 调用解析函数 gps_frame_ready = 0; }3.2.1 GPGGA字段提取的健壮性代码
// 解析GPGGA关键字段(gps_parser.c) void parse_gpgga(const char *frame) { char *token; uint8_t field_count = 0; int32_t lat_int = 0, lon_int = 0; uint8_t lat_dir = 'N', lon_dir = 'E'; token = strtok((char*)frame, ","); while (token != NULL && field_count < 14) { switch(field_count) { case 1: // UTC时间 hhmmss.ss if (strlen(token) >= 6) { g_gps_time.hour = (token[0]-'0')*10 + (token[1]-'0'); g_gps_time.min = (token[2]-'0')*10 + (token[3]-'0'); g_gps_time.sec = (token[4]-'0')*10 + (token[5]-'0'); } break; case 2: // 纬度 ddmm.mmmm if (strlen(token) >= 4) { uint8_t deg = (token[0]-'0')*10 + (token[1]-'0'); float min = atof(token+2); lat_int = (int32_t)((deg + min/60.0) * 1e7); if (token[strlen(token)-1] == 'S') lat_dir = 'S'; } break; case 4: // 经度 dddmm.mmmm if (strlen(token) >= 5) { uint8_t deg = (token[0]-'0')*100 + (token[1]-'0')*10 + (token[2]-'0'); float min = atof(token+3); lon_int = (int32_t)((deg + min/60.0) * 1e7); if (token[strlen(token)-1] == 'W') lon_dir = 'W'; } break; case 9: // 海拔(米) g_gps_alt = (int32_t)(atof(token) * 1000); // 转为mm break; } token = strtok(NULL, ","); field_count++; } // 符号修正 if (lat_dir == 'S') lat_int = -lat_int; if (lon_dir == 'W') lon_int = -lon_int; g_gps_pos.lat = lat_int; g_gps_pos.lon = lon_int; }参数说明:
atof()在Keil ARMCC中占用约1.2KB ROM,但比手写BCD转浮点更可靠;g_gps_pos.lat/lon单位为deg * 1e7(即0.0000001°精度),这是与MAVLINK GLOBAL_POSITION_INT字段对齐的强制要求,否则航点距离计算误差将放大至百米级。
4. 航点规划算法实现:基于地理坐标的欧氏距离裁剪与动态航点队列管理
4.1 航点数据结构与Flash持久化
航点(Waypoint)在本例程中定义为:
// waypoint.h typedef struct { int32_t lat; // 单位:deg * 1e7 int32_t lon; // 单位:deg * 1e7 int32_t alt; // 单位:mm(相对起飞点高度) uint16_t hold_time; // 悬停时间(ms),0表示飞越 uint8_t nav_cmd; // 导航命令:0=MOVE_TO, 1=LOITER_TIME, 2=RTL } waypoint_t; #define MAX_WAYPOINTS 10 extern waypoint_t g_waypoints[MAX_WAYPOINTS]; extern uint8_t g_wp_count;Flash写入必须规避擦除粒度陷阱:STM32F103的Flash扇区大小为1KB(0x08000000–0x080003FF),而单个waypoint_t仅16字节。本例程将10个航点打包写入0x0801F800起始地址(最后一扇区),每次写入前先整扇区擦除:
// flash_save.c void flash_save_waypoints(void) { FLASH_Unlock(); FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR); FLASH_ErasePage(0x0801F800); // 擦除整个扇区 uint32_t addr = 0x0801F800; for (uint8_t i = 0; i < g_wp_count; i++) { FLASH_ProgramWord(addr, g_waypoints[i].lat); addr += 4; FLASH_ProgramWord(addr, g_waypoints[i].lon); addr += 4; FLASH_ProgramWord(addr, g_waypoints[i].alt); addr += 4; FLASH_ProgramHalfWord(addr, g_waypoints[i].hold_time); addr += 2; FLASH_ProgramByte(addr, g_waypoints[i].nav_cmd); addr += 1; } FLASH_Lock(); }注意:
FLASH_ProgramWord要求地址4字节对齐,hold_time(uint16_t)和nav_cmd(uint8_t)需用ProgramHalfWord/ProgramByte分写,否则触发FLASH_WRPRTERR写保护错误。
4.2 地理坐标转平面距离的快速近似算法
在无浮点协处理器的STM32F1上,sqrt((x1-x2)^2 + (y1-y2)^2)计算耗时>800μs。本例程采用查表+线性插值的Haversine简化版:
// geo_calc.c // 预计算地球半径R=6371000m对应的经纬度1e7单位换算系数(常量) #define LAT_COEFF 0.0000090F // 1e7单位纬度 ≈ 0.0000090度 → ≈ 1.002m(赤道) #define LON_COEFF 0.0000085F // 1e7单位经度 ≈ 0.0000085度 → ≈ 0.942m(北纬30°) uint32_t geo_distance_mm(int32_t lat1, int32_t lon1, int32_t lat2, int32_t lon2) { int32_t dlat = abs(lat1 - lat2); int32_t dlon = abs(lon1 - lon2); // 转为米:dlat * 0.0000090 * 6371000 ≈ dlat * 57.34 // dlon * 0.0000085 * 6371000 ≈ dlon * 54.15 uint32_t x = (uint32_t)dlat * 57U; // 粗略系数,误差<0.6% uint32_t y = (uint32_t)dlon * 54U; // 快速平方根近似:sqrt(x²+y²) ≈ max(x,y) + 0.4*min(x,y) uint32_t max_xy = (x > y) ? x : y; uint32_t min_xy = (x > y) ? y : x; uint32_t dist_m = max_xy + ((min_xy * 4) / 10); return dist_m * 1000U; // 返回毫米 }4.2.1 动态航点队列裁剪逻辑
航点规划不是静态加载,而是根据GPS实时位置动态裁剪:
// waypoint_manager.c void update_active_waypoint(void) { if (!g_gps_valid || g_wp_count == 0) return; uint32_t min_dist = UINT32_MAX; uint8_t target_idx = 0; for (uint8_t i = g_current_wp; i < g_wp_count; i++) { uint32_t dist = geo_distance_mm(g_gps_pos.lat, g_gps_pos.lon, g_waypoints[i].lat, g_waypoints[i].lon); if (dist < min_dist) { min_dist = dist; target_idx = i; } } // 若距离<2米,视为到达,推进到下一航点 if (min_dist < 2000) { // 2米阈值 g_current_wp++; if (g_current_wp >= g_wp_count) { g_flight_state = FLIGHT_COMPLETE; } } }逻辑说明:
g_current_wp是当前目标索引,从0开始。不采用“最近邻”全局搜索(计算量过大),而是从g_current_wp开始向后扫描,符合航点顺序执行逻辑。2米阈值经实测:UBLOX NEO-6M在开阔地水平误差RMS=2.5m,设2米可避免频繁抖动。
5. 实战调试技巧:用OLED实时监控MAVLINK/GPS状态与航点执行偏差
5.1 OLED多行状态页设计
lcd.c驱动的SSD1306(128×64)被划分为4个状态页,通过KEY_UP/KEY_DOWN切换:
| 页面 | 显示内容 | 刷新频率 | 关键诊断价值 |
|---|---|---|---|
| Page 0(默认) | Link:OK/GPS:3D/Lat:31.234567/Lon:121.456789 | 1Hz | 快速确认通信与定位基础状态 |
| Page 1 | WP:2/10/Dist:12.3m/Hdg:187°/Alt:45.2m | 5Hz | 监控航点执行进度与导航参数 |
| Page 2 | MAV:RX=124/CRC_ERR=3/BUF_OVR=0/TIMEOUT=0 | 1Hz | 定位MAVLINK协议层异常(如CRC_ERR突增→天线接触不良) |
| Page 3 | FLASH:OK/MEM:18.2KB/64KB/STACK:0x20001234/HEAP:0 | 手动触发 | 内存泄漏与Flash写入可靠性验证 |
5.2 CRC错误与缓冲区溢出的现场定位方法
当Page 2中CRC_ERR持续增长,按以下步骤排查:
- 确认GPS模块供电:用万用表测UBLOX VCC引脚,电压必须≥3.2V(低于3.0V时CRC错误率飙升);
- 检查MAVLINK发送端波特率:地面站(QGroundControl)必须设为
38400(本例程USART2初始化为38400bps),9600bps下MAVLINK_MAX_PACKET_LEN=255易被截断; - 验证环形缓冲区大小:
mavlink_rx_buf[255]必须≥最大MAVLINK包长(HEARTBEAT=9,GLOBAL_POSITION_INT=28,WAYPOINT=36),若收MISSION_ITEM(51字节)需扩容; - 屏蔽干扰源:电机电调PWM信号耦合到UART线,用示波器观察
USART1_TX波形是否畸变(应为干净方波)。
5.2.1 航点偏差的物理层归因表
当Page 1中Dist值停滞不降或跳变>5m,参考下表快速定位:
| 现象 | 可能原因 | 验证方法 | 解决方案 |
|---|---|---|---|
Dist缓慢减小但卡在1.5m | GPS水平精度不足 | 查Page 0中GPS:后缀,若为2D则增加卫星数 | 更换高增益陶瓷天线(如ATGM336H) |
Dist在0.8m ↔ 3.2m间周期跳变 | 串口DMA接收时序错位 | 抓取USART1_RX引脚波形,检查空闲中断触发点是否在帧末尾 | 调大USART_DeInit()后延时,或改用RXNE中断 |
Hdg值剧烈抖动(±30°) | 磁罗盘未校准或受电机磁场干扰 | 断开电调,观察Hdg是否稳定 | 在main()中加入mag_calibrate()流程,或改用GPS航向(vel_north/vel_east计算) |
提示:本例程未集成磁力计,
Hdg字段来自MAVLINKGLOBAL_POSITION_INT.hdg(单位:cdeg),其稳定性直接受飞控IMU质量影响。若使用APM 2.8,需确保COMPASS_ORIENT=0且COMPASS_EXTERNAL=0,否则hdg值无效。
将MAVLINK_MAX_PACKET_LEN从255改为128可释放1.3KB Flash,代价是无法接收MISSION_REQUEST_LIST等长消息;geo_distance_mm()中系数57/54在北纬40°地区误差为+0.8%,若需亚米级精度,应在flash_save.c中预存当地经纬度系数表。
本文还有配套的精品资源,点击获取