
简介和芯星通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 而不是 GPGGAUM220-III 在双模状态下通常输出 GN 前缀的语句比如$GNGGA、$GNRMC。很多老解析器只认 GP 前缀把 GNGGA 当垃圾丢弃这是最常见的「有数据但解不出位置」的原因。GN 前缀的含义是北斗和 GPS 的联合解算结果GNGGA 字段结构如下字段示例含义0$GNGGA语句标识1023543.00UTC 时间 HHMMSS.xx23112.34562纬度ddmm.mmmmm 格式3N北纬 / 南纬412130.67890经度dddmm.mmmmm 格式5E东经 / 西经61定位质量0 无效、1 单点、2 差分、4 RTK 固定、5 RTK 浮点712参与定位的卫星数80.8HDOP 水平精度因子99.2海拔单位米15*68校验和纬度3112.34562不是十进制度是 31 度 12.34562 分转十进制度要算31 12.34562 / 60。校验和是$和*之间所有字符的异或结果以十六进制写在*后面。这两条规则是解析器的地基支持包里的 nmea_parser.c 也遵循同一套逻辑。2.3 三条接入路径怎么选同样一颗模块接入 Android 有三条路成本差很多路径侵入性适用系统开发量定位数据去向应用层 serialport低APK 自带 JNIAndroid 4.4 以上均可小只在本 App 内后台被杀就断传统 HALgps.h系统级需写入 system 分区Android 4.4 ~ 7.x中全系统 LocationManagerHIDL 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 还是 115200UM220-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 到 LocationManagerGNSS 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.gnss1.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.gnss1.1-service.um220, vendor: true, srcs: [GnssHAL.cpp], shared_libs: [ android.hardware.gnss1.0, android.hardware.gnss1.1, libhidlbase, libhidltransport, liblog, libutils, ], init_rc: [android.hardware.gnss1.1-service.um220.rc], }配套的 rc 文件注册服务service vendor.gnss-um220 /vendor/bin/hw/android.hardware.gnss1.1-service.um220 class hal user system group system shell seclabel u:r:hal_gnss:s0 oneshotseclabel 里的策略标签要和你平台的 SELinux 配置一致不同平台差异较大。样板环境下可以先setenforce 0验证功能再回头补策略。另外设备 manifest 里必须声明这个 HIDL 实例否则 framework 扫描不到服务manifest version1.0 typedevice hal formathidl nameandroid.hardware.gnss/name version1.1/version interface nameIGnss/name instancedefault/instance /interface /hal /manifestAndroid 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, timeout2) for _ in range(30): line ser.readline().decode(ascii, errorsignore).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(ffix{fix} sats{f[7]} lat{lat:.6f} lon{lon:.6f} hdop{f[8]}) time.sleep(0.1)这段脚本的价值在于把「模块有没有输出」和「解析对不对」分开验证。如果 USB-TTL 下都跑不出 fix问题在硬件或天线不需要碰系统。如果 PC 上正常而系统里不行问题锁定在 HAL 或权限层。Python 脚本里errorsignore是有意为之波特率配错时避免解码异常直接中断循环让脚本能持续观察。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/$GPRMCGN 前缀的语句全被丢弃。这是从 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 流开始走一遍五分钟内就能把问题定到具体环节。希望帮到你。本文还有配套的精品资源点击获取