news 2026/10/15 1:48:27

RK3566 android12 虚拟GPS定位实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
RK3566 android12 虚拟GPS定位实现

RK3566 android12 虚拟GPS定位实现(不改 App、不 root,第三方定位软件直接可用)

适用:RK3566 / RK356X 平台 Android 12。整机没有 GPS 模组(或者有模组但装在天线收不到星的位置),而系统需要稳定上报一个固定坐标。典型场景:广告机、工控一体机、自助终端、展厅样机、车载终端等固定安装的设备。

0. 先说效果和代价

改完之后的表现:

  • 任意第三方定位 App 直接可用—— 不需要 root、不需要装 mock 框架、不需要开「允许模拟位置」、App 也不需要声明任何额外权限;
  • App 无法从 API 层面分辨—— 因为坐标是从真实的 GNSS provider 链路出来的,不是 Mock Provider,所以Location.isFromMockProvider()返回false;
  • 开机即有定位,不依赖 adb、不依赖任何人工操作(这一点对出货设备是决定性的)。

代价也说清楚:

  • 要改系统源码、要重新编译烧录;
  • 坐标是静态的,不会移动,所以轨迹类应用(运动记录、导航)行为会不正常;
  • 坐标存在persist属性里,恢复出厂设置会被清掉。

和另外三种做法比一下

同样是「让设备上报一个假坐标」,路子差别很大,选错会白干:

方案改动量需要 adb / root开机即生效所有 App 无感可用适用场景
A. 打 GNSS HAL 补丁(本文)改系统源码,要刷机不需要是是出货整机、固定点位设备
B.cmd locationTest Provider(Android 11+ 官方接口)零改代码需要(每次开机配一次)否,重启失效是,但是否被打 mock 标记需按版本实测研发调试、样机演示
C. 开发者选项 + Mock 定位 App零改代码不需要需人工开开关否,会被isFromMockProvider()识别,部分 App 直接拒绝个人测试
D. 在自家 App 里注入坐标改 App不需要是只对这一个 App 有效只调自己应用

B 方案的具体用法(不同 Android 版本子命令名可能有细微差异,以adb shell cmd location -h为准):

adb shell cmd location providers add-test-provider gps_test adb shell cmd location providers set-test-provider-location gps_test--location22.5431,114.0579--accuracy8adb shell cmd location providers set-test-provider-enabled gps_testtrue

为什么出货设备必须走 A:B 依赖 adb,且重启失效 —— 设备到了客户手里,没人会去跑 adb。A 把坐标固化进系统,开机就在。

一、原理:为什么必须从 HAL 这一层下手

Android 的定位链路是分层的:

第三方 App(地图 / 打卡 / 定位测试工具) ↓ LocationManager / LocationManagerService (在这里判断 provider 是否可用、位置是否 mock) ↓ GnssLocationProvider ↓ IGnss HAL 服务(android.hardware.gnss@1.0) ↓ 厂商 HAL 实现 ←── 本文就改在这里 ↓ /dev/ttyXXX(真实 GPS 模组,本方案没有这一步)

关键在于mock 标记是在 HAL 之上、由 Mock Provider 注入时才打上的。一旦跳过 HAL 这一层、直接在厂商实现里产出一个 fix,上层全链路都会把它当成「真实卫星定位」——LocationManagerService看到的只是一个普通的 GNSS 位置更新,没有任何可疑字段。

这就是「实测第三方任意定位软件均可使用」的技术原因,也是这个方案和装 mock App 的本质区别。

下面按顺序做三步:让系统认到 GPS 能力 → 把 HAL 跑起来 → 让 HAL 上报一个固定坐标。

二、第一步:让系统认到 GPS 这个能力

2.1 在 compatibility matrix 里放开 gnss 1.0

Android 12 引入了 VINTF compatibility matrix:系统会校验「设备实际提供的 HAL」是否落在「框架允许的范围」内。矩阵里原本只声明了2.0-1,而 TD-GNSS-HAL 提供的是1.0,所以要把 1.0 加进允许集合。

--- a/hardware/interfaces/compatibility_matrices/compatibility_matrix.6.xml +++ b/hardware/interfaces/compatibility_matrices/compatibility_matrix.6.xml @@ -225,6 +225,7 @@ </hal> <hal format="hidl" optional="true"> <name>android.hardware.gnss</name> + <version>1.0</version> <version>2.0-1</version> <interface> <name>IGnss</name>

2.2 在 manifest.xml 里注册 HAL 服务

matrix 声明的是「允许提供哪些版本」,manifest 声明的是「实际提供了哪些版本」。两个文件必须配套。

<manifestversion="1.0"type="device"><halformat="hidl"><name>android.hardware.gnss</name><transport>hwbinder</transport><version>1.0</version><interface><name>IGnss</name><instance>default</instance></interface></hal></manifest>

⚠️两处的<version>必须一致。matrix 里写了1.0而 manifest 里写别的版本(或反过来),会出现编译期校验失败或服务起不来的问题。这是移植 GNSS 时最常犯的错,改完养成顺手对一遍的习惯。

2.3 声明 GPS feature,并拷贝权限声明文件

android.hardware.location.gps.xml这份文件声明了FEATURE_LOCATION_GPS等定位相关能力。App 调PackageManager.hasSystemFeature(FEATURE_LOCATION_GPS)判断设备有没有 GPS 时,读的就是它。

这一步容易漏:不拷这个文件,即使 HAL 已经正常跑起来、框架层也能拿到坐标,仍然会有一部分 App 认为「这台设备没有 GPS」而直接禁用定位功能。

BOARD_HAS_GPS := true ifeq ($(BOARD_HAS_GPS),true) PRODUCT_COPY_FILES += \ frameworks/native/data/etc/android.hardware.location.gps.xml:$(TARGET_COPY_OUT_VENDOR)/etc/permissions/android.hardware.location.gps.xml endif

用一个BOARD_HAS_GPS开关把这段包起来,是为了同一份 device 配置能同时支持「带 GPS 的机型」和「不带 GPS 的机型」,不用改代码分支 —— 多机型项目建议保留这个写法。

三、第二步:移植 TD-GNSS-HAL

📌待补充—— 这里请把你当时实际移植的步骤和补丁贴进来(源码/so 放哪个目录、PRODUCT_PACKAGES加了哪个 service、配置文件怎么改)。下面是我按通用流程整理的清单,可以对照着检查和补全。

TD-GNSS-HAL 是泰斗(TD1030 系列)GNSS 芯片的 HAL 实现,对外提供android.hardware.gnss@1.0的 HIDL 接口。移植一般涉及四件事:

  1. 放置源码 / 预编译库:按项目规范放到hardware/或vendor/下,注意Android.mk/Android.bp里的模块名和路径要与后面的PRODUCT_PACKAGES对应。
  2. 加入编译:PRODUCT_PACKAGES += android.hardware.gnss@1.0-service(或厂商自带的 service 名),否则烧完机lshal里根本看不到这个服务。
  3. 产品配置:串口节点名、波特率之类的配置项(常见于tdgnss.conf之类的文件)。本文方案虽然不用真实串口,但配置文件里的必要项不能缺—— 如果 HAL 在初始化阶段因为找不到配置/节点而提前return,后面你注入的 fix 永远发不出去,现象就是「代码都改对了但 App 就是没定位」。
  4. SELinux:HAL 进程运行在hal_gnss域,它需要能读取persist.gps.*这几个属性、以及访问它自身需要的节点。属性如果没有在property_contexts里登记,读属性会被策略拦掉。

移植完先验证 HAL 是否真的起来了,再往下改代码:

# 确认 HIDL 服务已注册adb shell lshal|grep-ignss# 看 GNSS 相关日志adb logcat-ball|grep-iE"gnss|gps"# 看 SELinux 拦截adb shelldmesg|grep-iavc

lshal里看不到android.hardware.gnss@1.0::IGnss/default的话,先别动上报代码 —— 问题一定在第 1~4 步里。这一步能省掉大量「改了半天没反应」的排查时间。

四、第三步:改上报流程,从属性里读坐标

改动位置:tdgnss.c的nmea_reader_parse。思路是把「从串口解析 NMEA 报文」换成「从系统属性读经纬度」,其余上报路径完全不动。

4.1 读取自定义属性

charpropbuf_latitude[PROPERTY_VALUE_MAX];property_get("persist.gps.latitude",propbuf_latitude,"0");charpropbuf_longitude[PROPERTY_VALUE_MAX];property_get("persist.gps.longitude",propbuf_longitude,"0");charpropbuf_latitudeHemi[PROPERTY_VALUE_MAX];property_get("persist.gps.latitudeHemi",propbuf_latitudeHemi,"N");charpropbuf_longitudeHemi[PROPERTY_VALUE_MAX];property_get("persist.gps.longitudeHemi",propbuf_longitudeHemi,"E");

属性名分开用的是persist.前缀,运行时可写,setprop后重启生效 —— 这样同一套固件可以装到不同城市的不同设备上,现场只改属性、不用重编。

4.2 填充 fix 结构体

r->sv_status.num_svs=6;//卫星数量r->sv_status_changed=1;//状态回调r->sv_status.used_in_fix_mask|=(1<<(r->sv_status.num_svs));r->fix.accuracy=8.0;//精度r->fix.timestamp=1685782786;//时间戳r->fix.latitude=strtod(propbuf_latitude,NULL);r->fix.longitude=strtod(propbuf_longitude,NULL);r->fix.flags|=GPS_LOCATION_HAS_LAT_LONG;r->fix.flags|=GPS_LOCATION_HAS_ACCURACY;D("lat : %f lon : %f ",r->fix.latitude,r->fix.longitude);

4.3 四处必须核对的细节(这一节比代码本身重要)

上面这段是我原始版本的做法,能跑通,但有四个点建议每台设备都对着核一遍,它们对应的是四类不同的「莫名其妙不生效」:

①used_in_fix_mask的位运算

r->sv_status.used_in_fix_mask|=(1<<(r->sv_status.num_svs));

used_in_fix_mask是「哪几颗卫星参与了定位」的位图,bit i 对应sv_list[i]。当num_svs = 6时,有效下标是 0~5,掩码应该是0x3F;而1 << 6得到的是0x40,指向的是第 7 颗 —— 一个并不存在的卫星。

后果:一部分 App 会遍历0 ~ num_svs逐位检查「这颗星星用没用上」,结果全部为 0,判定为「没有卫星参与定位」,从而丢弃这个 fix。

建议改成按位数生成掩码:

uint32_tn=r->sv_status.num_svs;r->sv_status.used_in_fix_mask=(n>=32)?0xFFFFFFFFu:((1u<<n)-1);

用=而不是|=,避免多次进入这个函数时掩码被越叠越乱。

② 半球标志读出来了,但没有参与换算

propbuf_latitudeHemi/propbuf_longitudeHemi这两个值在上面只被property_get读出,后面没有任何地方用到它们。而r->fix.latitude/longitude是带符号的十进制度—— 南纬、西经必须取负值。

也就是说当前代码在南北回归线以外的地区会直接定错位置:读进来S却当成北纬,坐标会跑到地球另一侧的对称点。

要支持全球坐标,补上换算:

doublelat=strtod(propbuf_latitude,NULL);doublelon=strtod(propbuf_longitude,NULL);if(propbuf_latitudeHemi[0]=='S'||propbuf_latitudeHemi[0]=='s')lat=-lat;if(propbuf_longitudeHemi[0]=='W'||propbuf_longitudeHemi[0]=='w')lon=-lon;r->fix.latitude=lat;r->fix.longitude=lon;

如果项目只在国内使用,那更简单 —— 直接不用这两个属性,省得后人误以为它生效了。

③ 时间戳是硬编码的

r->fix.timestamp=1685782786;//时间戳

固定值意味着这个 fix 的时间永远停在那一刻,不会前进。地图类应用会显示一个恒定的「最后更新时间」,记录轨迹的应用会因为时间不递增而丢弃后续点。

建议改成取实时时间,并注意单位要和本文件里其他位置对timestamp的处理保持一致(不同 SDK 版本里这个字段存在「秒」和「毫秒」两种写法,改之前先搜一下同文件里其他地方的赋值方式,照抄单位):

structtimevaltv;gettimeofday(&tv,NULL);r->fix.timestamp=(int64_t)tv.tv_sec*1000+tv.tv_usec/1000;

④ 属性默认值"0"会让设备定位到 (0, 0)

纬度0、经度0在几内亚湾,是客户端地图上经典的「空岛」位置。没配置过属性的机器会老老实实报到这里。

给一个合理的兜底值(比如项目所在地),或者加个判断:属性没设置就不上报GPS_LOCATION_HAS_LAT_LONG,让 App 表现为「暂时无定位」而不是「定位到了非洲西海岸」。

4.4 属性配置与 SELinux

坐标用setprop写进去:

adb shell setprop persist.gps.latitude22.5431adb shell setprop persist.gps.longitude114.0579adb shell setprop persist.gps.latitudeHemi N adb shell setprop persist.gps.longitudeHemi E# 确认写进去了adb shell getprop persist.gps.latitude

两个提醒:

  • persist.属性落盘在/data/property/下,所以/data必须先挂载好—— 如果 HAL 启动得比/data挂载还早,读到的就是默认值,现象和「属性没设」一模一样;
  • HAL 进程读属性同样受 SELinux 约束。setprop能写不代表 HAL 能读,新属性名如果没有在property_contexts里登记,读侧可能被拦掉。排查方法还是看dmesg | grep avc。

五、验证

改完刷机后按顺序看:

# 1) 写入坐标adb shell setprop persist.gps.latitude22.5431adb shell setprop persist.gps.longitude114.0579adb shellreboot# 2) HAL 是否起来了adb shell lshal|grep-ignss# 3) 代码里那行 D() 打出来的坐标对不对adb logcat-ball|grep"lat :"# 4) 框架层实际吐出来的位置adb shell dumpsys location# 5) 设备有没有宣称自己支持 GPSadb shell pm list features|grep-ilocation

最后一步就是装一个第三方定位软件(或者地图 App)看实际落点。

原文的实测截图(保留作为证据):

六、几个必须知道的限制

  1. 坐标是静态的。速度、方位角恒为 0,位置永远不动。导航类 App 会一直提示「重新规划路线」,运动记录类 App 会记不到里程。这是方案本身的边界,不是 bug。
  2. 部分 App 会做合理性交叉校验。除了看 GNSS,还会拿基站、Wi-Fi、IP 归属地做比对。如果你上报的坐标和设备的实际网络环境差得太远,对方平台侧仍有可能判定异常 —— 换的是 HAL,换不掉服务端的风控。
  3. 恢复出厂会丢。persist属性在/data下,恢复出厂即被清空,坐标掉回默认值。要求「出厂永久固定」的场景,需要把兜底值直接写在代码里。
  4. 合规提示。这个方案的正确用途是自有设备的测试、演示、固定点位上报。不要用来伪造考勤打卡、或者绕过第三方平台的定位限制—— 那是另一回事,风险和责任都不在技术层面。

七、小结

整个方案的核心只有一句话:在 GNSS HAL 这一层直接把 fix 造出来,绕过真机卫星,让框架和所有上层 App 都把它当成真实定位。

三步的顺序不能乱:

  1. 先让系统认到 GPS 能力—— matrix + manifest 的版本要一致,权限声明文件不能漏,否则 HAL 起来了 App 也当你不支持;
  2. 再把 HAL 跑起来—— 用lshal确认服务注册成功,这一步不过就别改上报代码;
  3. 最后改上报流程—— 从属性读坐标,同时核对used_in_fix_mask、半球符号、时间戳、默认值这四处。

第 2 步和第 3 步之间那个「HAL 初始化失败导致 fix 永远发不出去」的坑,是这类移植里花时间最多的地方 —— 现象和「属性没读到」几乎一样,但根因完全不同,lshal一查就能分流。


深耕 Android 嵌入式与 RK 平台系统级开发多年,日常处理显示、定位、存储、开机时序这类系统级问题。如果你在做 RK 平台整机项目时遇到类似的系统级疑难,欢迎留言或私信交流。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 1:47:59

iTOP-4412 U-Boot移植:重定位机制与UART驱动实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/15 1:47:52

ggplot2火山图绘制避坑指南:数据预处理、图层顺序与标度设置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/15 1:47:46

MP2642+STM32F746的2S电池主动均衡方案设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/15 1:47:28

requests文件下载进阶:流式传输、断点续传与踩坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/15 1:47:17

百度之星2020初赛第二场:算法竞赛中的建模降维与工程思维

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华