// MAX7219 x 4 Day and Time Clock
// https:// forum.arduino.cc/t/max7219-x-4-day-and-time-clock/1398045
/*
Max7219 Display (4 0ff)
by Rod McMahon
22/7/2025
*/
/*
The Hours and Minutes are displayed after a clock read
from the DS1307.
The DOW is a cycling number from 0 - 6 which increments
at midnight. The DOW cannot be read from the DS1307 as
there is no mechanism to manually set the actual date in Y/M/DOW.
At first programming the DS1307 is time set and would hold
time by the battery through power outages.
There are three adjust time push buttons, DOW, Hours and Min.
I have stripped the library LEDControl.h file and not used a
a library to reduce code and to remove the additional items
used in LEDControl for 8 bit displays.
Also this code will compile on ESP32 etc. The library will not.
Arrangement of devices
----------------
From Left (connector is far right)
Device Address 3,2,1,0 as you look at display
Arrangement of LEDs per device
-----------------------
Top LH Corner is (0,0) in all cases
0,1,2,3,4,5,6,7 Row
1,
2,
3,
4,
5,
6,
7
Col
Set Row and Col commands
----------------------------
Set Row 0b11100011 will give XXX---XX from Left to Right
Set Col 0b11100011 will give XXX---XX from top to bottom
(Note that the code sends a row directly but activates LED by LED
for a column)
Layout of Banner, letters and numbers on device
---------------------------------
Banner - Device 3 Row 0 cols 0/8 and Device 2 Row 0 Col 0/3
DOW Device 3 Row 3 Col 1/7 and then to Device 2 Col 0/3
Hour Ten Device 2 Col 7 and Device 2 Col 1/2 (split)
Hour Unit Device 1 Col 3/4/5
Min Ten Device 0 Col 1/2/3
Min Unit Device 0 Col 5/6/7
Sec Device 2 Col 7 Row 2 and 5
Numbers
-----------------------
Each number (0-9)represented as by 3 columns
[10][3] array
DOW
-----------------------
Each DOW (7 in total) represented by 3 letters
SUN,MON,TUE,WED,THU,FRI,SAT,SUN and each letter
by 3 columns
The combination of letters and spaces gives an
[7][12] array
The banner has been added into each column
to give the 8 column values (6 for the letters)
*/
#include <RTClib.h>
#include <Wire.h>
// Max7219 register locations
#define OP_DECODEMODE 9
#define OP_INTENSITY 10
#define OP_SCANLIMIT 11
#define OP_SHUTDOWN 12
#define OP_DISPLAYTEST 15
// Coms IO allocation UNO
// int SPI_MOSI = 11;
// int SPI_CS = 7; // Any one
// int SPI_CLK = 13;
// Hardwired to DS3107
// SDA A4
// SCL A5
// Coms IO allocation ESP32
// int SPI_MOSI = 23;
// int SPI_CS = 19; // Any one
// int SPI_CLK = 18;
// Hardwired to DS3107
// SDA 21
// SCL 22
// Coms IO allocation Nano
int SPI_MOSI = 11;
int SPI_CS = 10; // Any one
int SPI_CLK = 13;
// Hardwired to DS3107
// SDA A4
// SCL A5
// IO Allocations Time Adjust push buttons
// Assign IO as required
int DOWAdj = 3; // adjust DOW
int HourAdj = 4; // Adjust Hour
int MinAdj = 5; // Adjust Minute
// Variables
int devices = 4; // Number of series MAX7219
byte spidata[16];
byte status[64];
// Numbers
// These are column entries
// Numbers are each 3 x 8
int Numbers[10][3] =
{
{0x7e, 0x81, 0x7e}, // 0
{0x40, 0xff, 0x00}, // 1
{0x43, 0x8d, 0x71}, // 2
{0x42, 0x91, 0x6e}, // 3
{0xf0, 0x10, 0xff}, // 4
{0xf1, 0x91, 0x9f}, // 5
{0x7e, 0x91, 0x4e}, // 6
{0x80, 0x80, 0xff}, // 7
{0x6e, 0x91, 0x6e}, // 8
{0x70, 0x90, 0x7f}, // 9
};
// Days of Week with banner
// Banner and space takes two leds, letters are 3 x 6 each
// These are column entries
int DOW[7][12] =
{
{0x00, 0x99, 0xa9, 0xae, 0x80, 0xbf, 0x81, 0xbf, 0x80, 0xbf, 0xb0, 0x9f}, // SUN
{0x00, 0xbf, 0x90, 0xbf, 0x80, 0x9e, 0xa1, 0x9e, 0x80, 0xbf, 0xb0, 0x9f}, // MON
{0x00, 0xa0, 0xbf, 0xa0, 0x80, 0xbf, 0x81, 0xbf, 0x80, 0xbf, 0xa9, 0xa1}, // TUE
{0x00, 0xbe, 0x87, 0xbe, 0x80, 0xbf, 0xa9, 0xa1, 0x80, 0xbf, 0xa1, 0x9e}, // WED
{0x00, 0xa0, 0xbf, 0xa0, 0x80, 0xbf, 0x88, 0xbf, 0x80, 0xbf, 0x81, 0xbf}, // THU
{0x00, 0xbf, 0xa8, 0xa0, 0x80, 0xbf, 0xa4, 0x9b, 0x80, 0x80, 0xbf, 0x80}, // FRI
{0x00, 0x99, 0xa9, 0xae, 0x80, 0x9f, 0xa8, 0x9f, 0x80, 0xa0, 0xbf, 0xa0}, // SAT
};
// Clock Variables
int NowHour; // From clock read
int NowMin;
int NowSec;
int HourTen ; // For display
int HourUnit;
int MinTen ;
int MinUnit;
int OldMinute; // for display updates
int OldSec;
RTC_DS1307 RTC; // clock item
DateTime now;
int DOWCount; // Counter for DOW
// ==================================
// Set DOW 12 Columns split over
// two devices
// ==================================
void setDOW( int no)
{
for (int i = 0; i < 12; i++)
{
if (i > 7)
setColumn(2, i - 8, DOW[no][i]);
else
setColumn(3, i, DOW[no][i]);
}
}
// ==================================
// Set Time
// Hour Ten split between two devices
// so done as columns
// ==================================
void setTime()
{
setColumn(2, 7, Numbers[HourTen][0]); // Hour Ten
setColumn(1, 0, Numbers[HourTen][1]);
setColumn(1, 1, Numbers[HourTen][2]);
setNumber(1, 3, HourUnit); // Hour Unit
setNumber(0, 1, MinTen); // Min Ten
setNumber(0, 5, MinUnit); // Min Unit
}
// ==================================
// Set Second Pulse
// Controlled by RTC
// ==================================
void setSecond(bool On)
{
if (On == true)
{
setLed(1, 2, 7, true);
setLed(1, 5, 7, true);
}
else
{
setLed(1, 2, 7, false);
setLed(1, 5, 7, false);
}
}
// ==================================
// Set a Number
// ==================================
void setNumber(int addr, int col, int no)
{
for (int i = 0; i < 3; i++)
setColumn(addr, col + i, Numbers[no][i]);
}
// ==================================
// Clear Status Register
// ==================================
void ClearStatus()
{
for (int i = 0; i < 64; i++)
status[i] = 0x00;
}
// ==================================
// LED setup
// ==================================
void LedControlSetUp(int numDevices)
{
ClearStatus();
// First Setup each
for (int i = 0; i < numDevices; i++)
{
spiTransfer(i, OP_DISPLAYTEST, 0);
setScanLimit(i, 7); // scanlimit is set to max on startup
spiTransfer(i, OP_DECODEMODE, 0); // decode is done in source
clearDisplay(i);
shutdown(i, true); // we go into shutdown-mode on startup
}
// then initialized each
for (int address = 0; address < devices; address++)
{
shutdown(address, false); /*The MAX72XX is in power-saving mode on startup*/
setIntensity(address, 3); /* Set the brightness to a medium values */
clearDisplay(address);/* and clear the display */
}
}
// ==================================
// ShutDown
// ==================================
void shutdown(int addr, bool b)
{
if (b)
spiTransfer(addr, OP_SHUTDOWN, 0);
else
spiTransfer(addr, OP_SHUTDOWN, 1);
}
// ==================================
// Set Scan Limit
// ==================================
void setScanLimit(int addr, int limit)
{
if (limit >= 0 && limit < 8)
spiTransfer(addr, OP_SCANLIMIT, limit);
}
// ==================================
// Set Intensity
// ==================================
void setIntensity(int addr, int intensity)
{
if (intensity >= 0 && intensity < 16)
spiTransfer(addr, OP_INTENSITY, intensity);
}
// ==================================
// Clear Display
// ==================================
void clearDisplay(int addr)
{
int offset;
offset = addr * 8;
for (int i = 0; i < 8; i++)
{
status[offset + i] = 0;
spiTransfer(addr, i + 1, status[offset + i]);
}
}
// ==================================
// Set an individual LED
// ==================================
void setLed(int addr, int row, int column, boolean state)
{
int offset;
byte val = 0x00;
if (row < 0 || row > 7 || column < 0 || column > 7)
return;
offset = addr * 8;
val = B10000000 >> column;
if (state)
status[offset + row] = status[offset + row] | val;
else
{
val = ~val;
status[offset + row] = status[offset + row] & val;
}
spiTransfer(addr, row + 1, status[offset + row]);
}
// ==================================
// Set a Row
// Value is byte representation
// ==================================
void setRow(int addr, int row, byte value)
{
if (0 <= row || row <= 7)
spiTransfer(addr, row + 1, value);
}
// ==================================
// Set a Column
// ==================================
void setColumn(int addr, int col, byte value)
{
byte val;
// if (col < 0 || col > 7)
// return;
if (0 <= col || col <= 7)
for (int row = 0; row < 8; row++)
{
val = value >> (7 - row);
val = val & 0x01;
setLed(addr, row, col, val);
}
}
// ==================================
// SPI Transfer
// ==================================
void spiTransfer(int addr, volatile byte opcode, volatile byte data)
{
int offset = addr * 2;
int maxbytes = devices * 2;
for (int i = 0; i < maxbytes; i++) // Create an array with the data to shift out
spidata[i] = (byte)0;
spidata[offset + 1] = opcode; // put our device data into the array
spidata[offset] = data;
digitalWrite(SPI_CS, LOW); // enable the line
for (int i = maxbytes; i > 0; i--) // Now shift out the data
shiftOut(SPI_MOSI, SPI_CLK, MSBFIRST, spidata[i - 1]);
digitalWrite(SPI_CS, HIGH); // latch the data onto the display
}
// ==================================
// Forces a start
// ==================================
void ResetClock()
{
GetClock();
setDOW(DOWCount);
setTime();
OldMinute = NowMin;
OldSec = NowSec;
}
// ==================================
// Get Clock info
// Separates the Tens and Units
// ==================================
void GetClock()
{
now = RTC.now(); // Reads the Clock
NowHour = now.hour();
NowMin = now.minute();
NowSec = now.second();
HourTen = NowHour / 10; // Extracts the digits
HourUnit = NowHour % 10;
MinTen = NowMin / 10;
MinUnit = NowMin % 10;
}
// ==================================
// Runs clock
// The DOW just cycles 0 to 6 at
// midnight. Display not taken from
// clock
// ==================================
void RunClock()
{
GetClock();
if ((NowHour == 0) && (NowMin == 0) && (NowSec == 0)) // Midnight
{
DOWCount++;
if (DOWCount > 6)
DOWCount = 0;
setDOW(DOWCount);
delay(1000); // get past the second or will advance
}
if (OldMinute != NowMin) // Display at each minute change
{
setTime();
OldMinute = NowMin;
}
if (OldSec != NowSec) // pulses the second leds
{
setSecond(true);
OldSec = NowSec;
delay(500);
setSecond(false);
}
}
// ==================================
// Adjusts day of week, hour and min
// ==================================
void AdjustClock()
{
if (digitalRead(DOWAdj) == LOW)
{
DOWCount++;
if (DOWCount > 6)
DOWCount = 0;
setDOW(DOWCount);
Serial.print("DoW = ");
Serial.println(DOWCount);
delay(200);
}
if (digitalRead(HourAdj) == LOW)
{
int HourCount = NowHour;
HourCount++;
if (HourCount > 23)
HourCount = 0;
RTC.adjust(DateTime(now.year(), now.month(), now.day(), HourCount, NowMin, NowSec));
Serial.print("Hour = ");
Serial.println(HourCount);
delay(100);
ResetClock();
}
if (digitalRead(MinAdj) == LOW)
{
int MinCount = NowMin;
MinCount++;
if (MinCount > 59)
MinCount = 0;
RTC.adjust(DateTime(now.year(), now.month(), now.day(), NowHour, MinCount, NowSec));
Serial.print("Min = ");
Serial.println(MinCount);
delay(100);
ResetClock();
}
}
// ==================================
// Setup
// ==================================
void setup()
{
Serial.begin(115200);
pinMode(SPI_MOSI, OUTPUT);
pinMode(SPI_CLK, OUTPUT);
pinMode(SPI_CS, OUTPUT);
pinMode(DOWAdj, INPUT_PULLUP);
pinMode(HourAdj, INPUT_PULLUP);
pinMode(MinAdj, INPUT_PULLUP);
digitalWrite(SPI_CS, HIGH); // set up LED
LedControlSetUp(devices);
Wire.begin();
RTC.begin();
RTC.adjust(DateTime(__DATE__, __TIME__)); // sets to date of sketch
// RTC.adjust(DateTime(25,7,23,11,12,40));
ResetClock(); // starts the clock on startup
delay(1000);
Serial.println("Setup Finished");
}
// ==================================
// Loop
// ==================================
void loop()
{
AdjustClock(); // looks to see if adjusting
RunClock(); // runs the clock
}