Skip to content

Commit

Permalink
ts2phc: Support using a GPS radio as the master clock.
Browse files Browse the repository at this point in the history
Many GPS radios provide both a 1-PPS and time of day information via
NMEA sentences.  This patch introduces a ts2phc master that decodes
the "recommended minimum data" sentence, RMC, which provides UTC time
and a validity flag.  Together with the file based leap second table,
this sentence provides adequate time of day for determining the time
of the PPS edge.

Signed-off-by: Richard Cochran <[email protected]>
  • Loading branch information
richardcochran committed May 7, 2020
1 parent 43c51cf commit 7486e6e
Show file tree
Hide file tree
Showing 12 changed files with 687 additions and 3 deletions.
3 changes: 3 additions & 0 deletions config.c
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,9 @@ struct config_item config_tab[] = {
PORT_ITEM_INT("ts2phc.extts_correction", 0, INT_MIN, INT_MAX),
PORT_ITEM_ENU("ts2phc.extts_polarity", PTP_RISING_EDGE, extts_polarity_enu),
PORT_ITEM_INT("ts2phc.master", 0, 0, 1),
GLOB_ITEM_STR("ts2phc.nmea_remote_host", ""),
GLOB_ITEM_STR("ts2phc.nmea_remote_port", ""),
GLOB_ITEM_STR("ts2phc.nmea_serialport", "/dev/ttyS0"),
PORT_ITEM_INT("ts2phc.pin_index", 0, 0, INT_MAX),
GLOB_ITEM_INT("ts2phc.pulsewidth", 500000000, 1000000, 999000000),
PORT_ITEM_ENU("tsproc_mode", TSPROC_FILTER, tsproc_enu),
Expand Down
6 changes: 3 additions & 3 deletions makefile
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@ DEBUG =
CC = $(CROSS_COMPILE)gcc
VER = -DVER=$(version)
CFLAGS = -Wall $(VER) $(incdefs) $(DEBUG) $(EXTRA_CFLAGS)
LDLIBS = -lm -lrt $(EXTRA_LDFLAGS)
LDLIBS = -lm -lrt -pthread $(EXTRA_LDFLAGS)
PRG = ptp4l hwstamp_ctl nsm phc2sys phc_ctl pmc timemaster ts2phc
FILTERS = filter.o mave.o mmedian.o
SERVOS = linreg.o ntpshm.o nullf.o pi.o servo.o
TRANSP = raw.o transport.o udp.o udp6.o uds.o
TS2PHC = ts2phc.o ts2phc_generic_master.o ts2phc_master.o ts2phc_phc_master.o \
ts2phc_slave.o
TS2PHC = ts2phc.o lstab.o nmea.o serial.o sock.o ts2phc_generic_master.o \
ts2phc_master.o ts2phc_phc_master.o ts2phc_nmea_master.o ts2phc_slave.o
OBJ = bmc.o clock.o clockadj.o clockcheck.o config.o designated_fsm.o \
e2e_tc.o fault.o $(FILTERS) fsm.o hash.o interface.o msg.o phc.o port.o \
port_signaling.o pqueue.o print.o ptp4l.o p2p_tc.o rtnl.o $(SERVOS) sk.o \
Expand Down
202 changes: 202 additions & 0 deletions nmea.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
/**
* @file nmea.c
* @note Copyright (C) 2020 Richard Cochran <[email protected]>
* @note SPDX-License-Identifier: GPL-2.0+
*/
#include <malloc.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>

#include "nmea.h"
#include "print.h"

#define NMEA_CHAR_MIN ' '
#define NMEA_CHAR_MAX '~'
#define NMEA_MAX_LENGTH 256

enum nmea_state {
NMEA_IDLE,
NMEA_HAVE_DOLLAR,
NMEA_HAVE_STARTG,
NMEA_HAVE_STARTX,
NMEA_HAVE_BODY,
NMEA_HAVE_CSUMA,
NMEA_HAVE_CSUM_MSB,
NMEA_HAVE_CSUM_LSB,
NMEA_HAVE_PENULTIMATE,
};

struct nmea_parser {
char sentence[NMEA_MAX_LENGTH + 1];
char payload_checksum[3];
enum nmea_state state;
uint8_t checksum;
int offset;
};

static void nmea_reset(struct nmea_parser *np);

static void nmea_accumulate(struct nmea_parser *np, char c)
{
if (c < NMEA_CHAR_MIN || c > NMEA_CHAR_MAX) {
nmea_reset(np);
return;
}
if (np->offset == NMEA_MAX_LENGTH) {
nmea_reset(np);
}
np->sentence[np->offset++] = c;
np->checksum ^= c;
}

static int nmea_parse_symbol(struct nmea_parser *np, char c)
{
switch (np->state) {
case NMEA_IDLE:
if (c == '$') {
np->state = NMEA_HAVE_DOLLAR;
}
break;
case NMEA_HAVE_DOLLAR:
if (c == 'G') {
np->state = NMEA_HAVE_STARTG;
nmea_accumulate(np, c);
} else {
nmea_reset(np);
}
break;
case NMEA_HAVE_STARTG:
np->state = NMEA_HAVE_STARTX;
nmea_accumulate(np, c);
break;
case NMEA_HAVE_STARTX:
np->state = NMEA_HAVE_BODY;
nmea_accumulate(np, c);
break;
case NMEA_HAVE_BODY:
if (c == '*') {
np->state = NMEA_HAVE_CSUMA;
} else {
nmea_accumulate(np, c);
}
break;
case NMEA_HAVE_CSUMA:
np->state = NMEA_HAVE_CSUM_MSB;
np->payload_checksum[0] = c;
break;
case NMEA_HAVE_CSUM_MSB:
np->state = NMEA_HAVE_CSUM_LSB;
np->payload_checksum[1] = c;
break;
case NMEA_HAVE_CSUM_LSB:
if (c == '\n') {
/*skip the CR*/
return 0;
}
if (c == '\r') {
np->state = NMEA_HAVE_PENULTIMATE;
} else {
nmea_reset(np);
}
break;
case NMEA_HAVE_PENULTIMATE:
if (c == '\n') {
return 0;
}
nmea_reset(np);
break;
}
return -1;
}

static void nmea_reset(struct nmea_parser *np)
{
memset(np, 0, sizeof(*np));
}

static int nmea_scan_rmc(struct nmea_parser *np, struct nmea_rmc *result)
{
int cnt, i, msec = 0;
char *ptr, status;
uint8_t checksum;
struct tm tm;

pr_debug("nmea sentence: %s", np->sentence);
cnt = sscanf(np->payload_checksum, "%02hhx", &checksum);
if (cnt != 1) {
return -1;
}
if (checksum != np->checksum) {
pr_err("checksum mismatch 0x%02hhx != 0x%02hhx on %s",
checksum, np->checksum, np->sentence);
return -1;
}
cnt = sscanf(np->sentence,
"G%*cRMC,%2d%2d%2d.%d,%c",
&tm.tm_hour, &tm.tm_min, &tm.tm_sec, &msec, &status);
if (cnt != 5) {
cnt = sscanf(np->sentence,
"G%*cRMC,%2d%2d%2d,%c",
&tm.tm_hour, &tm.tm_min, &tm.tm_sec, &status);
if (cnt != 4) {
return -1;
}
}
ptr = np->sentence;
for (i = 0; i < 9; i++) {
ptr = strchr(ptr, ',');
if (!ptr) {
return -1;
}
ptr++;
}
cnt = sscanf(ptr, "%2d%2d%2d", &tm.tm_mday, &tm.tm_mon, &tm.tm_year);
if (cnt != 3) {
return -1;
}
tm.tm_year += 100;
tm.tm_mon--;
result->ts.tv_sec = mktime(&tm);
result->ts.tv_nsec = msec * 1000000UL;
result->fix_valid = status == 'A' ? true : false;
return 0;
}

int nmea_parse(struct nmea_parser *np, const char *ptr, int buflen,
struct nmea_rmc *result, int *parsed)
{
int count = 0;
while (buflen) {
if (!nmea_parse_symbol(np, *ptr)) {
if (!nmea_scan_rmc(np, result)) {
*parsed = count + 1;
return 0;
}
nmea_reset(np);
}
buflen--;
count++;
ptr++;
}
*parsed = count;
return -1;
}

struct nmea_parser *nmea_parser_create(void)
{
struct nmea_parser *np;
np = malloc(sizeof(*np));
if (!np) {
return NULL;
}
nmea_reset(np);
/* Ensure that mktime(3) returns a value in the UTC time scale. */
setenv("TZ", "UTC", 1);
return np;
}

void nmea_parser_destroy(struct nmea_parser *np)
{
free(np);
}
44 changes: 44 additions & 0 deletions nmea.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* @file nmea.h
* @note Copyright (C) 2020 Richard Cochran <[email protected]>
* @note SPDX-License-Identifier: GPL-2.0+
*/
#ifndef HAVE_NMEA_H
#define HAVE_NMEA_H

#include <stdbool.h>
#include <time.h>

/** Opaque type. */
struct nmea_parser;

struct nmea_rmc {
struct timespec ts;
bool fix_valid;
};

/**
* Parses NMEA RMC sentences out of a given buffer.
* @param np Pointer obtained via nmea_parser_create().
* @param buf Pointer to the data to be parsed.
* @param buflen Length of 'buf' in bytes.
* @param rmc Pointer to hold the result.
* @param parsed Returns the number of bytes parsed, possibly less than buflen.
* @return Zero on success, non-zero otherwise.
*/
int nmea_parse(struct nmea_parser *np, const char *buf, int buflen,
struct nmea_rmc *rmc, int *parsed);

/**
* Creates an instance of an NMEA parser.
* @return Pointer to a new instance on success, NULL otherwise.
*/
struct nmea_parser *nmea_parser_create(void);

/**
* Destroys an NMEA parser instance.
* @param np Pointer obtained via nmea_parser_create().
*/
void nmea_parser_destroy(struct nmea_parser *np);

#endif
96 changes: 96 additions & 0 deletions serial.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/**
* @file serial.c
* @note Copyright (C) 2020 Richard Cochran <[email protected]>
* @note SPDX-License-Identifier: GPL-2.0+
*/
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <termios.h>

#include "print.h"
#include "serial.h"

#define CANONICAL 1

static int open_serial_baud(const char *name, tcflag_t baud, int icrnl, int hwfc)
{
struct termios nterm;
int fd;

fd = open(name, O_RDWR | O_NOCTTY);
if (fd < 0) {
pr_err("cannot open %s : %m", name);
return fd;
}
memset(&nterm, 0, sizeof(nterm));

/* Input Modes */
nterm.c_iflag = IGNPAR; /* Ignore framing errors and parity errors */
if (icrnl) {
/* Translate carriage return to newline on input */
nterm.c_iflag |= ICRNL;
}

/* Output Modes */
nterm.c_oflag = 0;

/* Control Modes */
nterm.c_cflag = baud;
nterm.c_cflag |= CS8; /* Character size */
nterm.c_cflag |= CLOCAL; /* Ignore modem control lines */
nterm.c_cflag |= CREAD; /* Enable receiver */
if (hwfc) {
/* Enable RTS/CTS (hardware) flow control */
nterm.c_cflag |= CRTSCTS;
}

/* Local Modes */
if (CANONICAL) {
nterm.c_lflag = ICANON; /* Enable canonical mode */
}

nterm.c_cc[VTIME] = 10; /* timeout is 10 deciseconds */
nterm.c_cc[VMIN] = 1; /* blocking read until N chars received */
tcflush(fd, TCIFLUSH);
tcsetattr(fd, TCSANOW, &nterm);
return fd;
}

int serial_open(const char *name, int bps, int icrnl, int hwfc)
{
tcflag_t baud;

switch (bps) {
case 1200:
baud = B1200;
break;
case 1800:
baud = B1800;
break;
case 2400:
baud = B2400;
break;
case 4800:
baud = B4800;
break;
case 9600:
baud = B9600;
break;
case 19200:
baud = B19200;
break;
case 38400:
baud = B38400;
break;
case 57600:
baud = B57600;
break;
case 115200:
baud = B115200;
break;
default:
return -1;
}
return open_serial_baud(name, baud, icrnl, hwfc);
}
19 changes: 19 additions & 0 deletions serial.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* @file serial.h
* @note Copyright (C) 2020 Richard Cochran <[email protected]>
* @note SPDX-License-Identifier: GPL-2.0+
*/
#ifndef HAVE_SERIAL_H
#define HAVE_SERIAL_H

/**
* Opens a serial port device.
* @param name Serial port device to open.
* @param bps Baud rate in bits per second.
* @param icrnl Pass 1 to map CR to NL on input, zero otherwise.
* @param hwfc Pass 1 to enable hardware flow control, zero otherwise.
* @return An open file descriptor on success, -1 otherwise.
*/
int serial_open(const char *name, int bps, int icrnl, int hwfc);

#endif
Loading

0 comments on commit 7486e6e

Please sign in to comment.