Raspberry-Pi-Projekte: GPS-Empfänger

Prof. Jürgen Plate

Raspberry Pi: GPS-Empfänger

Allgemeines

GPS basiert auf Satelliten, die kontinuierlich Informationen ausstrahlen, welche die aktuelle Position des Satelliten (Bahndaten) und die genaue Uhrzeit enthalten. Zur Positionsbestimmung muss ein GPS-Empfänger die Signale von mindestens vier Satelliten gleichzeitig empfangen. Eigentlich reichen dazu auch die Signale von drei Satelliten aus, aber die GPS-Empfänger haben keine Uhr, die genau genug ist, um die Laufzeiten der Signale korrekt messen zu können. Deshalb wird das Signal eines vierten Satelliten benötigt, mit dem dann auch die genaue Zeit im Empfänger bestimmt werden kann. Im GPS-Empfangsgerät werden die vier Signallaufzeiten zwischen Satelliten und Empfangsantenne errechnet. Daraus werden dann die aktuelle Position (inklusive der Höhe) und die genaue Empfängeruhrzeit ermittelt. Mit den GPS-Signalen lässt sich aber nicht nur die Position, sondern auch die Geschwindigkeit und die Bewegungsrichtung des Empfängers ermitteln. Damit ein GPS-Empfänger immer zu mindestens vier Satelliten Kontakt haben kann, werden beim GPS-System insgesamt mindestens 24 Satelliten eingesetzt, welche die Erde zweimal pro Tag in einer Bahnhöhe von ca. 20.200 km umkreisen.

Die eigene Position kann der Empfänger erst dann feststellen, wenn er die Satellitenpositionen kennt und seine interne Uhr mit den Satelliten synchronisiert hat. Im Normalfall hat das GPS-Gerät die Bahndaten der umlaufenden Satelliten gespeichert. Es handelt sich um die sogenannten Almanachdaten, die bis zu 180 Tage im Voraus bereit stehen. Der Almanach wird im Funksignal der Satelliten übertragen. Er ist 37500 Bit lang und die Datenrate beträgt etwa 50 Bit/s. Daher dauert es mehr als 12 Minuten, bis der komplette Almanach übertragen ist. Zwar kann der GPS-Empfänger schon vor Ende der Übertragung nach den Signalen bereits bekannter Satelliten suchen, aber es kann in seltenen Fällen nach dem Einschalten eine Viertelstunde dauern, bis die Position ermittelt ist. Moderme Empfänger speichern den Almanach und aktualisieren ihn ständig. Da der Empfänger den zuletzt ermittelten Standort und über die eingebaute Uhr die Zeit weiss, kann er mit dem Almanach feststellen, welche Satelliten sich momentan über dem Horizont befinden und sich überhaupt empfangen lassen.

Die Empfangssignale sind außerordentlich schwach, weil die Sender der Satelliten eine geringe Ausgangsleistung haben und etwa 20.000 km entfernt sind. Der Empfänger soll daher eine möglichst freie Himmelssicht haben. Das lässt sich auf offener See gut verwirklichen, aber für die restlichen Empfangssituationen bestehen oft Einschränkungen, etwa Abdeckungen durch Berge (im Tal) oder hohe Gebäude in einer Stadt, GPS in geschlossenen Räumen (auch am Fenster sind immer noch etwa die Hälfte der Satelliten abgedeckt) oder eine Antenne im Auto (meist ist eine Außenantenne notwendig). Oft reichen die verbleibenden Satelliten dennoch für einen zur Positionsermittlung. Dafür sind immer mindestens drei Satelliten notwendig, wer auch die Höhe sehen will, benötig mindestens vier.

Mittels des GPS-Signals läßt sich auch Geschwindigkeit ermitteln. Anfangs hat man einfach die Wegdifferenz für eine bestimmte Zeit ermittelt (nach der Formel V = Ds/Dt). Moderne GPS-Systeme sind um ein Vielfaches genauer. Hierbei hängt es aber auch von der Anzahl an Satelliten ab, die das Gerät aktuell empfängt. Normalerweise berechnet ein GPS-Ssystem durch die Messung der Frequenzverschiebung (Doppler-Verschiebung) die Geschwindigkeit. Wenn sich also z. B. das Auto bewegt, erhöht oder verringert sich die Signalfrequenz der Satelliten ganz leicht. Die Frequenzänderung Df der Signalfrequenz f und die Lichtgeschwindigkeit erlauben die Berechnung nach der Formel: v = Df * c / f / 2. Je mehr Satelliten gefunden werden, umso genauer ist die Positionsbestimmung. Durch diese Methode ist die Geschwindigkeitsmessung auf unter 0,2 km/h genau.

Anschluss

Einen GPS Empfänger kann man an einen Raspberry Pi auf zwei Arten anschliessen, an der seriellen Schnittstelle oder per USB. Die meisten GPS Empfänger sind USB-Geräte. Sie funktionieren sofort unter Linux da diese als (pseudo-)serielle Geräte konzipiert sind. Da die USB-Empfänger sich als serielles Gerät identifizieren, werden keine Treiber benötigt. Meine Wahl fiel auf einen USB-GPS-Empfänger von Navilock, und zwar den "NL-602U USB 2.0 GPS u-blox 6". Von Navilock gibt es eine ganze Palette von GPD-Systemen, von Modulen, wo man die Antennen- und Schnittstellenanbindung selbst entwerfen und einePlatine bauen muss, bis hin zu kompletten Empfängern für drinnen oder draussen - sogar mit Bluetooth-Anbindung ohne Kabel. Der "NL-602U" arbeitet ideal mit dem Raspberry Pi zusammen, da er von Raspbian unterstützt wird. Der Empfänger hat einen SuperSense-Chipsatz und eine hochempfindliche Antenne eingebaut. Die Technische Daten:

Installation

Man steckt den "NL-602U" an einer USB-Buchse ein und bootet den RasPi (wenn der RasPi schon läuft, genügt natürlich das Einstecken). dann kann mans chon mal schauen, ob der Empfänger richtig eingebunden wurde:
pi@raspberrypi ~ $ lsusb
Bus 001 Device 002: ID 0424:9512 Standard Microsystems Corp.
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 001 Device 003: ID 0424:ec00 Standard Microsystems Corp.
Bus 001 Device 004: ID 1546:01a6 U-Blox AG
Die letzte Zeile mit dem Eintrag "U-Blox AG" weist auf den GPS-Empfänger hin. Das Betriebssystem ordnet dem GPS-Empfänger automatisch eine virtuelle Gerätedatei zu. Im gezeigten Fall ist es der Port /dev/ttyACM0. Sind eventuell andere USB-Geräte mit (pseudo-)serieller Schniuttstelle angeschlossen, kann es auch /dev/ttyACM1 etc. sein. Mit dem folgenden Befehl ist es möglich, die System-Messages auszulesen, um den virtuellen Port des "NL-602U" herauszufinden.
pi@raspberrypi ~ $ dmesg | less
  ...
[    1.892998] usb 1-1.3: new full-speed USB device number 4 using dwc_otg
[    1.995632] usb 1-1.3: New USB device found, idVendor=1546, idProduct=01a6
[    1.995739] usb 1-1.3: New USB device strings: Mfr=1, Product=2, SerialNumber=0
[    1.995816] usb 1-1.3: Product: u-blox 6  -  GPS Receiver
[    1.995867] usb 1-1.3: Manufacturer: u-blox AG - www.u-blox.com
[    2.705229] udevd[159]: starting version 175
[    4.227988] gpiomem-bcm2835 20200000.gpiomem: Initialised: Registers at 0x20200000
[    4.236598] bcm2708_i2c 20804000.i2c: BSC1 Controller at 0x20804000 (irq 79) (baudrate 100000)
[    5.226448] cdc_acm 1-1.3:1.0: ttyACM0: USB ACM device
[    5.249829] usbcore: registered new interface driver cdc_acm
[    5.249926] cdc_acm: USB Abstract Control Model driver for USB modems and ISDN adapters
  ...
Man muss ggf. in dem Datenwust, den dmeg liefert, etwas suchen. Im Ausschnitt oben findet man in der dritten Zeile von unten die gewünschte Info: ... ttyACM0: USB ACM device . Anhand dieser Informationen kann man auch sicher sein, dass der GPS-Empfänger grundlegend funktioniert.

Für den ersten Zugriff auf den GPS-Empfänger genügen Bordmittel. Der Befehl cat -v /dev/ttyACM0 liest die Informationen des"NL-602U" und gibt sie aus.

pi@raspberrypi ~ $ cat -v /dev/ttyACM0
$GPTXT,01,01,02,u-blox ag - www.u-blox.com*50^M
$GPTXT,01,01,02,HW  UBX-G60xx  00040007 *52^M
$GPTXT,01,01,02,EXT CORE 7.03 (45970) Mar 17 2011 16:26:24*44^M
$GPTXT,01,01,02,ROM BASE 7.03 (45969) Mar 17 2011 16:18:34*57^M
$GPTXT,01,01,02,ANTSUPERV=AC SD PDoS SR*20^M
$GPTXT,01,01,02,ANTSTATUS=OK*3B^M
$GPRMC,142502.000,A,4814.1495,N,01140.6188,E,0.96,257.30,120316,,,A*66^M
$GPVTG,257.30,T,,M,0.96,N,1.8,K,A*08^M
$GPGGA,142502.000,4814.1495,N,01140.6188,E,1,06,2.5,476.7,M,46.2,M,,0000*5C^M
$GPGSA,A,3,20,13,24,18,15,12,,,,,,,3.7,2.5,2.7*3C^M
$GPGSV,4,1,13,01,01,037,,10,15,319,,11,01,024,,12,25,224,10*72^M
$GPGSV,4,2,13,13,46,153,11,15,68,231,16,17,40,091,,18,23,287,19*70^M
$GPGSV,4,3,13,19,32,130,,20,12,224,26,24,51,288,18,28,31,051,*73^M
$GPGSV,4,4,13,30,00,092,*43^M
$GPGLL,4814.1495,N,01140.6188,E,142502.000,A,A*5A^M
$GPRMC,142503.000,A,4814.1496,N,01140.6192,E,0.95,257.30,120316,,,A*6C^M
$GPVTG,257.30,T,,M,0.95,N,1.8,K,A*0B^M
$GPGGA,142503.000,4814.1496,N,01140.6192,E,1,05,2.9,476.2,M,46.2,M,,0000*5F^M
$GPGSA,A,3,20,13,24,18,15,,,,,,,,4.1,2.9,2.9*3C^M
Wie man am "^M" am Zeilenende sieht, beendet der Empfänger jede Zeile mit "Carriage Return" (CR) und "Line Feed" (LF). Unter Linux brauchen wir das CR nicht, es genügt das LF.

Fertige Software für GPS

Sofern nichts anderes dabei steht, lassen sich alle Tools per apt-get install auf dem Rechner installieren. Bitte daran denken, dass die Installation Root-Rechte erfordert (sudo ...).

gpsd ist ein Daemon-Programm, das die Daten von einem oder mehreren GPS-Empfängern ausliest, die per USB, Bluetooth oder serieller Schnittstelle an den Computer angeschlossen sind. Die erfassten Daten (geografische Position, Richtung, Geschwindigkeit) können von Client-Applikationen genutzt werden. Zusätzlich stellen die Entwickler zusätzliche Hilfswerkzeuge zur Diagnose bereit. gpsd kann auch mehrere GPS-Anwendungen gleichzeitig bedienen oder über das Netzwerk zur Verfügung stellen. Er kann mittels sudo apt-get install gpsd gpsd-clients auf die übliche Weise installiert werden.

Nach der erfolgreichen Installation kann man den gpsd-Daemon von Hand starten:

sudo gpsd -b /dev/ttyUSB0 -F /var/run/gpsd.sock -G
Mit diesem Befehl ist der gpsd-Server für alle Programme erreichbar, auch per Telent von einem Windows-System aus. Der TCP-Port ist 2947. Will man auf dem Klicki-Bunti-Niveau bleiben, bieten sich etliche Programme an:

Programmierung

Man braucht aber all diese Programme nicht unbedingt, wenn man selber programmieren kann (oder will) bzw. wenn es sich um spezielle Auswertungen handelt. Wenn Sie mit Python loslegen wollen, brauchen Sie zumindest die Serial-Library pySerial. Es gibt auch eine GPS-Library namens python-gps. Beide Bibliotheken können mittels apt-get install ... installiert werden.

Bei der C-Programmierung muss gar nichts installiert werden, da ist alles schon dabei. Ein erstes Programm soll nur mal das oben beschriebene Test-Kommando cat ersetzen und den Output der GPS-Maus auf dem Bildschirm anzeigen. Wichtig sind hier nur die Parameter, mit der die serielle Schnittstelle initialisiert wird (Funktion UART_Init). Ist die Schnittsteller erstmal geöffnet, kann mit read() und write() darauf zugegriffen werden. Es wird jeweils zeichenweise bis zum NL gelesen und dann die Zeile ausgegeben. CR wird dabei ausgeblendet.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <termios.h>
#include <inttypes.h>

#define BAUDRATE B115200
#define UART "/dev/ttyACM0"

/* 
 * Oeffen der seriellen Schnittstelle fuer den GPS-USB-Empfaenger
 */
int UART_Init()
  {
  int FD;
  struct termios options = {};

  FD = open(UART, O_RDONLY | O_NOCTTY);
  if (FD < 0)
    {
    printf("Fehler beim Oeffnen von %s\n", UART);
    exit(-1);
    }
  memset(&options, 0, sizeof(options));
  options.c_cflag &= ~PARENB;         /* kein Paritybit */
  options.c_cflag &= ~CSTOPB;         /* 1 Stoppbit */
  options.c_cflag &= ~CSIZE;          /* 8 Datenbits */
  options.c_cflag = CS8 | CLOCAL | CREAD;
  options.c_iflag = IGNPAR;
  options.c_oflag = 0;
  options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
  options.c_cc[VTIME] = 10;
  options.c_cc[VMIN] = 1;
  cfsetspeed(&options, BAUDRATE);
  tcflush(FD, TCIFLUSH);
  tcsetattr(FD, TCSAFLUSH, &options);
  return FD;
  }

int main(int argc, char** argv)
  {
  int FD;                  /* Filehandle fuer die Schnittstelle */
  unsigned char Zeichen;   /* eingelesenes Zeichen */
  int Laenge, Anz;         /* Hilfsvariablen fuer das Lesen */
  char Puffer[4000] = "";  /* Eingabepuffer */

  FD = UART_Init();
  Laenge = 0;

  /* zeilenweise einlesen */
  while (1)
    {
    Anz = read(FD, &Zeichen, 1);
    if (Anz > 0)
      {
      if(Zeichen == '\n')
        {
        Puffer[Laenge] = '\0';
        puts(Puffer);
        Laenge = 0;
        }
      else if(Zeichen != '\r')
        {
        Puffer[Laenge] = Zeichen;
        Laenge++;
        }
      }
    }

  close (FD);
  return 0;
  }
Im Folgenden werden zwei der GPS-Records untersucht (eine umfassende Übersicht liefert das NMEA-Referenzhandbuch - siehe Links am Ende der Seite). Es handelt sich um die Datensätze "GPGGA" und "GPRMC", die nicht nur die Positionsdaten, sondern auch Datum und Uhrzeit.

GPGGA liefert die "Global Positioning System Fixed Data". Ein Beispiel aus der oben dargestellten Liste ist die folgende Zeile.

$GPGGA,142502.000,4814.1495,N,01140.6188,E,1,06,2.5,476.7,M,46.2,M,,0000*5C^M
Die Felder sind durch Kommata getrennt und haben die folgende Bedeutung (Quelle: NMEA-Handbuch). Beim Feld "Position Fix Indicator" bedeutet 0 = "Fix not available or invalid", die Werte 1 und 2 bedeuten "fix valid":

Der zweite Datensatz, GPRMC, liefert die "Recommended Minimum Specific GNSS Data". Ein Beispiel aus der oben dargestellten Liste ist die folgende Zeile.

$GPRMC,142502.000,A,4814.1495,N,01140.6188,E,0.96,257.30,120316,,,A*66^M
Die Felder sind durch Kommata getrennt und haben die folgende Bedeutung (Quelle: NMEA-Handbuch).

Aus diesem Datensatz werden im folgenden Programm das Datum und die Uhrzeit entnommen. Beide Datensätze geben Breitengrad (Latitude) und Längengrad (Longitude) an. Beim GPRMC können nicht nur die Höhe, sondern auch andere Werte entnommen werden. Bei Breitengrad besteht der Datensatz aus zwei Ziffern Grad, zwei Ziffern Minute und nach dem Dezimalpunkt die Minutenbruchteile vierstellig. Danach folgt die Angabe, ob Süd (S) oder Nord (N). Beim Längengrad ist die Gradangabe dreistellig, ansonsten entspricht er den Breitengrad, wobei hier in Ost (E) oder West (W) unterschieden wird.

Etwas Theorie

Die Erde ist ein (annähernd) kugelförmiger Planet mit einem Durchmesser von ca. 12.740 km und einem Umfang von ca. 40.000 km. Um seinen Standort zu bestimmen, legt man ein Gitternetz aus Längen- und Breitenkreisen über die Erdkugel. Damit kann die Position mit zwei Zahlenwerten eindeutig angegeben werden (Bildquelle: www.explorermagazin.de/gps/gpsbasic.htm).

Der Äquator teilt die Erdkugel in die nördliche und die südliche Halbkugel. Die anderen Breitenkreise verlaufen parallel dazu, jeweils 90 in nördlicher und 90 in südlicher Richtung vom Äquator aus gesehen. Die Längenkreise liegen in Ost-West-Richtung nebeneinander, wobei sie jeweils den Nord- und den Südpol verbinden. Da hier die gesamte Kugel in Nord-Südrichtung zu unterteilen ist, gibt es insgesamt 360 Längen-Halbkreise. Der Greenwich-Meridian nähe London ist der so genannte Nullmeridian. Von ihm aus wird in östlicher und westlicher Richtung gezählt. Mit diesen Breiten- und Längenkreisen wird, wie gesagt, die Position auf der Erdoberfläche bestimmt.

Die Breite (Latitude) wird in Bogengraden (°), Bogenminuten (´) und Bogensekunden (´´) angegeben, wobei ein Grad aus 60 Bogenminuten besteht und eine Bogenminute aus 60 Bogensekunden. Beim GPS erfolgen diese werte entweder in Grad, Minuten, Sekunden (Möglichkeit 1, z. B. "52° 22' 45'' N"), in Grad und Dezimalminuten (Möglichkeit 2, die häufigste, z. B: "22° 13,234' S") oder in Dezimalgrad (Möglichkeit 3, z. B. "12,34321 N"). Die Breitenangaben enthalten - wie die Beispiel schon zeigen, als Zusatz "N" (nördlich des Äquators) oder "S" (südlich des Äquators). Der Nordpol hat damit eine geografische Breite von 90° N (Nord), der Südpol dem entsprechend 90° S (Süd).

Auch die Länge (Longitude) wird auf die gleiche Weise beschrieben wie die Breite. Wie erwähnt, läuft der Nullmeridian durch Greenwich. Sein Gegenstück, der 180°-Meridian stellt die "Internationale Datumsgrenze" dar. Beide teilen die Erde in eine westliche und eine östliche Halbkugel. Auch für die Notation der POsition gibt es die drei oben gezeigten Möglichkeiten. Nur sind hier die Zusätze entweder "E" (East = Ost) oder "W" (West).

Mein derzeitiger Standort hat beispielsweise die Koordinaten 48° 14.1495' N und 011° 40.6188' E, also eine Angabe in Dezimalgrad (im GPS-GPRMC-Datensatz stehen die Werte kompaktifiziert als 4814.1495,N,01140.6188,E).

Die Umrechnung in Grad, Minuten und Sekunden ist ganz einfach: Die Zahl vor dem Komma ist immer die Gradangabe. Für die Minuten müssen die Nachkommastellen des Breiten- oder Längengrads mit 60 multipliziert werden. Beispiel: 48° 14.1495' Die 48 Bogengrad sind klar, ebenso die 14 Bogenminuten (vor dem Dezimalpunkt). Bleibt der Nachkommaanteil 0.1495. Der wird nun mit 60 multipliziert:

0,1495 * 60 = 8,97 was knapp 9 Bogensekunden entspricht.

GPS-Daten auslesen

Das erweiterte Programm liest und untersucht zwei GPS-Records, den GPGGA und den GPRMC. Daraus werden die Daten extrahiert und in einer Structure location und zwei Variablen für Datum und Uhrzeit gespeichert. Die Structure erlaubt dann die Weiterverarbeitung - im Beispielprogramm werden die Werte einfach ausgegeben. Weil die NMEA-Funktionen etwas länglich sind, wurden sie in eine Quelldatei und eine Headerdatei ausgelagert. So genügt es, im Programm die Headerdatei anzugeben. Wenn man aus der Quelldatei keinr binäre Objectdatei machen will, kann man sie beim Compilieren einfach mit angeben. Ausserdem muss wegen der Rechnerei die Mathe-Library eingebunden werden. Daher lautet der Compileraufruf für die Hauptdatei gps.c

gcc -Wall -o gps -lm gps.c nmea.c
In der Datei gps.c wird die Schnittstelle geöffnet und dann liest das Programm vom GPS-Empfänger so lange, bis beide o. a. Records ausgewertet wurden. Am Programmanfang werden die Structure location, die Baudrate und die Schnittstellenadresse vereiunbart. Die Funktion UART_Init() öffet die serielle Schnittstelle für den GPS-USB-Empfänger uns stellt die Parameter für den Datentransfer ein. Die Hauptarbeit leistet die Funktion gps_location(). Sie sammelt aus den beiden GPS-Records Daten über Länge, Breite, Geschwindigkeit, Richtung, Datum und Uhrzeit ein. Das Problem dabei ist, dass man nicht weiss, wann einer der beiden GPS-Records "vorbeikommt" und in welcher Reihenfolge sie gelesen werden. Deshalb ist die Funktion als kleine State-Machine programmiert. Die Uhrzeit (Format: hhmmss.sss) wird als Dezimalzahl (double) gespeichert, das Datum (Format: ddmmyy) als Long Integer. Damit diese beiden Angaben auch als Strimg im üblichen Format verwendet können, gibt es die beiden Umwandlungsfunktionen gps_date2str() und gps_time2str(). Im Hauptprogramm wird nur gps_location() aufgerufen. Danach folgt die Kontrollausgabe der Werte.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <termios.h>
#include <inttypes.h>
#include <math.h>

#include "nmea.h"

#define BUFSIZE 4000
#define BAUDRATE B115200
#define UART "/dev/ttyACM0"

/* Speichern der GPS-Daten */
struct location
  {
  double latitude;    /* Breitengrad */
  double longitude;   /* Laengengrad */
  double speed;       /* Geschwindigkeit */
  double altitude;    /* Hoehe ueber NN */
  double course;      /* Marschrichtung */
  };


/*
 * Oeffen der seriellen Schnittstelle fuer den GPS-USB-Empfaenger
 */
int UART_Init()
  {
  int FD;
  struct termios options = {};

  FD = open(UART, O_RDONLY | O_NOCTTY);
  if (FD < 0)
    {
    printf("Fehler beim Oeffnen von %s\n", UART);
    exit(-1);
    }
  memset(&options, 0, sizeof(options));
  options.c_cflag &= ~PARENB;         /* kein Paritybit */
  options.c_cflag &= ~CSTOPB;         /* 1 Stoppbit */
  options.c_cflag &= ~CSIZE;          /* 8 Datenbits */
  options.c_cflag = CS8 | CLOCAL | CREAD;
  options.c_iflag = IGNPAR;
  options.c_oflag = 0;
  options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
  options.c_cc[VTIME] = 10;
  options.c_cc[VMIN] = 1;
  cfsetspeed(&options, BAUDRATE);
  tcflush(FD, TCIFLUSH);
  tcsetattr(FD, TCSAFLUSH, &options);
  return FD;
  }

/*
 * Daten ueber Laenge, Breite, Geschwindigkeit, Richtung, Datum und Uhrzeit
 * vom GPS-Empfaenger einsammeln. Die Uhrzeit (Format: hhmmss.sss) wird als
 * Dezimalzahl (double) gespeichert, das Datum (Format: ddmmyy) als Long Integer.
 */
void gps_location(int FD, struct location *coord, double *time, long int *date)
  {
  char buffer[BUFSIZE];
  int Laenge, Anz, Fin;
  char Zeichen;
  unsigned char status = N_EMPTY;
  struct gpgga gpgga;
  struct gprmc gprmc;

  /* Schleife, bis jeweils ein GPGGA- und ein GPRMC-Record gelesen wurden. */
  while(status != N_COMPLETE)
    {
    Laenge = 0;
    Fin = 0;

    /* eine Zeile einlesen */
    while(! Fin)
      {
      Anz = read(FD, &Zeichen, 1);
      if (Anz > 0)
        {
        if(Zeichen == '\n')
          {
          buffer[Laenge] = '\0';
          Fin = 1;
          }
        else if(Zeichen != '\r')
          {
          buffer[Laenge] = Zeichen;
          Laenge++;
          }
        }
      }
    /* Abhaengig vom Record-Typ die Daten einsammeln */
    switch (nmea_get_message_type(buffer))
      {
      case N_GPGGA:
        nmea_parse_gpgga(buffer, &gpgga);
        gps_deg(&(gpgga.latitude), gpgga.lat, &(gpgga.longitude), gpgga.lon);
        coord->latitude = gpgga.latitude;
        coord->longitude = gpgga.longitude;
        coord->altitude = gpgga.altitude;
        if (status == N_GPRMC)
          status = N_COMPLETE;
        else
          status |= N_GPGGA;
        break;
      case N_GPRMC:
        nmea_parse_gprmc(buffer, &gprmc);
        coord->speed = gprmc.speed;
        coord->course = gprmc.course;
        *date = gprmc.date;
        *time = gprmc.time;
        if (status == N_GPGGA)
          status = N_COMPLETE;
        else
          status |= N_GPRMC;
        break;
      }
    }
  }


/*
 * Datumsangabe (gespeichert als long int, Format: ddmmyy) in String umwandeln
 */
void gps_date2str(long int date, char *datestr)
  {
  char buf[10];
  /* date: ttmmyy */

  buf[0] = '\0';
  sprintf(datestr,"%02ld", ((date/10000) % 100)); /* Tag */
  strcat(datestr, ".");
  sprintf(buf,"%02ld", ((date/100) % 100));       /* Monat */
  strcat(datestr, buf);
  strcat(datestr, ".");
  sprintf(buf,"%02ld", (date % 100));             /* Jahr */
  strcat(datestr, "20");
  strcat(datestr, buf);
  }


/*
 * Zeitangabe (gespeichert als double, Format: hhmmss.sss) in String umwandeln
 * (ohne Nachkommastellen der Sekunden)
 */
void gps_time2str(double time, char *timestr)
  {
  char buf[10];
  long int ttt;

  ttt = (long) time;
  buf[0] = '\0';
  sprintf(timestr,"%02ld", (1 + (ttt/10000) % 100)); /* Stunde (UTC+1 -> MESZ) */
  strcat(timestr, ":");
  sprintf(buf,"%02ld", ((ttt/100) % 100));       /* Minute */
  strcat(timestr, buf);
  strcat(timestr, ":");
  sprintf(buf,"%02ld", (ttt % 100));             /* Sekunde */
  strcat(timestr, buf);
  }


int main(int argc, char** argv)
  {
  int FD;                       /* Filehandle fuer serielle Schnittstelle */
  struct location coordinates;  /* Datensammler füer GPS-Standort */
  long int date;                /* Datum */
  double time;                  /* Uhrzeit */
  char buffer[100];             /* Puffer */

  /* Schnittstelle oeffnen */
  FD = UART_Init();

  /* Standort ermitteln */
  gps_location(FD, &coordinates, &time, &date);

  /* Kontroll-Ausgabe */
  printf("Breitengrad:      %lf\n", coordinates.latitude);
  printf("Laengengrad:      %lf\n", coordinates.longitude);
  printf("Hoehe ueber NN:   %lf\n", coordinates.altitude);
  printf("Geschwindigkeit:  %lf Richtung: %lf\n",
                 coordinates.speed, coordinates.course);
  printf("Uhrzeit (double): %lf Datum (long): %ld\n", time, date);

  /* Uhrzeit/Datum in String umwandeln */
  gps_date2str(date, buffer);
  printf("Datum (String):   %s\n", buffer);

  gps_time2str(time, buffer);
  printf("Uhrzeit (String): %s\n", buffer);

  close (FD);
  return 0;
  }
Von der Bilbliothek wird hier nur das Headerfile aufgelistet, die Datei mit den Funktionen ist etwas länglich und bietet keine interessanten Erkenntnisse. Wer reinschauen will: Die Links am Ende dieser Seite liefern alle Dateien.
#ifndef _NMEA_H_
#define _NMEA_H_

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

#define N_EMPTY 0x00
#define N_GPRMC 0x01
#define N_GPGGA 0x02
#define N_UNKNOWN 0x00
#define N_COMPLETE 0x03
#define N_CHECKSUM_ERR 0x80
#define N_MESSAGE_ERR 0xC0

#define N_GPRMC_STR "$GPRMC"
#define N_GPGGA_STR "$GPGGA"

/* Struktur fuer GPGGA-Record */
struct gpgga
  {
  double latitude;           /* Breitengrad */
  char lat;                  /* 'N' oder 'S' */
  double longitude;          /* Laengengrad */
  char lon;                  /* 'E' oder 'W' */
  unsigned char quality;     /* "Quality"-Feld */
  unsigned char satellites;  /* Anzahl Satelliten */
  double altitude;           /* Hoehe ueber dem Meeresspiegel */
  };

/* Struktur fuer GPRMC-Record */
struct gprmc
  {
  double latitude;           /* Breitengrad */
  char lat;                  /* 'N' oder 'S' */
  double longitude;          /* Laengengrad */
  char lon;                  /* 'E' oder 'W' */
  double speed;              /* Geschwindigkeit */
  double course;             /* Marschrichtung */
  double time;               /* Uhrzeit */
  long int date;             /* Datum */
  };

/* Messagetyp ermitteln (GPGGA, GPRMC, etc..) */
char nmea_get_message_type(char *);

/* Checksum eines Records pruefen */
char nmea_valid_checksum(char *);

/* GPGGA-Record untersuchen und auswerten */
void nmea_parse_gpgga(char *, struct gpgga *);

/* GPRMC-Record untersuchen und auswerten */
void nmea_parse_gprmc(char *, struct gprmc *);

/* Laengen- oder Breitenangabe von Grad in Dezimalnotation umrechnen */
double gps_deg_dec(double deg_point);

/* Laenge und Breite von Grad in Dezimalnotation umrechnen */
void gps_deg(double *latitude, char ns, double *longitude, char we);

#endif
Ein Probelauf des Programms zeigt beispielsweise die folgende Ausgabe:
pi@raspberrypi ~/GPS $ gcc -Wall -o gps -lm gps.c nmea.c
pi@raspberrypi ~/GPS $ ./gps
Breitengrad:      48.235848
Laengengrad:      11.676708
Hoehe ueber NN:   478.500000
Geschwindigkeit:  0.000000 Richtung: 78.110000
Uhrzeit (double): 135757.000000 Datum (long): 140316
Datum (String):   14.03.2016
Uhrzeit (String): 14:57:57

Geschwindigkeit und Entfernung

Übrigens pendelt sich die Geschwindigkeit, die ja auf mehreren Messungen beruht, erst nach einiger Zeit auf den richtigen Wert ein. Die Berechnung zwischen zwei Koordinaten ist nicht ganz trivial, weil die Erde ja annähernd Kugelgestalt besitzt. Während der Abstand zwischen zwei Breitenkreisen immer konstant 111.3 km beträgt, variiert der Abstand zwischen zwei Längenkreisen in Abhängigkeit von der geografischen Breite: Am Äquator ist er ebenfalls 111.3 km, an den Polen hingegen 0. Der Abstand wird nach der Formel 111.3 * cos(Breite) berechnet. Bei der Berechnung mit einem Computer ist zu beachten, dass fast alle Programmiersprachen in der Regel den Winkel im Bogenmaß (Radiant) erwartet. Die Umrechnung von Grad in Radiant erfolgt nach der Beziehung 1° = p/180 ≈ 0.01745. Eine annähernd genaue Berechnung der Entfernung zweier Punkte (lat1, lon1) und (lat2, lon2) ergibt sich aus (Breite und Länge in Grad):

lat = (lat1 + lat2)/2 * 0.01745
dx = 111.324 * cos(lat) * (lon1 - lon2)
dy = 111.324 * (lat1 - lat2)
Entfernung in km = sqrt(dx2 + dy2)
Leider versagen diese einfachen Methoden bei größeren Entfernungen. Die Längen- und Breitenkreise bilden ja kein rechtwinkliges Gitternetz, da die Erde kugelförmig ist. Auf kleinen Karten wie einer Wanderkarte kann man die Verzerrungen ignorieren und die obige Formel verwenden. Bei einer Reise von Berlin nach Peking geht das nicht mehr. Zur Anwendung kommt der Seitenkosinussatz (sphärische Trigonometrie):
cos(g) = sin(lat1) * sin(lat2) + cos(lat1) * cos(lat2) * cos(lon2 - lon1)
Entfernung in km = 6378.388*acos(sin(lat1)*sin(lat2) + cos(lat1)*cos(lat2)*cos(lon2 - lon1))
Wie schon erwähnt, sind alle Werte im Bogenmaß anzugeben. Rechnet man dagegen mit Grad, ist statt der Konstanten 6378.388 der Wert 6378.388*π/180 = 111.324 zu verwenden.

Im WWW finden Sie zahlreiche Seiten, welche die Formeln schon implementiert haben und bei denen Sie nur noch die Längen- und Breitenangaben in ein Formular eintragen müssen.

GPS-Tools

Es existieren natürlich etliche Tools zum Auswerten der GPS-Daten - für den Fall, dass Sie keine Lust haben, selbst zu programmieren.

gpsd

gpsd ist ein Dienst-Programm ("Daemon"), das die Daten von einem oder mehreren GPS-Empfängern ausliest, die an den Computer angeschlossen sind. Die Daten können dann direkt von Client-Applikationen genutzt werden. Neben dem eigentlichen Dienst gibt es zusätzliche Hilfswerkzeuge zur Diagnose. gpsd kann auch mehrere Clients gleichzeitig bedienen oder die Daten über das Netzwerk zur Verfügung stellen. Zum Testen kann man mit dem telnet-Befehl (Standardport 2947) den GPS-Empfänger über gpsd erreichen: telnet localhost 2947 Beachten Sie, dass beim Beenden des gpsd (wie bei fast jedem Serverprogramm) der Port noch einige Zeit als "belegt" markiert ist (einfach mal 'ne Pause machen). gpsd kann mittels apt-get install gpsd istalliert werden.

GPS-Hilfsprogramme

Die GPS-Daten können auf der Konsole mit dem Programm cgps angezeigt werden. Sobald Sie den GPS-Daemon gestartet haben, können Sie den GPS-Client cgps aufrufen. Dieser holt sich die GPS-Informationen vom GPS-Daemon und zeigte diese an (cgps -s).

Mit dem Programm gpsmon der gpsd-Toolsuite lassen sich die vom GPS-Empfänger gesendet Daten in Form von NMEA Sätzen auf der Console ansehen. Somit kann wunderbar überprüft werden ob Daten empfangen werden. Außerdem liefert das Programm eine Übersicht, ähnlich wie cgps.

Mit dem Befehl xgps steht ein grafische Programm zur Verfügung. xgps zeigt eine grafische Darstellung der aktuellen GPS-Informationen wie Position, Zeit, Geschwindigkeit und Position der Satelliten. In der Standardansicht erscheint für jeden GPS-Satelliten ein Kreis. Unterschiedliche Farben repräsentieren die Empfangsstärke, bei einem ausgefüllten Kreis wurde der zugehörige Satellit in die letzte Positionsberechnung einbezogen. Ist an dem Raspberry Pi kein Monitor angeschlossen kann die Grafikausgabe auch über SSH auf einem anderen System erfolgen.

Möchte man den zurückgelegten Weg aufzeichnen, nimmt man den gpxlogger. Das Programm läuft im Terminal und schreibt dort fortlaufend die Postionsdaten im GPX-Format alle 5 Sekunden auf die Standardausgabe. Um die Strecke zu speichern, muss man die Ausgabe in eine Datei umlenken, zum Beispiel mit gpxlogger > track.gpx. Mit [Strg]+[C] beendet man die Aufzeichnung. Das Ergebnis verarbeitet jede Anwendung, die das GPS Exchange Format versteht.

Links


Copyright © Hochschule München, FK 04, Prof. Jürgen Plate und die Autoren
Letzte Aktualisierung: