☰
Android GPS底层驱动源码解析:从HAL到JNI的完整调用链
2026/10/21 13:47:53 网站建设 项目流程

简介:面向Android底层驱动开发与移植场景,资料聚焦GPS从Java层到HAL层的完整通路,拆解JNI与HAL交互、GpsInterface定义、GpsLocationProvider生命周期以及GpsStatus与导航消息回调等关键机制。既有原理讲解,也附关键源码,能帮助定位GPS驱动开发、移植或调试中的常见问题,并为理解Wi-Fi、蓝牙等其他硬件驱动提供类比思路。资源为zip压缩包,共5个文件,含2篇docx原理文档、1个rar补充资料,以及Android.mk构建脚本和c源码文件,整体约9.28MB。docx用于梳理架构,mk与c给出编译配置和驱动实现片段。已有839人浏览学习,适合有一定Android基础、希望深入HAL/JNI层的系统开发者。

1. GPS Android底层驱动:打通HAL到JNI的关键源码怎么读

做Android定位开发的人,十有八九会被GPS驱动这块黑匣子卡住:Java层调一下LocationManager就能拿到经纬度,但底层到底怎么把NMEA数据从串口搬上来、HAL层接口和JNI方法是怎么对上号的,翻遍网上教程也难找到一份能跑通的完整资料。这份带着关键源码的GPS底层驱动资料,解决的正是这个断层问题——从HAL硬件抽象层到JNI回调注册,再到数据上抛到框架层,把整条链路的代码串起来讲透。适合三类人:刚接手车载或工控GPS模块的驱动开发、想在方案上自研定位逻辑的系统工程师、以及被vendor目录下那堆GPS相关源码搞得一头雾水的入门者。不需要你有深厚的内核功底,但最好懂点C和JNI基础,否则看HAL层结构体时会有点吃力。

2. 驱动调用链:从Java层到串口的四层流转

2.1 四层架构里各自干的是什么活

GPS驱动在Android系统里不是“一个驱动”这么简单,它是从上层API一直演化到硬件寄存器的一整条链。标准的分层是:应用层用LocationManager拿位置,框架层由LocationProvider和LocationManagerService负责调度,再往下是GpsLocationProvider这个JNI类,它通过JNI方法调进C++层,C++层再通过HAL接口调到底层硬件。这份资料里把每一层的文件名、关键函数、数据流转方向都做了标注,拿到源码包后第一件事不是编译,而是照着调用链把文件对应起来。

常见做法是先从hardware/libhardware/include/hardware/gps.h这个头文件读起,因为它是整个HAL层的契约。里面定义了gps_device_t、GpsInterface、GpsCallbacks这些核心结构体,HAL层要实现GpsInterface里的init、start、stop、inject_time等接口,而上层JNI则通过hw_get_module拿到这个接口指针。资料里的源码注释把每个结构体字段都解释得很细,比如GpsCallbacks里的location_cb、status_cb、sv_status_cb分别对应位置上报、状态变化、卫星状态回调,这三者是最容易搞混的。

我在实际项目里习惯先画一张数据流图:串口或者I2C出来的NMEA语句 → HAL层解析出定位数据 → 填进GpsLocation结构体 → 触发location_cb回调 → JNI层把C结构体转换成Java对象 → 上抛给LocationManagerService。这张图画明白了,后面所有代码读起来都是顺的,画不明白的地方就是资料里该重点看的部分。

2.2 为什么HAL接口设计成结构体指针而不是直接导出函数

Android的HAL设计有个区别于普通Linux驱动的特点:它不是让上层直接dlopen某个so然后调符号,而是通过hw_get_module按模块ID去加载,拿到的是hw_module_t和hw_device_t这两个基础结构体。GPS模块在gps.h里扩展出一个gps_device_t,第一个字段就是hw_device_t common,这就是“继承”的C语言实现方式。

// gps.h 核心结构体(资料源码节选,注释是我加的) typedef struct { /** 必须是第一个字段,HAL框架靠它识别设备 */ hw_device_t common; /** 获取GpsInterface接口指针 */ const GpsInterface* (*get_gps_interface)(struct gps_device_t* dev); } gps_device_t; typedef struct { /** 上层传入的回调函数集合 */ size_t size; gps_location_callback location_cb; gps_status_callback status_cb; gps_sv_status_callback sv_status_cb; gps_nmea_callback nmea_cb; } GpsCallbacks;

逻辑说明:上层JNI先hw_get_module(GPS_HARDWARE_MODULE_ID, &module)拿到模块,再通过module->methods->open()打开设备,得到一个gps_device_t指针,最后调用get_gps_interface拿到真正的操作接口。这种设计的好处是硬件实现可以完全替换,只要结构体契约不变,上层代码一行都不用改。

参数说明:hw_device_t common这个第一个字段是硬性要求,HAL框架在open之后会做类型转换,如果厂商实现时把字段顺序搞错,轻则crash,重则内存踩踏。GpsCallbacks里的size字段必须初始化成结构体实际大小,很多自定义实现的坑都出在这个字段没赋值或者赋错值,导致上层判断回调版本时走错分支。

2.3 源码包里哪些文件先读、哪些可以后读

这份资料的源码包不是平铺的,它模拟了一个典型的vendor目录结构。我拆包后的阅读顺序建议是:先读gps.h和gps.cpp(或对应实现文件),了解接口契约;然后读JNI目录下的com_android_location_provider_GpsLocationProvider.cpp,看JNI函数怎么映射到HAL调用;再读hardware.c,这部分是HAL模块加载和设备打开的流程,通常厂商不太会改;最后才是串口驱动或者I2C驱动,这块跟具体硬件绑定很紧,只有需要调试底层收发时才用得上。

资料里特意标注了哪些文件是必须按原样保留的,比如gps.h这种契约头文件,改了会破坏ABI;哪些文件是示意性质的,比如某个gps_parse.c里的NMEA解析可能是精简版,实际产品里需要换成正则匹配更完整的解析器。这个区分很重要,不然你照着源码抄到自己的项目里,会发现解析不了某些厂商的特殊NMEA语句。

3. HAL层实现:GPS抽象接口与源码级改造

3.1 实现GpsInterface的六个步骤

拿到了接口契约,接下来就是填实现。资料里HAL层源码实现了一套完整的GpsInterface,包含init、start、stop、cleanup、inject_time、inject_location这六个基础函数。我在自己项目里通常把这六个函数的职责拆成三组理解:生命周期管理(init/cleanup)、定位状态机(start/stop)、数据注入(inject_time/inject_location)。

init是第一个被调用的函数,它接收上层传下来的GpsCallbacks结构体指针,实现里要把它存到全局变量里,因为后续所有定位数据都要通过这些回调上抛。这里有个容易踩的细节:回调函数指针在哪个线程被调用,是由HAL实现决定的,不是由上层决定的。如果你的HAL层是开了一个独立线程轮询串口,那回调就在那个线程里触发;如果是在start里用select事件驱动,那就在事件循环线程里触发。

// HAL层 init 实现示意 static GpsCallbacks s_callbacks; static int gps_init(GpsCallbacks* callbacks) { if (callbacks == NULL) { return -1; } // 回调结构体必须深拷贝,不能只存指针 // 因为上层可能传的是栈上临时变量 s_callbacks = *callbacks; return 0; } static int gps_start() { // 常见做法:开一个pthread,循环read串口数据 // 每次读到完整NMEA行后,调用 s_callbacks.nmea_cb pthread_create(&s_thread, NULL, gps_reader_thread, NULL); return 0; }

逻辑说明:gps_init里把回调做了一次结构体拷贝,而不是保存指针,这是血泪经验——有厂商实现直接存了上层传下来的指针,结果上层那个JNI对象在GC时把内存回收了,底层还在往野指针里写回调,导致偶发性crash。结构体拷贝的开销可以忽略不计,因为回调函数指针本来就是几字节的地址。

参数说明:callbacks指针判空是基本操作,但还需要做一步——检查callbacks->size是否大于等于sizeof(GpsCallbacks),因为旧版系统可能传一个更小的结构体,你直接访问callbacks->nmea_cb会越界。资料源码里这个检查做得很细,值得抄走。

3.2 NMEA数据解析:从串口原始字节到GpsLocation

模块上电后输出的原始数据是ASCII码的NMEA语句,常见的有GGA、RMC、GSV、GSA这几种类型。底层HAL收到一行完整语句后,要做的是解析出经纬度、UTC时间、卫星数、海拔这些字段,填充到GpsLocation结构体里。

资料里给了一份精简的解析实现,它核心思路是:按逗号切分字段,然后按每种语句类型的固定字段顺序索引取值。比如GGA语句的格式是$GPGGA,时间,纬度,N,经度,E,定位质量,卫星数,水平精度,海拔,M,...,那么索引2是纬度,索引4是经度。这里有个转换坑:NMEA里纬度格式是ddmm.mmmm(度分格式),要转成dd.dddddd的小数度才能填进GpsLocation的latitude字段。

// NMEA经纬度解析 static double nmea_to_decimal(double raw, char direction) { // raw=4802.1234 => 纬度48度02.1234分 int degrees = (int)(raw / 100); double minutes = raw - degrees * 100; double decimal = degrees + minutes / 60.0; if (direction == 'S' || direction == 'W') { decimal = -decimal; } return decimal; } // GGA语句解析简化示意 static int parse_gga(char* line, GpsLocation* loc) { char* tokens[20]; int count = split_by_comma(line, tokens, 20); if (count < 15) { return -1; // 字段数不够,语句可能被截断 } loc->latitude = nmea_to_decimal(atof(tokens[2]), tokens[3][0]); loc->longitude = nmea_to_decimal(atof(tokens[4]), tokens[5][0]); loc->accuracy = atof(tokens[8]); loc->altitude = atof(tokens[9]); loc->timestamp = utc_to_epoch(tokens[1]); return 0; }

逻辑说明:nmea_to_decimal里的raw / 100取整数部分作为度,余数除以60换算成度的小数部分,这是NMEA格式的标准换算方法。方向字母是N/S/E/W,注意南纬和西经要取负值,这是全世界开发者最容易犯的经典错误——把纬度填成正数,结果定位点跑到另一个半球。

参数说明:GpsLocation里的flags字段是用来标记哪些字段有效的位掩码,比如GPS_LOCATION_HAS_LAT_LONG表示经纬度有效,GPS_LOCATION_HAS_ALTITUDE表示海拔有效。上层框架会检查这个字段,如果没置位,对应数据会被丢弃。我在开发时被这个坑过两次:一次忘置位导致明明解析对了数据但框架层不认,另一次是只置了经纬度位没置时间位,导致定位时间一直是1970年。

3.3 串口参数设置与数据粘包处理

GPS模块通常通过UART接出来,波特率常见的是9600或115200。HAL层open串口时需要用tcsetattr设置raw模式,关闭回显和流控。资料里给的串口初始化代码比较完整,但有一个地方容易被忽略——VMIN和VTIME这两个参数。

// 串口初始化关键配置 struct termios options; tcgetattr(fd, &options); // 原始模式:不做任何行处理,不做回显 cfmakeraw(&options); // 波特率设置,GPS模块最常见的是9600 cfsetispeed(&options, B9600); cfsetospeed(&options, B9600); // VMIN=1 表示至少读到1个字节才返回 // VTIME=0 表示不启用超时轮询,无限等待 options.c_cc[VMIN] = 1; options.c_cc[VTIME] = 0; tcsetattr(fd, TCSANOW, &options);

逻辑说明:cfmakeraw把终端设成原始模式,这样串口读到的就是干净的字节流,不会被驱动层劫持做行编辑或信号处理。VMIN=1, VTIME=0是阻塞读的经典组合,read会一直等直到至少有1字节到达。如果你不想让读线程无限阻塞,可以用select加超时,或者设VMIN=0, VTIME=10表示最多等1秒。

参数说明:NMEA数据是文本流,没有固定的报文边界,靠$或者换行符来分隔。这就带来粘包和半包问题——一次read可能读到半条语句,或者一个read里包含了多条完整语句。常见做法是维护一个环形缓冲区或者动态拼接缓冲,每次读到数据先追加到buffer里,然后按换行符把完整行切出来处理,不完整的行留在buffer等待下次数据。

我一般会再加一层保险:解析时校验行首$和末尾的NMEA校验值(*XX格式的异或和)。有些模块输出会包含\r\n,切行时要过滤掉\r,否则字符串比较时坑你半天。资料源码里这个缓冲区管理逻辑写得很清楚,直接复用没问题。

4. JNI桥接:回调注册与NMEA数据上抛

4.1 JNI层和HAL层怎么完成对接

JNI层是整个链路里最容易被看得云里雾里的部分。GpsLocationProvider这个Java类定义了一些native方法,比如native_init、native_start、native_stop,JNI目录下的C++文件负责实现这些native方法。核心逻辑是:在native_init里调用HAL的gps_init,传入一个JNI回调函数指针;然后把HAL的GpsInterface指针存起来,后续native_start、native_stop都是直接调这个指针对应的函数。

回调那边有点绕——HAL层C代码调用location_cb时,参数是一个GpsLocation*结构体指针,但JNI的location_cb要做的不是直接使用这个指针,而是把它转换成Java对象,再调用Java层的方法把位置对象上抛。

// JNI层 location_cb 实现示意 static void location_callback(GpsLocation* location) { JNIEnv* env = NULL; // 从线程局部存储里拿JNIEnv,不能缓存全局的JNIEnv // 因为不同线程的JNIEnv不通用 JavaVM* vm = g_jvm; vm->AttachCurrentThread(&env, NULL); // 确保类和方法ID已缓存,避免每次查找性能损耗 jclass clazz = g_location_class; jmethodID method = g_location_cb_method; // 将C结构体转成Java对象 jobject locObj = env->NewObject(g_location_class, g_location_init_method); env->SetDoubleField(locObj, g_latitude_field, location->latitude); env->SetDoubleField(locObj, g_longitude_field, location->longitude); env->CallVoidMethod(g_provider_object, method, locObj); vm->DetachCurrentThread(); }

逻辑说明:AttachCurrentThread这步是关键——HAL层的读线程是C/C++线程,不在Java虚拟机管理范围内,要调用Java对象必须先把线程attach到JVM上。attach后拿到的JNIEnv*是线程私有的,所以JNI层通常会在启动时保存JavaVM*全局指针(g_jvm),在回调里通过它attach。

参数说明:g_provider_object这个全局引用指向Java层的GpsLocationProvider实例,需要在native_init时通过NewGlobalRef创建一次。这里有个内存管理的坑:如果你在回调里用NewLocalRef而不是提前缓存全局引用,高频率定位回调会疯狂创建局部引用,导致JNI局部引用表溢出,系统直接crash。资料建议的做法是在native_init阶段就把类、方法、字段的ID全部缓存好,回调里只用ID不做查找。

4.2 三种常见回调线程模型对比

GPS HAL层的回调可以跑在三种不同的线程模型下,这份资料里对三种都做了源码级说明。第一种是串口读线程直接回调,实现最简单但定位回调可能被串口I/O阻塞拖慢;第二种是独立回调线程配一个队列,HAL层读线程把GpsLocation放进队列,回调线程从队列取数据再上抛,解耦效果好但多一次拷贝;第三种是直接用looper机制介入Native层消息循环,这在较新版本Android上用得更多,但要处理的消息类型更多。

我在车载项目里用的是第二种,因为GPS模块输出的频率通常是1Hz到10Hz,队列长度配32个足够,回调线程block在管道或者条件变量上,有数据才唤醒。队列的好处是串口读线程不会被JNI调用的耗时卡住——JNI里CallVoidMethod如果触发Java层做重逻辑(比如写数据库),串口读线程会被拖住,后续NMEA数据可能积压在串口驱动缓冲区里丢失。

资料里有坑提醒:如果使用队列,要处理数据覆盖策略。定位数据可以丢旧保新,但Status状态数据最好保旧丢新,因为状态变化(比如冷启动、热启动)是低频但重要的事件,被挤掉会导致上层状态错乱。这个细节资料里专门写了一小节,我十分认同。

4.3 JNI注册表与符号导出

JNI方法注册有两种方式:静态注册靠Java_包名_类名_方法名的符号命名让虚拟机自动查找;动态注册在JNI_OnLoad里调用RegisterNatives手动建立映射。资料里的源码用的是动态注册,这是主流做法,因为方法名改动时不用改C函数名,而且注册表集中在一个数组里,一眼看全所有绑定关系。

// JNI动态注册表示意 static JNINativeMethod g_methods[] = { { "native_init", "()V", (void*)android_gps_GpsLocationProvider_init }, { "native_start", "()Z", (void*)android_gps_GpsLocationProvider_start }, { "native_stop", "()V", (void*)android_gps_GpsLocationProvider_stop }, { "native_cleanup", "()V", (void*)android_gps_GpsLocationProvider_cleanup }, }; 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 clazz = env->FindClass("com/android/location/provider/GpsLocationProvider"); if (clazz == NULL) { return JNI_ERR; } // 注册绑定,checkVersion确保方法签名匹配 if (env->RegisterNatives(clazz, g_methods, sizeof(g_methods) / sizeof(g_methods[0])) < 0) { return JNI_ERR; } return JNI_VERSION_1_6; }

逻辑说明:JNI_OnLoad是so被加载后自动调用的入口,在这里完成所有native方法注册。JNINativeMethod结构体里的三个字段分别是:Java方法名、方法签名、C函数指针。方法签名里的()V代表无参数返回void,()Z代表无参数返回boolean,写错签名注册会失败,运行时NoSuchMethodError,这个报错能排查半天。

参数说明:JNI_OnLoad里必须返回JNI_VERSION_1_6,这是告诉虚拟机当前so支持的最低JNI版本。不同版本的Android对JNI版本的要求不同,但返回1_6是兼容性最好的选择。如果返回的版本号高于虚拟机支持的版本,加载会直接失败,错误信息在logcat里还看不出来,容易误判成so没编译进去。

5. 排查手册:GPS驱动常见的六个翻车现场

5.1 串口权限被SELinux挡住

现象:HAL层open串口设备节点返回Permission denied,但明明root用户能正常读写,代码逻辑也没问题。在Android系统上尤其常见,因为SELinux强制访问控制会拦截非system域进程访问/dev/ttyS*这类节点的行为。

原因:SELinux策略里没有为GPS相关进程或服务域定义tty_device的读写权限。GPS HAL通常是跑在system_server进程里,或者作为独立vendor进程运行,这两者的SELinux domain不同,权限规则也不同。

解决:在device/xxx/sepolicy目录下补充te规则,比如定义allow system_server tty_device:chr_file rw_file_perms,或者为GPS专用进程创建单独的domain并赋权。改完需要重新编译boot.img或者vendor.img,单独push sepolicy文件不生效。这条经验我从某车载平台的AOSP 9.0开发中得来,折腾了两天才定位到SELinux头上。

5.2 JNI回调崩溃在高频定位场景

现象:定位频率开到10Hz后,跑几分钟到几十分钟不等,出现JNI DETECTED ERROR IN APPLICATION,程序直接crash,logcat里能看local reference table overflow字样。

原因:JNI局部引用表默认容量有限(通常是512个左右),高频回调里如果NewObject创建的对象没有及时被DeleteLocalRef释放,或者对象被存进全局引用一直累积,表就会被撑爆。特别是4.x和部分5.x版本的Android,查错机制还比较原始,只会给出一句笼统的overflow提示。

解决:在回调循环里每次用完job对象后手动DeleteLocalRef,或者在native_init阶段就用GetFieldID缓存字段位置,直接改GpsLocation对应Java对象的字段而不是反复创建新对象。我后来改成复用单个Java对象、只更新字段值的方案后,连续跑48小时没再崩过。

5.3 串口数据乱码或丢字节

现象:从串口读出来的NMEA语句偶发乱码,$GP开头变成$G缺失或者中间夹杂不可见字符,解析函数直接返回-1跳过,定位成功率断崖式下跌。

原因:多数情况是波特率不匹配、串口线过长导致信号质量差,或者模块的TXD和主板的RXD之间存在电平不匹配(TTL和RS232混接)。少部分是串口驱动dts里未正确配置流控或FIFO触发阈值。

解决:第一步把波特率固定成模块手册标称的数值(9600或115200),不要自动探测。第二步检查硬件原理图,确认模块接口电平是1.8V还是3.3V,配错电平虽然不会烧芯片但会导致数据采样错位。第三步在软件层加宽松解析——只要行首是$且第3个字符是P或G(兼容$GPGGA和$BDGGA),就尝试解析,解析失败不报错,等下一行。这个容错策略在实际路测中把定位成功率从84%提到了97%。

5.4 史诗级翻车:GPS模块没输出但系统自测通过

现象:HAL层编译通过、JNI注册正常、logcat里能看到GpsInterface初始化日志,但串口上就是没有任何NMEA数据,模块的PPS灯也不闪。更迷惑的是拿到PC上用USB转串口工具又能收到数据,说明模块本身是好的。

原因:主板上的GPS模块供电引脚没使能,或者模块的复位引脚被拉低。很多模块的VCC需要软件通过GPIO控制,不是常供电;还有模块的RESET_N引脚在启动时要求先拉低再释放,时序不对模块就卡在上电自检阶段。

解决:查原理图里模块的供电GPIO和复位GPIO,在HAL层或者更早的bootloader里把它设为输出高。这个坑最坑人的地方在于系统层所有日志都是正常的,因为软件链路没报错,只是硬件没跑起来。我后来给自己定了一条铁律:任何GPS模块接入新板子,第一件事用示波器量VCC引脚电平,低于标称值90%直接查电源设计,别在软件里瞎调。

5.5 时间戳上抛错误导致定位结果偏差

现象:定位到了,但回调出来的时间和本地时间差了8个小时,或者时间完全不对(比如1970年),地图显示定位点是准的但时间轴全乱。

原因:NMEA的GGA语句里时间字段是UTC时间,格式是hhmmss.sss,需要转换成当地时区。资料里的utc_to_epoch函数就是干这个的,但如果系统时区设置的是GMT+08:00且代码里没做时区偏移补偿,就会出现整8小时偏差。另一个更隐蔽的问题:很多模块输出的时间字段是00到23的小时,但有些老模块会输出24:xx这种格式表示跨天,解析函数没处理就会算出一天前的日期。

解决:解析时先判断小时字段是否等于24,如果是就减24并给日期加一天;然后在JNI层用TimeZone.getDefault()获取系统时区偏移量,加到UTC时间戳上。资料里给了一个处理跨天逻辑的参考实现,直接从$GPRMC语句的日期字段(ddmmyy格式)同步更新日期,避免只用GGA语句导致日期不同步。

5.6 编译通过但运行时报符号找不到

现象:HAL层so编译正常,但运行时logcat报dlopen failed或者cannot locate symbol "gps_get_extension",这个符号在源码里明明定义了。

原因:编译链接时没把对应的源文件编进去,或者函数被编成了C++符号(带mangling)而HAL反射查找按C符号规则去找,导致符号名对不上。很多新手会把#include "gps.h"写在.cpp文件里但没加extern "C"包裹,C++编译器把函数名修饰成_Z13gps_get_extensionv之类的,HAL框架用dlsym查gps_get_extension自然失败。

解决:检查Android.bp或Android.mk里的源文件列表,确认所有实现文件都被编进so。然后查看编译产物里的符号表:nm -D libgps.so | grep gps_get_extension,如果输出的是_Z...开头,说明是C++符号,需要在头文件和实现处都加extern "C"才能真正导出C接口。这个经验我是在调试某厂方案的GPS HAL时悟到的,当时查了两天logcat,最后用nm命令一眼定位。

6. 验证与进阶:固化一套驱动自检流程

拿到这份资料后,不要急着改自己的代码,先按下面的清单验证环境,确保链路本身是通的,再谈进阶。第一步是确认串口设备节点存在且权限正确,ls -l /dev/ttyS*看看当前用户有没有读写权限;第二步是验证模块数据输出正常,可以在HAL层的读线程入口加一个临时日志,把读到的原始NMEA打印到logcat;第三步是用Android自带的LocationManager测试应用(adb shell cmd location也是好办法)触发定位,观察logcat里是否有location_cb被调用的痕迹。

进阶技巧里最实用的是把NMEA原始数据透传到Java层。资料源码里保留了一个nmea_cb回调,正常情况下框架层不会处理原始NMEA,但你在调试时可以在JNI层把这份数据通过SystemProperties或者一个调试socket转发出来,用PC上的串口工具实时对比。这个做法能快速定位是HAL层解析问题还是上层框架问题——如果gpsd日志里能看到完整NMEA但框架层没有location出来,那问题一定出在解析或结构体填充。

最后有个验证小技巧:用假GPS数据注入函数inject_location,绕过HAL层,直接在JNI层喂一个固定的经纬度给框架层。如果上层能正确显示这个假位置,说明JNI及其上层链路全通,剩下的问题全在HAL层和硬件,排查面瞬间砍掉一半。这个习惯我现在每次调试GPS都强制走一遍,先注入假数据验证软件栈,再连真机验证硬件链路,定位问题的时间从按天算缩短到按小时算。从那以后,我每次拿到新GPS模块,第一件事就是跑这套注入流程,确认软件链路活了再上示波器量硬件。希望帮到你。

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

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

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

立即咨询