// LM - Hybrid sketch, based on examples for DS3231 real time clock from Adafruit // https://learn.adafruit.com/adafruit-ds3231-precision-rtc-breakout/arduino-usage // and WWVB decoder from: https://www.youtube.com/watch?v=OeZzNehKL_Y&t=3s // // // Lloyd Milligan, January 2017 #include #include "RTClib.h" #include #include const boolean LCD = true; // If true LCD is enabled (always for this application). const boolean SER = false; // If true Serial com is enabled for debugging WWVB part. const boolean SERRTC = false; // If true RTC data are displayed via Serial. boolean resetRTC = false; // Set artificial time into RTC on startup - Normally false! // RTC may acquire nonsense time when testing, or otherwise. // Use this flag to set RTC once, then unset flag and reload. // Note: Forgetting to unset and reload will cause the clock // to revert to the sketch compile time after loss of power. RTC_DS3231 rtc; // Oscillator i2c address is 0x68 (can't be changed) // Serial LCD support shares the i2c bus [Analog pins 4 (SDA) and 5 (SCL)] const int ROWS = 4; // If 16x2 change ROWS to 2 const int COLS = 20; // If 16x2 change COLS to 16 LiquidCrystal_I2C lcd(0x27, COLS, ROWS); const int BACKLIGHTPIN = 2; // Switch backlight on/off (D2) char daysOfTheWeek[7][12] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}; // For RTC to LCD const char SPACE = ' '; String sLCD[4] = {"", "", "", ""}; DateTime rtcNow; // From rtc.now() // From WWVB sketch char str[20]; int nStart, nEnd, nWid; int nLastDecode; int nBytes[6]; int nIndex, nMask; int nMode; int wwvbHour, wwvbMinute, wwvbDay, wwvbYear, wwvbSecond; int nPinState; int keyVal, oldKey, curKey; // LM: Duplicate wwvb bits as integer array int sBit[64]; int ut1; int yyyy; // 4-digit year const int CENTURY = 2000; // For 4-digit year computation // LM: Not sure if the following booleans will be used - include for now // syn suppresses nuisance output until after first dsync(). boolean syn = false; // Simulated or actual dsync() boolean resetOnSignalLoss = true; // If syn == false, rely on dsync() to enable output. // After dsync() a missing 'S' should reset. // Similarly a '1' in a reserved (unused) 0-bit indicates signal loss. // To see output no matter what, define syn = true, reset... = false. // The following boolean will be set in the timer1 ISR // and will control execution of the processRTC() function. boolean interruptFlipFlop = false; const int TESTPIN = 10; boolean toggle = false; // Toggle test pin on-off (indicator LED) // For updating RTC from valid WWVB decode const int TIMEZONE = -5; // Hours offset from UT - West is negative (0 for Universal Time) const int DSTSECS = 3600; // Seconds to add for DST const int NORMYEAR[12] = {31,59,90,120,151,181,212,243,273,304,334,365}; const int LEAPYEAR[12] = {31,60,91,121,152,182,213,244,274,305,335,366}; boolean saveSync = true; // Write last WWVB sync data to EEPROM for display after // Arduino power-down re-up. void setup () { pinMode(TESTPIN, OUTPUT); digitalWrite(TESTPIN, LOW); // Off if not used if (SER or SERRTC) Serial.begin(9600); if (LCD) lcd.init(); if (SER or SERRTC) delay(3000); // wait for console opening if (! rtc.begin()) { if (SERRTC) Serial.println("Couldn't find RTC"); while (1); } if ((rtc.lostPower()) or (resetRTC)) { // See note above if (SERRTC) { Serial.println("RTC lost power or was set to the wrong date or time."); Serial.println("Reseting to the time this sketch was compiled.."); } // From the Adafruit ds3231 example sketch - // The following line sets the RTC to the date & time this sketch was compiled // plus 16 seconds (upload and startup delay on my computer with SER = true.) rtc.adjust(DateTime(F(__DATE__), F(__TIME__)) + TimeSpan(0, 0, 0, 16)); // The following line sets the RTC with a spelled-out date & time, for example to set // January 21, 2014 at 3am you would call: // rtc.adjust(DateTime(2014, 1, 21, 3, 0, 0)); } for (int i = 0; i < 5; i++) nBytes[i] = 0; for (int i = 0; i < 64; i++) // Convenience copy as integer array sBit[i] = 0; nIndex = 0; nMode = 0; nMask = 0x80; nStart = nEnd = millis(); nPinState = digitalRead(3); oldKey = analogRead(0); wwvbHour = wwvbMinute = wwvbSecond = wwvbDay = wwvbYear = 0; // From: http://www.instructables.com/id/Arduino-Timer-Interrupts/ // Initialize timer1 interrupt cli();//stop interrupts //set timer1 interrupt at 1Hz TCCR1A = 0;// set entire TCCR1A register to 0 TCCR1B = 0;// same for TCCR1B TCNT1 = 0;//initialize counter value to 0 // set compare match register for 1hz increments OCR1A = 15624;// = (16*10^6) / (1*1024) - 1 (must be <65536) // turn on CTC mode TCCR1B |= (1 << WGM12); // Set CS10 and CS12 bits for 1024 prescaler TCCR1B |= (1 << CS12) | (1 << CS10); // enable timer compare interrupt TIMSK1 |= (1 << OCIE1A); sei();//allow interrupts if (saveSync and (COLS == 20)) { // Attempt to restore last WWVB sync String s = readEEPROM(20); if (s.charAt(4) == 'D') { // Weak check that EEPROM contains real data sLCD[2] = "Last WWVB sync: "; sLCD[3] = s; } } } void loop () { // DS3231 - if (interruptFlipFlop) { // timer1 interrupt (once per second) processRTC(); interruptFlipFlop = false; } // WWVB - readclock(); wwvbSecond = wwvbSecond % 60; // In case dsync() not reached! if (wwvbSecond == 0) if (resetOnSignalLoss and noSync()) syn = false; } void processRTC() { rtcNow = rtc.now(); if (SERRTC) { Serial.print(rtcNow.year(), DEC); Serial.print('/'); Serial.print(rtcNow.month(), DEC); Serial.print('/'); Serial.print(rtcNow.day(), DEC); Serial.print(" ("); Serial.print(daysOfTheWeek[rtcNow.dayOfTheWeek()]); Serial.print(") "); Serial.print(rtcNow.hour(), DEC); Serial.print(':'); Serial.print(rtcNow.minute(), DEC); Serial.print(':'); Serial.print(rtcNow.second(), DEC); Serial.println(); } if (LCD) { if (digitalRead(BACKLIGHTPIN) == HIGH) lcd.backlight(); else lcd.noBacklight(); sLCD[0] = displayDay(rtcNow); sLCD[1] = displayTime(rtcNow); displayLCD(); } if (SERRTC) Serial.println(); } void displayLCD() { for (int row=0; row < ROWS; row++) { while (sLCD[row].length() < COLS) sLCD[row].concat(SPACE); lcd.setCursor(0, row); sLCD[row].remove(COLS); lcd.print(sLCD[row]); } return; } String monthAbbreviation(int month) { switch(month) { case 1 : return "Jan"; case 2 : return "Feb"; case 3 : return "Mar"; case 4 : return "Apr"; case 5 : return "May"; case 6 : return "Jun"; case 7 : return "Jul"; case 8 : return "Aug"; case 9 : return "Sep"; case 10 : return "Oct"; case 11 : return "Nov"; case 12 : return "Dec"; default : return ""; } } String dayAbbreviation(int nDay) { switch(nDay) { case 0 : return "Sun."; case 1 : return "Mon."; case 2 : return "Tue."; case 3 : return "Wed."; case 4 : return "Thu."; case 5 : return "Fri."; case 6 : return "Sat."; default : return ""; } } String displayDay(DateTime dt) { // Formatted day and date String s = dayAbbreviation(dt.dayOfTheWeek()); String t; s.concat(SPACE); s.concat(monthAbbreviation(dt.month())); s.concat(SPACE); s.concat(String(dt.day())); s.concat(SPACE); // With abbreviated day name, year fits both displays s.concat(String(dt.year())); if (COLS == 20) s.concat(" ..."); return s; } String displayTime(DateTime dt) { // Formatted time int hr = dt.hour(); String meridiem = "AM "; if (hr == 12) meridiem = "PM "; else if (hr > 12) { hr -= 12; meridiem = "PM "; } else if (hr == 0) hr = 12; String s="", t; if (COLS == 20) s.concat(" "); s.concat(String(hr)); s.concat(":"); t = String(dt.minute()); if (t.length() == 1) t = "0" + t; s.concat(t); s.concat(":"); t = String(dt.second()); if (t.length() == 1) t = "0" + t; s.concat(t); s.concat(SPACE); s.concat(meridiem); return s; } // WWVB Decoder [Adapted from: https://www.youtube.com/watch?v=OeZzNehKL_Y&t=3s] void readclock(void) { int nRead; nRead = digitalRead(3); if (nRead != nPinState) { nPinState = nRead; // we get here on every transition of the input signal (edge) if (nRead == HIGH) { // when sig goes high, just save the system clock nStart = millis(); wwvbSecond++; if (wwvbSecond == 60) wwvbSecond = 0; } else if (nRead == LOW) { // when sig goes low, do all the work // this means we have a new bit from the receiver nEnd = millis(); // nWid is our measurement of the pulse width in ms. // this can vary a bit, so we dice it up into three // very generous windows. nWid = nEnd - nStart; if (nWid > 725) // > 725ms = sync pulse { if (nLastDecode == 2) { dsync(); nLastDecode = 3; } else { sync(); nLastDecode = 2; } } else if (nWid < 275) { its0(); nLastDecode = 0; } else { its1(); nLastDecode = 1; } } } } void its1(void) { nBytes[nIndex] |= nMask; nMask = nMask >> 1; sBit[wwvbSecond] = 1; // Raw data convenience array } void its0(void) { nMask = nMask >> 1; sBit[wwvbSecond] = 0; // Raw data convenience array } void sync(void) { nMask = 0x100; nIndex++; sBit[wwvbSecond] = 1; } void dsync(void) { wwvbSecond = 0; nMask = 0x80; nIndex = 0; showit(); for (int i = 0; i < 5; i++) { nBytes[i] = 0; } nMode = 1; if (SER) { Serial.println(); Serial.print("D"); // "D" for dsync() } sBit[wwvbSecond] = 1; } void showit(void) { int i; // LM: Multiple substitutions of Serial.print for lcd.print - if (SER) Serial.println(); if (nMode == 1) { Decode(); if (SER) { Serial.print("UTC: "); Serial.print(wwvbHour); Serial.print(":"); } if (wwvbMinute <= 9 and SER) Serial.print("0"); if (SER) { Serial.print(wwvbMinute); Serial.print(" DAY: "); Serial.print(wwvbDay); Serial.print(" YR: "); // Serial.print(wwvbYear); } yyyy = displayYear(); if (SER) { Serial.print(yyyy); Serial.print(" UT1: "); } ut1 = ut1Delta(); if (ut1 > 0 and SER) Serial.print("+"); if (SER) { Serial.print(float(ut1)/10, 1); displayBitArray(); // via Serial } } else { if (SER) Serial.print("In sync"); } if (nMode == 1) syncRTC(); // Set (adjust) RTC from WWVB } void Decode(void) { wwvbMinute = nBytes[0] & 0x0f; wwvbMinute += ((nBytes[0] >> 5) & 0x07) * 10; wwvbHour = nBytes[1] & 0x0f; wwvbHour += ((nBytes[1] >> 5) & 0x07) * 10; // WWVB sends the time AFTER the time mark, so we need to add a minute // for the correct time. wwvbMinute++; if (wwvbMinute >= 60) { wwvbMinute = 0; wwvbHour++; if (wwvbHour >= 24) { wwvbHour = 0; } } wwvbDay = (nBytes[3] >> 5) & 0x0f; wwvbDay += (nBytes[2] & 0x0f) * 10; wwvbDay += ((nBytes[2] >> 5) & 0x03) * 100; wwvbYear = (nBytes[5] >> 5) & 0x0f; wwvbYear += ((nBytes[4]) & 0x0f) * 10; // Bits (seconds) 36 through 43 have the UT1 correction // Bits (seconds) 36, 37, and 38 convey the sign // Bits (seconds) 40 through 43 encode the number of 10ths as BCD } int displayYear() { int tens = sBit[48] + 2 * sBit[47] + 4 * sBit[46] + 8 * sBit[45]; int ones = sBit[53] + 2 * sBit[52] + 4 * sBit[51] + 8 * sBit[50]; return CENTURY + 10 * tens + ones; } boolean noSync() { // Special test to avoid reams of defective output // LM: Experimental if (sBit[9] + sBit[19] + sBit[39] + sBit[49] + sBit[59] != 5) return true; if (sBit[10] + sBit[11] + sBit[20] + sBit[21] + sBit[34] + sBit[35] + sBit[44] + sBit[54] > 0) return true; return false; } int ut1Delta() { // LM: int correction = sBit[43] + 2 * sBit[42] + 4 * sBit[41] + 8 * sBit[40]; if (sBit[36] == 1 and sBit[37] == 0 and sBit[38] == 1) return correction; else if (sBit[36] == 0 and sBit[37] == 1 and sBit[38] == 0) return -correction; else if (correction == 0) return correction; else return -99; // Error indicator } void displayBitArray() { // Called from showit() if (not SER) return; // Serial only .. int i, j, sec; Serial.println(); // Not reached unless SER == true for (i = 0; i < 6; i++) { for (j = 0; j < 10; j++) { Serial.print(sBit[10*i + j]); } if (i < 5) Serial.print("|"); } if (notValid()) Serial.print(" [Invalid]"); } boolean notValid() { // Extensible validity tests // Called from displayBitArray() and from syncRTC() // Return true if and only if test determines decode to be NOT valid if (noSync()) return true; // 13 known bits if (ut1 < -9) return true; // Do not recompute! if (ut1 > +9) return true; if (yyyy > (CENTURY + 99)) return true; if (wwvbHour >= 24) return true; if (wwvbMinute >= 60) return true; if (wwvbDay > 366) return true; // Not observed! return false; } // Based on: http://www.instructables.com/id/Arduino-Timer-Interrupts/ // Interrupt service routine - ISR(TIMER1_COMPA_vect){//timer1 interrupt 1Hz // interruptFlipFlop is detected in loop() to enable processing RTC // loop() resets the variable interruptFlipFlop = true; return; } // On valid WWVB decode, sync RTC to WWVB time void syncRTC() { // Called from showit after return from Decode() // WWVB variables: hour, minute, day, year, yyyy, ut1 // Use setTime(hr,min,sec,day,month,yr) if (notValid()) return; // Avoid mis-adjusting RTC int monNum = 0; int dayNum = wwvbDay; // True in January! if (leapYear(yyyy)) { for (int i=0; i<12; i++) { if (wwvbDay <= LEAPYEAR[i]) { monNum = i+1; if (monNum > 1) dayNum = wwvbDay - LEAPYEAR[i-1]; break; } } } else { // Not a leap year for (int i=0; i<12; i++) { if (wwvbDay <= NORMYEAR[i]) { monNum = i+1; if (monNum > 1) dayNum = wwvbDay - NORMYEAR[i-1]; break; } } } DateTime ut (yyyy, monNum, dayNum, wwvbHour, wwvbMinute, 0); // For next... // Time zone correction long tzSeconds = 3600L * TIMEZONE; DateTime lt (ut.unixtime() + tzSeconds); // Local time // DST correction here if (isDST()) lt = (DateTime) lt.unixtime() + DSTSECS; // Next command synchronizes RTC to WWVB // Temporarily comment-out when WWVB simulator is connected rtc.adjust(lt); displaySync(); // Requires 4-row LCD if (SER) { // Debug Serial.println(); Serial.print("DEBUG - dayNum: "); Serial.print(dayNum); Serial.print(" monNum: "); Serial.print(monNum); Serial.print(" ... ut: "); Serial.print(ut.hour()); Serial.print(":"); Serial.print(ut.minute()); Serial.print(" lt: "); Serial.print(lt.hour()); Serial.print(":"); Serial.print(lt.minute()); } return; } // Upon setting RTC to WWVB time, display information about the last sync void displaySync() { if (ROWS < 4) return; sLCD[2] = "Last WWVB sync: "; sLCD[3] = String(yyyy); sLCD[3].concat("D"); if (wwvbDay < 100) sLCD[3].concat("0"); if (wwvbDay < 10) sLCD[3].concat("0"); sLCD[3].concat(String(wwvbDay)); sLCD[3].concat("@"); if (wwvbHour < 10) sLCD[3].concat("0"); sLCD[3].concat(String(wwvbHour)); if (wwvbMinute < 10) sLCD[3].concat("0"); sLCD[3].concat(wwvbMinute); sLCD[3].concat("UT1"); if (ut1 < 0) { sLCD[3].concat("-0."); sLCD[3].concat(String(-ut1)); } else { sLCD[3].concat("+0."); sLCD[3].concat(String(ut1)); } if (SER) { Serial.println(); Serial.print(sLCD[2]); Serial.println(); Serial.print(sLCD[3]); } if (saveSync) writeEEPROM(sLCD[3]); return; } // Additional date/time utilities boolean leapYear(int y) { // True if and only if y is a leap year if (not (y%4 == 0)) return false; // Not evenly divisible by 4 if (y%400 == 0) return true; // Evenly divisible by 400 if (y%100 == 0) return false; // Evenly divisible by 4 and 100 and not by 400 return true; // Evenly divisible by 4 and not by 100 } boolean isDST() { // True if and only if DST is in effect return (sBit[57] == 1) and (sBit[58] == 1); } // EEPROM - Store last WWVB sync data in EEPROM // for retrieval after power cycling void writeEEPROM(String s) { int sLen = s.length(); if (sLen > EEPROM.length()) return; // Sync data length = 20 for (int i=0; i EEPROM.length()) sLen = EEPROM.length(); String s=""; for (int i=0; i EEPROM.length()) len = EEPROM.length(); for (int i=0; i