This document is a short note.

Introduction

Linux can synchronize with GPS time thanks to a PPS signal. The implementation come from the NTP need to synchronize against a GPS ([1], [2]).

We will use the PPS serial line discipline in order to acquire the PPS signal. The DB9's pin that receive the PPS is the DCD (carrier detect, or pin number 1) [3].

The kernel can automatically handle the phase and/or frequency locking [4].

System configuration

On the slave machine (the one that need to be synchronized), you have to check your kernel configuration :

General setup
  Timers subsystem
    Timer tick handling 
      constant rate no dynticks
Device drivers 
  [*] PPS support
    [*] PPS kernel consumer support
    [M] Kernel timer client (Testing client, use for debug)
    [M] PPS line discipline
  [*] PTP clock support

PPS kernel consumer support (CONFIG_NTP_PPS) allow the kernel to handle PPS and reduce jitter compared to userspace handling. As it only works on non-"tickless" systems, you will need to check "constant rate no dynticks".

In order to test the PPS system, we nned the userspace tools (warning : this isn't the genuine tools, this branch implement the kernel consumer control) [5] :

    git clone https://github.com/ago/pps-tools.git

Testing

You can follow the official linuxpps guide [6].

For the test driver :

    modprobe pps_ktimer
    ppstest /dev/pps0
For the serial line discipline test :
    modprobe pps_ldisc
    ldattach PPS /dev/ttyS0
    ppsctl -a -h /dev/pps1

Programming in C

On the slave side

Open a tty file descriptor.
int tty_fd = open("/dev/ttyS0", O_RDWR | O_NOCTTY);
Attach the "line discipline" (see the ldattach implementation in [7] sys-utils/ldattach.c).
#include <sys/ioctl.h>

int ldisc = N_PPS;
ioctl(tty_fd, TIOCSETD; &ldisc);
Open a pps file descriptor.
int pps_fd = open("/dev/pps1", O_RDWR);
Bind the PPS to a "kernel consumer" (see the RFC of the PPS API [8], or in the code of timepps.h [5]).
#include <linux/pps.h>

struct pps_bind_args kc_bind_args;
kc_bind_args.tsformat = PPS_TSFMT_TSPEC;
kc_bind_args.consummer = PPS_KC_HARDPPS;
kc_bind_args.edge = PPS_CAPTUREASSERT;
ioctl(pps_fd, PPS_KC_BIND, &kc_bind_args);
This code will enable the PPS monitoring by the kernel. Tell the kernel to lock phase and frequency on the PPS (see [4] or in the code of ppsctl.c [5]).
#include <sys/timex.h>

struct timex tx;
tx.modes = ADJ_STATUS;
tx.status |= (STA_PPSFREQ | STA_PPSTIME);
adjtimex(&tx);
If you need to adjust the clock, use the adjtimex syscall. The kernel documention contain an exemple with the ADJ_SETOFFSET mode [9] that isn't documented in the adjtimex manual (probably not standard).
#include <sys/timex.h>

#ifndef ADJ_SETOFFSET
#define ADJ_SETOFFSET 0x0100
#endif

struct timex tx;
tx.modes = ADJ_SETOFFSET;
tx.time.tv_sec = offset;
tx.time.tv_usec = 0;
adjtimex(&tx);

On the master side

If you need to test the system, you will probably need to generate a PPS on the DCD. With a null modem cable, you can drive the DTR pin for this purpose.

References