简介:和芯星通UM220-III Android支持包是一套面向Android开发者的定位芯片集成方案,旨在解决UM220-III北斗/GPS芯片在Android系统上的适配与调用问题,覆盖framework定制组件、hal硬件抽象层代码、测试apk、协议PDF文档及移植协助文档,链路完整,适合从事导航、追踪及物联网定位开发的工程师使用。包内共29个文件,以10个java框架代码、hal层c/cpp/h实现、aidl接口及xml配置为主,另含2份PDF协议文档与1个apk测试程序,整体仅2.55MB,目录结构清晰,便于按模块查阅。其中协议文档详述芯片工作模式、数据格式及通信协议,移植协助文档提供步骤指南、注意事项与常见问题解答。该包尤其适合在Android源码中集成或调试UM220-III的开发者,已有851人学习。借助支持包可快速理解framework修改前与修改后的差异、hal接口设计思路及协议规范,并通过测试apk快速验证定位性能与精度,避免重复踩坑,缩短开发周期。
1. 把 UM220-III 北斗 GPS 模块接进 Android 系统:这套支持包先解决哪三件事
UM220-III 是原厂和芯星通推出的北斗 B1 / GPS L1 双模定位模块,输出标准 NMEA 0183 文本,硬件上只要接一颗串口和一根天线就有数据。但把它放进 Android 设备问题就来了:从 /dev/ttyS1 读到的一堆 $GNGGA 字符串,到系统 LocationManager 能直接使用的经纬度,中间隔着串口权限、NMEA 解析、GNSS HAL 三层工序。这套 Android 支持包解决的正是这三层:HAL 接入源码、串口配置脚本和应用层验证 Demo。适合手里有 Android 主板、配了 UM220-III、却一直卡在「系统拿不到定位」上的嵌入式与车载工程师。
2. 拆包识货:支持包目录、双模 NMEA 协议和三条接入路径的选型对比
拿到支持包这类压缩包,最容易踩的坑是「文档写的是上一个平台、代码里的设备节点是别人的」。别急着 make,先花十分钟把目录结构和协议文档过一遍,能省掉后面一整天排错。下面按我拆包时的顺序讲。
2.1 解包后先看这五个文件,而不是先读手册
解压之后目录一般分 Docs、hal_src、AppDemo、Lib、Scripts 几块,具体文件以你下载到的压缩包为准。我一般不会先打开那份几十页的 PDF,而是先找五个关键文件,它们的现状直接决定这个包能不能在当前板子上用:
- Android.mk / Android.bp:看 HAL 库编译目标。
LOCAL_MODULE_PATH指向vendor还是system,决定你是否需要 root 后重打包 system 分区。 - hal 源码里的串口打开部分:全局搜
open(,看它写死的节点名是ttyS0、ttyMT1还是别的,和你的原理图是否对得上。 - init / ueventd 脚本:看节点权限配置。没有这一步,App 或 HAL 打开串口必然
Permission denied。 - AppDemo 的 AndroidManifest.xml:看它申请了哪些权限,是否依赖系统签名,是否带
android:sharedUserId这类敏感属性。 - NMEA 协议文档:重点翻默认输出语句和私有配置指令,这决定你需不需要先给模块发一串配置命令。
这套包的目录结构立得住,是因为它把「系统级 HAL」和「验证用 App」分开打包了。你量产时合入 HAL,调试时用 App 直读串口对比,两边互不干扰。如果解压后只有一堆 .so 没有源码,就只能当黑盒用,排错空间小很多,这也是我优先选带 hal_src 的版本的原因。
2.2 NMEA 双模细节:为什么模块吐的是 GNGGA 而不是 GPGGA
UM220-III 在双模状态下通常输出 GN 前缀的语句,比如$GNGGA、$GNRMC。很多老解析器只认 GP 前缀,把 GNGGA 当垃圾丢弃,这是最常见的「有数据但解不出位置」的原因。GN 前缀的含义是北斗和 GPS 的联合解算结果,GNGGA 字段结构如下:
| 字段 | 示例 | 含义 |
|---|---|---|
| 0 | $GNGGA | 语句标识 |
| 1 | 023543.00 | UTC 时间 HHMMSS.xx |
| 2 | 3112.34562 | 纬度,ddmm.mmmmm 格式 |
| 3 | N | 北纬 / 南纬 |
| 4 | 12130.67890 | 经度,dddmm.mmmmm 格式 |
| 5 | E | 东经 / 西经 |
| 6 | 1 | 定位质量:0 无效、1 单点、2 差分、4 RTK 固定、5 RTK 浮点 |
| 7 | 12 | 参与定位的卫星数 |
| 8 | 0.8 | HDOP 水平精度因子 |
| 9 | 9.2 | 海拔,单位米 |
| 15 | *68 | 校验和 |
纬度3112.34562不是十进制度,是 31 度 12.34562 分,转十进制度要算31 + 12.34562 / 60。校验和是$和*之间所有字符的异或,结果以十六进制写在*后面。这两条规则是解析器的地基,支持包里的 nmea_parser.c 也遵循同一套逻辑。
2.3 三条接入路径怎么选
同样一颗模块,接入 Android 有三条路,成本差很多:
| 路径 | 侵入性 | 适用系统 | 开发量 | 定位数据去向 |
|---|---|---|---|---|
| 应用层 serialport | 低,APK 自带 JNI | Android 4.4 以上均可 | 小 | 只在本 App 内,后台被杀就断 |
| 传统 HAL(gps.h) | 系统级,需写入 system 分区 | Android 4.4 ~ 7.x | 中 | 全系统 LocationManager |
| HIDL HAL | 系统级,vendor 分区 | Android 8.0 以上 | 中高 | 全系统 LocationManager |
我的建议是:工程验证用应用层,量产必须走 HAL/HIDL。原因是应用层读串口存在三个硬伤:App 退到后台容易被系统回收;两个 App 同时抢一个串口设备互相干扰;定位数据不进系统框架,地图类 App 拿不到位置。HAL 路径把 GNSS 数据注入 framework,对所有上层应用透明,这才是支持包真正的价值所在。下面两章就按这条链路往下拆。
3. 串口接入与 NMEA 分包:设备节点、波特率、校验和一个都不能少
3.1 设备节点与 ueventd 权限
先对着原理图确认 UM220-III 接在主控哪一路 UART 上。主控平台不同节点名差异很大:瑞芯微平台一般是ttyS0~ttyS4,联发科平台常见ttyMT0~ttyMT3,全志平台常映射为ttyS1。节点名错了,后面所有调试都是白费。
Android 的 /dev 节点权限由 ueventd 和 init 共同管理,直接 open 通常报Permission denied。常见做法是在 ueventd.rc 里追加一条设备节点规则:
/dev/ttyS1 0666 system system这条规则的逻辑是:当内核上报ttyS1设备时,ueventd 把它归到 system 用户组并放开读写权限。注意 ueventd 在设备热插拔时会重置权限,所以写在 init.rc 里的chmod 666不一定持久,写 ueventd.rc 更稳。
如果你用的是 Android 8.0 以上版本,还要留意 SELinux。我一般先在 adb 里执行一次adb shell setenforce 0,如果串口马上能打开,说明是策略问题,需要给 GNSS 服务进程补tty_device的读写权限,而不是怀疑硬件。
3.2 波特率与串口参数:先试 9600 还是 115200
UM220-III 出厂波特率以模块标注或配套文档为准,常见是 9600 和 115200 两档。拿到模块第一件事,是用 USB-TTL 转接板接在 PC 上看原始输出,确认波特率再合入系统代码,不要凭感觉猜。
termios 配置里有一组参数是必设的:
int fd = open("/dev/ttyS1", O_RDWR | O_NOCTTY | O_NONBLOCK); struct termios cfg; tcgetattr(fd, &cfg); cfsetispeed(&cfg, B115200); cfsetospeed(&cfg, B115200); cfmakeraw(&cfg); cfg.c_cflag |= CLOCAL | CREAD; cfg.c_cflag &= ~(CRTSCTS); // 关闭硬件流控 cfg.c_cc[VMIN] = 1; cfg.c_cc[VTIME] = 0; tcsetattr(fd, TCSANOW, &cfg);cfmakeraw是关键,它把串口切成原始模式,防止内核把\r\n转成\n、把0x11/0x13当流控吃掉。CLOCAL避免因 modem 信号变化挂起进程,CREAD允许接收数据。接线注意三点:主控 TXD 接模块 RXD、主控 RXD 接模块 TXD、GND 必须共地。串口电平上,如果主控是 1.8V IO 而 UM220-III 是 3.3V TTL,需要加电平转换,否则会出现有数据但全是乱码的现象。
3.3 读线程怎么写:按 $ 重同步、按 \n 分帧
串口 read 返回多少字节和 NMEA 语句边界没有对应关系,可能一次读到半条语句,也可能一次读三条。正确做法是做一个按字符推进的状态机,用$作为语句起点、\n作为结束标志。核心代码如下:
static void nmea_reader_on_data(const char *buf, int len) { static char line[128]; // NMEA 最长约 82 字节,留余量 static int line_len = 0; for (int i = 0; i < len; i++) { char ch = buf[i]; if (ch == '$') { // 新语句起点,直接重同步,丢弃之前所有残留 line[0] = '$'; line_len = 1; } else if (ch == '\n') { // 行尾,去掉可能的 \r 后交给解析器 while (line_len > 0 && line[line_len - 1] == '\r') line_len--; if (line_len >= 8 && line[0] == '$') { process_nmea_line(line, line_len); } line_len = 0; } else if (line_len < (int)sizeof(line) - 1) { line[line_len++] = ch; } // 超过 line 容量且未遇到 \n 的异常数据,放弃等下一个 $ } }这个状态机的设计逻辑是:$是绝对同步点,只要读到$就清掉前面所有残留,即使之前丢了一半包也能马上恢复。\n是语句结束标志,\r是 NMEA 标准行尾的一部分,要去掉才能做字符串处理。缓冲区 128 字节是安全值,普通 GGA 语句不超过 82 字节,给长语句留了余地。
拿到一行之后先做校验和,宁可丢弃不可错用:
static int nmea_checksum_ok(const char *line, int len) { const char *star = memchr(line, '*', len); if (!star || star + 2 >= line + len) return 0; unsigned char cs = 0; for (const char *p = line + 1; p < star; p++) { cs ^= (unsigned char)*p; } char hex[3] = { star[1], star[2], 0 }; return (unsigned)strtol(hex, NULL, 16) == cs; }校验和失败最常见的原因是波特率配错导致字节错位,其次是模块输出语句本身不带校验和——但这颗模块的标准 NMEA 输出是带的。我在调试时会把校验失败的原始行打出来看一眼内容,能快速区分硬件干扰和配置错误。
3.4 GNGGA 解析与度分转换
校验通过后,从 GGA 语句里取经纬度、fix 状态、卫星数和 HDOP。纬度转十进制度的实现:
static double nmea_to_dd(const char *dm) { // 输入 "3112.34562" -> 31 + 12.34562 / 60 if (dm[0] == '\0') return 0.0; double val = atof(dm); int deg = (int)(val / 100.0); double minute = val - deg * 100.0; return deg + minute / 60.0; }完整解析流程是:按逗号拆分 GGA 语句 → 取第 6 个字段的 fix 值 → fix 大于 0 才更新位置 → 第 2 字段转纬度、第 4 字段转经度 → 带上 N/S/E/W 符号做正负判断。HDOP 转 accuracy 没有统一标准,常见做法是乘 2 作为水平误差半径,因为这颗模块单点定位时 HDOP 通常在 0.5~1.5 之间,乘 2 给出的精度值更接近真实 CEP。真要精确标定,需要跑一段静态数据和真值比对,工程验证阶段用经验系数足够了。
4. 从 NMEA 到 LocationManager:GNSS HAL 注入与 HIDL 迁移要点
4.1 传统 gps.h HAL 的最少实现骨架
Android 6/7 时代,GNSS 没有 HIDL,全部走libhardware的 gps.h 接口。支持包里的 hal_src 核心就是三件事:打开串口、起读线程、用回调上报位置。核心骨架如下:
static GpsCallbacks s_cbs; static void report_position(double lat, double lon, double acc) { if (!s_cbs.location_cb) return; GpsLocation loc; memset(&loc, 0, sizeof(loc)); loc.size = sizeof(GpsLocation); // 必须置位,framework 按 flags 判断哪些字段有效 loc.flags = GPS_LOCATION_HAS_LAT_LONG | GPS_LOCATION_HAS_ACCURACY; loc.latitude = lat; loc.longitude = lon; loc.accuracy = acc; // 单位是纳秒,取系统 elapsedRealtimeNanos 即可 loc.timestamp = elapsedRealtimeNanos(); s_cbs.location_cb(&loc); } static int gps_start(void) { pthread_create(&s_reader_thread, NULL, nmea_reader_thread, NULL); return 0; } static const GpsInterface s_gps_if = { .size = sizeof(s_gps_if), .init = gps_init, .start = gps_start, .stop = gps_stop, .cleanup = gps_cleanup, }; const GpsInterface *gps_get_hardware_interface(void) { return &s_gps_if; } static struct hw_module_methods_t gps_module_methods = { .open = gps_device_open, }; struct hw_module_t HAL_MODULE_INFO_SYM = { .tag = HARDWARE_MODULE_TAG, .id = GPS_HARDWARE_MODULE_ID, .methods = &gps_module_methods, };GpsInterface里的size字段必须正确赋值,framework 会按 size 判断接口版本,填错直接导致加载失败。flags是另一个容易被忽略的点:只填经纬度不填 flags,上层以为数据无效,回调同样白传。读线程在gps_start里创建、gps_stop里退出,注意串口 read 要设置超时,否则 stop 时线程卡在 read 上退不出来——这也是常见崩溃点。
这套 HAL 编译产物是gps.um220.so,放到/vendor/lib/hw/或/system/lib/hw/,系统通过GPS_HARDWARE_MODULE_ID找到它。优点是实现简单、依赖少;缺点是它只活在 Android 8.0 之前的框架里。
4.2 Android 8.0 以上的 HIDL 迁移
Android 8.0 之后 GNSS 从 gps.h 迁到 HIDL,接口变为android.hardware.gnss@1.0/1.1的IGnss。HAL 层实现的变化是:不再暴露gps_get_hardware_interface,而是实现IGnss接口,通过setCallback注册回调。UM220 这类模块本身只有串口 NMEA 数据,HIDL 层的核心工作没变:读串口、解析、通过IGnssCallback的gnssLocationCb上报。
HIDL 版本需要编译注册为系统服务,Android.bp 大致如下:
cc_library_shared { name: "android.hardware.gnss@1.1-service.um220", vendor: true, srcs: ["GnssHAL.cpp"], shared_libs: [ "android.hardware.gnss@1.0", "android.hardware.gnss@1.1", "libhidlbase", "libhidltransport", "liblog", "libutils", ], init_rc: ["android.hardware.gnss@1.1-service.um220.rc"], }配套的 rc 文件注册服务:
service vendor.gnss-um220 /vendor/bin/hw/android.hardware.gnss@1.1-service.um220 class hal user system group system shell seclabel u:r:hal_gnss:s0 oneshotseclabel 里的策略标签要和你平台的 SELinux 配置一致,不同平台差异较大。样板环境下可以先setenforce 0验证功能,再回头补策略。另外,设备 manifest 里必须声明这个 HIDL 实例,否则 framework 扫描不到服务:
<manifest version="1.0" type="device"> <hal format="hidl"> <name>android.hardware.gnss</name> <version>1.1</version> <interface> <name>IGnss</name> <instance>default</instance> </interface> </hal> </manifest>Android 12 之后官方又开始推 AIDL 版 GNSS 接口,原厂支持包如果只给 HIDL,在高版本系统上要么做一层 AIDL 兼容,要么使用兼容层让旧 HIDL 服务继续跑。选平台时提前确认目标系统版本,能省掉一次大改。
4.3 应用层兜底验证:serialport 直读串口
HAL 编译一次要几分钟,排错效率太低。我习惯先在 PC 上用 Python 验证模块本身没问题,再进系统。pyserial 一把梭:
import serial import time def dm_to_dd(part: str) -> float: if len(part) < 6: return 0.0 val = float(part) deg = int(val // 100) minute = val - deg * 100 return deg + minute / 60.0 ser = serial.Serial('/dev/ttyUSB0', 115200, timeout=2) for _ in range(30): line = ser.readline().decode('ascii', errors='ignore').strip() if not line.startswith('$GNGGA'): continue star = line.find('*') cs = 0 for ch in line[1:star]: cs ^= ord(ch) if cs != int(line[star + 1:star + 3], 16): print('checksum fail:', line) continue f = line.split(',') fix = int(f[6]) if fix > 0: lat = dm_to_dd(f[2]) if f[3] == 'N' else -dm_to_dd(f[2]) lon = dm_to_dd(f[4]) if f[5] == 'E' else -dm_to_dd(f[4]) print(f'fix={fix} sats={f[7]} lat={lat:.6f} lon={lon:.6f} hdop={f[8]}') time.sleep(0.1)这段脚本的价值在于把「模块有没有输出」和「解析对不对」分开验证。如果 USB-TTL 下都跑不出 fix,问题在硬件或天线,不需要碰系统。如果 PC 上正常而系统里不行,问题锁定在 HAL 或权限层。Python 脚本里errors='ignore'是有意为之,波特率配错时避免解码异常直接中断循环,让脚本能持续观察。
5. 避坑手册:UM220-III 在 Android 上定位失败的五个常见现场
这套流程做下来,我见过最多的五类问题,每条按「现象 → 原因 → 解决」写。
5.1 打开串口一直 Permission denied
现象:HAL 和 App 都报open /dev/ttyS1: Permission denied,但 shell 下用 adb 却能读到数据。
原因:ueventd.rc 里没有对应节点规则,或者 SELinux 拦截了服务进程的串口访问。应用层和 HAL 进程的用户组不同,系统用户能读不代表应用能读。
解决:先在 ueventd.rc 加/dev/ttyS1 0666 system system并重打包 boot 分区;如果加了还报错,执行setenforce 0复测,能通就补 SELinux 策略,给 GNSS 服务进程加tty_device的读和写权限。有些平台还要确认服务进程在正确 cgroup 里,否则节点规则配了也进不去。
5.2 明明有数据,解析器却一直解不出位置
现象:cat /dev/ttyS1能看到$GNGGA和$GNRMC每秒都在滚,但 App 的定位回调始终为空。
原因:解析器只认$GPGGA/$GPRMC,GN 前缀的语句全被丢弃。这是从 GPS 单模代码移植到双模模块时最典型的翻车点。
解决:把解析器里的字符串匹配从GPGGA改成GNGGA,同时兼容两类前缀,即line[3] == 'G' && line[4] == 'N'或line[3] == 'G' && line[4] == 'P'。如果模块可以配置成单 GPS 模式,也会输出 GP 前缀,所以兼容判断最稳。
5.3 HAL 加载成功但 LocationManager 一直没回调
现象:串口线程有数据、解析也过,但系统定位服务里看不到任何位置上报。
原因:HIDL 场景下,服务没有注册成 default 实例,framework 扫描不到。传统 HAL 场景里,GpsCallbacks注册时机不对,init里的回调注册晚于 framework 调用。
解决:先adb shell lshal | grep gnss看 HIDL 服务是否在线,不在线检查 manifest 声明和 rc 文件;在线但无回调,则在 HAL 里加日志确认setCallback是否被调用。我踩过的版本里有个细节:HIDL 1.0 的start()调用时机和旧 HAL 不同,必须在回调注册之后才start,否则上报事件无人接收。
5.4 卫星数能到 10 颗,FIX 却一直为 0
现象:GNGGA 里卫星数 10 颗以上,但 fix 字段恒为 0,永远不出经纬度。
原因:天线没接或接了但馈电没打开。GSV 语句显示的是可见卫星,不等于能参与定位解算。室内窗边测试时模块能收到几颗星但信噪比不够,同样出现「有星无 fix」。
解决:确认天线是主动有源还是无源。有源天线要检查模块的馈电配置,UM220-III 这类模块一般由外围电路提供 3.3V 或 5V 馈电,量一下天线座中心针电压就知道。然后去室外开阔地把天线平放朝天,冷启动等 30~60 秒再看 fix。我把这颗模块的冷启动时间写进自检清单,避免产生「上电秒定」的错误预期。
5.5 模块偶发重启或无输出:电平、电源和复位时序
现象:串口数据时有时无,或者模块工作一段时间后无输出,重启后恢复。
原因:常见是 3.3V TTL 直接接了 1.8V IO 的主控,或者模块供电纹波太大。模块瞬态电流变化时电源跌落,导致内部复位。
解决:先排除电平问题——主控串口 IO 电压必须和模块匹配,1.8V 平台要加电平转换芯片。电源上在模块 VCC 引脚就近放一个 100uF 电解电容再加 0.1uF 陶瓷电容,供电走线避开大电流回路。我用示波器看过一个案例,log 里没有报错,但电源轨在定位瞬间跌了 400mV,加电容后问题消失。这个坑在量产板比开发板上更常见,因为开发板电源余量大。
6. 四步自检:从原始串口 NMEA 流到系统定位的完整闭环
定位链路的每一层都可能单独出问题,最怕的是把整条链路当黑匣子试错。我每次拿到新板子都强制走四步自检,每步确认一个明确的中间产物。
第一步,验证硬件和原始流。先不看系统,用 USB-TTL 转接板在 PC 上收模块输出,确认每秒有 GNGGA。在 Android 上等价操作是:
adb shell "su 0 chmod 666 /dev/ttyS1 && cat /dev/ttyS1 | head -n 20"如果这里看不到数据,问题在接线、供电或波特率,后面几步都不用开始。
第二步,验证解析。用 4.3 节的 Python 脚本在 PC 上跑一遍 GNGGA 的度分转换和校验和,确认能输出 fix 和非零经纬度。跑不通的话,是解析逻辑的问题,不是系统的问题。
第三步,验证 HAL 注入。合入支持包的 HAL 或 HIDL 服务后,用adb logcat -s GnssHAL看有没有 location 上报日志,再用adb shell dumpsys location确认系统定位服务里有最新的经纬度和精度。
第四步,验证应用层。打开任意地图类 App,或者用一个带定位权限的测试 App,看状态栏定位图标和坐标是否持续更新。
这套自检流程的价值在于每一层都有独立的失败信号:第一步失败看硬件,第二步失败看解析,第三步失败看 HAL 注册,第四步失败看权限。我吃过亏的教训是直接跳到第四步,App 显示无法获取定位,结果查了半天发现是 USB-TTL 转接板的 TX 和 RX 插反了,模块数据压根没进主板。从那以后我每次做 GNSS 适配都强制从原始 NMEA 流开始走一遍,五分钟内就能把问题定到具体环节。希望帮到你。
本文还有配套的精品资源,点击获取