Для логгера высоты в походе понадобились не только координаты, но и высота с числом спутников — а в уроке про NMEA и $GPRMC мы разобрали ровно одно предложение, самое ходовое: время, статус, координаты, скорость и дата. Высота и спутники живут в соседнем предложении $GPGGA: приёмник NEO-6M раз в секунду вываливает пачку строк на 9600 бод, и обе строки этой пачки — два окна на один и тот же фикс. Ниже соберём разбор GGA теми же руками, без библиотеки, и заодно закроем две ловушки, на которых я спотыкался сам: координаты в градусах-минутах и буфер приёма, который сжирает хвост пачки.
NMEA 0183 — текстовый протокол: строки до 80 символов, маркер со знаком доллара, поля через запятую, в хвосте звёздочка и XOR-сумма — её механику я показывал в том же уроке про RMC. GGA несёт время, широту и долготу, качество фикса, число спутников и высоту; RMC — статус A или V, скорость и дату. Строки в пачке приходят с разными маркерами: $GP — GPS, $GN — ГЛОНАСС вместе с GPS, $GL — только ГЛОНАСС, и две буквы после доллара ещё не гарантия того же набора полей. Поля дублируются не зря: RMC может прийти с пустыми координатами при статусе V, а GGA в этот же миг честно напишет качество 0 — смотреть надо на тот набор, что нужен вашей задаче.
План такой: вытащить поля через indexOf и substring, перевести широту и долготу в десятичные градусы одной дробью и наладить построчное чтение, чтобы пачка на 300-400 байтов в секунду не рвалась о буфер на 64. Симулятор ниже кормит парсер строками с настоящего модуля — движок GPS-железа не знает, и это честная оговорка: на живой плате те же строки приходят в порт. Обе ловушки лечатся за вечер, а поиск их на живой трассе стоил мне двух выездов.
GGA: качество фикса, спутники, высота
Живая строка с моего модуля выглядит так: $GPGGA,123519.00,5545.1234,N,03737.5678,E,1,09,0.9,154.5,M,13.9,M,,. После маркера идут поля по порядку: время UTC, широта, полусфера N или S, долгота, полусфера E или W, качество фикса, спутники, геометрический фактор, высота в метрах. Качество фикса — главный переключатель: 0 — приёмник спутников не видит, поля пустые или старые; 1 — обычный фикс, 2 — дифференциальная поправка, точнее в разы, но под открытым небом. Девять спутников в моём примере — удачный день; под деревьями счёт падает до четырёх, и высота начинает плясать на десятки метров. Геометрический фактор в следующем поле — качество геометрии спутников над головой: ниже единицы хорошо, выше двух — точность плывёт, и это первый кандидат на выброс при коротком логе.
Разбор полями держится на двух String-методах: indexOf находит ближайшую запятую, substring отрезает поле, хвост едет дальше. Весь цикл — функция pole(s, n): n раз найти запятую и срезать. Для двух-трёх полей из одной строки этого хватает с запасом; когда полей десятки и строк много, C-способ со strtok экономнее — он разобран в уроке про strtok и atoi. Числа из полей достаю toFloat — механика перевода строк в числа та же, что у parseInt в уроке про parseInt, только с точкой. IndexOf возвращает позицию с нуля, а минус один — когда запятой нет: проверяйте промах до substring, иначе получите пустое поле и молча потеряете спутников. Функция pole в скетче ниже и в симуляторе крутится без единой правки — на то она и двенадцать строк.
| Поле GGA | Пример | Нюанс |
|---|---|---|
| время UTC | 123519.00 | часы, минуты, секунды и сотые — готовая метка для лога |
| качество фикса | 1 | 0 — фикса нет, координаты не трогаем; 2 — дифпоправка |
| спутники | 09 | под деревьями падает до 4 — высота пляшет |
| широта | 5545.1234 | ddmm.mmmm: две цифры градусов, дальше минуты |
| долгота | 03737.5678 | dddmm.mmmm: три цифры, ведущий ноль — часть формата |
| высота | 154.5 | метры над уровнем моря, пляшет на 10-30 м под лесом |
Ловушка ddmm.mmmm и буфер на 64
Ловушка первая — наивный parseFloat. toFloat на поле 5545.1234 вернёт 5545,1234 градуса — на карте это сотня километров севернее цели, и логгер даже не заметит промаха: число-то есть. В NMEA широта и долгота пишутся градусами с минутами: 5545.1234 — это 55 целых градусов и 45,1234 минуты, в минуте шестьдесят долей. Перевод: градусы — целая часть от деления на 100, минуты — остаток, итог — градусы плюс минуты, делённые на шестьдесят: 55 + 45,1234/60 = 55,752057. У долготы градусов три цифры, и ведущий ноль в 03737 — часть формата, а не украшение; анатомия обоих полей с картинкой полусфер — в уроке про RMC.
Ловушка вторая — буфер приёма на 64 байта. При 9600 бод непрерывный поток заполняет его за 67 мс; модуль шлёт не поток, а пачку из пяти-семи строк раз в секунду, но пачка едет плотно, и delay(1000) в loop оставляет буферу те же 67 мс — хвост гибнет молча, без флагов, я это уже разбирал в уроке про буфер UART. На NEO-6M заводская скорость 9600 и пачка — шесть-семь строк по 60-70 байтов, то есть за первую секунду фикса в буфер просится около четырёхсот байтов, и наивная пауза в секунду теряет почти всё. Лечение дешёвое: читать порт в каждом обороте loop по одному терминанту — available, readStringUntil с символом перевода строки, парсить, и только потом остальное. Планировщик задач на millis из урока про millis вместо delay здесь обязателен: любые паузы — только между чтениями, не вокруг них.
Моё мнение после трёх логгеров: для двух-трёх полей из GGA и RMC разбор руками дешевле и понятнее библиотеки — сорок строк, которые вы контролируете. TinyGPSPlus уместна, когда нужны курс, скорость в узлах и дата сразу из трёх предложений и возиться с полями надоело. Скорость порта у большинства модулей заводская 9600 — её и держите: 38400 у некоторых новых чипов стоит только если вы её сами прописали, а SoftwareSerial на нештатных скоростях — отдельная боль из урока про SoftwareSerial.
// NEO-6M: TX модуля -> D4 (RX SoftwareSerial), скорость заводская 9600.// Разбор $GPGGA без библиотеки: запятые, indexOf/substring, ddmm.mmmm -> градусы.// Раз в фикс - одна строка отчёта: метка millis, координаты, высота, спутники.#include <SoftwareSerial.h>SoftwareSerial gps(4, 3); // TX модулю не нужен: GPS только говорит String stroka = ""; String pole(String s, int n) { String rest = s; for (int k = 0; k <= n; k++) { int i = rest.indexOf(","); if (i < 0) return ""; if (k == n) return rest.substring(0, i); rest = rest.substring(i + 1); } return "";} float gradusy(String p) { float v = p.toFloat(); float g = (int)(v / 100.0); // целые градусы: широта 2 цифры, долгота 3 float m = v - g * 100.0; // остаток - минуты return g + m / 60.0; // минуты за шестьдесят} void razborGGA(String s) { int kach = pole(s, 6).toInt(); // поле 0 - маркер, 6 - качество фикса: 0 - фикса нет if (kach == 0) { Serial.println("."); // отчёт жив: точка вместо строки return; } float lat = gradusy(pole(s, 2)); float lon = gradusy(pole(s, 4)); float vysota = pole(s, 9).toFloat(); Serial.print(millis()); Serial.print(" lat="); Serial.print(lat, 6); Serial.print(" lon="); Serial.print(lon, 6); Serial.print(" h="); Serial.print(vysota, 1); Serial.print(" m sput="); Serial.println(pole(s, 7));} void setup() { Serial.begin(9600); gps.begin(9600); // заводская скорость большинства модулей Serial.println("GPS: zhdem fix");} void loop() { while (gps.available() > 0) { // вычитываем всё, что накопилось char c = gps.read(); if (c == 10) { // перевод строки - строка готова if (stroka.startsWith("$GPGGA")) razborGGA(stroka); stroka = ""; } else { stroka += c; } }}Скетч буферизует строку посимвольно: пока символ не перевод строки — доклеиваем к String, по терминатору отдаём в разбор и чистим. В памяти живёт ровно одна строка на семьдесят байтов, и буфер приёма не успевает переполниться: чтение в каждом обороте выгрызает поток быстрее, чем он приходит, — девятьсот шестьдесят байтов в секунду для Uno не скорость. Никаких пауз в loop: пачка приходит целиком, и каждая строка доезжает с маркером в голове и хвостом после звёздочки.
XOR-сумму после звёздочки на прямом коротком проводе я не проверяю: вероятность порчи на десяти сантиметрах трассы ничтожна, а кода экономится строк десять. Как только строки едут через Bluetooth-мост или длинную линию — проверяю обязательно, механика сверки разобрана в уроке про NMEA: XOR от маркера до звёздочки, сравнение с hex-хвостом, промах — строка в мусор. И дату для журнала берите из RMC: GGA дату не носит, пара полей из соседней строки закрывает вопрос. Качество фикса 0 печатается точкой: раз в секунду отчёт живёт, и по количеству точек между фиксами видно, сколько секунд приёмник блуждал, а не завис ли он вместе с координатами.
Высоту в лог я беру не голую: под лесом она пляшет на десятки метров, и среднее пяти фиксов пишет спокойные числа вместо судорожных. Число спутников рядом: ниже четырёх строка помечается и в среднее не попадает. Шесть знаков после запятой в координатах — не избыточность: один знак — это одиннадцать метров дороги, и логгер на шестом знаке отличает подъезд от калитки; усреднение координат при этом честнее, чем усреднение минут, — считайте только в градусах.
- 1. Где в NMEA живёт число спутников?
- 2. toFloat на поле 5545.1234 вернул 5545,1234. Что это значит?
- 3. Буфер приёма 64 байта при непрерывном потоке 9600 бод заполняется за...
- 4. Строка пришла без начального доллара. Что делать парсеру?
- 5. Качество фикса в GGA равно 0. Что с координатами?
- GGA — качество фикса, спутники, высота; RMC — статус A/V, скорость, дата: оба окна на один фикс
- Поля вынимаются indexOf и substring: нашёл запятую — отрезал — хвост едет дальше
- ddmm.mmmm в градусы: целое от деления на 100 плюс остаток, делённый на 60; у долготы три цифры
- Буфер 64 байта при 9600 заполняется за 67 мс: читать построчно в каждом обороте loop
- Первый символ — доллар, маркер $GP, счёт запятых: три проверки перед разбором
Частые вопросы по теме урока
- Где в NMEA живёт число спутников?
- GGA несёт качество фикса, спутники и высоту; RMC — статус A/V, скорость и дату. Два предложения — два окна на один фикс. В GGA после качества фикса
- toFloat на поле 5545.1234 вернул 5545,1234. Что это значит?
- NMEA пишет координаты градусами с минутами. Наивный toFloat даёт промах на сотню километров; правильный итог — 55,752057 градуса. Это градусы-минуты: 55 + 45,1234/60 — нужен перевод
- Буфер приёма 64 байта при непрерывном потоке 9600 бод заполняется за...
- 64 байта при 9600 бод — по миллисекунде на байт, итого около 67 мс. Пачка строк едет плотно: пауза в loop дольше — хвост гибнет без флагов. ~67 мс
- Строка пришла без начального доллара. Что делать парсеру?
- Хвост пачки без маркера разбирается в чужие координаты. Две строки проверки — первый символ и маркер $GP — дешевле одного ложного лога. Отбросить: обрезка без маркера — мусор со сдвинутыми полями
- Качество фикса в GGA равно 0. Что с координатами?
- Ноль качества — приёмник спутников не видит: координатные поля пустые или устаревшие. Диод фикса гасим и ждём следующую пачку. Поля пустые или старые — не трогать