
GPS-приемники сегодня используются в самых разных устройствах - от автомобильных трекеров до беспилотных летательных аппаратов, независимо от применения, большинство таких модулей передают информацию о положении в формате NMEA 0183, в этой статье я разберу, как принять эти данные от GPS-модуля на микроконтроллер STM32 и преобразовывать их в удобный для программы вид.
А также в статье будут рассмотрены два варианта подключения GPS-приемников к микроконтроллеру STM32:
Модуль GPS c UART-интерфейсом (TTL-уровни), подключаемый на прямую к микроконтроллеру;
Модуль GPS с интерфейсом RS-232, данные от такого типа gps, необходимо принимать через преобразователь уровней TTL.
В данном проекте используются GPS-приемники: LS23030 (UART) и LS23036(RS-232).
Схема подключения GPS-UART

Сигнал GPS, подключается к выводу PA10-31_контакт - RX(МК-STM32F103)
Для более стабильного напряжения питания можно использовать следующую схему, в которой работает понижающий преобразователь MP231, но необходим источник +12В, в моем случае используется аккумуляторная сборка (NiMH/Pb +12В).
![Понижающий преобразователь напряжения MP2315 [ +12V до +5V ] Понижающий преобразователь напряжения MP2315 [ +12V до +5V ]](https://habrastorage.org/r/w780/getpro/habr/upload_files/d77/7b3/151/d777b3151f98dfd96ef264fb08e0579d.png)
!!! P.S. заранее прошу прощения за некачественные картинки осциллограмм, осциллограф (с возможностью сохранения данных на флешку), некорректно производит измерения ...!!!
Вид осциллограммы передаваемых данных модуля-gps(uart) (линия TX)

Показатель амплитуды данных от модуля gps(uart) = delta [ 3.4V ], можно подключать к микроконтроллеру STM32.
Схема подключения GPS(RS-232)

Сигнал модуля-gps(rs-232), сначала приходит на 13 контакт преобразователя ADM3202, далее преобразованный сигнал (TTL) уходит на PA10-31_контакт - RX(МК-STM32F103)
Схема подключения ADM3202 к МКSTM32 - макет

Также для более стабильного напряжения питания по +5В, можно использовать схему преобразователя напряжения MP2315.
Для более стабильного напряжения питания по +3В, можно использовать следующую схему, в которой работает линейный стабилизатор напряжения LP2985.

Краткая информация о преобразователе ADM3202
Микроконтроллеры STM32, работают с логическими уровнями TTL/CMOS - обычно это 3.3В или 5В, интерфейс RS-232, напротив, использует более высокие и отрицательные напряжения ( от ±3В до ±12В), что делает их напрямую несовместимыми.
Если подключить напрямую модуль-GPS (RS-232) к выводам МК-STM32, это может не только привести к искажению данных, но и физически повредить выводы. ADM3202 решает эту задачу, переводя сигналы из одного уровня в другой, в обоих направлениях.
ADM3202 - это двухканальный приемопередатчик уровней RS-232 - TTL, выполняет сразу две задачи:
Преобразование входящих RS-232 сигналов в безопасные TTL-уровни(RX-канал);
Преобразование исходящих TTL-сигналов микроконтроллера в RS-232(TX-канал).
Для формирования требуемых амплитуд RS-232, внутри микросхемы используется помповый преобразователь напряжения(chage pump) с четырьмя внешними конденсаторами, это позволяет работать от одного источника питания (от 3В до 5.5В).
Вид осциллограммы передаваемых данных модуля-gps(rs-232) до преобразования ADM3202

Показатель амплитуды данных от модуля gps(rs-232) до преобразования = delta [ 10.6V ], нельзя подключать к микроконтроллеру STM32.
Вид осциллограммы передаваемых данных модуля-gps(rs-232) после преобразования ADM3202

Показатель амплитуды данных от модуля gps(rs-232) после преобразования = delta [ 3.6V ], можно подключать к микроконтроллеру STM32.
Настройка микроконтроллера STM32F103 в CubeIDE
Конфигурация Parametr Settings
В параметрах USART (Parametr Settings) я выбираю:
Mode: Asynchronous (асинхронный режим);
Baud Rate: 9600 бит/с (в моем примере два модуля-gps (rs-232 и uart) работают на скорости 9600).
все остальные параметры без изменений.

Конфигурация NVIC Settings
Захожу в параметр (NVIC Settings) и включаю глобальное прерывание
Для отслеживания состояния интерфейса USART и обработки важных событий (например, завершения приема или ошибки), в разделе NVIC Settings было включено глобальное прерывание USART, это обеспечивает возможность немедленного реагирования со стороны микроконтроллера на изменения состояния периферии без постоянного опроса регистров.

При работе с GPS-модулями, которые передают NMEA-сообщения раз в секунду (1Hz), важно правильно организовать прием данных, чтобы не пропустить ни одного пакета. Необходимо настроить DMA в режиме Circular данный режим минимизирует нагрузку на процессор и гарантирует надежный прием.
Конфигурация DMA Settings
Захожу в параметр DMA Settings и выполняю следующие настройки:
Выбор потока/канала: USART1_RX (прием данных);
Mode: Circular ;
Increment Memory Address: Enabled (автоинкремент памяти);
Data Width: Byte (8 бит, соответствует формату NMEA).

Реализация программного кода(настройка и прием данных)
Коротко о NMEA 0183
Это текстовый протокол, используемый для передачи данных между морским и авиационным навигационным оборудованием, включая GPS-приемники. Большинство современных GPS-модулей выводят информацию именно в этом формате.
Основные особенности:
Текстовый формат – данные передаются в виде ASCII-строк;
Структура сообщений – каждая строка начинается с
$
, содержит идентификатор типа данных и заканчивается контрольной суммой;Скорость передачи – обычно 9600 бод (но может быть и выше для высокочастотных модулей);
Частота обновления – чаще всего 1 раз в секунду (1Hz), но бывают 5Hz, 10Hz и более.
Пример строки (GGA – Global Positioning System Fix Data):
№ |
Поле |
Значение |
Описание |
1 |
UTC-время |
112530.000 |
11:25:30 UTC |
2 |
Широта |
6012.3456 |
60° 12.3456′ |
3 |
Полушарие широты |
N/S |
Север/ЮГ |
4 |
Долгота |
03015.6789 |
30° 15.6789′ |
5 |
Полушарие долготы |
E/W |
Восток/Запад |
6 |
Fix Quality |
1 |
GPS фикс (автономный) |
7 |
Спутники |
10 |
10 спутников |
8 |
HDOP |
0.95 |
Горизонтальная точность |
9 |
Высота |
45.3 |
Высота над уровнем моря |
10 |
Ед. высоты |
M |
Метры |
11 |
Геоид. высота |
12.5 |
Высота геоида |
12 |
Ед. геоида |
M |
Метры |
13 |
Дифф. коррекция |
пусто |
Нет данных |
14 |
CRC |
*65 |
Контрольная сумма |
После обработки GGA:
Время: 11:25:30
Широта: 60.20576° N
Долгота: 30.261315° E
Качество фикса: 1
Спутников: 10
Высота: 45.3 м
Пример строки (RMC – Recommended Minimum Navigation Information):
№ |
Поле |
Значение |
Описание |
1 |
UTC-время |
112530.000 |
11:25:30 UTC |
2 |
Статус |
А |
Данные действительные |
3 |
Широта |
6012.3456 |
60° 12.3456′ |
4 |
Полушарие широты |
N/S |
Север/ЮГ |
5 |
Долгота |
03015.6789 |
30° 15.6789′ |
6 |
Полушарие долготы |
E/W |
Восток/Запад |
7 |
Скорость |
5.12 |
5.12 узла |
8 |
Курс |
87.45 |
87.45° |
9 |
Дата |
110825 |
11 августа 2025 |
10 |
Маг. отклонение |
пусто |
- |
11 |
Ед. маг. откл |
пусто |
- |
12 |
Режим |
A |
Автономный GPS |
13 |
CRC |
*6C |
Контрольная сумма |
После обработки RMC:
Время: 11:25:30
Статус: Данные действительные
Широта: 60.20576° N
Долгота: 30.261315° E
Скорость: 5.12 узла (~9.48 км/ч)
Курс: 87.45°
Дата: 11.08.2025
Также прикрепляю еще одну ссылку, где в детальности продемонстрирована расшифровка протокола NMEA0183 [https://wiki.iarduino.ru/page/NMEA-0183/].
Определение структур данных в заголовочном файле [ NMEA.h ]
Для удобства работы с навигационной информацией из gps-приемника, здесь я заранее описываю набор структур, каждая из которых отвечает за свой логический блок данных.
#ifndef INC_PROJECT_GNSS_NMEA_H_
#define INC_PROJECT_GNSS_NMEA_H_
// Время в часах, минутах, секундах
typedef struct {
int hour;
int min;
int sec;
int msec;
}TIME;
typedef struct{
int calculation;
}COORDINATE_CALC;
// Координаты + направление (NS/EW)
typedef struct {
float latitude;
char NS;
float longitude;
char EW;
float altitude;
char unit;
}LOCATION;
//Высота и единицы измерения
typedef struct {
float altitude;
char unit;
}ALTITUDE;
//Дата
typedef struct {
int Day;
int Mon;
int Yr;
}DATE;
//Набор данных из GGA
typedef struct {
LOCATION lcation;
TIME tim;
int isfixValid;
ALTITUDE alt;
int numofsat;
COORDINATE_CALC calc;
}GGASTRUCT;
//Набор данных из RMC
typedef struct {
DATE date;
float speed;
float course;
int isValid;
}RMCSTRUCT;
//Объединение наборов GGA и RMC
typedef struct {
GGASTRUCT ggastruct;
RMCSTRUCT rmcstruct;
}GPSSTRUCT;
int decodeGGA (char *GGAbuffer, GGASTRUCT *gga);
int decodeRMC (char *RMCbuffer, RMCSTRUCT *rmc);
#endif /* INC_PROJECT_GNSS_NMEA_H_ */
Реализация модуля парсинга протокола NMEA.c
Функция decodeGGA()
Парсит строку $GPGGA и заполняет структуру GGASTRUCT данными: время, координаты, высота, количество спутников, качество фиксации.
Шаги работы функции:
-
Подготовка к поиску нужных данных
Переменная inx - это текущий индекс в строке GGAbuffer;
Сначала иду пропуск ненужных полей (счетчик двигается до следующей ,)
-
Проверка валидации качества позиционирования
-
В NMEA поле качества фиксации (Fix Quality) может быть:
0 - нет фикса, 1 - GPS Fix, 2-DGPS Fix, 4/5/6 другие корректные варианты;
Если поле содержит из разрешенных цифр, то в gga->isfixValid устанавливается в 1, иначе функция возвращает ошибку.
-
-
Чтение времени
Извлекается время в формате HHMMSS (UTC);
Преобразуются числа;
Корректируется по смещению GMT, при необходимости меняется день (daychange++ или daycahnge--).
-
Чтение широты (Latitude)
Формат в NMEA: DDMM.MMM;
Первые цифры - градусы, остальное - минуты;
Код выделяет минуты, делит их на 60 и добавляет к градусам;
Если NS == 'S', широта делается отрицательной.
-
Чтение долготы (Longitude)
Формат в NMEA: DDMM.MMM;
Аналогично широте, но первые 3 цифры - градусы;
Если EW == 'W', долгота делается отрицательной.
-
Чтение типа вычисления координат
Из поля после долготы извлекается число (gga->calc.calculation), указывающее метод позиционирования.
-
Чтение количества спутников
Следующее поле - количество видимых спутников (gga->numofsat).
-
Пропуск HDOP
HDOP (Horizontal Dilution of Precision) не используется, просто пропускается.
-
Чтение высоты
Поле с высотой (gga->alt.altitude) и единицами (gga->alt.unit, обычно 'M' - метры).
-
Завершение
Возвращается 0 при успешном разборе.
Функция decodeRMC()
Парсит строку $GPRMC и заполняет структуру RMCSTRUCT и извлекает: валидность данных, скорость, курс и дату.
-
Пропуск времени
Сначала идет поле времени, но в этой функции оно не сохраняется.
-
Проверка валидности
Если после времени идет А(Active) - данные актуальны;
Если V(Void) нет актуальных данных, функция возвращает ошибку.
-
Пропуск координат
Пропускаются поля широты, долготы и направления (NS/EW).
-
Чтение скорости
В NMEA скорость указывается в узлах;
Код переводит строку в число с плавающей точкой и записывает в rmc->speed.
-
Чтение курса
Следующее поле - курс (угол направления движения в градусах относительно севера).
-
Чтение даты
Формат: DDMMYY;
Код выделяет день, месяц, год, корректирует день с учетом daychange (из GGA), и записывает в rmc->date.
uint8_t GMT = 0; //RU
uint8_t inx = 0;
uint8_t hr=0,min=0,day=0,mon=0,yr=0;
uint8_t daychange = 0;
/*Первый параметр это буфер GGA, в котором сохраняется строка GGA,
* Второй параметр это указатель на структуру GGA*/
int decodeGGA (char *GGAbuffer, GGASTRUCT *gga)
{
inx = 0;
char buffer[12];
int i = 0;
while (GGAbuffer[inx] != ',') inx++; // 1st ','
inx++;
while (GGAbuffer[inx] != ',') inx++; // After time ','
inx++;
while (GGAbuffer[inx] != ',') inx++; // after latitude ','
inx++;
while (GGAbuffer[inx] != ',') inx++; // after NS ','
inx++;
while (GGAbuffer[inx] != ',') inx++; // after longitude ','
inx++;
while (GGAbuffer[inx] != ',') inx++; // after EW ','
inx++;
// while (GGAbuffer[inx] != ',') inx++; // информация о типе вычисления координат ','
// inx++;
// доситиг символа/ знака для определения исправления
//проверка шаблона в буфере прошла успешно
/*Далее происходит проверка чисел, если в буфере число равно 1 , 2 или 6
* то данные являются действительными*/
if ((GGAbuffer[inx] == '1') || (GGAbuffer[inx] == '2') || (GGAbuffer[inx] == '4') || (GGAbuffer[inx] == '5') || (GGAbuffer[inx] == '6')) // 0 указывает на отсутствие исправления
{
gga->isfixValid = 1; //данные действительны
inx = 0; //сбрасываю индекс, далее начну с inx = 0 и буду извлекать информацию
}
else
{
gga->isfixValid = 0; // если данные не действительны
return 1; // return error
}
while (GGAbuffer[inx] != ',') inx++; // 1st ','
/*********************** Get TIME ***************************/
//(Update the GMT Offset at the top of this file)
/*Здесь я сначала копирую время в буфер
* данные в буфере по прежнему имеют симфольный формат, мне необходимо их изменить
* на числовой формат, сделать я это могу с помощью atoi функции исп. для преобразования строки в число*/
inx++; // достижение первого числа, вовремя
memset(buffer, '\0', 12);
i=0;
while (GGAbuffer[inx] != ',') // копирую время до того как поймаю ','
{
buffer[i] = GGAbuffer[inx];
i++;
// if(i>sizeof(buffer)){
// return 0;
// }
inx++;
}
hr = (atoi(buffer)/10000) + GMT/100; // получаю часы из 6-ти значного числа
min = ((atoi(buffer)/100)%100) + GMT%100;
//данная часть кода предназначена для регулировки времени в соответствии со смещением GMT
if (min > 59)
{
min = min-60;
hr++;
}
if (hr<0)
{
hr=24+hr;
daychange--;
}
if (hr>=24)
{
hr=hr-24;
daychange++;
}
//Сохраняю данные в tim, элемент структуры GGA
gga->tim.hour = hr;
gga->tim.min = min;
gga->tim.sec = atoi(buffer)%100;
// gga->tim.msec = (hr+min+atoi(buffer)%100)*1000;
/***************** Get LATITUDE **********************/
inx++; //Достижение первого числа в широте
memset(buffer, '\0', 12);
i=0;
while (GGAbuffer[inx] != ',') // Копировать до достижения заданной широты ','
{
buffer[i] = GGAbuffer[inx];
i++;
// if(i>sizeof(buffer)){
// return 0;
// }
inx++;
}
if (strlen(buffer) < 6) return 2; //Если длина буфера не подходит, вернуть ошибку
//int16_t num;// = (atoi(buffer)); // Изменить буфер на число, он преобразует только до десятичной дроби
int j = 0;
float grad;
while (buffer[j] != '.') j++; // Выяснить, сколько цифр перед десятичной точкой
j-=2;//++;
grad=atof (&buffer[j])/60.0f;
buffer[j]='#';
grad += (atof(buffer));
// int declen = (strlen(buffer))-j;
// int dec = atoi ((char *) buffer+j);
// float lat = (num/100.0) + (dec/pow(10, (declen+2)));
gga->lcation.latitude = grad;//lat;
inx++;
gga->lcation.NS = GGAbuffer[inx];
if(gga->lcation.NS=='S'){
gga->lcation.latitude=-gga->lcation.latitude;
}
/*********************** GET LONGITUDE **********************/
inx++; // ',' После символа NS
inx++; // Дойти до первой цифры в значении долготы
memset(buffer, '\0', 12);
i=0;
while (GGAbuffer[inx] != ',') // Копировать до достижения заданной высоты ','
{
buffer[i] = GGAbuffer[inx];
i++;
// if(i>sizeof(buffer)){
// return 0;
// }
inx++;
}
//num = (atoi(buffer));
j = 0;
while (buffer[j] != '.') {
j++;
// if (j>sizeof(buffer)){
// return 0;
// }
}
j-=2;//++;
grad=atof (&buffer[j])/60.0f;
buffer[j]='#';
grad += (atof(buffer));
// declen = (strlen(buffer))-j;
// dec = atoi ((char *) buffer+j);
// lat = (num/100.0) + (dec/pow(10, (declen+2)));
gga->lcation.longitude = grad;//lat;
inx++;
gga->lcation.EW = GGAbuffer[inx];
if(gga->lcation.EW=='W'){
gga->lcation.longitude=-gga->lcation.longitude;
}
/**************************************************/
//Пропустить исправление позиции
inx++; // ',' after E/W
/*************** Информация о типе вычисления координат ********************/
inx++; // position fix
memset(buffer, '\0', 12);
i=0;
while (GGAbuffer[inx] != ',')
{
buffer[i] = GGAbuffer[inx];
i++;
// if(i>sizeof(buffer)){
// return 0;
// }
inx++;
}
gga->calc.calculation = atoi(buffer);
// int declen_1 = (strlen(buffer));
// int dec_1 = atoi ((char *) buffer);
// int calc = (num_1) + (dec_1/pow(10, (declen_1)));
// gga->calc.calculation = calc;
inx++; // ',' после фиксации позиции;
// количесвто спутников
memset(buffer, '\0', 12);
i=0;
while (GGAbuffer[inx] != ',')
{
buffer[i] = GGAbuffer[inx];
i++;
// if(i>sizeof(buffer)){
// return 0;
// }
inx++;
}
gga->numofsat = atoi(buffer);
inx++;
/***************** skip HDOP *********************/
while (GGAbuffer[inx] != ',') inx++;
/*************** Altitude calculation ********************/
inx++;
memset(buffer, '\0', 12);
i=0;
while (GGAbuffer[inx] != ',')
{
buffer[i] = GGAbuffer[inx];
i++;
// if(i>sizeof(buffer)){
// return 0;
// }
inx++;
}
float alt = (atof(buffer));
// j = 0;
// while (buffer[j] != '.') j++;
// j++;
// int declen = (strlen(buffer))-j;
// int dec = atoi ((char *) buffer+j);
// int lat = (num) + (dec/pow(10, (declen)));
gga->alt.altitude = alt;//изменил
inx++;
gga->alt.unit = GGAbuffer[inx];
return 0;
}
int decodeRMC (char *RMCbuffer, RMCSTRUCT *rmc)
{
inx = 0;
char buffer[12];
int i = 0;
while (RMCbuffer[inx] != ',') inx++; // 1st ,
inx++;
while (RMCbuffer[inx] != ',') inx++; // После time ,
inx++;
if (RMCbuffer[inx] == 'A')
{
rmc->isValid = 1;
}
else
{
rmc->isValid =0;
return 1;
}
inx++;
inx++;
while (RMCbuffer[inx] != ',') inx++; // после latitude,
inx++;
while (RMCbuffer[inx] != ',') inx++; // после NS ,
inx++;
while (RMCbuffer[inx] != ',') inx++; // после longitude ,
inx++;
while (RMCbuffer[inx] != ',') inx++; // после EW ,
// Получить скорость
inx++;
i=0;
memset(buffer, '\0', 12);
while (RMCbuffer[inx] != ',')
{
buffer[i] = RMCbuffer[inx];
i++;
// if(i>sizeof(buffer)){
// return 0;
// }
inx++;
}
if (strlen (buffer) > 0){
int16_t num = (atoi(buffer));
int j = 0;
while (buffer[j] != '.') j++; // тоже, что и выше
j++;
int declen = (strlen(buffer))-j;
int dec = atoi ((char *) buffer+j);
float lat = num + (dec/pow(10, (declen)));// изменил
rmc->speed = lat;
}
else rmc->speed = 0;
// Получить курс
inx++;
i=0;
memset(buffer, '\0', 12);
while (RMCbuffer[inx] != ',')
{
buffer[i] = RMCbuffer[inx];
i++;
// if(i>sizeof(buffer)){
// return 0;
// }
inx++;
}
if (strlen (buffer) > 0){
int16_t num = (atoi(buffer));
int j = 0;
while (buffer[j] != '.') j++;
j++;
int declen = (strlen(buffer))-j;
int dec = atoi ((char *) buffer+j);
float lat = num + (dec/pow(10, (declen)));//изменил
rmc->course = lat;
}
else
{
rmc->course = 0;
}
// Получить дату
inx++;
i=0;
memset(buffer, '\0', 12);
while (RMCbuffer[inx] != ',')
{
buffer[i] = RMCbuffer[inx];
i++;
// if(i>sizeof(buffer)){
// return 0;
// }
inx++;
}
// Дата в формате 120295
day = atoi(buffer)/10000;
mon = (atoi(buffer)/100)%100;
yr = atoi(buffer)%100;
day = day+daychange;// коррекция из-за сдвига по Гринвичу
// сохранить данные в структуру
rmc->date.Day = day;
rmc->date.Mon = mon;
rmc->date.Yr = yr;
return 0;
}
Реализация модуля обработчика потока от UART-GPS uartProc_GNSS.c
Функция uart_Handler_GNSS - это основной обработчик UART-потока, вызывается постоянно из главного цикла.
Логика работы:
Проверяет, сработали ли прерывания DMA (половина буфера или полный буфер);
Если пришли новые данные - устанавливает флаг активности GPS (gParams.isGPS = 1);
-
Поиск GGA или RMC
В режиме shabloneMode = 0 ищет последовательность GGA или RMC;
Когда шаблон найден, переключаемся в режим shabloneMode = 1;
В режиме 1 копирует байты до символа конца строки (13 или 10);
-
Действие когда собрана строка
Записывает строку в буфер buf_GGA или buf_RMC в зависимости от типа;
Обновляет время последнего получения GPS (gps_time_receive).
-
Декодирование
Вызывает decodeGGA() и decodeRMC() для извлечения данных в структуру gpsData.
-
Формирование выходного пакета:
Если хотя бы одно из сообщений валидно, формирует строку с координатами, временем, количеством спутников, режим фикса, высотой и курсом.
Записывает в результат в uart_rezult_buf_out_AB[] с преамбулой 0x5A 0xA5 и длиной пакета.
Если в течение (DELAY_GPS_STATUS_CONNECT)1000 миллисекунд новых данных нет - GPS считается отключенным (gParams.isGPS= 0).
uint8_t* dpi_getGPS_buffer (Возвращает указатель на готовый пакет данных для передачи ведущему устройству, а так же его размер).
void uart_startRecieving_GNSS (Запускает прием данных от GPS-приемника в режиме DMA).
extern volatile uint8_t uartRxFullIRDone; //сработало прерывание по полному буферу
extern volatile uint8_t uartRxHalfIRDone; //сработало прерывание по половине
extern volatile uint8_t uartRxABDone;//сработало прерывание на прием от ведущего устройства
extern volatile uint8_t uartTxIRDone_AB; //сработало прерывание на отправку ведущему устройству
extern short status_UART;
//для проверки что gps отключили
#define DELAY_GPS_STATUS_CONNECT 1000//1000
uint32_t gps_time_recieve = 0;//когда получили данные от GPS
//uint8_t gps_connect_status=-1;//-1=не подключен 1=подключен
//E N D для проверки что gps отключили
#define SIZEBUF_result_out_ab 55 //57 //53 0x5A 0xA5 0
unsigned char uart_rezult_buf_out_AB[SIZEBUF_result_out_ab]={0,};
//"00.0000000 00.0000000 00:00:00.000 00 0 000.0 00.0>";
//60.1234567 47.1234567 16:11:33:128 09 2 134.2 34.6
int size_rez_buf_ab=0;
//буфер для сырых данных от GPS
#define SIZEBUF_uart_rx_buf 100//1000 уменьшил буфер для быстрого заполнения, чтобы у меня быстро не срабатывало время отсутствия gps
uint8_t uart_rx_buf[SIZEBUF_uart_rx_buf]={0,};
//буфер для GPS-GGA
#define SIZEBUF_buf_GGA 100
char buf_GGA[SIZEBUF_buf_GGA]={0,};
//буфер для GPS-RMC
#define SIZEBUF_buf_RMC 100
char buf_RMC[SIZEBUF_buf_RMC]={0,};
// NMEA
GPSSTRUCT gpsData;
int flagGGA = 0, flagRMC = 0;
//для составления строк
#define SIZEBUF_shablon 3
char shablonGNGGA[]="GGA";
char shablonGNRMC[]="RMC";
short shablon_iGNGGA=0;
short shablon_iGNRMC=0;
char shablonMode=0;//0=ищем шаблон//1=ожидаем символ конца строки 13
char shablonMode_1=0;
//буфер для сборки строки
#define SIZEBUF_result 100
char uart_rezult_buf1[SIZEBUF_result]={0,};
char uart_rezult_buf2[SIZEBUF_result]={0,};
char* uart_rezult_buf=uart_rezult_buf1;
short uart_rezult_buf_i=0;//индекс
char* uart_bufRow=uart_rezult_buf1;//буфер с целой строкой
//E N D буфер для сборки строки
//E N D для составления строк
void uart_Handler_GNSS(void)//эту функцию нужно вызывать постоянно
{
uint32_t ms = HAL_GetTick();
char isData=0;
char* pData=(char*)uart_rx_buf;
if(uartRxFullIRDone){
uartRxFullIRDone = 0;
pData=(char*)&uart_rx_buf[SIZEBUF_uart_rx_buf/2];
isData=1;
}
if(uartRxHalfIRDone){
uartRxHalfIRDone = 0;
isData=1;
}
if(isData){
isData=0;
//статус о том что gps активен
gParams.isGPS=1;
for(int i =0;i<SIZEBUF_uart_rx_buf/2;i++){
switch (shablonMode) {
case 0://0=ищем шаблон
if(pData[i]==shablonGNGGA[shablon_iGNGGA]){
shablon_iGNGGA++;
}else{
shablon_iGNGGA=0;
}
if(pData[i]==shablonGNRMC[shablon_iGNRMC]){
shablon_iGNRMC++;
}else{
shablon_iGNRMC=0;
}
if(shablon_iGNGGA || shablon_iGNRMC){
uart_rezult_buf[uart_rezult_buf_i]=pData[i];uart_rezult_buf_i++;
if(shablon_iGNGGA>=SIZEBUF_shablon || shablon_iGNRMC>=SIZEBUF_shablon){
shablon_iGNGGA=0;
shablon_iGNRMC=0;
shablonMode=1;//переходим в режим поиска конца строки
}
}else{
uart_rezult_buf_i=0;
}
break;
case 1://1=ожидаем символ конца строки 13
if(pData[i]==13 || pData[i]==10){
//собрали строку
uart_rezult_buf[uart_rezult_buf_i]='\r';
uart_rezult_buf_i++;
if(uart_rezult_buf_i>=SIZEBUF_result){uart_rezult_buf_i=SIZEBUF_result;}
uart_rezult_buf[uart_rezult_buf_i]='\n';
//переключаемся на следующий буфер
if(uart_rezult_buf==uart_rezult_buf1){
uart_rezult_buf=uart_rezult_buf2;
uart_bufRow=uart_rezult_buf1;
}else{
uart_rezult_buf=uart_rezult_buf1;
uart_bufRow=uart_rezult_buf2;
}
if(uart_bufRow[0] == 'G')//Поток данных от GGA
{
memcpy(buf_GGA,uart_bufRow,SIZEBUF_buf_GGA);
gps_time_recieve=ms;
}
if(uart_bufRow[0] == 'R')//Поток данных от RMC
{
memcpy(buf_RMC,uart_bufRow,SIZEBUF_buf_RMC);
gps_time_recieve=ms;
}
//-----------------------------------------Работа с NMEA--------------------------------------------------------------
if (decodeGGA(buf_GGA, &gpsData.ggastruct) == 0) flagGGA = 2; // 2 indicates the data is valid
else flagGGA = 1; // 1 indicates the data is invalid
//if(ttt){decodeGGA(buf_GGA, &gpsData.ggastruct);}//отладка
if (decodeRMC(buf_RMC, &gpsData.rmcstruct) == 0) flagRMC = 2; // 2 indicates the data is valid
else flagRMC = 1; // 1 indicates the data is invalid
if ((flagGGA == 2) | (flagRMC == 2))
{
uart_rezult_buf_out_AB[0]=0x5A;
uart_rezult_buf_out_AB[1]=0xA5;
//В данном буфере находятся данные от GPS после расшифровки NMEA, их можно уже непосредственно продвигать дальше - - -
size_rez_buf_ab=sprintf((char*)&uart_rezult_buf_out_AB[3], "%.6f %.6f %02d:%02d:%02d.%03d %02d %d %.2f %.2f>",
gpsData.ggastruct.lcation.latitude,gpsData.ggastruct.lcation.longitude,
gpsData.ggastruct.tim.hour,gpsData.ggastruct.tim.min, gpsData.ggastruct.tim.sec,
gpsData.ggastruct.tim.msec,gpsData.ggastruct.numofsat,gpsData.ggastruct.calc.calculation,
gpsData.ggastruct.alt.altitude,gpsData.rmcstruct.course);
gParams.isRTK_GPS = gpsData.ggastruct.calc.calculation;//параметры режима работы GPS
uart_rezult_buf_out_AB[2]=size_rez_buf_ab;
size_rez_buf_ab+=3;//размер преамбулы+поле размер
}
//переходим в режим поиска шаблона
uart_rezult_buf_i=0;
shablonMode=0;
}else{
uart_rezult_buf[uart_rezult_buf_i]=pData[i];
uart_rezult_buf_i++;
if(uart_rezult_buf_i>=SIZEBUF_result){uart_rezult_buf_i=SIZEBUF_result;}
}
break;
}
}
//-опустить
}
if(gps_time_recieve && ((ms - gps_time_recieve) >= DELAY_GPS_STATUS_CONNECT)){
gParams.isGPS=0;
//uart_startRecieving_GNSS();//было убрано, потому что когда часто происходило условие,
// было обнуление статуса, и перезапуск приема от uart, это и была ошибка, потому что записывалось по верх уже имеющихся данных, новые данные
}
}
//Геттер на отправку
uint8_t* dpi_getGPS_buffer(int* ret_buff_size)
{
if(ret_buff_size){*ret_buff_size=size_rez_buf_ab;}
return uart_rezult_buf_out_AB;
}
void uart_startRecieving_GNSS(void)
{
status_UART=1;//1=startRecieving 2=RxHalf 3=RxCplt 4=прием от АБ (uart3)//отладка
memset(uart_rx_buf,0,sizeof(uart_rx_buf));
HAL_UART_Receive_DMA(&huart1, (uint8_t*)uart_rx_buf, SIZEBUF_uart_rx_buf);//начинаю прием данных от gps на uart1
}
Функции прерываний
void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart)
{
if(huart == &huart1){
status_UART=2;//1=startRecieving 2=RxHalf 3=RxCplt //отладка
uartRxHalfIRDone = 1; //сработало прерывание по половине
}
}
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) //Callback от UART RX
{
if(huart == &huart1){//GPS
//HAL_UART_DMAStop(huart);
status_UART=3;//1=startRecieving 2=RxHalf 3=RxCplt //отладка
uartRxFullIRDone = 1; //сработало прерывание по полному буферу
}
}
// Обработчик ошибок UART
void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1 && enResetUART) { //GPS
/* Сброс ошибок и восстановление работы */
HAL_UART_DeInit(huart);
HAL_UART_Init(huart);
uartRxFullIRDone = 0;
uartRxHalfIRDone = 0;
uart_startRecieving_GNSS();
// if(status_UART==1){//1-send 2=startRecieving 3=finishRecieving 4=TxCplt
// uart_sendData();
// }else{
// uart_startRecieving_GNSS();
// }
}
Главный модуль
void proj_main()
{
volatile const char *ch = ";V-F-BIN;ver: "VER_PROG(VER_a,VER_b,VER_c);(void)ch;
uart_GNSS_init();
uart_startRecieving_GNSS();//Здесь я начинаю принимать данные от gps
while (1){
//хэндлеры
uart_Handler_GNSS();
}//while (1)
}
Ссылка на скачивание исходного кода [ https://t.me/ChipCraft В закрепленном сообщении [ #исскуствомк_исходный_код -Исходный код для Module_GPS_NMEA0183]
Немного искусства :)

Постарался записать трек своими проходами слово H A B R, но к сожалению из-за плохого качества сигнала, получилось коряво, интересующие для меня данные с gps это (Дата, время, широта, долгота и высота).
Графический GPS-трекер я разработал для тестирования на С#, если Вам будет интересно, пишите в комментариях и я с радостью напишу статью.
Если статья показалась Вам интересной, буду рад выпустить для Вас еще множество статей исследований по всевозможным видам устройств, так что, если не хотите их пропустить – буду благодарен за подписку на мой ТГ-канал: https://t.me/ChipCraft.
Комментарии (20)
Indemsys
13.08.2025 06:29Про DMA неясно написано.
Там буфер не кольцевой, а скорее переключаемый.
DMA при отсутствии полных данных просто замрет и не выдаст прерывания.
Чтобы забрать данные без прерывания придется организовать программный полинг.
Автора спасает только то, что модули GPS выдают данные безостановочно.
Но будут задержки в десятки миллисекунд на приеме.
Для быстрых движущихся средств это уже будет проблемой.
Метод приема в статье недоработан. Так лучше не делать.
Все эти сорсы тоже не несут смысла.
Claude Sonnet отлично разбирается в NMEA, и за секунды напишет полный парсер, гораздо полнее чем в этой статье и еще добавит инерциальную навигацию сверху.DM_ChipCraft Автор
13.08.2025 06:29Спасибо большое за комментарий, обязательно внесу поправку, и постараюсь поймать момент когда GPS может выдавать данные с задержкой и провести тесты своего примера
IgnatF
13.08.2025 06:29Для все эти датчики по стандарту выдают строку, с данным разделенными через запятую. Просто разбиваем строку по ней, и нужный элемент из массива вставляем куда необходимо. Для нео 6м так в свое время делал. Насчет уровня напряжения, мы же на получение данных в основном работаем, тут разницы нет.
DM_ChipCraft Автор
13.08.2025 06:29По поводу уровня напряжения, я к сожалению уже спалил порт-uart несколько раз.. всегда конечно теперь стараюсь делать преобразование.
chnav
13.08.2025 06:29Для тех кто пишет навигацию реального времени надо учитывать время прихода сообщений. Пока приняли строку целиком на скорости 9600, пока разобрали - объект уже уехал. Я думаю многие сталкивались с тем что, например, вы уже на перекрёстке, а в старом навигаторе ещё только подъезжаете. По-хорошему надо отрабатывать PPS и делать экстраполяцию. В крайнем случае делать синхронизацию по внутреннему таймеру, отмечая время прихода первого байта '$' очередной эпохи вне зависимости от типа сообщения (GGA, RMC, ZDA).
DM_ChipCraft Автор
13.08.2025 06:29Большое Вам спасибо, очень полезный совет, обязательно прислушаюсь к нему и произведу доработку, благодарность, еще раз.
SpiderEkb
13.08.2025 06:29Экстаполяция будет давать "забросы" в поворотах. Тут что-то хитрее нужно. Какое-то быстрое извлечение координат для отображения. А потом уже полный детальный разбор.
chnav
13.08.2025 06:29Для сложных траекторий идеальный способ - фильтр Калмана + MEMS + компас.
И ещё нужно учитывать, что приёмнику тоже нужно время (доли секунд ?) для рассчета координаты. Можно подобрать latency экспериментально и вносить в виде константы. Так что PPS всё-таки предпочтительнее.
SpiderEkb
13.08.2025 06:29С ФК не так просто... Я игрался со всем этим для обработки уже существующих треков. Достаточно сложно настроить ФК чтобы он более-менее корректно работал только по GPS данным. Тут практически не хуже будет работать намного более простоя и быстрый алгоритм DES - двойное экспоненциальное сглаживание. Но тоже не идеально.
ФК, наверное, будет лучше если у вас есть еще инерциальная система - независимые данные по скорости, ускорению и курсу. Вот тогда, если использовать их в предсказательной части ФК, можно добиться неплохих результатов.
chnav
13.08.2025 06:29С постобработкой треков всё сложно - координаты NMEA уже сглажены фильтром Калмана в приёмнике и поэтому от забросов на поворотах никак не избавиться.
В геодезии в постобработке сырых данных применяют двойной проход туда-обратно, грубо говоря вторым проходом идём от конца измерений к началу. Но опять же насколько это применимо к трекам... Так что да, ИНС это наилучший выход.
В своё время, ещё до массового распространения смартфонов с MEMS, был такой топовый навигатор TomTom Go 930 со встроеным акселерометром. Идея в том чтобы по поперечным ускорениям отслеживать ответвления в тоннелях. Грубо, но лучше чем ничего.
SpiderEkb
13.08.2025 06:29Уже записанный трек обрабатывать проще т.к. там уже есть все данные от начала и до конца.
Насчет ФК в чипах не сильно уверен что там он достатчоно сильный. Там же принцип фактически тот же самый, что в тривиальном ФНЧ, разница только в том, что коэффициент фильтрации (сглаживания) постоянно корректируется.
chnav
13.08.2025 06:29Насчет ФК в чипах не сильно уверен что там он достатчоно сильный.
Зависит от чипа. Я разные треки видел, как правило чем хуже приёмник (н-р ранние смартфоны) тем сильнее фильтр, чтобы хоть как-то сгладить данные от ущербной антенны.
В нормальных приёмниках динамическая модель настраивается (юзером) - пешеход, автомобиль, самолёт, море.
DM_ChipCraft Автор
13.08.2025 06:29В целом конечно, благодаря Вам [chnav,SpiderEkb ] появились варианты, как можно постараться улучшить систему, надо пробовать, тестировать, спасибо за рассуждение :))
SpiderEkb
13.08.2025 06:29Я в своей время занимался обработкой треков, правда, уже записанных логгером.
Потом отдал исходники другому человеку, который выложил их на github Посмотрите, может что интересное найдется... Можно начать с документации - там основные алгоритмы описаны.
SpiderEkb
А чем, например, вот этот парсер не устроил? Или вот этот?
DM_ChipCraft Автор
Я естественно не претендую что я лучший парсер в мире продемонстрировал :) когда я занимался разработкой этого проекта в закромах сети я к сожалению не видел данные варианты, собирал информацию по частям, вот этот вариант мне понравился [https://nmea.sourceforge.net/] обязательно его проанализирую, спасибо Вам большое за информацию.
SpiderEkb
Ну я не говорил про лучше-хуже :-) Просто спросил - это старые парсеры, они давно доступны. Просто подумал - может быть были какие-то причины именно свой написать.
Бывают ситуации когда готовое по каким-то причинам не подходит...
И, кстати, заголовок сообщения выглядит так:
$[тип источника, 2 символа][тип сообщения, 3 символа]
т.е. тот же RMC может быть
$GPRMC для GPS,
$GNRMC для ГЛОНАСС
или $GLRMC, $GARMC, $GBRMC... (Gallileo, BeDow еще что-то там).
Для "мультисистемных" приемников там будет валиться дикая каша из всего подряд. По несколько штук одних и тех же сообщений от разных систем на одну точку.
DM_ChipCraft Автор
Так точно, GP и GN я видел в потоках, поэтому и сделал простой алгоритм поиска по шаблону [GGA и RMC], у меня было несколько GPS-приемников, это LS23030/36 он выдает GPGGA/GPRMC, и еще один очень крутой GPS-приемник , это DGPS FORA ONE точность замечательная, вот как раз этот gps выдает GNGGA/GNRMC, тестировал свою систему и фильтр работает корректно.
DM_ChipCraft Автор
А вот если Вам интересно, сравнение GPS-приемников
SpiderEkb
Да, у дорогих приемников есть внутренние алгоритмы обработки сигналов.
Вообще, это очень большая проблема. Причем, чем меньше скорость, тем более "неровный" трек получается. Там же не строго точное положение выдается, а наиболее вероятное - тот самый HDOP как раз и показывает (косвенно) радиус окружности в которой может быть расположено реальное положение. А выдается ее центр, как "наиболее вероятное".
Если стоять на месте, то точка положения будет "гулять".
В городской застройке все еще хуже т.к. там много переотраженных от стен знаний сигналов.
Современные дорогие чипы умеют сглаживать трек, избавляясь от "пилы".