去年冬天,我把车停进一个没有顶灯的机械车库,在里面找车找了二十分钟。当时我就在想,为什么没有一个小东西,能让我掏出手机就知道车在哪儿。后来看了不少成品GPS追踪器,要么要月租,要么平台突然停止运营,要么定位精度差得离谱,干脆决定自己动手搓一个。
这个项目我前后改了三个版本,从最初的Arduino Uno加串口GPS模块,到最后用ESP32做的低功耗多用途追踪器,中间踩了不少坑。做出来的这台设备,既能当行车轨迹记录仪,也能变成家里电瓶车、工具箱的防盗定位器,而且数据完全自己掌控,不依赖任何第三方平台。整机物料成本在六七十元左右,比市面上不少两三百块的成品还靠谱。
这篇内容会从选型讲到硬件连接,再讲到固件、数据可视化和实测功耗,把整个过程中真正有用的细节和踩过的坑全部摊开说。不论你是刚开始接触GPS模块的小白,还是已经玩过一段时间想把自己的追踪器功能做全,都应该能从里面找到点能直接抄的东西。
1. 为什么自己做一台多用途GPS追踪器,而不是直接买成品
1.1 成品追踪器最让人头疼的几件事
先说说成品GPS追踪器的通病。市面上标称"免安装、免布线"的小盒子,价格从六七十到五六百都有,但真正用起来你会发现三个问题。
第一是平台依赖。很多厂家用自己家的私有云,app挂在上面,一旦厂商经营不善或者调整服务,整个设备直接变成砖头。我就见过一个朋友的车辆定位器,用了两年之后平台关停,机器还亮灯,但数据再也没法看了。这种事情发生一次,你就知道数据握在自己手里有多重要。
第二是月租和流量费。部分追踪器依赖内置SIM卡,第一年免费或者低价,第二年续费价格就上来了。对于一台只用来记录骑行轨迹的设备,这笔钱属于纯粹浪费。
第三是扩展能力差。成品追踪器一般只给你一个固定的app界面,不能自定义报警规则,不能把轨迹导出到地图软件做分析,更不可能加一个震动传感器或者按钮。所有功能都被厂商框死了。
倒不是所有成品都不好,只是如果你喜欢折腾,又希望这个设备能跟着你的需求一起长大,自己做的可玩性完全不是一个量级。
1.2 我给自己定的需求清单
动手之前,先把需求列清楚,否则很容易变成"做出来一台能定位但不知道干嘛用的东西"。我当时的需求是这些:
- 能记录GPS坐标和时间,并且把数据存到本地存储卡里,不需要联网也能事后分析。
- 能通过Wi-Fi把位置上报到自己的服务器,实时查看,但要能开关,平时不耗电。
- 支持低功耗待机,放在车上不用频繁充电。
- 有一个按键,可以在不方便掏手机的场合一键记录当前位置。
- 整体成本控制在100元以内,核心部件必须全部独立可替换。
按照这个需求清单,我最终选定的方案是ESP32主控加UART接口的GPS模块,外加一个TF卡模块。整机在3.3V供电下工作,GPS模块负责接收卫星信号,ESP32负责解析NMEA协议和存储数据。没有用任何现成的GPS一体板,因为一体板的体积和开源性都不够理想。
这个项目做出来之后,我先后把它用在电动自行车记录、登山背包轨迹记录,以及汽车停车位置查询这几个场景。使用前提都是自己的东西,或者经过授权许可,建议你同样只在合规合法的前提下使用。
2. 硬件平台和连接细节:主控、GPS模块、电源、天线
2.1 主控选型:ESP32、Arduino Uno还是STM32
很多第一次接触GPS项目的朋友会问,GPS模块不是随便接到单片机串口就能用吗?确实能,但不同的主控直接影响功耗和数据处理能力。
Arduino Uno最容易被搜到,也最容易复现,但它有两个硬伤。第一是工作电压5V,很多GPS模块是3.3V逻辑,直接接会有电平不匹配的问题,需要加电平转换。第二是Uno没有Wi-Fi模块,想实时上传位置还得外接ESP8266,板子体积和功耗都上去了。
STM32的成本不高,功耗控制也很好,但开发环境对新手不够友好,而且生态库相比Arduino少很多,一个TinyGPS++的解析库在STM32上要自己裁剪,没必要把这个简单项目复杂化。
我最后用的是ESP32。理由很简单:
- 自带Wi-Fi和蓝牙,想做实时上报的时候不需要额外挂模块。
- 两个UART口,一个专门接GPS,一个接调试串口,互不干扰。
- 3.3V逻辑,和市面上绝大多数GPS模块直接兼容。
- 支持深度睡眠,待机电流能压到很低。
- Arduino环境下直接写代码,库都是现成的。
ESP32型号上,我建议用ESP32 DevKit_V1这种最普通的开发板就够了。没必要上带屏幕的版本,带屏幕反而增加功耗和体积。如果你计划做电池供电并且空间敏感,可以直接用ESP32-WROOM模组自己画板子,但第一版还是开发板更容易排查问题。
2.2 GPS模块选型:NEO-6M、NEO-8M、ATGM336H怎么选
GPS模块是整个项目的核心。市面上UART输出的GPS模块大概分两类:一类是u-blox的NEO系列,另一类是中科微的ATGM系列。
NEO-6M是最常见的入门模块,价格十几块,很多学习套件里都有。它的问题是只支持GPS单星系统,在城市里高楼遮挡多的时候定位速度比较慢。NEO-8M算是升级版,加入了北斗和GLONASS支持,性能和价格都比较均衡。ATGM336H则是国产低价方案,同样支持多星系统,某些版本还支持低功耗模式,性价比很高。
我实测下来,NEO-6M在空旷环境冷启动大约需要40到50秒,ATGM336H大约20到30秒。在高架桥下或者城市峡谷路段,支持多星系统的模块明显更稳,漂移也更小。所以如果预算允许,直接跳过NEO-6M,选ATGM336H或者NEO-8M。
接线方面,多数GPS模块的串口是3.3V TTL,直接和ESP32的GPIO相连就可以:
- GPS模块VCC接ESP32的3.3V
- GPS模块GND接ESP32的GND
- GPS模块TXD接ESP32的GPIO16(默认RXD)
- GPS模块RXD接ESP32的GPIO17(默认TXD)
注意,千万不能把GPS模块直接接到5V引脚,尤其是那些看起来是专门为Arduino设计的模块,它们的板载稳压芯片虽然能扛5V,但串口逻辑电平仍然可能是3.3V,接3.3V最安全。
2.3 电源、电平和天线布局,三个最容易出问题的地方
硬件连接看起来简单,真正翻车全在细节上。
电源是我踩过最大的坑。第一版我用的是普通USB充电宝供电,移动电源为了省电,当检测到负载电流小于某个阈值时会自动断电。GPS模块加ESP32休眠时电流只有十几毫安,充电宝一检测到电流太小就切断电源,设备直接关机。后来我换成18650锂电池加TP4056充电模块,这个问题才彻底解决。
电平问题藏在那些标着"兼容5V/3.3V"的模块里。有些GPS模块厂家为了兼容老单片机,会在RXD引脚上做5V输入容忍,但TXD输出仍然是3.3V,这没问题。真正会出问题的是你把其他5V单片机的TXD接到ESP32的RXD上,可能直接烧毁引脚,所以如果是接Uno,一定要确定是否有电平转换。
天线布局决定了定位快不快。GPS天线对方向很敏感,陶瓷天线的最佳接收面是正对天空,而且周围不要有大面积金属遮挡。我把第一批样机装在一个全铝壳里,结果开机二十分钟都搜不到星,后来把天线贴着塑料外壳开口处才恢复正常。
从实际使用角度,如果你的追踪器要长时间放在车里,一定要用带IPEX接口的外置有源天线模块,把天线黏在仪表台或者前挡风玻璃下面,信号比内置陶瓷天线好很多。
3. 固件开发:定位解析、数据记录和状态反馈
3.1 读取NMEA数据,不是看着串口输出那么简单
GPS模块上电之后,会以一定频率向外输出NMEA格式的文本数据,最常见的句子是$GPRMC、$GPGGA这些。比如一句典型的$GPRMC长这样:
$GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A看起来晦涩,但核心字段很好认:第二个字段是UTC时间,第六到第七个字段是纬度,第八到第九个字段是经度。不要自己在串口里逐字符拆,直接用一个叫TinyGPS++的老牌解析库,几行代码就能拿到干净的经纬度。
我当时用ESP32写的第一版固件长这样:
#include <TinyGPS++.h> #include <HardwareSerial.h> TinyGPSPlus gps; HardwareSerial gpsSerial(1); void setup() { Serial.begin(115200); gpsSerial.begin(9600, SERIAL_8N1, 16, 17); } void loop() { while (gpsSerial.available() > 0) { char c = gpsSerial.read(); if (gps.encode(c)) { if (gps.location.isValid()) { Serial.print(gps.location.lat(), 6); Serial.print(","); Serial.println(gps.location.lng(), 6); } } } }这里有一个很重要的检查点:GPS模块上电后并不是立即有数据,也不是每一条数据都有效。很多新手看到串口有输出就以为定上位了,实际上NMEA里有一堆无效记录,必须靠gps.location.isValid()来判断当前坐标是否真实可用。这个判断会绕开从冷启动到首次定位之间那段"看得到数据、没有坐标"的尴尬时间。
另外,波特率不要默认。很多NEO-6M模块出厂波特率是9600,ATGM336H有的版本默认115200,如果你在代码里用一个波特率,模块用另一个,串口打印出来全是乱码。排查时先用USB转TTL接模块,在电脑串口助手里确认模块实际输出波特率,再写进固件。
3.2 存储方式:本地SD卡、Flash写入和MQTT上报
做多用途追踪器,存储方案取决于你想怎么用。我在这台设备上同时实现了三种模式,用配置文件切换。
第一种是SD卡记录模式。用SPI接口接一个TF卡模块,GPS解析出来之后,每隔一秒把时间、纬度、经度、速度、卫星数写进一个CSV文件。这种方式好处是全程不依赖网络,记录完了把卡拔出来插电脑,直接拿Python分析轨迹。适合骑行、爬山、行车记录这些场景。
SD卡接线时需要特别留意,TF卡模块的CS引脚可以自定义,但SCK、MOSI、MISO最好用ESP32的默认SPI引脚,不然SPI库参数要改。我第一次自己乱接,卡能初始化但读写偶尔卡死,最后查资料才知道是同一个SPI总线上多个设备争用的问题。
第二种是内部Flash存储。ESP32内置Flash有4MB,刨去固件空间还能剩下几百KB,用来存坐标点绰绰有余。但Flash写入有寿命限制,不适合频繁一秒写一次。我一般只把关键点写入Flash,比如按下按键时的位置、开关机时的位置、电量低报警时的位置,等到下次开机时一次性上报。
第三种是MQTT上报模式。需要Wi-Fi覆盖时把ESP32连接路由器,通过MQTT协议把实时坐标发到自己的服务器,配合后端的Web地图做成实时追踪页面。这种模式功耗最大,不适合长时间开,我只在需要实时看位置的时候手动打开。
三种模式不是互相冲突的,完全可以在代码里用按键切换,或者做成按时间窗口自动切换。
3.3 按键、LED和蜂鸣器:最简单的交互设计
对多用途设备来说,不能没有交互。哪怕不装屏幕,至少得有一个按键和一颗LED。
我在设计里留了一颗LED和一个按键:
- 短按按键:记录一个标记点,同时让LED快速闪三下,表示"当前位置已记录"。
- 长按5秒:切换Wi-Fi上报模式,开或者关,切换时蜂鸣器响一声。
- 单闪:GPS未定位。
- 双闪:GPS已定位。
- 长亮:正在上传或者充电。
这些反馈逻辑在代码里并不复杂,但能让你在外面不用掏手机就知道设备当前状态。LED和按键分别接两个GPIO,内部上拉下拉处理一下就行。蜂鸣器不建议长时间响,只在模式切换或者报警时短响200毫秒,避免户外使用时一直吵。
另一个值得做的功能是震动报警。把ESP32的GPIO接一个震动传感器,当设备检测到持续震动又同时检测到坐标快速变化,就认为被移动了。这时候蜂鸣器响,或者通过Wi-Fi推送一条消息到手机。这个功能对放在工具箱或者自行车上很有用。
4. 从原始日志到可视化地图:轨迹回放和地理围栏
4.1 用Python把NMEA日志变成KML轨迹
SD卡里记录的原始NMEA日志,直接看就是一堆文本,但如果用Python稍微处理一下,就能生成KML文件,然后扔进Google Earth或者各种离线地图工具里直接看轨迹。
处理流程很简单:先把SD卡里的NMEA文件拷到电脑,找个能解析NMEA的库pynmea2,逐行读取$GPRMC,提取有效定位点,再写入KML的LineString节点。
import pynmea2 kml_lines = ['<?xml version="1.0" encoding="UTF-8"?>', '<kml xmlns="http://www.opengis.net/kml/2.2">', '<Document><Placemark><LineString><coordinates>'] with open('gps_log.nmea') as f: for line in f: try: msg = pynmea2.parse(line) except pynmea2.ParseError: continue if isinstance(msg, pynmea2.types.talker.RMC): if msg.status == 'A': lat = msg.latitude lon = msg.longitude if lat and lon: kml_lines.append(f'{lon},{lat},0 ') kml_lines.append('</coordinates></LineString></Placemark></Document></kml>') with open('track.kml', 'w') as f: f.write('\n'.join(kml_lines))这里有个细节:NMEA坐标是"度分"格式,比如4807.038表示48度07.038分,不是48.07038度。解析库会帮你转成十进制,但如果自己写正则解析,记得先除以60换算,不然轨迹会跑到海里去。
KML文件打开后,可以看到整条轨迹的速度变化和停顿点。我在骑行测试时发现,红绿灯路口的坐标会来回跳,看起来像个锯齿。这不是GPS坏了,而是多径效应,后面实测部分会专门讲。
4.2 GPS坐标偏移问题,直接决定轨迹能不能对上地图
如果你的轨迹只是自己分析,那WGS84坐标就够了。但如果要叠加到高德、百度这类国内地图软件上,坐标一定会偏出去几十到几百米。
原因在于地图厂商使用的坐标系和GPS原始坐标系不同。GPS模块输出的是WGS84,而大部分国内地图用的是GCJ-02或者BD-09,中间需要坐标转换。网上有公开的转换算法,也可以直接用现成的库。
WGS84 -> GCJ-02: 国内地图通用 GCJ-02 -> BD-09: 百度地图额外偏置我在做轨迹叠加的时候,一开始直接拿WGS84坐标丢到高德地图,轨迹整体偏到了马路旁边的小区里,后来加了一段GCJ-02转换代码,轨迹才和实际道路完全重合。这个坑如果不提前知道,很容易误以为是GPS模块精度不够。
4.3 地理围栏:判断"是否离开某个区域"
地理围栏是本项目里比较有实用价值的功能。思路是:在地图上设定一个中心点和一个半径,设备每次定位后计算当前位置到中心点的距离,超过半径就触发报警。
距离计算用Haversine公式就能满足精度需求,不用上大而全的地理库。核心逻辑是:
import math def haversine(lat1, lon1, lat2, lon2): R = 6371000 p1 = math.radians(lat1) p2 = math.radians(lat2) dp = math.radians(lat2 - lat1) dl = math.radians(lon2 - lon1) a = math.sin(dp/2)**2 + math.cos(p1)*math.cos(p2)*math.sin(dl/2)**2 return R * 2 * math.asin(math.sqrt(a))如果周围一公里内没有目标点,就可以认为设备已经离开围栏。在地理围栏里,我最开始只看到"距离超过阈值"就立刻报警,结果GPS在城市里轻微漂移一下就会误报,最后加了两个条件:
- 连续3个定位点都超过半径,才触发报警。
- 期间速度大于某个值,说明是移动导致的,而不是单纯的漂移。
这套逻辑放在固件里,设备本身就能做判断,不用等到服务器端算。
5. 实测:冷启动、城市峡谷和功耗优化的真实数据
5.1 冷启动时间:从按下开机到首个有效定位
固件和硬件都调试完,最重要的是实测。我没有直接在城市道路上跑,而是先放在窗台上测试冷启动时间。
所谓冷启动,就是GPS模块完全断电后重新上电,没有星历、没有时间和位置信息,从零开始搜索卫星的过程。实测NEO-6M在阴天窗台上大概需要46秒,ATGM336H大约25秒。如果是在空旷的室外停车场,ATGM336H能到15秒以内。
改善冷启动的方法有几种:
- 使用支持多个卫星系统的模块,能搜到的卫星数量明显变多。
- 加装外部有源天线,比内置陶瓷天线快不少。
- 定期从服务器获取星历数据,在连接Wi-Fi时推送给GPS模块,这个操作叫AGPS辅助定位,启动时间能压到几秒。
- 模块一旦定位成功,之后手动重启的热启动会快很多,大约2到5秒出坐标。
所以如果设备不是放很久不用,平时待机时不要完全断电,而是进低功耗模式,让模块保持热启动状态,体验会好很多。
5.2 城市峡谷、隧道和地下车库里的表现
城市实测时,我沿着一条两边都是高楼的街道走了一圈,发现轨迹并不是那么理想,主要表现是三种情况。
第一是高架桥下定位点大范围漂移。GPS信号被桥体反射和遮挡,模块收到的伪距含噪声很大,导致坐标点一会儿跳到左边,一会儿跳到右边,轨迹变成一条来回打摆的曲线。这种情况单靠民用GPS很难彻底解决,只能通过算法过滤。我用的笨办法是:如果前后两个点之间的速度看起来超过了合理范围,就判定该点为漂移点并丢弃。
第二是地库和隧道完全丢星。GPS信号需要看到天空,一旦进入封闭空间,模块会直接输出无效坐标,或者停住最后一个有效位置不动。这时候需要记录"最后有效位置"和"丢星时间",等重新定位后再恢复轨迹。我在固件里加了一个状态字段,当连续N秒没有有效定位时,写入一条lost事件,事后分析就知道哪段路没数据。
第三是树木遮挡下的精度下降。公园里树冠茂密的地方,卫星数可能从十几颗掉到四五颗,水平精度从3米劣化到10米左右。轨迹上的表现是偶尔出现一个小尖刺,结合速度过滤可以去掉。
5.3 把平均功耗从100毫安压到30毫安
设备第一版用ESP32开发板加NEO-6M模块,整机工作时实测电流在100毫安左右。用2000毫安时的电池,理论上能跑20小时,但考虑到待机需求,还是不够理想。
我做了三步优化:
第一步,把ESP32的CPU频率降到80MHz。对GPS解析来说完全够用,功耗下降还不少。
第二步,GPS模块的更新率从5Hz降到1Hz。不是说GPS模块输出频率高就好,追踪车辆和人的移动速度,1Hz已经够了,降低更新率能省模块的内置处理功耗。
第三步,把ESP32进入深度睡眠模式。平时设备处于深度睡眠状态,每隔10秒醒来一次读取GPS模块的定位结果,如果坐标没变就继续睡,坐标变了就额外开Wi-Fi上报。实测定点待机时整机电流能压到十几毫安。
如果你计划做真正长时间待机的设备,更激进的方案是给GPS模块单独供电,ESP32在大部分时间里完全休眠,用一个定时器周期性唤醒GPS模块,定位成功后再唤醒ESP32。这个方案可以把平均功耗压到毫安级,但实现起来会复杂一些,需要外接一颗MOS管控制GPS模块电源。
6. 踩坑清单和后续升级方向
6.1 我在这套项目里踩过的五个坑
这个项目从第一版到基本稳定,前后迭代了三轮,遇到的坑不少,挑几个最有代表性的列在下面。
第一,TF卡写入卡顿。问题不是SD库,而是劣质TF卡速度太慢。GPS一秒一条数据,按理说任何卡都能扛住,但有些卡在连续写入小文件时会卡几百毫秒,导致GPS数据丢失。后来换成品牌高速卡,问题消失。
第二,模块供电电压不干净。ESP32开发板上的3.3V稳压器是从USB或电池降压来的,如果电源本身有毛刺,GPS模块的定位精度会受影响。排查方法是看定位稳定后的HDOP值变化,电源毛刺会导致HDOP异常跳变。
第三,GPS天线焊点松动。把天线焊到模块之后没有做固定处理,结果震动几次就虚焊了,症状是卫星数量忽多忽少,定位时好时坏。重新焊好并打热熔胶才稳定。
第四,串口打印干扰。GPS的TXD同时接了ESP32和USB调试串口,结果调试口也有GPS数据,看起来像串口混乱、定位失败。其实是因为两个串口共用了一个引脚,把调试口换掉就好了。
第五,室内假定位。在靠近窗户的地方测试,模块偶尔会报出一些距真实位置几十米的坐标,这是信号反射造成的假定位。应对方法是增加HDOP值过滤,只有水平精度达到要求的坐标才记录。
6.2 从原型到实用:后续还能怎么升级
这套方案跑通之后,还可以往几个方向继续升级。
第一个方向是把Wi-Fi换成4G Cat.1模块。ESP32自带Wi-Fi,离开路由器或者手机热点就没了。如果想实现真正的移动追踪,可以外接一个支持Cat.1网络的通信模块,比如常见的Air724UG或者ML302,用AT指令把GPS坐标通过HTTP或者MQTT发到服务器。不过模块成本和SIM卡费用都会上来,适合真的需要24小时实时追踪的场景。
第二个方向是加蓝牙信标找物。在ESP32上加一个BLE广播,平时用低功耗模式工作,当手机靠近时通过蓝牙连接读取最后的GPS坐标,相当于多了一个离线找物品的功能。
第三个方向是加太阳能充电。如果设备要长时间放在户外,可以把TP4056换成带太阳能输入的保护板,搭配一块小的柔性太阳能板,户外晴天基本可以做到永不断电。
第四个方向是数据采集频率的自适应。在固件里加入运动检测,静止时停止记录,移动时恢复记录,配合震动传感器,能把存储空间和电量的利用率提升不少。
我自己对这套设备接下来的规划,是把手上的三个版本整合成一版:保留ATGM336H模块,重新画一块邮票孔ESP32核心板,加上震动传感器、外置天线座和锂电保护,最后缩小到火柴盒大小。等这版板子打样测试完,再把原理图和固件整理出来单独写一篇。
如果你也想动手做,我的建议是别一开始就想把所有功能一次做齐全。先用开发板加杜邦线,把基本的定位日志跑通,能在一张地图上看到自己的轨迹了,再慢慢加按键、震动报警、低功耗这些东西。很多人上手就想着直接画PCB,结果硬件接线问题还没搞清楚,板子回来一样是折腾。先把这条路走通,后面所有升级都只是顺手的事。