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
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:
- from degrees: the coordinates go into
floatfirst, then the formula. Afloathas a 24-bit mantissa: near 55° the step is 3.815·10⁻⁶ degree, 0.424 m; - differences in integers: the latitude and longitude differences
are computed in integer degrees × 10⁷ and only then go into
float— exactly; - flat: the same differences, but instead of haversines —
√((Δλ·cos φ)² + Δφ²), without
sinandasin.
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
- The NEO-6M talks by itself: UART 8N1, per the u-blox description in Zephyr 9600 bit/s, once a second; the model burst is up to 504 bytes, 525 ms, 52.5 % of the line.
- An NMEA sentence:
$, fields separated by commas,*and the XOR of the body as two hexadecimal digits; RMC — time, status, coordinates, date; GGA — quality, satellites, HDOP, altitude; GSV — satellites in view; VTG — course and speed. - The 256-byte DMA ring on
HT,TC,IDLE: 4602 intact sentences of 4602 accepted, 115 replacements, 43 cuts, 3 long ones caught; polling every 500 ms — 10 RMC of 579. - Coordinates as integers, degrees × 10⁷: minutes × 10⁶ / 6, error up to 3.71 mm; 20 012 checks without mismatches; time as Unix seconds by integer arithmetic.
- Distance:
floatfrom degrees is off by up to 0.94 m on any segment; the haversine on integer differences — 18 cm at 1000 km; the flat formula — 4 mm at 10 km. - PPS on the 16-bit TIM2: 13.89 ns, a wrap every 910.2 µs; with the
half-wrap check of
CCR1— 0 wrong stamps, naive — up to 221 of 999. - UBX: Fletcher-8 from the class to the end of the payload; CFG-RATE
200 ms —
DE 6A, ACK-ACK —16 3F; 5 fixes per second need 38 400 bit/s.
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, °) || 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.