资讯详情

Android GPS 驱动开发实战:从 HAL 到 JNI 的定位链路解析

📅 2026/11/2 4:34:38 | 华诺云谱 👁 阅读
Android GPS 驱动开发实战:从 HAL 到 JNI 的定位链路解析
简介面向Android系统开发与驱动调试人员这份资料围绕GPS底层驱动展开聚焦JNI与HAL层的协作机制帮助读者理清Java层与本地代码之间的调用链路。包内共5个文件以docx分析文档为主辅以rar源码包、mk编译脚本与c源文件压缩包约9.28MB便于对照阅读与本地编译验证。内容涉及JNI函数定义与实现、HAL接口如何初始化GPS硬件并上报定位数据、GpsInterface在LocationManagerService中的使用以及GpsLocationProvider生命周期、GpsStatus与导航消息回调等关键环节并附有车载导航GPS HAL移植分析材料。已有841人学习适合希望深入理解Android定位服务底层实现、排查驱动通信问题或迁移GPS HAL的开发者参考。1. 从一份 GPS 驱动资料说起Android 定位链路到底卡在哪很多人以为 Android 上拿个经纬度就是调个LocationManager的事真到项目里把设备塞进金属外壳、冷启动第一次定位等了四十多秒才发现问题根本不在应用层。这份 GPS Android 底层驱动资料价值就在于它把从应用往下到内核驱动的那条链路摊开了应用层 API、Framework 的 LocationManagerService、JNI 桥接、HAL 接口、串口或 I2C 上的 NMEA 数据流一层一层对上。它适合两类人一是做车载、手持终端、工业平板这类定制设备的系统开发需要把 GPS 模块真正跑通二是想搞明白 JNI 与 HAL 层怎么衔接、驱动怎么把原始卫星数据喂给上层的开发者。资料里附了关键源码不是纯理论文档能对着改。2. 技术栈与目录结构先看清这份资料由哪几块拼起来拿到一份底层驱动资料最怕上来就翻代码翻到一半不知道自己在哪一层。我一般先看目录把「应用—框架—JNI—HAL—驱动」这条纵向链路和「配置—编译—调试」这条横向支撑分开再决定从哪切入。这份资料的结构基本就是按这个逻辑组织的。2.1 纵向分层五层结构各自负责什么Android 的定位链路是严格分层的每一层职责清晰出问题时也能快速定位到层。资料里的源码目录大致对应下面这张表层级典型目录/文件职责常见语言应用层app/调用 LocationManager、处理回调Java/Kotlin框架层frameworks/base/services/core/java/.../location/服务管理、权限、Provider 调度JavaJNI 层frameworks/base/services/core/jni/Java 与 Native 的对象转换、回调注册CHAL 层hardware/libhardware/include/hardware/gps.h定义标准接口厂商实现C/C驱动层kernel/drivers/、串口节点读串口/I2C 原始数据、上电时序C这张表不是让你背而是让你在翻代码时随时知道「我现在在第几层」。比如看到gps.h里的gps_device_t结构体就知道这是 HAL 契约具体实现由厂商的xxx_gps.c提供看到JNIEnv*和jclass就知道在 JNI 层做 Java 对象和 C 结构体的映射。2.2 横向支撑配置、编译与调试脚本纵向链路之外资料里还有几类支撑文件新手最容易忽略但恰恰是跑通的关键Board 配置设备树或板级配置文件里 GPS 的串口编号、波特率、GPIO 上电引脚、复位引脚定义。串口号写错驱动读到的永远是空。权限与 SELinux 策略ueventd.rc里给串口节点赋权限sepolicy里允许 HAL 访问该设备节点。这一块不配HAL 打开设备直接返回Permission denied。编译入口Android.bp或Android.mk决定 HAL 库编成xxx.gps.default.so还是别的名字名字对不上框架就加载不到。调试脚本抓 NMEA 原始流的cat /dev/ttySx、看 HAL 日志的logcat -s GpsHal、查服务状态的dumpsys location。我一般会先把这几类文件过一遍确认串口、权限、库名三件事再动手编译。这三件事任意一个错后面所有调试都是白费功夫。2.3 源码阅读顺序从契约往实现读资料里源码不少全读一遍不现实。合理的顺序是「先读契约再读实现最后读调用方」先读gps.h搞清楚 HAL 要求实现哪些回调init、start、stop、set_position_mode、delete_aiding_data等以及GpsCallbacks里上层会回调哪些函数location_cb、status_cb、nmea_cb、sv_status_cb。再读厂商 HAL 实现看它怎么把这些回调填进去、怎么起线程读串口、怎么解析 NMEA 后调location_cb。最后读 JNI 层看GpsCallbacks怎么被包装成 Java 的IGpsCallback以及native_init怎么把gps_device_t拿到手。这个顺序的好处是你始终知道「当前这段代码在满足谁的约定」不会读着读着迷失。熟手可以直接跳到 HAL 实现新手建议老老实实按这个顺序走一遍。3. 核心模块拆解JNI 与 HAL 层怎么把卫星数据送上去这一章是整份资料的重心。GPS 驱动跑不通八成问题出在 JNI 与 HAL 的衔接上要么 HAL 没被加载要么回调没注册上要么数据格式对不上。下面按「HAL 接口定义 → 厂商实现 → JNI 桥接 → 数据上报」的顺序拆。3.1 HAL 接口gps.h 里的契约长什么样Android 的 GPS HAL 是一套 C 结构体契约核心是gps_device_t和GpsInterface。资料里的gps.h关键部分大致如下节选保留结构// hardware/libhardware/include/hardware/gps.h typedef struct { size_t size; int (*init)(const GpsCallbacks* callbacks); // 初始化注册回调 int (*start)(void); // 启动定位 int (*stop)(void); // 停止定位 void (*cleanup)(void); // 释放资源 int (*inject_time)(GpsUtcTime time, int64_t timeReference, int uncertainty); int (*set_position_mode)(GpsPositionMode mode, GpsPositionRecurrence recurrence, uint32_t min_interval, uint32_t preferred_accuracy, uint32_t preferred_time); int (*delete_aiding_data)(GpsAidingData flags); } GpsInterface; typedef struct { size_t size; void (*location_cb)(GpsLocation* location); // 位置回调 void (*status_cb)(GpsStatus* status); // 状态回调 void (*sv_status_cb)(GpsSvStatus* sv_info); // 卫星状态回调 void (*nmea_cb)(GpsUtcTime timestamp, const char* nmea, int length); // 原始 NMEA void (*set_capabilities_cb)(uint32_t capabilities); void (*acquire_wakelock_cb)(void); void (*release_wakelock_cb)(void); pthread_t (*create_thread_cb)(const char* name, void (*start)(void*), void* arg); } GpsCallbacks;这段代码要抓住两个点。第一GpsInterface是「上层调你」的入口init必须把GpsCallbacks存下来后面解析出位置就靠它回调。第二GpsCallbacks是「你调上层」的出口location_cb是最终把经纬度送上去的函数nmea_cb则把原始语句透传给上层做调试或辅助定位。参数上要特别注意size字段每个结构体第一个成员都是size上层用它做版本兼容判断。厂商实现里如果忘了给size赋值或者赋了个旧版本的值框架可能直接拒绝加载。这是很隐蔽的一个坑后面避坑章节还会提。3.2 厂商 HAL 实现起线程读串口、解析 NMEA、回调上报HAL 实现的核心就三件事打开设备节点、起一个读线程、解析数据后回调。资料里的实现骨架大致是这样// vendor/xxx/gps/xxx_gps.c 结构示意 static int xxx_gps_init(const GpsCallbacks* callbacks) { g_callbacks *callbacks; // 保存上层回调 g_callbacks.size sizeof(GpsCallbacks); // 版本兼容别漏 return 0; } static void* gps_read_thread(void* arg) { char buf[512]; while (g_running) { int n read(g_fd, buf, sizeof(buf) - 1); // 阻塞读串口 if (n 0) { usleep(10000); continue; } buf[n] \0; parse_nmea(buf); // 解析 GGA/RMC 等语句 } return NULL; } static int xxx_gps_start(void) { g_fd open(/dev/ttyS1, O_RDWR | O_NOCTTY); // 串口号按板级配置改 if (g_fd 0) return -1; set_serial(g_fd, 9600); // 波特率按模块手册改 g_running 1; pthread_create(g_tid, NULL, gps_read_thread, NULL); return 0; } static void parse_nmea(const char* line) { if (strncmp(line, $GPGGA, 6) 0) { GpsLocation loc {0}; loc.size sizeof(GpsLocation); // 从 GGA 语句里取纬度、经度、海拔、定位质量 if (nmea_get_gga(line, loc.latitude, loc.longitude, loc.altitude)) { loc.flags GPS_LOCATION_HAS_LAT_LONG | GPS_LOCATION_HAS_ALTITUDE; g_callbacks.location_cb(loc); // 上报给上层 } } g_callbacks.nmea_cb(systemTime(), line, strlen(line)); // 原始语句透传 }逐段说。xxx_gps_init里保存回调时顺手把size补上这是很多厂商代码漏掉的一步。gps_read_thread是阻塞读读不到就短暂休眠避免空转吃满 CPU这里用usleep(10000)而不是忙等是血泪经验——早期版本忙等直接把一个核跑满。xxx_gps_start里串口号和波特率是最容易改错的两个参数串口号看板级配置波特率看模块手册9600 和 115200 都常见写错就是读不到数据。parse_nmea里只处理了 GGA实际项目还要处理 RMC拿时间和速度、GSV拿卫星信噪比资料里给了完整解析函数可以对着补。提示location_cb里传的GpsLocation必须把size赋成sizeof(GpsLocation)否则上层解析会越界或丢弃。这个字段和GpsCallbacks.size一样是版本兼容的命门。3.3 JNI 桥接Java 对象与 C 结构体的双向映射JNI 层的职责是把 Java 侧的IGpsCallback和 C 侧的GpsCallbacks对上。资料里的 JNI 代码核心是注册回调和转换数据// frameworks/base/services/core/jni/com_android_server_location_GnssLocationProvider.cpp结构示意 static void location_callback(GpsLocation* location) { JNIEnv* env getJNIEnv(); // 把 C 结构体转成 Java 对象字段 jclass clazz env-FindClass(android/location/Location); jobject loc env-NewObject(clazz, ...); env-SetDoubleField(loc, gLatitudeField, location-latitude); env-SetDoubleField(loc, gLongitudeField, location-longitude); // 回调 Java 侧方法 env-CallVoidMethod(gCallbackObj, gOnLocationChangedMethod, loc); } static void nmea_callback(GpsUtcTime ts, const char* nmea, int len) { JNIEnv* env getJNIEnv(); jstring s env-NewStringUTF(nmea); env-CallVoidMethod(gCallbackObj, gOnNmeaReceivedMethod, (jlong)ts, s); } static jint native_init(JNIEnv* env, jobject obj, jobject callback) { gCallbackObj env-NewGlobalRef(callback); // 全局引用别用局部 GpsCallbacks cbs { .size sizeof(GpsCallbacks), .location_cb location_callback, .nmea_cb nmea_callback, // ... 其余回调 }; const GpsInterface* iface get_gps_interface(); // 从 HAL 拿接口 return iface-init(cbs); }这里有两个关键点。第一gCallbackObj必须用NewGlobalRef用局部引用出了 JNI 函数就失效回调时直接崩这是 JNI 开发最经典的翻车点。第二get_gps_interface()内部走的是hw_get_module(GPS_HARDWARE_MODULE_ID, ...)它会按gps.default.so或板级指定的名字去加载 HAL 库库名或路径不对就返回空init自然失败。参数说明location_callback里SetDoubleField的字段 ID 要在JNI_OnLoad里用GetFieldID提前缓存每次回调都FindClass会拖慢性能。nmea_callback里NewStringUTF要求传入的是合法 UTF-8NMEA 是纯 ASCII没问题但如果你的模块混了二进制数据要先做长度截断。3.4 数据上报链路一次定位从串口到应用的完整路径把上面几段串起来一次定位的完整路径是应用调requestLocationUpdates请求下发到LocationManagerService。服务通过 JNI 调native_initJNI 从 HAL 拿到GpsInterface并init注册GpsCallbacks。服务调startHAL 打开串口、起读线程。模块输出 NMEA 语句读线程read到后调parse_nmea。parse_nmea解析出经纬度填GpsLocation调location_cb。JNI 的location_callback把 C 结构体转成 JavaLocation对象回调 Java 侧。服务把Location分发给应用的回调。这条链路上任何一环断了应用侧表现都是「一直拿不到定位」。排查时按这个顺序从下往上查先cat /dev/ttySx看有没有 NMEA 输出有就说明驱动和模块没问题再看logcat里 HAL 有没有解析日志再看 JNI 有没有回调日志最后看服务有没有分发。这个自下而上的排查法比在应用层瞎猜高效得多。4. 编译与运行把 HAL 库编出来并让框架加载到代码看懂了下一步是让它真正跑在设备上。这一章讲编译配置、库加载和验证都是能直接抄的步骤。4.1 编译配置Android.bp 里几个不能错的字段HAL 库的编译入口通常是Android.bp关键字段如下cc_library_shared { name: gps.default, // 库名框架按这个找 srcs: [xxx_gps.c, nmea_parser.c], shared_libs: [liblog, libhardware, libcutils], cflags: [-Wno-unused-parameter], vendor: true, // 厂商分区库 }name字段决定生成的库名框架默认找gps.default.so如果你的板级配置指定了别的名字比如gps.xxx.so两边必须一致。vendor: true表示编到 vendor 分区Android 8 以后 HAL 库基本都在 vendor 分区漏了这个字段会编到 system 分区导致加载失败。shared_libs里libhardware是必须的hw_get_module就在里面。编译命令按项目习惯常见是mmm hardware/xxx/gps/或make gps.default。编完确认产物路径一般在out/target/product/device/vendor/lib64/hw/gps.default.sohw子目录不能少hw_get_module就是去这个目录找的。4.2 设备节点与权限ueventd.rc 和 sepolicy 两处都要配库编出来了加载时还是可能失败因为 HAL 打不开串口。两处配置缺一不可# ueventd.rc给串口节点赋权限 /dev/ttyS1 0666 system system # sepolicy允许 hal_gps 域访问该设备节点 allow hal_gps tty_device:chr_file rw_file_perms;第一处解决「节点权限不够」第二处解决「SELinux 拦截」。只配第一处SELinux enforcing 模式下照样Permission denied日志里会有avc: denied记录用dmesg | grep avc能看到具体缺哪条规则。我一般先临时setenforce 0验证是不是 SELinux 问题确认后再补规则别一上来就关 SELinux 跑那样上线必翻车。4.3 验证三条命令确认链路通没通跑起来后按下面三条命令逐层验证# 1. 看串口有没有原始 NMEA 输出 cat /dev/ttyS1 # 2. 看 HAL 日志有没有解析和回调 logcat -s GpsHal GnssLocationProvider # 3. 看服务层定位状态 dumpsys location | grep -A 5 last location第一条有$GPGGA之类的语句说明驱动和模块正常第二条有location_cb相关日志说明 HAL 解析和回调正常第三条能看到经纬度说明整条链路通了。哪一条断就回到对应层查。这个三段验证法我每次移植新板子都走一遍比直接看应用有没有定位快得多。5. 避坑与排查五个真实踩过的坑底层驱动调试坑都很具体现象和原因往往对不上。下面五条是我和同行踩过的按「现象 → 原因 → 解决」写照着对能省不少时间。5.1 现象HAL 库明明编出来了框架就是加载不到原因通常是三个库名和框架找的名字不一致、库没放在hw子目录、或者编到了 system 分区而框架在 vendor 分区找。解决先ls /vendor/lib64/hw/确认库在不在、名字对不对再查板级配置里GPS_HARDWARE_MODULE_ID对应的名字最后确认Android.bp里vendor: true。三者对齐后hw_get_module才能返回非空。5.2 现象串口能读到 NMEA但应用一直没定位原因多半是location_cb没被调用或者调用了但GpsLocation.size没赋值导致上层丢弃。解决在parse_nmea里加日志确认解析出了经纬度再确认loc.size sizeof(GpsLocation)这行在。还有一个隐蔽原因flags没设GPS_LOCATION_HAS_LAT_LONG上层认为这个位置没有有效经纬度直接忽略。5.3 现象定位能出来但过一会儿就断反复重连原因是读线程异常退出或串口被关闭。常见于read返回 -1 时没处理EINTR信号打断后线程直接退出。解决read返回 -1 时判断errno EINTR就continue其他错误才退出并重开串口。另外检查acquire_wakelock_cb有没有实现息屏后系统休眠会把读线程挂起定位自然断。5.4 现象冷启动第一次定位要等很久热启动很快这是正常现象冷启动要下载星历ephemeris和历书almanac模块没有辅助数据就得从卫星慢慢收几十秒很正常。想加速就实现inject_time和delete_aiding_data配合上层做 AGPS 辅助数据注入。资料里给了inject_time的骨架但辅助数据服务器配置属于应用层的事驱动侧只负责把时间注入进去。别指望改驱动能把冷启动从四十秒降到五秒那是辅助数据的作用。5.5 现象SELinux enforcing 下 HAL 打不开设备permissive 下正常原因就是缺 sepolicy 规则。解决dmesg | grep avc找到denied那一行按里面的scontext、tcontext、tclass补allow规则编进 sepolicy 后重启验证。别用setenforce 0长期跑那是掩耳盗铃上线一定出问题。补规则时注意域名字要对GPS HAL 的域一般是hal_gps不同平台可能叫hal_gnss以实际avc日志为准。6. 二次开发换模块、加字段、做验证的几条实操资料拿到手真正的工作往往不是原样跑通而是换一个 GPS 模块、加几个上报字段、或者把定位数据接进自己的业务。这一章讲改造思路和验证方法。6.1 换模块改三个地方就能适配新硬件换 GPS 模块本质是换「怎么读数据」和「怎么解析」。三个地方必改串口号和波特率xxx_gps_start里、上电和复位时序板级 GPIO 配置有些模块要先拉复位再上电、NMEA 语句集不同模块输出的语句可能不同比如有的默认不出 GSV。如果新模块走 I2C 而不是串口读线程要换成 I2C 读解析逻辑不变。改完用第 4 章的三段验证法走一遍确认每层都通。6.2 加字段从 NMEA 里多取几个值往上送业务常要速度、航向、卫星数。速度在 RMC 语句里卫星数在 GGA 的定位质量字段或 GSV 里。加字段的步骤先在parse_nmea里解析出来填进GpsLocation对应字段并补flags比如GPS_LOCATION_HAS_SPEED、GPS_LOCATION_HAS_BEARING再确认 JNI 层把这些字段转成了 JavaLocation的对应属性。漏了flags是最常见的错字段填了但上层不认。6.3 验证方法用固定 NMEA 回放做回归改完驱动最怕的是「这次能定位下次换个场景又不行」。我一般做一个 NMEA 回放工具把真实抓到的 NMEA 语句存成文件用socat或自己写的小程序灌进一个虚拟串口让 HAL 去读。这样不依赖真实卫星信号室内就能回归测试改解析逻辑时特别有用。回放文件里要覆盖冷启动、热启动、丢星、多系统GPS北斗几种场景每种跑一遍看输出对不对。# 用 socat 建一对虚拟串口一端灌 NMEA一端给 HAL 读 socat -d -d pty,raw,echo0 pty,raw,echo0 # 假设输出 /dev/pts/3 和 /dev/pts/4把 HAL 的串口指向 /dev/pts/4 cat nmea_sample.txt /dev/pts/3这个法子有个前提HAL 里的串口路径要能改所以我在实现时会把串口号做成可配置的宏或属性方便测试时切到虚拟串口。这个习惯是从一次「改完解析逻辑现场没信号没法验证只能等下次出车」的教训里来的。从那以后我每次改 NMEA 解析都强制先用回放跑一遍再上车省得来回折腾。希望这些能帮到你少走点我走过的弯路。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑