Come utilizzare un ricevitore gps con Arduino

Il ricevitore GPS (Global Positioning System) è oggi un dispositivo alla portata di tutti, dal costo contenuto e dal semplice utilizzo. Questo articolo vuole essere una guida introduttiva all’interpretazione dei dati di posizione forniti dal ricevitore gps, senza utilizzare librerie esterne. Ho utilizzato una scheda Arduino/Genuino UNO e il modulo Adafruit Ultimate GPS Breakout v3 (vedi l’articolo su Amazon).

arduino Gps

Il modulo ha una sensibilità di ben -165dBm (in tracking), questo significa che riesce a rilevare segnali di 1.25nVolt, ha un refresh di 10 Hz e permette di ‘inseguire’ 22 satelliti.

Questo dispositivo rende semplice la gestione del tracking dei satelliti ed del calcolo della posizione. I dati vengono restituiti sul canale seriale TTL, alla frequenza di 1Hz. E’ sufficiente collegarsi alla seriale per poter leggere i dati. I dati principali sono quelli di posizione (espressa in Latitudine e Longitudine) la quota e l’orario zulu ma sono presenti anche altre informazioni relative ai satelliti utilizzati per il fix.

Lo schema elettrico seguente mostra i collegamenti da effettuare tra una UNO e la Adafruit Ultimate GPS Breakout v3:

Schema elettrico arduino uno gps

Questo schema serve per inviare direttamente sul Serial Monitor i dati restituiti dal modulo gps. Il segnale trasmesso dal dispositivo viene convogliato sulla linea di trasmissione seriale della UNO affinché possa raggiungere il pc attraverso la porta usb. Carichiamo sulla UNO uno sketch vuoto:

void setup() {
  // put your setup code here, to run once:

}

void loop() {
  // put your main code here, to run repeatedly:

}

Terminato l’upload del programma lanciamo il Serial Monitor e settiamo il bps a 9600. La schermata seguente illustra le stringhe generate dal ricevitore GPS.

gps data restitution

Nel mio caso il sensore gps non aveva ancora agganciato i satelliti, i dati ricevuti indicano una latitudine e una longitudine pari a zero. Il led posto sul pcb indica quando avviene il fix dei satelliti, se il led lampeggia ogni secondo il gps sta cercando di agganciare i segnali dei satelliti, quando vengono agganciati il led lampeggia ogni 15 secondi. Il fix dei satelliti è funzione della posizione dell’antenna del sensore, in campo aperto, senza nessun ostacolo, avviene entro il minuto mentre se ci troviamo in posizioni a bassa copertura (ad esempio dentro casa) il sensore impiegherà molto più tempo e nei casi peggiori non riuscirà ad elaborare il segnale.

Per interpretare i dati è necessario conoscere quali informazioni vengono prodotte dal ricevitore gps. Sul Serial Monitor notiamo quattro righe con informazioni differenti che si ripetono ogni secondo. Queste stringhe iniziano tutte con il simbolo del dollaro e con un codice di cinque caratteri. Il codice specifica quali dati sono  presenti nella relativa stringa, in ogni stringa i dati vengono separati dalle virgole (campi), ulteriori informazioni possono essere apprese in seguendo questo link.

Le quattro stringhe sono riassunte nel box seguente:

$GPGGA,235951.800,,,,,0,00,,,M,,M,,*79
$GPGSA,A,1,,,,,,,,,,,,,,,*1E
$GPRMC,235951.800,V,,,,,0.00,0.00,050180,,,N*40
$GPVTG,0.00,T,,M,0.00,N,0.00,K,N*32

La stringa che inizia con $GPGGA (Global Positioning System Fix Data), contiene le informazioni della latitudine, della longitudine, della quota e del tempo.
La stringa che inizia con $GPGSA, contiene dati relativi al DOP (Diluizione della precisione) e al prn (pseudo random noise) dei satelliti utilizzati per il fix.
La stringa che inizia con $GPRMC indica il valore minimo raccomandato dei dati di tempo, velocità e posizione.
La stringa che inizia con $GPVTG indica il Track made good e il valore di Groud Speed.

La stringa che ci interessa maggiormente è quella che inizia con $GPGGA perché contiene i dati di posizione ed il tempo. Il codice che andremo a scrivere recupererà i dati separandoli dalle virgole.

Modifichiamo il circuito precedente perché ci serve sia la seriale hardware sia quella software. La seriale software la useremmo per ricevere i dati dal sensore gps mentre la seriale hardware la useremmo per inviare i dati al pc:

Adafruit ultimate gps breakout

Verifichiamo con questo codice che i dati ricevuti dalla seriale software (attivata sul pi 2 e 3) vengano ritrasmessi al pc impiegando la seriale hardware:

#include <SoftwareSerial.h>

//pin 2 RX e pin 3 TX
SoftwareSerial mySerial(2, 3);

void setup() {
  //init seriale hardware
  Serial.begin(9600);
  //init seriale software
  mySerial.begin(9600);
}

void loop() {
  //controllo se nella seriale software ci sono dati
  if (mySerial.available()) {
    //se sono presenti dati leggili ed inviali
    //alla seriale hardware
    Serial.write(mySerial.read());
  }
}

Con questo codice otterremmo sul Serial Monitor gli stessi dati ottenuti col primo esempio.
Naturalmente se è avvenuto il fix del segnale, i campi delle stringhe viste in precedenza conterranno i dati di posizione e orario, inoltre ne otterremmo anche una quinta  che inizierà per $GPGSV (indica nel dettaglio i parametri dei satelliti agganciati), quindi il risultato sarà qualcosa del genere:

$GPGSV,3,1,11,03,03,111,00,04,15,270,00,06,01,010,00,13,06,292,00*74
$GPGSV,3,2,11,14,25,170,00,16,57,208,39,18,67,296,40,19,40,246,00*74
$GPGSV,3,3,11,22,42,067,42,24,14,311,43,27,05,244,00,,,,*4D
$GPGGA,235317.000,4003.9039,N,10512.5793,W,1,08,1.6,1577.9,M,-20.7,M,,0000*5F
$GPGSA,A,3,19,28,14,18,27,22,31,39,,,,,1.7,1.0,1.3*35 
$GPRMC,225446,A,4916.45,N,12311.12,W,000.5,054.7,191194,020.3,E*68 
$GPVTG,360.0,T,348.7,M,000.0,N,000.0,K*43

Ora cercherò di isolare la stringa che inizia con $GPGGA. Per prima cosa ho modificato il codice per concatenare i caratteri ricevuti sulla seriale software in un oggetto stringa in questo modo potrò elaborarla usando i metodi della classe String:

#include <SoftwareSerial.h>

//pin 2 RX e pin 3 TX
SoftwareSerial mySerial(2, 3);
String NMEA;

void setup() {
  //init seriale hardware
  Serial.begin(9600);
  //init seriale software
  mySerial.begin(9600);
}

void loop() {
  if (mySerial.available()) {
    //leggo la prima riga
    char c = mySerial.read();
    if (c != '\n') {
      NMEA += c;
    }
    else {
      Serial.println(NMEA);
      //resetto la stringa
      //altrimenti accoderei tutti i caratteri ricevuti!
      NMEA = "";
    }
  }
}

Ottenute solo le stringhe ho la possibilità di capire come inizia la stringa, ad esempio il metodo subString restituisce una sotto stringa in funzione degli argomenti passati nei parametri. Il codice seguente invia al Serial Monitor solo le stringhe che iniziano con $GPGGA:

#include <SoftwareSerial.h>

//pin 2 RX e pin 3 TX
SoftwareSerial mySerial(2, 3);
String NMEA;

void setup() {
  //init seriale hardware
  Serial.begin(9600);
  //init seriale software
  mySerial.begin(9600);
}

void loop() {
  if (mySerial.available()) {
    //leggo la prima riga
    char c = mySerial.read();
    if (c != '\n') {
      NMEA += c;
    }
    else {
      //printo solo la stringa che mi interessa
      //quella che inizia con GPGGA
      //substring estrapola dalla stringa usando come parametri
      //la posizione iniziale e finale in questo caso 
      //parte dalla posizione 0 alla posizone 6
      if(NMEA.substring(0,6) == "$GPGGA")      
        Serial.println(NMEA);
      //resetto la stringa  
      NMEA = "";
    }
  }
}

Ho così isolato la stringa che mi interessa maggiormente. Continuo la scrittura del codice con alcune istruzioni che mi permettono di estrapolare i dati separati dalle virgole, quindi utilizzerò il metodo indexOf per passare i parametri al metodo subString:

#include <SoftwareSerial.h>

//pin 2 RX e pin 3 TX
SoftwareSerial mySerial(2, 3);
String NMEA;
int posizione_delimitatore = 0;
int index = 0;

void setup() {
  //init seriale hardware
  Serial.begin(9600);
  //init seriale software
  mySerial.begin(9600);
}

void loop() {
  if (mySerial.available()) {
    //leggo la prima riga
    char c = mySerial.read();
    if (c != '\n') {
      NMEA += c;
    }
    else {
      posizione_delimitatore = 0;
      //indice di partenza zero
      index = 0;
      //printo solo la stringa che mi interessa
      if (NMEA.substring(0, 6) == "$GPGGA")
      {
        //Serial.println(NMEA);
        //fino a che il metodo indexOf non restituisce -1
        //elabora la stringa
        while (posizione_delimitatore != -1)
        {
          //ottieni la posizione della prima virgola 
          posizione_delimitatore = NMEA.indexOf(",", index);
          //invia al Serial Monitor la sotto stringa usando come indice
          //il dato ottenuto da indexOf
          Serial.println(NMEA.substring(index, posizione_delimitatore));

          //memorizza il nuovo indice di partenza
          index = posizione_delimitatore;
          //incrementa l'indice di 1 per escludere la
          //virgola 
          index++;
        }
      }
      //resetta la stringa
      NMEA = "";
    }
  }
}

Il risultato sarà qualcosa del genere:

$GPGGA,152454.000,4003.1968,N,10512.5793,E,1,05,1.56,890.9,M,48.0,M,,*6A
$GPGGA
152454.000
4003.1968
N
10512.5793
E
1
05
1.56
890.9
M
48.0
M

*6A

Il codice suddivide correttamente tutti i campi. Molti di questi dati possono essere ulteriormente filtrati, ad esempio a me interessa ottenere il dato di tempo, la latitudine, la longitudine e la quota. Il codice seguente invia al Serial Monitor solo questi campi:

#include <SoftwareSerial.h>

//pin 2 RX e pin 3 TX
SoftwareSerial mySerial(2, 3);
String NMEA;
int posizione_delimitatore = 0;
int index = 0;
int campo = 0;

void setup() {
  //init seriale hardware
  Serial.begin(9600);
  //init seriale software
  mySerial.begin(9600);
}

void loop() {
  if (mySerial.available()) {
    //leggo la prima riga
    char c = mySerial.read();
    if (c != '\n') {
      NMEA += c;
    }
    else {
      posizione_delimitatore = 0;
      index = 0;
      //questa variabile viene usata per contare tutti i campi trovati
      campo = 0;
      //printo solo la stringa che mi interessa
      if (NMEA.substring(0, 6) == "$GPGGA")
      {
        //Serial.println(NMEA);

        while (posizione_delimitatore != -1)
        {
          posizione_delimitatore = NMEA.indexOf(",", index);

          //in funzione del campo invio al serial monitor
          //solo i dati che mi interessano
          switch (campo)
          {
            case 2:
            case 4:
            case 9:
              Serial.println(NMEA.substring(index, posizione_delimitatore));
              break;
          }

          index = posizione_delimitatore;
          index++;
          //incrementa il campo trovato
          campo++;
        }
      }
      NMEA = "";
    }
  }
}

Abbiamo cosi ottenuto il recupero dei dati di posizione e tempo senza utilizzare una libreria esterna. Le librerie rendono tutto più semplice e veloce ma credo che sia altrettanto importante capire cosa avviene dietro le quinte.

I dati ottenuti sono pronti per l’utilizzo in tutti quei progetti cui è necessario recuperare la posizione geografica e il tempo corrente.