Android GPS HAL 驱动移植实战:UART 串口 NMEA 解析与 3 大关键接口实现

📅 2026/7/11 22:17:52 👁️ 阅读次数 📝 编程学习
Android GPS HAL 驱动移植实战:UART 串口 NMEA 解析与 3 大关键接口实现

Android GPS HAL 驱动移植实战:UART 串口 NMEA 解析与 3 大关键接口实现

在 Android 系统开发中,GPS 功能是许多应用场景的核心需求。本文将深入探讨如何为通过 UART 串口输出 NMEA 数据的 GPS 模块编写 HAL 层驱动,重点解析 NMEA 数据格式,并实现与 Android 框架对接的 3 个关键接口。

1. GPS HAL 驱动开发基础

Android 的硬件抽象层(HAL)是连接底层硬件和上层框架的桥梁。对于 GPS 模块,HAL 层主要负责:

  • 初始化 GPS 硬件设备
  • 解析 NMEA 数据流
  • 通过回调接口向框架层上报位置信息
  • 管理 GPS 工作状态

典型的 GPS HAL 驱动需要实现以下核心结构体:

typedef struct { size_t size; int (*init)(GpsCallbacks* callbacks); int (*start)(void); int (*stop)(void); void (*cleanup)(void); int (*inject_time)(GpsUtcTime time, int64_t timeReference, int uncertainty); int (*inject_location)(double latitude, double longitude, float accuracy); void (*delete_aiding_data)(GpsAidingData flags); int (*set_position_mode)(GpsPositionMode mode, GpsPositionRecurrence recurrence, uint32_t min_interval, uint32_t preferred_accuracy, uint32_t preferred_time); const void* (*get_extension)(const char* name); } GpsInterface;

提示:Android 不同版本对 HAL 接口的要求有所差异,Android 7.x 使用传统的 GPS HAL 接口,而 Android 9+ 则使用 GNSS HAL 接口。

2. UART 串口通信实现

大多数 GPS 模块通过 UART 串口与主控芯片通信,典型配置如下:

参数典型值说明
波特率9600/115200需与 GPS 模块设置一致
数据位8
停止位1
校验位
流控

在 HAL 驱动中打开串口的基本流程:

int open_serial_port(const char *port, int baudrate) { int fd = open(port, O_RDWR | O_NOCTTY | O_NDELAY); if (fd < 0) { ALOGE("Cannot open %s: %s", port, strerror(errno)); return -1; } struct termios options; tcgetattr(fd, &options); cfsetispeed(&options, baudrate); cfsetospeed(&options, baudrate); options.c_cflag |= (CLOCAL | CREAD); options.c_cflag &= ~PARENB; options.c_cflag &= ~CSTOPB; options.c_cflag &= ~CSIZE; options.c_cflag |= CS8; options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); options.c_oflag &= ~OPOST; tcsetattr(fd, TCSANOW, &options); return fd; }

3. NMEA 数据解析详解

NMEA 0183 是 GPS 模块普遍采用的标准化数据格式,由美国国家海洋电子协会制定。每条 NMEA 语句以$开头,以\r\n结尾,字段间用逗号分隔。

3.1 常见 NMEA 语句类型

  • GGA- GPS 定位信息
  • GSA- 当前卫星信息
  • GSV- 可见卫星信息
  • RMC- 推荐最小定位信息
  • VTG- 地面速度信息

以 GGA 语句为例解析字段含义:

$GPGGA,082923.00,3901.106815,N,11712.322006,E,1,12,1.0,60.6,M,-4.0,M,,*5A
字段含义示例值
1UTC 时间082923.00
2纬度3901.106815
3纬度方向(N/S)N
4经度11712.322006
5经度方向(E/W)E
6定位质量(0=无效,1=有效)1
7使用卫星数12
8HDOP(水平精度因子)1.0
9海拔高度60.6
10高度单位(M=米)M
11大地水准面高度-4.0
12高度单位M
13差分GPS数据年龄
14差分参考站ID
15校验和*5A

3.2 NMEA 解析代码实现

void parse_nmea(const char *nmea, GpsLocation *location) { if (strstr(nmea, "$GPGGA") != NULL) { char *token = strtok((char *)nmea, ","); int field = 0; double lat = 0, lon = 0; while (token != NULL) { switch (field) { case 1: // UTC时间 location->timestamp = parse_utc_time(token); break; case 2: // 纬度 lat = atof(token); break; case 3: // 纬度方向 if (*token == 'S') lat = -lat; break; case 4: // 经度 lon = atof(token); break; case 5: // 经度方向 if (*token == 'W') lon = -lon; break; case 6: // 定位质量 location->flags |= (*token == '1') ? GPS_LOCATION_HAS_ACCURACY : 0; break; case 9: // 海拔高度 location->altitude = atof(token); location->flags |= GPS_LOCATION_HAS_ALTITUDE; break; } token = strtok(NULL, ","); field++; } location->latitude = convert_to_decimal_degrees(lat); location->longitude = convert_to_decimal_degrees(lon); } }

4. 三大关键接口实现

4.1 初始化接口 (init)

init接口负责注册回调函数,这些回调将在 HAL 需要与框架通信时使用:

static int gps_init(GpsCallbacks* callbacks) { if (callbacks == NULL) { ALOGE("callbacks cannot be NULL"); return -EINVAL; } // 保存回调函数 sGpsCallbacks = *callbacks; // 创建串口读取线程 pthread_create(&sThread, NULL, gps_thread_loop, NULL); return 0; }

关键回调函数说明:

  • location_cb- 上报位置信息
  • status_cb- 上报 GPS 状态变化
  • sv_status_cb- 上报卫星状态
  • nmea_cb- 上报原始 NMEA 数据

4.2 启动/停止接口 (start/stop)

static int gps_start(void) { sGpsState = GPS_STATE_STARTED; // 发送启动命令到GPS模块 const char *cmd = "$PMTK101*32\r\n"; write(sFd, cmd, strlen(cmd)); // 通知框架GPS已启动 if (sGpsCallbacks.status_cb != NULL) { sGpsCallbacks.status_cb(GPS_STATUS_SESSION_BEGIN); } return 0; } static int gps_stop(void) { sGpsState = GPS_STATE_STOPPED; // 发送停止命令到GPS模块 const char *cmd = "$PMTK104*37\r\n"; write(sFd, cmd, strlen(cmd)); return 0; }

4.3 位置模式设置接口 (set_position_mode)

static int gps_set_position_mode(GpsPositionMode mode, GpsPositionRecurrence recurrence, uint32_t min_interval, uint32_t preferred_accuracy, uint32_t preferred_time) { char cmd[64]; // 根据模式生成不同的PMTK命令 switch (mode) { case GPS_POSITION_MODE_STANDALONE: snprintf(cmd, sizeof(cmd), "$PMTK886,%d*2A\r\n", 0); break; case GPS_POSITION_MODE_MS_BASED: snprintf(cmd, sizeof(cmd), "$PMTK886,%d*2A\r\n", 1); break; case GPS_POSITION_MODE_MS_ASSISTED: snprintf(cmd, sizeof(cmd), "$PMTK886,%d*2A\r\n", 2); break; default: return -EINVAL; } write(sFd, cmd, strlen(cmd)); return 0; }

5. Android 版本适配要点

不同 Android 版本对 GPS HAL 的要求有所差异,以下是关键版本的区别:

Android 版本HAL 接口类型主要变化
7.x 及以下Legacy GPS HAL使用传统的 GpsInterface 结构体
8.xGNSS HAL 1.0引入 HIDL 接口,支持更多卫星系统
9.xGNSS HAL 1.1增加测量接口和导航消息接口
10+GNSS HAL 2.0支持低功耗模式和新卫星系统

对于 Android 9+ 设备,还需要在设备 manifest 中添加 GNSS HAL 服务声明:

<hal format="hidl"> <name>android.hardware.gnss</name> <transport>hwbinder</transport> <version>1.1</version> <interface> <name>IGnss</name> <instance>default</instance> </interface> </hal>

6. 调试与测试技巧

在开发 GPS HAL 驱动时,以下调试方法非常有用:

  1. NMEA 模拟器:使用工具如gps-sdr-sim生成模拟 NMEA 数据流
  2. ADB 命令
    adb shell dmesg | grep gps # 查看内核日志 adb logcat | grep -i gps # 查看框架层日志
  3. 权限配置:确保 SELinux 策略允许 HAL 访问串口设备
    allow hal_gps_default vendor_gps_device:chr_file { read write open };
  4. 测试工具
    • gps_test- 命令行测试工具
    • GPSTest- 开源 Android 应用
    • Google 地图 - 验证实际定位效果

在实际项目中,我曾遇到 GPS 模块首次定位时间(TTFF)过长的问题。通过分析发现是 AGPS 数据注入不正确导致的,修正后 TTFF 从 2-3 分钟缩短到 30 秒以内。