GPS北斗双模组件驱动开发和定位数据解析—【万物智能之开源鸿蒙OpenHarmony系统实战开发系列教程】
RS232 回环通了。今天接定位模块。它吐的是文本语句,必须窗边或室外;解析时别把精度因子当成海拔。



海拔一直不对。经纬度有时候有、有时候空。有人改解析改了一下午,把逗号再 split 一次,给空字段填 0,越改越怪。
NMEA $GGA 按逗号切开,下标从 0 算句子名:
| 下标 | 字段 | 例 |
|---|---|---|
| 0 | 句子名 | `GNGGA` 或 `GPGGA` |
| 1 | UTC | `092530.00` |
| 2 | 纬度 ddmm.mmmm | `2232.1234` |
| 3 | N/S | `N` |
| 4 | 经度 dddmm.mmmm | `11403.5678` |
| 5 | E/W | `E` |
| 6 | 定位质量 | `0` 无效,`1` GPS,`2` 差分 |
| **7** | **卫星数** | `08` |
| **8** | **HDOP** | `1.2` |
| **9** | **海拔** | `45.3` |
卫星是 f[7],海拔是 f[9],f[8] 是水平精度因子 HDOP。把 f[6] 当卫星、把 f[8] 当海拔,质量 1 会被显示成「1 颗星」,HDOP 1.2 会被显示成「海拔 1.2 米」。这就是「海拔不对」的全部。
未定位时 2、4、9 本来就是空字符串。空不是解析失败。室内、天线没对天、冷启动前两分钟,质量是 0,经纬度空着,卫星可能是 0 也可能是 3、4 还不够解算。UART 通路在工作——你已经读到了 $GNGGA。缺的是天空。
产品 rk3568_evb。9600 8N1。口不要写死。

1. 谁吐 $GNGGA,谁就是 GPS
板级 dts 有人把 uart8 注释成 GPS,有人把模块插到 uart7 那组排针上。丝印、注释、昨天同事的口播,三份可以是三个答案。
同一组 UART 排针一次一个模块。GPS、BLE、LoRa 不能同时插。打开之后读 1 秒,句子认模块:
$GNGGA/$GPGGA/$GNRMC/$GNGLL→ GPS- 可打印 ASCII 且不是
$G开头 → 可能是 BLE 透传 - 二进制或
FE开头的十六进制帧 → 可能是 LoRa / Zigbee 配置帧
扫这三根,波特率先 9600:
/dev/ttyS7
/dev/ttyS8
/dev/ttyS3
ttyS2 不要扫。那是 1500000 调试口。
UART 那篇的 uart_tool 直接用:
hdc shell "/data/local/tmp/uart_tool read /dev/ttyS7 9600 text 1200"
hdc shell "/data/local/tmp/uart_tool read /dev/ttyS8 9600 text 1200"
hdc shell "/data/local/tmp/uart_tool read /dev/ttyS3 9600 text 1200"
哪根吐出 $GN 或 $GP,后面就用哪根。hex 模式认 $:24 47 4E 47 47 41 是 $GNGGA,24 47 50 47 47 41 是 $GPGGA。
三根都空:
/proc/tty/driver/serial对应行的rx:在不在涨。涨了是波特率或 ASCII 过滤;不涨是没接到这根口、模块没上电、天线座空着还不会自己发(多数模块没定位也会发 GGA,空字段)。- 权限
0666。HAP 打不开、hdc 能开,UART 那篇开机脚本。 - 模块插错组。换排针,再扫一遍,不要先改代码。
2. 电气和设备树
GPS 模块是 TTL UART,9600,8N1,没有 RTS/CTS。TX 接 SoC RX,RX 接 SoC TX,地共。有源天线要 3.3 V 馈电,天线口空着就别指望定位。
uart7 / uart8 在板级 dts 里 okay 即可,UART 那篇写过。GPS 不需要自己的 compatible 节点——它不是 I2C 从设备,内核不会给它起 /dev/gps0。用户态打开 tty,读字节流。
&uart7 {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&uart7m1_xfer>;
};
&uart8 {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&uart8m0_xfer>;
};
冷启动:模块上电之后要下载星历,窗外或室外,1~2 分钟常见,有的要更久。室内钢筋和镀膜玻璃会把 L1 吃掉。把板子放窗台、天线立起来,等。不要在地下车库里改解析。
热启动:刚定位过、电容还饱,几十秒。不要用热启动的体感去要求冷启动。
3. 句子长什么样
定位成功的 GGA(字段用逗号隔开,行尾校验 *XX):
$GNGGA,092530.00,2232.1234,N,11403.5678,E,1,08,1.2,45.3,M,0.0,M,,*7A
未定位:
$GNGGA,092530.00,,,,,0,00,99.99,,,,,,*5E
质量 0,纬度经度空,卫星 00,海拔空。这句能读到,UART 就通了。应用应该显示「未定位,卫星 0」,而不是「解析失败」或把空字符串 parseFloat 成 NaN 再显示成 0 度 0 米——那会看起来像在几内亚湾。
RMC 补时间和地速:
$GNRMC,092530.00,A,2232.1234,N,11403.5678,E,0.15,0.00,210826,,,A*6C
A 有效,V 无效。地速单位节,在 f[7](RMC 的 7,不是 GGA 的 7)。两套句子字段表不一样,不要共用一个 f[7]。
$GNGGA 和 $GPGGA 都合法。双系统模块发 GN,单 GPS 发 GP。判断用 indexOf("GGA") 或 startsWith("GNGGA") || startsWith("GPGGA"),不要只认 GP。
4. 非 ASCII 会把 ArkTS 弄崩
波特率开成 115200 去读 9600 的模块,tty 缓冲里是 0x80 以上的脏字节。napi_create_string_utf8 造出非法 UTF-8,ArkTS split(',') / startsWith 直接崩,页面闪退。看起来像 GPS 页有 bug,其实是串口设置错了。
UART 那篇文本模式已经过滤:只留 0x20~0x7E、\r、\n、\t。GPS 解析走文本,不要走 hex 再自己转——hex 不过滤,脏数据会进解析器。
过滤之后若变成空串,同时 hex 读是乱码:换回 9600。rx 计数照样涨,因为内核按你设的波特率采样,它不管对不对。
5. gps_nmea.c
扫口、读 1 秒、拆 GGA/RMC、ddmm.mmmm 转十进制度。32 位 musl。依赖UART 那篇同一套 termios;为了单文件能编过,把 open/read 写在里面。
/* gps_nmea.c — 扫 tty、解析 GGA/RMC。
* 用法:
* gps_nmea scan
* gps_nmea /dev/ttyS7
*/
#include <fcntl.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <termios.h>
#include <unistd.h>
static int open_9600(const char *path)
{
int fd = open(path, O_RDWR | O_NOCTTY | O_NONBLOCK);
if (fd < 0) {
perror(path);
return -1;
}
struct termios t;
memset(&t, 0, sizeof(t));
if (tcgetattr(fd, &t) != 0) {
perror("tcgetattr");
close(fd);
return -1;
}
cfsetispeed(&t, B9600);
cfsetospeed(&t, B9600);
t.c_cflag = (t.c_cflag & ~CSIZE) | CS8 | CLOCAL | CREAD;
t.c_cflag &= ~(PARENB | CSTOPB | CRTSCTS);
t.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
t.c_iflag &= ~(IXON | IXOFF | IXANY | INLCR | ICRNL);
t.c_oflag &= ~OPOST;
t.c_cc[VMIN] = 0;
t.c_cc[VTIME] = 0;
tcflush(fd, TCIOFLUSH);
if (tcsetattr(fd, TCSANOW, &t) != 0) {
perror("tcsetattr");
close(fd);
return -1;
}
return fd;
}
static int read_ascii(int fd, char *buf, int bufsz, int timeout_ms)
{
int used = 0;
int idle = 0;
unsigned char rb[512];
while (idle < timeout_ms && used < bufsz - 1) {
struct pollfd p;
p.fd = fd;
p.events = POLLIN;
p.revents = 0;
int pr = poll(&p, 1, 100);
int r = 0;
if (pr > 0 && (p.revents & POLLIN))
r = (int)read(fd, rb, sizeof(rb));
if (r > 0) {
int i;
for (i = 0; i < r && used < bufsz - 1; i++) {
unsigned char c = rb[i];
if ((c >= 0x20 && c <= 0x7E) || c == '\r' || c == '\n' || c == '\t')
buf[used++] = (char)c;
}
idle = 0;
} else {
idle += 100;
}
}
buf[used] = '\0';
return used;
}
/* ddmm.mmmm + 半球 → 十进制度。南/西为负。dot-2 位是分。 */
static int to_deg(const char *raw, const char *hemi, double *out)
{
const char *dot;
int deg_len;
char degbuf[8];
double deg, minutes;
if (!raw || !raw[0] || !out)
return -1;
dot = strchr(raw, '.');
if (!dot)
return -1;
deg_len = (int)(dot - raw) - 2;
if (deg_len < 1 || deg_len > 3)
return -1;
memcpy(degbuf, raw, (size_t)deg_len);
degbuf[deg_len] = '\0';
deg = atof(degbuf);
minutes = atof(raw + deg_len);
*out = deg + minutes / 60.0;
if (hemi && (hemi[0] == 'S' || hemi[0] == 'W'))
*out = -*out;
return 0;
}
static void parse_line(char *line)
{
char *f[20];
int n = 0;
char *save = NULL;
char *p;
char *star;
if (line[0] == '$')
line++;
star = strchr(line, '*');
if (star)
*star = '\0';
p = strtok_r(line, ",", &save);
while (p && n < 20) {
f[n++] = p;
p = strtok_r(NULL, ",", &save);
}
if (n < 1)
return;
if (strcmp(f[0], "GNGGA") == 0 || strcmp(f[0], "GPGGA") == 0) {
const char *lat = (n > 2) ? f[2] : "";
const char *ns = (n > 3) ? f[3] : "";
const char *lon = (n > 4) ? f[4] : "";
const char *ew = (n > 5) ? f[5] : "";
const char *fix = (n > 6) ? f[6] : "";
const char *sat = (n > 7) ? f[7] : ""; /* 卫星,不是 f[6] */
const char *hdop = (n > 8) ? f[8] : ""; /* HDOP,不是海拔 */
const char *alt = (n > 9) ? f[9] : ""; /* 海拔 */
double dlat = 0, dlon = 0;
int has_pos = (lat[0] && lon[0] && to_deg(lat, ns, &dlat) == 0 &&
to_deg(lon, ew, &dlon) == 0);
printf("GGA fix=%s sats=%s hdop=%s alt=%s\n",
fix[0] ? fix : "-", sat[0] ? sat : "-",
hdop[0] ? hdop : "-", alt[0] ? alt : "-");
if (has_pos)
printf(" lat=%.6f lon=%.6f\n", dlat, dlon);
else
printf(" no fix (empty lat/lon is normal indoors / cold start)\n");
return;
}
if (strcmp(f[0], "GNRMC") == 0 || strcmp(f[0], "GPRMC") == 0) {
const char *utc = (n > 1) ? f[1] : "";
const char *st = (n > 2) ? f[2] : "";
const char *spd = (n > 7) ? f[7] : "";
printf("RMC status=%s utc=%s speed_kn=%s\n",
st[0] ? st : "-", utc[0] ? utc : "-", spd[0] ? spd : "-");
}
}
static int look_gps(const char *buf)
{
return strstr(buf, "$GNGGA") || strstr(buf, "$GPGGA") ||
strstr(buf, "$GNRMC") || strstr(buf, "$GPRMC") ||
strstr(buf, "GNGGA") || strstr(buf, "GPGGA");
}
static void dump_sentences(char *buf)
{
char *save = NULL;
char *line = strtok_r(buf, "\r\n", &save);
while (line) {
if (line[0] == '$' || strstr(line, "GGA") || strstr(line, "RMC"))
printf("RAW %s\n", line);
parse_line(line);
line = strtok_r(NULL, "\r\n", &save);
}
}
static int probe_port(const char *path)
{
char buf[4096];
int fd = open_9600(path);
int n;
if (fd < 0)
return 0;
n = read_ascii(fd, buf, sizeof(buf), 1000);
close(fd);
printf("=== %s n=%d ===\n", path, n);
if (n <= 0) {
printf("(empty)\n");
return 0;
}
if (look_gps(buf)) {
dump_sentences(buf);
return 1;
}
printf("(not NMEA, first 80 bytes) %.80s\n", buf);
return 0;
}
int main(int argc, char **argv)
{
const char *cands[] = { "/dev/ttyS7", "/dev/ttyS8", "/dev/ttyS3", NULL };
int i;
if (argc >= 2 && strcmp(argv[1], "scan") == 0) {
int found = 0;
for (i = 0; cands[i]; i++) {
if (probe_port(cands[i]))
found++;
}
if (!found)
printf("no $GGA on ttyS7/S8/S3 @9600. check antenna, 0666, rx counter.\n");
return found ? 0 : 2;
}
if (argc >= 2) {
return probe_port(argv[1]) ? 0 : 2;
}
fprintf(stderr, "usage: %s scan | %s /dev/ttyS7\n", argv[0], argv[0]);
return 1;
}
OHOS=/path/to/OpenHarmony
CLANG=$OHOS/prebuilts/clang/ohos/linux-x86_64/llvm/bin/clang
SYSROOT=$OHOS/prebuilts/ohos-sdk/linux/11/native/sysroot
$CLANG --target=arm-linux-ohos --sysroot=$SYSROOT -O2 -Wall \
-Wl,--dynamic-linker=/system/lib/ld-musl-arm.so.1 \
-o gps_nmea gps_nmea.c
hdc file send gps_nmea /data/local/tmp/gps_nmea
hdc shell "chmod 0755 /data/local/tmp/gps_nmea"
hdc shell "/data/local/tmp/gps_nmea scan"
扫到 GPS 之后盯着那根口看 1~2 分钟:
hdc shell "/data/local/tmp/gps_nmea /dev/ttyS7"
(口换成 scan 打印出来的那根。)质量从 0 变成 1,纬度经度从空变成数字,才叫定位。中间那些空字段的 GGA,留下当「通路通」的证据。
6. ArkTS:扫口 + 拆字段
libboard.so 的 uartOpen / uartRead / uartClose UART 那篇已经有。GPS 页不要自动 setInterval 叠同步 uartRead,UI 会假死。点「读一次」,或者以后上异步版。
import board from 'libboard.so';
@Entry
@Component
struct GpsPage {
@State msg: string = '先扫口。谁吐 $GGA 谁就是 GPS。天线对天,冷启动 1-2 分钟。';
@State cur: string = '/dev/ttyS7';
@State opened: boolean = false;
@State out: string = '';
private ports: string[] = ['/dev/ttyS7', '/dev/ttyS8', '/dev/ttyS3'];
aboutToDisappear() {
if (this.opened) {
board.uartClose();
this.opened = false;
}
}
build() {
Column() {
Text('GPS NMEA').fontSize(20).fontWeight(FontWeight.Bold).margin({ bottom: 8 })
Text('9600 8N1。室内经纬度空着不是 bug。卫星 f[7],海拔 f[9],f[8] 是 HDOP。')
.fontSize(13).fontColor('#C62828').margin({ bottom: 8 })
Row() {
ForEach(this.ports, (p: string) => {
Button((this.cur === p ? '> ' : '') + p).fontSize(12).margin({ right: 6 })
.onClick(() => { this.pick(p); })
}, (p: string) => p)
}.margin({ bottom: 8 })
Row() {
Button(this.opened ? '关闭' : '打开 9600').onClick(() => { this.toggle(); })
Button('读一次').margin({ left: 8 }).onClick(() => { this.readOnce(); })
Button('三口各读 1 秒').margin({ left: 8 }).onClick(() => { this.scan(); })
}
Text(this.msg).fontSize(14).margin({ top: 10 }).fontColor('#1565C0')
Text(this.out).fontSize(13).margin({ top: 8 })
}.padding(16).width('100%')
}
pick(p: string) {
if (this.opened) {
board.uartClose();
this.opened = false;
}
this.cur = p;
this.msg = '已选 ' + p;
}
toggle() {
if (this.opened) {
board.uartClose();
this.opened = false;
this.msg = '已关闭';
return;
}
if (board.uartOpen(this.cur, 9600) === 0) {
this.opened = true;
this.msg = '已打开 ' + this.cur + ' @9600';
} else {
this.msg = '打开失败。crw、0666、不是 ttyS2。';
}
}
scan() {
if (this.opened) {
board.uartClose();
this.opened = false;
}
let report: string = '';
let i: number;
for (i = 0; i < this.ports.length; i++) {
let p: string = this.ports[i];
if (board.uartOpen(p, 9600) !== 0) {
report += p + ': open fail\n';
continue;
}
let s: string = board.uartRead(1000, 0);
board.uartClose();
if (s.indexOf('GGA') >= 0 || s.indexOf('RMC') >= 0) {
report += p + ': GPS 句子\n';
this.cur = p;
} else if (s.length === 0) {
report += p + ': 空\n';
} else {
report += p + ': 非 NMEA ' + s.substring(0, 40) + '\n';
}
}
this.out = report;
this.msg = '扫完。打开上面标了 GPS 句子的那根。';
}
readOnce() {
if (!this.opened) {
this.msg = '先打开';
return;
}
let s: string = board.uartRead(800, 0);
if (s.length === 0) {
this.out = '800ms 空。换口、看 rx、天线。';
return;
}
this.out = this.format(s);
}
format(s: string): string {
let latV = '', latH = '', lonV = '', lonH = '', alt = '', sats = '', fixq = '', utc = '', speed = '', rawGga = '';
let chunks: string[] = s.split('$');
let i: number;
for (i = 0; i < chunks.length; i++) {
let ln: string = chunks[i];
let f: string[] = ln.split(',');
if (ln.startsWith('GNGGA') || ln.startsWith('GPGGA')) {
if (rawGga.length === 0) {
rawGga = ln;
}
if (f.length > 2 && f[2].length > 0) {
latV = f[2];
latH = f[3];
}
if (f.length > 4 && f[4].length > 0) {
lonV = f[4];
lonH = f[5];
}
if (f.length > 6) {
fixq = f[6];
}
if (f.length > 7) {
sats = f[7];
}
if (f.length > 9 && f[9].length > 0) {
alt = f[9];
}
} else if (ln.startsWith('GNRMC') || ln.startsWith('GPRMC')) {
if (f.length > 1 && f[1].length > 0) {
utc = f[1];
}
if (f.length > 7 && f[7].length > 0) {
speed = f[7];
}
}
}
if (latV.length === 0) {
let nsat: number = parseInt(sats);
if (Number.isNaN(nsat)) {
nsat = 0;
}
let why: string = nsat === 0
? '一颗都没有:天线没对天 / 在室内 / 馈电没有'
: ('有 ' + nsat.toString() + ' 颗但未解算,窗边再等 1-2 分钟');
return '未定位 质量=' + (fixq.length ? fixq : '?') +
' 卫星=' + nsat.toString() + '\n' + why + '\n' +
(rawGga.length ? ('GGA $' + rawGga.substring(0, 80)) : '(这包没有 GGA)');
}
return '已定位 质量=' + fixq + ' 卫星=' + sats +
'\n纬度 ' + this.toDeg(latV, latH) +
'\n经度 ' + this.toDeg(lonV, lonH) +
(alt.length ? ('\n海拔 ' + alt + ' m') : '') +
(speed.length ? ('\n地速 ' + speed + ' kn') : '') +
(utc.length ? ('\nUTC ' + utc) : '');
}
toDeg(raw: string, hemi: string): string {
if (raw.length === 0) {
return '';
}
let dot: number = raw.indexOf('.');
if (dot < 3) {
return raw;
}
let degLen: number = dot - 2;
let deg: number = parseFloat(raw.substring(0, degLen));
let min: number = parseFloat(raw.substring(degLen));
let v: number = deg + min / 60;
if (hemi === 'S' || hemi === 'W') {
v = -v;
}
return v.toFixed(6);
}
}
f[7]、f[9] 写在注释里不够,代码里就要用这两个下标。复查时搜 f[6] 当卫星、f[8] 当海拔,搜到就是回归。
7. 十进制度
NMEA 纬度是 ddmm.mmmm(度、分),经度 dddmm.mmmm。2232.1234 不是 2232 度。小数点前两位是分,再往前是度:
2232.1234 → 22 度 + 32.1234 分 → 22 + 32.1234/60 ≈ 22.535390
南纬、西经为负。不要把原始字符串直接当十进制度画地图,点会飞到海里。
分不够两位(畸形句)原样显示,不要 parseFloat 整串。
8. 现场:有句子、没定位
gps_nmea scan 打出 GGA,质量 0,lat 空。这不是软件问题。
天线:有源天线要 3.3 V。万用表量天线座中心导体对地。0 V,模块没馈电或座子虚焊。天线线弯折、接头进水,同样 0 星。
位置:窗边玻璃若是镀膜,贴着玻璃也不够,伸出窗外或上屋顶。地下车库放弃。
时间:冷启动盯着质量字段。0 → 1 的那一刻,经纬度才会有数字。提前 20 秒读一次就下结论,会把正常冷启动写成 bug。
模块灯:有的 1 Hz 闪是定位,常亮或灭是未定位,以说明书为准,不要和别的模组的灯语混用。
rx 不涨:TX/RX 接反最常见。模块 TX 必须进 SoC RX。接反的时候你 write 进模块的 RX,模块不在乎,它自己仍在 TX 上吐 GGA,只是进错了 SoC 脚。
9. 现场:闪退
hilog 没有你的业务 log,进程没了。先怀疑非法 UTF-8:波特率 115200、过滤没开、uartRead 的 asHex=0 却塞进了脏字节。把 asHex 改 1 看是不是乱码。改回 9600,过滤打开,闪退消失。
其次是 UI 线程阻塞。setInterval(300) 里调同步 uartRead(600),间隔被读占满,停止点不了,看门狗把 Ability 杀掉。改成手动读一次。
第三,split('$') 之后第一段往往是空的或半截,startsWith('GNGGA') 对第一段失败是正常的,从第二段开始才是句子。不要因为第一段不是 GGA 就报解析错误。 行尾 *7A 是异或,通路调试不必验。f.length 不够 10 时不要读 f[9]。
室内验收:scan 能见到 $GxGGA,未定位显示「未定位」而不是 0°。窗外验收:质量 ≥ 1,卫星走 f[7],海拔走 f[9] 且不是 HDOP 那个 1.x。同一对脚换 BLE/LoRa 后 NMEA 会消失,拔掉改回 9600 再扫。ttyS3/7/8 都要 chmod 0666。
系列第 43 篇 · 芯片:瑞芯微 RK3568 · OpenHarmony 4.1(API 11) · Linux 5.10
更多推荐




所有评论(0)