How to build a stratum 1 NTP server with GPS time reference

Since a reliable time reference is crucial to any network, we were looking for replacement hardware for our failing time servers which were strata 2+ anyway. Nowadays GPS synchronized NTPs are the way to go, and as GPS receivers have become reasonably cheap, we decided to follow this route too. For the GPS receiver, we chose the Garmin GPS 18x LVC since it sports the highly precise PPS signal and a serial interface. In Switzerland it can be ordered at http://www.gps-shop.ch/ with the usual premium profit margin compared to other countries (CHF 145.- vs. USD 60.-). Our original idea was to use a alix2c3 board from PC Engines for the time server, but it turned out that the AMD Geode companion chip on this device does not implement the RS232 handshare lines we need for the PPS signal. We then settled on the alix1c that we have come to love in earlier projects and that also has a fully equipped serial port. There's a nice website explaining in detail how to connect the GPS to the serial port. The author suggests an LED to indicate the presence of the PPS signal. This is a great idea, but we would like to show the PPS not on a hardware level, but directly from the SHM (shared memory) where ntpd fetches it from. We therefore connected two transistor-driven LEDs to the alix GPIO port (pins 13 and 14, GP21 and 22) and use a small C daemon to read the SHM and control the LEDs: gpsheartbeat.c - compile it with gcc -o gpsheartbeat gpsheartbeat.c and copy it to /usr/local/bin/

/*
 * gpsheartbeat.c - PPS heartbeat on PC Engines alix 1c status LEDs
 * (c) 2008 daduke <daduke@daduke.org>
 *
 */

 #include <assert.h>
 #include <getopt.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <sys/io.h>
 #include <sys/ipc.h>
 #include <sys/shm.h>
 #include <sys/types.h>
 #include <syslog.h>
 #include <time.h>
 #include <unistd.h>

static int addr = 0x2E;
 #define REG (addr)   /* Extended Function Enable Registers */
 #define VAL (REG+1) /* Extended Function Data Register */
 #define LED1ON 0x02
 #define LED2ON 0x04

unsigned short state;       //which LED is on
unsigned short second;        //second from GPS
unsigned short oldSecond;     //second from last iteration
volatile struct shmTime *p;     //pointer to shared memory structure

struct shmTime {
  int    mode; /* 0 - if valid set
          *       use values,
          *       clear valid
          * 1 - if valid set
          *       if count before and after read of values is equal,
          *         use values
          *       clear valid
          */
  int    count;
  time_t clockTimeStampSec;
  int    clockTimeStampUSec;
  time_t receiveTimeStampSec;
  int    receiveTimeStampUSec;
  int    leap;
  int    precision;
  int    nsamples;
  int    valid;
};

struct shmTime * getShmTime (int unit) {
  int shmid=shmget (0x4e545030+unit, sizeof (struct shmTime), IPC_CREAT|0777);
        if (shmid==-1) {
                perror ("shmget");
                exit (1);
        } else {
                struct shmTime *p=(struct shmTime *)shmat (shmid, 0, 0);
                if ((int)(long)p==-1) {
                        perror ("shmat");
                        p=0;
                }
                assert (p!=0);
                return p;
        }
}

void loop() {
  second = p->receiveTimeStampSec;      //what time is it??
  if (second != oldSecond) {
    if (state) {
      outb_p(0xF1, REG); /* select CRF1 */
      outb_p(LED1ON, VAL); /* turn on LED1 */
      state = 0;
    } else {
      outb_p(0xF1, REG); /* select CRF1 */
      outb_p(LED2ON, VAL); /* turn on LED2 */
      state = 1;
    }
    oldSecond = second;
  }

  usleep(10000);
}

int main(int argc, char *argv[]) {
  if (argc<=1) {
    printf ("\ninvoke with %s -d to run in daemon mode\n\n",argv[0]);
  }

  iopl(3);          //allow outb
  outb_p(0x87, REG); /* Enter extended function mode */
  outb_p(0x87, REG); /* Again according to manual */

  outb_p(0x07, REG); /* point to logical device number reg */
  outb_p(0x08, VAL); /* select logical device 8 (GPIO2) */
  outb_p(0x30, REG); /* select CR30 */
  outb_p(0x01, VAL); /* set bit 0 to activate GPIO2 */

  outb_p(0xF0, REG); /* select CRF0 */
  outb_p(0x00, VAL); /* set GPIO2 to output*/

  p=getShmTime(1);  //populate shared memory structure

  static int ch, daemon;
  pid_t pid, sid;

  daemon = 0;
  while ((ch = getopt(argc, argv, "d")) != -1) {
      switch (ch) {
                case 'd':
                daemon = 1;
                break;
                }
      }
  state = 0;
  oldSecond = 61;         //should be different from anything we get from GPS
  if (daemon) {
    printf ("entering daemon mode\n");
    pid = fork();

    if (pid < 0) {
      exit(EXIT_FAILURE);
    } else if (pid > 0) {
      exit(EXIT_SUCCESS);
    }

    umask(0);

    sid = setsid();

    if (sid < 0) {
      exit(EXIT_FAILURE);
    }

    if ((chdir("/")) < 0) {
      exit(EXIT_FAILURE);
    }

    syslog (LOG_NOTICE, " started by User %d", getuid ());

    while (1) {
      loop();
    }

    exit(EXIT_SUCCESS);
  } else {
    printf ("running in foreground\n");
    while (1) {
      loop();
    }
  }
}

/etc/init.d/gpsheartbeat is then used to run the daemon at system startup:

 #!/bin/sh

 ### BEGIN INIT INFO
 # Provides:        gpsheartbeat
 # Required-Start:  $gpsd
 # Required-Stop:   $gpsd
 # Default-Start:   2 3 4 5
 # Default-Stop:    0 1 6
 # Short-Description: Start GPS PPS heartbeat daemon
 ### END INIT INFO

PATH=/sbin:/bin:/usr/sbin:/usr/bin

. /lib/lsb/init-functions

NAME=gpsheartbeat
DAEMON=/usr/local/bin/gpsheartbeat

test -x $DAEMON || exit 5

RUNASUSER=root
UGID=$(getent passwd $RUNASUSER | cut -f 3,4 -d:) || true

case $1 in
        start)
                log_daemon_msg "Starting gpsheartbeat daemon" "gpsheartbeat"
                if [ -z "$UGID" ]; then
                        log_failure_msg "user \"$RUNASUSER\" does not exist"
                        exit 1
                fi
                start-stop-daemon --start --quiet --oknodo --exec $DAEMON -- -d
                log_end_msg $?
                ;;
        stop)
                log_daemon_msg "Stopping gpsheartbeat server" "gpsheartbeat"
                start-stop-daemon --stop --quiet --oknodo --exec $DAEMON
                log_end_msg $?
                ;;
        restart|force-reload)
                $0 stop && sleep 2 && $0 start
                ;;
        *)
                echo "Usage: $0 {start|stop|restart}"
                exit 2
                ;;
esac

In the distribution of our choice all necessary software to get a GPS-synchronized ntpd going is gpsd. It doesn't have a config file, but dpkg-reconfigure gpsd asks the right questions. ntpd needs some lines of configurations in /etc/ntp.conf:

 # use GPS to get time
server 127.127.28.0 minpoll 4 maxpoll 4
fudge 127.127.28.0 time1 -0.170 refid GPS

server 127.127.28.1 minpoll 4 maxpoll 4 prefer
fudge 127.127.28.1 refid PPS

The first two lines add the regular GPS time reference, which is not very precise (tens of ms). Only the PPS (last two lines) provides sub-ms accuracy. There's one last catch before the alix is ready to serve time: there's a well-known problem that prevents the board from booting when no monitor is attached. The solution, as suggested, is to connect pins 5 and 12 of the VGA port on the alix. Works like a charm.

What to do if ntp does not get a GPS signal

sometimes after a restart ntp does not receive the PPS pulse, hence ntp won't sync and the LEDs don't blink. Problem is that shared memory is somehow screwed up. Remedy:

/etc/init.d/gpsheartbeat stop
/etc/init.d/ntp stop
/etc/init.d/gpsd stop
ipcs

the last command shows all registered shared memory segments. We have to delete the stuck ones:

ipcrm -M 0x4e545031

of course you'll have to use the correct numbers from the ipcs command. Then:

/etc/init.d/gpsd start
/etc/init.d/ntp start
/etc/init.d/gpsheartbeat start