millis() and micros() - interaction with SoftwareSerial?

I'm trying to play with GPS with the idea of making a nice clock. I've configured the Sparkfun GPS shield with RX and TX on pins 3 and 4 for soft serial, and PPS on pin 2 (leaving 0 and 1 for upload and serial monitor debugging). I've also got an AdaFruit LCD shield.

One experiment I've run is to set up an interrupt on RISING PPS. In this routine, I simply set a volatile global variable to the the fractional second portion of micros() (that is, % 1000000). In loop(), I print the value out.

Now, what I expect is that a random number shows up initially, but that it stays fairly consistent over the medium term, changing by one or two after watching it for a while.

Instead, what I see is that a different number shows up every second, several hundred thousand apart, in a descending "sawtooth".

If I change my soft serial to two NC pins, I see something a bit more like I expect. The number decrements by about 1000 every second, so that's still a ms of error per second (seems like a hell of a lot to me), but not like before.

Does SoftwareSerial interfere with millis() and micros()?

Software serial turns interrupts off while sending/receiving a byte.

millis() uses interrupts.

One experiment I've run is to set up an interrupt on RISING PPS ...

And you are using interrupts too.

Where is your sketch? You forgot to put it on.

Well, the sketch is really messy right now. Here are the important (for the moment) parts:

#define PPS_PIN 2
#define PPS_INT 0

#define RX_PIN 4
#define TX_PIN 3
#define GPS_BAUD 4800

#define MICROS_PER_SECOND 1000000

SoftwareSerial gps_port(RX_PIN, TX_PIN);
TinyGPS gps;

void setup() {
  display.setMCPType(LTI_TYPE_MCP23017);
  display.begin(16, 2);
  
  Serial.begin(9600);
  
  pinMode(PPS_PIN, INPUT);
  attachInterrupt(PPS_INT, ppsInterrupt, RISING);
  
  gps_port.begin(GPS_BAUD);
  
  display.setBacklight(WHITE);
  display.print("GPS clock");
  delay(2000);
  display.clear();  
}

volatile unsigned long pps_micros;

void ppsInterrupt() {
  pps_micros = micros() % MICROS_PER_SECOND;
}

void loop() {

   Serial.println(pps_micros);
   delay(500);

}

at least I would change the following to keep the interrupt as short as possible (thereby affecting software serial less)

void ppsInterrupt() 
{
  pps_micros = micros();
}

void loop() 
{
   Serial.println(pps_micros % MICROS_PER_SECOND);
   delay(500);
}

to speed up a bit more you might use Serial.begin(115200);

I gave up on SoftwareSerial, but did wind up with a sub-second accurate GPS clock, which was the goal. Here's the sketch:

#include <Wire.h>
#include <LiquidTWI2.h>
//#include <SoftwareSerial.h>
#include <TinyGPS.h>
#include <Time.h>
#include <Timezone.h>

#define PPS_PIN 2
#define PPS_INT 0
#define RX_PIN 4
#define TX_PIN 3
#define GPS_BAUD 4800

#define LCD_I2C_ADDR 0x20 // for adafruit shield or backpack

LiquidTWI2 display(LCD_I2C_ADDR, 0, 0);
//SoftwareSerial gps_port(RX_PIN, TX_PIN);
#define gps_port Serial
TinyGPS gps;
time_t prevTime = 0; // when the digital clock was displayed
unsigned int prevTenths = 99; // not 0-9
boolean complained = false;
unsigned long last_pps_millis;

/*

For this to work, an extension must be made to the Arduino Time library. This
method's intent is to designate the precise start of a second. It does this by
replacing the prevMillis value saved in the library with the current value of
millis(), but preserving any "owed" updates.

void syncSecond() {
  unsigned long now_millis = millis();
  while (((int)(now_millis - prevMillis)) > 500) { // 500 so we sync to the *nearest* second
    // we're owed at least one update
    now_millis -= 1000;
  }
  prevMillis = now_millis;
}

*/

void pps_interrupt() {
  last_pps_millis = millis();
  syncSecond();
}

void setup() {
  gps_port.begin(GPS_BAUD);
    
  pinMode(PPS_PIN, INPUT);
  attachInterrupt(PPS_INT, pps_interrupt, RISING);
 
  display.setMCPType(LTI_TYPE_MCP23017);
  display.begin(16, 2);
    
  setSyncProvider(gpsTimeSync);
  
  display.setBacklight(WHITE);
  display.print("GPS clock");
 
  delay(2000);
  display.clear();  
}

TimeChangeRule summer = { "PDT", Second, Sun, Mar, 2, -7*60 };
TimeChangeRule winter = { "PST", First, Sun, Nov, 2, -8*60 };
Timezone zone(winter, summer);

time_t gpsTimeSync() {
  unsigned long fix_age = 0;
  gps.get_datetime(NULL, NULL, &fix_age);
  if (fix_age < 2000) {
    unsigned int tenths = ((millis() - last_pps_millis) / 100) % 10;
    tmElements_t tm;
    int year;
    gps.crack_datetime(&year, &tm.Month, &tm.Day, &tm.Hour, &tm.Minute, &tm.Second, NULL, NULL);
    tm.Year = year - 1970;
    time_t out = makeTime(tm);
    if (tenths >= 5) out++; // round to the nearest second given our PPS discipline
    return out;
  }
  return 0;
}

void updateDisplay(time_t Now, unsigned int tenths) {
  tmElements_t tm;
  char buf[16];
  breakTime(Now, tm);
 
  display.setCursor(0, 0);
  sprintf(buf, " %02d:%02d:%02d.%1d %s   ", hourFormat12(Now), tm.Minute, tm.Second, tenths, isPM(Now)?"PM":"AM");
  display.print(buf);
  display.setCursor(0, 1);
  sprintf(buf, "  %2d-%s-%04d   ", tm.Day, monthShortStr(tm.Month), tmYearToCalendar(tm.Year));
  display.print(buf);

}

void loop() {
  while(gps_port.available()) {
    gps.encode(gps_port.read());
  }
  time_t Now = zone.toLocal(now());

  if (timeStatus() != timeNotSet && timeStatus() != timeNeedsSync) {
    unsigned int tenths = ((millis() - last_pps_millis) / 100) % 10;
    if (Now != prevTime || prevTenths != tenths) {
      prevTime = Now;
      prevTenths = tenths;
      complained = false;
      display.setBacklight(GREEN);
      updateDisplay(Now, tenths);
    }
  } else {
    if (!complained) {
      complained = true;
      display.setBacklight(RED);
      display.clear();
      display.print("Waiting for sync");
    }
  }
}

Given that it's using an LCD display, there's certainly no point in attempting to show more resolution than that. The LCD takes so long to physically change that there's almost no point in showing tenths at all.