mikeolson
Published © Apache-2.0

Metric Clock

Keep time in decidays, centidays, millidays, 100-microdays..

IntermediateWork in progress202
Metric Clock

Things used in this project

Hardware components

Arduino UNO
Arduino UNO
×1
Adafruit DS3231 Precision RTC breakout
×1
Adafruit white-on-blue 16x2 LCD with potentiometer, header
×1
Resistor 10k ohm
Resistor 10k ohm
×1
Adafruit 40 jumper wires, M/M
×2
HiLetgo GY-NEO6MV2 NEO-6M GPS
×1

Hand tools and fabrication machines

Soldering iron (generic)
Soldering iron (generic)
Solder Wire, Lead Free
Solder Wire, Lead Free
Breadboard, Plain
Breadboard, Plain

Story

Read more

Schematics

Breadboard image

Code

metric_clock_GPS_RTC.ino

C/C++
Source code
/*
 *  Metric Clock: Keep track of the elapsed 100-microday units since midnight, and display both
 *  that four-digit (metric) time and the hh:mm time.
 *
 *  Uses a DS3231 RTC for accurate time measurement and a NEO6M GPS to set the clock.
 */

#include <DS3231.h>           // DS3231 RTC
#include <LiquidCrystal.h>    // LCD display
#include <SoftwareSerial.h>   // How we talk to the NEO6M GPS receiver
#include <TinyGPS++.h>        // NEO6M GPS receiver

// Arduino digital pins for the LCD display
#define RS 12
#define EN 11
#define D4 4
#define D5 5
#define D6 6
#define D7 7
LiquidCrystal lcd(RS, EN, D4, D5, D6, D7);

// Arduino digital pins for the NEO6M GPS receiver. "Transmit" and "receive" are from the point of
// view of the component. The Arduino transmits to the GPS' receiver, and vice versa.
#define NEO6M_RX    10
#define NEO6M_TX    9
#define NEO6M_BAUD  9600
SoftwareSerial ss(NEO6M_RX, NEO6M_TX);
TinyGPSPlus gps;

// should be settable by some config interface, but for now: California, summertime!
#define MY_UTC_OFFSET -7

DS3231 myRTC;

// We count ticks in an interrupt handler with 1.024kHz frequency from the RTC.
// Volatile variables are modified in the interrupt handler.
volatile uint16_t uday_ticks, min_ticks, half_sec_ticks;
volatile bool five_microdays_elapsed, half_sec_elapsed, min_elapsed;
uint16_t count_five_microdays_elapsed, count_half_secs_elapsed;

// Arduino interrupts are hardwired to pins 2 (INT0) and 3 (INT1). RTC low INTR pin wired to 2.
// You need a 10k omh pull-up resistor on that pin for the low interrupt to work.
#define RTC_SQW_PIN 2

/*
 *  This interrupt handler is called every 1.024 msec. It counts ticks and manages some boolean
 *  variables to tell the loop hander when five microdays, half a second or a full minute have
 *  passed.
 */
void myRTCIntrHdlr()
{
  // Five microdays is 432 msec. At 1.024kHz that's 442 ticks.
  if (++uday_ticks == 442)
  {
    uday_ticks = 0;
    five_microdays_elapsed = true;
  }
  
  // half a second is 512 ticks
  if (++half_sec_ticks == 512)
  {
    half_sec_ticks = 0;
    half_sec_elapsed = true;
  }

  // a minute is 1024*60 ticks at 1.024kHz
  if (++min_ticks == 61440)
  {
    min_ticks = 0;
    min_elapsed = true;
  }
}

/*
 * showtime() -- show the current date and time, metric and traditional, on the LCD display.
 */
 
void showtime() {
  DateTime rightNow;
  float secs_since_midnight; // 86,400 is too many for the 16-bit int types on the Arduino Uno
  unsigned int udays_hundred_since_midnight;
  uint16_t hud, md, cd, dd; // hundred-microday units, millidays, centidays, decidays
  uint16_t hours_ten, hours_one, minutes_ten, minutes_one;
  int year, month, day;
  
  // Read the real-time clock -- accurate to 2ppm
  rightNow = RTClib::now();

  // first line of the display gets the current date
  year = rightNow.year();
  month = rightNow.month();
  day = rightNow.day();
  lcd.setCursor(3,0);
  lcd.print(year);
  lcd.print("/");
  lcd.print(month / 10);
  lcd.print(month % 10);
  lcd.print("/");
  lcd.print(day / 10);
  lcd.print(day % 10);

  // We want to convert the current HH:MM:SS to the number of 100-microday intervals since midnight.
  // A hundred microdays is 8.64 seconds. We turn current time into seconds since midnight, then
  // convert that to 100-microday count, then bust that up for display so that we get leading zeroes
  // our our output. We do the "seconds" calculation in floats because otherwise we overflow the
  // sixteen-bit integer types on the Arduino Uno. The number of 100-microday units in a day is
  // 10,000, and that fits comfortably in the unsigned integer we use after dividing by 8.64.

  secs_since_midnight = (rightNow.hour() * 3600.0) + (rightNow.minute() * 60.0)
                        + (float) rightNow.second();
  udays_hundred_since_midnight = (unsigned int) (secs_since_midnight / 8.64);
  hud = udays_hundred_since_midnight % 10;
  md = (udays_hundred_since_midnight / 10) % 10;
  cd = (udays_hundred_since_midnight / 100) % 10;
  dd = (udays_hundred_since_midnight / 1000) % 10;

  lcd.setCursor(0, 1);
  lcd.print(dd);
  lcd.print(cd);
  // skip a space for the flashing decimal point
  lcd.setCursor(3, 1);
  lcd.print(md);
  lcd.print(hud);
  
  hours_ten = rightNow.hour() / 10;
  hours_one = rightNow.hour() % 10;
  minutes_ten = rightNow.minute() / 10;
  minutes_one = rightNow.minute() % 10;
  
  lcd.setCursor(11,1);
  lcd.print(hours_ten);
  lcd.print(hours_one);
  // skip a space for the flashing colon
  lcd.setCursor(13, 1);
  lcd.print(minutes_ten);
  lcd.print(minutes_one);
}

/*
 * setRTCfromGPS() -- what it says on the label.
 *
 * This routine is a placeholder until I ingegrate a full-featured library that can handle time zone
 * conversion, DST and leap year calculations. I do a minimal job of all that now. This only gets
 * called when we start up, to set the clock for the first time. It should also be called periodically
 * to resync the clock when the specified drift might affect correctness of the display. The DS3231
 * can drift 2ppm, or 2 microdays per day, so we should sync every fifty days.
 */

void setRTCfromGPS()
{
  bool dateSet = false, timeSet = false;
  int year, month, day, hour, minute, second, centisecond;
  int new_hour, new_day, new_month, new_year;
  int month_days[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
  
  lcd.clear();
  lcd.setCursor(1,0);
  lcd.print("Acquiring GPS");
  
  while (!dateSet || !timeSet)
  {
    while (ss.available() > 0)
    {
      gps.encode(ss.read());
      if (gps.date.isUpdated())
      {
        Serial.println("Got date.");
        year = gps.date.year();
        month = gps.date.month();
        day = gps.date.day();
        dateSet = true;
      }
      if (gps.time.isUpdated())
      {
        Serial.println("Got time.");
        hour = gps.time.hour();
        minute = gps.time.minute();
        second = gps.time.second();
        centisecond = gps.time.centisecond();
        timeSet = true;
      }
    }
  }
  
  Serial.println("We're done.");

  /*
   * Should use the Timezone_Generic library here but it's really huge for an Arduino Uno.
   * We're going to correct the GPS time, which is in UTC, with the hard-coded timezone offset.
   */
  new_hour = hour + MY_UTC_OFFSET;
  new_day = day;
  new_month = month;
  new_year = year;
  if (new_hour > 24)
  {
    // roll forward one day
    hour = new_hour % 24;
    new_day = day + 1;
    
    if (new_day > month_days[month - 1])
    {
      // roll forward one month. ignores leap years.
      new_day = 1;
      new_month = month + 1;
      if (new_month > 12)
      {
        new_month = 1;
        new_year = year + 1;
      }
    }
  }
  else if (new_hour < 0)
  {
    // roll back a day
    new_hour += 24;
    new_day = day - 1;
    if (new_day < 1)
    {
      // roll back a month. ignores leap years.
      new_month = month - 1;
      if (new_month < 1)
      {
        new_month = 12;
        new_year = year - 1;
      }
      new_day = month_days[new_month - 1];
    }
  }
  hour = new_hour;
  day = new_day;
  month = new_month;
  year = new_year;
  
  Serial.print(year);
  Serial.print("/");
  Serial.print(month);
  Serial.print("/");
  Serial.print(day);
  Serial.print(" ");
  Serial.print(hour / 10);
  Serial.print(hour % 10);
  Serial.print(":");
  Serial.print(minute / 10);
  Serial.print(minute % 10);
  Serial.print(":");
  Serial.print(second / 10);
  Serial.print(second % 10);
  
  // set RTC time. the DS3231 assumes a base year of 2000.
  myRTC.setClockMode(false); // 24-hour mode
  myRTC.setYear(year - 2000);
  myRTC.setMonth(month);
  myRTC.setDate(day);
  myRTC.setHour(hour);
  myRTC.setMinute(minute);
  myRTC.setSecond(second);
}

void setup() {

  // Debug output to Arduino IDE console
  Serial.begin(9600);
  
  // How we talk to the clock
  Wire.begin();
  
  // 16x2 LCD display
  lcd.begin(16, 2);
  
  // how we talk to the GPS
  ss.begin(NEO6M_BAUD);

  // We're starting our counter at time t0
  uday_ticks = half_sec_ticks = min_ticks = 0;
  min_elapsed = half_sec_elapsed = five_microdays_elapsed = false;
  count_five_microdays_elapsed = 0;
  count_half_secs_elapsed = 0;
  
  // set the real-time clock from GPS
  setRTCfromGPS();

  // Okay, fire up the 1.024kHz oscillator and start handling interrupts. 
  attachInterrupt(digitalPinToInterrupt(RTC_SQW_PIN), myRTCIntrHdlr, FALLING);
  myRTC.enableOscillator(true, true, 1); // 1.024kHz, see header file

  lcd.clear();

  showtime();
}

/*
 *  The loop routine keeps track of some boolean variables that get set in the interrupt handler,
 *  and manages some counters. It handles the flashing of the decimal point in the metrick time,
 *  and the colon on the hh:mm time, directly. It also notices whether either of those two time
 *  displays needs to change, and calls showtime() if so.
 *
 *  I instrumented this program separately. The loop routine gets called between 12 and 16 times per
 *  millisecond, with an occasional outlier as low as 10 times per millisecond. Most of the time,
 *  it does nothing, since we need a lot of milliseconds to get to 5 microdays, half a second or
 *  a full second.
 */

void loop() {
  bool doshowtime = false;

  // If another 5 microdays have elapsed, we have work to do.
  if (five_microdays_elapsed)
  {
    five_microdays_elapsed = false;
    
    // Flash the colon sign on the clock.
    lcd.setCursor(8, 0);
    if (++count_five_microdays_elapsed & 0x01)
    {
        // Odd
        lcd.print(".");
    }
    else
    {
      // Even, it's been ten microdays. Clear the decimal point on the display.
      lcd.print(" ");
      
      // If it's been 100 microdays, we need to update the time on the display.
      if (count_five_microdays_elapsed == 20)
      {
        count_five_microdays_elapsed = 0;
        doshowtime = true;
      }
    }
  }
    
  if (half_sec_elapsed)
  {
    half_sec_elapsed = false;
    
    // flash the colon
    lcd.setCursor(8, 1);
    if (++count_half_secs_elapsed == 1)
    {
      lcd.print(":");
    }
    else
    {
      // been a full second
      count_half_secs_elapsed = 0;
      lcd.print(" ");
    }
  }
  
  if (min_elapsed)
  {
    min_elapsed = false;
    doshowtime = true;
  }
  
  if (doshowtime)
    showtime();
}

Credits

mikeolson

mikeolson

0 projects • 0 followers

Comments