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.