99в. GPS-приёмник NEO-6M: NMEA по UART, разбор строк, контрольная сумма, время и координаты, PPS
В разделах 99–99б датчики отвечали на запрос: прошивка читала регистры
по I²C или SPI. Модуль GPS устроен наоборот — он говорит сам, текстом,
раз в секунду, и молчать его не попросишь. Прошивке надо не потерять ни
байта, собрать из потока строки, отбросить битые и вытащить из полей
время, координаты и качество решения — без sscanf и без float. Для
точного времени у модуля есть второй выход — импульс PPS на границе
секунды. Мини-проект 198н собирает такой приёмник на STM32G474 с
32-битным TIM2 и калибровкой RTC; здесь — основа на STM32F103 и
недорогом модуле NEO-6M: строки и их сумма, приём через кольцо DMA,
разбор полей, расстояние между точками, PPS на 16-битном таймере и
настройка модуля двоичными командами UBX.
Опоры: RM0008 (DocID13902 Rev 17): частота таймеров на APB1 удваивается,
с. 93; запросы DMA1, таблица 78, с. 281 (USART1_RX — канал 5); делитель
скорости USART, с. 799; флаг IDLE, с. 820; флаги CC1OF, с. 409, и
CC1IF, UIF, с. 410. DocID13587 Rev 17 (STM32F103x8/xB): PA0 —
TIM2_CH1_ETR без допуска 5 В, с. 29; PA9 и PA10 — USART1_TX и
USART1_RX с допуском 5 В (FT), с. 31. Документов u-blox на NEO-6M и
стандарта NMEA 0183 в проекте нет. Формат кадра UBX, номера классов и
команд, раскладка CFG-PRT, CFG-RATE и CFG-RST, коды горячего, тёплого и
холодного старта — по заголовку protocol.h модемной библиотеки Zephyr
4.4.2; начальная скорость 9600 бит/с и период решения 1000 мс — по
описанию модулей u-blox там же (u-blox,common.yaml); порядок полей RMC
и GGA и сумма XOR — по разбору NMEA в Zephyr (gnss_nmea0183.c) и rusEFI
(nmea.cpp). Для NEO-6M всё это не сверялось. Все числа ниже — прогон
ядра _verify/gps99d/gps99d.c на компьютере (gcc 12.2.0) со своей
моделью приёмника, линии, кольца DMA и TIM2 на Python 3.11; прошивка
собрана armclang 6.24 под Cortex-M3.
Как это называется в документации
Оригинал Перевод GNSS receiver, fix приёмник спутниковой навигации; решение: координаты найдены NMEA sentence, talker ID строка NMEA; кто говорит: GP — GPS, GN — несколько систем RMC, GGA, GSV, VTG минимум навигации; решение с высотой; спутники в поле зрения; курс и скорость checksum контрольная сумма: XOR символов между $и*fix quality, HDOP качество решения; ухудшение точности по горизонтали UBX protocol, class, message ID двоичный протокол u-blox; класс и номер команды CFG-PRT, CFG-RATE, CFG-RST настройка порта; частота решений; сброс и старт ACK-ACK, ACK-NAK команда принята; команда отвергнута cold, warm, hot start холодный, тёплый, горячий старт time pulse, PPS импульс в начале каждой секунды overcapture новый захват при непрочитанном старом
99в.1 Что выдаёт модуль и почему нет решения в помещении
NEO-6M — приёмник GPS фирмы u-blox; продаётся обычно на маленькой плате с антенной, стабилизатором и разъёмом на четыре вывода: питание, земля, TX, RX. На части плат выведен и PPS. Связь — UART 8N1. Начальная скорость у модулей u-blox, по описанию в Zephyr, — 9600 бит/с, решение — раз в 1000 мс; документа на сам NEO-6M в проекте нет, так что для него это не проверено — проверьте терминалом.
| Сигнал | Вывод F103 | Периферия |
|---|---|---|
| TX модуля → приём | PA10 | USART1_RX, DMA1 канал 5, FT |
| RX модуля ← передача | PA9 | USART1_TX, FT |
| PPS | PA0 | TIM2_CH1, без допуска 5 В |
На 9600 бит/с байт с началом и стопом — 10 бит, 1,04 мс, в секунду — 960 байт. В модели стенда раз в секунду идёт пачка RMC, VTG, GGA, GSA, три GSV и GLL: при решении 493,2 байта в среднем и до 504 байт, 525 мс на линии — 52,5 % пропускной способности. Какой набор у настоящего модуля, задаёт его прошивка; для расчёта берите свою пачку из терминала.
Холодный и тёплый старт. Чтобы найти координаты, приёмнику нужны время, грубое положение и данные об орбитах спутников. Всё это он принимает со спутников же, и это небыстро. Если сведения сохранились (у модуля резервное питание, его не выключали надолго) — старт тёплый или горячий, решение появляется скорее. Если ничего нет — холодный, самый долгий. Сколько секунд занимает каждый, написано в документе на модуль — его нет, поэтому без чисел. Команда CFG-RST (99в.6) умеет нарочно стереть сохранённое и устроить любой из трёх стартов.
Почему нет решения в помещении. Сигнал спутника у земли очень
слабый, стены и крыша ослабляют его ещё сильнее, а отражения от стен
искажают дальность. В комнате модуль часто видит спутники в GSV, но
решения не даёт: RMC со статусом V, GGA с качеством 0, пустые поля
координат. Это не поломка — антенну к окну или на улицу. Прошивка
должна это переживать: пока решения нет, поля пустые, и разбор не должен
считать пустое поле нулём.
99в.2 Строка NMEA и контрольная сумма
Строка начинается с $, дальше адрес — кто говорит и что за строка
(GPRMC: GP — GPS, RMC — тип), поля через запятую, после * — две
шестнадцатеричные цифры суммы и конец строки \r\n. Пачка одной секунды
из модели, с решением, на ходу на восток:
$GPRMC,120140.00,A,5545.12490,N,03737.95132,E,19.438,90.00,231026,,,A*61
$GPVTG,90.00,T,,M,19.438,N,36.000,K,A*06
$GPGGA,120140.00,5545.12490,N,03737.95132,E,1,06,1.30,156.5,M,14.2,M,,*5D
$GPGSA,A,3,02,04,05,09,12,15,,,,,,,1.82,1.30,1.43*00
$GPGSV,3,1,11,02,10,020,28,04,17,057,22,05,24,094,30,09,31,131,38*7E
$GPGSV,3,2,11,12,38,168,19,15,45,205,20,17,52,242,44,21,59,279,35*77
$GPGSV,3,3,11,24,66,316,21,26,73,353,29,29,80,030,36*43
$GPGLL,5545.12490,N,03737.95132,E,120140.00,A,A*6C
Четыре строки, которые разбирает ядро:
| Строка | Поля по порядку |
|---|---|
| RMC | время hhmmss.ss, статус A/V, широта ddmm.mmmmm, N/S, долгота dddmm.mmmmm, E/W, скорость в узлах, курс, дата ddmmyy |
| GGA | время, широта, N/S, долгота, E/W, качество решения, спутников в решении, HDOP, высота, M, высота геоида, M |
| GSV | строк в серии, номер строки, спутников видно, и по четыре поля на спутник: номер, угол места, азимут, сигнал/шум |
| VTG | курс истинный, T, курс магнитный, M, скорость в узлах, N, скорость в км/ч, K |
Порядок полей RMC и GGA — как в разборе Zephyr и в таблицах в исходнике rusEFI; GSV — как у Zephyr; VTG и поле режима в конце RMC — по общеизвестному описанию, не сверялось. Качество в GGA: 0 — решения нет, 1 — GPS, 2 — дифференциальное (номера по перечислению Zephyr). GSA и GLL ядро пропускает: всё нужное есть в остальных.
Сумма. XOR всех символов между $ и *, не включая их. У строки
GPVTG,,,,,,,,,N — пачки до решения — первые шесть шагов дают 47, 17,
41, 15, 52, 7E, итог — 0x30. У RMC из примера тело 68 символов, сумма
0x61 — её вывел и Python своим кодом, и ядро на Си. XOR ловит любую
одну испорченную цифру, но не перестановку двух символов и не две
одинаковые ошибки в одном бите — для шума на линии этого хватает.
Сборка строки — автомат по байту: ждём $, копим тело и XOR, после *
две цифры, затем \r или \n. Всё, что не сходится, считается и
выбрасывается:
case S_BODY:
if (c == '*') {
r->line[r->len] = '\0';
r->st = S_HEX1;
} else if (c < 0x20u || c > 0x7Eu || c == '$') {
return restart(r, c); /* конец строки без суммы, новый $, двоичный байт */
} else if (r->len == GPS_LINE_MAX) {
r->too_long++;
r->st = S_IDLE;
} else {
r->line[r->len++] = (char)c;
r->cs ^= c; /* XOR всего между $ и * */
}
return GPS_NONE;
case S_HEX1:
case S_HEX2:
h = hexval(c);
if (h < 0)
return restart(r, c);
r->cs_rx = (uint8_t)(r->st == S_HEX1 ? h << 4 : r->cs_rx | h);
r->st++;
return GPS_NONE;
case S_END:
if (c != '\r' && c != '\n')
return restart(r, c);
r->st = S_IDLE;
if (r->cs != r->cs_rx) {
r->bad_cs++;
return GPS_NONE;
}
r->lines++;
return GPS_NMEA;
Новый $ посреди строки — прошлая оборвалась, но новая начинается сразу.
Строка длиннее 96 символов отбрасывается (у NMEA 0183, по общеизвестному
описанию, — до 82 вместе с $ и \r\n). Байт 0xB5 начинает двоичный
кадр UBX — те же провода несут ответы на команды (99в.6), и автомат
разбирает оба протокола.
99в.3 Приём: кольцо DMA, сборка строк и битый поток
Байты кладёт DMA1, канал 5 (запрос USART1_RX, таблица 78), в кольцо
256 байт по кругу — так же, как в разделе 16б, где кольцо, счётчик CNDTR
и три события разобраны подробно. Разбор зовут половина и конец кольца
(HT, TC) и тишина на линии (IDLE, с. 820): тишина отдаёт хвост
пачки сразу, не дожидаясь половины.
DMA1_Channel5->CCR = 0;
DMA1_Channel5->CPAR = (uint32_t)&USART1->DR;
DMA1_Channel5->CMAR = (uint32_t)rx_buf;
DMA1_Channel5->CNDTR = RX_SIZE;
DMA1->IFCR = DMA_IFCR_CGIF5;
DMA1_Channel5->CCR = DMA_CCR5_MINC | DMA_CCR5_CIRC | DMA_CCR5_HTIE | DMA_CCR5_TCIE | DMA_CCR5_EN;
USART1->BRR = CPU_HZ / 9600u; /* 0x1D4C: мантисса 468, дробь 12/16 */
USART1->CR3 = USART_CR3_DMAR | USART_CR3_EIE;
USART1->CR1 = USART_CR1_UE | USART_CR1_RE | USART_CR1_TE | USART_CR1_IDLEIE;
BRR = 0x1D4C: 72 МГц / (16 × 468,75) — ровно 9600 бит/с (формула на
с. 799). Функция gps_ring_poll берёт CNDTR, считает позицию записи
256 − CNDTR и прогоняет через автомат всё от позиции чтения до неё.
Готовую строку она отдаёт обратному вызову, а тот — разбору полей.
Стенд: модель приёмника гонит 600 секунд потока и портит его. Каждая
41-я строка — одна цифра заменена, сумма прежняя; каждая 97-я — оборвана
до *; раз в 200 с — строка длиннее буфера; раз в 150 с — 40 байт мусора
между пачками; на 100-й секунде — ответ ACK-ACK, на 200-й — он же с
неверной суммой. Решения нет до 40-й секунды и с 300-й по 319-ю
(«тоннель»). Всего 286 802 байта, 4760 строк, из них целых — 4602.
| Как забирать | Кольцо | Принято строк | Неверная сумма | Оборвано | Длинных | RMC |
|---|---|---|---|---|---|---|
| HT, TC, IDLE | 256 | 4602 | 115 | 43 | 3 | 579 |
| опрос раз в 100 мс | 256 | 4602 | 115 | 43 | 3 | 579 |
| опрос раз в 250 мс | 256 | 4602 | 115 | 43 | 3 | 579 |
| опрос раз в 500 мс | 256 | 1706 | 43 | 14 | 0 | 10 |
| опрос раз в 500 мс | 1024 | 4602 | 115 | 43 | 3 | 579 |
По событиям счётчики совпали с подложенным ровно: 115 замен, 43 обрыва, 3 длинных строки; мусор между пачками ничего не сломал, ACK-ACK принят, битый кадр UBX отброшен. Каждая принятая строка разобрана ядром так же, как своим разбором на Python, — 3441 напечатанная строка совпала.
Кольцо 256 байт заполняется за 266,7 мс. Опрос раз в 250 мс ещё успевает, раз в 500 мс — нет: DMA молча переписывает непрочитанное, и из 579 RMC остаются 10. Сумма тут почти не помогает: переписанные строки не приходят с неверной суммой, они не приходят вовсе. Опрос из главного цикла годится только с кольцом больше пачки — 1024 байта; надёжнее события.
99в.4 Разбор полей без sscanf и float
nmea_split заменяет запятые нулями и отдаёт массив указателей на поля;
пустое поле остаётся пустой строкой, и каждая функция разбора на пустой
строке отвечает ошибкой, а не нулём. Числа — целые с фиксированной
точкой: nmea_fixed("1.30", 2) даёт 130, лишние знаки отбрасываются.
sscanf и atof не нужны: они тянут в прошивку библиотеку разбора и
float, а у Cortex-M3 нет блока плавающей точки.
Координата. ddmm.mmmmm — градусы и минуты вместе. Минуты с шестью
знаками после точки — целое до 59 999 999, и деление на 6 сразу даёт
градусы × 10⁷, потому что 10⁶ / 60 = 10⁷ / 6:
uint32_t m6 = min * 1000000u + frac; /* минуты × 10⁶ */
uint32_t v = deg * 10000000u + (m6 + 3u) / 6u; /* минуты × 10⁶ / 60 = градусы × 10⁷ / 6 → с округлением */
if (v > lim * 10000000u)
return -1;
*e7 = hemi[0] == 'S' || hemi[0] == 'W' ? -(int32_t)v : (int32_t)v;
return 0;
+ 3 — округление половины вверх. Юг и запад — минус. В модели
5545.12490,N даёт 557 520 817, 03737.95132,E — 376 325 220;
3436.22800,S — −346 038 000, 05822.89300,W — −583 815 500
(−34,6038° и −58,38155°). Проверка на 20 012 значениях против точной
дроби на Python: расхождений 0, включая 12 крайних — пустое поле, 60
минут, больше 90° широты и 180° долготы, чужая буква полушария. Ошибка
перевода — не больше 0,3333 единицы 10⁻⁷ градуса при пяти знаках минут
(3,71 мм по широте) и 0,5 при семи (5,56 мм). Единица 10⁻⁷ градуса широты
— 1,112 см, а последний знак минут у пяти знаков — 1,85 см: int32_t в
градусах × 10⁷ хранит координату точнее, чем её отдаёт модуль.
Время и дата. hhmmss.ss — миллисекунды от полуночи, ddmmyy —
день, месяц и год 2000 + yy. Из них ядро считает секунды Unix целым
делением (дни от 1 марта, как в календаре). Прогон, сверенный с
datetime на Python:
| Время, дата UTC | Секунды Unix | Москва, UTC+3 |
|---|---|---|
| 120005.00, 231026 | 1792756805 | 23.10.2026 15:00:05 |
| 233000.00, 311226 | 1798759800 | 01.01.2027 02:30:00 |
| 235959.99, 290228 | 1835481599 | 01.03.2028 02:59:59 |
| 000000.00, 010100 | 946684800 | 01.01.2000 03:00:00 |
Местное время — плюс сдвиг к секундам Unix, а не к часам: так 23:30 по UTC 31 декабря правильно становится 1 января. Час 24, день 32 и месяц 13 разбор отвергает.
Решение. Координаты берутся только из RMC со статусом A и GGA с
качеством больше 0; при V прежние координаты остаются, но решения
нет. gps_has_fix — оба условия сразу. В потоке из 99в.3 решение
появилось в 12:00:40, пропало в 12:05:00 и вернулось в 12:05:20 — ровно
по модели. HDOP — ×100, у примера 130; пустое поле — 0xFFFF. Число
спутников в решении — из GGA, видимых и лучший сигнал — из серии GSV.
99в.5 Десятичные градусы и расстояние между точками
Десятичные градусы — градусы × 10⁷, делённые на 10⁷: 557 520 817 —
55,7520817°. Расстояние между двумя точками на шаре радиуса 6371 км даёт
формула гаверсинусов. На стенде она посчитана в double на Си и
отдельно на Python — совпали до 10⁻⁶ м (точность печати); это эталон.
На МК — три варианта во float:
- из градусов: координаты сначала во
float, потом формула. Уfloat24 бита мантиссы: у 55° шаг — 3,815·10⁻⁶ градуса, 0,424 м; - разности в целых: разность широт и долгот считается в целых
градусах × 10⁷ и только потом идёт во
float— точно; - плоская: те же разности, но вместо гаверсинусов —
√((Δλ·cos φ)² + Δφ²), без
sinиasin.
float gps_dist_hav(int32_t lat1, int32_t lon1, int32_t lat2, int32_t lon2)
{
float dphi = (float)((int64_t)lat2 - lat1) * E7RAD; /* разность — в целых, точно */
float dlmb = (float)dlon_e7(lon1, lon2) * E7RAD;
return hav(dphi, dlmb, (float)lat1 * E7RAD, (float)lat2 * E7RAD);
}
Наибольшая ошибка против эталона по 200 случайным парам на каждое расстояние, база — 55,75° с. ш. 37,63° в. д., метры:
| Расстояние | float из градусов | разности в целых | плоская |
|---|---|---|---|
| 1 м | 0,7656 | 0,0000 | 0,0000 |
| 10 м | 0,7106 | 0,0000 | 0,0000 |
| 100 м | 0,8233 | 0,0000 | 0,0000 |
| 1 км | 0,6012 | 0,0001 | 0,0002 |
| 10 км | 0,9380 | 0,0021 | 0,0040 |
| 100 км | 0,8003 | 0,0193 | 2,7058 |
| 1000 км | 0,6498 | 0,1839 | 2812,1652 |
«Из градусов» ошибается почти на метр на любом расстоянии — это шаг
float у 55°, и на метровом отрезке он съедает весь результат.
Гаверсинус на целых разностях ошибается на 1,9 см на 100 км и на 18 см
на 1000 км — около 2·10⁻⁷ расстояния. Плоская формула годится до десятков
километров: 4 мм на 10 км, 2,7 м на 100 км. Сферу вместо эллипсоида
прогон не оценивал — это отдельная ошибка модели Земли.
Через меридиан 180° долготы 179,9999° и −179,9999° дают разность
−359,9998° — и −3,6·10⁹ в градусах × 10⁷, что в int32_t не влезает.
Ядро вычитает в int64_t и поправляет на ±360°: обе формулы на целых
дают 22,238985 м, как эталон, а плоская без поправки насчитала бы
40 030,2 км. Путь в потоке 99в.3 — 522 точки с решением, 4800,002 м по
эталону; гаверсинус и плоская дали 4799,998, «из градусов» — 4799,958:
на прямом пути ошибки соседних отрезков гасят друг друга, на одном
отрезке — нет.
99в.6 PPS, точное время и настройка командами UBX
PPS. Фронт PPS стоит на границе секунды; строка со временем этой
секунды приходит позже, и какую секунду она описывает, решает модуль
(раздел 198н.6). Есть ли импульс без решения, какой он длины и
полярности — по документу на модуль, его нет; модель считает фронт
нарастающим. Захват — как в разделе 13.6: фронт на PA0 защёлкивает
счётчик TIM2 в CCR1, задержка прерывания на результат не влияет.
TIM2 у F103 — 16-битный. Без делителя он считает 72 МГц: отсчёт 13,89 нс,
круг — 910,2 мкс, за секунду — 1098,6 переполнения. Метку собирают из
счёта переполнений (старшие 16 бит) и CCR1 (младшие); 32-битная метка
обходит круг за 59,65 с, а разность соседних меток по модулю 2³² верна и
через круг. Подвох — когда в одном входе в прерывание стоят оба флага,
UIF и CC1IF (с. 410). Переполнение могло быть и до захвата, и после.
Решает сам захват: маленький CCR1 — счётчик уже перевалил, большой —
ещё нет:
uint32_t pps_isr(pps_t *p, uint32_t sr_uif, uint32_t sr_cc, uint16_t ccr)
{
uint32_t got = 0;
if (sr_cc) {
uint32_t hi = p->ovf;
if (sr_uif && ccr < 0x8000u)
hi++; /* переполнение было раньше захвата, но ещё не сосчитано */
got = stamp(p, hi << 16 | ccr);
}
if (sr_uif)
p->ovf++;
return got;
}
void TIM2_IRQHandler(void)
{
uint32_t sr = TIM2->SR;
uint16_t ccr = 0;
if (sr & TIM_SR_CC1IF)
ccr = (uint16_t)TIM2->CCR1; /* чтение снимает CC1IF */
if (sr & TIM_SR_UIF)
TIM2->SR = ~TIM_SR_UIF; /* rc_w0: ноль снимает только UIF */
if (pps_isr(&pps, sr & TIM_SR_UIF, sr & TIM_SR_CC1IF, ccr))
pps_count++;
}
Модель: кварц спешит на 20 млн⁻¹, фронт PPS дрожит на ±50 нс, вход в прерывание задержан на случайное время 0…L, 1000 секунд, импульс 500-й секунды пропущен. «Наивно» — сначала прибавить переполнение, потом собрать метку:
| L, мкс | 0 | 5 | 20 | 100 | 400 |
|---|---|---|---|---|---|
| неверных меток наивно, из 999 | 0 | 2 | 8 | 60 | 221 |
| неверных меток с проверкой | 0 | 0 | 0 | 0 | 0 |
Неверная метка сдвинута на 65 536 тактов — 0,91 мс, а период рядом с ней — на 910 млн⁻¹. Фильтр «секунда ±1 %» такой сдвиг не ловит: это 0,091 % секунды; отброшен только пропуск на 500-й секунде. Проверка по половине круга верна, пока задержка входа меньше 455 мкс, поэтому у TIM2 приоритет выше, чем у разбора строк. Ход кварца по секундам — от 19 902,8 до 20 097,2 млрд⁻¹ (истина 20 000), по окнам 64 с — 19 999…20 000. Сумма окна совпала и у наивного варианта: ошибочная метка портит два соседних периода в разные стороны. Испорченной остаётся сама метка — время фронта, ради которого PPS и заводят. Как по PPS подстраивать часы RTC — раздел 198н.5.
UBX. Двоичные команды u-blox: синхробайты 0xB5 0x62, класс, номер,
длина нагрузки (2 байта, младшим вперёд), нагрузка и сумма из двух байт.
Сумма — Флетчер-8 от класса до конца нагрузки: a += байт, b += a, оба
по модулю 256:
for (uint16_t i = 2; i < 6u + n; i++) { /* сумма — от класса до конца нагрузки */
a = (uint8_t)(a + out[i]);
b = (uint8_t)(b + a);
}
out[6 + n] = a;
out[7 + n] = b;
| Команда | Нагрузка | Кадр | Сумма |
|---|---|---|---|
| CFG-PRT, класс 0x06, номер 0x00 | 20 байт: порт 1 (UART), 8N1 — mode 0x08C0, 38 400 бит/с, вход UBX + NMEA, выход NMEA |
28 байт | 7E 6A |
| CFG-RATE, 0x06 0x08 | период 200 мс, решение на каждое измерение, время GPS | B5 62 06 08 06 00 C8 00 01 00 01 00 DE 6A |
DE 6A |
| CFG-RST, 0x06 0x04, холодный | стереть всё — 0xFFFF, программный сброс | B5 62 06 04 04 00 FF FF 01 00 0D 5F |
0D 5F |
| CFG-RST, тёплый / горячий | 0x0001 / 0x0000 | 12 байт | 10 6A / 0F 66 |
Python собрал те же кадры из описания полей своим кодом — все пять
совпали байт в байт. Ответ — ACK-ACK, класс 0x05, номер 0x01, в нагрузке
класс и номер принятой команды: B5 62 05 01 02 00 06 08 16 3F;
ACK-NAK — номер 0x00. ubx_is_ack отличает их от прочего.
Зачем 38 400 бит/с: пять решений в секунду — пять пачек, 5 × 504 байта.
На 9600 бит/с это 262,5 % пропускной способности — строки не влезут, на
38 400 — 65,6 %. Поэтому прошивка сначала шлёт CFG-PRT на 9600 бит/с,
ждёт ухода последнего стоп-бита (TC), переключает свой BRR на 1875
(0x753, ровно 38 400) и только потом шлёт CFG-RATE. Нет ACK-ACK за 2 с —
обратно на 9600. Сохраняет ли модуль настройку после выключения, зависит
от его памяти и отдельной команды сохранения — без документа не
проверено.
99в.7 Грабли
Нет решения у стола. В помещении сигнал слабый: GSV показывает
спутники, а RMC — V, GGA — 0. Антенну к окну; разбор должен жить с
пустыми полями.
Пустое поле прочиталось как ноль. До решения широта пустая, и
atoi("") даёт 0 — точка в Гвинейском заливе. Пустое поле — ошибка, а
решение — только при A и качестве больше 0.
Юг и запад без знака. Поле N/S и E/W — отдельное; без него Буэнос-Айрес (−34,6038°, −58,38155°) окажется в Иране. Знак ставит разбор координаты.
Координаты во float. Шаг float у 55° — 0,424 м: расстояние на
коротком отрезке ошибается почти на метр. Разности — в целых
градусах × 10⁷, во float — уже разность.
Опрос кольца реже, чем оно заполняется. 256 байт на 9600 бит/с — 266,7
мс; опрос раз в 500 мс оставил 10 RMC из 579, и неверных сумм почти нет —
строки просто пропали. Забирать по HT, TC, IDLE.
Битые строки. Замены, обрывы и длинные строки в прогоне пойманы все:
115, 43 и 3. Без суммы испорченная цифра в координате прошла бы в
решение. Сумма — заглавными шестнадцатеричными цифрами; конец строки —
\r или \n.
Метка PPS на круг мимо. Оба флага в одном входе: без проверки CCR1
по половине круга до 221 неверной метки из 999 при задержке 400 мкс.
Фильтр ±1 % их не видит.
Двоичный кадр глотает текст. Испорченная длина UBX даёт до 65 535 байт «нагрузки», и автомат съел бы секунды NMEA. Кадр длиннее 64 байт отбрасывается сразу.
Сменили скорость модуля, а свою — нет. После CFG-PRT модуль говорит
на 38 400, а USART ещё на 9600 — в кольце мусор. Свой BRR меняют после
ухода кадра, с запасным возвратом.
5 В на PA0. PA9 и PA10 допускают 5 В, PA0 — нет. Если плата модуля выдаёт PPS пятивольтовым уровнем — делитель. Чем питать плату и какой у неё уровень выходов, смотрите по плате: документа на неё в проекте нет.
99в.8 Итоги
- NEO-6M говорит сам: UART 8N1, по описанию u-blox в Zephyr — 9600 бит/с, раз в секунду; пачка модели — до 504 байт, 525 мс, 52,5 % линии.
- Строка NMEA:
$, поля через запятую,*и XOR тела двумя шестнадцатеричными цифрами; RMC — время, статус, координаты, дата; GGA — качество, спутники, HDOP, высота; GSV — видимые спутники; VTG — курс и скорость. - Кольцо DMA 256 байт по
HT,TC,IDLE: принято 4602 целых строки из 4602, поймано 115 замен, 43 обрыва, 3 длинных; опрос раз в 500 мс — 10 RMC из 579. - Координаты — целыми, градусы × 10⁷: минуты × 10⁶ / 6, ошибка до 3,71 мм; 20 012 проверок без расхождений; время — секунды Unix целым счётом.
- Расстояние:
floatиз градусов ошибается до 0,94 м на любом отрезке; гаверсинус на целых разностях — 18 см на 1000 км; плоская — 4 мм на 10 км. - PPS на 16-битном TIM2: 13,89 нс, круг 910,2 мкс; с проверкой
CCR1по половине круга — 0 неверных меток, наивно — до 221 из 999. - UBX: Флетчер-8 от класса до конца нагрузки; CFG-RATE 200 мс —
DE 6A, ACK-ACK —16 3F; 5 решений в секунду требуют 38 400 бит/с.
Полные исходники
gps99d.h — сборка NMEA и UBX, решение, расстояние, PPS, команды UBX, кольцо DMA.
/* gps99d.h — раздел 99в: приём GPS-модуля NEO-6M без регистров — общая часть прошивки fw99d.c и стенда host99d.c.
* Сборка строк NMEA и кадров UBX из потока байтов, разбор полей без sscanf и float, время UTC,
* координаты в градусах × 10⁷, расстояние между точками, метка PPS на 16-битном таймере, кадры UBX. */
#ifndef GPS99D_H
#define GPS99D_H
#include <stdint.h>
#define GPS_LINE_MAX 96u /* тело строки между $ и *: NMEA 0183 — до 82 символов вместе с $ и \r\n */
#define GPS_FIELDS 24u /* GSV — 20 полей, остальные меньше */
#define UBX_MAX 64u /* полезная нагрузка UBX, которую принимаем целиком */
/* ── что отдала сборка ────────────────────────────────────────────────── */
enum { GPS_NONE, GPS_NMEA, GPS_UBX };
enum { NMEA_OTHER, NMEA_RMC, NMEA_GGA, NMEA_GSV, NMEA_VTG, NMEA_BAD };
typedef struct {
uint8_t st; /* состояние автомата */
uint8_t cs, cs_rx; /* XOR тела и сумма из строки */
uint8_t len;
char line[GPS_LINE_MAX + 1u]; /* тело строки: от символа после $ до * */
uint8_t ucls, uid, cka, ckb; /* кадр UBX: класс, номер, сумма Флетчера */
uint16_t ulen, upos;
uint8_t ubx[UBX_MAX];
uint32_t lines, bad_cs, broken, too_long, ubx_ok, ubx_bad;
} gps_rx_t;
void gps_rx_init(gps_rx_t *r);
int gps_rx_byte(gps_rx_t *r, uint8_t c); /* GPS_NMEA: тело в r->line; GPS_UBX: кадр в ucls/uid/ubx */
uint8_t nmea_xor(const char *body); /* сумма тела строки — то, что стоит после * */
/* ── решение ──────────────────────────────────────────────────────────── */
typedef struct {
uint8_t day, month;
uint16_t year;
} gps_date_t;
typedef struct {
uint32_t t_ms; /* время UTC от полуночи, мс */
gps_date_t date;
int32_t lat, lon; /* градусы × 10⁷; юг и запад — минус */
uint8_t rmc_ok; /* RMC: статус A */
uint8_t quality; /* GGA: 0 — нет решения, 1 — GPS, 2 — дифференциальное */
uint8_t nsat; /* GGA: спутников в решении */
uint8_t in_view; /* GSV: спутников видно */
uint16_t hdop; /* GGA: HDOP × 100 */
int32_t alt_cm; /* GGA: высота над уровнем моря, см */
uint32_t speed_kmh; /* VTG: скорость, км/ч × 1000 */
uint32_t course; /* VTG: курс, градусы × 100 */
uint8_t snr_max; /* GSV: лучший сигнал за серию, дБГц */
uint8_t gsv_seen, gsv_total; /* GSV: сколько строк серии пришло и сколько обещано */
uint32_t rmc, gga, gsv, vtg, other, bad_field;
} gps_t;
void gps_init(gps_t *g);
int gps_line(gps_t *g, char *body); /* разобрать тело строки (портит его: запятые → '\0') */
int gps_has_fix(const gps_t *g); /* RMC «A» и GGA с качеством 1 и больше */
/* разбор полей — по одному, для проверки на стенде */
int nmea_split(char *body, const char *f[], int max);
int nmea_time(const char *s, uint32_t *ms);
int nmea_date(const char *s, gps_date_t *d);
int nmea_coord(const char *s, const char *hemi, int32_t *e7);
int nmea_fixed(const char *s, int dec, int32_t *v);
uint32_t gps_unix(const gps_date_t *d, uint32_t t_ms); /* секунды от 1970-01-01 UTC */
/* ── расстояние между точками (градусы × 10⁷), метры ─────────────────── */
float gps_dist_naive(int32_t lat1, int32_t lon1, int32_t lat2, int32_t lon2); /* float из градусов — так нельзя */
float gps_dist_hav(int32_t lat1, int32_t lon1, int32_t lat2, int32_t lon2); /* разности в целых, гаверсинус во float */
float gps_dist_flat(int32_t lat1, int32_t lon1, int32_t lat2, int32_t lon2); /* разности в целых, плоское приближение */
/* ── PPS: 32-битная метка из 16-битного захвата и счёта переполнений ──── */
typedef struct {
uint32_t ovf; /* переполнений таймера */
uint32_t last; /* прошлая метка */
uint8_t have;
uint32_t period; /* последний период, такты */
uint32_t seconds, rejected;
int64_t sum; /* тактов за окно */
uint32_t n; /* секунд в окне */
} pps_t;
void pps_init(pps_t *p);
uint32_t pps_isr(pps_t *p, uint32_t sr_uif, uint32_t sr_cc, uint16_t ccr); /* 1 — пришла новая метка */
uint32_t pps_isr_naive(pps_t *p, uint32_t sr_uif, uint32_t sr_cc, uint16_t ccr);
#define PPS_HZ 72000000u /* такт TIM2 на F103: APB1 36 МГц × 2 */
/* ── UBX ──────────────────────────────────────────────────────────────── */
#define UBX_CFG 0x06u
#define UBX_ACK 0x05u
#define UBX_CFG_PRT 0x00u
#define UBX_CFG_RST 0x04u
#define UBX_CFG_RATE 0x08u
uint16_t ubx_frame(uint8_t cls, uint8_t id, const uint8_t *pl, uint16_t n, uint8_t *out);
uint16_t ubx_cfg_prt(uint32_t baud, uint16_t in_proto, uint16_t out_proto, uint8_t *out);
uint16_t ubx_cfg_rate(uint16_t meas_ms, uint16_t nav, uint16_t time_ref, uint8_t *out);
uint16_t ubx_cfg_rst(uint16_t bbr, uint8_t mode, uint8_t *out);
int ubx_is_ack(const gps_rx_t *r, uint8_t cls, uint8_t id); /* 1 — ACK-ACK, 0 — ACK-NAK, -1 — не ответ */
/* ── кольцо DMA: забрать всё от позиции чтения до позиции записи ────── */
typedef struct {
const uint8_t *buf;
uint16_t size, rd;
} gps_ring_t;
typedef void (*gps_on_t)(int kind, gps_rx_t *r, void *ctx);
void gps_ring_poll(gps_ring_t *q, uint16_t ndtr, gps_rx_t *r, gps_on_t on, void *ctx);
#endif
gps99d.c — ядро без регистров: общая часть прошивки и стенда.
/* gps99d.c — раздел 99в: сборка NMEA и UBX, разбор полей, время, координаты, расстояние, PPS (см. gps99d.h). */
#include <math.h>
#include "gps99d.h"
/* ── сборка строк NMEA и кадров UBX из потока ─────────────────────────── */
enum { S_IDLE, S_BODY, S_HEX1, S_HEX2, S_END, U_SYNC2 = 10, U_CLASS, U_ID, U_LEN1, U_LEN2, U_DATA, U_CKA, U_CKB };
void gps_rx_init(gps_rx_t *r)
{
*r = (gps_rx_t){0};
}
static int hexval(uint8_t c)
{
if (c >= '0' && c <= '9')
return c - '0';
if (c >= 'A' && c <= 'F')
return c - 'A' + 10;
return -1;
}
static int restart(gps_rx_t *r, uint8_t c) /* строка оборвалась; c может начать новую */
{
r->broken++;
r->st = c == '$' ? S_BODY : c == 0xB5u ? U_SYNC2 : S_IDLE;
r->len = 0;
r->cs = 0;
return GPS_NONE;
}
static void ck(gps_rx_t *r, uint8_t c) /* Флетчер-8: две суммы по модулю 256 */
{
r->cka = (uint8_t)(r->cka + c);
r->ckb = (uint8_t)(r->ckb + r->cka);
}
int gps_rx_byte(gps_rx_t *r, uint8_t c)
{
int h;
switch (r->st) {
case S_IDLE: /* между строками всё, кроме $ и 0xB5, — мусор */
if (c == '$') {
r->st = S_BODY;
r->len = 0;
r->cs = 0;
} else if (c == 0xB5u) {
r->st = U_SYNC2;
}
return GPS_NONE;
case S_BODY:
if (c == '*') {
r->line[r->len] = '\0';
r->st = S_HEX1;
} else if (c < 0x20u || c > 0x7Eu || c == '$') {
return restart(r, c); /* конец строки без суммы, новый $, двоичный байт */
} else if (r->len == GPS_LINE_MAX) {
r->too_long++;
r->st = S_IDLE;
} else {
r->line[r->len++] = (char)c;
r->cs ^= c; /* XOR всего между $ и * */
}
return GPS_NONE;
case S_HEX1:
case S_HEX2:
h = hexval(c);
if (h < 0)
return restart(r, c);
r->cs_rx = (uint8_t)(r->st == S_HEX1 ? h << 4 : r->cs_rx | h);
r->st++;
return GPS_NONE;
case S_END:
if (c != '\r' && c != '\n')
return restart(r, c);
r->st = S_IDLE;
if (r->cs != r->cs_rx) {
r->bad_cs++;
return GPS_NONE;
}
r->lines++;
return GPS_NMEA;
case U_SYNC2:
r->st = c == 0x62u ? U_CLASS : c == '$' ? S_BODY : S_IDLE;
r->len = 0;
r->cs = 0;
return GPS_NONE;
case U_CLASS:
r->cka = r->ckb = 0;
ck(r, c);
r->ucls = c;
r->st = U_ID;
return GPS_NONE;
case U_ID:
ck(r, c);
r->uid = c;
r->st = U_LEN1;
return GPS_NONE;
case U_LEN1:
ck(r, c);
r->ulen = c;
r->st = U_LEN2;
return GPS_NONE;
case U_LEN2:
ck(r, c);
r->ulen |= (uint16_t)(c << 8); /* длина — младшим байтом вперёд */
r->upos = 0;
if (r->ulen > UBX_MAX) { /* не наш ответ или сбой: не глотать килобайты NMEA */
r->ubx_bad++;
r->st = S_IDLE;
} else {
r->st = r->ulen ? U_DATA : U_CKA;
}
return GPS_NONE;
case U_DATA:
ck(r, c);
r->ubx[r->upos++] = c;
if (r->upos == r->ulen)
r->st = U_CKA;
return GPS_NONE;
case U_CKA:
r->st = c == r->cka ? U_CKB : S_IDLE;
if (c != r->cka)
r->ubx_bad++;
return GPS_NONE;
default: /* U_CKB */
r->st = S_IDLE;
if (c != r->ckb) {
r->ubx_bad++;
return GPS_NONE;
}
r->ubx_ok++;
return GPS_UBX;
}
}
uint8_t nmea_xor(const char *body)
{
uint8_t cs = 0;
while (*body)
cs ^= (uint8_t)*body++;
return cs;
}
/* ── поля ─────────────────────────────────────────────────────────────── */
int nmea_split(char *body, const char *f[], int max)
{
int n = 0;
f[n++] = body;
for (char *p = body; *p; p++) {
if (*p == ',') {
*p = '\0'; /* пустое поле остаётся пустой строкой */
if (n == max)
return -1;
f[n++] = p + 1;
}
}
return n;
}
static int digits(const char *s, int n, uint32_t *v) /* ровно n цифр подряд */
{
uint32_t x = 0;
for (int i = 0; i < n; i++) {
if (s[i] < '0' || s[i] > '9')
return -1;
x = x * 10u + (uint32_t)(s[i] - '0');
}
*v = x;
return 0;
}
int nmea_fixed(const char *s, int dec, int32_t *v) /* «-12.345» при dec = 2 → -1234; лишние знаки отбрасываются */
{
int neg = *s == '-';
uint32_t x = 0;
int nd = 0, frac = -1;
s += neg;
if (!*s)
return -1;
for (; *s; s++) {
if (*s == '.' && frac < 0) {
frac = 0;
} else if (*s >= '0' && *s <= '9') {
if (frac >= dec)
continue;
if (++nd > 9)
return -1; /* не влезет в 32 бита */
x = x * 10u + (uint32_t)(*s - '0');
if (frac >= 0)
frac++;
} else {
return -1;
}
}
for (frac = frac < 0 ? 0 : frac; frac < dec; frac++)
x *= 10u;
*v = neg ? -(int32_t)x : (int32_t)x;
return 0;
}
int nmea_time(const char *s, uint32_t *ms) /* hhmmss.sss → мс от полуночи */
{
uint32_t hh, mm, ss, f = 0;
if (digits(s, 2, &hh) || digits(s + 2, 2, &mm) || digits(s + 4, 2, &ss) || hh > 23 || mm > 59 || ss > 60)
return -1;
if (s[6] == '.') {
uint32_t k = 100;
for (s += 7; *s >= '0' && *s <= '9'; s++, k /= 10u)
f += (uint32_t)(*s - '0') * k;
} else if (s[6]) {
return -1;
}
*ms = ((hh * 60u + mm) * 60u + ss) * 1000u + f;
return 0;
}
int nmea_date(const char *s, gps_date_t *d) /* ddmmyy */
{
uint32_t dd, mo, yy;
if (digits(s, 2, &dd) || digits(s + 2, 2, &mo) || digits(s + 4, 2, &yy) || s[6] || dd < 1 || dd > 31 || mo < 1 || mo > 12)
return -1;
d->day = (uint8_t)dd;
d->month = (uint8_t)mo;
d->year = (uint16_t)(2000u + yy);
return 0;
}
int nmea_coord(const char *s, const char *hemi, int32_t *e7)
{
int dot = 0;
while (s[dot] && s[dot] != '.')
dot++;
if (s[dot] != '.' || dot < 3 || dot > 5)
return -1; /* ddmm.mmmm или dddmm.mmmm */
uint32_t deg, min, frac = 0, lim;
if (hemi[0] == 'N' || hemi[0] == 'S')
lim = 90u;
else if (hemi[0] == 'E' || hemi[0] == 'W')
lim = 180u;
else
return -1;
if (digits(s, dot - 2, °) || digits(s + dot - 2, 2, &min) || min > 59u || deg > lim)
return -1;
const char *p = s + dot + 1;
for (int i = 0; i < 6; i++) { /* доли минуты — ровно 6 знаков: недостающие нули */
frac *= 10u;
if (*p >= '0' && *p <= '9')
frac += (uint32_t)(*p++ - '0');
else if (*p)
return -1;
}
while (*p >= '0' && *p <= '9')
p++; /* седьмой знак и дальше — меньше 2·10⁻⁸ градуса */
if (*p)
return -1;
uint32_t m6 = min * 1000000u + frac; /* минуты × 10⁶ */
uint32_t v = deg * 10000000u + (m6 + 3u) / 6u; /* минуты × 10⁶ / 60 = градусы × 10⁷ / 6 → с округлением */
if (v > lim * 10000000u)
return -1;
*e7 = hemi[0] == 'S' || hemi[0] == 'W' ? -(int32_t)v : (int32_t)v;
return 0;
}
static int32_t days_from_civil(int32_t y, uint32_t m, uint32_t d)
{
y -= m <= 2u; /* год начинается с марта: февраль — в конце */
int32_t era = y / 400;
uint32_t yoe = (uint32_t)(y - era * 400);
uint32_t doy = (153u * (m > 2u ? m - 3u : m + 9u) + 2u) / 5u + d - 1u;
uint32_t doe = yoe * 365u + yoe / 4u - yoe / 100u + doy;
return era * 146097 + (int32_t)doe - 719468;
}
uint32_t gps_unix(const gps_date_t *d, uint32_t t_ms)
{
return (uint32_t)days_from_civil(d->year, d->month, d->day) * 86400u + t_ms / 1000u;
}
/* ── предложения ──────────────────────────────────────────────────────── */
void gps_init(gps_t *g)
{
*g = (gps_t){0};
}
static int pos(const char *f[], int i, int32_t *lat, int32_t *lon)
{
return nmea_coord(f[i], f[i + 1], lat) || nmea_coord(f[i + 2], f[i + 3], lon);
}
static int is(const char *id, const char *what) /* «GPRMC», «GNRMC» → RMC: источник не важен */
{
for (int i = 0; i < 5; i++)
if (!id[i])
return 0;
return id[5] == '\0' && id[2] == what[0] && id[3] == what[1] && id[4] == what[2];
}
static int bad(gps_t *g)
{
g->bad_field++;
return NMEA_BAD;
}
int gps_line(gps_t *g, char *body)
{
const char *f[GPS_FIELDS];
int32_t v, lat, lon;
int n = nmea_split(body, f, GPS_FIELDS);
if (n < 1)
return bad(g);
if (is(f[0], "RMC")) { /* время, статус, широта, N/S, долгота, E/W, узлы, курс, дата */
if (n < 10)
return bad(g);
if (f[1][0] && nmea_time(f[1], &g->t_ms))
return bad(g);
g->rmc_ok = f[2][0] == 'A';
if (g->rmc_ok) {
if (pos(f, 3, &lat, &lon) || nmea_date(f[9], &g->date)) {
g->rmc_ok = 0;
return bad(g);
}
g->lat = lat;
g->lon = lon;
}
g->rmc++;
return NMEA_RMC;
}
if (is(f[0], "GGA")) { /* время, широта, N/S, долгота, E/W, качество, спутники, HDOP, высота */
uint32_t q;
int32_t ns = 0;
if (n < 10 || digits(f[6], 1, &q) || f[6][1] || (f[7][0] && (nmea_fixed(f[7], 0, &ns) || ns < 0 || ns > 99)))
return bad(g);
g->quality = (uint8_t)q;
g->nsat = (uint8_t)ns;
g->hdop = 0xFFFFu;
if (f[8][0] && nmea_fixed(f[8], 2, &v) == 0 && v >= 0 && v < 0xFFFF)
g->hdop = (uint16_t)v;
if (q) {
if (pos(f, 2, &lat, &lon) || nmea_fixed(f[9], 2, &g->alt_cm))
return bad(g);
g->lat = lat;
g->lon = lon;
}
g->gga++;
return NMEA_GGA;
}
if (is(f[0], "GSV")) { /* строк в серии, номер, видно спутников, по 4 поля на спутник */
uint32_t tot, num, view, snr;
if (n < 4 || nmea_fixed(f[1], 0, &v) || (tot = (uint32_t)v) == 0 || nmea_fixed(f[2], 0, &v)
|| (num = (uint32_t)v) == 0 || num > tot || nmea_fixed(f[3], 0, &v))
return bad(g);
view = (uint32_t)v;
if (num == 1u) {
g->snr_max = 0;
g->gsv_seen = 0;
}
for (int i = 7; i < n; i += 4) { /* отношение сигнал/шум; пусто — спутник не ведётся */
if (f[i][0] && nmea_fixed(f[i], 0, &v) == 0 && (snr = (uint32_t)v) > g->snr_max && snr < 100u)
g->snr_max = (uint8_t)snr;
}
g->gsv_seen++;
g->gsv_total = (uint8_t)tot;
g->in_view = (uint8_t)view;
g->gsv++;
return NMEA_GSV;
}
if (is(f[0], "VTG")) { /* курс истинный, T, магнитный, M, узлы, N, км/ч, K */
if (n < 9)
return bad(g);
g->course = 0;
g->speed_kmh = 0;
if (f[1][0]) {
if (nmea_fixed(f[1], 2, &v) || v < 0)
return bad(g);
g->course = (uint32_t)v;
}
if (f[7][0]) {
if (nmea_fixed(f[7], 3, &v) || v < 0)
return bad(g);
g->speed_kmh = (uint32_t)v;
}
g->vtg++;
return NMEA_VTG;
}
g->other++;
return NMEA_OTHER;
}
int gps_has_fix(const gps_t *g)
{
return g->rmc_ok && g->quality >= 1u;
}
/* ── расстояние ───────────────────────────────────────────────────────── */
#define R_EARTH 6371000.0f /* шар среднего радиуса */
#define E7RAD 1.7453292519943295e-9f /* (π / 180) / 10⁷ */
static float hav(float dphi, float dlmb, float phi1, float phi2)
{
float a = sinf(dphi * 0.5f), b = sinf(dlmb * 0.5f);
float h = a * a + cosf(phi1) * cosf(phi2) * b * b;
return 2.0f * R_EARTH * asinf(sqrtf(h < 1.0f ? h : 1.0f));
}
static int64_t dlon_e7(int32_t lon1, int32_t lon2) /* разность долгот через ±180° */
{
int64_t d = (int64_t)lon2 - lon1;
if (d > 1800000000)
d -= 3600000000;
if (d < -1800000000)
d += 3600000000;
return d;
}
float gps_dist_naive(int32_t lat1, int32_t lon1, int32_t lat2, int32_t lon2)
{
float la1 = (float)lat1 * 1e-7f, lo1 = (float)lon1 * 1e-7f; /* 24 бита мантиссы: шаг у 55° — 3,8·10⁻⁶ градуса */
float la2 = (float)lat2 * 1e-7f, lo2 = (float)lon2 * 1e-7f;
const float k = 0.017453292519943295f;
return hav((la2 - la1) * k, (lo2 - lo1) * k, la1 * k, la2 * k);
}
float gps_dist_hav(int32_t lat1, int32_t lon1, int32_t lat2, int32_t lon2)
{
float dphi = (float)((int64_t)lat2 - lat1) * E7RAD; /* разность — в целых, точно */
float dlmb = (float)dlon_e7(lon1, lon2) * E7RAD;
return hav(dphi, dlmb, (float)lat1 * E7RAD, (float)lat2 * E7RAD);
}
float gps_dist_flat(int32_t lat1, int32_t lon1, int32_t lat2, int32_t lon2)
{
float y = (float)((int64_t)lat2 - lat1) * E7RAD;
float x = (float)dlon_e7(lon1, lon2) * E7RAD * cosf(((float)lat1 + (float)lat2) * 0.5f * E7RAD);
return R_EARTH * sqrtf(x * x + y * y);
}
/* ── PPS ──────────────────────────────────────────────────────────────── */
void pps_init(pps_t *p)
{
*p = (pps_t){0};
}
static uint32_t stamp(pps_t *p, uint32_t t)
{
if (p->have) {
uint32_t d = t - p->last; /* по модулю 2³²: верно и через круг счёта */
p->period = d;
if (d > PPS_HZ - PPS_HZ / 100u && d < PPS_HZ + PPS_HZ / 100u) {
p->seconds++;
p->sum += d;
p->n++;
} else {
p->rejected++; /* пропуск импульса, помеха или ошибка метки */
}
}
p->last = t;
p->have = 1;
return 1;
}
uint32_t pps_isr(pps_t *p, uint32_t sr_uif, uint32_t sr_cc, uint16_t ccr)
{
uint32_t got = 0;
if (sr_cc) {
uint32_t hi = p->ovf;
if (sr_uif && ccr < 0x8000u)
hi++; /* переполнение было раньше захвата, но ещё не сосчитано */
got = stamp(p, hi << 16 | ccr);
}
if (sr_uif)
p->ovf++;
return got;
}
uint32_t pps_isr_naive(pps_t *p, uint32_t sr_uif, uint32_t sr_cc, uint16_t ccr)
{
if (sr_uif)
p->ovf++; /* сначала переполнение — и захват до него получит лишний круг */
return sr_cc ? stamp(p, p->ovf << 16 | ccr) : 0;
}
/* ── UBX ──────────────────────────────────────────────────────────────── */
uint16_t ubx_frame(uint8_t cls, uint8_t id, const uint8_t *pl, uint16_t n, uint8_t *out)
{
uint8_t a = 0, b = 0;
out[0] = 0xB5u;
out[1] = 0x62u;
out[2] = cls;
out[3] = id;
out[4] = (uint8_t)n;
out[5] = (uint8_t)(n >> 8);
for (uint16_t i = 0; i < n; i++)
out[6 + i] = pl[i];
for (uint16_t i = 2; i < 6u + n; i++) { /* сумма — от класса до конца нагрузки */
a = (uint8_t)(a + out[i]);
b = (uint8_t)(b + a);
}
out[6 + n] = a;
out[7 + n] = b;
return (uint16_t)(n + 8u);
}
static void le16(uint8_t *p, uint32_t v)
{
p[0] = (uint8_t)v;
p[1] = (uint8_t)(v >> 8);
}
uint16_t ubx_cfg_prt(uint32_t baud, uint16_t in_proto, uint16_t out_proto, uint8_t *out)
{
uint8_t pl[20] = {1u}; /* порт 1 — UART */
le16(pl + 4, 0x08C0u); /* mode: 8 бит (3 << 6), без чётности (4 << 9), 1 стоп-бит */
le16(pl + 8, baud);
le16(pl + 10, baud >> 16);
le16(pl + 12, in_proto); /* бит 0 — UBX, бит 1 — NMEA */
le16(pl + 14, out_proto);
return ubx_frame(UBX_CFG, UBX_CFG_PRT, pl, sizeof pl, out);
}
uint16_t ubx_cfg_rate(uint16_t meas_ms, uint16_t nav, uint16_t time_ref, uint8_t *out)
{
uint8_t pl[6];
le16(pl, meas_ms); /* период измерений, мс */
le16(pl + 2, nav); /* решение на каждое nav-е измерение */
le16(pl + 4, time_ref); /* 0 — UTC, 1 — время GPS */
return ubx_frame(UBX_CFG, UBX_CFG_RATE, pl, sizeof pl, out);
}
uint16_t ubx_cfg_rst(uint16_t bbr, uint8_t mode, uint8_t *out)
{
uint8_t pl[4] = {(uint8_t)bbr, (uint8_t)(bbr >> 8), mode, 0}; /* что стереть: 0 — горячий, 1 — тёплый, 0xFFFF — холодный */
return ubx_frame(UBX_CFG, UBX_CFG_RST, pl, sizeof pl, out);
}
int ubx_is_ack(const gps_rx_t *r, uint8_t cls, uint8_t id)
{
if (r->ucls != UBX_ACK || r->ulen != 2u || r->ubx[0] != cls || r->ubx[1] != id || r->uid > 1u)
return -1;
return r->uid; /* ACK-ACK — 1, ACK-NAK — 0 */
}
/* ── кольцо DMA ───────────────────────────────────────────────────────── */
void gps_ring_poll(gps_ring_t *q, uint16_t ndtr, gps_rx_t *r, gps_on_t on, void *ctx)
{
uint16_t wr = (uint16_t)(q->size - ndtr); /* куда DMA положит следующий байт */
if (wr >= q->size)
wr = 0; /* NDTR = 0 в миг перезагрузки */
while (q->rd != wr) {
int k = gps_rx_byte(r, q->buf[q->rd]);
if (++q->rd == q->size)
q->rd = 0;
if (k != GPS_NONE)
on(k, r, ctx); /* строка лежит в r->line до следующего байта */
}
}
fw99d.c — прошивка STM32F103: USART1 с DMA в кольцо, PPS на TIM2, настройка модуля.
/* fw99d.c — раздел 99в: GPS-модуль NEO-6M на STM32F103 (BluePill), 72 МГц.
* Приём: TX модуля → PA10 (USART1_RX), 9600 бит/с 8N1; DMA1 канал 5 кладёт байты в кольцо 256 байт по кругу,
* разбор зовут половина и конец кольца (HT, TC) и тишина на линии (IDLE).
* Передача: PA9 (USART1_TX) → RX модуля: CFG-PRT переводит порт на 38 400 бит/с, CFG-RATE — 5 решений в секунду;
* нет ACK-ACK за 2 с — назад на 9600.
* PPS модуля → PA0 (TIM2_CH1): захват по фронту на 72 МГц, 16 бит счётчика + счёт переполнений = 32-битная метка.
* Тактирование до 72 МГц — как в разделе 6 (здесь не повторяется); APB1 = 36 МГц, таймеры на нём — 72 МГц. */
#include "stm32f10x.h"
#include "gps99d.h"
#define CPU_HZ 72000000u
#define RX_SIZE 256u
_Static_assert(CPU_HZ / 9600u == 7500u && CPU_HZ % 9600u == 0u, "BRR = 7500 = 0x1D4C: 9600 бит/с без ошибки");
_Static_assert(CPU_HZ / 38400u == 1875u && CPU_HZ % 38400u == 0u, "BRR = 1875 = 0x753: 38 400 бит/с без ошибки");
_Static_assert(PPS_HZ == CPU_HZ, "TIM2 считает 72 МГц без делителя");
static uint8_t rx_buf[RX_SIZE];
static gps_ring_t ring = {rx_buf, RX_SIZE, 0};
static gps_rx_t rx;
static gps_t gps;
static pps_t pps;
static volatile uint32_t acked, nak, pps_count, uart_ore;
static volatile float home_m; /* расстояние от первой точки с решением */
static int32_t home_lat, home_lon;
static uint8_t home_set;
static void on_msg(int kind, gps_rx_t *r, void *ctx)
{
(void)ctx;
if (kind == GPS_UBX) {
int a = ubx_is_ack(r, UBX_CFG, UBX_CFG_RATE);
if (a == 1)
acked = 1;
else if (a == 0)
nak++;
return;
}
if (gps_line(&gps, r->line) == NMEA_RMC && gps_has_fix(&gps)) {
if (!home_set) {
home_lat = gps.lat;
home_lon = gps.lon;
home_set = 1;
}
home_m = gps_dist_hav(home_lat, home_lon, gps.lat, gps.lon);
}
}
/* ── приём: кольцо DMA ────────────────────────────────────────────────── */
static void rx_poll(void)
{
gps_ring_poll(&ring, (uint16_t)DMA1_Channel5->CNDTR, &rx, on_msg, 0);
}
void DMA1_Channel5_IRQHandler(void) /* HT или TC: пройдена половина или конец кольца */
{
DMA1->IFCR = DMA_IFCR_CGIF5;
rx_poll();
}
void USART1_IRQHandler(void)
{
uint32_t sr = USART1->SR;
if (sr & USART_SR_ORE)
uart_ore++;
if ((sr & (USART_SR_IDLE | USART_SR_ORE | USART_SR_NE | USART_SR_FE)) && !(sr & USART_SR_RXNE))
(void)USART1->DR; /* чтение SR, затем DR снимает IDLE и ошибки */
if (sr & USART_SR_IDLE)
rx_poll();
}
static void uart_init(void)
{
GPIOA->CRH = (GPIOA->CRH & ~0xFF0u) | 0x4B0u; /* PA9 — выход AF push-pull 50 МГц, PA10 — вход без подтяжки */
DMA1_Channel5->CCR = 0;
DMA1_Channel5->CPAR = (uint32_t)&USART1->DR;
DMA1_Channel5->CMAR = (uint32_t)rx_buf;
DMA1_Channel5->CNDTR = RX_SIZE;
DMA1->IFCR = DMA_IFCR_CGIF5;
DMA1_Channel5->CCR = DMA_CCR5_MINC | DMA_CCR5_CIRC | DMA_CCR5_HTIE | DMA_CCR5_TCIE | DMA_CCR5_EN;
USART1->BRR = CPU_HZ / 9600u; /* 0x1D4C: мантисса 468, дробь 12/16 */
USART1->CR3 = USART_CR3_DMAR | USART_CR3_EIE;
USART1->CR1 = USART_CR1_UE | USART_CR1_RE | USART_CR1_TE | USART_CR1_IDLEIE;
NVIC_SetPriority(DMA1_Channel5_IRQn, 5); /* один приоритет: разбор не прерывает сам себя */
NVIC_SetPriority(USART1_IRQn, 5);
NVIC_EnableIRQ(DMA1_Channel5_IRQn);
NVIC_EnableIRQ(USART1_IRQn);
}
static void uart_send(const uint8_t *b, uint16_t n)
{
for (uint16_t i = 0; i < n; i++) {
while (!(USART1->SR & USART_SR_TXE)) {
}
USART1->DR = b[i];
}
while (!(USART1->SR & USART_SR_TC)) { /* последний стоп-бит ушёл — можно менять скорость */
}
}
/* ── PPS на захват TIM2 ───────────────────────────────────────────────── */
void TIM2_IRQHandler(void)
{
uint32_t sr = TIM2->SR;
uint16_t ccr = 0;
if (sr & TIM_SR_CC1IF)
ccr = (uint16_t)TIM2->CCR1; /* чтение снимает CC1IF */
if (sr & TIM_SR_UIF)
TIM2->SR = ~TIM_SR_UIF; /* rc_w0: ноль снимает только UIF */
if (pps_isr(&pps, sr & TIM_SR_UIF, sr & TIM_SR_CC1IF, ccr))
pps_count++;
}
static void pps_tim_init(void)
{
GPIOA->CRL = (GPIOA->CRL & ~0xFu) | 0x4u; /* PA0 — вход без подтяжки */
TIM2->PSC = 0; /* 72 МГц: отсчёт 13,9 нс, круг 16 бит — 910,2 мкс */
TIM2->ARR = 0xFFFFu;
TIM2->CCMR1 = TIM_CCMR1_CC1S_0; /* IC1 — от TI1 (PA0) */
TIM2->CCER = TIM_CCER_CC1E; /* захват по фронту */
TIM2->CR1 = TIM_CR1_URS;
TIM2->EGR = TIM_EGR_UG;
TIM2->SR = 0;
TIM2->DIER = TIM_DIER_CC1IE | TIM_DIER_UIE;
pps_init(&pps);
NVIC_SetPriority(TIM2_IRQn, 2); /* выше разбора: задержка входа должна быть много меньше 455 мкс */
NVIC_EnableIRQ(TIM2_IRQn);
TIM2->CR1 |= TIM_CR1_CEN;
}
int main(void)
{
uint8_t f[32];
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
RCC->AHBENR |= RCC_AHBENR_DMA1EN;
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_USART1EN;
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
gps_rx_init(&rx);
gps_init(&gps);
uart_init();
pps_tim_init();
uart_send(f, ubx_cfg_prt(38400u, 3u, 2u, f)); /* вход UBX + NMEA, выход NMEA */
USART1->BRR = CPU_HZ / 38400u;
uart_send(f, ubx_cfg_rate(200u, 1u, 1u, f)); /* 200 мс — 5 решений в секунду, время GPS */
uint32_t t0 = DWT->CYCCNT;
while (!acked && DWT->CYCCNT - t0 < 2u * CPU_HZ) {
}
if (!acked)
USART1->BRR = CPU_HZ / 9600u; /* модуль не понял — вернуться на 9600 */
for (;;) {
__WFI(); /* всё делают прерывания */
}
}
Дальше: раздел 99г «Магнитометр-компас QMC5883L/HMC5883L по I2C: курс, калибровка hard/soft iron, наклон и магнитное склонение» — дешёвый магнитометр на I2C как компас: какая микросхема на плате, курс целыми, калибровка без float, поправка на наклон и склонение.