
简介ESP32 GPS高度计/可变仪完整开源工程面向航模、飞行器DIY与嵌入式开发者。项目融合IMU加速度计、气压计和GPS数据通过卡尔曼滤波器实现零延迟响应支持500Hz IMU、50Hz气压、10Hz GPS高速记录并具备LCD显示、航路点规划、轨迹记录、蓝牙传输和WiFi配置下载等功能。压缩包共409个文件约3.14MB以C/C源文件为主195个.h头文件、71个.c、32个.cpp及44个.ino Arduino脚本另有少量PNG/JPG图片、Markdown文档和工程配置涵盖ESP-IDF与Arduino两套代码结构附SPIFFS文件系统实现目录划分清晰。已有493人下载学习适合想深入理解多传感器融合、卡尔曼滤波及ESP32外设应用的读者便于直接编译验证、二次开发或移植。1. 项目概述与功能定位1.1 为什么做这个多功能GPS高度计先说这个项目的定位。市面上能买到的手持GPS接收机比如佳明、高明那些户外导航仪功能确实齐全但价格动辄一两千起步而且固件闭源、没法按自己的需求定制。比如我就想要一个能同时显示气压高度和GPS高度的设备用来在登山或者飞行运动中对比两种高度数据普通手持机很难满足这种细颗粒度的需求。所以我自己用ESP32搭了一套多功能GPS高度计把IMU惯性测量单元、气压计、GPS模块全部整合到一个板子上配一块LCD屏幕实时显示数据。整机下来物料成本不到一百块钱但功能上覆盖了航路点导航、航线规划、飞行轨迹记录、蓝牙串口透传、WiFi配置和轨迹文件下载日常户外运动或者作为学习嵌入式、学习传感器融合的练手项目都非常合适。从技术栈看这个项目牵扯到的知识点相当密集ESP32的双核FreeRTOS任务调度、I2C/SPI/UART三种总线协议、MPU6050六轴数据的姿态解算、BMP280气压计的高度换算、GPS的NMEA协议解析、LCD屏幕的驱动与UI布局、BLE蓝牙透传以及WiFi热点配网和Web Server文件下载。可以说做完这一个项目嵌入式开发里常见的外设基本都过了一遍。1.2 核心功能拆解和适用场景我在设计这个项目时把功能拆成了六大块传感器数据采集、实时显示、导航与规划、轨迹记录、蓝牙通信、WiFi配置管理。下面这个表可以清楚地看到每一块功能的实现方式和适用场景功能模块核心硬件/技术典型使用场景姿态与运动感知MPU6050 互补滤波判断设备倾斜角度、辅助导航方位判断气压高度测量BMP280 国际高度公式登山爬升率监测、飞行器高度参考卫星定位NEO-6M/NEO-M8N GPS NMEA解析户外定位、速度显示、轨迹记录数据可视化1.8寸/2.4寸SPI接口LCD实时显示经纬度、高度、速度、航向航路点导航大圆航线距离/方位角计算前往预设点、返航引导轨迹记录与导出SPIFFS文件存储 WiFi下载飞行轨迹复盘、户外行走记录无线调试蓝牙BLE SPP透传手机上实时查看传感器数据这个项目的适用人群很广。如果你是嵌入式初学者可以从中学到多传感器读取、显示屏驱动、数据融合这些基本功如果你是户外爱好者或者飞手可以直接拿来当自用装备如果你做ROS相关开发ESP32还有micro_ros的支持后续可以把IMU和GPS数据直接发到机器人上做传感器融合。我实际使用中发现这个设备在户外登山时特别好用。LCD上同时显示GPS高度和气压高度——GPS高度在大坡度路段会有几米到十几米的误差而气压高度短期稳定性好、能响应细小的爬升变化两者互补能给你一个更可靠的高程参考。2. 硬件选型与设计思路2.1 主控芯片选型分析主控选的是ESP32准确说是ESP32-WROOM-32模组。为什么不用STM32而选ESP32最关键的一点是ESP32自带WiFi和蓝牙这直接省掉了外挂无线模块的成本和复杂度。你要是在STM32F103上实现同样的功能至少得再加一个ESP8266或HC-05蓝牙模块板子上的走线和电源设计都复杂很多。另外一个很现实的原因是开发效率。ESP32有Arduino核心支持也有ESP-IDF官方框架不管你是用PlatformIO还是直接用Arduino IDE都能很快把外设驱动跑通。我自己习惯用PlatformIO因为它的依赖管理做得很好换一块板子不用改代码结构。从性能角度看ESP32是双核240MHz的Xtensa处理器带520KB SRAM跑姿态解算GPS解析屏幕刷新完全没压力甚至还有余量做日志记录。它的ADC虽然精度一般但这个项目里不涉及模拟采样所以没什么影响。2.2 传感器组合方案传感器的选型是我比较满意的部分。整个项目用了三个传感器各管一块首先是IMU我选了MPU6050六轴MEMS传感器集成了三轴陀螺仪和三轴加速度计。它通过I2C接口与ESP32通信地址是0x68。我就是在这个芯片上完成了基础的姿态解算后面还会细讲。然后是气压计BMP280同样走I2C总线测量精度可以做到±1 hPa。有了气压值通过国际气压高度公式就能换算成海拔高度分辨率能到0.1米级别这比GPS高度精确太多了。最后是GPS模块我用的是NEO-6M这个模块便宜、资料多UART TTL电平直接和ESP32的串口2相连。NEO-6M的定位精度在2.5米左右冷启动时间大概30秒左右。如果预算够可以升级到NEO-M8N支持GPS北斗双模室内外切换时的搜星速度会快不少。这里有个特别需要注意的坑就是I2C总线上挂了两个传感器MPU6050和BMP280必须给它们分配不同的地址而且上拉电阻的阻值要处理好。ESP32内部虽然带弱上拉但线长的情况下还是建议外接4.7K的上拉电阻到3.3V不然I2C时序容易出错。2.3 LCD屏幕驱动的选择LCD屏幕我试过两种方案一种是1.8寸的ST7735一种是2.4寸的ILI9341。从显示效果和代码复杂度综合看我更推荐2.4寸的ILI9341分辨率320x240色彩还原好字大了看得清在户外阳光下加个偏光镜片就能看清。驱动方式上用SPI接口四线制CS、DC、RES、MOSI、SCLK再加背光控制脚。ESP32的SPI接口速度快推ILI9341的刷新率能跑到40MHz以上画图形界面完全够流畅。有个细节很多人不注意LCD的背光引脚千万别直接接3.3V要通过一个MOS管或者三极管来控制。这个引脚一般在2.0V到3.0V之间就能完全点亮直接接3.3V虽然也能亮但长期工作会加速LED老化。我后来加了一个PWM控制还可以通过代码调节屏幕亮度来省电。2.4 整体连接图与引脚分配下面是我的引脚分配表各位拿去用的时候可以根据自己手里的开发板具体修改外设引脚说明MPU6050 SDAGPIO 21I2C数据线MPU6050 SCLGPIO 22I2C时钟线BMP280 SDAGPIO 21与IMU共用I2C总线BMP280 SCLGPIO 22与IMU共用I2C总线GPS TXGPIO 16接ESP32的UART2 RXGPS RXGPIO 17接ESP32的UART2 TXLCD CSGPIO 5SPI片选LCD DCGPIO 4数据/命令选择LCD RESGPIO 2复位LCD MOSIGPIO 23SPI主出从入LCD SCLKGPIO 18SPI时钟LCD BLGPIO 15背光PWM控制按键1GPIO 34菜单切换按键2GPIO 35确认/模式切换按键3GPIO 32返回你可能会问为什么GPS接到UART2而不是UART0这是因为UART0在ESP32上默认用作日志输出口如果接GPS设备调试信息就会和GPS数据混在一起很难排查问题。UART2是专门腾出来的我在代码里用Serial2.begin(9600, SERIAL_8N1, 16, 17)初始化其中9600是NEO-6M默认的波特率。3. 软件架构与任务划分3.1 FreeRTOS任务设计ESP32跑的是FreeRTOS这个项目的软件架构核心就是任务的划分和优先级设定。我总共创建了六个任务优先级从高到低排列如下任务名优先级功能周期/触发GPS数据解析5读取串口NMEA数据提取经纬度、速度、时间每100msIMU数据处理4读取MPU6050原始数据执行姿态解算每10ms气压计读取3读取BMP280温度和气压计算海拔每200ms界面刷新2更新LCD显示绘制航路点和轨迹每500ms轨迹记录1定时将位置数据写入文件系统每1s蓝牙发送1将传感器数据通过BLE发送到手机每500ms任务之间的通信用FreeRTOS队列避免全局变量被多任务乱改。比如GPS解析任务把解析好的位置数据封装成结构体通过xQueueSend发送到显示队列显示任务再通过xQueueReceive取出来用。这样做的好处是数据流清晰而且不用担心任务之间互相干扰。3.2 数据流与消息队列整个系统的数据流是这样的传感器采集任务把原始数据变成有意义的数据结构然后发给协议处理任务协议处理任务把所有数据打包成统一的帧格式再分别发给显示任务、存储任务和通信任务。在设计数据结构时我定义了一个sensor_data_t的结构体typedef struct { float latitude; float longitude; float altitude_gps; float altitude_baro; float speed_kmh; float heading; float climb_rate; uint8_t sat_num; uint32_t timestamp; } sensor_data_t;这个结构体在系统里所有任务间流转简单直接。GPS数据用到lat/lon/altitude_gps/speed/heading/sat_num气压计给altitude_baroIMU姿态解算给climb_rate提供辅助因为气压计计算爬升率时噪声大我用IMU的垂直加速度做了滤波融合。3.3 文件系统与数据存储轨迹记录功能依赖ESP32的SPIFFS文件系统。在platformio.ini里配置board_build.filesystem spiffsSPIFFS虽然简单但有个注意点它的擦写寿命有限我用的模组大概10万次擦写。所以在写轨迹记录代码时不能每秒钟都调用fopen/fwrite正确做法是先在内存中累积一个缓冲区攒够一定大小再一次性写入。我的实现方案是每秒收集一组GPS数据格式化成CSV行写入一个4KB的缓冲区当缓冲区满时一次性把对应内容追加到/spiffs/track_YYYYMMDD.csv文件中。这样既保证了轨迹记录不丢数据又降低了Flash磨损。轨迹文件格式timestamp,lat,lon,alt_gps,alt_baro,speed,heading 20250118102300,31.2304,121.4737,12.5,14.2,3.2,128.5 20250118102301,31.2305,121.4738,12.6,14.3,3.4,129.0这个CSV文件既能直接用Excel打开也能转换成KML格式导入Google Earth看3D轨迹非常方便。4. 关键模块实现细节4.1 姿态解算的核心原理与代码IMU这一块MPU6050能输出三轴加速度和三轴角速度但加速度计噪声大、短期漂移小陀螺仪短期精度高但长时间会漂移。单独用任何一个都不行必须把两者的数据融合起来。我用的是Madgwick互补滤波算法因为它计算量小、在ESP32上跑起来毫无压力而且对于航向角这样不需要高精度姿态的应用完全够用。相比之下卡尔曼滤波精度更高但状态矩阵运算复杂资源占用大没必要在这里铺开。核心解算代码主要分两步先从MPU6050读取原始数据然后调用Madgwick算法更新四元数。void readMPU6050() { Wire.beginTransmission(MPU_ADDR); Wire.write(0x3B); Wire.endTransmission(false); Wire.requestFrom(MPU_ADDR, 14, true); // 读取加速度计和陀螺仪原始数据 accel_x Wire.read()8 | Wire.read(); accel_y Wire.read()8 | Wire.read(); accel_z Wire.read()8 | Wire.read(); temp Wire.read()8 | Wire.read(); gyro_x Wire.read()8 | Wire.read(); gyro_y Wire.read()8 | Wire.read(); gyro_z Wire.read()8 | Wire.read(); // 转换为实际物理量 ax accel_x / 16384.0; // ±2g量程 ay accel_y / 16384.0; az accel_z / 16384.0; gx gyro_x / 131.0; // ±250°/s量程 gy gyro_y / 131.0; gz gyro_z / 131.0; madgwick_update(ax, ay, az, gx, gy, gz, dt); roll atan2(2*(q0*q1q2*q3), 1-2*(q1*q1q2*q2)) * RAD_TO_DEG; pitch asin(2*(q0*q2-q3*q1)) * RAD_TO_DEG; }在户外使用的时候MPU6050有个非常影响体验的问题是零漂。刚上电时陀螺仪的输出不是完美的0而是有一个小的偏移量。我的做法是在启动时采集50组数据取平均把这个平均值作为零点偏移存下来后续读数都减去这个偏移。4.2 气压高度换算与爬升率计算BMP280读取温度和气压比较简单关键在高度换算公式。大气压力的垂直分布可以用国际气压测高公式h (pow(1013.25 / P, 1/5.257) - 1) * (T 273.15) / 0.0065这个公式的原理就是大气静力学方程加上理想气体状态方程假设温度随高度线性递减。BMP280手册上还有一个更精确的补偿公式但日常使用用这个简化版就够了误差在几米以内。代码实现float pressureToAltitude(float pressure, float temperature) { float alt (pow(1013.25 / pressure, 0.190284) - 1) * (temperature 273.15) / 0.0065; return alt; }这里有个非常容易踩坑的地方如果不加温度修正气压高度在温差大的时候误差会很大。比如同样是1013hPa夏天35度的实际海拔和冬天0度的实际海拔能差十几米。所以我每次读取BMP280都是一个函数同时拿到温度和气压然后直接代入公式算高度。爬升率是户外设备另一个非常关键的数据。我在代码里维护一个滑动窗口保存最近5秒的气压高度采样线性回归算出斜率就是当前的垂直速度。这个计算在气压高度噪声较大时比直接相邻两秒差分要平滑得多。4.3 GPS数据解析与航路点导航算法GPS模块输出的NMEA-0183协议数据核心是$GPRMC和$GPGGA这两条语句。因为ESP32串口接收是流式的所以必须自己写状态机来提取每一行完整的语句。解析函数我封装成这样void parseNMEA(char *line) { if (strncmp(line, $GPRMC, 6) 0) { sscanf(line, $GPRMC,%f,%c,%f,%c,%f,%c,%f,%f,%d, time_val, status, lat, ns, lon, ew, speed_knots, course, date_val); if (status A) { // 数据有效转换为十进制度 latitude convertNMEAToDecimal(lat, ns); longitude convertNMEAToDecimal(lon, ew); speed_kmh speed_knots * 1.852; heading course; gps_fix true; } } }GPS输出的是度分格式比如3138.1234意思是31度38.1234分要先转成十进制度才能计算距离和方向。航路点导航用到了两个核心公式大圆距离和初始方位角。我在地球上两点间最短路径不是直线而是大圆弧线。代码实现如下float calcBearing(float lat1, float lon1, float lat2, float lon2) { float dLon (lon2 - lon1) * DEG_TO_RAD; lat1 * DEG_TO_RAD; lat2 * DEG_TO_RAD; float y sin(dLon) * cos(lat2); float x cos(lat1)*sin(lat2) - sin(lat1)*cos(lat2)*cos(dLon); float brg atan2(y, x) * RAD_TO_DEG; if (brg 0) brg 360; return brg; // 单位为度 } float calcDistance(double lat1, double lon1, double lat2, double lon2) { double R 6371000.0; // 地球半径单位米 double dLat (lat2 - lat1) * DEG_TO_RAD; double dLon (lon2 - lon1) * DEG_TO_RAD; double a sin(dLat/2)*sin(dLat/2) cos(lat1*DEG_TO_RAD)*cos(lat2*DEG_TO_RAD)* sin(dLon/2)*sin(dLon/2); double c 2 * atan2(sqrt(a), sqrt(1-a)); return R * c; }经纬度转直角坐标时如果距离只有几公里可以用等距离投影简化计算但在航路点导航这种跨几十公里的场景下直接用Haversine公式最稳妥。4.4 LCD界面布局与显示策略LCD显示这块我的布局分了三个页面。第一页是主数据页显示经纬度、GPS高度、气压高度、速度、航向、卫星数第二页是导航页显示距离目标航路点的距离、方位角、偏航角第三页是轨迹页画出已经走过的路线。字体方面ILI9341裸驱动自带的是ASCII字库用Adafruit GFX库的setTextSize放大显示。如果你需要显示中文航路点名称就得准备外部字库比如用LVGL的字体工具把字库转为C数组嵌入Flash。我后面试了下ESP32上跑LVGL效果确实不错UI能做得很漂亮但代价是内存开销大代码也复杂这个项目里我还是用的传统GFX裸写。有个非常影响可读性的细节是LCD的刷新策略。如果每500ms全屏刷新画面会闪烁明显。我的做法是分区域刷新固定显示的背景只在初始化时绘制一次动态数据用透明背景刷新这样既减少了SPI通信量也消除了闪烁感。另外户外阳光直射下LCD容易看不清可以加一个偏光片这个真的有效。我自己加的是0.15mm厚的偏光膜直接贴在LCD表面大概能把对比度提升一倍以上。4.5 蓝牙透传与WiFi配置管理蓝牙这块用的是ESP32自带的BLE没有使用经典蓝牙SPP。BLE在手机上连起来更方便Android上用nRF Connect或自定义的小程序就能收数据。我封装了BLEServer定义了一个Notify的Characteristic每500ms把sensor_data_t结构体序列化后发出去。WiFi配置功能是我认为最实用的功能之一。设备上电时会先检测是否保存过WiFi凭证。如果没保存就自动进入SoftAP配网模式创建一个名字为ESP32_VARIO_XXXX的热点。手机连上这个热点后浏览器访问192.168.4.1会打开一个配置页面填写家里/户外WiFi的SSID和密码提交后ESP32保存配置并重启然后自动连接这个WiFi。配网状态机的逻辑大概这样void checkWiFiConfig() { if (SPIFFS.exists(/wifi_config.txt)) { // 读取配置文件尝试连接 connectToWiFi(ssid, password); } else { // 启动SoftAP配置模式 startConfigServer(); } }WiFi连接成功后我还启动了一个HTTP Server提供两个功能一个是/download下载当前的轨迹记录文件另一个是/list列出所有历史轨迹文件。这样不用拔SD卡直接在手机上就能拿到轨迹数据方便顺手。5. 常见问题与排查技巧5.1 GPS收不到星怎么办这个是我在实际使用中遇到最多的一个问题。GPS模块放在室内基本收不到信号这很正常。但在室外空旷地带搜星慢或者定位失败就要从下面几个方向排查了首先检查GPS模块的天线。NEO-6M自带的有源陶瓷天线和外部有源天线不一样有源天线需要模块上的VCC供电NEO-6M是所有版本都带天线的但如果你用的是其他模块要注意排针定义。其次检查波特率我遇到过模块默认波特率不是9600的情况这就需要先用串口助手扫描一遍波特率把模块设置成9600并保存。还有一个非常关键但容易被忽略的点GPS模块的PPS引脚是脉冲输出不是必须接的。但如果你的代码里用了PPS中断来同步时间戳那么PPS一定要接对引脚。否则GPS虽然能定位但系统时间会一直不准。我这里提供一个排查流程串口直接连GPS模块的TX和RX用电脑串口工具看有没有NMEA语句输出。如果没有检查模块供电是否正常——GPS模块启动瞬间电流可能到100mA以上如果你用的开发板电源能力不够模块会反复重启表现就是搜不到星。5.2 IMU数据跳动和零漂问题MPU6050的数据跳动80%的原因是供电纹波或者I2C信号干扰。我这里两个传感器共用I2C信号线长了容易互相串扰。解决方法是加4.7K上拉电阻并且在MPU6050的VDD脚加一个100nF的去耦电容实测效果好很多。零漂问题前面提过做启动时校准。另外还有一个实用技巧定期做六面校准。用手机水平尺把设备分别放置在六个面朝上的状态采集加速度计的数据生成偏移量和缩放因子。我封装了calibrateIMU()函数让按键组合触发校准模式校准数据写入NVS掉电保存。这里有一个我在飞行测试中发现的坑如果你把设备安装在动力飞行器上螺旋桨的振动会让IMU数据高频抖动非常严重。这时候硬件上必须加减震海绵垫软件上在陀螺仪数据后面加一个低通滤波器比如gyro 0.9*gyro_prev 0.1*gyro_raw否则姿态角会完全失真。5.3 LCD花屏和乱码的解决LCD花屏最常见的原因是电源问题。ESP32在WiFi启动的瞬间电流最大能到240mA如果电源纹波大LCD的驱动芯片就会出错。我遇到过的情况是ESP32一连接WiFi屏幕就闪出各种彩色线条完全没法看刚开始还以为是代码问题后来用示波器一看3.3V那边纹波有将近200mV远超LCD芯片容忍的范围。解决方案是在电源输入端加大容量的钽电容比如470uF然后在LCD的VCC引脚上加一个10uF的去耦电容这个问题就彻底消失了。代码层面LCD初始化时一定要按芯片手册的时序操作先复位再初始化不能跳过延时。ILI9341初始化序列里的延时参数如果过短会导致部分功能不稳定比如显示颜色错乱或者无法写入显存。5.4 轨迹记录文件写入失败SPIFFS写入失败的原因有两个文件系统空间已满或者擦写次数过多导致坏块。我在代码里会定期检查剩余空间如果小于100KB就自动删除最早的轨迹文件。另外SPIFFS有一个特性写文件之后要主动fclose()才能把数据真正持久化。如果你写完数据不关文件日志在掉电后大概率丢失。所以我的轨迹记录任务里每次写完文件马上fflush并fclose下次写再fopen追加。虽然频繁开关文件效率低一点但可靠性优先。6. 扩展方向与个人体会6.1 向ROS 2生态靠拢如果你像我一样玩机器人肯定想把这个GPS高度计接入到ROS 2系统里。ESP32现在有官方的micro_ros_espidf_component支持可以直接运行Micro-ROS agent把IMU数据、气压高度、GPS位置打包成 ROS 2 topic 发布出来。我实际测试过在Humble版本下ESP32作为Micro-ROS节点节点接入专门的agent然后通过WiFi或者串口连接到上位机能稳定跑起来。这样一搞这个设备就从一个手持显示器变成了一个机器人传感器组。IMU数据给机器人的EKF里程计做融合气压高度辅助机器人在地图里的Z轴定位GPS则负责大范围的全局定位。6.2 更多优化空间目前功耗这块还有优化空间我大概测过整机平均电流在150mA左右用18650电池供电能撑6到7小时。如果再想省电可以睡眠模式下定时唤醒GPS模块测一次位置其他时间主控睡死这样续航能拉到一天以上但代价是显示界面的实时性会打折扣。界面方面如果觉得裸写LCD太原始可以尝试LVGL。ESP32跑LVGL性能是够的UI做出来会漂亮很多而且LVGL对触摸的支持很好可以加上电阻触摸屏做成一个完整的交互界面。6.3 我在实际使用中的几点体会做完这个项目我的最大体会是传感器融合没有想象中那么复杂但也绝对不简单。单独看每个传感器的数据都挺好但放在一起就各种问题——GPS在桥下会跳几十米气压计在大风天气里会抖得厉害IMU温度变化会导致零漂增大。真正让一个设备变得可用靠的不是某一个算法的精巧而是对每个传感器的特性有深入理解然后用工程手段把它们的优势组合起来。第二个体会是调试工具非常重要。我后面给项目加了一个基于Web的在线调试页面直接打开浏览器就能看到所有传感器的实时数据比反复刷串口日志高效太多。这个经验也推荐给大家。如果你准备动手复刻这个项目我的建议是先不要一次做齐全。先让LCD显示出来GPS数据再加气压计再加IMU最后接蓝牙和WiFi。每一步都验证通透了再进入下一步否则一出问题满屏的数据都不知道该信谁。本文还有配套的精品资源点击获取