The Raspberry Pi is an inexpensive ARM7-based single-board computer that runs Linux. Using it, together with an almost-equally-inexpensive  GPS receiver module from Adafruit Industries, I was able to set up a reasonably good NTP server for my home network. While the hardware side was almost ridiculously easy, the software required a bit of effort, including building a custom kernel and building ntpd from sources. Full details after the jump.

Caveats

There are a few things I should mention up front, in the hopes of not wasting anyone's time:

  • I am not the first person to use a Raspberry Pi as a GPS-based time source with PPS. What I'm doing here is just taking hard work that a bunch of other smart people did and collecting in one place for convenience. Please see the "Credits" section.
  • If you just need a local time source with minimal work, this is far from a "plug it in and it goes" solution. You'd almost certainly be better-served by an appliance product such as the Veracity VTN-TN.
  • If you have a reliable, always-on high-speed Internet connection, consider just using machines in the NTP server pool as your time source.
  • If all you care about is keeping the system time correct on a "human scale" (i.e., within a second or so), then there are much easier ways to do it, like a USB GPS dongle. (For starters, you won't have to compile any software from source or hack any hardware.)
  • This project requires building a custom kernel. If you aren't comfortable doing that (or don't know what that means) then you'll probably be happier and learn more if you start with something easier.
  • You'll also have to do some soldering. It isn't hard, but this isn't really a good first soldering project either.
  • Never hack hardware you can't afford to destroy. Any time you connect something to the GPIO header on your Raspberry Pi, there's the potential to wreck it if you screw up.

Parts List

With the exception of the last three items, that's just the basics you'll probably want to run the Raspberry Pi for any purpose.

Sourcing and Substitutions

The Raspberry Pi itself is getting easier to obtain, and newer Model B units come with 512KB 512MB of RAM (instead of the 256KB 256MB on my rev 1.0). Newer board revisions also have various other improvements. Get the newest one you can, but double-check the pin assignments on the GPIO header. I got my through Newark, but at the time of this writing they're out of stock. Check around, and don't pay much more than the $35.00 MSRP.

Model A boards are said to be starting to appear. They should work fine for this, but if you take this option you'll need to provide your own USB Ethernet adapter (and possibly a self-powered USB hub).

Obviously you can use whatever enclosure you like, or none at all. (My workbench is cluttered with enough conductive objects that running caseless would end badly for me.) You'll need access to the GPIO headers, so make sure you get a case that provides for that, or which you can easily modify.

There's a lot of flexibility in choosing an SD card. 4GB would be plenty of space. You might be able to get away with 2GB if you made a concentrated effort to keep things small, but at the price differential between 2GB and 4GB is under a dollar, so why bother? Very early Pi releases had a bootloader which would choke on Class 10 cards; this has long since been fixed. There's a detailed discussion of SD card choice including a compatibility list with specific models on the Pi Wiki. The usual advice about getting a name-brand card from a reputable vendor applies.

Any power supply that provides stable, clean 5VDC at 700ma or more should work. The Pi Wiki has a compatibility list. Don't go cheap on this component unless you're prepared to put a scope on the output and verify that the output is clean and in-spec while under load. Unstable power will cause intermittent problems.

If you have to go further than your junk box to source a USB or Ethernet cable, this may not be the best project for you.

The Adafruit Ultimate GPS Breakout board (based on the GlobalTop MTK3339 chipset) is a good value, works great and requires no fancy interfacing thanks to 3.3V outputs. It does require a small amount of soldering, but it's all easy through-hole stuff. Try to get the revision 3 board, as it brings the PPS output to the breakout header. (You can add a PPS output to earlier revisions, but it requires soldering a jumper wire to a relatively tiny surface-mount pinout.)

You can get cheaper GPS modules, but remember that 1) you will not get better than about ±100ms accuracy without a PPS output, and 2) the Pi GPIO interface (including the serial UART) requires 3.3V inputs, meaning you'll need level-shift circuitry if your GPS has 5V outputs.

SERIOUS WARNING: The Pi GPIO pins have no over-voltage protection. If you put more than 3.3V across them, you will wreck your Pi.

USB GPSen can be made to work, but once again you'll need a PPS output to get any real accuracy. This  will almost certainly require hardware hacking (very likely literal, as in "with a hacksaw").

The CR1220 coin battery is backup power for the GPS module, so it can "warm start" when the system powers up. It is optional, but if you have it, you'll get a fix (and PPS pulses) within a few seconds of power-up rather than a few minutes.

There are plenty of ways to connect the GPS module to the Pi. I chose the Schmartboard jumpers because they're easy to use and non-permanent. (Note that these jumpers are sold in a package of ten. You need five individual jumpers -- half a package. Not five packages.)

Hardware Assembly

Put your Pi in its enclosure. Make sure you will be able to connect jumpers to GPIO pins P1-04, P1-06, P1-08, P1-10 and P1-12. If you hold the Pi with the component side toward you and the yellow composite video output toward the top, the GPIO pins are in the top left corner. There are two rows of 13 pins each on the GPIO header. The ones you need are all on the row closest to the long edge of the Pi. Within that row, they're on the end close to the corner. See the pinout diagram.)

With the Adafruit Pi Box, I had to cut a small notch using a scroll saw.

Assemble the GPS breakout: Solder on the battery holder and headers. (Note that short end of the header pins gets inserted through the back of the board! I wasn't paying attention and put it on the component side on the first try, then had to de-solder it...) Insert the CR1220 battery.

Do not connect the GPS to the Pi yet. It probably wouldn't hurt anything, but since the serial UART on the Pi is configured as the system console by default, it would be prudent to do some of the software changes first. (Also, if your Pi works fine during setup then stops once you hook up the GPS, you'll have a better idea of where you went wrong.)

OS Installation and Setup

I started with a stock Raspbian "Wheezy" image from the Pi Foundation. Any reasonable distribution should work, but you'll need to make appropriate substitutions (in package manager commands, file locations and kernel sources) if you take a different option.

The Pi Wiki has detailed instructions for downloading an OS image and setting up your SD card.

Go through basic configuration and get connected to the Internet. Instructions for setup and first-time config are can be found on h2g2.

Once you have completed the initial configuration, everything else can be done from the command line via SSH. If you wish, you can disconnect the console from your Pi and work from another machine on your network.

Update your system and packages to the latest version:

sudo apt-get update
sudo apt-get upgrade
sudo apt-get dist-upgrade
sudo apt-get autoclean

Turning Off the Serial Console

Important: In this section, I talk about a serial port on the Pi, and indeed that's exactly what I mean. However, it is absolutely not the familiar EIA  RS-232-C kind of serial port. A garden-variety serial port uses ±15V signalling, which will kill your Pi. Even TTL-level adapters that use 5V are out. The Pi wants 3.3V serial levels ONLY.

In Raspbian, the default is to use the Pi's serial UART as a console. This is handy, because you can use it to watch the printk output from the kernel, before the system comes up into user-space. But for this project, we need to use the serial port for another purpose: reading NMEA sentences from (and sending commands to) the GPS.

Fortunately, this is really easy: Edit /boot/cmdline.txt to remove the text console=ttyAMA0,115200 . (Aside: Whenever I say "edit a file", please take it as read that I mean "make a backup copy of that file, then edit the original".)

Here is what my cmdline.txt looks like after the edit:

dwc_otg.lpm_enable=0 console=tty1 root=/dev/mmcblk0p2 rootfstype=ext4 elevator=deadline rootwait

Once you have made this change, shut down the Pi and remove power.

Connecting the GPS

With the power to the Pi disconnected, use jumpers to connect the following (all listed with Pi side first, then "to", then the GPS side).

  • P1-01 (+3.3V power) to >VIN (supply voltage)
  • P1-06 (Ground) to GND
  • P1-08 (UART0_TXD) to >RX (serial data in)
  • P1-10 (UART0_RXD) to <TX (serial data out)
  • P1-12 (GPIO18) to PPS (pulse-per-second output)

Double-check your wiring.

Boot up the Pi and confirm things seem to be working normally.

Verify you are receiving 9600 baud NMEA sentences on serial port /dev/ttyAMA0. You can do this with a terminal program like minicom, or by something like:

sudo stty -F /dev/ttyAMA0 9600
cat /dev/ttyAMA0

(Press ^C to exit.) You should see output not unlike the following:

$PGTOP,11,2*6E
$GPGGA,222249.000,4116.6600,N,12905.2500,E,2,11,0.75,29.9,M,-23.6,M,0000,0000*6F
$GPGSA,A,3,31,30,11,20,01,23,22,32,16,25,14,,1.51,0.75,1.32*02
$GPGSV,3,1,12,31,71,003,34,32,54,318,30,30,44,164,24,48,36,237,30*7B
$GPGSV,3,2,12,14,31,070,31,20,27,316,23,01,27,270,32,22,26,143,17*7F
$GPGSV,3,3,12,16,18,180,23,25,18,045,33,11,14,248,20,23,06,289,18*77
$GPRMC,222249.000,A,4116.6600,N,12905.2500,E,0.01,304.60,140213,,,D*74
$GPVTG,304.60,T,,M,0.01,N,0.02,K,D*3A

(The specific numbers you see will be different. I have redacted my location data in the above for privacy.) If you stuff like the above, it means your GPS module is working, and proves that everything is hooked up right (with the possible exception of the PPS output).

You may see a lot of empty fields:

$PGTOP,11,2*6E
$GPGGA,235948.799,,,,,0,0,,,M,,M,,*4E
$GPGSA,A,1,,,,,,,,,,,,,,,*1E
$GPGSV,1,1,01,14,,,32*7C
$GPRMC,235948.799,V,,,,,0.00,0.00,050180,,,N*47
$GPVTG,0.00,T,,M,0.00,N,0.00,K,N*32

If so, that means your GPS doesn't have a fix yet. That's normal when you first power it on. Give it about five minutes and see if it changes. (If you installed the CR1220 battery, subsequent power-ups will give you a fix much faster.)

If you still get output with lots of empty fields (like the second example) after the GPS has been powered up for a few minutes, you may be in a location where you're not getting a good signal. (If you're indoors and in a multi-story building and/or have a metal roof, that's likely the case.) Relocate the GPS or try an external antenna.

The $PGTOP sentence is a proprietary GlobalTop extension. You won't see it if you're using another brand of GPS module, but that's OK -- you don't need it.

Compiling a Custom Kernel

The problem with the stock kernel (for the purposes of this project) is that it doesn't provide any way for user-space programs to use the PPS signal from the GPS module in a sufficiently low-latency way. Sure, we could poll the GPIO pin and see the PPS signal, but what we really want is for it to cause an interrupt, then have a low-level interrupt handler make a note of exactly when that happened, and finally pass that information into user-space in a useful way.

Fortunately, there's already a standard PPS API (for getting PPS information into user-space), a Linux implementation of ditto, and a kernel driver for GPIO-based PPS sources in the mainline 3.6.x kernel sources. All we really need to do is to enable that driver, and add a tiny bit of initialization code to tell it which GPIO pin is the PPS interrupt source. But, that still means building our own kernel from sources.

(Update: If you really don't want to do this part, I've created an archive containing my kernel and related files. It is a 47MB bzipped tarball. Download it, unpack it in an empty temporary directory on your Pi, then skip to the "Transferring Updates to the Pi" section. Instead of the source locations I talked about in that section, get the files from where you unpacked the tarball. It's really better to build your own, though, as I don't plan on doing regular updates to this one...)

The Kernel Compilation page on the Pi Wiki is a great resource for this section. Please look there if you have trouble or need a more general process. I've tried to distill it down to the specific steps that worked for me.

This step will go a whole lot faster if you have another Linux box (like a desktop PC or laptop) that's much faster than the Pi. It's OK if it's a totally different architecture -- we can cross-compile.

The directions below are assuming your build machine is running Ubuntu 12.04.1LTS 64-bit Server, and is connected to the Internet. (Things should be the same or nearly so on any recent-vintage Ubuntu. If you're using another distro, you may need to do some translation.)

Get the Cross-Development Tools

We need a set of development tools that will allow us to build programs on our big speedy system that can run on the Pi. (We're going through this hassle because building a kernel takes hours and hours on the Pi. It does work, though.)

The good news is that there are pre-built packages for everything we need in the default repositories, and the package manager make it all easy to install:

sudo apt-get install gcc-arm-linux-gnueabi make ncurses-dev

Get the Kernel Sources

At the time of this writing, platform support for the Pi wasn't yet in the mainline kernel sources. There are lots of ways to get to a working kernel, but you'll probably experience the least friction if you stick with something close to what stock Raspbian is using. For now, at least, that means using the rpi-3.6.y branch from GitHub:

wget 'https://github.com/raspberrypi/linux/archive/rpi-3.6.y.tar.gz'
tar xvzf rpi-3.6.y.tar.gz
mv linux-rpi-3.6.y linux

Your kernel sources should already be in a pristine state, but make sure. cd to the top level of the kernel sources (if you aren't there already) and run the command

make mrproper

(Note that "make mrproper" removes any kernel configuration that's present. If you "make mrproper" again later, remember you'll have to do all the configuration stuff again.)

Get the Running Configuration

Rather than configure the kernel from scratch, we'll use the kernel already running on the Pi as a starting place. The configuration file used to build the running kernel is compiled in to the kernel itself, and is accessible via the /proc filesystem. To use it:

  1. Log in to the Pi.
  2. gunzip -c /proc/config.gz >config
  3. Copy config to .config (note the leading dot!) in the top level of the kernel sources on the machine where you're doing the build.

Figure out the Location of Your Cross-Development Toolchain

The tools used on your build host to build programs to run on the Pi all have a common prefix, in this case arm-linux-gnuabi-. For subsequent steps, we'll need to know this prefix and also the full path to the cross-development tools. To save time and typing, it's handy to put this information in an environment variable.

To find out where your cross-tools are:

which arm-linux-gnueabi-gcc

This should give you an answer like "/usr/bin/arm-linux-gnueabi-gcc". Assign this value to the environment variable CCPREFIX, omitting the trailing gcc (but leaving the trailing dash), like this:

export CCPREFIX='/usr/bin/arm-linux-gnueabi-'

If you're going to be doing a lot of cross-development, it might be handy to put the above command in your profile, or in a file you can source.

 Apply the Configuration

Configure your kernel sources using the config file you pulled from the running kernel on the Pi:

make ARCH=arm CROSS_COMPILE=${CCPREFIX} oldconfig

Note: Normally, the above command will complete without interaction. However, if the kernel you're building is not exactly the same version as the one running on your Pi, you may be asked additional configuration questions at this point. If you don't know how to answer, the best advice I can offer is 1) go on the Pi and do a "sudo apt-get update" and a "sudo apt-get dist-upgrade" to see if there is a newer kernel that's closer to the one you're building, or 2) use the default suggestions.

Make Necessary Configuration Changes

One of the reasons we're building a new kernel is that we need a feature that isn't available in the stock Raspbian kernels. To enable this feature, we'll need to modify the configuration. There are several ways to do this, but the easiest is probably the interactive menu-based config editor:

make ARCH=arm CROSS_COMPILE=${CCPREFIX} menuconfig

To make the necessary changes:

  1. pps-configWhen the menu appears, use the arrow keys to scroll down to Device Drivers then hit Enter.
  2. Scroll down to PPS Support then hit Enter again.
  3. Press 'M' to enable PPS support as a module.
  4. Scroll down to PPS client using GPIO.
  5. Press 'M' to enable the GPIO PPS client as a module.
  6. Verify that your screen looks like the screenshot to the right (click to enlarge):
  7. Hit the right arrow then Enter to exit up to the previous menu. Do so twice more.
  8. When asked if you wish to save, hit Enter to select Yes.

Note that you can also build the PPS support into the kernel directly (by hitting 'Y' instead of 'M' in steps 3 and 5, above). Doing so will make your kernel a little larger (and slightly slower to boot) but you won't have to worry about loading a module before you can use the PPS signal. The general policy in Raspbian seems to be to make everything possible a module, so I'm following their lead

Register a Platform Device as a PPS Source

In order for the PPS support in the kernel to do us any good, we need to define the source of the PPS signal. To accomplish this, we must edit the board-specific platform file for the Pi (arch/arm/mach-bcm-2708/bcm2708.c) and make three changes:

  1. Include the header linux/pps-gpio.h (to get typedefs for some data structures we'll need).
  2. Define a static data structure that specifies the GPIO pin we want to use and how exactly to use it.
  3. Call the bcm_register_device() function, passing in a pointer to the data structure from (2), to add the PPS source.

Here is a context diff of the changes:

--- bcm2708.c.0	2013-02-15 11:08:40.497566132 -0600
+++ bcm2708.c	2013-02-15 12:34:58.782693974 -0600
@@ -55,6 +55,7 @@
 #include <mach/system.h>

 #include <linux/delay.h>
+#include <linux/pps-gpio.h>

 #include "bcm2708.h"
 #include "armctrl.h"
@@ -64,6 +65,19 @@
 #include <linux/broadcom/vc_cma.h>
 #endif

+/* PPS-GPIO platform data */
+static struct pps_gpio_platform_data pps_gpio_info = {
+	.assert_falling_edge = false,
+	.capture_clear= false,
+	.gpio_pin=18,
+	.gpio_label="PPS",
+};
+
+static struct platform_device pps_gpio_device = {
+	.name = "pps-gpio",
+	.id = -1,
+	.dev = { .platform_data = &pps_gpio_info },
+};

 /* Effectively we have an IOMMU (ARMVideoCore map) that is set up to
  * give us IO access only to 64Mbytes of physical memory (26 bits).  We could
@@ -708,6 +722,7 @@
 	bcm_register_device(&bcm2708_vcio_device);
 #ifdef CONFIG_BCM2708_GPIO
 	bcm_register_device(&bcm2708_gpio_device);
+	bcm_register_device(&pps_gpio_device);
 #endif
 #if defined(CONFIG_W1_MASTER_GPIO) || defined(CONFIG_W1_MASTER_GPIO_MODULE)
 	platform_device_register(&w1_device);

You can download a copy of the above patch if that's easier than cutting-and-pasting. Or, you can download my original and patched versions of the bcm2708.c file. (If you are using my pre-patched file, please make sure the original version I started from matches your original. Future kernels may have other updates to this file that you don't want to lose.)

Note that I chose to use GPIO pin 18 merely because it was adjacent to the other pins I was using to connect my GPS module. You can certainly choose a different pin if you have a reason to do so.

Compile the Kernel

Go back to the top level of the kernel source tree, and build the kernel:

make ARCH=arm CROSS_COMPILE=${CCPREFIX} -j5

(The above example is for a four-core system. Replace the "5" with the number of cores you have plus one. If you prefer a single-threaded build, just omit the "-j5" entirely.)

The build process will take a few minutes or a few tens of minutes depending on your system.

Install the Modules

You'll also need the modules that go with the kernel you just built. Since we're not building on the system where this kernel is going to run, we'll need to install the modules to an alternate location:

make ARCH=arm CROSS_COMPILE=${CCPREFIX} INSTALL_MOD_PATH=~mylogin/temp_modules modules_install

Where ~mylogin/temp_modules is the directory where you want the modules installed. This should be an absolute path to a directory that does not already exist, and you should have write permission in the parent directory.

Obtaining the GPU Firmware and Libraries

You'll also want to get the GPU firmware, libraries and utilities (such as vcgencmd) that match the new kernel you've built. To obtain them:

wget 'https://github.com/raspberrypi/firmware/archive/next.tar.gz'
tar xvzf next.tar.gz

Transferring Updates to the Pi

At this point, you'll need to transfer a number of files and directories from your build host to the Pi. For all of the following cases, if there is an existing item of the same name already on the Pi, you'll need to delete it or (much better) move it aside.

  • The kernel itself: from linux/arch/arm/boot/zImage to /boot/kernel.img
  • The modules directory: from ~mylogin/temp_modules/lib/modules/3.6.11 to /lib/modules/3.6.11
  • The kernel firmware directory: from ~mylogin/temp_modules/lib/firmware to /lib/firmware . Note that if you have any custom firmware (such as for a wireless network adapter), you'll need to copy it back into the new /lib/firmware directory.
  • Bootloader-related files:
    • firmware-next/boot/bootcode.bin to /boot/bootcode.bin
    • firmware-next/boot/fixup.dat to /boot/fixup.dat
    • firmware-next/boot/start.elf to /boot/start.elf
  • GPU-related files: from firmware-next/hardfp/opt/vc to /opt/vc

Note that the above assumes your kernel version is 3.6.11. If not, your modules subdirectory will have a different name, and you must adjust the instructions accordingly.

Once all of the above have been transferred, run sync a couple times then reboot your Pi.

Verifying Kernel, GPU Utils and PPS Input

We just made some major changes, so it's a good time to make sure everything is working. Here's a minimum list of things I'd suggest you check -- feel free to add more :

  • Does the Pi boot up and can you log in?
  • Does uname -a show the kernel version you expect?
  • Do you see anything in the output of dmesg that looks like an error or warning? (Note that lots of people get warnings about "missed completion of command 18" from the SD card DMA code. That is probably harmless and in any case is unlikely to have anything to do with the changes you just made.)
  • Try vcgencmd measure_temp. It should give you temperature output similar to temp=50.2'C. If you get an error from this command, suspect a problem with the stuff in /opt/vc/.

If all seems well, try loading the pps_gpio module:

sudo modprobe pps_gpio

Once you have done so, the output of lsmod should include a couple of lines like:

Module                  Size  Used by
pps_gpio                2124  0
pps_core                7254  1 pps_gpio

(Don't worry if the sizes you see are different from the above. Also, if you built PPS and PPS GPIO support directly into the kernel rather than as modules, you don't need to do the modprobe, nor will you see pps_gpio or pps_core in the modules list.)

Run dmesg. The output should include lines like:

[   89.551595] pps_core: LinuxPPS API ver. 1 registered
[   89.551622] pps_core: Software ver. 5.3.6 - Copyright 2005-2007 Rodolfo Giometti <giometti@linux.it>
[   89.559630] pps pps0: new PPS source pps-gpio.-1
[   89.559696] pps pps0: Registered IRQ 188 as PPS source

(The timestamps will differ in your output. You should see these lines regardless of whether you built PPS support as a module or as an intrinsic part of the kernel. If you chose a different GPIO pin, the IRQ number you see will not match the example.)

Finally, you should see a new device special file /dev/pps0:

crwxrwxrwt 1 root tty 248, 0 Feb 15 14:30 /dev/pps0

If the PPS support is present and connected to an interrupt, the next thing to check is if PPS pulses are being received from the GPS module. To verify:

sudo apt-get install pps-tools
sudo ppstest /dev/pps0

This should produce output similar to:

trying PPS source "/dev/pps0"
found PPS source "/dev/pps0"
ok, found 1 source(s), now start fetching data...
source 0 - assert 1360961986.896233919, sequence: 1777 - clear  0.000000000, sequence: 0
source 0 - assert 1360961987.896296264, sequence: 1778 - clear  0.000000000, sequence: 0
source 0 - assert 1360961988.896357502, sequence: 1779 - clear  0.000000000, sequence: 0
source 0 - assert 1360961989.896418637, sequence: 1780 - clear  0.000000000, sequence: 0

(Press ^C to exit.) Your sequence numbers and timestamps will be different from the example, but the timestamps should be very close to one second apart, from each line to the next.

Loading Modules and Creating Device Symlinks on Boot

We want to use the GPS NMEA stream and PPS input with ntpd, which we want to start automatically at boot time. There are a couple of minor obstacles to this: First, we need to make sure the necessary modules get loaded without manual intervention (unless you're not using modules, in which case: never mind). Second, ntpd expects its input to come from device files with very specific names (which don't match the defaults).

These are both easy problems to solve.

To ensure a module is loaded at boot time, just append its name (on a line by itself, without the ".o") to the file /etc/modules. Example command:

sudo sh -c 'echo pps_gpio >>/etc/modules'

To create symlinks in /dev from the default device files to the names ntpd expects, create a new file /etc/udev/rules.d/09-pps.rules with contents as follows:

KERNEL=="ttyAMA0", SYMLINK+="gps0"
KERNEL=="pps0", OWNER="root", GROUP="tty", MODE="0777", SYMLINK+="gpspps0"

Once you have done both of the above, reboot the Pi. When it comes back up, check that the modules are loaded and that you have symlinks called /dev/gps0 and /dev/gpspps0.

Building ntpd with PPS Support

The stock ntpd (4.2.6p5) at the time of this writing does not appear to have support (or at least not working support) for PPS input. So, we'll have to build our own from source.

First, remove the stock NTP package if it is installed:

sudo apt-get remove ntp

Next, install the development libraries and header for Linux capabilities support:

sudo apt-get install libcap-dev

This package will allow us to build an ntpd which can do things like access the PPS ticker and slew the system clock without having to run as root.

Then, download the NTP sources from the official web site. There are two choices: production (older but more stable) and development (newer but less well-tested and potentially flaky). I opted for the development version (4.2.7p354 at the time of this writing), but that is a questionable choice.

Unpack the source archive and cd into the directory containing the source. Configure as follows:

./configure --enable-linuxcaps --with-NMEA --with-ATOM

This enables support for the aforementioned libcap stuff, reading NMEA sentences from the GPS module, and reading PPS pulses via the PPS API.

Once configuration is complete, build and install:

make
sudo make install

This will install the NTP utilities (including ntpd) under /usr/local/.

Configuring and Running ntpd

The system-provided script for launching ntpd needs some minor edits to work with ntpd installed in /usr/local/bin instead of /usr/sbin. In the file /etc/init.d/ntp, add /usr/local/bin to the beginning of the PATH, and set DAEMON to /usr/local/bin/ntpd. For reference, I have provided a copy of my /etc/init.d/ntp file. (If you're downloading the linked copy, remember to make it owned by root and to give it execute permission.)

You'll also need to configure ntpd via the file /etc/ntpd.conf. Here is what I am using:

driftfile /var/lib/ntp/ntp.drift
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 0.debian.pool.ntp.org iburst
server 1.debian.pool.ntp.org iburst
server 2.debian.pool.ntp.org iburst
server 3.debian.pool.ntp.org iburst

server 127.127.20.0 mode 16 prefer
fudge 127.127.20.0 flag1 1 time2 0.400

restrict -4 default kod notrap nomodify nopeer
restrict 127.0.0.1

(If you prefer, there is also a copy of the above available for direct download.) Most of that's pretty standard stuff, but note that we allow queries from anywhere; add "noquery" to the first restrict line if that's not what you want. Since the stock Raspbian kernel doesn't do IPv6, I've omitted IPv6 configuration from the config file. Also, the following lines deserve further explanation:

server 127.127.20.0 mode 16 prefer
fudge 127.127.20.0 flag1 1 time2 0.400

Any server with a numeric IP address where the first two octets are 127.127 is given special treatment by ntpd. The third octet is interpreted as a code specifying a driver for a locally-connected time source. A list of the available drivers can be found on the Reference Clock Support page -- the "Type" codes listed near the bottom of the page are the supported values for the third octet. Type "20" is the generic NMEA driver.

The fourth octet specifes a unit number. The driver looks for /dev/gpsN and /dev/gpsppsN where N is the unit number (in our case, 0).

The mode number specifies the GPS port speed and which NMEA sentences should be recognized. The value used above (16) means 9600 baud, all supported sentences. See the Generic NMEA GPS Receiver page for full documentation of the mode values (and the fudge values on the following line).

On the fudge line, "flag1 1" means to enable PPS signal processing. (The default is to disable it.)

The "time2" value on the fudge line is the average time from the "real" top of the second to the time the first recognized NMEA sentence for that second is fully received. The reasons for needing to know this are complicated, but they boil down to this: The NMEA stream tells ntpd what second it is, and the PPS pulse tells ntpd exactly when that second starts. If they're too far apart, ntpd doesn't trust that it knows which second the PPS pulse goes with. The time2 offset is there to give it a hint.

The real explanation, quoting from Juergen Perlinger on support.ntp.org:

And now the truth about the PPS locking: actually only the sub-second part of the difference between PPS time stamp and (receive time stamp - fudge time2) is evaluated and checked. If this difference is less than 400ms or bigger than 600ms (which is equivalent to -400ms) the receive time stamp will be substituted with the properly adjusted PPS time stamp. This might sound a bit complicated, but if you have a device that sends the data before the associated PPS pulse, you can use the proper negative value for fudge time2 and compensate for that behaviour.

Finally, to launch ntpd:

sudo /etc/init.d/ntp start

This should produce output that looks like:

[ ok ] Starting NTP server: ntpd.

Verifying Correct Operation

Make sure that the ntpd process is running. If not (or even if it is), check the end of /var/log/daemon.log for errors or warnings.

Check that ntpd has the /dev/gps0 and /dev/gpspps0 device files open:

sudo lsof /dev/gps0 /dev/gpspps0

This should produce output similar to:

COMMAND  PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
ntpd    3899  ntp    5u   CHR 204,64      0t0    8 /dev/ttyAMA0
ntpd    3899  ntp    6u   CHR  248,0      0t0 3127 /dev/pps0

(You will likely see different PIDs and node numbers in your output.)

Finally, try the following command:

ntpq -p

You should see output something like the following:

     remote           refid      st t when poll reach   delay   offset  jitter
==============================================================================
oGPS_NMEA(0)     .GPS.            0 l   42   64  377    0.000   -0.010   0.004
-paladin.latt.ne 216.171.124.36   2 u   43   64  377   63.209   -1.125   3.375
*216.45.57.38    69.25.96.13      2 u   60   64  377   45.596    1.644   0.623
+echelon.no-such 209.81.9.7       2 u   46   64  377   56.903    1.915   1.314
+bindcat.fhsu.ed 132.163.4.103    2 u   62   64  377   44.217    1.354   0.546

The specific values and hostnames you see will differ. The important things are:

  1. Is there a row where the "remote" is GPS_NMEA(0)? If so, that means that the NMEA driver is in use.
  2. On that row, is the "reach" value something greater than 0? If it is not 377, does it increase if you wait a few minutes?  (The "reach" value is the octal representation of an 8-bit vector of the most recent attempts to the source for that row. A "1" bit represents a success. The least-significant bit is the most recent attempt.)
  3. On that row, is the first character a 'o' (lower-case O as in Oscar)? That means that the PPS source is being used. Note that it may take a couple minutes to reach this state; it is normal to see a '*' when you first start ntpd. An 'x' is bad.
  4. After ntpd has been running  for a few hours, is the "jitter" value on that row a small number? (The number is in milliseconds. You should expect jitter to converge on a value that's 10µs or less. In other words, 0.010 or less in the ntpq output.)

Other Notes

There are a some obvious opportunities for improvements here:

  • By default, the GPS module sends a number of sentences which aren't needed for time synchronization. Worse still, some of these (notably GPGSV) vary in length according to the number of satellites overhead (and the position and signal strength of same). The GPGGA sentence is recognized by the ntpd NMEA driver and seems to always precede GPGSV, so it doesn't cause major problems, but it still seems like it would be a good idea to disable the un-needed sentences, and to use only the shortest ntpd-supported one (GPRMC?).
  • Routing the NMEA data through gpsd would make it available for other applications on the system while (in theory, anyway) not compromising accuracy. I made some attempts at this, but the stock gpsd (3.6) doesn't seem to handle the PPS stuff properly (or, I'm not using it properly).
  • It would be nice to get email or SNMP trap notification of various problems. Examples include poor GPS signal strength, long time to first fix after power-up and excessive jitter.
  • Adding an additional time source such as a Chronodot, DS1307 or WWV receiver would be a nifty hack.
  • Mounting the GPS module inside the Pi case and using non-temporary wiring would make things more tidy.
  • The Ethernet interface on the Pi is actually a USB device, driver via polling. That means there's significant built-in jitter.  I've found this amounts to about 50µs 500µs on my local network. Not bad, but not great either. It might be possible to get better timing properties by adding an ENC28J60-based interface to the SPI bus on the Pi. That's approaching the point where "start with a different platform" is a whole lot less work, though.

I've also identified some things that seem useful but actually aren't:

  • Increasing the port speed is tempting, but probably counter-productive. (A missed or garbled sentence is much worse that one that arrives a few milliseconds sooner or later.)
  • The GPS module is capable of updating at up to 10Hz, but increasing the update rate won't help. Only the first update during a given second will be considered by ntpd.

An interesting thought experiment: How do you tell time (at all, to say nothing of accurately) if there's no GPS signal, no WWV and no Internet? One thing that comes to mind is that you can use an accelerometer to measure "down", a flux-gate compass to find "North" and then use an aimable optical (or RF) sensor to measure the position of the sun relative to down and North...

Credits

Thanks to the following for their hard work, which I just put together in a more-or-less obvious way:

Edited 20130218 DGH: Corrected figure for jitter on local Ethernet.
Edited 20130218 DGH: Added missing Credits section.
Edited 20130511 DGH: Corrected Pi RAM sizes per HR.
Edited 20130520 DGH: Added note about custom firmware per HR.
Edited 20130526 DGH: Fixed broken download links.
Edited 20130526 DGH: Added downloadable kernel archive.