NTP Server via GPS on a Raspberry Pi

This post shows how to use a GPS receiver with a Raspberry Pi to build a stratum 1 NTP server. I am showing how to solder and use the GPS module (especially with its PPS pin) and listing all Linux commands to set up and check the receiver and its NTP part, which is IPv6-only in my case. Some more hints to increase the performance of the server round things off. In summary, this is a nice “do it yourself” project with a working stratum 1 NTP server at really low costs. Great. However, keep in mind that you should not rely on such projects in enterprise environments that are more focused on reliability and availability (which is not the case on self soldered modules and many config file edits).

This article is one of many blogposts within this NTP series. Please have a look!
Note that this project uses a GPS receiver with a PPS (pulse per second) pin, that is: It does not only get the imprecise time sent over a serial console (standard NMEA 0183 output, more details here), but also the highly precise sync tick which accurately declares the start of every second. This involves some soldering. ;)

At the time of writing (Nov 2018) I am using a Raspberry Pi 1 B (yes, the old one), kernel 4.14.71+ and Raspbian GNU/Linux 9 (stretch). I installed a few relevant packages and gave it a static IPv6 address. Legacy IP (IPv4) is not used at all, only IPv6. [UPDATE Oct 2021: I updated some commands and procedures. I am now using a Raspberry Pi 3 B, Raspberry Pi OS 10 (buster), kernel 5.10.63-v7+.]

1) Soldering the GPS Module

It’s all about the GPS module “GY-GPS6MV2”, though I am quite sure that there are some other GPS modules with a PPS pin out there. Google for it and get one from China for a couple of Euros. ;) I soldered the five cables directly to the Pi, while I used a small Pigtail from the Hirose connector (the one on the GPS module) to SMA female (not RP-SMA!) to mount it on the housing of the Pi. This SMA connector is commonly used by GPS antennas for cars. In fact, I did not use the small GPS antenna that was delivered with the GPS module at all, but a slightly more robust one that can be used outside as well.

Following is the assignment of the five cables from the GPS module to the Raspberry Pi. You need to look at the product details of the module to find the PPS pin. I used a ribbon cable, refer to the photos below:

  1. GPS module VCC, white: Pi pin 1, +3.3 V
  2. GPS module GND, black: Pi pin 6, GND
  3. GPS module RX, grey: Pi pin 8, TXD0
  4. GPS module TX, purple: Pi pin 10, RXD0
  5. GPS module PPS, blue: Pi pin 12, GPIO18

This is what it looked like on my desk:

When you’re finished you can just power on the Pi again. If you have a good GPS signal your GPS module will blink blue once a second:


2) Getting started with mere GPS

The first step is to check whether the GPS module is running and whether the Pi receives the generic NMEA 0183 sentences. You have to disable the serial login shell first while keeping the hardware port enabled: sudo raspi-config , navigate to “Interface Options” -> “P6 Serial” and:

  1. Would you like a login shell to be accessible over serial? –> No
  2. Would you like the serial port hardware to be enabled? –> Yes

followed by a reboot. After that, you can use “screen” to get some output:

[Thanks to the comment from Chris that you have to use /dev/ttyS0.] You can close it with Ctrl+a \ y . The output should be like this:

Now you need to install the gpsd daemon which needs to run in the background at startup: [Thanks to the comments from siga and Olli to use “systemctl enable …”]

After the reboot, check that the daemon is running:

With tools such as cgps -s or gpsmon you get user-friendly information from your GPS module. You should see a “3D” fix, the correct position from the GPS receiver, and many satellites in view:

Good so far. Your GPS is up and running. ;D

3) Leveraging PPS

You now have to activate the pulse per second functionality:

After the reboot, check that the pps module is loaded, which is the case if “lsmod” lists some drivers such as this:

and try to get a tick every second (cancel it with Ctrl+c as always):

Good again.

4) Configuring NTP

I have first installed NTP via

to have all those dependencies and startup scripts in place, but built it from source afterwards as well to run the server in the latest version:

(Fun fact: Since I am running my NTP server via IPv6-only meanwhile the NTP download page is IPv4-only, I had to download and copy the ntp package from another machine to the Raspberry Pi. Sigh. Yes, I know, DNS64/NAT64 would solve the problem.)

You have just installed the latest version of NTP, while “holding” the ntp package within dpkg to not overriding/downgrading it with future “apt update”s. You can verify this with the following: (Note the “h” in the very first column which indicates that the ntp package is on hold.)

Similarly, you can verify the running ntp version with:


Now you need to add the reference clock (GPS + PPS) to NTP: “Drivers have addresses in the form 127.127.t.u, where t is the driver type and u is a unit number in the range 0-3 to distinguish multiple instances of the same driver”, Reference Clock Support. In fact, we have to use two drivers, one for the GPS to get the rough time (driver 28, SHM, shared memory driver which uses the gpsd daemon) and another one for the PPS (driver 22).

There are a few things to consider. First, concerning the PPS: “While this driver can discipline the time and frequency relative to the PPS source, it cannot number the seconds. For this purpose an auxiliary source is required, ordinarily, a radio clock operated as a primary reference (stratum 1) source; however, another NTP time server can be used as well. For this purpose, the auxiliary source should be specified as the prefer peer, as described in the Mitigation Rules and the prefer Keyword page”, PPS Clock Discipline. And about the SHM and its “flag1”: “Skip the difference limit check if set. Useful for systems where the RTC backup cannot keep the time over long periods without power and the SHM clock must be able to force long-distance initial jumps. Check the difference limit if cleared (default)”, Shared Memory Driver.

TL;DR: I had many problems running the Pi with *only* GPS/PPS, because after reboots or temporary loss of GPS signal it did not come back into normal working mode. The solution was to add some more NTP servers with the “prefer” statement to continuously have the time synced, while PPS can then do the exact and very precise timing.

In the end, I used these lines for my two drivers in the sudo nano /etc/ntp.conf file:

in conjunction with some more (in my case: IPv6 capable) NTP servers:

(Note that I used “fudge time1 +0.130 …” to adjust the GPS time according to the other NTP servers. You can try some higher/lower values to have the offset of your SHM compared to the offsets of those other NTP servers very small.)

Finally, it should give you something like this:

Note the ° symbol (= PPS peer when the prefer peer is valid) before the PPS, as well as the * symbol (= system peer) before the SHM; refer to Peer Status Word. These symbols must be present, but it will take some time initially. Otherwise, your instance is not working correctly, which would show an x instead.

Congratulations! You have your own NTP server on a Raspberry Pi with GPS and PPS up and running. Go for it! :D

5) Enhanced PPS Performance

[UPDATE Nov. 2018] Please note that I was not able to reproduce this behaviour on a fresh Raspbian stretch installation in 2018 anymore. Independent of the below-mentioned settings the jitter of the PPS was about 4 µs. The following listings are from an NTP server with Raspbian jessie, kernel 4.4.26+, in 2016/2017.[/UPDATE]

I came across the very detailed page from David Taylor in which he has some thoughts about reducing the (already very good) jitter for the PPS. Section “Enhanced PPS performance” on his page.

I opened the file sudo nano /boot/cmdline.txt and added nohz=off at the end of this single line. Of course with a space before it. Followed by a sudo reboot.

The jitter of the PPS driver decreased from about 0.005 ms = 5 µs to about 2 µs. Wow!

6) Reducing Ethernet Latency

And even one more hint from David Taylor about reducing the Ethernet latency on a Pi: Adding another option to the single line in sudo nano /boot/cmdline.txt that states: smsc95xx.turbo_mode=N (reboot needed) indeed reduces the “delay” as shown in the “ntpq -p” output again. I used two Raspberry Pis connected via a single switch, the first time without the option, the second with the option set on both Pis. The delay between those NTP servers decreased from about 0.916 ms to 0.632 ms. Nice.

That’s it for now. Wow! If you have any suggestions, please write a comment.


In the meantime, we have moved to our own house in which I was finally able (allowed) to mount a professional GPS antenna on my very own rooftop:


As always here are some links for further reading:

Featured image “Kompass_.jpg” by Rolf Schmitz is licensed under CC BY-SA 2.0.

36 thoughts on “NTP Server via GPS on a Raspberry Pi

  1. I’ve followed your directions after finding many others out of date. I now have a Raspberry pi running with 0.001 offset and 0.001 jitter, with 19 NTP servers listed in the config.
    Thanks SO much for your efforts.

  2. This is a good article but in many systems NTP servers operate in intranet networks, without internet. Hence, an NTP server should be able to operate with only GPS (or radio) clock source. If you can update the article and it operates in “standalone” mode, then this will be a perfect article.

    1. The prefer at the end of the “server minpoll 4 maxpoll 4 prefer” line says to use 1pps for that clock. At some point in the past, that prefer would only work on one server line and it didn’t work on the serial port. I use use driver 20 and avoid gpsd which uses driver 28.

    2. Hey ilk,

      I tried to use this GPS-based NTP approach to run the server completely standalone, but I did not succeed. I had many situations in which the server did not get the correct time for whatever reason. Hence I am using some additional servers on the Internet with the “prefer” statement.

      I you really want to operate an own NTP server for enterprise environments you should NOT rely on a Raspberry Pi anyway. Please have a look at the NTP servers from Meinberg (available with different kind of radio sources such as GPS, DCF77, Galileo, …): https://weberblog.net/ntp-appliance-meinberg-lantime-syncfire/


  3. Many GPS devices allow you to put them in timekeeping mode where they assume they aren’t moving. Combining that with just sending the time sequence, the delays are much more predictable and the jitter will drop.

    Some samples of controlling them to help with web searches:

    For the Uputronics board:
    /home/pi/src/gpsControl -s -d /dev/ttyAMA0
    echo \$PUBX,40,ZDA,0,1,0,0*45 >/dev/gps0 #turn on GPZDA
    echo ‘$PUBX,40,GSV,0,0,0,0*59’ >/dev/gps0 #turn off PUBX

    For the Adafruit to get just the GPZ sequence:
    printf ‘$PMTK314,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0*29\r\n’ >/dev/serial0

    Tthe /dev/ names differ between RPi boards and clock 20 needs a /dev/gps0 so its link to /dev/serial0 to /dev/ttyAMA0
    The *29 is a checksum. Some units need a cr/lf and others don’t care.

  4. Worked flawlessly for me, both with a cheap chinese breakout card with an Ublox NEO-6M-0-001 chip and a small antenna, using NMEA and PPS, and with an old, (forgotten in a drawer ) USB version Ublox Gps, in this case only with NMEA. Thanks for your fine work!

  5. really nice site, I am in love with NTP in general, at work we have symmetricom appliances with rubidium oscillators

    just a hint, the correct way to enable a service with systemd is

    root@time0:~# systemctl enable gpsd
    Synchronizing state of gpsd.service with SysV service script with /lib/systemd/systemd-sysv-install.
    Executing: /lib/systemd/systemd-sysv-install enable gpsd
    Created symlink /etc/systemd/system/multi-user.target.wants/gpsd.service → /lib/systemd/system/gpsd.service.

    root@time0:~# systemctl status gpsd
    ● gpsd.service - GPS (Global Positioning System) Daemon
    Loaded: loaded (/lib/systemd/system/gpsd.service; enabled; vendor preset: enabled)
    Active: active (running) since Thu 2019-12-12 16:32:01 CET; 4min 18s ago
    Main PID: 1164 (gpsd)
    Tasks: 1 (limit: 2319)
    Memory: 556.0K
    CGroup: /system.slice/gpsd.service
    └─1164 /usr/sbin/gpsd

  6. another suggestion: gives chronyd a chance, it still “speak” NTP but it’s way better in general, and in particular for virtual machines or machines with a bad hardware clock

    Also, no need to compile it

    root@time0:~# grep -Ev "^#|^[[:blank:]]*$" /etc/chrony/chrony.conf
    refclock SHM 0 offset 0.000 delay 0.000 refid NMEA noselect
    refclock PPS /dev/pps0 lock NMEA refid PPS
    server 0.ch.pool.ntp.org iburst minpoll 5 maxpoll 6 noselect
    server 1.ch.pool.ntp.org iburst minpoll 5 maxpoll 6 noselect
    server 2.ch.pool.ntp.org iburst minpoll 5 maxpoll 6 noselect
    server 3.ch.pool.ntp.org iburst minpoll 5 maxpoll 6 noselect
    driftfile /var/lib/chrony/chrony.drift
    logdir /var/log/chrony
    maxupdateskew 100.0
    makestep 1 3
    root@time0:~# chronyc sources
    210 Number of sources = 6
    MS Name/IP address Stratum Poll Reach LastRx Last sample
    #? NMEA 0 4 377 18 +127ms[ +127ms] +/- 370us
    #* PPS 0 4 377 19 +607ns[ +797ns] +/- 303ns
    ^? tock.ntp.infomaniak.ch 1 5 377 1 +432us[ +432us] +/- 7303us
    ^? 3 5 377 2 -1076us[-1076us] +/- 13ms
    ^? ns1.nexellent.net 2 5 377 1 +2073us[+2073us] +/- 44ms
    ^? eudyptula.init7.net 2 5 377 1 +1828us[+1828us] +/- 57ms
    root@time0:~# chronyc sourcestats
    210 Number of sources = 6
    Name/IP Address NP NR Span Frequency Freq Skew Offset Std Dev
    NMEA 6 3 81 -22.907 140.278 +126ms 883us
    PPS 17 7 256 +0.000 0.009 +0ns 767ns
    tock.ntp.infomaniak.ch 12 7 265 -0.134 4.495 +167us 292us 12 7 265 -0.469 2.341 -1152us 179us
    ns1.nexellent.net 12 9 265 +0.542 4.079 +1922us 317us
    eudyptula.init7.net 12 6 265 -0.801 1.698 +1678us 116us

    1. Your link is blocked “for security reasons”. Do you want someone to visit it? Then better free it up

      1. It’s an issue I have since few months between Chrome and my nginx, I need manual actions every nginx update… it should be OK now.

  7. Hello Johannes, how are you? Related to this post, do you know if it is possible to output a 1PPS signal from the system clock through one of the Raspberry Pi GPIO pins? Many thanks – Ricardo.

    1. Uh, good question. I have no idea. Sorry.

      What do you need it for? A professional reason? Then you should probably rely on a professional clock as well…

      1. Just “for fun”. We would like to compare the results of “ppstest” with the results using an external time interval counter.

  8. Hi Johannes,

    very good guide. I foud your guide because I was struggling with autostart of the gpsd.
    I would highly recommend to add a service like described by the GPSD project:


    instead of using

    # the following symlink is needed to have gpsd started after boot correctly
    # ref: https://www.raspberrypi.org/forums/viewtopic.php?f=45&t=53644#p1048128
    sudo ln -s /lib/systemd/system/gpsd.service /etc/systemd/system/multi-user.target.wants/

    1. Yup, you’re absolutely correct. I will update this post once I am setting up another Pi with GPS (which will probably be within the next 12 months). ;)

  9. On the Pi3B you use /dev/ttyS0 instead of /dev/ttyAMA0 which is now used for the bluetooth serial interface. I think that’s true on Pi4 as well.

  10. In my experience, when there aren’t external servers to synchronize against, the prefer keyword on 28.0 causes ntp to first sync to the gps serial (hundreds of milliseconds off) and very slowly drift to the pps. Instead I use the noselect keyword for gps and the prefer keyword for pps. Also, I get 1pps from the gpsd driver (SHM1) 28.1 vs from the pps driver (PPS) 22.0, seems to work OK.

  11. Hi !
    Can you please provide a more detailled pic where you get the PPS Signal on this PCB ?

    Ciao Gerd

    1. Hello Gerd,

      well, you can zoom into the picture on the blog already, which gives you a quite detailed view of it: https://weberblog.net/wp-content/uploads/2018/11/DSC01670-PPS-angel%C3%B6tet.jpg
      If this is not enough, you can try to Google after it. ;) The name of the module is GY-GPS6MV2. As far as I remember, one of the pins of the chip has the PPS signal, while it was easier to solder it on the resistor which leads to the LED rather than on the chip itself.


  12. Hello Johannes,

    thanks a lot for this guide. Tried the setup with an RPi 4 and 5.10 kernel. Works (nearly out of the box) as expected. The gpsd service as well as the ntpd service is already there, when you install from the recent raspian repositority. So after compiling the newest version of ntpd you have to set it just on hold as you wrote. To enable the service just type

    ‘sudo systemctl enable ntp && sudo systemctl start ntp’ (same with the gpsd)

    In the standard config file I had to make some changes. It may be useful for other readers, as well. Several time queries worked well until the moment as the ntp server gets some load (maybe 5 to 10 queries per second. Than the clients get a 500 error and timeouts. I searched for that behavior for a while. I had to comment out the directives kod & limited, to get the server work with load as expected.

    # By default, exchange time with everybody, but don’t allow configuration.
    restrict -4 default notrap nomodify nopeer noquery # kod limited
    restrict -6 default notrap nomodify nopeer noquery # kod limited

    Furtermore I proxied the RPi4 through an nginx on an other machine. That could be interesting to the readers as well. That is very useful to not expose the whole RPi4 to the internet, and as well you can do some load balancing if you have a lot of clients and/or more than one timeserver. In my case its, because I only have one public IPv4 that I can use for that.

    I used the nginx stream directive with a small logging format to do that. Here ist my workling config (just set the proper IPs).

    stream {
    #set Log Format
    log_format upstream ‘$remote_addr [$time_local] ‘
    ‘$protocol $status $bytes_sent $bytes_received ‘
    ‘$session_time ‘

    # ntp upstream pool
    upstream time {
    server 192.168.xx.xx:123;
    #server 192.168.xx.xx:123;
    #server 192.168.xx.xx:123;
    #server 192.168.xx.xx:123 backup;

    # Time server proxy
    server {
    listen 123 udp;
    listen [::]:123 udp;
    access_log /var/log/nginx/time.access upstream;
    error_log /var/log/nginx/time.error;
    proxy_timeout 5s;
    proxy_requests 1;
    proxy_responses 1;
    proxy_pass time;

    I highly recomment to turn of logging after you checked that all works well. The ntp is such a small protocol and the log will be huge in a couple of hours if you expose the timeserver to pool.ntp.org.

    So have fun



    1. Hey Ulli,

      thanks for your hints!

      Concerning the “kod & limited”: To my mind this kicks in if the *same* clients sends to many queries. Not if many clients send queries. Maybe you are using a NAT device in front of the Pi so that the ntpd see the same incoming IP address for this? As I used my Pis for IPv6 only, I was not using any kind of NAT. I didn’t had any problems with rate limits (as far as I know).

      I used an F5 load balancer for my NTP services. There I had to disable the rate limits as well: https://weberblog.net/load-balancing-ntp-via-f5-big-ip-ltm/


  13. Congratulations! The first concise and coherent blog on this topic.
    Unfortunately most such blogs incl. the gpsd site are fragmented, obtuse, incoherent and overfilled with useless information. I spent days figuring out that I needed to connect the ublox PPS pin to a GPIO pin on the Rpi and then pipe it to pps0. Then days trying to get the gpsd compiled with the right flags so that I do not get “PPS ioctl(TIOCMIWAIT) failed”. gpsd tell you that this is bad, but offer no solution!
    Do you have such an excellent blog post combining Rpi, gpsd and chrony/NTPsec?

  14. I suspect that my pps0 is not being piped through to gpsd, even though the ntpq -p ouput seems ok:

    PPS(0) .PPS. 0 l – 16 0 0.000 +0.000 0.002
    *SHM(0) .GPS. 0 l – 16 1 0.000 +49.184 0.002

    When I run ppscheck I get:

    pi@raspberrypi-101:~/ntp-4.2.8p15 $ sudo ppscheck /dev/pps0
    # Seconds nanoSecs Signals
    PPS ioctl(TIOCMIWAIT) failed: 25 Inappropriate ioctl for device

    According to gpsd website it should be KPPS.

  15. OK, I solved this by downloading the latest version of gpsd, cd-ing into gpsd and then building with the following flags:

    pi@raspberrypi-101:~ $ sudo scons pps=yes ntpshm=yes nmea0183=yes ublox=yes fixed_port_speed=38400 fixed_stop_bits=1
    pi@raspberrypi-101:~ $ sudo scons check && udev-install

    In /boot/config.txt:

    When I run:
    sudo ppscheck /dev/pps0

    I now get:
    pi@raspberrypi-101:~ $ sudo ppscheck /dev/pps0
    INFO: ioctl(/dev/pps0, TIOCMGET) failed: Inappropriate ioctl for device(25)
    INFO: /dev/pps0 does not appear to be a tty
    INFO: kpps_caps 0x11F3

    # Src Seconds Signal Sequence

    KPPS 1658872884.068078399 assert 59
    KPPS 1658872883.168062214 clear 58
    KPPS 1658872884.168074787 clear 59

    Now I get this for ntpq -p :
    oPPS(0) .PPS. 0 l 10 16 377 0.000 +3.230 0.263
    xSHM(0) .GPS. 0 l 3 16 377 0.000 +70.159 0.425

  16. I’ve just upgraded my ntp servers to new hardware:

    Raspberry Pi 5
    Waveshare PoE Power Over Ethernet HAT (F)
    GeeekPi N07 M.2 PCIe to NVMe Bottom SSD
    Samsung 970 EVO 1TB SSD
    Uputronics Raspberry Pi GPS/RTC Expansion Board

    The new Pi 5 come with an onboard RTC. But you can disable it. Here’s the config:


    MS Name Stratum Poll Reach LastRx Last sample
    #* GPS 0 4 377 20 -444ns[ -628ns] +/- 151ns
    =- NTP2 1 6 377 24 +19us[ +19us] +/- 162us

Leave a Reply

Your email address will not be published. Required fields are marked *