- Raspberry Pi 2 – Model B – ARMv7 with 1G RAM
- Adafruit Ultimate GPS HAT for Raspberry Pi A+ or B+ Mini Kit
- SMA to uFL/u.FL/IPX/IPEX RF Adapter Cable
- GPS Antenna – External Active Antenna – 3-5V 28dB 5 Meter SMA
- CR1220 12mm Diameter – 3V Lithium Coin Cell Battery
- Micro USB cable – A/MicroB – 3ft
- USB Power Supply
- 32GB MicroSD Card
- Raspberry Pi 2 Case
Total cost for this project is around $130. I had a number of these items already, so the cost was considerably less.
OS and Setup
After acquiring the needed materials, you will need to setup your Raspberry Pi and install an OS. I chose Raspbian, any modern linux distro that’ll run on the Pi will do.
This guide focuses on the Pi2 however all the steps are the same for the original Pi. You will need to use a different header with the GPS HAT, and you have fewer GPIO pins to work with for the HAT, but it does not effect GPS.
One of the first steps you will need to do is to ensure that nothing else is using the UART. By default, raspbian has a console configured on ttyAMA0, you will need to disable it by removing ‘console=ttyAMA0,115200’ from /boot/cmdline.txt
apt-get install pps-tools
echo "dtoverlay=pps-gpio,gpiopin=4" >> /boot/config.txt echo "pps-gpio" >> /etc/modules
To test things out, we are going to install some GPS software
apt-get install gpsd gpsd-clients
Then run the following to see if GPS is working
After letting it run for a while, you should get a 3D fix.
Modify /etc/default/gpsd and change START_DAEMON to false. We do not want gpsd starting, as it will prevent NTP from opening it directly.
#!/bin/bash DEVICE=$1 ( # Only output /bin/echo -e "\$PMTK314,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0*29\r\n" # Output once a second /bin/echo -e "\$PMTK220,1000*1F\r\n" # Sample once a second /bin/echo -e "\$PMTK300,1000,0,0,0,0*1C\r\n" # Set baud to 115200 /bin/echo -e "\$PMTK251,115200*1F\r\n" ) > $DEVICE # Set Port speed to 115200 /bin/stty -F $DEVICE raw 115200 cs8 clocal -cstopb
chmod +x /usr/local/bin/init-gps.sh
Create a new file in /etc/udev/rules.d called 99-gps.rules with the following
KERNEL=="pps0",SYMLINK+="gpspps0",GROUP="dialout", MODE="0660" KERNEL=="ttyAMA0", SYMLINK+="gps0",GROUP="dialout", MODE="0660",RUN+="/usr/local/bin/init-gps.sh /dev/%k"
Reboot your pi and lets ensure things load properly.
Check that /dev/gps0 and /dev/gpspps0 exist.
Check that GPS is outputting $GPZDA data once a second.
should give you something like this
$GPZDA,165236.000,12,11,2015,,*56 $GPZDA,165237.000,12,11,2015,,*57 $GPZDA,165238.000,12,11,2015,,*58 $GPZDA,165239.000,12,11,2015,,*59 $GPZDA,165240.000,12,11,2015,,*57 $GPZDA,165241.000,12,11,2015,,*56 $GPZDA,165242.000,12,11,2015,,*55
You should NOT have extra line breaks between the sentences, if you do it will mess up NTP timing.
We want to use GPZDA and not GPRMC or other sentence types as they are the shortest and faster to parse for just time and thus will reduce latency. The difference isn’t much, but it’s something and since NTP will be holding the device exclusively, there is no point in getting navigational data.
In theory, you should be able to use gpsd with a shared memory segment, and rely on PPS to keep things in sync, but I haven’t tested this and I don’t need GPS navigational data for my NTP server.
The first thing you need to NTP with a build in NMEA driver with PPS support. To get that, you must recompile NTP with ATOM enabled.
Ensure your deb-src repos are enabled in /etc/apt/source.list
Install the necessary packages
apt-get build-dep ntp
Get the source
apt-get source ntp
Modify debian/rules and add –-enable-ATOM to the configure call.
Increase the version number in debian/changelog to be just below the next version using “~” (e.g. change 4.2.6.p5+dfsg-2 to 4.2.6.p5+dfsg-3~pps1).
Compile the package
Install the package
dpkg -i ntp_4.2.6.p5+dfsg-7+deb8u2~pps1_armhf.deb
Run the following to add NTP to dialout group so it has permissions to our devices
usermod -G dialout ntp
If DHCP is enabled, remove “ntp-servers” from the “request” line in /etc/dhcp/dhclient.conf
Remove /var/lib/ntp/ntp.conf.dhcp if present
Before we can configure NTP for how we will ultimately use it, we first need to use NTP to calculate the delay between GPS and PPS signals. This delay occurs because of processing of the GPS chip, the Pi, antenna wire length, etc. If we do not do this, then NTP will often times never get a PPS lock, or will mark our GPS source as a false ticker, or will simply be off a few hundred milliseconds.
First step is to change the CPU Governor from the default of ‘ondemand’ to ‘performance.
echo 'performance' > /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
For calibration it’s critical or frequency changes will affect jitter. I recommend you add the above to /etc/rc.local so that’s applied on boot, but that’s up to you.
Create a calibration /etc/ntpd.conf.
server 127.127.20.0 mode 88 minpoll 3 maxpoll 3 iburst prefer true fudge 127.127.20.0 refid GPS server 127.127.22.0 minpoll 3 fudge 127.127.22.0 flag1 1 flag3 1 refid PPS restrict -4 default kod notrap nomodify nopeer noquery restrict -6 default kod notrap nomodify nopeer noquery restrict 127.0.0.1 restrict ::1
ntpd -g -u root
# ntpq -pn
In this calibration ntp config file, we use two time sources a NMEA (20) driver and an ATOM (22) driver. We will use the differences in offset and jitter between these two to calculate the delay needed for the mode2 value in our final config file.
Where did the magic value of mode 88 come from you ask? 80 tells NTP to talk to /dev/gps0 using a baud rate of 115200. The 8 indicates to use $GPZDA and not $GPRMC or other NMEA sentences.
You should eventually get something like the below. Rerun until both sources are getting data.
remote refid st t when poll reach delay offset jitter ============================================================================== *127.127.20.0 .GPS. 0 l 1 8 17 0.000 5.757 22.346 o127.127.22.0 .PPS. 0 l - 8 3 0.000 414.437 0.946
If instead of an ‘o’ or ‘*’ you get some other status such as ‘x’, that’s ok for this test. It just indicates that NTP wont use it as a time source. For the sake of calibration, we don’t care we just want to know the approximate difference in offsets. The important part here is the value for ‘reach’ indicating it’s communicating and it is updating ‘offset’ for both sources.
Cheat Sheet for NTP Server Status
o = pps peer * = sys peer # = too distant + = selected x = false ticker - = discarded
We can calculate from the above that the difference in offsets between GPS and PPS at this time is (414.437 – 5.757) plus or minus a jitter of (22.346 + 0.946) or 408.68 +/ 23.292. This gives us a best guess mode2 value between 385.388 and 431.972. If we rerun ntpq -pn for a while, we can start to get an idea of what this value should be.
NTP will tolerate mode2 being off by as much as 400ms across samples before it throws out PPS, but GPS signal can hop around quite erratically as well so it’s best to get as close to this value as possible.
I ran mine for a few hours and logged the values to a file and picked a dozen or so samples with low jitter for both servers and came up with a value of 452.
We will also need to take into account leap seconds, adjusting our clock as necessary. Create the following shell script, and install it into “/usr/local/bin/leap-seconds.sh”:
#!/bin/sh cd /etc/ntp wget -q ftp://time.nist.gov/pub/leap-seconds.list &> /dev/null service ntp restart &> /dev/null
Then, create an entry in root’s crontab(5):
0 0 31 6,12 * /usr/local/bin/leap-seconds.sh
Make sure it’s executable:
chmod a+x /usr/local/bin/leap-seconds.sh
Modify /etc/ntp.conf to resemble below
driftfile /var/lib/ntp/ntp.drift leapfile /etc/ntp/leap-seconds.list #statsdir /var/log/ntpstats/ statistics loopstats peerstats clockstats filegen loopstats file loopstats type day enable filegen peerstats file peerstats type day enable filegen clockstats file clockstats type day enable server 127.127.20.0 mode 88 minpoll 3 maxpoll 3 iburst prefer fudge 127.127.20.0 flag1 1 flag2 0 flag3 1 time1 0.0 time2 0. refid GPS pool us.pool.ntp.org iburst restrict -4 default kod notrap nomodify nopeer noquery restrict -6 default kod notrap nomodify nopeer noquery restrict 127.0.0.1 restrict ::1
Replace with the time2 value you calculated from above and then restart ntpd.
service ntp restart
After a minute or so you should get something like the following
remote refid st t when poll reach delay offset jitter ============================================================================== o127.127.20.0 .GPS. 0 l 1 8 3 0.000 0.002 0.001 +126.96.36.199 127.67.113.92 2 u 2 64 1 40.306 0.943 0.055 188.8.131.52 184.108.40.206 2 u 1 64 1 81.439 1.836 0.076 220.127.116.11 18.104.22.168 2 u 2 64 1 40.201 0.597 0.026 22.214.171.124 126.96.36.199 2 u 1 64 1 46.196 -2.844 5.787
If instead of a ‘o’ you have a ‘*’ next to GPS source, you need to give ntp more time to get PPS lock OR you didn’t recompile NTP with PPS support. Mine usually gets a lock within a minute, however on rare occasion it never gets lock. I can only assume this is because of timings and they happen to be far enough off that lock never happens. Stopping ntp and starting it again results in a PPS lock within a minute.
# ntpq -c rl associd=0 status=041d leap_none, sync_uhf_radio, 1 event, kern, version="ntpd firstname.lastname@example.org Mon Nov 9 22:24:25 UTC 2015 (1)", processor="armv7l", system="Linux/4.1.12-v7+", leap=00, stratum=1, precision=-20, rootdelay=0.000, rootdisp=0.168, refid=GPS, reftime=d9ef5275.e7aa703a Thu, Nov 12 2015 10:01:25.904, clock=d9ef527b.73e87700 Thu, Nov 12 2015 10:01:31.452, peer=39586, tc=3, mintc=3, offset=0.001, frequency=-5.331, sys_jitter=0.002, clk_jitter=0.000, clk_wander=0.001
We are a stratum 1 server, serving a stratum 0 device with a precision of 953.67 nanoseconds (2^-20 seconds) and a jitter of 2 microseconds!
Now that we think we have everything setup correctly, start a capture
while [ 1 ]; do ntpq -pn | head -n 3 | tail -n 1; sleep 1; done >> gps.log
After 24 hours or so, run the following to generate a CSV file. Use this to graph your results using excel, google docs or ploty.
cut -c 71- gps.log | sed 's/^ \+//' > data.csv
Compare them to this graph