the code is here:
// packed date yymmdd w
uint32_t pd = 0x16010105; // obvious bogus (but valid) date
// packed time hhmmssff
uint32_t pt = 0x00000000; // intentionally bogus (for testing)
uint32_t last_tick_usec = 0UL;
char buf[50]; // this is intentionally bigger than I need
uint8_t set_state = 0;
// set_state indicates what is currently being set, as follows:
// 7 for hour tens, 6 for hour ones,
// 5 for minute tens, 4 for minute ones,
// 3 for second tens, 2 for second ones,
// 1 for normal timekeeping mode (i.e. finished setting the time)
// and it is 0 if the time has not yet been set
const uint32_t TIME_SET_MAX = 0x29595999;
// each digit is at max possible value for time
const uint32_t DATE_SET_MAX = 0x99123907;
// each digit (except units digit of month) is at max possible value for date
const uint8_t BUTTON_PIN_A = 12; // this button moves the cursor
const uint8_t BUTTON_PIN_B = 11; // this button makes the numbers change
uint8_t button_a_now;
uint8_t button_a_last;
uint8_t button_b_now;
uint8_t button_b_last;
enum rtc_status_type {
RTC_UNKNOWN,
RTC_PRESENT,
RTC_ABSENT,
RTC_TROUBLE
};
enum rtc_status_type rtc_status = RTC_UNKNOWN;
#include <LiquidCrystal.h>
#include <Wire.h>
#include <toneAC.h>
// RS EN DB4 DB5 DB6 DB7
LiquidCrystal lcd( 7, 6, 5, 4, 3, 2);
void setup() {
pinMode(BUTTON_PIN_A, INPUT_PULLUP);
pinMode(BUTTON_PIN_B, INPUT_PULLUP);
// pressing either button makes its pin's voltage go low
button_a_now = (digitalRead(BUTTON_PIN_A)==LOW);
button_a_last = button_a_now;
button_b_now = (digitalRead(BUTTON_PIN_B)==LOW);
button_b_last = button_b_now;
// set up the LCD
lcd.begin(16, 2); // LCD dimensions
// define special characters for single cell numerals 10 through 12
uint8_t singleCellTen[] = { 18, 21, 21, 21, 21, 21, 18, 0 };
uint8_t singleCellEleven[] = { 9, 27, 9, 9, 9, 9, 9, 0 };
uint8_t singleCellTwelve[] = { 22, 21, 17, 18, 20, 20, 23, 0 };
lcd.createChar(5, singleCellTen); // "illogical" indices, I know
lcd.createChar(6, singleCellEleven);
lcd.createChar(7, singleCellTwelve);
// initialize connection so we can read the time from the external RTC
Wire.begin();
// attempt to read the time from the external RTC
getNewTime();
// show (possibly bogus) time on display
displayTime();
}
void loop() {
button_a_last = button_a_now;
button_b_last = button_b_now;
// remember, pressing either button makes its pin's voltage go low
button_a_now = (digitalRead(BUTTON_PIN_A)==LOW);
button_b_now = (digitalRead(BUTTON_PIN_B)==LOW);
// has either button just been pushed down?
uint8_t pushed_a = (button_a_now && !button_a_last);
uint8_t pushed_b = (button_b_now && !button_b_last);
// what happens next depends on what mode we're in
if (set_state <= 1) {
// if we're in here, then the clock is running
if (pushed_a) {
// enter time setting mode
pt &= 0xFFFFFF00;
set_state = 15;
displayTime();
}
else {
// just show the time (regular timekeeping mode)
uint32_t old_pt = pt;
getNewTime();
if (pt != old_pt) {
if (set_state == 1) {
if (((pt + 0x00000030) & 0xFFFFFF80) != ((old_pt + 0x00000030) & 0xFFFFFF80)) {
uint16_t pt_hi = ((uint16_t)((pt >> 16) & 0x0000FFFF));
if ((pt_hi & 0x00FF) == 0x0000) {
// the "zeroth" minute of the hour ... but which hour?
// how many times to strike?
uint8_t strike_num = (((pt_hi >> 12) & 0xF) * 10) + ((pt_hi >> 8) & 0xF);
if (strike_num > 12) strike_num -= 12;
if (strike_num == 0) strike_num = 12;
// how many half-seconds into the minute are we?
uint16_t pt_lo = ((uint16_t)(pt & 0x0000FFFF));
uint8_t half_sec = (((pt_lo >> 12) & 0xF) * 20) + (((pt_lo >> 8) & 0xF) * 2);
if ((pt_lo & 0xFF) >= 0x50) half_sec++;
if (half_sec < 26) {
switch (half_sec) {
// Westminster Chimes
case 0: toneAC(330, 10, 420, true); break;
case 1: toneAC(415, 10, 420, true); break;
case 2: toneAC(370, 10, 420, true); break;
case 3: toneAC(247, 10, 735, true); break;
case 6: toneAC(330, 10, 420, true); break;
case 7: toneAC(370, 10, 420, true); break;
case 8: toneAC(415, 10, 420, true); break;
case 9: toneAC(330, 10, 735, true); break;
case 12: toneAC(415, 10, 420, true); break;
case 13: toneAC(330, 10, 420, true); break;
case 14: toneAC(370, 10, 420, true); break;
case 15: toneAC(247, 10, 735, true); break;
case 18: toneAC(247, 10, 420, true); break;
case 19: toneAC(370, 10, 420, true); break;
case 20: toneAC(415, 10, 420, true); break;
case 21: toneAC(330, 10, 735, true); break;
default: break;
}
}
else if ((half_sec < (26 + (3 * strike_num))) && ((half_sec % 3) == 2)){
// bong the hours
toneAC(415, 10, 750, true);
}
}
}
}
displayTime();
}
}
}
else if ((set_state >= 2) && (set_state <= 15)) {
// if we're in here, then we're in the midst of setting the date/time
if (pushed_a) {
// move the cursor
set_state--;
if (set_state == 13) set_state--; // this is not triskaidekaphobia
while ((set_state == 9) || (set_state == 8)) set_state--;
if (set_state == 7) {
pd &= 0xFFFFFF00;
pd += packedDateWeekday(pd);
}
if (set_state <= 1) {
// if we're finished setting the time, we start the clock running
if (rtc_status == RTC_PRESENT) {
// BEGIN code to write the time to the Chronodot
Wire.beginTransmission(0x68); // address DS3231
Wire.write(0x00); // select register
Wire.write((uint8_t)((pt >> 8) & 0xFF)); // seconds
Wire.write((uint8_t)((pt >> 16) & 0xFF)); // minutes
Wire.write((uint8_t)((pt >> 24) & 0xFF)); // hours
Wire.write((uint8_t)(pd & 0xFF)); // day of week
Wire.write((uint8_t)((pd >> 8) & 0xFF)); // day of month
Wire.write((uint8_t)((pd >> 16) & 0xFF)); // month
Wire.write((uint8_t)((pd >> 24) & 0xFF)); // year
Wire.endTransmission();
// END code to write the time to the Chronodot
}
last_tick_usec = micros();
}
displayTime();
}
else if (pushed_b) {
// change the number that the cursor is pointing to
// (that is, the number itself gets changed; the cursor does not move)
uint32_t digit_mask = (((uint32_t)0x0000000F)<<(4*(set_state&7)));
if (set_state >= 8) {
if (set_state == 12) digit_mask = 0x00FF0000; // special mask for month
// change one digit (or maybe two digits) of the date
if ((pd & digit_mask) >= (DATE_SET_MAX & digit_mask)) pd &= ~digit_mask;
else pd += (digit_mask & 0x11011111);
if (set_state == 12) {
// the month requires special treatment
if ((pd & 0x00FF0000) == ((uint32_t)0x00000000)) {
// there is no month 0
pd += 0x00010000;
}
if ((pd & 0x000F0000) > 0x00090000) {
// after month 9 comes month 10
pd = (pd & 0xFFF0FFFF) + 0x00100000;
}
}
}
else {
// change one digit of the time
if ((pt & digit_mask) >= (TIME_SET_MAX & digit_mask)) pt &= ~digit_mask;
else pt += (digit_mask & 0x11111111);
}
// show the new time
displayTime();
}
}
delay(10); // poor man's switch debounce
}
void displayTime() {
// set cursor to beginning of top row
lcd.setCursor(0, 0);
// 01234567890123456
if (set_state == 0) lcd.print(F("RTC not found! "));
else if (set_state == 1) {
switch (rtc_status) {
// 01234567890123456
case RTC_PRESENT: lcd.print(F("Date Time RTC")); break;
case RTC_ABSENT: lcd.print(F("Date Time ")); break;
case RTC_TROUBLE: lcd.print(F("!DEAD RECKONING!")); break;
default: lcd.print(F("rtc_status error")); break;
}
}
else if (set_state <= 7) {
switch ((uint8_t)(pd & 0xFF)) {
// 01234567890123456
case 1: lcd.print(F("Monday, right? ")); break;
case 2: lcd.print(F("Tuesday, right? ")); break;
case 3: lcd.print(F("Wednesday,right?")); break;
case 4: lcd.print(F("Thursday, right?")); break;
case 5: lcd.print(F("Friday, right? ")); break;
case 6: lcd.print(F("Saturday, right?")); break;
case 7: lcd.print(F("Sunday, right? ")); break;
default: lcd.print(F("Calendar error! ")); break;
}
}
else if (set_state <= 15) lcd.print(F("Set date & time:"));
else lcd.print(F("Oops, Error! "));
// set cursor to beginning of bottom row
lcd.setCursor(0, 1);
// determine the correct character for the month (we'll need this for the next step)
uint8_t month_byte = (uint8_t)((pd >> 16) & 0xFF);
char month_char = '*';
if (month_byte <= 0x09) month_char = (char)('0' + month_byte);
else if (month_byte == 0x10) month_char = (char)5;
else if (month_byte == 0x11) month_char = (char)6;
else if (month_byte == 0x12) month_char = (char)7;
else if ((month_byte >= 0x0A) && (month_byte <= 0x0F)) {
// this should never happen -- I'm including it strictly for debugging purposes
month_char = (char)('a' + (month_byte - 0xA));
}
//
// 01234567890123456
// show the date and time on the display, thus: YY.M.DD HH:MM:SS
sprintf(buf, "%02x.%c.%02x %02x:%02x:%02x",
(uint8_t)((pd>>24)&0xFF), month_char, (uint8_t)((pd>>8)&0xFF),
(uint8_t)((pt>>24)&0xFF), (uint8_t)((pt>>16)&0xFF), (uint8_t)((pt>>8)&0xFF));
// change the zeros to capital "O"s
for (int i=0; i<16; i++) {
if (buf[i]=='0') buf[i]='O';
}
lcd.print(buf);
switch (set_state) {
case 15: lcd.setCursor(0, 1); lcd.cursor(); break; // cursor under year tens
case 14: lcd.setCursor(1, 1); lcd.cursor(); break; // cursor under year ones
case 12: lcd.setCursor(3, 1); lcd.cursor(); break; // cursor under month
case 11: lcd.setCursor(5, 1); lcd.cursor(); break; // cursor under date tens
case 10: lcd.setCursor(6, 1); lcd.cursor(); break; // cursor under date ones
case 7: lcd.setCursor(8, 1); lcd.cursor(); break; // cursor under hour tens
case 6: lcd.setCursor(9, 1); lcd.cursor(); break; // cursor under hour ones
case 5: lcd.setCursor(11, 1); lcd.cursor(); break; // cursor under minute tens
case 4: lcd.setCursor(12, 1); lcd.cursor(); break; // cursor under minute ones
case 3: lcd.setCursor(14, 1); lcd.cursor(); break; // cursor under second tens
case 2: lcd.setCursor(15, 1); lcd.cursor(); break; // cursor under second ones
default: lcd.noCursor(); // if not setting the time, then no cursor
}
}
void getNewTime() {
//
// In case no external RTC is available, this function can serve as a substitute.
// However, if there is an external RTC, then this function will make use of it.
//
const uint32_t ONE_TICK_USEC = 50000UL; // update the time every 0.05 second
const uint32_t ONE_TICK_PACKED = 0x00000005; // see previous line
uint32_t old_pt = pt;
while ((micros()-last_tick_usec)>=ONE_TICK_USEC) {
// figure out how much time has passed
last_tick_usec += ONE_TICK_USEC;
pt = timeAdd(pt, ONE_TICK_PACKED);
}
if (pt>=0x24000000) {
pt = timeSub(pt, 0x24000000);
// advance the date (yes, it really is this complicated!)
// BEGIN date advance code
if ((pd & 0x000000FF) >= 0x00000007) pd &= 0xFFFFFF00;
pd += 0x00000101;
if ((pd & 0x00000F00) >= 0x00000A00) pd = (pd & 0xFFFFF0FF) + 0x00001000;
if ((pd & 0x0000FF00) >= 0x00003200) pd = (pd & 0xFFFF00FF) + 0x00010100;
if ((pd & 0x00FFFF00) >= 0x00123200) pd = (pd & 0xFF0000FF) + 0x01010100;
if ((pd & 0x0FFFFF00) >= 0x09123200) pd = (pd & 0xF00000FF) + 0x10010100;
if ((pd & 0x00FFFF00) == 0x00113100) pd = (pd & 0xFF0000FF) + 0x00120100;
if ((pd & 0x00FFFF00) == 0x00093100) pd = (pd & 0xFF0000FF) + 0x00100100;
if ((pd & 0x00FFFF00) == 0x00063100) pd = (pd & 0xFF0000FF) + 0x00070100;
if ((pd & 0x00FFFF00) == 0x00043100) pd = (pd & 0xFF0000FF) + 0x00050100;
if ((pd & 0x00FFFE00) == 0x00023000) pd = (pd & 0xFF0000FF) + 0x00030100;
if (!((((pd & 0x10000000) >> 3) ^ (pd & 0x03FFFF00)) == 0x00022900)) {
if ((pd & 0x00FFFF00) == 0x00022900) {
pd = (pd & 0xFF0000FF) + 0x00030100;
}
}
// END date advance code
}
if ((rtc_status == RTC_UNKNOWN) || (old_pt != pt)) {
if ((rtc_status == RTC_UNKNOWN) || (rtc_status == RTC_PRESENT)) {
// We will attempt to read the RTC.
// If we succeed, we assume that whatever date and time it gave us is correct.
// If we fail, we assume that we don't have a good RTC, and we don't try to read it again.
uint32_t expected_pt = pt;
// BEGIN attempt to read the time
// send request to receive data starting at register 0
Wire.beginTransmission(0x68); // 0x68 is DS3231 device address
Wire.write((uint8_t)0); // start at register 0
Wire.endTransmission();
Wire.requestFrom(0x68, 7); // request seven bytes (ss, mi, hh, wd, dd, mo, yy)
// check for a reply from the RTC, and use it if we can
if (Wire.available() >= 7) {
// if we're here, we got a reply and it is long enough
// so now we read the time
pt = (((Wire.read()) & (((uint32_t)(0x000000FF)))) << 8); // seconds
pt += (((Wire.read()) & (((uint32_t)(0x000000FF)))) << 16); // minutes
pt += (((Wire.read()) & (((uint32_t)(0x000000FF)))) << 24); // hours
pd = (((Wire.read()) & ((uint32_t)(0x000000FF)))); // day of week
pd += (((Wire.read()) & (((uint32_t)(0x000000FF)))) << 8); // day of month
pd += (((Wire.read()) & (((uint32_t)(0x000000FF)))) << 16); // month
pd += (((Wire.read()) & (((uint32_t)(0x000000FF)))) << 24); // year
rtc_status = RTC_PRESENT;
set_state = 1; // because we have successfully read the time
}
else {
// if we're here, then we did not get a reply we could use
if (rtc_status == RTC_UNKNOWN) rtc_status = RTC_ABSENT;
else rtc_status = RTC_TROUBLE;
}
// END attempt to read the time
//
// Most RTC units do not handle fractional seconds.
// What follows is an attempt at estimating the missing fractional seconds.
if ((pt & 0xFFFFFF00) == (expected_pt & 0xFFFFFF00)) {
// hour, minute, and second are the same as expected
// so we assume that the fractional seconds are also the same as expected
pt = expected_pt;
}
else if (timeAdd(pt, 0x00000100) == (expected_pt & 0xFFFFFF00)) {
// time is one second earlier than expected
pt = timeAdd(pt, 0x00000099);
}
else if ((pt == 0x23595900) && (expected_pt < 0x00000100)) {
// special case the second just before midnight
pt = 0x23595999;
}
}
}
}
// some old time manipulation functions I dug up for this
uint32_t timeAdd (uint32_t x, uint32_t y) {
// no sanity checking of input
// format is hhmmssff with ff being decimal fractions of a second
// "out of range" results are e.g. A0000000 for 100 hours
uint32_t binsum = x + y;
uint32_t carry = ((binsum + 0x06A6A666) ^ x ^ y) & 0x11111110;
return (binsum + ((carry - (carry>>4)) & 0x06A6A666));
}
uint32_t timeSub (uint32_t x, uint32_t y) {
// no sanity checking of input
// format is hhmmssff with ff being decimal fractions of a second
// "negative" results are e.g. F9595999 for -0.01 second
uint32_t bindiff = x - y;
uint32_t borrow = (bindiff ^ x ^ y) & 0x11111110;
return (bindiff - ((borrow - (borrow>>4)) & 0x06A6A666) );
}
/*
Usage example:
2 h 58 min 30.98 s --> 0x02583098
0 h 1 min 44.06 s --> 0x00014406
Addition:
timeAdd(0x02583098, 0x00014406) gives 0x03001504
which means 3 h 0 min 15.04 s
Subtraction:
timeSub(0x02583098, 0x00014406) gives 0x02564692
which means 2 h 56 min 46.92 s
*/
// ... and a date manipulation function I made just for this
uint8_t packedDateWeekday(uint32_t p) {
// Input is a packed date. Last two digits are ignored.
// Format is YYMMDDXX where XX is don't care.
// Example: 0x16062688 means June 26, 2016
// Output is the day of the week corresponding to the input date.
// (1 for Monday, 2 for Tuesday, ..., 7 for Sunday)
// There is hardly any sanity checking of input.
uint8_t x = p >> 24;
x -= (6 * ((x >> 4) & 0x0F));
x += 4;
if ((p & 0x00FF0000) <= 0x00020000) x--;
x += (x >> 2);
switch ((uint8_t)((p >> 16) & 0xFF)) { // the 0xFF part is paranoia
case 0x03: x += 4; break;
case 0x04: break;
case 0x05: x += 2; break;
case 0x06: x += 5; break;
case 0x07: break;
case 0x08: x += 3; break;
case 0x09: x += 6; break;
case 0x10: x += 1; break;
case 0x11: x += 4; break;
case 0x12: x += 6; break;
case 0x01: x += 2; break;
case 0x02: x += 5; break;
default: return 0;
}
x += ((p >> 8) & 0xFF);
x += ((p >> 12) & 0x0F);
while (x > 7) x -= 7;
return x;
}