Czy wyobrażasz sobie życie bez nawigacji? Ja na przykład nie. Urządzenia lokalizacyjne są jak dla mnie jednym z lepszych wynalazków. Tym bardziej, że mamy takie w naszych kieszeniach. Możemy również dołączyć odbiornik GPS do naszych projektów elektronicznych. Zobacz jakie to proste!
Jak działa GPS?
W bardzo mocnym skrócie. W okół Ziemi krąży 31 satelitów których parametry lotów są dokładnie znane. Znajdują się na wysokości ok 20 tysięcy kilometrów nad powierzchnią Ziemi. Orbity tych satelitów są kołowe, a okrążenie planety zajmuje im dokładnie 11 godzin i 58 minut. Na stałe włączonych jest 28 satelitów. Reszta jest testowa lub są po prostu wyłączone.
Satelity te emitują sygnały na dwóch częstotliwościach: 1575,42 MHz oraz 1227,6 MHz. Na podstawie sygnałów odebranych od satelitów, odbiornik jest w stanie obliczyć naszą pozycję.
Tylko jak? Na przykład znając prędkość rozchodzenia się fali radiowej możemy określić czas dotarcia sygnałów radiowych do odbiornika. W nadawanym sygnale znajduje się kilka cennych dla namierzania informacji, w tym pozycje satelitów, dlatego na tej podstawie jesteśmy w stanie nakreślić nasze współrzędne. Im więcej jest widocznych satelitów, tym dokładność wyznaczania pozycji jest większa.
Bardzo dobry opis całego systemu znajdziesz na kanale RS Elektronika na YouTube. Bardzo polecam Ci tam zerknąć.
Odbiornik GPS NEO6MV2
Producentem modułu GPS jest firma u-blox. Odbiorniki NEO-6 to cała rodzina układów. Ten, którym się dzisiaj zajmuję to dokładnie NEO-6-M-0-001, a moduł na którym jest umieszczony nazywa się NEO6MV2.
Dokumentacja podaje kilka ciekawych informacji na temat samego odbiornika. Najważniejsze z nich to:
- Maksymalna ilość satelitów: 50
- Zimny start: 27 sekund
- Ciepły start: 27 sekund
- Gorący start: 1 sekunda
- Wspomagany start (A-GPS): poniżej 3 sekund
- Dokładność pozycjonowania: 2,5 metra
- Maksymalna częstotliwość aktualizacji pozycji: 5 Hz (domyślnie 1 Hz)
- Komunikacja: UART, USB, SPI
Kompletny moduł posiada statusową diodę LED, pamięć EEPROM do zapamiętywania konfiguracji oraz baterię podtrzymującą, która pomaga przy ciepłym/gorącym starcie.
Na złącze modułu NEO6MV2 został wyprowadzony jedynie interfejs UART. Jest on podstawową formą komunikacji. Moduł posiada jeszcze jedno złącze µFL do podłączenia anteny. Można kupić moduł z anteną w zestawie. Jest to najczęściej niewielka antena, która działa dosyć dobrze na zewnątrz, natomiast w pomieszczeniach jest zdecydowanie gorzej. Ja musiałem długo czekać z anteną przyklejoną do okna.
Po podłączeniu zasilania od razu na pin RX układ nadaje komunikaty NMEA. Czymże jest ten enigmatyczny skrót?
NMEA
NMEA (National Marine Electronics Association) jest to protokół komunikacyjny pomiędzy morskimi urządzeniami elektronicznymi. Ciekawe co nie? Jednak jest on standardem jeżeli chodzi o urządzenia GPS.
Dane są w postaci komunikatów zapisanych kodem ASCII. Są wiec w pewnym stopniu czytelne dla człowieka. Pojedynczy komunikat może zawierać maksymalnie 82 znaki. Każde zdanie rozpoczyna się dolarem ($) następnie widnieje identyfikator zdania, a po nim kolejne dane oddzielone przecinkiem. Zdanie kończy się zestawem znaków “\n\r”.
Dla systemu GPS wszystkie komunikaty będą rozpoczynały się od “$GP”. Zdefiniowane jest kilkadziesiąt rodzajów wiadomości, ale dla GPS znalazłem 19 użytkowych, które opisane są tutaj: http://aprs.gids.nl/nmea/
NEO-6 w domyślnej konfiguracji wystawia tylko kilka z nich:
- $GPRMC – rekomendowana wiadomość, minimalny zestaw danych nawigacyjnych
- $GPVTG – kurs i prędkość
- $GPGGA – wyznaczone w odbiorniku dane nawigacyjne GPS
- $GPGSA – Współczynniki “rozmycia” dokładności DOP
- $GPGSV – Numery PRN i położenie potencjalnie widocznych satelitów oraz siła sygnałów
- $GPGLL – Położenie geograficzne
Niektóre dane powtarzają się w kilku komunikatach. Oczywiście można edytować to, co nadaje odbiornik na interfejs UART. Można do tego wykorzystać protokół UBX jednak łatwiej jest podłączyć moduł do komputera i skorzystać z aplikacji u-center. Jest to potężne narzędzie które oczywiście odczyta dane z modułu NEO-6, ale też dowolnie skonfiguruje, a nawet zaktualizuje firmware modułu. Do podłączenia modułu, który ma tylko UART, możesz użyć konwertera UART<->USB, który jest do kupienia w moim sklepie.
Podłączenie oraz konfiguracja Cube
Dzisiaj skorzystam z płytki Nucleo F401RE. Do konfiguracji firmware oraz programowania wykorzystałem STM32CubeIDE w wersji 1.0.2 oraz biblioteki HAL w wersji F4 1.24.1.
Pozwól, że chyba po raz pierwszy na łamach bloga pominę schemat. Podłączenie modułu jest na prawdę proste i sprowadza się do podłączenia zasilania oraz pinów TX i RX do Nucleo. Płytkę z GPS zasiliłem z 5V, a komunikację podłączyłem do UART1:
- TX GPS do RX Nucleo na PA10
- RX GPS do TX Nucleo na PA9
Wyprowadziłem sobie też pin testowy do pomiaru czasów 🙂
Teraz Cube. Poza standardowymi ustawieniami dla płytki Nucleo jak np. HCLK = 84 MHz, ustaw UART1 jako 9600 baud 8n1. Włącz też przerwanie globalne od UART1 w zakładce NVIC Settings.
Tak skonfigurowany projekt możesz wygenerować 🙂 Po wygenerowaniu bardzo ważne jest, aby w opcjach projektu dodać do linkera -u_printf_float bo będziemy z tego korzystać.
Kod
Dawno nie pisałem nic bazującego na odbiorze UART. Jednak dałem radę hehe 🙂 Tym razem postawiłem na odbiór pojedynczych znaków w przerwaniu, aby nie oczekiwać blokująco na nadchodzące dane. Po kolei.
W pierwszej kolejności wypadałoby zainicjować bibliotekę. Utworzyłem zmienną strukturalną do przechowywania wszystkich danych dotyczących naszego modułu. Dzięki tej strukturze możesz podłączyć kilka niezależnych modułów NEO-6 ponieważ zawiera ona wskaźnik do UART oraz własny bufor odbiorczy i roboczy dla niego.
typedef struct { // // UART stuff // UART_HandleTypeDef *neo6_huart; uint8_t UartBuffer[GPS_UART_BUFFER_SIZE]; uint8_t UartBufferHead; uint8_t UartBufferTail; uint8_t UartBufferLines; uint8_t WorkingBuffer[GPS_WORKING_BUFFER_SIZE]; // // Time and Date // uint8_t Hour; uint8_t Minute; uint8_t Second; uint8_t Day; uint8_t Month; uint8_t Year; // // Position // double Latitude; char LatitudeDirection; double Longitude; char LongitudeDirection; double Altitude; // // Speed // double SpeedKnots; double SpeedKilometers; // // Satelites parameters // uint8_t SatelitesNumber; uint8_t Quality; // 0 - no Fix, 1 - Fix, 2 - Dif. Fix uint8_t FixMode; // 1 - no Fiz, 2 - 2D, 3 - 3D double Dop; // Dilution of precision double Hdop; // Dilution of precision for flat coords double Vdop; // Dilution of precision for height }NEO6_State;
Inicjalizacja w takim razie wypełnia strukturę danymi początkowymi, przypisuje UART dla odbiornika oraz uruchamia nasłuch na interfejsie szeregowym.
void NEO6_Init(NEO6_State *GpsStateHandler, UART_HandleTypeDef *huart);
Przejdę do omówienia w jaki sposób STM32 pozyskuje wszystkie dane z odbiornika GPS. Jak pamiętasz, NEO-6 sam wysyła dane standardowo co sekundę. Przerwanie UART ustawione jest na każdy, pojedynczy odebrany bajt. Dlaczego? Nigdy nie jesteśmy pewni ile przyjdzie znaków. Poza tym w ten sposób łatwiej jest kontrolować moment w którym nadszedł koniec komunikatu przez rozpoznanie znaku ‘\n’.
Odbiór znaku znajduje się w funkncji NEO6_ReceiveUartChar, którą należy umieścić w przerwaniu od zakończenia odbioru UART.
/* USER CODE BEGIN 4 */ void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if(huart == GpsState.neo6_huart) { NEO6_ReceiveUartChar(&GpsState); } } /* USER CODE END 4 */
Zerknijmy wgłąb tej funkcji. Działa ona na buforze odbiorczym ze struktury danych GPS. Jest to bufor kołowy/cykliczny w którym nie ma początku oraz końca. Nowe dane dopisywane są w kolejne miejsce w buforze, a zabierane są usuwane z “początku”. Przy czym ten początek i koniec cały czas wędrują w zależności co na tym buforze jest wykonywane.
Jako obsługę przepełnienia wybrałem porzucanie nadchodzących danych. Na szczęście ustawiając odpowiednio duży bufor oraz, gdy mikrokontroler nie będzie blokowany na jakimś zadaniu, do takiej sytuacji przytkania nie dojdzie. Bufor dobrze jest ustawić na wartość 2x najdłuższy komunikat. Jak wiemy ze standardu NMEA, maksymalnie będzie to 81 znaków, więc minimalny bufor to 162 bajty. Ja ustawiłem 256 bo to taka ładna, okrągła liczba.
Ok. Co się dzieje, gdy jednak jest miejsce? Sprawdzane są 3 warunki:
- Znak 0x0D, czyli <CR> lub ‘\n’. Pamiętasz co jest na końcu zdania NMEA? Między innymi ten znak. Gdy zostanie on odebrany, inkrementuję zmienną mówiącą o tym, ile komunikatów czeka na analizę oraz oczywiście wpisuję ten znak do bufora kołowego.
- Znak 0x0A 0raz 0x00. 0x0A to <LF> lub ‘\r’. Wystarczy nam jeden znak kończący, więc porzucam te drugi. Podobnie z zerem. Nie niesie ono żadnej informacji w komunikacji ASCII. Z drugiej strony też nie powinno się zdarzyć, że dostaniemy zero z modułu, więc można by go pominąć w rozważaniach…
- Pozostałe znaki są po prostu dodawane do bufora.
Na samym końcu włączam ponownie nasłuch UART RX na jeden znak.
Jest taka funkcja jak
void NEO6_Task(NEO6_State *GpsStateHandler);
którą musisz umieścić w pętli głównej programu. Sprawdza ona, czy nadszedł już jakiś pełny komunikat. Jeżeli tak, pobiera całość z bufora kołowego do bufora roboczego oraz analizuje(parsuje) wiadomość. Jak można taką wiadomość przeparsować?
Parsowanie NMEA
W tym celu bardzo przydatne są dwie(trzy) funkcje:
- strcmp – porównywanie cstringów
- strtok – cięcie stringu na mniejsze kawałki po “tokenie”
- strtoke – modyfikacja strtok o której za chwilę
Zauważ jedną, charakterystyczną rzecz w NMEA. Dane zawsze są przedzielone przecinkiem oraz każda informacja ma swoje określone miejsce. Przykład tego, co printuje moduł po złapaniu fixa:
Jeszcze jest taki mały kawałek po gwiazdce, który jest sumą kontrolną. Ja ją pominę przy parsowaniu. Ufam, że dostaję dane poprawne.
Warto byłoby wykorzystać te przecinki. W tym celu idealnie sprawdzi się funkcja strtok.
char *strtok(char *s, const char *delim);
Jak jej używać? Przyjmuje w argumentach wskaźnik do stringa, który ma być pocięty oraz token po którym będzie dzielił. Naszym tokenem jest przecinek.
Co robi z tym przecinkiem? Wstawia w jego miejsce 0x00, czyli znak końca cstringa i zwraca wskaźnik do tego zmodyfikowanego stringa. Teraz można łatwo na nim operować.
No dobra, ale dalsza część bufora nam wyparowała, czy co? Co z tymi znakami po przecinku kurka?!
Otóż kolejne wywołanie strtok zwróci nam wskaźnik do kolejnego stringa, tego po ostatnio znalezionym tokenie, czyli znaki między drugim pierwszym a drugim przecinkiem. Ważne jest, aby teraz w argumencie nie podawać wskaźnika do kolejnego stringa. Aby dalej ciąć ten sam bufor, należy teraz w pierwszym argumencie podać NULL.
Przykład prosto z kodu. Aby wyciągnąć nagłówek komunikatu NMEA używam strtok. Powiedzmy, że komunikat wygląda tak:
$GPRMC,081836,A,3751.65,S,14507.36,E,000.0,360.0,130998,011.3,E*62
char* ParsePoiner = strtok((char*)GpsStateHandler->WorkingBuffer, ",");
Podałem wskaźnik na bufor roboczy w którym znajduje się pełen komunikac NMEA. Jako token oczywiście ‘,’. Pod ParsePointer znajduje się w tej chwili string “$GPRMC”.
Kolejne wywołanie
ParsePoiner = strtok(NULL, ",");
Spowoduje, że pod ParsePointer będzie teraz string “081836” oznaczający godzinę. Możemy go teraz zamienić na liczbę oraz przypisać do odpowiednich zmiennych.
Wywołajmy jeszcze raz strtok.
ParsePoiner = strtok(NULL, ",");
Teraz w ParsePointer jest “A”. Robimy tak dopóki nie uzyskamy wszystkich danych. Po drodze działając na tych pociętych stringach funkcjami do zamiany ascii na float przykładowo. I to jest cała magia parsowania w ten sposób. Mam nadzieje, że rozumiesz.
Pułapka strotk
Nie ma róży bez kolców. Takim wielkim kolcem jest właśnie ta super funkcja strtok. Dlaczego? Zauważ, że niektóre dane są puste tj. są dwa przecinki pod rząd. Zwłaszcza jeśli nie ma złapanego fix’a. Dlaczego jest to niebezpieczne?
Otóż funkcja strtok nie zwraca pustych stringów. Zamiast tego zwraca kolejny string z jakąś zawartością. W efekcie, jeżeli którejś danej zabraknie w ciągu tokenów, mamy popsute parsowanie… Można napisać jakieś inteligentniejsze parsowanie jednak… można tez napisać lepszą funkcję do tokenów. Taką, która zwróci pusty string.
Znalazłem implementację takiej funkcji na ukochanej przez programistów z problemami stronie.
Funkcja nazywa się strtoke i działa dokładnie tak samo jak standardowy strok z tym, że potrafi zwrócić pusty string. Można śmiało zamienić wszystkie funkcje na tą poprawioną. Teraz puste dane nie zostaną pominięte. Sukces!
Efekt działania
Przykładowy kod, który napisałem printuje na UART2 dane, które otrzymał od GPS na UART1. Jednak nie printuje zawsze lecz tylko wtedy, gdy moduł złapie fix’a.
Zerknijmy jeszcze w analizator logiczny, aby zobaczyć jak sobie radzą przerwania oraz parsowanie danych.
Otrzymanie wszystkich komunikatów z NEO-6 wygląda tak.
Cała ramka, którą wysyła moduł ma ponad 100 ms. Ta tutaj nie ma złapanego fixa. Na biurku mi nie łapie… Dorzucając danych po złapaniu sygnału GPS, ramka ta będzie nieco dłuższa.
Jak możesz zauważyć w drugim wierszu znajduje się mnóstwo wywołań przerwania od odbioru znaku. Wywołują się często co wyglądają poważnie. Jak mocno wybijają one mikrokontroler z normalnej pracy?
Przerwanie trwa około 2,8 µs. Biorąc pod uwagę to, że czas ten obejmuje też obsługę testowego pinu GPIO, to jest to bardzo mało. Takie przerwanie wywoływane jest co równe 1,04 ms w momencie, kiedy nadchodzi kolejny znak.
Na jeszcze niższym piętrze widzisz kilka pików. Zaznaczyłem w tym miejscu momenty parsowania danych.
Parsowanie, czyli cięcie, przeszukiwanie stringów oraz konwersja danych trwa około 100 µs.
Podsumowanie
Obsługa GPS jest bardzo łatwa. Wystarczy kilka sztuczek, aby w pełni cieszyć się komunikatami nadawanymi przez moduł NEO-6. Odbiór oraz obróbka danych jest bardzo szybka. Pamiętaj też, że czasy które przedstawiłem dotyczą częstotliwości taktowania HCLK równej 84 MHz.
Gdzie można wykorzystać taki GPS? Ja ostatnio widzę dla niego zastosowanie w urządzeniu pomiarowym z komunikacją LoRa. Niby wiemy, gdzie się znajduje urządzenie, ale produkując kilka takich, nie musimy się martwić o ręczne wprowadzanie lokalizacji. Fajne rozwiązanie podrzucił mi ostatnio jeden z czytelników. Był to dron pomiarowy. Wykonuje on pomiary jakości powietrza i automatycznie zapisuje je na karcie SD razem lokalizacją pomiaru. Ekstra zastosowanie.
Dodatkowym atutem użycia GPS jest dokładny czas. Można z jego pomocą synchronizować RTC lub pokusić się nawet o całkowite usunięcie zegara z urządzenia.
Odbiorniki GPS aktualnie są wszędzie. Chociażby w smartfonach, czy samochodach. Nawet w dronach znajdziemy GPS, który na przykład wspomaga pilotowanie maszyny.
Pełny projekt wraz z biblioteką znajdziesz jak zwykle na moim GitHubie: link
Jeśli zauważyłeś jakiś błąd, nie zgadzasz się z czymś, chciałbyś coś dodać istotnego lub po prostu uważasz, że chciałbyś podyskutować na ten temat, napisz komentarz. Pamiętaj, że dyskusja ma być kulturalna i zgodna z zasadami języka polskiego.
6 komentarzy
Andrzej K · 10/05/2023 o 08:46
dla nowszych wersji STM32CUBEIDE (testowane na 1.12.1) trzeba z pliku Inc/gps_neo6.h przenieść deklarację: NEO6_State GpsState; do pliku main.c Wtedy projekt kompiluje się bez błędów (Discord – Akademia Embedded – 09.05.2023 do 10.05.2023)
OSMAN K · 14/01/2022 o 09:52
I perform these operations with stm323f407vg. I connect the GPS’s RX to PA9, TX to PA 10. I also connect PA3 and PA2 to usbttl. I constantly get ‘no fix’ on the serial port screen. I cannot transfer the data I receive from huart1 to huart2. The code doesn’t go into ‘if(NEO6_IsFix(&GpsState))’. GPS is working normally. I’m having a problem why? Can you help me? Thanks.
Mateusz Salamon · 14/01/2022 o 10:16
You gave me too little info. I don’t know your code.
What do you want to do? Use my lib + mirror UART1 to UART2? What UART mode are you using?
Andrzej K · 28/04/2021 o 10:51
Analizuję funkcję void NEO6_ReceiveUartChar (Neo6_State *GpsStateHandler) i wydaje mi się że przy nie odbieraniu danych z bufora funkcja porzuci tylko 1 bajt nadchodzących danych (przy pełnym buforze) a następne nadchodzące dane nadpiszą istniejące dane w buforze. Więc go zniszczą. Nie jest to krytyczne ale jednak. Czy dobrze analizuję? Aby funkcja porzucała nadmiarowe dane nie powinniśmy mieć przypisania (w pierwszym if-ie) GpsStateHandler->UartBufferHead=GpsStateHandler->UartBufferTail
Proszę o interpretację. Dobrze myślę???
Mateusz Salamon · 17/05/2021 o 13:20
Racja! Jakiś błąd mi się wkradł 🙂 Zapisałem go sobie i poprawię później. Dzięki!
Kamil · 30/07/2024 o 22:44
zastanawiam się co miałeś na myśli pisząc “analizuję funkcję void NEO6_ReceiveUartChar, skoro w artykule nawet nie ma jej ciała, tylko jest samo wywołanie. Gdzie jest kod który ty widzisz, a ja nie widzę?