Embedded HandbookApps
All sections › XI. Sensors: connection, circuits, drivers
STM32F103 · level intermediate · 43 min read
Open in the interactive handbook

99в. NEO-6M GPS receiver: NMEA over UART, sentence parsing, checksum, time and coordinates, PPS

In sections 99–99б the sensors answered a request: the firmware read registers over I²C or SPI. A GPS module works the other way round — it talks by itself, in text, once a second, and it cannot be asked to keep quiet. The firmware must not lose a single byte, must assemble sentences from the stream, throw away the broken ones and pull time, coordinates and fix quality out of the fields — without sscanf and without float. For precise time the module has a second output — the PPS pulse on the second boundary. Mini-project 198н builds such a receiver on the STM32G474 with the 32-bit TIM2 and RTC calibration; here is the basis on the STM32F103 and the inexpensive NEO-6M module: sentences and their checksum, reception through a DMA ring, field parsing, the distance between points, PPS on a 16-bit timer and configuring the module with binary UBX commands.

Sources: RM0008 (DocID13902 Rev 17): the frequency of the timers on APB1 is doubled, p. 93; DMA1 requests, table 78, p. 281 (USART1_RX is channel 5); the USART baud rate divider, p. 799; the IDLE flag, p. 820; the CC1OF flag, p. 409, and CC1IF, UIF, p. 410. DocID13587 Rev 17 (STM32F103x8/xB): PA0 is TIM2_CH1_ETR without 5 V tolerance, p. 29; PA9 and PA10 are USART1_TX and USART1_RX with 5 V tolerance (FT), p. 31. The project has no u-blox documents on the NEO-6M and no NMEA 0183 standard. The UBX frame format, class and command numbers, the layout of CFG-PRT, CFG-RATE and CFG-RST, the hot, warm and cold start codes come from the protocol.h header of the Zephyr 4.4.2 modem library; the initial speed of 9600 bit/s and the fix period of 1000 ms come from the description of u-blox modules in the same tree (u-blox,common.yaml); the field order of RMC and GGA and the XOR checksum come from the NMEA parser of Zephyr (gnss_nmea0183.c) and rusEFI (nmea.cpp). None of this was checked for the NEO-6M. All numbers below come from a run of the core _verify/gps99d/gps99d.c on a computer (gcc 12.2.0) with our own model of the receiver, the line, the DMA ring and TIM2 in Python 3.11; the firmware is built by armclang 6.24 for the Cortex-M3.

What it is called in the documentation

Original Meaning
GNSS receiver, fix satellite navigation receiver; fix: the coordinates are found
NMEA sentence, talker ID NMEA sentence; who talks: GP — GPS, GN — several systems
RMC, GGA, GSV, VTG navigation minimum; fix with altitude; satellites in view; course and speed
checksum checksum: XOR of the characters between $ and *
fix quality, HDOP fix quality; horizontal dilution of precision
UBX protocol, class, message ID u-blox binary protocol; command class and number
CFG-PRT, CFG-RATE, CFG-RST port setup; fix rate; reset and start
ACK-ACK, ACK-NAK command accepted; command rejected
cold, warm, hot start cold, warm, hot start
time pulse, PPS a pulse at the start of each second
overcapture a new capture while the old one is unread

At the top, the connection of the NEO-6M module to the STM32F103: module TX to PA10, module RX from PA9, PPS to PA0; USART1 at 9600 bit/s with BRR 0x1D4C and DMA1 channel 5 put bytes into a 256-byte ring, parsing is called by the HT, TC and IDLE events; PPS goes to a TIM2 rising-edge capture at 72 MHz with a 13.9 ns tick, the 16-bit counter wraps every 910.2 µs, and the stamp is extended to 32 bits. In the middle, an RMC sentence with checksum 61: UTC time, status, latitude ddmm.mmmmm and hemisphere, longitude, speed in knots, course, date; the latitude 5545.12490,N becomes the number 557 520 817 — degrees × 10⁷ — without sscanf and float. At the bottom, one second on the line: a burst of RMC, VTG, GGA, GSA, three GSV and GLL of up to 504 bytes lasts 525 ms, an HT or TC event every 128 bytes, IDLE after the burst; the 256-byte ring fills in 266.7 ms, so polling every 500 ms loses sentences, while the events do not


99в.1 What the module gives and why there is no fix indoors

The NEO-6M is a GPS receiver made by u-blox; it is usually sold on a small board with an antenna, a regulator and a four-pin connector: power, ground, TX, RX. Some boards also bring out PPS. The link is UART 8N1. The initial speed of u-blox modules, per the description in Zephyr, is 9600 bit/s, a fix once per 1000 ms; the project has no document on the NEO-6M itself, so for it this is not verified — check with a terminal.

Signal F103 pin Peripheral
module TX → receive PA10 USART1_RX, DMA1 channel 5, FT
module RX ← transmit PA9 USART1_TX, FT
PPS PA0 TIM2_CH1, without 5 V tolerance

At 9600 bit/s a byte with start and stop is 10 bits, 1.04 ms, 960 bytes per second. In the bench model a burst of RMC, VTG, GGA, GSA, three GSV and GLL goes once a second: with a fix 493.2 bytes on average and up to 504 bytes, 525 ms on the line — 52.5 % of the capacity. The set of a real module is chosen by its firmware; for the calculation take your own burst from a terminal.

Cold and warm start. To find coordinates, a receiver needs the time, a rough position and the satellite orbit data. It receives all of this from the satellites themselves, and that is slow. If the data has been kept (the module has a backup supply, it was not off for long), the start is warm or hot and the fix appears sooner. If nothing is left, the start is cold, the longest. How many seconds each takes is written in the module document — it is absent, hence no numbers. The CFG-RST command (99в.6) can deliberately erase what was kept and arrange any of the three starts.

Why there is no fix indoors. A satellite signal near the ground is very weak, walls and the roof weaken it further, and reflections from walls distort the range. In a room the module often sees satellites in GSV but gives no fix: RMC with status V, GGA with quality 0, empty coordinate fields. It is not a fault — move the antenna to a window or outside. The firmware must survive this: while there is no fix the fields are empty, and the parser must not take an empty field for zero.


99в.2 The NMEA sentence and its checksum

A sentence starts with $, then the address — who talks and what the sentence is (GPRMC: GP — GPS, RMC — the type), fields separated by commas, after * two hexadecimal digits of the checksum and the end of line \r\n. A one-second burst from the model, with a fix, moving east:

$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

The four sentences the core parses:

Sentence Fields in order
RMC time hhmmss.ss, status A/V, latitude ddmm.mmmmm, N/S, longitude dddmm.mmmmm, E/W, speed in knots, course, date ddmmyy
GGA time, latitude, N/S, longitude, E/W, fix quality, satellites in the fix, HDOP, altitude, M, geoid height, M
GSV sentences in the series, sentence number, satellites in view, and four fields per satellite: number, elevation, azimuth, signal to noise
VTG true course, T, magnetic course, M, speed in knots, N, speed in km/h, K

The field order of RMC and GGA follows the Zephyr parser and the tables in the rusEFI source; GSV follows Zephyr; VTG and the mode field at the end of RMC follow the well-known description and were not checked. The GGA quality: 0 — no fix, 1 — GPS, 2 — differential (numbers from the Zephyr enumeration). The core skips GSA and GLL: everything needed is in the others.

The checksum. XOR of all characters between $ and *, not including them. For the sentence GPVTG,,,,,,,,,N — a burst before the fix — the first six steps give 47, 17, 41, 15, 52, 7E, the result is 0x30. The RMC of the example has a 68-character body, checksum 0x61 — Python computed it with its own code and the C core did the same. XOR catches any single corrupted digit, but not a swap of two characters and not two identical errors in one bit — for line noise that is enough.

Assembling a sentence is a byte-wise state machine: wait for $, collect the body and the XOR, after * two digits, then \r or \n. Everything that does not match is counted and thrown away:

    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;

A new $ inside a sentence means the previous one broke off, but the new one starts at once. A sentence longer than 96 characters is dropped (NMEA 0183, per the well-known description, allows up to 82 including $ and \r\n). The byte 0xB5 starts a binary UBX frame — the same wires carry replies to commands (99в.6), and the state machine parses both protocols.


99в.3 Reception: the DMA ring, sentence assembly and a broken stream

The bytes are put by DMA1, channel 5 (the USART1_RX request, table 78), into a 256-byte ring in circular mode — just as in section 16б, where the ring, the CNDTR counter and the three events are covered in detail. Parsing is called by the half and the end of the ring (HT, TC) and by silence on the line (IDLE, p. 820): silence hands over the tail of the burst at once, without waiting for the half.

    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 MHz / (16 × 468.75) — exactly 9600 bit/s (the formula on p. 799). The function gps_ring_poll takes CNDTR, computes the write position 256 − CNDTR and runs everything from the read position up to it through the state machine. It hands a finished sentence to a callback, and the callback hands it to field parsing.

The bench: the receiver model drives 600 seconds of stream and corrupts it. Every 41st sentence has one digit replaced with the checksum kept; every 97th is cut before *; every 200 s comes a sentence longer than the buffer; every 150 s, 40 bytes of garbage between bursts; at the 100th second an ACK-ACK reply, at the 200th the same with a wrong checksum. There is no fix until the 40th second and from the 300th to the 319th ("a tunnel"). In total 286 802 bytes, 4760 sentences, of them intact — 4602.

How to take Ring Sentences accepted Wrong checksum Cut Long RMC
HT, TC, IDLE 256 4602 115 43 3 579
poll every 100 ms 256 4602 115 43 3 579
poll every 250 ms 256 4602 115 43 3 579
poll every 500 ms 256 1706 43 14 0 10
poll every 500 ms 1024 4602 115 43 3 579

With the events the counters matched what was planted exactly: 115 replacements, 43 cuts, 3 long sentences; the garbage between bursts broke nothing, the ACK-ACK was accepted, the broken UBX frame was dropped. Each accepted sentence was parsed by the core the same way as by our own parser in Python — 3441 printed lines matched.

A 256-byte ring fills in 266.7 ms. Polling every 250 ms still keeps up, every 500 ms does not: DMA silently overwrites the unread data, and of 579 RMC only 10 remain. The checksum hardly helps here: the overwritten sentences do not arrive with a wrong checksum, they do not arrive at all. Polling from the main loop works only with a ring larger than the burst — 1024 bytes; the events are more reliable.


99в.4 Parsing the fields without sscanf and float

nmea_split replaces the commas with zeros and returns an array of pointers to the fields; an empty field stays an empty string, and every parsing function answers an empty string with an error, not with zero. Numbers are fixed-point integers: nmea_fixed("1.30", 2) gives 130, extra digits are dropped. sscanf and atof are not needed: they pull a parsing library and float into the firmware, and the Cortex-M3 has no floating-point unit.

A coordinate. ddmm.mmmmm is degrees and minutes together. Minutes with six digits after the point are an integer up to 59 999 999, and dividing by 6 gives degrees × 10⁷ at once, because 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 rounds a half up. South and west are minus. In the model 5545.12490,N gives 557 520 817, 03737.95132,E gives 376 325 220; 3436.22800,S gives −346 038 000, 05822.89300,W gives −583 815 500 (−34.6038° and −58.38155°). A check on 20 012 values against the exact fraction in Python: 0 mismatches, including 12 edge cases — an empty field, 60 minutes, more than 90° of latitude and 180° of longitude, a wrong hemisphere letter. The conversion error is at most 0.3333 units of 10⁻⁷ degree with five minute digits (3.71 mm in latitude) and 0.5 with seven (5.56 mm). A unit of 10⁻⁷ degree of latitude is 1.112 cm, and the last minute digit with five digits is 1.85 cm: an int32_t in degrees × 10⁷ holds the coordinate more precisely than the module gives it.

Time and date. hhmmss.ss gives milliseconds from midnight, ddmmyy gives the day, the month and the year 2000 + yy. From them the core computes Unix seconds by integer division (days from 1 March, as in the calendar). The run, checked against datetime in Python:

UTC time, date Unix seconds Moscow, 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

Local time is an offset added to the Unix seconds, not to the hours: that way 23:30 UTC on 31 December correctly becomes 1 January. Hour 24, day 32 and month 13 are rejected by the parser.

The fix. Coordinates are taken only from RMC with status A and GGA with quality above 0; with V the previous coordinates stay, but there is no fix. gps_has_fix checks both conditions at once. In the stream of 99в.3 the fix appeared at 12:00:40, was lost at 12:05:00 and came back at 12:05:20 — exactly as in the model. HDOP is ×100, 130 in the example; an empty field is 0xFFFF. The number of satellites in the fix comes from GGA, the visible ones and the best signal from the GSV series.


99в.5 Decimal degrees and the distance between points

Decimal degrees are degrees × 10⁷ divided by 10⁷: 557 520 817 is 55.7520817°. The distance between two points on a sphere of radius 6371 km is given by the haversine formula. On the bench it is computed in double in C and separately in Python — they matched to 10⁻⁶ m (the print precision); this is the reference. On the MCU there are three variants in 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);
}

The largest error against the reference over 200 random pairs per distance, base point 55.75° N 37.63° E, metres:

Distance float from degrees differences in integers flat
1 m 0.7656 0.0000 0.0000
10 m 0.7106 0.0000 0.0000
100 m 0.8233 0.0000 0.0000
1 km 0.6012 0.0001 0.0002
10 km 0.9380 0.0021 0.0040
100 km 0.8003 0.0193 2.7058
1000 km 0.6498 0.1839 2812.1652

"From degrees" is off by almost a metre at any distance — this is the float step near 55°, and on a one-metre segment it eats the whole result. The haversine on integer differences is off by 1.9 cm at 100 km and by 18 cm at 1000 km — about 2·10⁻⁷ of the distance. The flat formula is good up to tens of kilometres: 4 mm at 10 km, 2.7 m at 100 km. The run did not estimate a sphere instead of an ellipsoid — that is a separate error of the Earth model.

Across the 180° meridian the longitudes 179.9999° and −179.9999° give a difference of −359.9998° — and −3.6·10⁹ in degrees × 10⁷, which does not fit into an int32_t. The core subtracts in int64_t and corrects by ±360°: both integer formulas give 22.238985 m, like the reference, while the flat one without the correction would count 40 030.2 km. The path in the stream of 99в.3 is 522 points with a fix, 4800.002 m by the reference; the haversine and the flat formula gave 4799.998, "from degrees" 4799.958: on a straight path the errors of neighbouring segments cancel each other, on a single segment they do not.


99в.6 PPS, precise time and configuration with UBX commands

PPS. The PPS edge stands on the second boundary; the sentence with the time of that second comes later, and which second it describes is decided by the module (section 198н.6). Whether there is a pulse without a fix, its length and polarity are in the module document, which is absent; the model takes the edge as rising. Capture is as in section 13.6: an edge on PA0 latches the TIM2 counter into CCR1, and interrupt latency does not affect the result.

TIM2 on the F103 is 16-bit. Without a prescaler it counts 72 MHz: a tick of 13.89 ns, a wrap every 910.2 µs, 1098.6 overflows per second. The stamp is built from the overflow count (the upper 16 bits) and CCR1 (the lower ones); the 32-bit stamp wraps every 59.65 s, and the difference of neighbouring stamps modulo 2³² stays correct across the wrap. The catch is when one interrupt entry sees both flags, UIF and CC1IF (p. 410). The overflow could have been before the capture or after it. The capture itself decides: a small CCR1 means the counter has already wrapped, a large one means not yet:

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++;
}

The model: the crystal is fast by 20 ppm, the PPS edge jitters by ±50 ns, the interrupt entry is delayed by a random time 0…L, 1000 seconds, the pulse of the 500th second is missing. "Naive" means adding the overflow first and then building the stamp:

L, µs 0 5 20 100 400
wrong stamps naive, of 999 0 2 8 60 221
wrong stamps with the check 0 0 0 0 0

A wrong stamp is shifted by 65 536 ticks — 0.91 ms, and the period next to it by 910 ppm. The "second ±1 %" filter does not catch such a shift: it is 0.091 % of a second; only the gap at the 500th second is rejected. The half-wrap check is correct while the entry latency is below 455 µs, which is why TIM2 has a higher priority than sentence parsing. The crystal drift per second is from 19 902.8 to 20 097.2 ppb (truth 20 000), over 64 s windows 19 999…20 000. The window sum also matched for the naive variant: a wrong stamp spoils two neighbouring periods in opposite directions. What stays spoiled is the stamp itself — the time of the edge, which is the whole reason PPS is wired. How to trim the RTC clock by PPS is section 198н.5.

UBX. The u-blox binary commands: sync bytes 0xB5 0x62, class, number, payload length (2 bytes, little end first), the payload and a two-byte checksum. The checksum is Fletcher-8 from the class to the end of the payload: a += byte, b += a, both modulo 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;
Command Payload Frame Checksum
CFG-PRT, class 0x06, number 0x00 20 bytes: port 1 (UART), 8N1 — mode 0x08C0, 38 400 bit/s, input UBX + NMEA, output NMEA 28 bytes 7E 6A
CFG-RATE, 0x06 0x08 period 200 ms, a fix on every measurement, GPS time B5 62 06 08 06 00 C8 00 01 00 01 00 DE 6A DE 6A
CFG-RST, 0x06 0x04, cold erase everything — 0xFFFF, software reset B5 62 06 04 04 00 FF FF 01 00 0D 5F 0D 5F
CFG-RST, warm / hot 0x0001 / 0x0000 12 bytes 10 6A / 0F 66

Python built the same frames from the field description with its own code — all five matched byte for byte. The reply is ACK-ACK, class 0x05, number 0x01, with the class and number of the accepted command in the payload: B5 62 05 01 02 00 06 08 16 3F; ACK-NAK has number 0x00. ubx_is_ack tells them apart from anything else.

Why 38 400 bit/s: five fixes per second are five bursts, 5 × 504 bytes. At 9600 bit/s that is 262.5 % of the capacity — the sentences will not fit, at 38 400 it is 65.6 %. So the firmware first sends CFG-PRT at 9600 bit/s, waits for the last stop bit to leave (TC), switches its own BRR to 1875 (0x753, exactly 38 400) and only then sends CFG-RATE. No ACK-ACK within 2 s — back to 9600. Whether the module keeps the setting after power-off depends on its memory and a separate save command — not verified without a document.


99в.7 Pitfalls

No fix at the desk. Indoors the signal is weak: GSV shows satellites, while RMC says V and GGA says 0. Move the antenna to a window; the parser must live with empty fields.

An empty field read as zero. Before the fix the latitude is empty, and atoi("") gives 0 — a point in the Gulf of Guinea. An empty field is an error, and a fix exists only with A and quality above 0.

South and west without a sign. The N/S and E/W field is separate; without it Buenos Aires (−34.6038°, −58.38155°) ends up in Iran. The sign is set by the coordinate parser.

Coordinates in float. The float step near 55° is 0.424 m: the distance over a short segment is off by almost a metre. Differences go in integer degrees × 10⁷, and only the difference goes into float.

Polling the ring less often than it fills. 256 bytes at 9600 bit/s take 266.7 ms; polling every 500 ms left 10 RMC of 579, and there are almost no wrong checksums — the sentences simply vanished. Take on HT, TC, IDLE.

Broken sentences. Replacements, cuts and long sentences were all caught in the run: 115, 43 and 3. Without the checksum a corrupted digit in a coordinate would get into the fix. The checksum uses upper-case hex digits; the end of line is \r or \n.

A PPS stamp one wrap off. Both flags in one entry: without checking CCR1 against half a wrap, up to 221 wrong stamps of 999 at a 400 µs latency. The ±1 % filter does not see them.

A binary frame swallows the text. A corrupted UBX length gives up to 65 535 bytes of "payload", and the state machine would eat seconds of NMEA. A frame longer than 64 bytes is dropped at once.

The module speed changed, ours did not. After CFG-PRT the module talks at 38 400 while USART is still at 9600 — garbage in the ring. Our BRR is changed after the frame has left, with a fallback.

5 V on PA0. PA9 and PA10 tolerate 5 V, PA0 does not. If the module board outputs PPS at a five-volt level — a divider. How to power the board and what level its outputs have, see the board: the project has no document on it.


99в.8 Summary

Full sources

gps99d.h — NMEA and UBX assembly, the fix, distance, PPS, UBX commands, the DMA ring.

/* 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 — the core without registers: the part shared by the firmware and the bench.

/* 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 firmware: USART1 with DMA into a ring, PPS on TIM2, module configuration.

/* 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();                                        /* всё делают прерывания */
    }
}

Next: section 99г "QMC5883L/HMC5883L magnetometer compass over I2C: heading, hard/soft iron calibration, tilt and magnetic declination" — a cheap I2C magnetometer as a compass: which chip is on the board, heading in integers, calibration without float, tilt correction and declination.

Open in the interactive handbook