Конечно, ты можешь купить GPS-приемник в Китае за несколько долларов. А можешь и не покупать — все равно он есть у тебя в телефоне, в навигаторе в машине… Но если ты хочешь быть настоящим хакером-инженером и разобраться в технологии GPS на низком уровне, то добро пожаловать в эту статью. Разберемся так, что мало не покажется!
История разработки глобальной системы позиционирования (Global Positioning System, GPS) уходит корнями в 50-е годы прошлого века, а первый спутник был запущен в 1974 году. Первоначально система использовалась лишь военными, но после трагедии с самолетом авиакомпании Korean Airlines, который был сбит над территорией СССР, гражданские службы также получили возможность работы с GPS. В 1993-м окончательно было решено предоставить GPS для использования гражданскими службами на безвозмездной основе, а после отключения намеренного округления положения точность возросла со ста метров до двадцати. В наши дни точность продолжает увеличиваться, а стоимость приемников — снижаться. Поэтому сделать GPS-логгер, ну или навигатор, может любой, кто родился с паяльником вместо…ладно-ладно, это шутка была. Одно другого не исключает :).
Матчасть
Итак, что же должно делать наше устройство?
- Получить информацию о текущем положении от GPS-приемника.
- Разобрать ее.
- Показать на экране текущее положение приемника, а также видимые спутники.
Для этого придется узнать, что таится за терминами GPS, NMEA-0183 и алгоритм Брезенхема.
GPS
Конечно, детально разбираться в нюансах работы GPS не требуется, поскольку всю работу по вычислению координат, скорости, курса и других параметров за нас возьмет на себя GPS-приемник. Но базу знать надо.
В первую очередь надо понять, что GPS-приемник ничего и никогда не передает спутникам (если ты мне не веришь, то просто обрати внимание, что приемники могут быть размером с флешку, в то время как антенны спутниковых телефонов порой больше самого телефона). Задача спутниковой навигации заключается в определении приемником своих координат, если известны точные координаты передатчиков. Если мы знаем расстояние до спутников, то с помощью элементарных геометрических построений можно с некоторой точностью рассчитать свои координаты.
Для нахождения расстояния между приемником и передатчиком необходимо сначала синхронизировать их часы, а затем посчитать искомое расстояние, зная скорость распространения радиоволны, а также задержку между временем передачи и временем приема.
Как я уже писал выше, приемнику необходимо знать точные положения передатчиков. Подобная информация предоставляется передатчиком и называется «альманах». Естественно, эта информация устаревает, поэтому в зависимости от «свежести» альманаха можно выделить три типа задержки между включением приемника и определения его первых точных координат: «холодный старт», «теплый старт» и «горячий старт».
Существуют способы, которые позволяют уменьшить время старта: AGPS (получение альманаха альтернативными способами — через интернет или Почтой России), DGPS (исключение искажения сигнала атмосферой) и другие. Но я их рассматривать не буду, поскольку для выполнения поставленной задачи этого не нужно.
Теперь разберемся с тем, в каком же виде рассчитанные координаты появляются на выходе устройства. Для этого существует специальный стандарт.
NMEA-0183
NMEA — National Marine Electronics Association, а NMEA-0183 (согласно Википедии) — текстовый протокол связи морского (как правило, навигационного) оборудования (или оборудования, используемого в поездах) между собой. Вот строки, приходящие от моего приемника.
(...)
$GPRMC,174214.00,A,5541.23512,N,03749.12634,E,3.845,178.09,150914,,,A*6F
$GPVTG,178.09,T,,M,3.845,N,7.121,K,A*35
$GPGGA,174214.00,5541.23512,N,03749.12634,E,1,04,4.98,178.2,M,13.1,M,,*56
$GPGSA,A,3,20,14,04,17,,,,,,,,,8.85,4.98,7.31*02
$GPGSV,4,1,16,01,67,242,,02,,,15,03,,,15,04,53,284,35*74
$GPGSV,4,2,16,05,,,23,07,,,19,08,,,21,10,,,24*7F
$GPGSV,4,3,16,11,,,14,12,,,12,14,35,058,37,17,24,311,34*72
$GPGSV,4,4,16,20,40,275,29,22,08,097,,37,25,199,27,39,25,195,32*73
$GPGLL,5541.23512,N,03749.12634,E,174214.00,A,A*6D
(...)
Сначала определим похожие части каждой строки. Легко видеть, что все они начинаются одинаково и более-менее одинаково заканчиваются. $GP — информация идет от приемника GPS (ты ведь понимаешь, что на корабле куча других датчиков: если бы у нас был аварийный маяк, то строка начиналась бы с $EP, а если эхолот, то с $SD, ну и так далее). Каждая строка обязательно заканчивается контрольной XOR-суммой всех байтов в строке начиная от $ и заканчивая * — это как раз те два символа в конце строки. И не забываем про символы <CR> и <LF> после контрольной суммы. Разберем каждую из строк подробнее.
$GPRMC,hhmmss.ss,A,aaaa.aaaa,N,bbbb.bbbb,E,c.c,d.d,DDMMYY,z1,z2,e*ff
GPRMC— GPS Recommended Minimum Navigation Information sentence C — рекомендуемый минимум навигационной информации, строка типа С.hhmmss.ss— время по всемирному координированному времени UTC, когда была произведена фиксация положения.A— флаг достоверности информации. ЕслиV, то информации верить нельзя.aaaa.aaaaa— величина широты. Первые две цифры — градусы, вторые две — целое значение количества угловых минут, после точки — дробная часть количества угловых минут (переменной длины).N— северная широта. ЕслиS, то южная.bbbb.bbbbb— величина долготы. Первые две цифры — градусы, вторые две — целое значение количества угловых минут, после точки — дробная часть количества угловых минут (переменной длины).E— восточная долгота. ЕслиW, то западная.c.c— горизонтальная скорость в узлах (умножить на 1,852 для получения скорости в километрах в час), целая и дробная части имеют переменную длину.d.d— направление скорости (путевой угол, курс) в градусах, целая и дробная части имеют переменную длину.DDMMYY— текущая дата.z1— отсутствующая у нас величина направления магнитного склонения.z2— также отсутствующее у нас направление магнитного склонения.e— индикатор режима.ff— контрольная сумма.
$GPVTG,a.a,T,b.b,M,c.c,N,d.d,K,A*e
$GPVTG— GPS Track Made Good and Ground Speed — строка с информацией о курсе и скорости.a.a— курс в градусах.T— True, флаг достоверности информации.b.b— направление магнитного склонения (у нас его нет).M— Magnetic, да, действительно магнитное.c.c— горизонтальная скорость в узлах (умножить на 1,852 для получения скорости в километрах в час).N— kNots, узлы.d.d— горизонтальная скорость в километрах в час (и умножать ничего не надо).K— километры в час.ee— контрольная сумма.
$GPGGA,hhmmss.ss,a.a,N,b.b,E,c,d,e.e,f.f,M,g.g,M,h.h,*i
$GPGGA— Global Positioning System Fix Data — строка с информацией о текущем местоположении.hhmmss.ss— время по всемирному координированному времени UTC, когда была произведена фиксация положения.a.a— величина широты.N— северная широта. ЕслиS, то южная.b.b— величина долготы.E— восточная долгота. ЕслиW, то западная.c— флаг качества сигнала GPS.d— количество используемых спутников.e.e— фактор снижения точности (DOP, Dilution of precision).f.f— высота расположения приемника над уровнем моря.M— высота дается в метрах.g.g— различие между геоидом (истинной формой нашей планеты) и эллипсоидом по WGS84 (трехмерная система координат для позиционирования).M— различие дается в метрах.h.h— номер станции, передающей поправки DGPS.i— контрольная сумма.
$GPGSA,A,x,y1,y2,y3,y4,y5,y6,y7,y8,y9,y10,y11,y12,z1,z2,z3*i
$GPGSA— GPS DOP and Active satellites — строка с информацией о спутниках, использованных для определения местоположения и о факторах снижения точности.A— автоматический режим выбора работы в 2D или 3D,M— ручной режим, когда жестко выбран, например, 2D.x— режим работы приемника:0— координаты не определены,1— режим 2D,2— режим 3D.y1..y12— номера спутников, используемых для определения местоположения приемника.z1..z2— PDOP, HDOP, VDOP (факторы снижения точности по положению, в горизонтальной плоскости и в вертикальной плоскости соответственно).i— контрольная сумма.
$GPGSV,a,b,c1,d1,e1,f1,c2,d2,e2,f2,c3,d3,e3,f3,c4,d4,e4,f4*i
GPGSV— GPS Satellites in View — строка содержит в себе информацию о номере, азимуте, высоте над горизонтом и соотношением сигнал/шум спутника. В строке максимально может быть четыре спутника.a— общее количество строкGPGSV.b— номер текущей строки.c1..c4— номер спутника.d1..d4— высота над горизонтом в градусах(0..90).e1..e4— азимут спутника в градусах(0..359).f1..f4— соотношение сигнал/шум в дБ(0..99).
$GPGLL,5541.23512,N,03749.12634,E,174214.00,A,A*6D — на этой строке нет смысла останавливаться подробно, поскольку она содержит в себе координаты и время, а это мы уже имеем в строках GPRMC и GPGGA.
Разумеется, производителям GPS-приемников не запрещается добавлять собственные строки. У моего приемника можно при запуске увидеть такие:
$GPTXT,01,01,02,u-blox ag - www.u-blox.com*50
$GPTXT,01,01,02,HW UBX-G60xx 00040007 FF7FFFFFp*53
$GPTXT,01,01,02,ROM CORE 7.03 (45969) Mar 17 2011 16:18:34*59
$GPTXT,01,01,02,ANTSUPERV=AC SD PDoS SR*20
$GPTXT,01,01,02,ANTSTATUS=DONTKNOW*33
$GPTXT,01,01,02,ANTSTATUS=INIT*25
$GPTXT,01,01,02,ANTSTATUS=OK*3B
Алгоритм Брезенхема
Этот алгоритм является одним из самых старых алгоритмов компьютерной графики — он был разработан Джеком Брезенхемом (IBM) аж в 1962 году. С его помощью происходит растеризация графического примитива, другими словами, этот алгоритм определяет координаты пикселей, которые необходимо зажечь на экране, чтобы полученный рисунок примитива совпадал с оригиналом.
Представим, что мы рисуем линию, идущую из точки (0; 0) в (100, 32), как ты помнишь, у нашего экрана разрешение 128 x 64 точки. Несложно посчитать, что угол между этой прямой и осью Х составляет менее 45 градусов. Работа алгоритма заключается в последовательном переборе всех координат по оси Х в диапазоне от 0 до 100 и расчете соответствующей координаты Y. Логично, что в большинстве случаев значение координаты Y будет дробным, а это значит, что надо каким-то образом выбрать целочисленное значение координаты. Это делается путем выбора ближайшего пикселя. Для других углов наклона прямой, а также для окружностей, эллипсов и прочего алгоритм имеет аналогичный вид (подробнее о нем можно почитать в Википедии).
Алгоритм Брезенхема использует только операции сложения и вычитания целых чисел: обычно использование арифметики дробных чисел замедляет работу контроллера. Обычно, но не в нашем случае, поскольку внутри контроллера STM32F303VC находится ядро ARM Cortex-M4 с FPU. FPU (Floating Point Unit) — устройство, ускоряющее работу с дробными числами (математический сопроцессор), поэтому нас ничто не ограничивает и мы можем использовать алгоритм DDA-линии. Интересную демонстрацию ускорения работы МК при рисовании фракталов можно посмотреть на YouTube.
Железо
Что же мы используем в проекте?
- Отладочную плату STM32F3-Discovery;
- модуль UART GPS NEO-6M от WaveShare на базе приемника u-blox NEO-6M;
- ЖК-матрицу МТ-12864А.
Про ЖК-экран я рассказывал в сентябрьском номере (№ 188), кратко лишь скажу, что сделан он на базе контроллеров KS0108, а схема подключения к STM32F3-Discovery не изменилась: разъем для подключения экрана содержит в себе 20 пинов, описание представлено в списке по следующей маске: <номер> — <название из даташита> — <описание из даташита> — <куда подключается>.
- 1 — Ucc — питание — к 5V на Discovery.
- 2 — GND — земля — к GND на Discovery.
- 3 — Uo — вход питания ЖК-панели для управления контрастностью — к подстроечному резистору.
- 4..11 — DB0..DB7 — шина данных — к PD0..PD7 на Discovery.
- 12, 13 — E1, E2 — выбор контроллера — к PD8,PD9 на Discovery.
- 14 — RES — сброс — к PD10 на Discovery.
- 15 — R/W — выбор: чтение/запись — к PD11 на Discovery.
- 16 — A0 — выбор: команда/данные — к PD12 на Discovery.
- 17 — E — стробирование данных — к PD13 на Discovery.
- 18 — Uee — выход DC-DC преобразователя — к подстроечному резистору.
NEO-6M
Данный приемник производится швейцарской компанией u-blox, основанной в 1997 году. Линейка модулей Neo-6 представлена разновидностями G, Q, M, P, V и T, каждый из которых обладает своими характерными возможностями: например, Neo-6P имеет возможность очень точного (с ошибкой <1 м) определения положения за счет метода Precise Point Positioning (PPP).
Приемник Neo-6M обладает следующими свойствами:
- время холодного или теплого старта — 27 с;
- время горячего старта — 21 с;
- максимальная частота выдачи информации — 1 Гц;
- диапазон частот импульсов на пин PPS — 0,25 Гц — 1 кГц;
- максимальная точность определения положения — 2,5 м;
- максимальная точность определения скорости — 0,1 м/с;
- максимальная точность определения курса — 0,5 градуса.
Neo-6M умеет использовать SBAS (Satellite Based Augmentation System) — спутниковые системы дифференциальной коррекции, что увеличивает точность определения положения до 2 м, а также AGPS (Assisted GPS) для снижения времени холодного старта. Получение данных AGPS происходит с сайта u-blox с помощью сервисов AssistNow Online и AssistNow Offline (долгосрочный альманах). Модуль обладает поддержкой протоколов NMEA, UBX и RTCM. UBX — проприетарный протокол от u-blox, а RTCM — протокол для передачи модулю данных о дифференциальной коррекции DGPS. Также для связи доступны интерфейсы UART, I2C, SPI и USB.
Для работы с приемниками существует оригинальная утилита u-center, имеющая на момент написания статьи версию 8.11 (рис. 1).

Видно, что Neo-6M обладает огромным потенциалом, но детально описать все его возможности не хватит места, поэтому ограничимся предлагаемыми из коробки: только UART на скорости 9600, только NMEA, частота импульсов — 1 Гц.
В плане подключения все предельно просто: линии VCC, GND, RX и TX на приемнике подключаем к +3.3V, GND, PA9 и PA10 на Discovery соответственно.
Программа
Она должна отображать текущее положение приемника, скорость, направление движения, факторы снижения точности, время, дату, а еще показывать в полярной системе координат используемые спутники. Вот примерно так, как это делает u-center на рис. 2.

Как только строка line от Neo-6M принимается контроллером, происходит ее разбитие на токены (массив charTokens) — на подстроки, которые в исходной строке разделены запятыми.
char *token = malloc(strlen(line) + 1);
char *token2 = malloc(strlen(line) + 1);
int currentTokenNumber = 0;
int currentCharInTokenNumber = 0;
strcpy(token, line);
char *delimeter = ",";
while (token != NULL) {
token2 = strpbrk(token + 1, delimeter);
if (token2 == NULL) {
// В конце строки меняем разделитель на "*"
delimeter = "*";
token2 = strpbrk(token + 1, "*");
}
/* Копируем часть строки между разделителями */
currentCharInTokenNumber = 0;
/* Очищаем значение токена */
memset(charTokens[currentTokenNumber], '', MAX_TOKEN_LENGTH);
for (char *ch = token + 1; ch < token2; *(ch++)) {
charTokens[currentTokenNumber][currentCharInTokenNumber] = *ch;
currentCharInTokenNumber++;
}
currentTokenNumber++;
if (delimeter[0] == '*') {
token = NULL;
} else {
token = token2;
}
}
Казалось бы, вполне логично использовать функцию strtok, но я этого не делаю. Причину покажу на примере. Пусть имеется строка a,b,,,c. Результат разбития ее на токены с помощью strtok будет таким: 'a', 'b', 'c'. Для разбора NMEA это недопустимо, поскольку в этом протоколе значения токенов зависят от положения в строке. Результат работы описанного выше метода включает в себя пустые токены — 'a', 'b', '0', '0' 'c'.
Для удобного хранения информации о положении приемника, о точности определения положения, а также о параметрах спутников были написаны три структуры данных.
Положение и скорость приемника, а также дата и время:
struct _minimumNavigationInfo {
float latitude;
char latModificator; // East or West
float longitude;
char lonModificator; // North or South
float groundSpeed;
float speedAngle;
float height;
char heightModificator; // Metres or smth else
char time[9]; // "hh:mm:ss"
char date[9]; // "DD.MM.YY"
char isValid;
};
Структура точности определения координат:
struct _fixInfo {
double PDOP;
double HDOP;
double VDOP;
};
Структура о номере спутника, его положении и качестве сигнала:
struct _satelliteInfo {
int satelliteId;
float height;
float azimuth;
float SNR; //signal-to-noise ratio -- соотношение сигнал/шум
int isFull;
};
Если информация о спутнике не полная, например есть информация о высоте и азимуте, но нет о соотношении сигнал/шум, то в поле isFull записывается нулевое значение. Такие спутники при выводе на «радар» будут игнорироваться.
Заполнение структуры на основе массива токенов происходит очень просто: после разбора строки GPGSA в массиве charTokens значения факторов снижения точности *DOP находятся в элементах за номерами 15, 16 и 17.
fixInfo->PDOP = atof(charTokens[15]);
fixInfo->HDOP = atof(charTokens[16]);
fixInfo->VDOP = atof(charTokens[17]);
Теперь можно разобранную информацию смело выводить на экран (рис. 3).

FIN
Теперь ты знаешь, что в GPS тоже нет ничего сложного (если не лезть в дебри), а если тебе хочется понять суть спутниковой навигации, то добро пожаловать на курс от Стэнфорда GPS: An Introduction to Satellite Navigation, with an interactive Worldwide Laboratory using Smartphones или от Университета Миннесоты From GPS and Google Maps to Spatial Computing на Coursera.
А в качестве домашнего задания я поставлю перед тобой три задачи: 1. Добавить возможность записи трека. 2. Заменить монохромный экран на цветной. 3. Вместе с WizFi220 (из номера 188) снабдить устройство возможностью получения A-GPS. Если возникли какие-нибудь вопросы, пиши мне на email, который можно найти в начале статьи. Удачи!
SRC
Весь код ты можешь найти на https://github.com/argrento. Просто скопируй с заменой файлы в папку Template из Standard Peripheral Library.
