1. 项目整体设计与方案取舍
做 GPS 导航这类小系统,最容易被低估的不是写代码,而是把一堆“看起来都能用”的器件凑到一起之后的稳定性问题。我前后做过几个基于 STM32 微控制器的 GPS 导航系统设计与实现版本,从最初只能把经纬度打印到串口,到后来能在地图上画出轨迹、计算里程、记录到 SD 卡,中间踩过的坑基本都和选型、供电、天线、数据过滤有关,反而是 NMEA 解析本身没花多少时间。这篇文章就按我实际做下来的一套方案展开,把每个环节为什么这么选、参数怎么算、问题怎么查讲清楚,适合刚上手嵌入式定位设备的同学,也适合已经能把数据显示出来但定位老是飘、老是丢点的人。
这套系统的核心目标很朴素:MCU 通过串口接收 GNSS 模块输出的定位数据,解析出经纬度、速度、航向、时间、卫星数、定位质量等字段,做有效性过滤和坐标转换,然后在本地屏幕上显示当前位置与行驶信息,同时把轨迹写入存储介质,必要时还能通过无线模块把位置回传。它解决的是一类很实际的诉求——在成本可控、功耗可控、体积可控的前提下,让一个嵌入式设备“知道自己在哪里、以多快的速度往哪里走”。对初学者来说,这是一个非常好的综合练习:串口、定时器、中断、DMA、文件系统、显示驱动、浮点运算与定点运算的取舍,全都能练到。
1.1 先把需求拆成可执行的功能块
很多人一上来就写代码,写到一半发现需求没想清楚:到底要不要显示地图?要不要离线存储?要不要做坐标系纠偏?这些决定了主控资源、存储方案和软件复杂度的上限。我在动手前习惯先列一张功能清单,把它分成“必须”“最好有”“以后再说”三档,这个方法在资源有限的 MCU 项目里非常管用。
必选部分是:串口稳定接收 GNSS 数据、解析关键语句、判定定位是否有效、计算出可用于显示的本地坐标、屏幕刷新显示、按键切换页面。可选部分是:轨迹记录到存储卡、里程与最高速度统计、航向指示、卫星信噪比柱状图。以后再说的是:离线地图渲染、路径规划、语音播报,这些对 RAM 和算力的要求会直接跨一个量级。
为什么要把离线地图往后排,原因很直接:一张哪怕只覆盖一个城市的栅格地图,分辨率稍高就是几十兆字节,STM32 的片上 Flash 和 RAM 根本放不下,必须外挂存储、做瓦片索引、做坐标变换和渲染缓冲,工作量比前面所有模块加起来还大。把这部分砍掉,整套系统在 64KB RAM 的芯片上就能跑得很舒服,这是我在第一次尝试失败后得到的教训——当时把地图位图直接转成 C 数组编译进程序,链接阶段就爆了。
注意:功能清单一定要和硬件资源一起过一遍,尤其是 RAM。GNSS 的接收缓冲、NMEA 解析用的临时字符串、屏幕显存、文件系统缓存,这几块加起来很吃内存,64KB 的芯片在做 128×64 单色屏时比较宽裕,换成 320×240 彩色屏就会很紧张。
1.2 主控芯片怎么选:F103 还是 F407
主控这块我实际用过两类:一类是 STM32F103 系列这类 Cortex-M3 内核、主频 72MHz、带 64KB Flash 和 20KB RAM 的经典型号;另一类是 STM32F407 系列这类 Cortex-M4F 内核、主频 168MHz、带浮点单元和更大 RAM 的型号。选哪个,取决于你的界面复杂度和是否需要做姿态融合。
只做串口收数、NMEA 解析、字符屏或小尺寸单色屏显示、存储卡记录,F103 完全够用,成本也低。但有两个地方会明显吃力:一是浮点运算,M3 没有硬件 FPU,所有的 float 运算都靠软件库模拟,一次除法几百个时钟周期,1Hz 的数据量还能忍,一旦要做高频滤波或者大量三角函数就会拖慢主循环;二是刷彩屏,SPI 刷 320×240 的整屏数据,时间开销相当可观。
如果预算允许并且后面想接惯性传感器做融合、或者要上彩屏和更丰富的界面,直接上带 FPU 的 M4 型号更省心,浮点运算快一个数量级,做三角函数、开方、滤波都不会成为瓶颈。我后来的版本统一换成了 M4 平台,代码结构没变,只是把软件浮点换成硬件浮点,刷新率和滤波迭代次数立刻宽松了很多。
还有一个折中方案:主控仍用 F103,但把所有浮点运算改成定点数运算。经纬度用 1e-7 度的整数表示,速度用 0.01 km/h 的整数表示,三角函数用查表加线性插值。这个做法我在一个功耗敏感的项目里用过,效果不错,代价是代码可读性下降,调试时脑内要一直做单位换算。
| 对比项 | M3 平台(72MHz,无 FPU) | M4F 平台(168MHz,带 FPU) |
|---|---|---|
| NMEA 解析 | 完全够用,注意别用 sscanf 解浮点 | 轻松,余量充足 |
| 浮点滤波 | 软件模拟,需控制调用频率 | 硬件浮点,可提高迭代频率 |
| 显示方案 | 单色 OLED / 小尺寸字符屏 | 彩色 SPI 屏也扛得住 |
| 存储卡记录 | 够用,注意缓冲区大小 | 从容 |
| 成本与功耗 | 更低 | 略高 |
1.3 GNSS 模块与天线:精度、成本与体积的平衡
模块选型主要看三件事:支持的星座数量、冷启动首次定位时间、是否有备份电源引脚。市面上常见的低成本模块以单 GPS 或 GPS+北斗双模为主,默认串口波特率多为 9600,输出标准 NMEA 语句,3.3V TTL 电平,接口简单到只需要 VCC、GND、TX、RX 四根线就能出数据。多模模块在城市环境里的可见卫星数更多,定位稳定性会好一些,但价格和功耗也略高。
这里有个很多人忽略的点:模块标称的定位精度通常是开阔地、水平面、静态条件下的统计值,实际在楼宇之间、树下、车内,误差可能从几米涨到几十米。这不是模块坏了,是信号被反射和遮挡导致的多径效应。指望靠换更贵的模块解决城市峡谷问题并不现实,正确做法是在软件上做数据过滤,把质量差的点剔除掉。
天线部分常见两类:陶瓷贴片天线和有源天线。贴片天线体积小、成本低,但增益有限,通常要配合良好的地平面;有源天线内部带低噪声放大,需要模块或主板提供馈电,灵敏度明显更好,适合设备外壳内空间较大的场景。我用过的一个失败案例是把贴片天线贴在金属外壳内侧,结果冷启动三分钟都定不下来,换成外置有源天线后十几秒就锁星了。
| 天线类型 | 优点 | 局限 | 适用场景 |
|---|---|---|---|
| 陶瓷贴片 | 体积小、免馈电、成本低 | 对地平面敏感,遮挡下表现差 | 小型手持设备、塑料外壳 |
| 有源天线 | 灵敏度高、锁星快 | 需要馈电,体积大 | 车载、户外设备、金属外壳 |
| 外置磁吸 | 摆放灵活,可远离干扰源 | 走线长,需注意屏蔽 | 车载临时安装 |
2. 硬件电路与电磁兼容细节
硬件部分是这套系统最容易翻车的地方,而且症状往往很迷惑:代码明明没改,换个板子就搜不到星;数据偶尔乱码,查半天发现是电源纹波。我在第一版硬件上吃了不少亏,后来总结出一条经验——GNSS 模块对电源干净程度和射频环境的要求,比它对代码的要求高得多。
2.1 供电与电流峰值:GPS 模块的隐藏脾气
GNSS 模块的平均电流通常在二三十毫安这个量级,但它在启动瞬间和周期性捕获阶段会有明显的电流波动,如果供电走线细、去耦不足,电压就会被拉低,表现是模块反复重启或者输出数据中断。我的做法是给模块单独一路低压差线性稳压器,输入输出各配 10 微法加 0.1 微法的组合,走线尽量短而宽,绝不和电机、蜂鸣器、无线发射模块共用一条细线。
为什么强调用线性稳压器而不是开关电源,原因在于开关电源的开关频率及其谐波可能落在 GNSS 接收频段附近,形成宽带噪声,直接抬高接收机的噪声底,表现为信噪比下降、可见星数变少。如果系统里必须用开关电源(比如电池供电需要升压),那就把开关频率选得远离接收频段,做好电感和输出滤波,并且把电源部分物理上远离天线。
还有一个容易忽略的细节:模块的备份电源引脚。这个引脚接一颗小容量电池或超级电容,用于在主电源断开后维持星历和实时时钟,从而实现热启动。热启动首次定位时间通常在一秒量级,冷启动则可能要三十秒以上。如果你的设备是“用完就关”的便携设备,这颗备份电池带来的体验差异非常明显。我第一次做的时候把这脚悬空了,结果每次开机都要在户外站半分钟等定位,用户直觉上会认为设备坏了。
注意:备份电池的功耗非常低,但要确认模块手册里对备份电压范围的要求,直接接主电源会失去备份意义,接错电压可能损坏模块。
2.2 串口连线、电平与备份电池
串口连接本身很简单,模块的发送接 MCU 的接收,模块的接收接 MCU 的发送,两边共地。这里有两个高频错误:一是忘了共地,只连了信号线,数据时有时无;二是把发送接收接反了,现象是完全没有数据。判断方法很朴素,用逻辑分析仪或者示波器看模块的发送脚,如果有规律的数据波形说明模块在工作,问题是 MCU 侧的配置或连线。
电平方面,绝大多数模块输出 3.3V TTL,与 3.3V 的 MCU 直接相连即可。如果你的主控是 5V 系统,必须做电平转换,不能靠串电阻硬扛,长期工作在超限电压下引脚寿命会受影响。反过来,如果 MCU 是 3.3V 而模块是 5V 输出,同样需要转换,否则可能打坏 MCU 的输入引脚。
配置串口时的参数是固定套路:波特率与模块默认值一致(常见 9600,也有出厂 38400 或 115200 的),8 位数据位,1 位停止位,无校验。为什么不用校验位,因为 NMEA 语句本身就带异或校验和,链路层再加校验意义不大,还会让部分模块的配置模式失灵。
2.3 PCB 布局:天线附近不能出现的几样东西
射频这块我不算专家,但踩过的坑足够写出几条硬规矩。天线要尽量靠近板边或者外壳开口处,正上方和正下方不要有覆铜、电池、屏蔽罩;晶振、开关电源电感、高速数字走线要远离天线区域,至少保持十几毫米以上的距离;如果天线是通过馈线连接的,馈线要走阻抗受控的走线,并在模块端做好匹配。
地平面是贴片天线的“另一半”,很多人只关注天线本身,忘了给天线下方铺一块完整的地。地平面尺寸太小,天线的谐振点会偏移,增益下降;地平面被走线割裂,效果同样打折。我一般保证天线下方地平面连续,并且和主地通过多个过孔连接,避免形成细长的回流路径。
还有一个特别隐蔽的问题:屏幕排线和存储卡座。SPI 时钟频率高的时候,排线就是一根天线,辐射出去会干扰接收。我的处理是把屏幕排线尽量短、加地线伴行,必要时在排线上套铁氧体磁环,或者在软件上降低 SPI 时钟频率试试看定位是否变好——如果降频后搜星明显改善,那就基本锁定是数字噪声干扰。
3. 软件架构:从串口字节流到定位数据
软件部分我倾向于把整条链路切成四层:字节接收层、语句切分层、字段解析层、业务应用层。这样切的好处是每一层单独可测,出问题能快速定位是哪一层。比如屏幕上坐标不动,可能是接收层没收到数据,也可能是解析层字段错位,还可能是应用层的过滤条件把所有点都判为无效,分层之后调试效率会高很多。
3.1 DMA + 空闲中断的接收框架
用轮询或者单字节中断接收都可以,但在 9600 波特率下每秒大约 960 字节,模块还可能同时输出多条语句,单字节中断会让 CPU 频繁进出中断,影响其他任务。更稳妥的方式是用 DMA 把串口数据搬到缓冲区,再用串口空闲中断来判断“一帧数据收完了”。
空闲中断的原理是:串口在接收完一个字节后,如果超过一个字符时间没有新数据到来,硬件会置位空闲标志。利用这个特性,可以把不定长的 NMEA 语句按帧切开,避免在软件里逐字节判断换行符。下面是我常用的骨架代码,基于 HAL 库的接收空闲 DMA 接口:
#define GNSS_BUF_SIZE 512 static uint8_t gnss_buf[GNSS_BUF_SIZE]; static volatile uint16_t gnss_len = 0; static volatile uint8_t gnss_ready = 0; void GNSS_Start(void) { HAL_UARTEx_ReceiveToIdle_DMA(&huart2, gnss_buf, GNSS_BUF_SIZE); __HAL_DMA_DISABLE_IT(&hdma_usart2_rx, DMA_IT_HT); } /* 回调里只做搬运标记,不做解析 */ void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) { if (huart->Instance == USART2) { gnss_len = Size; gnss_ready = 1; HAL_UARTEx_ReceiveToIdle_DMA(&huart2, gnss_buf, GNSS_BUF_SIZE); __HAL_DMA_DISABLE_IT(&hdma_usart2_rx, DMA_IT_HT); } }这段代码的关键点在于“回调里不解析”。很多新手直接在中断回调里跑字符串处理,一旦数据量大或者解析里带浮点运算,就会挤占中断时间,导致后续数据丢失。正确做法是把长度和标志置位,主循环检测到标志后再处理,处理完清标志。缓冲区建议给到 512 字节以上,因为模块一次可能连续输出五六条语句,总长度接近四百字节。
提示:关闭 DMA 的半传输中断是必要的,否则一次接收会触发两次回调,第二次拿到的是半截数据,解析必然失败。这个坑我踩过一次,现象是每隔一帧就解析失败一帧。
3.2 NMEA 协议逐字段拆解与解析实现
NMEA 语句的通用格式是:以美元符号开头,逗号分隔字段,星号后面跟两位十六进制校验和,最后是回车换行。校验和是美元符号与星号之间所有字符的异或值。解析时先验证校验和,再按逗号切分,这一步能把绝大多数噪声和半截数据挡在外面。
真正需要的关键语句其实不多。定位信息主要来自推荐最小定位信息语句和定位质量语句,前者给出时间、经纬度、速度、航向、日期,后者给出定位质量因子、使用卫星数、水平精度因子、海拔。卫星信噪比语句只在做信号强度显示时才需要,如果只是记录轨迹,完全可以把它关掉以减少数据量。
下面这条是典型的推荐最小定位信息语句结构:
$GNRMC,083559.00,A,2237.4621,N,11402.8891,E,0.004,,241120,,,A*6C字段含义依次是:UTC 时间、状态(A 有效 / V 无效)、纬度(度分格式)、南北半球、经度、东西半球、地面速度(节)、航向(度)、UTC 日期、磁偏角、磁偏方向、模式指示。这里最容易出错的是经纬度的“度分”格式——2237.4621 表示 22 度 37.4621 分,需要拆成整数度和小数分再换算:
/* 输入 ddmm.mmmm 形式的字符串,输出十进制度 */ double nmea_to_deg(const char *s) { double raw = atof(s); int deg = (int)(raw / 100.0); double min = raw - deg * 100.0; return deg + min / 60.0; }为什么不用 sscanf 一口气把整条语句拆完,因为 sscanf 处理浮点会引入较大的代码体积和较慢的执行速度,在没有硬件浮点的平台上代价更明显。用指针手动扫描逗号位置,再按字段类型分别转换,速度更快也更可控。我一般的做法是先扫描一遍把所有逗号的偏移记到数组里,然后按索引取字段,这样代码可读性和效率都不错。
还有一点值得提醒:不同模块输出的语句前缀不一样,单 GPS 模块是 GP 开头,双模模块可能输出 GN、GB、BD 等多种前缀。解析时不要硬编码判断“GP”,直接判断语句标识符部分(逗号前的最后三个字符)更稳妥,或者干脆只按语句类型关键字匹配。
3.3 数据有效性过滤:把脏点挡在门外
原始定位数据里混着大量不可直接使用的点。我见过的最夸张的一次,设备静止放在窗台上,轨迹图上却出现了一条横跨半个城市的直线,原因就是某个时刻模块输出了一组跳变坐标。如果不做过滤,这种点会污染里程统计和轨迹记录。
过滤分几层。第一层看状态位,状态为无效的直接丢弃,这是最基本的。第二层看定位质量因子,单点定位、差分定位、固定解的可信度差别很大,如果项目要求较高,可以把质量因子低于某个阈值的点剔除。第三层看水平精度因子,这个值反映卫星几何分布的好坏,数值越大定位越不可信,我一般设一个门槛,超过就标记为低质量。第四层做运动学一致性检查,用相邻两点的时间和距离算隐含速度,如果超过合理上限(比如每小时二百公里),就判定为跳点丢弃。
| 过滤层级 | 判据 | 典型阈值 | 作用 |
|---|---|---|---|
| 状态位 | 有效标志 | 仅接受有效 | 拦截无效解 |
| 质量因子 | 定位解类型 | 单点及以上 | 排除未定位 |
| 精度因子 | 水平精度因子 | 小于 5.0 | 排除几何差 |
| 卫星数 | 参与解算卫星数 | 大于等于 4 | 排除欠定解 |
| 速度一致性 | 隐含速度 | 小于 200 km/h | 拦截跳变点 |
速度一致性检查的具体算法是:用两点的经纬度算球面距离,除以时间差得到速度,与阈值比较。这里不用太精确的公式,用等距圆柱近似就够了,误差在百分之一以内,对过滤来说完全够用。真正需要注意的是时间戳,一定要用 UTC 时间算差值,不要用本地毫秒计数,否则跨整秒的时候会出现负值。
4. 坐标转换、里程计算与显示存储
数据从模块出来是 WGS84 坐标,直接显示给用户会有个问题:在某些地图上叠加会有几百米的偏移。这不是模块的错,是坐标系本身不同。如果只是自己记录轨迹、在自绘的网格上显示,那用原始坐标完全没问题;如果要叠加到常见的在线地图上,就必须做转换。
4.1 WGS84 到本地平面坐标的换算
转换分两步走。第一步是球面坐标到平面坐标的投影,常用的做法是以某个参考点为原点,用等距圆柱近似或者墨卡托投影把经纬度差换算成米:
/* 以 ref 为原点的局部平面坐标,单位米,适用于几公里范围 */ void wgs84_to_local(double lat, double lon, double ref_lat, double ref_lon, double *x, double *y) { const double M_PER_DEG_LAT = 111320.0; *y = (lat - ref_lat) * M_PER_DEG_LAT; *x = (lon - ref_lon) * M_PER_DEG_LAT * cos(ref_lat * M_PI / 180.0); }为什么用这个近似而不是完整的大地测量公式,因为在小范围内(几公里)误差只有厘米到分米级,而计算量小得多。参考点的选择也影响精度,一般选第一次有效定位的点,或者选设备常用区域的中心点。如果设备活动范围超过几十公里,就要考虑用更严谨的投影,否则横向误差会随距离增长。
第二步是坐标系纠偏,如果目标地图使用非 WGS84 的坐标体系,需要套用公开的偏移算法。这部分我不建议自己从头推导,用现成的公开算法实现即可,注意算法里有迭代逼近的过程,调用频率不要太高,1Hz 完全没问题。我一般只在需要显示到地图上时才做转换,内部存储和里程计算仍然用原始坐标,这样逻辑更清晰,也便于以后切换到其他地图。
4.2 速度、航向与里程的平滑处理
模块直接给出的速度和航向抖动比较明显,尤其是低速状态下,静止时航向可能随机跳变。直接显示会让人觉得设备“不靠谱”,所以要做平滑。最简单的一阶低通滤波就够用:
/* alpha 越小越平滑,代价是响应变慢,实测 0.2~0.3 比较均衡 */ speed_filt = speed_filt * (1.0f - alpha) + speed_raw * alpha;航向的平滑要特殊处理,因为它是个环形量,359 度和 1 度实际只差 2 度,直接做算术平均会算出 180 度。正确做法是先把角度转成单位向量,对两个分量分别滤波,再反算角度。这个细节我在第一版里没注意,结果设备直行时航向显示在 0 度和 360 度之间乱跳。
里程计算用相邻有效点的距离累加。要注意的是,静止时模块输出的坐标会有米级抖动,如果每个点都累加,一晚上能“走”出好几公里。解决办法是设一个最小位移阈值,比如位移小于三米就不累加,同时速度低于某个值时不更新最大速度。这些细节看起来琐碎,但直接决定最终数据是否可信。
还有一个经验:里程统计要区分“累计里程”和“本次行程”,并且提供手动清零的入口。我最早做的版本只能累计,测试时想让数据归零只能重启,非常别扭。
4.3 屏幕刷新与SD卡轨迹记录
显示部分我用过单色 OLED 和彩色 SPI 屏两种。单色屏刷新快、代码简单、占用 RAM 少,适合只显示数字和简单图标的场景;彩色屏视觉效果好,但整屏刷新耗时明显,需要做局部刷新或者限制刷新区域。不管哪种,刷新频率都不要跟着数据频率走,1Hz 的定位数据配 5Hz 的界面刷新比较舒服,太快了人眼也看不出差别,还白占 CPU。
界面布局我一般分三页:主页显示经纬度、速度、航向、卫星数;轨迹页显示一个以当前位置为中心的简易网格和最近若干点的连线;状态页显示定位质量、精度因子、海拔、UTC 时间和设备运行时长。按键用两个就够,一个翻页一个清零,长按短按区分功能。
存储卡记录这块,如果用文件系统,建议先打开文件后保持打开状态,间隔写入,不要每条数据都开关文件,否则写入耗时和卡寿命都不划算。写入频率 1Hz 即可,每条记录包含时间戳、经纬度、速度、航向、卫星数、质量因子,用逗号分隔的文本格式,方便事后用表格软件打开分析。缓冲区攒够一定条数再统一写入,能显著降低卡的操作次数。
注意:拔卡前一定要先停止写入并关闭文件,否则最后一段数据大概率丢失,严重时文件系统会损坏。这个坑我在测试阶段踩过不止一次。
5. 搜星慢、漂移、乱码:排查实录
调试阶段遇到的问题,八成集中在“收不到数据”“数据乱码”“定位不准”这三类。我习惯按链路顺序排查:先确认硬件有没有在工作,再看数据有没有到 MCU,再看解析对不对,最后看应用逻辑有没有把它过滤掉。这个顺序能避免在错误的层面上瞎折腾。
5.1 定位问题排查树
搜不到星是最常见的问题。第一步确认天线,把设备拿到室外空旷处,天线面朝上,等待时间给足(冷启动至少留两分钟)。如果在室内窗边测试,很可能一颗星都搜不到,这不是设备问题。第二步看数据里有没有卫星数信息,如果能看到卫星但定位状态一直是无效,说明信号弱或者几何分布差,需要换位置再试。第三步看备份电池,如果每次上电都是冷启动,首次定位时间会很长,接上备份电源后改善明显。
定位漂移分两种情况。静止漂移是正常的,单点定位在开阔地的水平误差几米很常见,设备放在桌上坐标小幅跳动不必大惊小怪。运动中的漂移要看是不是跳点,用前面说的速度一致性检查基本能过滤掉。还有一种漂移是坐标系问题,轨迹整体偏移几百米但形状正确,那就是没做坐标转换,跟模块精度无关。
| 现象 | 可能原因 | 排查动作 |
|---|---|---|
| 完全没有数据 | 接线反、未共地、波特率不符 | 示波器看模块发送脚波形 |
| 有数据但全是无效解 | 天线被遮挡、室内测试 | 移到室外空旷处重新上电 |
| 冷启动极慢 | 备份电源缺失、天线增益低 | 检查备份脚电压,换有源天线 |
| 轨迹整体偏移 | 未做坐标系转换 | 核对目标地图坐标系 |
| 偶发跳点 | 多径、信号瞬时劣化 | 加精度因子与速度一致性过滤 |
| 数据间歇性乱码 | 电源纹波、地线干扰、缓冲区溢出 | 查电源纹波,加大缓冲,降 SPI 频率 |
5.2 串口数据问题
乱码是另一大类。先看波特率,模块出厂默认值不一定和你配置的一致,尤其是一些可配置模块,可能被上一次的配置改过。用示波器量一下一个位的宽度,反推波特率,是最快的确认方式。再看缓冲区,如果缓冲区小于一帧数据的长度,长语句会被截断,解析自然失败。
还有一种隐蔽的乱码来自中断优先级。如果串口接收和屏幕刷新的优先级配置不当,高优先级任务长时间占用 CPU,串口硬件溢出标志置位,数据就丢了。我的做法是把串口 DMA 接收设为较高优先级,屏幕刷新放在主循环里做,中间用状态机衔接,避免长时间阻塞。
数据解析错误也常被误认为乱码。比如字段索引算错一位,纬度读到了经度,或者半球标志没处理导致南纬变成北纬。这类问题的特点是数据“看起来合理但位置不对”,调试时把原始语句和解析结果并排打印出来对照,一眼就能看出来。
5.3 问题速查表
| 排查项 | 检查方法 | 期望结果 | 不满足时的处理 |
|---|---|---|---|
| 模块供电 | 万用表测模块电源脚 | 稳定在标称值附近 | 换独立稳压,加去耦 |
| 串口波形 | 示波器看发送脚 | 有规律数据帧 | 检查模块是否上电、天线是否接 |
| 波特率 | 量位宽反推 | 与配置一致 | 逐一试常见波特率 |
| 校验和 | 打印原始语句与计算结果 | 两者一致 | 检查缓冲区截断、帧边界 |
| 定位状态 | 打印状态字段 | 有效 | 移到室外,等待冷启动 |
| 精度因子 | 打印该字段 | 小于阈值 | 换位置,或提高过滤门槛 |
| 坐标转换 | 与已知点对比 | 偏差在预期内 | 检查转换算法与参考点 |
6. 可继续扩展的方向
这套系统跑通之后,往下扩展的空间其实挺大,而且每个方向都有明确的实用价值,不是纯粹为了炫技。我在几个不同需求的版本里分别做过下面的尝试,有的收益很大,有的纯属折腾。
6.1 秒脉冲授时与时间同步
很多 GNSS 模块除了串口还会引出一路秒脉冲输出,上升沿与世界时整秒对齐,精度在几十纳秒量级。用它配合 MCU 的定时器捕获功能,可以做一个相当准的本地时钟:串口给出整秒对应的日期时间,脉冲给出精确的秒边界,两者结合就能把本地时间误差压到毫秒以内,还能顺便校准 MCU 内部低速时钟的偏差。
实现起来不难,把脉冲引脚接到定时器的输入捕获通道,捕获中断里记录计数值,同时读取串口刚解析出的时间。要注意的是串口语句到达时间和脉冲之间有几毫秒到几十毫秒的延迟,需要做补偿,补偿量可以通过多次测量取平均得到。这个功能在需要给多设备打统一时间戳的场景里非常有用。
6.2 低成本惯导融合
GNSS 在隧道、地下车库、密集楼宇之间会失锁,短则几秒长则几分钟。如果设备同时装了加速度计和陀螺仪,失锁期间可以靠航位推算短时维持位置估计,等信号恢复后再做校正。我做过一个简化版本,只用陀螺仪积分航向、用上次有效速度积分位移,失锁十秒内位置误差还能接受,再长就飘得没法看了。
真要做扎实,得上互补滤波或者扩展卡尔曼滤波,把 GNSS 位置速度作为观测量,惯性数据作为预测,这个工作量不小,但效果提升明显。对于只想练手的同学,建议先做最简单的航位推算,把数据链路和状态切换逻辑跑通,再考虑上滤波算法。
6.3 低功耗与周期性唤醒
电池供电的设备,功耗是大问题。GNSS 模块连续工作时的电流对小型电池来说压力不小,常见的优化是周期性开关:定位成功后记录位置,然后让模块进入待机或者直接断电,MCU 进入低功耗模式,定时器唤醒后再上电定位。因为有备份电源维持星历,热启动通常一两秒就能重新定位,整体平均功耗能降一个量级。
这里的取舍在于定位的实时性。如果设备是放在背包里做轨迹记录,每隔几十秒定位一次完全够用,轨迹照样连贯;如果是要做实时导航,那就不能频繁断电。我在一个长期部署的记录设备上用了这个策略,采样间隔设成三十秒,一节小容量电池能撑好几天,代价是轨迹在转弯处会显得比较生硬。
我自己在这套系统上花时间最多的其实不是写代码,而是反复拿着设备到不同环境里跑,看它在树下、天桥下、楼宇之间到底表现如何,然后回头调过滤阈值和天线位置。软件能把数据变好看,但真正决定定位质量的还是天线摆在哪、电源干不干净、测试环境够不够真实。