/*
* 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();
}
Comments