/* * MaxDominoClock.ino * Domino Clock using a Max7219 or Max7221 8x8 LED display driver * Arduino Uno/Nano sketch * Version: Feb 21, 2025 * Author: G. Forrest Cook, www.solorb.com/elect * Released under the GPL V3 license * * The MAX7219 IC connects to the Arduino SPI port, (11)(MOSI) & (13)(SCK). * the !load pulse is on digital (9) * * A Dallas DS3231 real-time clock chip is connected to the I2C bus. * The seconds animation mode is selected by a switch on digital 3. * The 12 or 24 hour mode is selected by a switch on digital 4. * Time is set via three pushbuttons on digital 6, 7 and 8. * The inputs are all pulled up to +5V through internal resistors * and are grounded when the switches and buttons are activated. */ #include #include "RTClib.h" #define brightPin 3 // brightness select switch #define h1224Pin 4 // 12/24 hour mode switch #define animPin 5 // display animation switch #define ts1Pin 6 // time set switch #1 (Hour) #define ts2Pin 7 // time set switch #2 (Minute) #define ts3Pin 8 // time set switch #3 (Zero Seconds) #define loadPin 9 // MAX7219 load data pin #define STARTTIME 750 // delay before starting the clock display #define RRCTIME 3 // random row colum rate #define FLASHTIME 50 // Display test time #define RASTIME 22 // raster scan rate #define RDICETIME 200 // random dice rate #define SETTIME 600 // time set switch rate #define RTCRTIME 50 // delay between reading RTC RTC_DS3231 rtc; // Dallas real-time clock DateTime Dtime; byte Lastsec; byte BrightLev=5; // display brightness level byte BrightDim=1; // Brightess up/down flag byte Workspace[8]; // array copy of 8x8 pixels byte Secpos; // alternating dots position byte Hourmode; // 12/24 hour mode byte Newhour=0; // Animate display on the new hour // single Dice digits 0-9 in a 3x3 dot array (3 bytes) // two Dice digits become one Domino static byte Dicedig[] = {2,5,2, 0,2,0, 4,0,1, 4,2,1, 5,0,5, 5,2,5, 5,5,5, 5,7,5, 7,5,7, 7,7,7}; // Initial eye candy sequence static byte Raster[] = {1,2,4,8,16,32,64,128,0}; // Seconds animation, zig-zag on two middle rows static byte Secrow3[] = {1,0,4,0,16,0,64,0,128,0,32,0,8,0,2,0}; static byte Secrow4[] = {0,2,0,8,0,32,0,128,0,64,0,16,0,4,0,1}; void setup() { byte i, j; SPI.begin(); // SPI initializes port B. pinMode(brightPin, INPUT_PULLUP); // display brightness switch pinMode(h1224Pin, INPUT_PULLUP); // 12/24 hour mode switch pinMode(animPin, INPUT_PULLUP); // display seconds mode switch pinMode(ts1Pin, INPUT_PULLUP); // Time set pushbutton #1 pinMode(ts2Pin, INPUT_PULLUP); // Time set pushbutton #2 pinMode(ts3Pin, INPUT_PULLUP); // Time set pushbutton #3 pinMode (loadPin, OUTPUT); // Setup MAX7219 chip load pin digitalWrite(loadPin, LOW); Max7219out(0x01, 0x0C); // turn shutdown mode off Max7219out(0x00, 0x09); // turn off the 7 segment BCD decoder Max7219out(BrightLev, 0x0A); // set intensity, range is 00-0F Max7219out(0x07, 0x0B); // set scan limit register to all colums on Max7219out(0x01, 0x0F); // set display test on delay(FLASHTIME); Max7219out(0x00, 0x0F); // set display test off // Serial.begin(9600); // serial can be used for debugging // Serial.println("Starting"); rtc.begin(); // start up the real-time clock if (rtc.lostPower()) // set the initial date to the compile time. rtc.adjust(DateTime(F(__DATE__), F(__TIME__))); MatrixClear(); // blank the display WorkspaceClear(); // blank the ram pixel workspace Secpos=255; // Initial eye candy, raster display for (j=0; j<=7; j++) // sequence rows top to bottom { for (i=0; i<=8; i++) // sequence pixels left to right on one row { Max7219out(Raster[i], 8-j); delay(RASTIME); } } // Second eye candy, random display RandomRowCol(350); // flash some random dots delay(STARTTIME); Dtime = rtc.now(); // read the time at startup } void loop() { Hourmode = digitalRead(h1224Pin); // check 12/24 hour mode switch DominoHourMin(); // show the hours and minutes SecondsHold(); // animate the seconds, read the time, wait } // Display the hour and minute as two rows of dominos void DominoHourMin() { byte nibl, nibh; byte hour, min; hour = Dtime.hour(); min = Dtime.minute(); if (!digitalRead(animPin)) // check for the animation switch { if (min==0 && !Newhour) // display random dots on the new hour { RandomRowCol(500); // flash some random dots Newhour=1; // The new hour animation is done } else if (min==1 && Newhour) Newhour=0; } WorkspaceClear(); // blank the ram pixel workspace if (Hourmode) // deal with hours in 12 hour mode. { if (hour>12) hour-=12; else if (hour==0) hour=12; } DominoDigs((byte)hour, 0); // show the hour on the top DominoDigs((byte)min, 1); // show the minutes on the bottom WorkspaceToDisp(); // upload the ram pixel workspace } // This function hangs on until the minutes change and returns after // seconds == 1. // Display the seconds ticks as moving dots in matrix lines 3 and 4 // if the animate mode switch is active. // Inner loop code (ButtonCheck) goes here. void SecondsHold() { byte seconds; do { ButtonCheck (); // check for time set buttons Dtime = rtc.now(); // read the time, get the seconds seconds = (byte)Dtime.second(); if (seconds != Lastsec) // check for a change of the seconds { Lastsec = seconds; if (!digitalRead(animPin)) // check for the animation switch { Secpos++; if (Secpos>=16) Secpos=0; Workspace[3] = Secrow3[Secpos]; Workspace[4] = Secrow4[Secpos]; } else // seconds animation off { Workspace[3] = 0; // clear the seconds part of the display Workspace[4] = 0; } WorkspaceToDisp(); } delay(RTCRTIME); // slow the time reads down } while (seconds > 0); // loop until the minute changes } // Display two digits on the top (0) or bottom (1) // as two dice digits (1 domino) void DominoDigs(byte num, byte topbot) { if (!topbot) // upper two digits { DiceDigToWS(num / 10, 0); DiceDigToWS(num % 10, 1); } else // lower two digits { DiceDigToWS(num / 10, 2); DiceDigToWS(num % 10, 3); } } // Send a dice digit to 1 of 4 corners in the workspace void DiceDigToWS(byte num, byte corner) { byte digidx; digidx = num*3; // start of 3 bytes of dice digit data switch (corner) { case 0: // upper right Workspace[0] &= 0xF8; Workspace[0] |= Dicedig[digidx++]; Workspace[1] &= 0xF8; Workspace[1] |= Dicedig[digidx++]; Workspace[2] &= 0xF8; Workspace[2] |= Dicedig[digidx]; break; case 1: // upper left Workspace[0] &= 0x1F; Workspace[0] |= Dicedig[digidx++] * 32; Workspace[1] &= 0x1F; Workspace[1] |= Dicedig[digidx++] * 32; Workspace[2] &= 0x1F; Workspace[2] |= Dicedig[digidx] * 32; break; case 2: // lower right Workspace[5] &= 0xF8; Workspace[5] |= Dicedig[digidx++]; Workspace[6] &= 0xF8; Workspace[6] |= Dicedig[digidx++]; Workspace[7] &= 0xF8; Workspace[7] |= Dicedig[digidx]; break; case 3: // lower left Workspace[5] &= 0x1F; Workspace[5] |= Dicedig[digidx++] * 32; Workspace[6] &= 0x1F; Workspace[6] |= Dicedig[digidx++] * 32; Workspace[7] &= 0x1F; Workspace[7] |= Dicedig[digidx] * 32; break; } } // blank the ram pixel workspace void WorkspaceClear() { byte i; for (i=0; i<8; i++) Workspace[i] = 0; } // copy all 8 rows of the ram pixel workspace to the display void WorkspaceToDisp() { byte i; for (i=0; i<8; i++) Max7219out(Workspace[i], 8-i); // reverse the bit order } // Display random rows and columns void RandomRowCol(int count) { int j; for (j=0; j<=count; j++) { Max7219out((byte)random(0,256), (byte)random(1,9)); delay(RRCTIME); } } // clear all of the pixels in the display void MatrixClear() { byte i; for (i=1; i<=8; i++) Max7219out(0, i); } // Output control and display data to the MAX7219 LED display // Display rows are 1-8 with 0 at the top void Max7219out(byte data, byte addr) { // SPI speed(Hz), bit order, clock mode SPI.beginTransaction(SPISettings(10000000, MSBFIRST, SPI_MODE0)); digitalWrite(loadPin, LOW); // activate the load pin SPI.transfer(addr); // Shift out one byte of address/ctrl SPI.transfer(data); // Shift out one byte of data digitalWrite(loadPin, HIGH); // turn off load, latch the data SPI.endTransaction(); } // See if any of the time set buttons are pressed void ButtonCheck() { if (!digitalRead(ts1Pin)) SetHour(); // check time set switches if (!digitalRead(ts2Pin)) SetMin(); if (!digitalRead(ts3Pin)) ZeroSec(); if (!digitalRead(brightPin)) SetBrightness(); } // Increment the hour while the button is pressed, zero seconds on exit. void SetHour() { byte hour; WorkspaceClear(); // blank the ram pixel workspace while (!digitalRead(ts1Pin)) { Dtime = rtc.now(); // Get the time hour = Dtime.hour(); hour++; if (Hourmode) // deal with hours in 12 hour mode. { if (hour>12) hour-=12; else if (hour==0) hour=12; } else if (hour == 24) hour = 0; DominoDigs(hour, 0); // Display the incremented hour WorkspaceToDisp(); rtc.adjust(DateTime(Dtime.year(), Dtime.month(), Dtime.day(), \ hour, Dtime.minute(), 0)); delay(SETTIME); } } // Increment the minutes while the button is pressed, zero seconds on exit. void SetMin() { byte minute; WorkspaceClear(); // blank the ram pixel workspace while (!digitalRead(ts2Pin)) { Dtime = rtc.now(); // Get the time minute = Dtime.minute(); minute++; if (minute == 60) minute = 0; DominoDigs(minute, 1); // Display the incremented minute WorkspaceToDisp(); rtc.adjust(DateTime(Dtime.year(), Dtime.month(), Dtime.day(), \ Dtime.hour(), minute, 0)); delay(SETTIME); } } // Zero the seconds when the button is released. void ZeroSec() { WorkspaceClear(); // blank the ram pixel workspace DominoDigs(0, 1); // Display 00 for seconds WorkspaceToDisp(); while (!digitalRead(ts3Pin)); //wait for the button to release. Dtime = rtc.now(); // Get the time rtc.adjust(DateTime(Dtime.year(), Dtime.month(), Dtime.day(), \ Dtime.hour(), Dtime.minute(), 0)); } // Set the brightness level void SetBrightness() { WorkspaceClear(); // blank the ram pixel workspace while (!digitalRead(brightPin)) { if (BrightDim) { BrightLev++; if (BrightLev > 15) { BrightLev=14; BrightDim=0; } } else { BrightLev--; if (BrightLev == 255) { BrightLev=1; BrightDim=1; } } Max7219out(BrightLev, 0x0A); // set intensity, range is 00-0F DominoDigs(BrightLev+1, 0); // Display the brightness as 1-16 on top DominoDigs(88, 1); // Display 88 on the bottom WorkspaceToDisp(); delay(SETTIME); } DominoHourMin(); // revert to hours and minutes display }