☰
UM220-III北斗/GPS模块Android集成:从NMEA解析到避坑指南
2026/10/14 18:26:42 网站建设 项目流程

简介:面向Android底层开发与设备集成人员的和芯星通UM220-III定位芯片支持包,主要解决Android系统识别与调用该芯片北斗/GPS定位能力的问题,包含framework定制、HAL硬件抽象层、测试应用及配套文档。压缩包共29个文件,大小约2.55MB,涵盖java框架接口、c/cpp与hal驱动实现、aidl接口定义、xml配置、mk编译脚本、apk测试程序,以及协议规范与移植参考pdf,可支撑从驱动适配到上层服务调试的完整流程。目前已有851人学习下载。借助这套支持包,开发者能直接利用HAL标准接口获取位置数据,通过测试APK验证定位成功率、精度和信号强度,并依据协议文档调整芯片工作模式与功耗;移植协助文档则按步骤指导如何整合到自有项目中,显著降低集成门槛,适用于车载导航、物流追踪及物联网终端等场景。

1. 和芯星通 UM220-III 这套 Android 支持包,到底在解决什么问题

拿一块 UM220-III 北斗/GPS 双模模块往 Android 设备上一接,很多人以为插上串口就能出坐标了。实际上你面对的是一个完全没有系统驱动的串口设备、一堆以$GNGGA开头的 NMEA 文本流,以及 Android 系统里那些并不想理你的 USB 权限。所谓“Android 支持包”,核心就是把这三点串起来:让 Android 应用能拿到模块吐出的 NMEA 语句,解析成经纬度、速度、UTC 时间,再稳定地送进地图或业务逻辑里。这篇笔记的价值就在这:把从硬件接线到坐标落地的完整链路讲透,适合正在做车辆定位、手持采集终端、农业导航这类项目的嵌入式或 Android 开发人员。我会直接给出验证过的做法和参数,也把容易翻车的地方单独拎出来说。

2. 先把链路搞清楚:UM220-III 在 Android 设备上是怎样把定位数据送上来的

2.1 通信链路选型:USB 转串口还是开发板 UART

UM220-III 对外输出的是一路 UART TTL 电平信号,协议是 NMEA 0183。Android 设备要拿到这路数据,常见的是两条路。

第一条路是手机/平板通过 USB OTG 接一个 USB 转 TTL 适配器。适配器一端接模块的 TX/RX/GND,另一端插进 Android 设备的 USB 口。这种方式的好处是换主机方便,不用动硬件;缺点是要处理 Android 的 USB Host 权限申请,而且不同 USB 转 TTL 芯片(FT232、CP210x、CH340 这类)对应的 VID/PID 不一样,枚举逻辑得做兼容。

第二条路是开发板方案,比如 RK 系列、全志系列这类带 UART 引脚的板子,模块直接接板载 UART。这种方式不用走 USB 权限,但需要 root 权限或系统级串口访问权限去读/dev/ttyS*,普通应用如果没有 system 权限是打不开节点的。常见做法是写一个带系统签名的应用,或者把串口读取逻辑放到一个 root 守护进程里。

我一般先看产品形态再选:如果做的是手持设备,以 Android 系统为主,优先走 USB 转串口,因为换机器不用改硬件;如果做的是集成度高的车机或工控机,板载 UART 更稳,少一个 USB 转接的延迟和供电问题。

2.2 认识波特率、NMEA 0183 和 $GNGGA/$GNRMC

UM220-III 默认波特率是 9600,8 个数据位、1 个停止位、无校验,也就是常说的 9600 8N1。这是入手第一步必须确认的东西。模块本身支持通过串口指令改波特率,但默认值就是 9600,改完之后要么断电重启生效,要么发指令后立即生效,不同固件行为略有区别。

模块因为支持北斗和 GPS 双模,输出的 NMEA 语句大多以$GN开头而不是$GP。$GNGGA是定位核心语句,包含 UTC 时间、纬度、经度、定位质量、可见卫星数、海拔;$GNRMC是推荐最小定位信息,包含状态位、经纬度、速度、航向和 UTC 日期。还有一个值得留意的是$GNGSV,它会把可见卫星分成多句输出,里面能看到卫星编号 PRN 以及前缀是 G(GPS)还是 C(北斗)。

把这个认识放到 Android 集成里,意义在于:你不应该去解析所有语句,只要盯住 GGA 和 RMC 就够了。GSV 用于调试天线和卫星可见性时可以看,平时业务里没必要处理。

2.3 验证链路的最小步骤:串口助手先于代码

很多人拿到支持包第一时间就写 Android 代码,结果数据读不出来,分不清是硬件问题还是软件问题。正确的顺序是先用电脑上的串口助手验证一遍模块输出。把 USB 转 TTL 插到电脑上,设备管理器里能看到对应的 COM 口,打开串口设为 9600 8N1,上电后应该能持续收到类似下面的语句:

$GNGGA,082735.000,3114.4250,N,12132.3325,E,1,08,1.2,14.6,M,-5.3,M,,*5F $GNRMC,082735.000,A,3114.4250,N,12132.3325,E,0.02,0.00,140625,,,D*47

看到这两条,说明模块、天线、串口链路都是通的,后面做 Android 集成才有意义。如果一条都没有:先看模块供电是否在 3.0V 到 3.6V 范围,再看 TX/RX 是否接反,最后看天线是否有源且馈电正常。前面这步能省掉大量在 Android 端排查的时间。

提示:模块上电后如果处于冷启动状态,首次定位可能需要 30 秒到几分钟不等,串口助手要耐心等一会。

3. 支持包集成第一步:在 Android 工程里把串口数据读上来

3.1 权限、USB 设备枚举与权限申请

如果是 USB 转串口方案,Android 应用要做的第一件事是声明 USB Host 功能,并在运行时申请访问 USB 设备的权限。应用启动后,枚举当前连接的 USB 设备,找到 VID/PID 匹配的那个,然后向用户弹窗申请权限。

// 声明:应用需要 USB Host 能力 // <uses-feature android:name="android.hardware.usb.host" /> // <uses-permission android:name="android.permission.USB_PERMISSION" /> UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE); HashMap<String, UsbDevice> deviceList = usbManager.getDeviceList(); UsbDevice targetDevice = null; for (UsbDevice device : deviceList.values()) { // 以常见的 FT232 为例,通常 VID=0x0403, PID=0x6001 // CP210x 通常是 VID=0x10C4, PID=0xEA60 // 自己设备对应的 VID/PID 可以通过 USB 描述符或串口助手确认 if (device.getVendorId() == 0x0403 && device.getProductId() == 0x6001) { targetDevice = device; break; } } if (targetDevice != null) { PendingIntent permissionIntent = PendingIntent.getBroadcast( this, 0, new Intent("com.example.USB_PERMISSION"), PendingIntent.FLAG_IMMUTABLE); usbManager.requestPermission(targetDevice, permissionIntent); }

这段代码的逻辑分三层。第一层拿到UsbManager实例,它是访问所有 USB 设备的入口。第二层遍历getDeviceList()返回的设备表,按 VID/PID 匹配目标设备。第三层调用requestPermission触发系统弹窗。需要注意PendingIntent.FLAG_IMMUTABLE在 Android 12 以上是强制要求的,不加会直接崩溃。

参数选择上做了两个兼容:VID/PID 不是写死在代码里,而是做成可配置项,因为实际项目里可能换不同芯片的 USB 转串口模块;权限广播的 action 字符串也要注册到 manifest 里,否则回调收不到。这一步踩坑最多的是:设备已经插上了,但getDeviceList()是空的。原因通常是两块,一是设备插上时应用还没启动,需要监听UsbManager.ACTION_USB_DEVICE_ATTACHED广播;二是应用没有声明 USB Host feature,系统直接没有枚举权限。

3.2 串口读取代码与数据分包问题

拿到 USB 权限之后,就要通过 USB 设备打开串口通道了。支持包里通常会封装一个SerialPort类,底层用的是UsbSerialPort或直接操作文件描述符。这里给出一个基于文件描述符方式的读取框架,也是开发板上最常用的方式:

// 打开串口设备节点 /dev/ttyUSB0(USB 转串口)或 /dev/ttyS3(板载 UART) // 需要 root 或有 system 权限才能访问这些节点 File device = new File("/dev/ttyUSB0"); if (!device.exists()) { throw new IOException("串口节点不存在,请确认设备连接"); } // 常见的打开方式是直接构造 FileDescriptor FileDescriptor fd = device.openFd(); FileInputStream inputStream = new FileInputStream(fd); FileOutputStream outputStream = new FileOutputStream(fd); byte[] buffer = new byte[4096]; int len; while (!isStopped) { len = inputStream.read(buffer); if (len > 0) { String rawData = new String(buffer, 0, len, StandardCharsets.US_ASCII); // 交给解析层处理 handleNmeaData(rawData); } }

这个循环本身很简单,但有几个容易被忽略的参数选择。读缓冲设为 4096 字节,对应 9600 波特率下约 4 秒的数据量,实际使用中这个大小足够容纳连续几条 NMEA 语句,同时不会因为缓冲太大而增加延迟。

真正要小心的是 NMEA 数据的分包问题。串口数据不是一个完整句子一次到达的,read()返回的数据可能只包含一句话的一部分,也可能一次包含好几句话。如果直接把rawData交给解析函数按整句处理,必然出现“只剩半句”的情况。一个可靠的做法是维护一个字符串缓冲区,收到数据先追加进去,再按$和换行符切分出完整句子:

StringBuilder frameBuffer = new StringBuilder(); private void handleNmeaData(String chunk) { frameBuffer.append(chunk); String data = frameBuffer.toString(); // 按换行符切分,保留最后一段不完整的数据 int lastNewLine = data.lastIndexOf('\n'); if (lastNewLine > 0) { String completePart = data.substring(0, lastNewLine); frameBuffer = new StringBuilder(data.substring(lastNewLine + 1)); for (String line : completePart.split("\n")) { String trimmed = line.trim(); if (trimmed.startsWith("$") && trimmed.length() > 8) { parseNmeaLine(trimmed); } } } }

这里的逻辑是:每次收到新数据先追加到frameBuffer,然后找最后一个换行符,把之前的内容当作完整数据进行切分,剩下的留在缓冲里等下一次数据到达。这种“留尾巴”的思路专门应对串口分包,是实际开发里必须处理的一环。

3.3 关键参数:波特率 9600、读取超时与线程模型

串口参数里最不能乱调的就是波特率。UM220-III 默认 9600,如果你的代码里用了 115200,读出来的数据就会是乱码。这个问题我在项目里见过多次,现象很典型:串口助手能出数据,代码里读出来却是乱码或全空。排查方法是在初始化串口时加一句日志打印当前波特率,对照硬件配置确认。

线程模型方面,串口读取必须放在独立线程里。Android 主线程不能做阻塞式 I/O 操作,inputStream.read()在没有数据到来时会一直阻塞,放主线程会直接触发 ANR。常见做法是启动一个专门的后台线程跑读取循环,用 volatile 布尔变量控制停止。这里还有一个细节:退出页面或关闭应用时一定要inputStream.close()并中断线程,否则串口节点被占用,下次打开会失败,这个问题在反复进出页面的场景里特别明显。

4. 把 NMEA 变成坐标:解析层设计与常见字段

4.1 解析 GGA 获取经纬度、定位质量与可见卫星数

拿到完整 NMEA 语句后,解析层要做的第一件事是按逗号拆分字段。$GNGGA的固定格式有 14 个字段,下面给出一个相对完整的解析实现:

// 示例输入: $GNGGA,082735.000,3114.4250,N,12132.3325,E,1,08,1.2,14.6,M,-5.3,M,,*5F privatevoid parseGGA(String[] fields) { if (fields.length < 14) { return; } String utcTime = fields[1]; // 082735.000 格式为 HHmmss.sss String latitude = fields[2]; // 3114.4250 表示 31 度 14.4250 分 String latDirection = fields[3]; // N 或 S String longitude = fields[4]; // 12132.3325 表示 121 度 32.3325 分 String lonDirection = fields[5]; // E 或 W String quality = fields[6]; // 0=无效 1=GPS定位 2=差分定位 String satCount = fields[7]; // 参与定位的卫星数 String hdop = fields[8]; // 水平精度因子,值越小精度越高 if ("S".equals(latDirection)) { latitude = "-" + latitude; } if ("W".equals(lonDirection)) { longitude = "-" + longitude; } double latDeg = convertToDegrees(latitude); double lonDeg = convertToDegrees(longitude); } private double convertToDegrees(String nmeaCoordinate) { // NMEA 坐标格式为 DDMM.MMMM,需要转换为十进制度 double raw = Double.parseDouble(nmeaCoordinate); int degrees = (int) (raw / 100); // 取度 double minutes = raw - degrees * 100; // 取分 return degrees + minutes / 60.0; // 度 + 分/60 }

解析逻辑的核心是理解 NMEA 的度分格式。3114.4250不是 31.144250 度,而是 31 度 14.4250 分,转换时要先除以 100 取整数部分作为度,余下部分除以 60 转换为度的小数。这个转换做反了,坐标会偏差几十公里。

定位质量字段quality也值得看一眼:0 表示定位无效或正在收敛,1 表示 GPS/北斗单点定位,2 表示差分定位。在车机项目里,这个字段是用来做定位有效性判断的第一道门槛,quality == 0时即便经纬度有数值也不能送给业务层。

4.2 解析 RMC 获取 UTC 时间、航速与航向

RMC 语句里的速度单位是节,1 节等于 1.852 公里每小时,很多项目直接拿字段当公里每小时用,导致速度显示偏差快一倍。下面给出带单位换算的解析代码:

// 示例输入: $GNRMC,082735.000,A,3114.4250,N,12132.3325,E,0.02,0.00,140625,,,D*47 privateParsedRMC parseRMC(String[] fields) { ParsedRMC rmc = new ParsedRMC(); rmc.utcTime = fields[1]; // UTC 时间 rmc.status = fields[2]; // A=有效定位 V=无效 rmc.latitude = fields[3]; rmc.longitude = fields[5]; rmc.speedKnots = Double.parseDouble(fields[7]); rmc.courseDegrees = Double.parseDouble(fields[8]); rmc.utcDate = fields[9]; // DDMMYY 格式 // 节转公里每小时 rmc.speedKph = rmc.speedKnots * 1.852; // 注意:RMC 里的航向是地面航迹角,不是车头朝向 return rmc; }

RMC.status是A才表示定位有效,这是比 GGA 的 quality 更直观的另一个判据。航向courseDegrees需要特别注意:它表示的是地面航迹方向(相对真北),不是车头朝向,车辆静止时这个值可能乱跳,做导航时要做滤波。

UTC 时间也要处理时区。082735.000是 UTC 时间,北京时间要加 8 小时。一个常见做法是在解析时先拿到完整时间字符串,再通过SimpleTimeZone或OffsetDateTime做转换,不要在拼接时手写加减逻辑,因为日期跨天时会算错。

4.3 北斗/GPS 双模判断:卫星编号 ID 前缀与系统识别

UM220-III 支持北斗和 GPS 同时接收,了解当前使用的是哪个系统有助于判断定位质量。卫星编号可以从$GNGSV语句中读取:

$GNGSV,3,1,11,01,38,211,35,05,57,307,44,07,31,232,,12,42,271,*77

GSV 语句第一个字段是总句数,第二个是当前句号,第三个是总卫星数,后面每 4 个字段为一颗卫星的信息,依次是 PRN 号、仰角、方位角、信噪比。PRN 号前面的标记可以看到系统来源。

卫星编号的判断规则:GPS 卫星的 PRN 范围是 01 到 32,北斗卫星的 PRN 通常是 C01 到 C37。有的模块在 GSV 语句里直接给 C01 这种带前缀的编号,有的给纯数字。处理逻辑建议统一按字符串前缀判断:

private SatelliteSystem detectSatelliteSystem(String prn) { if (prn.startsWith("C") || prn.startsWith("B")) { return SatelliteSystem.BDS; // 北斗 } else if (prn.startsWith("G")) { return SatelliteSystem.GPS; } else { return SatelliteSystem.UNKNOWN; } }

这个判断本身不难,但实际调试天线时很有用:如果 GSV 里全是 G 开头的卫星,北斗通道可能有问题;如果总卫星数很多但信噪比全在 20 以下,多半是天线安装位置不好或者馈电异常。建议调试阶段把 GSV 原始日志完整输出到一个文件里,方便复现。

5. UM220-III Android 集成避坑:从串口打不开到坐标漂移

5.1 避坑:USB 设备节点变化导致打不开

现象:应用第一次打开串口成功,拔掉 USB 转串口重新插上后,代码里死等/dev/ttyUSB0这个节点,结果文件不存在,串口打不开。

原因:USB 设备节点由内核动态分配。如果之前拔掉的是 ttyUSB0 对应的设备,再插上时内核可能分配为 ttyUSB1;开发板上还要考虑 UART 口的 enable 引脚时序,节点出现有延迟。

解决:打开串口前先扫描/dev/ttyUSB*和/dev/ttyACM*的所有节点,找到可读的哪一个再使用。还要监听UsbManager.ACTION_USB_DEVICE_ATTACHED和ACTION_USB_DEVICE_DETACHED广播,在设备插拔时重做节点扫描,而不是只扫描一次。

5.2 避坑:NMEA 数据丢帧与读写缓冲不足

现象:定位间隔出现不连续,比如上一秒有数据,下一秒没数据;或者速度值出现短暂的 0 和实际值跳变。

原因:读取线程的read()被中断后没有等待就把线程关闭了,或者 USB 转串口芯片的接收缓冲区溢出。9600 波特率下数据量不大,但应用层处理慢时会积压。

解决:把读取缓冲从 1024 提到 4096,解析不要在主线程做;线程关闭时先置停止标志位,再inputStream.close(),让阻塞的read()抛异常退出,而不是直接thread.interrupt()。

5.3 避坑:天线馈电不足导致搜星慢或掉星

现象:模块在室外空旷处依然要很久才能定位,甚至定位成功后频繁漂移;GSV 里卫星信噪比普遍低于 30。

原因:UM220-III 需要外接有源天线才能获得稳定信号。有源天线需要供电,如果供电电压不足或电流不够,天线内部放大器不工作,信号衰减严重。

解决:确认天线供电电压在模块规格允许范围内,查看模块附近是否有天线馈电引脚需要额外接 3.3V 或 5V 电源。还有一个容易被忽略的坑:天线接头接触不良,特别是使用转接线时,要用万用表量一下天线端供电是否到位。

5.4 避坑:时间字段用错导致数据错位

现象:显示的时间比本地时间慢 8 小时,或者日期不对导致消息推送时间错乱。

原因:NMEA 里的时间是 UTC 时间,且 RMC 的日期字段是DDMMYY格式,解析时直接拼接容易出错。

解决:用一个统一的时间转换入口,在解析 RMC 时先拼出完整 UTC 时间字符串,再用Calendar.setTimeZone设置为 UTC 时区,最后转换成本地时区。不要在转换进程中重复加减 8 小时。

5.5 避坑:把网络定位和 GNSS 定位混用

现象:室内环境下定位坐标漂移严重,或者定位点突然跳到几公里外,业务方怀疑模块有问题。

原因:Android 系统的LocationManager会融合网络定位和 GNSS 定位。如果代码里监听的是系统的LocationListener,出来的坐标可能来源于 WiFi 或基站定位,而不是模块输出。

解决:直接从串口读取模块的 NMEA 数据自行解析,完全不经过LocationManager。只有把模块数据作为唯一定位源,才能保证坐标的准确性和可控性。项目需求如果要求快速出首次定位,可以做 AGNSS,但需要单独的辅助数据通道,不能直接用网络定位结果混合。

6. 再往前走一步:让定位从“能出数”到“用得稳”

6.1 冷启动与热启动:复位命令和搜星时间控制

UM220-III 支持通过串口指令控制系统复位和搜星策略。冷启动指令格式为$PCAS10,3后跟校验和。冷启动会清空星历、历书和时间,搜星时间会明显变长,适合模块首次上电或长时间存放后使用。热启动指令为$PCAS10,1,保留星历,搜星速度会快很多。

在 Android 集成里,这些指令需要通过串口输出流发送,本质就是往FileOutputStream写一行字符串再刷新。发送后建议至少等 500ms 再开始读解析,因为模块复位期间会有一段静默期。

6.2 谨慎对待“纯北斗”或“纯 GPS”模式

有些项目会希望禁用北斗,只保留 GPS,或者反过来。UM220-III 支持配置星座系统,但要注意两点:一是配置指令对版本有要求,部分固件可能不支持;二是盲目配置可能会让模块进入异常状态,需要恢复出厂设置指令。

我个人的习惯是:默认保持双模,不做星座裁剪。只有在外部明确要求“仅使用北斗”作为合规性考核时,才通过指令切换,并且在代码里保留一个恢复双模的入口,方便现场回滚。

6.3 验证定位精度的一种朴素方法:静态收敛对比

拿到一套能出坐标的 Android 终端后,怎么验证定位稳不稳?最朴素但也最有效的方法是静态测试。把终端固定在空旷位置,连续记录 10 分钟以上的经纬度数据,计算坐标点的离散程度。合格的标准是:95% 的定位点落在半径 3 米以内;如果标准差超过 5 米,优先排查天线位置和周围是否有强反射面。

输出日志时建议加上时间戳、quality 字段和参与定位的卫星数。这些辅助字段能帮你快速定位问题:如果 quality 为 1 但卫星数只有 4 颗,精度受限是正常的;如果卫星数有 12 颗但坐标漂移大,可能是多路径效应。用这种自我验证方式验证过的终端,再去应对现场验收会少很多被动。

做这个支持包集成项目时我自己踩过一次印象深刻:模块在桌面上放着,天线挨着电脑机箱,测试时定位点飘得离谱,一直怀疑是解析代码的问题,最后把天线挪到窗外开阔位置,数据立刻稳定下来。从那以后,我所有定位相关的调试都先确认天线环境,再检查代码。希望这篇笔记里的链路分析和参数选择能帮你少走这些弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询