简介:围绕GPS Android底层驱动的中文资料包,面向Android系统开发者与驱动移植工程师,系统讲解JNI与HAL层的协同原理,并附关键源码与移植分析笔记。压缩包共5个文件,大小约9.28MB,内含2份Word讲解文档、1个RAR辅助资料包、1个构建脚本及1个C语言源文件,文档侧重原理梳理,脚本与源码用于构建验证,结构清晰,便于对照学习。该资料已有839人浏览学习,内容结合基于Android 2.3的车载导航GPS HAL移植与剖析实例,深入覆盖JNI函数原型定义与本地实现、HAL层接口初始化与定位数据上报流程、GpsInterface在LocationManagerService中的调用关系,以及GpsStatus状态查询、GpsNavigationMessageCallback导航消息接收等组件的具体用法。同时讲解了如何通过JNI头文件声明本地接口,并利用ndk-build编译生成动态库供Java层调用,帮助读者从源码层面打通GPS驱动的开发链路。读者可借此理清GPS服务从Java层到底层硬件的完整调用链路,理解GpsLocationProvider生命周期管理与位置更新逻辑,为后续在真实设备上调试GPS驱动、优化定位性能,甚至类比开发Wi-Fi、蓝牙等同类硬件驱动提供扎实参考。
1. GPS Android底层驱动,不是调个LocationManager那么简单
把一块串口GPS模块接到开发板上,烧一套Android系统,打开地图,定位图标永远灰的。Logcat里只有一句not available。多数人第一反应是应用层API写错了,但真正卡住你的,往往是HAL模块没注册、JNI方法符号没对接上。这份“GPS Android底层驱动资料(附关键源码)”不是讲怎么调requestLocationUpdates,而是把UART串口、HAL抽象、JNI bridge、Framework调用这一整条链路拆开,附上能编译的源码与脚本。适合正在做BSP移植、想在板子上把GPS真正跑起来的开发。读完你会明白,让定位数据出现在getLastKnownLocation里,只需要把底层这些代码逐行咬透。
2. 整体架构拆解:从硬件抽象到 Framework 的调用链
2.1 源码结构:每一层都有对应的文件
拿到这份资料,第一件事是把目录树展开。压缩包里的结构并不复杂,但每层都能对应到Android系统里的一个真实位置:
gps_android_driver/ ├── README.md ├── gps.h ├── gps.c ├── Android.bp ├── jni/ │ ├── com_server_location_GpsLocationProvider.cpp │ └── Android.bp ├── app/ │ ├── GpsTestApp/ │ ├── AndroidManifest.xml │ └── MainActivity.java └── scripts/ ├── setup_uart.sh └── test_loc.sh粗看只有几个文件,但它覆盖了三层代码:gps.h和gps.c是HAL层,负责屏蔽具体硬件差异,对外提供一个标准GPS设备接口;jni/目录下的.cpp是JNI层,负责把C层的能力暴露给Java Framework;app/是一个验证用的Android工程,里面就一个页面,用来触发LocationManager.getLastKnownLocation。三个部分合起来就是一条完整链路。
常见做法是把HAL模块放到AOSP源码树的hardware/libhardware/modules/gps/下,因为这样可以直接用系统的编译框架。资料里单独给了一份Android.bp,目的就是让你既可以整个扔进AOSP编,也可以抽出源码自己搭NDK工程。我一般会先把gps.c和gps.h放到独立目录,确认模块加载正常,再去动JNI,因为HAL有没有被加载,是这条链路里最好排查的。
还要注意gps.c里定义的结构体不是随便写的。Android HAL要求模块文件导出一个固定的全局符号HAL_MODULE_INFO_SYM,系统通过hw_get_module去.so里找这个符号。如果导出的符号名变了,或者模块id字段和上层请求的字符串对不上,就会返回-1。这部分代码我会在下一章逐段讲。
scripts/setup_uart.sh看起来不起眼,但它解决的是串口设备权限问题。开发板上/dev/ttyS1大概率只有root或者特定用户组能用,App要读串口,必须把设备节点权限放开,或者加对应SELinux策略。没有这个脚本,就算HAL代码完全正确,你也会发现打开串口时返回Permission denied。
2.2 调用链与NMEA数据格式约定
定位数据在这套架构里的流动顺序是固定的。我从App侧往前理,方便你对照代码看:
- App 调用
LocationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER) - 这个请求丢给系统进程里的
LocationManagerService LocationManagerService找到对应的GpsLocationProviderGpsLocationProvider是个Java对象,它内部通过loadLibrary("gps")加载JNI库,然后调用init()、start()等 native 方法- JNI层拿到 native 方法后,通过
hw_get_module(GPS_HARDWARE_MODULE_ID)加载 HAL 模块,拿到gps_device_t - HAL层
start()里会开一个串口读线程,不断从/dev/ttyS1读取NMEA语句 - HAL解析出经纬度后,通过回调函数往上层送,最终封装成 Android 的
Location对象
这条链路里有两个位置最容易断:一是hw_get_module找不到模块,整个native方法直接返回false;二是串口读线程没有权限或读不到数据,导致上层一直拿不到回调。资料里的核心源码其实就是在解决这两件事。
NMEA数据格式也是绕不开的。GPS模块上电后主动输出$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,...这样的句子,字段按逗号分隔。GGA里包含标准定位时间、纬度、经度、卫星数、海拔等。HAL层的parse_nmea要做的工作,就是把字符串第3、4、5、6个字段抠出来,换算成double。资料里没有完整实现所有句子,只针对GGA和RMC做了大多项目都够用的解析,这也是我认为它适合做原型验证的原因——不会一上来就让你陷到十几个NMEA消息类型里。
在继续前,我想说清楚为什么要一层JNI夹在中间。Android的Java框架跑在ART虚拟机里,不能直接调C函数;HAL层又是标准的C接口。JNI就是那座桥。它一方面把Java的native方法init()和C层的gps_init对应起来,另一方面负责在Java对象和C结构体之间传递数据。很多初接触的人会跳过JNI,直接写一个自己调用的.so,用System.loadLibrary加载后native方法却找不到符号,这就是因为没按JNI的注册规则来。资料里专门把JNI_OnLoad和RegisterNatives写出来,就是让你少走这个弯路。
需要特别提醒的是,Android 8.0之后系统倾向于用HIDL接口替代传统直通式HAL,但很多快速开发板仍然保留hw_get_module这一套。资料里的代码走的是传统HAL模型,在Android 7-10之间都比较好编译;如果目标是Android 11以上,就得考虑接口迁移,我在最后一章会给出改造思路。
3. HAL 与 JNI 核心实现:一份能跑通的关键源码
3.1 HAL 层:hw_module_t 与串口读写的骨架
HAL层的代码量不大,但骨架必须完整。第一个要看的,是模块入口定义:
// gps.c 头部 #include <hardware/gps.h> #include <hardware/hardware.h> #include <fcntl.h> #include <unistd.h> static int gps_device_open(const struct hw_module_t* module, const char* name, struct hw_device_t** device); 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, .name = "GPS HAL for Android", .methods = &gps_module_methods, };这里HAL_MODULE_INFO_SYM必须叫这个名字,不能改动。Android 的hw_get_module(GPS_HARDWARE_MODULE_ID, &module)会去/vendor/lib/hw/或/system/lib/hw/下找gps.default.so,然后通过dlopen拿到这个符号,再检查id是否匹配。所以四个关键点分别是:文件要叫gps.default.so、符号名要叫HAL_MODULE_INFO_SYM、id要来自gps.h、路径要在HAL标准目录下。任何一个不对,结果都是module not found。
接下来是open回调:
static int gps_device_open(const struct hw_module_t* module, const char* name, struct hw_device_t** device) { gps_device_t *dev = calloc(1, sizeof(gps_device_t)); if (!dev) return -ENOMEM; dev->common.tag = HARDWARE_DEVICE_TAG; dev->common.version = 0; dev->common.module = (struct hw_module_t*)module; // 把资料里实现的函数挂到设备结构体上 dev->init = gps_init; dev->start = gps_start; dev->stop = gps_stop; *device = (struct hw_device_t*)dev; return 0; }这里calloc清空结构体,防止残留野指针。dev->common里的tag必须等于HARDWARE_DEVICE_TAG,这是hw_device_t的基本检查。gps_device_t是gps.h定义的结构体,最前面一个是hw_device_t common,后面跟着若干函数指针。这些函数指针就是上层通过gps_device->start()能直接调用的入口。常见坑是只实现了init和start,漏了stop,上层在某些场景下会崩溃,当然至少要保证start正常工作。
串口读写是核心。资料里把读线程写得很完整,我摘一段加注释:
static void* gps_reader_thread(void* arg) { int fd = open("/dev/ttyS1", O_RDWR | O_NOCTTY); if (fd < 0) { ALOGE("open ttyS1 failed: %s", strerror(errno)); return NULL; } struct termios cfg; tcgetattr(fd, &cfg); cfsetispeed(&cfg, B9600); cfsetospeed(&cfg, B9600); cfg.c_cflag |= (CLOCAL | CREAD); cfg.c_cflag &= ~CSIZE; cfg.c_cflag |= CS8; cfg.c_cflag &= ~PARENB; cfg.c_cflag &= ~CSTOPB; tcsetattr(fd, TCSANOW, &cfg); char buf[256]; int n; while ((n = read(fd, buf, sizeof(buf) - 1)) > 0) { buf[n] = '\0'; parse_nmea(buf, strlen(buf)); } close(fd); return NULL; }波特率通常默认9600,8N1,但要注意不是所有模块都这样。有些模块上电默认38400,必须先在PC上确认。O_NOCTTY防止串口被当作控制终端导致输入输出被shell接管;CLOCAL | CREAD是保证即使没连调制解调器也能读数据。c_cflag那几行是在配置无校验、1位停止位、8位数据位,这套配置兼容大部分NMEA设备。
read是按底层缓冲区来的,不一定一次就是一行,所以parse_nmea需要先按\n切分。资料里的做法是维护一个静态环形缓冲,把每个字节追加进去,遇到\n就取出来处理。这个细节虽然小,但直接影响数据解析正确性,我在第五章会把它展开成一个典型踩坑场景。
3.2 JNI 层:native 方法与 Java 回调的桥接
JNI 层在整份源码里的位置很特殊。它不是一个完整的HAL,也不是纯Java,而是一串C++函数,通过RegisterNatives绑定到系统Java类GpsLocationProvider上。核心函数如下:
// com_server_location_GpsLocationProvider.cpp #include <jni.h> #include <hardware/hardware.h> #include <hardware/gps.h> static gps_device_t* sGpsDevice = NULL; static jboolean init(JNIEnv* env, jobject obj) { const hw_module_t* module = NULL; if (hw_get_module(GPS_HARDWARE_MODULE_ID, &module) != 0) { ALOGE("hw_get_module failed"); return JNI_FALSE; } gps_device_t* dev = NULL; module->methods->open(module, GPS_HARDWARE_MODULE_ID, (struct hw_device_t**)&dev); sGpsDevice = dev; return JNI_TRUE; } static jboolean start(JNIEnv* env, jobject obj) { if (sGpsDevice && sGpsDevice->start) { return sGpsDevice->start() == 0 ? JNI_TRUE : JNI_FALSE; } return JNI_FALSE; }这里我把函数名简化了,实际资料里的函数名会带完整包名前缀,避免符号冲突。可以看到init()做的事情和上层Java的调用是对应的:拿到HAL模块,打开设备,存到静态指针。start()则直接透传到HAL层。如果你发现sGpsDevice是空指针,说明init没有在start之前被调,或者init已经失败了。
注册表是JNI的枢纽:
static const JNINativeMethod kMethods[] = { { "init", "()Z", (void*)init }, { "start", "()Z", (void*)start }, }; extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) { JNIEnv* env = NULL; if (vm->GetEnv((void**)&env, JNI_VERSION_1_6) != JNI_OK) { return JNI_ERR; } jclass cls = env->FindClass( "com/android/server/location/GpsLocationProvider"); if (cls == NULL) { return JNI_ERR; } if (env->RegisterNatives(cls, kMethods, sizeof(kMethods)/sizeof(kMethods[0]))) { return JNI_ERR; } return JNI_VERSION_1_6; }这里的()Z是JNI描述符,表示无参、返回boolean。FindClass的字符串必须是Java层类的全限定名,用/分隔。Android系统源码里的GpsLocationProvider类位于com.android.server.location,如果你的目标源码版本改了包名,这里就要跟着改。资料里给的是一种标准写法,好处是它可以被同一个.so加载多次,不会重复找到类。
实际开发中,我一般会在这两层之间加一层日志,而不是只在上层看结果。比如在init里打ALOGD("gps native init called"),在start里打日志并打印返回值。这样判断问题在哪里非常高效:日志只到JNI层,说明HAL没加载成功;日志过了JNI层,说明问题在串口或解析。
NMEA解析函数我最后提一句:
static void parse_nmea(const char* line, int len) { if (strncmp(line, "$GPGGA", 6) == 0) { // 按逗号拆分字段 // 字段2, 4 分别是纬度经度的度分格式,转成double再回调上层 } }虽然只有一段注释,但它背后是GPS知识的一个门槛:4807.038表示 48°07.038′,需要拆成度加分/60换算成十进制。很多人在这一步把小数点位置放错,差出的距离就是几十公里。资料里的源码有一整段用于转换,我建议拿到后先拿模块实测的串口数据对一下。
4. 编译与运行:在 Android 源码树里把 GPS 模块拉起来
4.1 源码树摆放与模块编译
要跑这份驱动,最直接的方式是把源码放进AOSP工程里,编成系统镜像的一部分。这也是资料里Android.bp存在的意义。假设你的工作目录是~/aosp,把资料解压后创建一个目录:
mkdir -p ~/aosp/hardware/libhardware/modules/gps cp gps.h gps.c Android.bp ~/aosp/hardware/libhardware/modules/gps/如果使用 Android.bp 构建,内容可以参考下面这份:
cc_library_shared { name: "gps.default", vendor: true, relative_install_path: "hw", srcs: ["gps.c"], shared_libs: [ "libhardware", "liblog", "libcutils", ], }name里的.default会生成gps.default.so,安装路径由于vendor: true和relative_install_path: "hw"会变成/vendor/lib/hw/gps.default.so。注意,Android 10之后已经不太推荐在hardware/libhardware/modules下直接放模块,但你做的是传统HAL,放到这里还是可以编,只是系统会提示模块被deprecated。如果想放在一个更干净的位置,也可以放在device/<产品名>/gps/下,然后通过PRODUCT_PACKAGES加入构建。
编译命令老工程师都有肌肉记忆:
source build/envsetup.sh lunch aosp_你的产品-userdebug mmm hardware/libhardware/modules/gps/新版本里mmm已被移除,可以用:
m gps.default编译产物在out/target/product/<产品名>/vendor/lib/hw/gps.default.so。如果你用的是Android.bp,检查路径是否和你声明的模块名一致。如果打包进系统镜像,直接烧录即可。如果只是想快速试,不再刷机,你可以用adb root后把.sopush 进/vendor/lib/hw/。
这里有个容易翻车的地方:新手总是直接push,却忘了先重新挂载系统分区。现在的Android系统/vendor是只读的,需要先执行:
adb root adb remount adb push gps.default.so /vendor/lib/hw/ adb shell chmod 644 /vendor/lib/hw/gps.default.so adb reboot如果不remount,会收到Read-only file system错误。这不是资料的问题,是所有系统级文件修改的通用步骤。
对于只做上层验证的同学,一个现实问题是手头没有完整AOSP源码。资料里JNI目录下那份Android.bp也能用作NDK构建的参考,你可以用 CMake 的思路把.cpp编成libgps_jni.so,然后在App里System.loadLibrary("gps_jni")。但是HAL层调用hw_get_module需要系统库头和运行环境,纯粹NDK工程里没有这些。所以我的建议是:没有源码树,至少准备一台能adb remount的开发板,先在板子上验证HAL,再用NDK单独编JNI层,最后合到一起。
4.2 设备端验证:串口、模块与日志三步走
编译完成并烧录或push好后,先用最笨但有效的方法确认串口硬件通不通。这套资料里的scripts/setup_uart.sh内容大致是:
#!/system/bin/sh # 给串口设备开权限,方便GPS HAL读取 chmod 666 /dev/ttyS1 # 有些板子需要重新配置gpio mux,把串口功能引出来 # echo 1 > /sys/class/misc/.../select手动验证时,我先在adb shell里执行:
adb shell su chmod 666 /dev/ttyS1 cat /dev/ttyS1如果模块上电正常,应该能看到一行一行$GPGGA开头的报文。如果这里没有输出,那问题出在硬件或串口配置,而不在Android驱动,后面做再多的HAL适配都是白费。我见过至少两次现象是:HAL代码没问题,最后发现ttyS1被其他驱动占用了,用cat /dev/ttyS1直接会卡住。
串口通之后,验证JNI和HAL的对接。先确认模块文件存在:
adb shell ls -l /vendor/lib/hw/gps.default.so adb shell ls -l /system/lib/hw/gps.default.so然后打开日志过滤,启动资料里的测试App:
adb shell setprop log.tag.GpsLocationProvider VERBOSE adb shell am start -n com.example.gpstest/.MainActivity adb logcat -s GpsLocationProvider GPS JNI如果你看到hw_get_module或GPS HAL module not found,说明HAL没被找到,对照第五章第一条处理。如果看到串口数据被解析并且回调了,App界面上就会显示经纬度。这套验证流程在大部分Android 7-10的板子上都能跑通。
为了让调试更方便,资料里的JNI层编译了一个独立的可执行文件版本,但依赖系统库。如果你把libhardware等库静态编进去,可以考虑这个路径。不过我还是推荐直接在系统里跑,因为hw_get_module依赖系统的/vendor/lib/hw/路径和硬件模块加载机制,单独在用户态模拟出来的HAL环境不一定和真实一致。
5. 常见问题与排查:定位数据没出来,先查这三处
这一章是资料里最值钱的部分。下面三条问题是我在跑类似驱动时最常遇到的,每条都按现象、原因、解决三个步骤写清。
5.1 HAL设备加载失败:hw_get_module 返回 -1 的排查细节
现象:logcat里出现hw_get_module failed,或者GpsLocationProvider: GPS HAL module not found,App的定位接口永远不返回数据。有时候你明明编译生成了.so,也push到了板上,但系统就是报not found。
原因:最常见的是模块ID字符串没对上。你编译出的gps.default.so里的HAL_MODULE_INFO_SYM.id是"android.hardware.gps",但Framework层请求的可能是另一个字符串。另外,如果gps.default.so没编译进系统或者被系统分区清理掉,也会出现这个错误。还有一种很隐蔽的情况:你有两个.so,一个在/vendor/lib/hw/,一个在/system/lib/hw/,系统按ro.hardware属性选了一个,而那个文件是旧的。
解决:先用adb shell ls /vendor/lib/hw/看.so在不在。在的话,执行:
adb shell setenforce 0临时关闭SELinux再看日志,如果恢复正常,说明是SELinux策略拦住了dlopen。如果还不行,检查gps.h里GPS_HARDWARE_MODULE_ID的宏定义,把它与AOSP源码里hardware/libhardware/include/hardware/gps.h的值逐一比对,改成一致。我一般会在HAL模块入口加一行ALOGI("GPS HAL id=%s", HAL_MODULE_INFO_SYM.id);,这样能在日志里看到实际值,对比请求值,原因一眼就清楚。
再深一层,hw_get_module的查找路径和ro.hardware有关。默认会去/system/lib/hw/<id>.<ro.hardware>.so找,如果没有,再退回/system/lib/hw/<id>.default.so。如果你的板子ro.hardware被设置成某个奇怪名字,可能就命中了gps.board.so,而不是gps.default.so。解决办法是看adb shell getprop ro.hardware的输出,把编译产物重命名成对应的格式,或者删掉多余文件避免误加载。
5.2 JNI注册失败:类名变化导致系统服务闪退
现象:App或系统服务一闪退,logcat里出现JNI RegisterNatives failed,有时是No implementation found for void com.android.server.location.GpsLocationProvider.native_init()。更高频的画面是系统定位进程直接重启。
原因:Android不同版本里GpsLocationProvider的包名和类名经历过几次变化。例如旧版本在com.android.location,新版本在com.android.server.location。资料里的JNI代码FindClass写死了类名,换了源码树就会找不到。另一个原因是Java侧的方法签名和注册表里的signature对不上,比如Java是private native void init(),JNI侧却注册成()Z,类型不匹配,注册就会失败。
解决:把JNI_OnLoad里的FindClass字符串改成你当前Android版本实际包名。怎么查?用grep -r "class GpsLocationProvider" -n frameworks/base/services/core/java。找到后把com/android/server/location/GpsLocationProvider替换成完整路径。同时确认Java类的native方法声明和注册表里的方法名、签名完全一致。我遇到最隐蔽的情况是Java侧方法加了synchronized,JNI侧照常注册,这个不影响。
还要注意JNI_OnLoad本身有没有被执行。如果.so是System.loadLibrary加载的,JVM会默认找JNI_OnLoad;但如果你用dlopen方式手动加载,没有触发JNI_OnLoad,RegisterNatives 就永远不会执行。资料里的.so走的是正常loadLibrary路径,但你自己扩展测试工程时不要改成dlopen。
5.3 串口乱码与无回调:波特率和行缓冲的坑
现象:cat /dev/ttyS1能看到输出,但内容是..?..@.x这样的乱码;或者程序运行后一直没定位回调,但串口分明在吐数据。
原因:乱码十有八九是波特率不对。GPS模块默认波特率不一定都是9600,很多小厂商改成了115200,必须看模块规格书。没有回调则有可能是缓冲区没有按行切分,两条NMEA句子粘在一起,解析函数没找到$GPGGA行首;或者模块冷启动需要几十秒,你以为没数据,其实是在搜星。
解决:先用PC串口工具确认实际波特率,然后修改gps.c里的cfsetispeed和cfsetospeed。如果串口数据不是规整文本,不要尝试用read强行读,先按字节十六进制打出来看帧结构。针对粘包,我习惯用一个环形缓冲,每次read之后把所有字节追加进去,再按\n找完整行,丢给解析函数。这个能解决大多数设备上buf被拆分导致的丢帧问题。
另外,不要忽略cat /dev/ttyS1之后你的终端本身会进入raw模式。退出cat后先恢复串口设置,否则后续进程可能读不到正常数据。我每次验证完都会把HAL里的tcsetattr重新执行一遍。如果你发现HAL读线程正常但一直没有回调,还要检查一下GPS模块在室内是否拿不到有效定位。模块输出GGA里第7个字段是定位质量指示,0是未定位,1是GPS定位,2是差分定位。日志里加上这个字段的判断,能快速区分是驱动问题还是天线问题。
6. 二次开发改造:把这套驱动适配到你的板子上
6.1 替换串口设备与定位参数
资料默认写死了/dev/ttyS1和9600。你要做的第一件事是改成自己板子的实际串口设备。常见的命名有/dev/ttyS1、/dev/ttyMT2、/dev/ttyHSL0。改的时候全局搜一下ttyS1,把open的参数换掉,同时确认setup_uart.sh里的GPIO复用配置对应你的板子。如果模块输出电平是3.3V或TTL,需要确认转串口芯片方向,这部分不是代码能解决的。换完设备之后,记得用第五章的验证流程先cat串口,码通再编译。
6.2 从传统HAL迁移到HIDL服务
Android 8以后,Treble架构要求HAL运行在独立进程,传统hw_get_module方式不再被官方推荐。如果你从资料代码迁移到高版本,不要死磕JNI直接拉HAL,可以保留gps.c作为HIDL服务的后端,把gps_device_t包到IGnss实现里,JNI侧改为通过HIDL client接口调用。改造核心是把hw_get_module换成IGnss::getService(),其余数据解析逻辑不变。这样既保住了底层代码,又跟上了新架构。
从那以后,我每次调GPS都要先过一遍“串口能不能cat出数据、HAL模块能不能被找到、JNI注册有没有报错”三座山,再谈定位精度和冷启动优化。这份资料的价值就是把这三座山的代码都摆在眼前,让你不用在黑匣子里瞎猜。希望帮到你。
本文还有配套的精品资源,点击获取