简介:一套面向GPS Android底层驱动开发的完整学习资料,适合驱动工程师、嵌入式开发者及 Android 系统学习者深入理解 JNI 与 HAL 层工作原理。资源共5个文件,zip压缩包大小仅9.28MB,包含2份Word文档、1个RAR压缩包及 Android.mk 与 C 源码,兼顾原理文档与可编译源码。内容覆盖 GpsInterface、GpsLocationProvider、GpsStatus 等核心模块,并结合 JNI 函数实现和 HAL 层硬件交互流程,详细讲解 GPS 定位数据的获取、解析与上报,以及初始化、设置参数、启动定位等关键操作,同时梳理了从 Java 层到底层硬件的完整调用链与生命周期管理。文档中还有基于 Android 2.3 的 GPS HAL 移植分析,能够提供实际移植与调试中的排错思路,并涉及 ndk-build 编译和动态链接库生成环节;此外,资料对 GpsStatus 的状态反馈、GpsNavigationMessageCallback 的导航消息处理也做了说明,有助于上层应用与底层硬件的衔接。已有832人浏览学习,适合想从源码层面掌握 GPS 驱动架构并提升定位调试能力的开发者。 上个月在产线碰上一个挺典型的case:一批新组装的板子烧完固件进系统,地图能打开,但定位图标一直是灰的。硬件说天线焊接没问题,软件说驱动已经正常加载,两边僵了两天。最后我拿串口调试工具怼到GNSS芯片的输出脚上,发现UART收到的数据全是"GIFGIFGIF"这样的乱码——波特率被初始化成115200,而GPS芯片出来的是9600。就这么一个底层配置问题,整套GPS Android底层驱动看起来"没毛病",实际上数据根本没法用。
这个经历很能说明问题:GPS定位不准、定位慢、偶尔掉星,很多人第一反应是算法问题或者环境问题,但真正干过底层BSP的人都知道,八成以上的"玄学定位故障",根子都在驱动链路里。这篇文章就把我整理的GPS Android底层驱动资料整理成文,附上关键源码,从内核gnss子系统、HAL层到framework层的完整数据通路一次讲清楚,重点聊聊底层驱动是怎么影响定位误差、偏航和TTFF的。
1. 从硬件到App:一条定位数据的完整链路
1.1 GNSS芯片、天线与AP的三种连接方式
主控SoC和GNSS接收芯片之间,物理连接方式就三种,搞清楚这个才能看懂驱动往哪写。
- UART串口:最常见。GPS芯片的TX/RX直接接到AP的某个UART控制器上,典型波特率9600、38400或115200。以前的老方案(比如高通8x09平台外挂u-blox)几乎全是这种。优点是驱动简单、调试方便,缺点是数据带宽有限,NMEA 0183协议每秒也就几条语句,完全够用。
- I2C/SPI:少部分低功耗方案用,比如博通BCM4774早期型号在部分手表方案上走I2C,数据量小,但容易受总线调度影响。
- USB:多见于外置GNSS接收机,比如树莓派上直接插一个USB GPS模块。驱动侧就是一个USB串口设备,内核自动枚举成ttyUSB0。
射频天线的连接方式同样值得注意。手机和平板几乎都用有源天线,天线端集成了一级LNA(低噪声放大器),需要主控提供一路电源(通常是3.3V或1.8V,经RF电感馈入)。BSP工程师在设备树里漏配这路电源,或者GPIO没拉起来,结果就是信号从天线进来就衰减得没法看,信噪比长期在20dB以下。
1.2 从内核到App的四层软件架构
Android里一条定位数据从卫星到地图App,要经过四层:
| 层级 | 路径 | 核心职责 |
|---|---|---|
| 内核层 | drivers/gnss 子系统、tty/serdev 驱动 | 接收GNSS芯片的NMEA原始数据,向用户空间暴露 /dev/gnss0 设备节点 |
| HAL层 | hardware/interfaces/gnss(新)/ hardware/libhardware/modules/gps(旧) | 对接设备节点,解析NMEA语句,向上提供GnssLocation、GnssMeasurement等结构化数据 |
| Framework层 | GnssLocationProvider → LocationManagerService | 策略调度、AGPS辅助数据下发、位置计算与融合(GPS/WiFi/基站) |
| 应用层 | 地图、GPS Test等 | 消费位置结果,展示卫星状态、精度因子等 |
内核gnss子系统是Linux 4.10以后才合入mainline的,早期的Android方案压根没有这个,都是厂商自己写个tty驱动,再在HAL里open("/dev/ttyS1")。现在的新平台,无论高通、MTK还是展锐,基本都是走内核标准gnss框架,驱动代码量能省一大半。
这里有个容易混淆的点:内核层只负责数据搬运,不解析NMEA。GPS芯片输出的$GPGGA、$GPRMC这些语句,在内核驱动里就是个字节流,把它交给用户空间完事。真正的NMEA解析和定位解算,在HAL层和framework层。所以如果你的驱动把字节读丢了、读乱了,上层解析全跟着乱。
2. 内核层驱动:gnss-serial源码拆解
2.1 设备树节点配置
以典型的串口型GNSS芯片为例,设备树节点长这样:
/dts-v1/; / { model = "GNSS Test Board"; compatible = "vendor,gnss-board"; &uart3 { status = "okay"; pinctrl-names = "default"; pinctrl-0 = <&uart3_pins>; current-speed = <9600>; gnss { compatible = "gnss,serial"; vcc-supply = <&pmic_ldo16>; vin-supply = <&pmic_ldo5>; }; }; };关键就三件事:UART使能、波特率对、供电给上。很多新人的坑是只改了status = "okay",忘了配current-speed,结果内核gnss-serial驱动probe时用了不匹配的波特率,跟GNSS芯片实际输出不一致,读上来的全是乱码。这个跟我开头说的产线case一模一样。
2.2 probe函数里必须做对的四件事
内核里的drivers/gnss/serial.c是标准实现,核心probe逻辑大概是这个框架:
static const struct of_device_id gnss_serial_of_match[] = { { .compatible = "gnss,serial" }, {}, }; MODULE_DEVICE_TABLE(of, gnss_serial_of_match); static int gnss_serial_probe(struct serdev_device *serdev) { struct gnss_serial *gserial; int ret; gserial = kzalloc(sizeof(*gserial), GFP_KERNEL); if (!gserial) return -ENOMEM; serdev_device_set_drvdata(serdev, gserial); gserial->serdev = serdev; /* 第1件事:波特率必须和GNSS芯片输出一致 */ serdev_device_set_baudrate(serdev, 9600); /* 第2件事:流控关闭,大多数GPS芯片不支持RTS/CTS */ serdev_device_set_flow_control(serdev, false); /* 第3件事:注册serdev接收回调 */ ret = serdev_device_open(serdev); if (ret) goto err_free; serdev_device_set_client_ops(serdev, &gnss_serial_ops); /* 第4件事:注册gnss设备,向用户空间暴露 /dev/gnss0 */ gserial->gdev = gnss_register_device(&gserial->gdev); if (IS_ERR(gserial->gdev)) { ret = PTR_ERR(gserial->gdev); goto err_close; } return 0; err_close: serdev_device_close(serdev); err_free: kfree(gserial); return ret; }四件事的顺序有讲究。波特率必须最先设,否则上电后第一包NMEA数据就可能因波特率不匹配丢掉;流控也必须关掉,因为很多GNSS模块的引脚定义里压根没有RTS/CTS,开启了反而会一直处于流控等待状态。
2.3 NMEA数据从天线到内核的流动
数据流方向是这样的:
GNSS天线 → 射频前端 → 基带芯片(定位解算)→ UART TX → SoC UART RX FIFO → DMA搬运 → tty/serdev层 → gnss核心层 → /dev/gnss0 字符设备 → HAL (libgnss) → NMEA解析 → GNSS定位结果容易丢包的环节有三个:
- UART RX FIFO溢出。芯片默认FIFO深度16字节,NMEA一条语句最长82字节。如果DMA没有及时把FIFO搬走,DMA或中断优先级低,FIFO就会覆盖,表现是语句中间缺一段,HAL解析GGA时校验不过。
- 休眠唤醒间隙。AP进入低功耗模式,UART时钟被关闭或降频,唤醒瞬间正好有数据来,就丢了。
- serdev vs tty的接收竞争。如果内核同时注册了tty端口和serdev客户端,两边抢数据,会出现数据重复或错位。
所以,拿到一个"定位时好时坏"的bug,别急着怀疑算法,先看cat /dev/gnss0能不能连续、完整地吐出NMEA。能,问题在上层;不能,问题在驱动或硬件。
3. 定位误差、TTFF与信噪比:哪些锅在底层驱动
3.1 误差来源的逐层拆解
定位误差从来不是单一原因,我习惯按层拆:
| 误差来源 | 典型原因 | 排查方法 |
|---|---|---|
| 天线侧 | 有源天线供电不足、天线增益低、天线位置遮挡 | 看信噪比,低于25dB基本是天馈问题 |
| 射频前端 | PCB走线损耗大、晶振频偏 | 用信号模拟器做射频指标测试 |
| 底层驱动 | 波特率错、丢字符、休眠唤醒丢数、时间戳不准 | 原始NMEA报文连续性检查 |
| 环境多径 | 城市峡谷、室内环境信号反射 | HDOP值明显偏高,卫星多但精度差 |
| 星历数据 | AGPS辅助数据过期、星历未下载 | 冷启动TTFF异常长,超过60秒 |
注意一个反直觉的现象:卫星数量多不等于定位精度好。如果收到的12颗星里有8颗是低仰角反射信号,HDOP照样是3以上,位置照样飘。底层驱动能做的,是保证每颗卫星的原始测量值能完整、实时地上报给上层,让上层有机会用算法剔除坏值。
3.2 HDOP、信噪比和定位状态:三个最有用的指标
在Android里调试GPS,不需要一上来就看日志,先看三个指标:
- 信噪比(CN0,单位dBHz):正常开阔环境下,30~50dBHz。低于20基本锁定不了星,低于30定位精度会明显变差。
- HDOP(水平精度衰减因子):1~2优秀,2~5一般,超过5说明卫星几何分布很差,这时候即使能定位,误差也大得离谱。
- GGA语句第6字段(定位质量指示):0=无效,1=单点定位,2=差分定位。
底层驱动如果丢字符,信噪比和HDOP倒不会直接错,但一定会表现为:明明卫星很多,第一次定位就是出不来,或者定位后频繁跳回0(无效)。
3.3 冷启动、热启动与AGPS辅助数据在底层的作用
TTFF(首次定位时间)是终端用户最能感知的GPS体验指标:
- 冷启动:设备完全没有星历和位置参考,要从零搜索卫星、下载星历,通常20~60秒,AGPS辅助下可降到10秒以内。
- 热启动:星历还在有效期内,重新捕获很快,1~5秒。
- 温启动:介于两者之间。
底层驱动在TTFF上的角色很纯粹但极关键:必须保证从GNSS芯片出来的每一条NMEA和原始测量值都没有被拖延、丢弃、篡改。特别是framework层下发AGPS辅助数据后,芯片会立刻输出一批缩短TTFF的信息(如GSA里带的新星历ID),如果此时驱动的接收线程优先级低、被其他中断打断,这批数据没及时读完,芯片就进入超时等待,TTFF被拖得很长。
3.4 WiFi定位与GPS定位的根本差异
这个也是评论区常问到的问题。两者根本区别在"有没有主动测距":
| 维度 | GPS定位 | WiFi定位 |
|---|---|---|
| 定位原理 | 接收卫星信号,测量伪距,三边测量解算 | 扫描周围AP的BSSID,比对指纹数据库 |
| 精度 | 开阔环境5~10米 | 城市/室内20~100米 |
| 功耗 | 较高(射频常开) | 较低 |
| 适用场景 | 室外开阔地 | 室内、高楼密集区 |
| 对天线的要求 | 高,需要有源天线或至少增益够的无源天线 | 低,一般的WiFi天线即可 |
对底层开发的影响在于:WiFi定位不涉及GNSS驱动,问题域简单得多。而GPS定位一旦出问题,你面对的是从天线到framework的整条链路。
4. 三边测量算法:底层原始数据如何变成坐标
4.1 从伪距到位置解算的数学本质
三边测量(Trilateration)的直观理解是:已知三颗卫星的坐标((x_i, y_i, z_i))和接收机到卫星的距离(r_i),那么接收机一定在以卫星为球心、(r_i)为半径的球面上。三颗卫星的三个球面相交于两个点,再结合地球椭球面约束,就能唯一确定接收机位置。
实际工程中,每个距离(r_i)都被接收机时钟误差污染,形成"伪距"。所以未知数是四个:位置x、y、z和接收机钟差(\Delta t)。方程是:
[ \sqrt{(x-x_i)^2+(y-y_i)^2+(z-z_i)^2} + c \cdot \Delta t = \rho_i ]
四颗卫星刚好四个方程,但噪声下无唯一解,工程上一般都超定,用最小二乘法迭代求解。底层驱动不需要实现这个算法,但framework层的GnssLocationProvider要跑到位置解算,依赖的底层输入有两个:一是每个卫星的原始伪距(来自GnssMeasurement),二是卫星星历/历书(来自芯片内部或AGPS)。
4.2 HAL与Framework的分工
Android新版本的GNSS HAL接口(android.hardware.gnss@2.x)会把两类数据往上送:
GnssLocation:芯片自己解算出的经纬度、海拔、速度、bearing。多数GNSS芯片基带内部已经做了解算,framework拿到的直接就是位置。这种模式下,三边测量算法其实跑在芯片内部固件里,Android端只是个消费方。GnssMeasurement:原始伪距、载波相位、多普勒频移、C/N0等原始观测量。framework或上层算法(比如RTKLIB等后处理软件)可以用这些数据自己解算位置,达到更高精度(比如载波相位差分)。做车载高精度定位的经常用这套。
所以,从底层驱动视角看,AGNSS的位置解算跑在芯片固件里,Android framework更多是策略和融合角色。驱动层的核心任务不是"算",而是把芯片算出来的结果和芯片要吃的辅助数据,完整、低延迟地搬到位。算法再好,原始数据进不来或损坏了,出来的坐标也是灾难。
4.3 驱动层的错误为什么会让算法失效
举三个真实场景:
- 串口丢了一个字节,导致GGA行的校验和错误。HAL层解析校验失败,直接丢弃这帧数据,位置就少更新一次。
- 内核上报时间戳用的不是GNSS芯片的事件时间,而是AP收到数据那一刻的中断时间。两者差几十毫秒,对单点定位可能只造成几米误差,但对需要精准时间戳的原始测量值(GnssMeasurement),这就是致命的。
- UART FIFO溢出后,芯片重发了一帧,但此时framework已经进入超时状态,重新发起AGPS辅助数据请求,TTFF被往复拉长。
5. 实战排查:从一串日志锁死问题根因
5.1 抓取GPS全链路日志的手段
我调试GPS问题时,会一路同时开四个窗口:
# 窗口1:看内核gnss驱动是否正常 adb shell dmesg | grep -i gnss adb shell ls -l /dev/gnss0 # 窗口2:实时读原始NMEA(需root) adb root adb shell cat /dev/gnss0 # 窗口3:看framework层GNSS日志 adb logcat -s GnssLocationProvider:V GNSS:V # 窗口4:看HAL层(如果有debug开关) adb shell setprop persist.vendor.gnss.debug 1 adb logcat -s gpsd:Vcat /dev/gnss0是最粗暴也是最有效的一招。如果cat出来的NMEA语句间隔均匀、校验和全部正常,内核驱动基本没问题;如果出现半行、乱码、超过2秒没数据,驱动这边跑不掉。
5.2 读懂一条GGA报文
$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47分段拆开看:
| 字段 | 示例值 | 含义 |
|---|---|---|
| 语句ID | GPGGA | 全球定位系统固定数据 |
| UTC时间 | 123519 | 12:35:19 |
| 纬度 | 4807.038N | 北纬48度07.038分 |
| 经度 | 01131.000E | 东经11度31.000分 |
| 定位质量 | 1 | 1=单点定位,0=无效 |
| 卫星数 | 08 | 使用的卫星数量 |
| HDOP | 0.9 | 水平精度因子,优秀 |
| 海拔 | 545.4,M | 海拔545.4米 |
定位质量字段是0,说明接收机没定位;是1但有HDOP大的情况,说明定位了但精度差;卫星数很多却一直显示0,就要高度怀疑底层驱动丢数据或者天线信号有问题。
5.3 我踩过的三个底层驱动坑
这里把这些年遇到的高频问题列一下,都是能直接抄作业的方向:
坑1:波特率不对导致乱码
现象:cat /dev/gnss0输出GIFGIFF这样的乱码,或者完全空白。 根因:设备树里current-speed和GNSS芯片实际波特率不一致,或者GNSS芯片固件被重新配置成别的波特率。 解决:先在芯片端确认默认波特率(多数是9600),改设备树current-speed;如果芯片支持命令改波特率(如u-blox的CFG-PRT),需要在上层HAL端先发协议命令同步。
坑2:有源天线供电GPIO没拉起来
现象:开阔室外,卫星只搜到2~3颗,信噪比15~20dBHz,定位状态始终0。 根因:设备树里漏配了天线LNA的供电GPIO,或者PMIC的ldo没有设成always-on。 解决:用示波器量天线焊点的电压,确认3.3V存在;检查GPIO的pinctrl配置,别只设status = "okay",还得把GPIO拉高。
坑3:休眠唤醒后DMA收不到数据
现象:待机一晚上,唤醒后地图能开但定位图标灰很久,重启App或开关飞行模式才恢复。 根因:底层串口在suspend时关了DMA通道,resume后DMA没有正确恢复,UART RX FIFO溢出后数据全丢。 解决:在驱动的resume回调里重新初始化DMA描述符,或者干脆在suspend时保持UART时钟不关(代价是功耗高一点)。这个问题的排查思路是:唤醒后立刻cat /dev/gnss0,发现没有任何输出,而模块本身还在正常工作(可以用示波器确认模块TX脚有没有波形)。示波器有波形但系统读不到,十有八九就是DMA或时钟恢复的问题,重点往resume路径里找。
6. 没有GPS信号怎么测:信号模拟器与低成本调试方案
6.1 信号模拟器在研发和产线的正确用法
在室内开发测试时,最头疼的是收不到真实卫星信号,尤其是做产线全功能测试,总不能把产线搬到天台。GPS信号模拟器就是干这个用的:它从天线口注入一路模拟的GPS/GNSS射频信号,不依赖真实卫星,就能让被测设备认为自己收到了完整的卫星群。
这类设备(比如思博伦Spirent GSS系列、Rohde & Schwarz的SMBV100B等)可以配置卫星数量、星座、多径场景、信号强度,甚至模拟车辆在路上移动的场景。研发阶段用它做回归测试,可以稳定复现"今天定位准、明天不准"的玄学问题;产线阶段配合自动化测试程序,几分钟就能判定一台整机的GPS接收链路是否正常。
有几个测试细节值得注意:
- 信号衰减校准:模拟器输出到设备天线口之间通常要加衰减器,保证注入功率在-130dBm左右(接近真实信号强度)。不加衰减直接注入,芯片会饱和,反而解不出位置。
- 场景文件复用:把标准测试场景(比如10颗GPS卫星、HDOP<2、静止状态)保存成场景文件,产线和研发共用同一套基线,用统一的TTFF和定位误差标准卡板。
- 多星座测试:只测GPS是不够的。现在手机基本都支持北斗和格洛纳斯,产线测试场景里至少要配GPS+BD双星座,避免某些射频通道设计缺陷漏测。
6.2 低成本的开发替代方案
信号模拟器动辄几十万,个人开发者或者小团队不可能都配。我的替代方案是树莓派3B+加一个U-blox NEO-M8N模块,总共也就一两百块钱。树莓派的UART串口(ttyAMA0)接M8N模块,一边跑个小的NMEA转发脚本,一边用GPS Test去验证Android端的逻辑。缺点是没有射频前端,无法真正测灵敏度,但开发调试底层数据通路、验证NMEA解析逻辑完全够用。
如果必须在室内给开发机注入"可以定位"的信号,还有一个折中办法:买一个室内GPS转发器(antenna repeater),把室外的真实信号转发进来。注意转发器需要贴窗放接收天线,并且增益不能太大,否则前端饱和后一样定位失败。这个方案成本几百块,效果虽然达不到产线标准,但日常联调足够。
最后再分享一点个人经验
做了这么多年BSP,我最大的体会是:GPS定位问题不要一上来就怀疑算法和天线,先把原始NMEA链路打通再说。cat /dev/gnss0能看到连续、校验正确的数据,就已经解决了80%的"假定位问题"。产线那边出现过最离谱的case,是波峰焊温度把GNSS芯片附近的晶振烤到频偏,定位误差飙到五十米,但NMEA链路一切正常。这种问题靠驱动是修不好的,得靠产线测试站的信号模拟器加上定位精度判据才能筛出来。所以,底层驱动的交付标准不只是"能读到数据",还要能证明"读到的数据是对的、连续的、时间戳是可靠的"。能做到这一步,GPS调试里的那些玄学,大多就变成明牌了。
本文还有配套的精品资源,点击获取