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(UART) к микроконтроллеру STM32F103
Подключение модуля-GPS(UART) к микроконтроллеру STM32F103

Сигнал GPS, подключается к выводу PA10-31_контакт - RX(МК-STM32F103)

Для более стабильного напряжения питания можно использовать следующую схему, в которой работает понижающий преобразователь MP231, но необходим источник +12В, в моем случае используется аккумуляторная сборка (NiMH/Pb +12В).

Понижающий преобразователь напряжения MP2315 [ +12V до +5V  ]
Понижающий преобразователь напряжения MP2315 [ +12V до +5V ]

!!! P.S. заранее прошу прощения за некачественные картинки осциллограмм, осциллограф (с возможностью сохранения данных на флешку), некорректно производит измерения ...!!!

Вид осциллограммы передаваемых данных модуля-gps(uart) (линия TX)

Осциллограмма амплитуды данных от модуля-gps(uart)
Осциллограмма амплитуды данных от модуля-gps(uart)

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

Схема подключения GPS(RS-232)

Подключение модуля-GPS(RS-232) через ADM3202 к МК-STM32F103
Подключение модуля-GPS(RS-232) через ADM3202 к МК-STM32F103

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

Схема подключения ADM3202 к МКSTM32 - макет

Макет ADM3202 и подключение к STM32F103
Макет ADM3202 и подключение к STM32F103

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

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

Линейный стабилизатор напряжения LP2985 (+5В  +3В)
Линейный стабилизатор напряжения LP2985 (+5В +3В)

Краткая информация о преобразователе 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) до преобразования ADM3202
Осциллограмма амплитуды данных от модуля-gps(rs-232) до преобразования ADM3202

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

Вид осциллограммы передаваемых данных модуля-gps(rs-232) после преобразования ADM3202

Осциллограмма амплитуды данных от модуля-gps(rs-232) после преобразования ADM3202
Осциллограмма амплитуды данных от модуля-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).

все остальные параметры без изменений.

Настройка "Parameter settings"
Настройка "Parameter settings"

Конфигурация NVIC Settings

Захожу в параметр (NVIC Settings) и включаю глобальное прерывание

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

Настройка "NVIC Settings"
Настройка "NVIC Settings"

При работе с GPS-модулями, которые передают NMEA-сообщения раз в секунду (1Hz), важно правильно организовать прием данных, чтобы не пропустить ни одного пакета. Необходимо настроить DMA в режиме Circular данный режим минимизирует нагрузку на процессор и гарантирует надежный прием.

Конфигурация DMA Settings

Захожу в параметр DMA Settings и выполняю следующие настройки:

  1. Выбор потока/канала: USART1_RX (прием данных);

  2. Mode: Circular ;

  3. Increment Memory Address: Enabled (автоинкремент памяти);

  4. Data Width: Byte (8 бит, соответствует формату NMEA).

Настройка "DMA Settings"
Настройка "DMA Settings"

Реализация программного кода(настройка и прием данных)

Коротко о NMEA 0183

Это текстовый протокол, используемый для передачи данных между морским и авиационным навигационным оборудованием, включая GPS-приемники. Большинство современных GPS-модулей выводят информацию именно в этом формате.

Основные особенности:

  • Текстовый формат – данные передаются в виде ASCII-строк;

  • Структура сообщений – каждая строка начинается с $, содержит идентификатор типа данных и заканчивается контрольной суммой;

  • Скорость передачи – обычно 9600 бод (но может быть и выше для высокочастотных модулей);

  • Частота обновления – чаще всего 1 раз в секунду (1Hz), но бывают 5Hz, 10Hz и более.

Пример строки (GGA – Global Positioning System Fix Data):

GGA,112530.000,6012.3456,N,03015.6789,E,1,10,0.95,45.3,M,12.5,M,,*65

Поле

Значение

Описание

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):

RMC,112530.000,A,6012.3456,N,03015.6789,E,5.12,87.45,110825,,,A*6C

Поле

Значение

Описание

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-потока, вызывается постоянно из главного цикла.

Логика работы:

  1. Проверяет, сработали ли прерывания DMA (половина буфера или полный буфер);

  2. Если пришли новые данные - устанавливает флаг активности GPS (gParams.isGPS = 1);

  3. Поиск GGA или RMC

    • В режиме shabloneMode = 0 ищет последовательность GGA или RMC;

    • Когда шаблон найден, переключаемся в режим shabloneMode = 1;

    • В режиме 1 копирует байты до символа конца строки (13 или 10);

  4. Действие когда собрана строка

    • Записывает строку в буфер buf_GGA или buf_RMC в зависимости от типа;

    • Обновляет время последнего получения GPS (gps_time_receive).

  5. Декодирование

    • Вызывает decodeGGA() и decodeRMC() для извлечения данных в структуру gpsData.

  6. Формирование выходного пакета:

    • Если хотя бы одно из сообщений валидно, формирует строку с координатами, временем, количеством спутников, режим фикса, высотой и курсом.

    • Записывает в результат в uart_rezult_buf_out_AB[] с преамбулой 0x5A 0xA5 и длиной пакета.

  7. Если в течение (DELAY_GPS_STATUS_CONNECT)1000 миллисекунд новых данных нет - GPS считается отключенным (gParams.isGPS= 0).

  8. uint8_t* dpi_getGPS_buffer (Возвращает указатель на готовый пакет данных для передачи ведущему устройству, а так же его размер).

  9. 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]

Немного искусства :)

Тестовый проход GPS
Тестовый проход GPS

Постарался записать трек своими проходами слово H A B R, но к сожалению из-за плохого качества сигнала, получилось коряво, интересующие для меня данные с gps это (Дата, время, широта, долгота и высота).

Графический GPS-трекер я разработал для тестирования на С#, если Вам будет интересно, пишите в комментариях и я с радостью напишу статью.


Если статья показалась Вам интересной, буду рад выпустить для Вас еще множество статей исследований по всевозможным видам устройств, так что, если не хотите их пропустить – буду благодарен за подписку на мой ТГ-канал: https://t.me/ChipCraft.

Комментарии (20)


  1. SpiderEkb
    13.08.2025 06:29

    А чем, например, вот этот парсер не устроил? Или вот этот?


    1. DM_ChipCraft Автор
      13.08.2025 06:29

      Я естественно не претендую что я лучший парсер в мире продемонстрировал :) когда я занимался разработкой этого проекта в закромах сети я к сожалению не видел данные варианты, собирал информацию по частям, вот этот вариант мне понравился [https://nmea.sourceforge.net/] обязательно его проанализирую, спасибо Вам большое за информацию.


      1. SpiderEkb
        13.08.2025 06:29

        Ну я не говорил про лучше-хуже :-) Просто спросил - это старые парсеры, они давно доступны. Просто подумал - может быть были какие-то причины именно свой написать.

        Бывают ситуации когда готовое по каким-то причинам не подходит...

        И, кстати, заголовок сообщения выглядит так:

        $[тип источника, 2 символа][тип сообщения, 3 символа]

        т.е. тот же RMC может быть

        $GPRMC для GPS,
        $GNRMC для ГЛОНАСС
        или $GLRMC, $GARMC, $GBRMC... (Gallileo, BeDow еще что-то там).

        Для "мультисистемных" приемников там будет валиться дикая каша из всего подряд. По несколько штук одних и тех же сообщений от разных систем на одну точку.


        1. DM_ChipCraft Автор
          13.08.2025 06:29

          Так точно, GP и GN я видел в потоках, поэтому и сделал простой алгоритм поиска по шаблону [GGA и RMC], у меня было несколько GPS-приемников, это LS23030/36 он выдает GPGGA/GPRMC, и еще один очень крутой GPS-приемник , это DGPS FORA ONE точность замечательная, вот как раз этот gps выдает GNGGA/GNRMC, тестировал свою систему и фильтр работает корректно.


        1. DM_ChipCraft Автор
          13.08.2025 06:29

          А вот если Вам интересно, сравнение GPS-приемников


          1. SpiderEkb
            13.08.2025 06:29

            Да, у дорогих приемников есть внутренние алгоритмы обработки сигналов.

            Вообще, это очень большая проблема. Причем, чем меньше скорость, тем более "неровный" трек получается. Там же не строго точное положение выдается, а наиболее вероятное - тот самый HDOP как раз и показывает (косвенно) радиус окружности в которой может быть расположено реальное положение. А выдается ее центр, как "наиболее вероятное".

            Если стоять на месте, то точка положения будет "гулять".

            В городской застройке все еще хуже т.к. там много переотраженных от стен знаний сигналов.

            Современные дорогие чипы умеют сглаживать трек, избавляясь от "пилы".


  1. Indemsys
    13.08.2025 06:29

    Про DMA неясно написано.
    Там буфер не кольцевой, а скорее переключаемый.
    DMA при отсутствии полных данных просто замрет и не выдаст прерывания.
    Чтобы забрать данные без прерывания придется организовать программный полинг.
    Автора спасает только то, что модули GPS выдают данные безостановочно.
    Но будут задержки в десятки миллисекунд на приеме.
    Для быстрых движущихся средств это уже будет проблемой.
    Метод приема в статье недоработан. Так лучше не делать.

    Все эти сорсы тоже не несут смысла.
    Claude Sonnet отлично разбирается в NMEA, и за секунды напишет полный парсер, гораздо полнее чем в этой статье и еще добавит инерциальную навигацию сверху.


    1. DM_ChipCraft Автор
      13.08.2025 06:29

      Спасибо большое за комментарий, обязательно внесу поправку, и постараюсь поймать момент когда GPS может выдавать данные с задержкой и провести тесты своего примера


  1. IgnatF
    13.08.2025 06:29

    Для все эти датчики по стандарту выдают строку, с данным разделенными через запятую. Просто разбиваем строку по ней, и нужный элемент из массива вставляем куда необходимо. Для нео 6м так в свое время делал. Насчет уровня напряжения, мы же на получение данных в основном работаем, тут разницы нет.


    1. DM_ChipCraft Автор
      13.08.2025 06:29

      По поводу уровня напряжения, я к сожалению уже спалил порт-uart несколько раз.. всегда конечно теперь стараюсь делать преобразование.


  1. chnav
    13.08.2025 06:29

    Для тех кто пишет навигацию реального времени надо учитывать время прихода сообщений. Пока приняли строку целиком на скорости 9600, пока разобрали - объект уже уехал. Я думаю многие сталкивались с тем что, например, вы уже на перекрёстке, а в старом навигаторе ещё только подъезжаете. По-хорошему надо отрабатывать PPS и делать экстраполяцию. В крайнем случае делать синхронизацию по внутреннему таймеру, отмечая время прихода первого байта '$' очередной эпохи вне зависимости от типа сообщения (GGA, RMC, ZDA).


    1. DM_ChipCraft Автор
      13.08.2025 06:29

      Большое Вам спасибо, очень полезный совет, обязательно прислушаюсь к нему и произведу доработку, благодарность, еще раз.


    1. SpiderEkb
      13.08.2025 06:29

      Экстаполяция будет давать "забросы" в поворотах. Тут что-то хитрее нужно. Какое-то быстрое извлечение координат для отображения. А потом уже полный детальный разбор.


      1. chnav
        13.08.2025 06:29

        Для сложных траекторий идеальный способ - фильтр Калмана + MEMS + компас.

        И ещё нужно учитывать, что приёмнику тоже нужно время (доли секунд ?) для рассчета координаты. Можно подобрать latency экспериментально и вносить в виде константы. Так что PPS всё-таки предпочтительнее.


        1. SpiderEkb
          13.08.2025 06:29

          С ФК не так просто... Я игрался со всем этим для обработки уже существующих треков. Достаточно сложно настроить ФК чтобы он более-менее корректно работал только по GPS данным. Тут практически не хуже будет работать намного более простоя и быстрый алгоритм DES - двойное экспоненциальное сглаживание. Но тоже не идеально.

          ФК, наверное, будет лучше если у вас есть еще инерциальная система - независимые данные по скорости, ускорению и курсу. Вот тогда, если использовать их в предсказательной части ФК, можно добиться неплохих результатов.


          1. chnav
            13.08.2025 06:29

            С постобработкой треков всё сложно - координаты NMEA уже сглажены фильтром Калмана в приёмнике и поэтому от забросов на поворотах никак не избавиться.

            В геодезии в постобработке сырых данных применяют двойной проход туда-обратно, грубо говоря вторым проходом идём от конца измерений к началу. Но опять же насколько это применимо к трекам... Так что да, ИНС это наилучший выход.

            В своё время, ещё до массового распространения смартфонов с MEMS, был такой топовый навигатор TomTom Go 930 со встроеным акселерометром. Идея в том чтобы по поперечным ускорениям отслеживать ответвления в тоннелях. Грубо, но лучше чем ничего.


            1. SpiderEkb
              13.08.2025 06:29

              Уже записанный трек обрабатывать проще т.к. там уже есть все данные от начала и до конца.

              Насчет ФК в чипах не сильно уверен что там он достатчоно сильный. Там же принцип фактически тот же самый, что в тривиальном ФНЧ, разница только в том, что коэффициент фильтрации (сглаживания) постоянно корректируется.


              1. chnav
                13.08.2025 06:29

                Насчет ФК в чипах не сильно уверен что там он достатчоно сильный.

                Зависит от чипа. Я разные треки видел, как правило чем хуже приёмник (н-р ранние смартфоны) тем сильнее фильтр, чтобы хоть как-то сгладить данные от ущербной антенны.

                В нормальных приёмниках динамическая модель настраивается (юзером) - пешеход, автомобиль, самолёт, море.


  1. DM_ChipCraft Автор
    13.08.2025 06:29

    В целом конечно, благодаря Вам [chnav,SpiderEkb ] появились варианты, как можно постараться улучшить систему, надо пробовать, тестировать, спасибо за рассуждение :))


    1. SpiderEkb
      13.08.2025 06:29

      Я в своей время занимался обработкой треков, правда, уже записанных логгером.

      Потом отдал исходники другому человеку, который выложил их на github Посмотрите, может что интересное найдется... Можно начать с документации - там основные алгоритмы описаны.