Mit einem GPS Modul die richtige Uhrzeit für den Raspberry PI 3 A+ setzen, ganz ohne Netzwerk

Einer der größten Nachteile des Raspberry PI ist das Fehlen einer batteriegepufferten Uhr. In der Regel spielt das keine große Rolle, da der Raspberry mit einem Netzwerk verbunden ist und sich so beim Starten die Uhrzeit über das Network Time Protocol (NTP) holen kann. Es gibt aber Situationen, wo kein Netzwerk zur Verfügung steht und man trotzdem die genaue Uhrzeit benötigt. 

Uhrzeit ohne Netzwerk mit GPS

Zurzeit baue ich an der Version 2.0 des Zyklochrons. Die Uhr soll natürlich überall die richtige Uhrzeit anzeigen, ohne dass man sie erst umständlich mit einem WLAN verbinden muss. Für den Prototyp war das noch kein Problem, ein verkaufsfertiges Produkt braucht aber eine andere Lösung. 

Das Einfachste wäre, ein RTC (Real-time clock) Modul zu verbauen. Diese Variante scheitert aber aus einem rein praktischen Grund. Meine Uhr hat keinerlei Tasten, um initial die Uhrzeit einzustellen. Die zweite naheliegende Lösung wäre ein DCF77-Empfänger. Das habe ich auch versucht. Grundsätzlich hat der Empfänger ein Zeitsignal geliefert, aber sobald der Schrittmotor-Treiber in Betrieb war, kam nichts mehr. Leider hat sich hier bestätigt, was man oft im Netz lesen kann, nämlich dass ein DCF77 Empfänger sehr empfindlich auf Störsignale reagiert. Die dritte Option wäre ein GPS-Empfänger, denn das GPS-Signal überträgt nicht nur die Position, sondern auch die genaue Uhrzeit. Da diese Variante sehr gut funktioniert, möchte ich euch beschreiben, wie man mit einem GPS-Modul und einem kleinen Python Skript die Uhrzeit des Raspberry setzen kann.

Das GPS-Modul

Für meinen Aufbau verwende ich ein GT-U7 GPS-Modul der Firma Goouuu Tech. Dieses Modul ist kompatibel zum NEO-6M GPS Modul der Firma u-blox.

Uhrzeit ohne Netzwerk mit GPS für den Raspberry PI 3

Das GPS Modul bietet eine ganze Reihe von Features, wovon ich hier nur die Wichtigsten aufzählen möchte:

  • UART, USB, I2C und SPI Schnittstellen, davon stehen auf der Platine UART und USB zur Verfügung
  • Baud Rate der seriellen Schnittstelle: 4800-230400 (default 9600)
  • PPS-Ausgang (Pulse per second) liefert ein Signal im Sekundentakt und ist mit einer LED verbunden. Blinkt die LED, hat das GPS Modul genug Satelliten gefunden und die Position konnte ermittelt werden
  • Spannungsregler, damit das Modul auch mit 5V betrieben werden kann. Der NEO-6M benötigt 3,3V
  • Stromverbrauch unter 50mA
  • Eine Pufferbatterie versorgt das Modul für bis zu zwei Wochen mit Strom. Dadurch werden wichtige Informationen wie die Uhrzeit oder die letzte Position gespeichert.
  • Ein serielles EEPROM, das mit dem I2S Bus verbunden ist, speichert die Einstellungen
  • Aktualisierungsrate 5Hz
  • Unterstützte Kommunikationsprotokolle: NMEA, UBX Binary, RTCM
  • Time to fix: Kaltstart: 27s, Warmstart: 1s. Diese Werte gelten aber nur bei optimalen Bedingungen, in der Realität kann es deutlich länger dauern.
  • Externe Keramik-Antenne
  • Die Neo-6M Module unterstützen nur GPS. Die Neo-7M Module beherrschen auch das russische GLONASS Satellitennavigationssystem und die Neo-M8 Serie zusätzlich noch das europäische Galileo und das chinesische Beidou System

Der Preis für das GPS Modul bewegt sich so um die 10,- Euro. Bedenkt man, was das Modul alles kann, finde ich das wirklich nicht viel. Vielleicht werde ich auch noch das NEO-M8 Modul ausprobieren, speziell die Performance im Indoor-Bereich. Wer weiß, vielleicht wird das meine erste Bestellung bei AliExpress :-)

Anschluss an die serielle Schnittstelle des Raspberry PI

Der Anschluss des Neo-6M Moduls ist denkbar einfach. Ich verwende die UART Schnittstelle, daher müssen neben der Stromversorgung nur die RX und TX Anschlüsse verbunden werden, und zwar kreuzweise, also RX am Raspberry mit TX am GPS Modul bzw. TX am Raspberry mit RX am GPS Modul. Da ich hier ausschließlich Daten lese, kann die Sendeleitung des GPS Moduls genau genommen sogar weggelassen werden. Leider konnte ich für mein Modul keine Fritzing Bibliothek finden, daher hier der Schaltplan für ein ähnliches GPS Modul. Das Grundkonzept ist Dasselbe.

Uhrzeit ohne Netzwerk mit GPS für den Raspberry PI 3

Jetzt sind noch einige Änderungen in der Konfiguration des Raspberry Pi 3 nötig, um die serielle Schnittstelle verwenden zu können. Der Raspberry 3 hat zwei UART Schnittstellen. PL011 UART (PrimeCell UART) und mini UART. Standardmäßig wird die PL011 UART für Bluetooth verwendet und die mini UART ist mit den RX/TX Pins verbunden und wird auch für die Linux Konsole verwendet. Der Takt der mini UART Schnittstelle ist mit dem GPU Takt gekoppelt, was zur Folge hat, dass die Baud Rate nicht stabil ist, was wiederum Probleme mit der Verbindung verursachen kann. 

Da die serielle Konsole stören würde, muss sie deaktiviert werden. Dazu wird in der Datei /boot/cmdline.txt. ein Eintrag gelöscht. Der Editor wird dazu mit

sudo nano /boot/cmdline.txt

gestartet. Die Datei schaut bei mir folgendermaßen aus:

console=serial0,115200 console=tty1 root=PARTUUID=c64d7f36-02 rootfstype=ext4 elevator=deadline fsck.repair=yes rootwait

Der fett markierte Teil ist die Konfiguration für die serielle Konsole und muss einfach gelöscht werden. Danach sollte die Zeile folgendermaßen aussehen.

console=tty1 root=PARTUUID=c64d7f36-02 rootfstype=ext4 elevator=deadline fsck.repair=yes rootwait

Zusätzlich wird noch der Dienst der seriellen Konsole mit diesen Kommandos deaktiviert:

sudo systemctl stop serial-getty@ttyAMA0.service
sudo systemctl disable serial-getty@ttyAMA0.service


Jetzt muss noch Bluetooth deaktiviert und die PL011 UART Schnittstelle aktiviert werden, damit die RX/TX Pins verwendet werden. Dafür wird die Datei /boot/config.txt mit

sudo nano /boot/config.txt

geöffnet und am Ende folgende Zeilen eingefügt:

# Disable bluetooth
dtoverlay=pi3-disable-bt

# Enable UART
enable_uart=1
init_uart_baud=9600

Da Bluetooth deaktiviert ist, können auch die zugehörigen Dienste deaktiviert werden. Das passiert mit diesen Kommandos:

sudo systemctl stop hciuart
sudo systemctl disable hciuart

sudo systemctl stop bluetooth.service
sudo systemctl disable bluetooth.service

Abschließend muss der Raspberry noch mit dem Kommando

sudo reboot

neu gestartet werden, damit die Änderungen wirksam werden.

Damit ist der Raspberry bereit, mit dem GPS Modul zu kommunizieren. 

Python Script zum Auslesen der GPS Informationen

Zunächst wird mit diesem Python Script geprüft, ob das GPS Modul ordnungsgemäß funktioniert: Damit das Script funktioniert, muss noch ein Python Modul installiert werden, um auf die serielle Schnittstelle zugreifen zu können. Dazu führt man den Befehl

pip3 install pyserial

aus. Das Python Script zum Lesen der seriellen Schnittstelle ist ganz simpel:

import io
import serial
import sys
import logging

logger = logging.getLogger()
logger.addHandler(logging.StreamHandler(sys.stdout))
logger.setLevel(logging.DEBUG)

ser = serial.Serial(
        port = '/dev/ttyAMA0',
        baudrate = 9600,
        parity = serial.PARITY_NONE,
        stopbits = serial.STOPBITS_ONE,
        bytesize = serial.EIGHTBITS,
        timeout = 1
)
sio = io.TextIOWrapper(io.BufferedRWPair(ser, ser))

while True:
        try:
                line = sio.readline()

                logger.debug (line)

        except serial.SerialException as e:
                logger.error('SerialException: {}'.format(e))
                break
        except UnicodeDecodeError as e:
                logger.error('UnicodeDecodeError: {}'.format(e))
        continue

Zunächst startet man das Script und schließt dann das GPS Modul an die Stromversorgung an. Das Modul startet sofort mit der Ausgabe und die ersten Zeilen sollten folgendermaßen aussehen:

$GPTXT,01,01,02,u-blox ag - www.u-blox.com*50
$GPTXT,01,01,02,HW  UBX-G70xx   00070000 *77
$GPTXT,01,01,02,ROM CORE 1.00 (59842) Jun 27 2012 17:43:52*59
$GPTXT,01,01,02,PROTVER 14.00*1E
$GPTXT,01,01,02,ANTSUPERV=AC SD PDoS SR*20
$GPTXT,01,01,02,ANTSTATUS=DONTKNOW*33
$GPTXT,01,01,02,LLC FFFFFFFF-FFFFFFFD-FFFFFFFF-FFFFFFFF-FFFFFFF9*53

Sobald das Modul einen GPS Fix hat, werden folgende Zeilen geschickt:

$GPRMC,183125.00,A,4812.84035,N,01619.41598,E,1.030,,131021,,,A*75
$GPVTG,,T,,M,1.030,N,1.908,K,A*21
$GPGGA,183125.00,4812.84035,N,01619.41598,E,1,03,14.13,244.5,M,42.1,M,,*6F
$GPGSA,A,2,13,15,19,,,,,,,,,,14.17,14.13,1.00*08
$GPGSV,4,1,15,01,01,048,,05,04,205,,10,12,326,,12,10,224,11*71
$GPGSV,4,2,15,13,57,157,26,14,40,056,09,15,66,229,23,17,39,095,15*7F
$GPGSV,4,3,15,19,31,129,23,21,02,025,,23,24,293,,24,43,287,*7E
$GPGSV,4,4,15,28,59,062,,30,12,094,,39,18,124,*46
$GPGLL,4812.84035,N,01619.41598,E,183125.00,A,A*6E

Was man hier sieht, ist das sogenannte NMEA-Protokoll. Dieses Protokoll dient zur Kommunikation zwischen Navigations- und Endgeräten. Die Daten werden mittels ASCII-basierten Datensätzen übertragen, die einen recht simplen Aufbau haben. Der GPRMC Datensatz setzt sich beispielsweise folgendermaßen zusammen:

$ Beginn des Datensatzes
GP Geräte ID. GP = Global Positioning System (GPS)
RMC Datensatz ID. RMC = Recommended Minimum Sentence C
183125.00 UTC Zeit im Format HHMMSS.SS
A Status (A = valid, V = nicht valid)
4812.84035 Breitengrad
N Ausrichtung 
01619.41598 Längengrad
E Ausrichtung
1.030 Geschwindigkeit in Knoten
  Kurs 
131021 Datum im Format DDMMYY
  magnetische Abweichung
  Vorzeichen der Abweichung (E oder W)
A Signalintegrität 
A = Autonomous mode,
D = Differential Mode,
E = Estimated (dead-reckoning) mode
M = Manual Input Mode
S = Simulated Mode
N = Data Not Valid
* Ende des Datensatzes
75 Prüfsumme


Das NMEA-Protokoll definiert recht viele dieser Datensatz-Typen, aber der GPRMC Datensatz genügt in diesem Fall, da er Datum und Uhrzeit überträgt.

Das obige Python Script muss also erweitert werden und den GPRMC Datensatz so lange lesen, bis das GPS Modul die Position ermitteln konnte und einen gültigen Datensatz liefert. Für das Parsen des NMEA-Protokolls gibt es das Python Modul pynmea2. Mit dem Befehl

pip3 install pynmea2

wird es installiert. Hier ein Beispiel, wie der Datensatz gelesen werden kann:

import pynmea2

line = "$GPRMC,092757.00,A,4812.82676,N,01619.39135,E,0.800,,021021,,,A*76"
msg = pynmea2.parse(line)

zeit = msg.datetime

print (zeit)

Damit hat man eigentlich fast alle Informationen, die man braucht. Ein kleines Problem gibt es aber noch. Die GPS Satelliten senden die UTC Zeit. Um die lokale Zeit zu ermitteln, muss man die Zeitzone wissen, in der man sich befindet. Das klingt im ersten Moment recht einfach, aber wenn man sich die Karte der Zeitzonen anschaut, wird einem schnell bewusst, wie komplex diese Aufgabe ist. Glücklicherweise gibt es dafür das Python Modul timezonefinder, das anhand der gesendeten Koordinaten die Zeitzone ermitteln kann. Das Modul wird mit 

pip3 install timezonefinder

installiert. Damit ist es jetzt möglich, von der Position auf die Zeitzone zu schließen und die lokale Uhrzeit zu setzen. Das finale Python Script schaut dann folgendermaßen aus:

import io
import pynmea2
import serial
import time
import pytz
import logging
import sys
import os
from timezonefinder import TimezoneFinder

logger = logging.getLogger()
logger.addHandler(logging.StreamHandler(sys.stdout))
logger.setLevel(logging.DEBUG)

ser = serial.Serial(
    port = '/dev/ttyAMA0',
    baudrate = 9600,
    parity = serial.PARITY_NONE,
    stopbits = serial.STOPBITS_ONE,
    bytesize = serial.EIGHTBITS,
    timeout = 1
)
sio = io.TextIOWrapper(io.BufferedRWPair(ser, ser))

while True:
    try:
        line = sio.readline()
        msg = pynmea2.parse(line)

        if type(msg) == pynmea2.types.talker.RMC:

            status = msg.status

            if status == 'A':
                logger.debug('Got Fix')

                zeit = msg.datetime

                latitude = msg.latitude
                longitude = msg.longitude

                tf = TimezoneFinder()
                zeitzone_string = tf.timezone_at(lng=longitude, lat=latitude)

                logger.debug('Set timezone to %s', zeitzone_string)
                os.system(f"timedatectl set-timezone {zeitzone_string}")

                zeitzone = pytz.timezone(zeitzone_string)
                zeit_mit_zeitzone = zeit.replace(tzinfo=pytz.utc).astimezone(zeitzone)
                unix_zeit = time.mktime(zeit_mit_zeitzone.timetuple())

                logger.debug('Set time to %s', zeit_mit_zeitzone)
                clk_id = time.CLOCK_REALTIME
                time.clock_settime(clk_id, float(unix_zeit))

                break

    except serial.SerialException as e:
        logger.error('Device error: {}'.format(e))
        break
    except pynmea2.ParseError as e:

        logger.error('Parse error: {}'.format(e))
    except UnicodeDecodeError as e:
        logger.error('UnicodeDecodeError error: {}'.format(e))
    continue

Da dieses Python Script die Uhrzeit und Zeitzone des Betriebssystems ändert, muss es mit root-Rechten gestartet werden. Zusätzlich muss das sudo Kommando mit der Option -E ausgeführt werden, damit die installierten Python Module noch gefunden werden. Der Aufruf sieht beispielsweise so aus:

sudo -E python3 settime.py

Der folgende Screenshot zeigt, wie Datum/Uhrzeit und die Zeitzone von dem Script geändert wurden:

Uhrzeit ohne Netzwerk mit GPS für den Raspberry PI 3

Mit diesem Python Script es nun möglich, die genaue Uhrzeit des Raspberry Pi 3 zu setzen, ohne dass eine Netzwerkverbindung nötig ist. Die Voraussetzung ist natürlich, dass man GPS Empfang hat. Bisher habe ich es auch innerhalb der Wohnung immer geschafft, einen GPS Fix zu bekommen. Es kann aber eine ganze Weile dauern, bis es soweit ist.

Konversation wird geladen