Skip to content

Instantly share code, notes, and snippets.

@spatula75
Last active June 25, 2023 04:14
Show Gist options
  • Save spatula75/09e3a345c29c9628e351953bb2d98fde to your computer and use it in GitHub Desktop.
Save spatula75/09e3a345c29c9628e351953bb2d98fde to your computer and use it in GitHub Desktop.
Setting up a GPS-based ntpd using a Raspberry Pi and the Adafruit Ultimate GPS Hat

The Problem

What sounded like a simple enough thing to want to do-- run ntpd on a Raspberry Pi that synchronizes with gps for use on my local LAN-- quickly turned into a nearly hopeless situation. Many guides existed online purporting to show how to do this; however, they universally failed to achieve results, either because they never actually worked, worked on some older version of Linux, or only worked by sheer luck for the author, and not for anybody else (maybe because they failed to document some critical steps).

Further, many of these guides were written by folks who don't really know what they're doing in Linux/Unix. They mean well, but some of the advice I saw was questionable at best, and some of the suggestions I found were outright wrong. There was also a lot of pointless installing-only-to-uninstall and other such nonsense.

I haven't had a chance to really boil these instructions down all the way to a step-by-step guide, but my hope is that in outlining some of the general problems I encountered and the way I worked past them, along with what each of the steps actually does, the next person trying to do this has some better suggestions other than random, stabby incantations from someone who was basically just guessing.

Preparing the Environment

I opted to start with NOOBS for my Raspberry Pi 3, and I went with a straightforward Raspbian installation with no GUI. The GUI would never be useful for this exercise, so there is no point in the extra bloat. Then I followed the Adafruit instructions for disabling the hardware UART console. This step is important, because the GPS hat uses the hardware UART device to communicate the current GPS positioning and time.

What the Adafruit instructions failed to mention is that you also need to disable Bluetooth on a Pi3, because this also tries to use the UART. To achieve this, you need to do two things. First, add the line dtoverlay=pi3-disable-bt to /boot/config.txt and also disable the service in systemd with systemctl disable hciuart. It's worth noting that systemd is a steaming pile of hot garbage whose answer to what a megamonolith initd had become was to become an even bigger megamonolith.

We also need to make sure that the system is going to boot with the console on tty1. Make your /boot/cmdline.txt look like this by adding dwc_otg.lpm_enable=0, nohz=off to the end of the line and making console=tty1:

dwc_otg.lpm_enable=0 console=tty1 root=/dev/mmcblk0p7 rootfstype=ext4 elevator=deadline fsck.repair=yes rootwait nohz=off

(actual device names may vary.) The nohz=off disables "dynamic ticks" in the kernel, something that is desirable for a timing-sensitive application like an ntp server timing itself against GPS.

Lastly, tell the Pi to run at its top speed all the time, and that we want to use the maximum UART speed by default by adding these options to /boot/config.txt:

force_turbo=1
dtoverlay=pps-gpio,gpiopin=4
init_uart_baud=115200

Then, we need to disable switching to the ondemand performance governor as well. As root:

echo GOVERNOR=\"performance\" > /etc/default/cpufrequtils
apt-get install cpufrequtils
systemctl enable cpufrequtils
systemctl start cpufrequtils
systemctl disable raspi-config

You can confirm that you're using the performance governor with cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor.

Setting up ntpd

Since by all accounts the ntpd found in the Debian packaging system lacks support for synchronizing against pps0, it is sadly necessary to compile ntpd from scratch. Check https://www.eecis.udel.edu/~ntp/ntp_spool/ntp4/ntp-4.2/ for the latest version. The below example uses 4.2.8p13.

sudo apt-get update
sudo apt-get install libcap-dev
wget http://www.eecis.udel.edu/~ntp/ntp_spool/ntp4/ntp-4.2/ntp-4.2.8p13.tar.gz
tar xzvf ntp-4.2.8p13.tar.gz
cd ntp-4.2.8p13
./configure --prefix=/usr --enable-all-clocks --enable-parse-clocks \
--enable-SHM --enable-debugging --sysconfdir=/var/lib/ntp --with-sntp=no \
--with-lineeditlibs=edit --without-ntpsnmpd --disable-local-libopts \
--disable-dependency-tracking --enable-linuxcaps --enable-pps --enable-ATOM 
make
sudo make install

We ultimately want to run ntpd as the ntp user, so add a system user for this:

sudo adduser --system ntp --group
sudo usermod -a -G dialout ntp

Finally, we want ntpd to run as a system daemon. Fortunately, ntpd comes with a sample System V init script that is almost what we need.

Grab scripts/rc/ntpd from inside the same directory as the ntpd source, and tack this onto the top of the script:

#
### BEGIN INIT INFO
# Provides:          ntpd
# Required-Start:    $remote_fs $local_fs gpsd
# Required-Stop:     $remote_fs $local_fs gpsd
# Should-Start:
# Should-Stop:
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: NTP server
# Description:
### END INIT INFO

Then copy the script to /etc/init.d and load it with systemctl daemon-reload.

We want ntpd to watch /dev/pps0 (the pulse-per-second device) for pulses from the GPS hat, and for that, we need symlinks, because ntpd believes this device should be called /dev/gpspps0. To make sure we get the pps0 device, add pps-gpio to /etc/modules, and to get the symlinks we need, create the file /etc/udev/rules.d/99-gps.rules that contains this line:

KERNEL=="pps0",SYMLINK+="gpspps0"

Now to get udev to actually create those symlinks now, versus waiting to reboot, run sudo udevadm trigger.

Last, make sure to tell timedatectl that you plan to use ntpd, and make sure that systemd won't run its own time sychronization:

sudo timedatectl set-ntp true
sudo systemctl disable systemd-timesyncd.service

Preparing GPSd

The basic idea behind this configuration is that gpsd will receive both text updates from the GPS hat about the current time AND it will watch the pps0 device. Then it will reconcile these two pieces of information and make them available to ntpd. ntpd will also be watching pps0, though this may be overkill in the end. Many "how to" documents try to disable gpsd entirely and get ntpd to do the reconciling, or use some hodge-podge approach, but I had very little / no success with these strategies. ntpd just doesn't seem to do a great job at reconciling the two sources of information itself.

The reason this reconciling is even necessary is that the text data being delivered by gpsd regarding the current time itself takes time to send; 115200 bits per second isn't very fast. Additionally the strings of ASCII text it sends vary in length, so you can't even subtract some calculated offset of time to accurately produce a true GPS time out of it. To truly know within microseconds when a second begins, you need the pps0 leading-edge pulse detection. But pps0 can't provide you the current time, only the detail about when a second begins.

So gpsd will watch both these pieces of information-- the text representation of the current time, and the instant that the time moves on to the next second.

To get your GPS hat sending the right strings at the right speed, first figure out what speed it's currently using. If you've never set it up before, or you're just not sure, start with this:

stty -F /dev/ttyAMA0 raw 9600 cs8 clocal -cstopb
cat /dev/ttyAMA0

If you're getting sane looking text, a whole bunch of lines that start with $GP, great! If not, try other numbers in place of 9600, like 19200, 38400, 57600, or 115200. Once you land on the right settings, you should start seeing sane output.

If you are on any speed other than 115200, do this:

echo -en '$PMTK251,115200*1f\r\n' > /dev/ttyAMA0
stty -F /dev/ttyAMA0 raw 115200 cs8 clocal -cstopb
cat /dev/ttyAMA0

You should now be seeing the same text as before, but faster. If you are getting gibberish, go back to the first step, figure out what speed the hat is using, and try again.

Before you install gpsd, you must install pps-tools to get the all-important timepps.h header file which enables the use of the kernel PPS device. You should also install libncurses-dev so you can use gpsmon.

apt-get install pps-tools
apt-get install libncurses-dev

Now we need to install gpsd. From several sources, I have read (and experienced) that the prebuilt gpsd package is both old and flaky, so we want to build it from scratch. Head on over to [http://download-mirror.savannah.gnu.org/releases/gpsd/] and download the latest version; e.g. for gpsd-3.25.zip:

wget http://download-mirror.savannah.gnu.org/releases/gpsd/gpsd-3.25.zip
unzip gpsd-3.25.zip
cd gpsd-3.25
scons gpsd_user=gps gpsd_group=gps
sudo scons udev-install
sudo adduser --system gps --group
sudo usermod -a -G dialout gps
sudo systemctl enable gpsd

Note that we're passing the options gpsd_user and gpsd_group to scons so that gpsd will drop root permissions and then run as the user gps, which we're creating with adduser much like we did for ntpd. (There's a bug in the gpsd documentation at the time of this writing which indicates the option is gpsd-user rather than gpsd_user.)

Next, configure gpsd. Edit /etc/default/gpsd and make it look like this:

# Default settings for the gpsd init script and the hotplug wrapper.

# Start the gpsd daemon automatically at boot time
START_DAEMON="true"

# Use USB hotplugging to add new USB devices automatically to the daemon
USBAUTO="false"

# Devices gpsd should collect to at boot time.
# They need to be read/writeable, either by user gpsd or the group dialout.
DEVICES="/dev/ttyAMA0 /dev/pps0"

# Other options you want to pass to gpsd
GPSD_OPTIONS="-n -D 2"

This tells gpsd not to look for a USB GPS device, and that you want it to read from ttyAMA0 (the serial UART) and from pps0 (the pulse-per-second device). The -n option tells gpsd to begin polling GPS immediately, without waiting for any client to connect. -D 2 elevates the debug logging slightly without being too verbose, in case troubleshooting is needed later.

Configuring ntpd

First, make sure DHCP on your Pi won't ever overwrite the ntp.conf with sudo rm -f /etc/dhcp/dhclient-exit-hooks.d/ntp /etc/dhcp/dhclient-exit-hooks.d/timesyncd and disable the systemd timesync service: sudo systemctl disable systemd-timesyncd.

Speaking of janky ways of configuring things (I really can't stand systemd), ntpd's configuration mechanism goes above and beyond the call of stupid. If you want to ntpd to pull information from sources other than other actual ntp servers, it uses magical server "addresses" that start with 127.127. These aren't real servers, obviously. These are a massive kludge to tack on capabilities to ntpd without making configuration files harder to parse but easier to read. Here is an example working ntp.conf, and I'll discuss what each of these options actually means because so few explanations are ever provided by "how to" guides.

This goes in /etc/ntp.conf:

tos mindist 0.5

restrict default limited kod nomodify notrap noquery nopeer
restrict source  limited kod nomodify notrap noquery

restrict 127.0.0.1
restrict ::1
restrict 192.168.1.0 mask 255.255.255.0 nomodify notrap nopeer

# GPS PPS consolidated with ASCII output
server 127.127.28.2 minpoll 4 maxpoll 4 true iburst prefer
fudge 127.127.28.2 flag1 1 refid GPPS stratum 1

driftfile /var/log/ntpstats/ntp.drift

You also need to create the directory for the drift file if you haven't already:

sudo mkdir /var/log/ntpstats
sudo chown ntpd:ntpd !$

The first line tells ntpd to tolerate a source being up to half a second off from another source. The reason we want this is that the GPS text that arrives on ttyAMA0 could be up to half a second off from the pulse on pps0. We still want to know about this time for the sake of seeing it when we make NTP queries, even though we're not going to consider it authoritative. It may not even be necessary, and we could potentially skip that source entirely, but I've found it useful for troubleshooting.

The batch of restrict lines tells ntpd what its default security behavior should be, that the local host should be allowed to do anything, and that my local LAN should be able to run queries but not modify the server's behavior.

Then we get to the interesting part with the magical addresses. After the first two magic octets come a driver number followed by a unit number. So in this configuration file, we have driver number 22, unit 0 as well as driver number 28 units 0 and 2. These driver numbers and unit numbers are somewhat documented in the official NTP docs and gpsd docs.

For gathering the GPS time consolidated with the PPS driver information, we need Driver 28. Driver 28 is the "shared memory" driver and its units are 0 for the GPS time text, 1 for the pps-corrected GPS time, and 2 for the pps-corrected GPS time, but world-readable.

So we set up the two Driver 28 units. Setting flag1 to 1 tells the driver to disregard the timing difference between the local clock and GPS; this is pretty handy when setting up the Pi for the first time when your clock may be radically off, or if you're not using a battery in your GPS hat. The refid shows up in ntp queries, and stratum 1 is what we're all striving for here, isn't it.

Lastly, Driver 28 unit 2 is given preferential treatment for the source of the time- this is the pps-corrected GPS time from gpsd, so we really do want to believe it. iburst allows a quicker convergence of the correct time when the system starts up.

Now if everything is working, you should be able to service ntpd start and within a minute or so, see an ntpq -pn display that looks like this:

     remote           refid      st t when poll reach   delay   offset  jitter
==============================================================================
*127.127.28.2    .GPPS.           0 l    7   16  377    0.000    0.001   0.002

Bonus: advertising the server to LAN clients

I also wanted my LAN clients to be told about the ntp server when they got a DHCP address. It turns out to be straightforward with my Asus router running AsusWRT-Merlin. In the Administration / System Menu, set Enable JFFS custom scripts and configs to 'Yes', set Enable SSH to LAN Only, and allow password login to Yes.

If you don't have "Enable JFFS..." it may already be enabled; try ssh-ing in (see below) and run nvram set jffs2_scripts=1 && mkdir /jffs/configs

Then ssh admin@192.168.1.1, cd /jffs/configs and echo dhcp-option=42,192.168.1.2 > dnsmasq.conf.add, chmod a+rx /jffs/configs/dnsmasq.conf.add then service restart_dnsmasq. Replace 192.168.1.2 with the static address of your Raspberry Pi, and whenever clients get a DHCP address, they'll be told the NTP server's address is option 42, the time server.

@spatula75
Copy link
Author

spatula75 commented Jan 19, 2020

Once I'm feeling braver, it may be possible to skip udev symlinks and skip recompiling ntpd completely by just setting up GPS time synchronization in gpsd, then pointing ntpd at Driver 28 unit 2 exclusively. It's entirely likely that nothing else is actually required to get microsecond-accurate GPS time into ntpd, but I need to do some more experimenting to be sure. It's plausible that this doesn't need to be anywhere near as complicated as people have been making it with gpsd doing the heavy lifting.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment