Embedded HandbookПриложения
Все разделы › XI. Датчики: подключение, схемы, драйверы
STM32F103 · уровень средний · 43 мин чтения
Открыть в интерактивном справочнике

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 новый захват при непрочитанном старом

Вверху — подключение модуля NEO-6M к STM32F103: TX модуля на PA10, RX модуля от PA9, PPS на PA0; USART1 на 9600 бит/с с BRR 0x1D4C и DMA1 канал 5 кладут байты в кольцо 256 байт, разбор зовут события HT, TC и IDLE; PPS идёт на захват TIM2 по фронту на 72 МГц с отсчётом 13,9 нс, 16-битный счётчик обходит круг за 910,2 мкс, и метка собирается до 32 бит. Посередине — строка RMC с суммой 61: время UTC, статус, широта ddmm.mmmmm и полушарие, долгота, скорость в узлах, курс, дата; широта 5545.12490,N без sscanf и float становится числом 557 520 817 — градусы × 10⁷. Внизу — секунда на линии: пачка RMC, VTG, GGA, GSA, трёх GSV и GLL до 504 байт идёт 525 мс, каждые 128 байт событие HT или TC, после пачки IDLE; кольцо 256 байт заполняется за 266,7 мс, поэтому опрос раз в 500 мс теряет строки, а события — нет


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 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 Итоги

Полные исходники

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, &deg) || 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, поправка на наклон и склонение.

Открыть в интерактивном справочнике