Hardware components | ||||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 4 | ||||
| × | 1 | ||||
| × | 36 | ||||
| × | 18 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
Software apps and online services | ||||||
| ||||||
Hand tools and fabrication machines | ||||||
| ||||||
|
While browsing Hackaday.io, I came across a beautiful clock (Instructables) made with an array of 7-segment displays by Frugha. I really wanted to build this but Frugha's version is quite large due to it using 0.56" 7-segment displays. My design uses 0.28" 7-segment displays making it only a quarter of the size.
The software has been rewritten to incorporate an alarm clock and a configuration screen to set the time, date and alarm.
VideoConfiguration screenThe clock is controlled by four buttons. The SELECT button (top most) will show the configuration screen. The current selection will flash. Pressing the SELECT button again will cycle through the different configuration sections (that is Time, Date, Alarm, Tune, Brightness, Date Format and Clock).
Pressing the ENTER button (2nd down from the top) while one of the menu items is selected will allow you change its settings. Where there is more than one setting for a menu item, the ENTER button will cycle between them. The active setting will flash.
When a setting is flashing, the UP and DOWN buttons (bottom two buttons) will change the setting.
Date screenPressing the ENTER button (2nd down from the top) while the clock is displayed will show the date screen for five seconds.
The Date Format section on the configuration screen allows you to set the date format (DD-MM or MM-DD) and font used.
To simply the board design, my version uses 4-digit displays rather than the single digit displays used in the original version. Because 0.28" displays are small, I had to put the MAX7219 chips on a separate board. To simply wiring, machined male and female pin headers are used to connect the boards,
The Eagle files have been included should you wish to have the boards commercially made or do as I did and make them yourself. I used the Toner method in making mine.
The column boardsSix column boards which hold all the displays need to be made. When soldering the male machine headers, start with the middle set and work to the outside edge. Use a matchstick or something similar to lift the socket enough so you can solder the pins easily. Use a small soldering iron with a fine tip and 0.5mm solder.
After you have created the column modules, I recommend you paint the display border with matt black paint. This will stop the white edges showing if a display isn't perfectly aligned with its neighbor. Also using a marker pen, number each column module. This helps when they are inserted into the MAX7219 motherboard.
When designing the motherboard and column boards, I didn't map the correct MAX7219 segments or digits to their corresponding display pins. I wanted to keep the PCB layout as simple as possible and adjust for any difference in software.
I recommend when it comes to the female machined sockets that you place them on the male pins of the column boards first and then solder the assembly in place while they are connected. This will mean the column pins will align exactly with their corresponding sockets. Should you need to remove them, having the column modules numbered will ensure they go back in the same position.
The MPU boardThe microprocessor board contains all the other components. The four buttons and two SMD capacitors are mounted on the back of the board and all other components are mounted on the front of the board. To keep a low profile, the FTDI pin header that is usually is soldered to the Arduino Pro Mini is now soldered directly to the PCB. Two wires go from the DTR and VCC pins directly down to the PCB.
The STL files for the front and back have been included. Both were printed using a 0.2 layer height and a brim. The front also has supports enabled that touched the build plate only.
The MAX7219 motherboard with its column modules slide in from the front. If you find it a bit loose, use a layer or two of masking tape as packing.
The MPU board is hot glued to the back.
The software requires that the MD_MAX72XX library has been installed in your Arduino IDE. I have provided my Hardware_Test_V1 sketch for testing. It will light up every segment so that you can check for shorts or broken wiring. Once the display works, upload Clock_V4 sketch.
/*---------------------------------------------------
* 7 Segment array clock
*
* Based on clock by Frugha (https://hackaday.io/project/169632-7-segment-display-array-clock)
*
* Modifications by John Bradnam (jbrad2089@gmail.com)
* - Changed hardware to use 0.28" 7 segment displays.
* - MAX7219 device, digit and segments order has been changed to simply an otherwise
* complex PCB board routing.
* - Hardware uses Arduino Mini Pro, DS1302 RTC and 4 push buttons
* - Added Alarm function with selectable alarm usic
* - Added Brightness setting
* - Added Setup screen to set time, date, alarm, music and brightness
* - Added Date screen (displays for two seconds when ENTER button is pressed
* Update 2020-06-16
* - Added date format option on configuration screen
* - Increased date screen timeout from 2 to 5 seconds
* - Added a configuarble choice of font for date display
*--------------------------------------------------*/
#include <MD_MAX72xx.h>
#include <SPI.h>
#include <TimeLib.h>
#include <DS1302RTC.h>
#include <EEPROM.h>
#include "button.h"
#include "Tunes.h"
#include "Digits.h"
#define HARDWARE_TYPE MD_MAX72XX::GENERIC_HW
#define MAX_DEVICES 18
#define LED_CLK 13 // or SCK (WHI)
#define LED_DATA 11 // or MOSI (BRN)
#define LED_CS 10 // or SS (YEL)
#define SPEAKER 2
#define SW_UP 3
#define SW_DOWN 4
#define RTC_CLK 5
#define RTC_IO 6
#define SW_ENTER 7
#define RTC_CE 8
#define SW_SELECT 9
DS1302RTC rtc(RTC_CE, RTC_IO, RTC_CLK);
// SPI hardware interface
MD_MAX72XX mx = MD_MAX72XX(HARDWARE_TYPE, LED_CS, MAX_DEVICES);
// Arbitrary pins
//MD_MAX72XX mx = MD_MAX72XX(HARDWARE_TYPE, LED_DATA, LED_CLK, LED_CS, MAX_DEVICES);
//EEPROM handling
#define EEPROM_ADDRESS 0
#define EEPROM_MAGIC 0x0BAD0DAD
typedef struct {
uint32_t magic;
bool alarm;
uint8_t minutes;
uint8_t hours;
bool format12hr;
uint8_t brightness;
uint8_t tune;
bool formatDmy;
bool squareFont;
} EEPROM_DATA;
EEPROM_DATA EepromData; //Current EEPROM settings
void clockButtonPressed(void);
void enterButtonPressed(void);
void downButtonPressed(void);
void upButtonPressed(void);
Button* clockButton;
Button* enterButton;
Button* downButton;
Button* upButton;
//Operating modes
bool inSubMenu = false;
#define SETUP_FLASH_RATE 200;
unsigned long setupTimeout;
bool setupDisplayState = false;
enum ClockButtonModesEnum { CLOCK, TIME_SET, DATE_SET, ALARM_SET, TUNE_SET, BRIGHT_SET, FORMAT_SET };
ClockButtonModesEnum clockMode = CLOCK;
enum TimeSetMenuEnum { TIME_HOUR, TIME_MIN, TIME_FORMAT };
TimeSetMenuEnum timeSetMode = TIME_HOUR;
enum DateSetMenuEnum { DATE_YEAR, DATE_MONTH, DATE_DAY };
DateSetMenuEnum dateSetMode = DATE_YEAR;
enum AlarmSetMenuEnum { ALARM_HOUR, ALARM_MIN, ALARM_STATE };
AlarmSetMenuEnum alarmSetMode = ALARM_HOUR;
enum FormatSetMenuEnum { DAY_MONTH, FONT_STYLE };
FormatSetMenuEnum formatSetMode = DAY_MONTH;
int lastSeconds = -1;
bool alarmRinging = false; //true when alarm is on
bool alarmCancelled = false; //alarm cancelled by user
bool musicPlaying = false; //true if playing a song
bool clockColon = false; //show/hide colon
int8_t dom[] = {31,28,31,30,31,30,31,31,30,31,30,31};
tmElements_t newTime; //Used to store new time
void setup()
{
Serial.begin(115200);
//Eprom
readEepromData();
//Initialise buttons
clockButton = new Button(SW_SELECT);
enterButton = new Button(SW_ENTER);
downButton = new Button(SW_DOWN);
downButton->Repeat(downButtonPressed);
upButton = new Button(SW_UP);
upButton->Repeat(upButtonPressed);
//Initialise sound
pinMode(SPEAKER,OUTPUT);
mx.begin();
mx.control(MD_MAX72XX::INTENSITY, EepromData.brightness); //0..MAX_INTENSITY
mx.update(MD_MAX72XX::OFF) ; //No Auto updating
setSyncProvider(rtc.get); // the function to get the time from the RTC
if(timeStatus() != timeSet)
{
Serial.println("RTC Sync Bad");
newTime.Year = CalendarYrToTm(2020);
newTime.Month = 5;
newTime.Day = 30;
newTime.Hour = 14;
newTime.Minute = 53;
newTime.Second = 0;
time_t t = makeTime(newTime);
setTime(t);
rtc.set(t);
if (rtc.set(t) != 0)
{
Serial.println("Set Time Failed");
}
}
newTime.Year = CalendarYrToTm(year());
newTime.Month = month();
newTime.Day = day();
newTime.Hour = hour();
newTime.Minute = minute();
newTime.Second = second();
Serial.println("Time: " + String(newTime.Hour) + ":" + String(newTime.Minute) + ":" + String(newTime.Second));
Serial.println("Date: " + String(newTime.Day) + "/" + String(newTime.Month) + "/" + String(tmYearToCalendar(newTime.Year)));
clockMode = CLOCK;
showTime(true);
}
void loop()
{
testButtons();
if (clockMode == CLOCK)
{
showTime(false);
if (EepromData.alarm && EepromData.hours == hour() && EepromData.minutes == minute())
{
if (!alarmCancelled)
{
alarmRinging = true;
playSong(melodies[EepromData.tune]);
}
}
else
{
alarmCancelled = false;
alarmRinging = false;
}
}
else
{
showSetup(false);
}
delay(100);
}
//---------------------------------------------------------------
//Test if any buttons have been pressed
void testButtons()
{
//Single press buttons
if (clockButton->Pressed())
{
clockButtonPressed();
}
if (enterButton->Pressed())
{
enterButtonPressed();
}
//Don't need to check result of pressed since the button handler will invoke its repeat function
upButton->Pressed();
downButton->Pressed();
}
//---------------------------------------------------------------
//Handle CLOCK btton
void clockButtonPressed()
{
if (cancelAlarm()) return;
inSubMenu = false;
clockMode = (clockMode == FORMAT_SET) ? CLOCK : (ClockButtonModesEnum)((int)clockMode + 1);
if (clockMode == CLOCK)
{
Serial.println("Saving Time: " + String(newTime.Hour) + ":" + String(newTime.Minute));
Serial.println(" From: " + String(hour()) + ":" + String(minute()));
if (newTime.Year != CalendarYrToTm(year()) || newTime.Month != month() || newTime.Day != day() || newTime.Hour != hour() || newTime.Minute != minute())
{
//Update time
Serial.println("Updating RTC");
newTime.Second = second();
time_t t = makeTime(newTime);
setTime(t);
rtc.set(t);
if (rtc.set(t) != 0)
{
Serial.println("Set Time Failed");
}
}
writeEepromData();
showTime(true);
}
else
{
if (clockMode == TIME_SET)
{
newTime.Year = CalendarYrToTm(year());
newTime.Month = month();
newTime.Day = day();
newTime.Hour = hour();
newTime.Minute = minute();
newTime.Second = second();
Serial.println("Loading Time: " + String(hour()) + ":" + String(minute()));
}
showSetup(true);
}
}
//---------------------------------------------------------------
//Handle ENTER btton
void enterButtonPressed()
{
if (cancelAlarm()) return;
if (clockMode != CLOCK)
{
if (!inSubMenu)
{
timeSetMode = TIME_HOUR;
dateSetMode = DATE_YEAR;
alarmSetMode = ALARM_HOUR;
formatSetMode = DAY_MONTH;
inSubMenu = true;
}
else
{
switch (clockMode)
{
case TIME_SET: timeSetMode = (timeSetMode == TIME_FORMAT) ? TIME_HOUR : (TimeSetMenuEnum)((int)timeSetMode + 1); break;
case DATE_SET: dateSetMode = (dateSetMode == DATE_DAY) ? DATE_YEAR : (DateSetMenuEnum)((int)dateSetMode + 1); break;
case ALARM_SET: alarmSetMode = (alarmSetMode == ALARM_STATE) ? ALARM_HOUR : (AlarmSetMenuEnum)((int)alarmSetMode + 1); break;
case FORMAT_SET: formatSetMode = (formatSetMode == FONT_STYLE) ? DAY_MONTH : (FormatSetMenuEnum)((int)formatSetMode + 1); break;
}
}
showSetup(true);
}
else
{
showDate(day(), month(), year());
delay(5000);
}
}
//---------------------------------------------------------------
//Handle DOWN btton
void downButtonPressed()
{
if (cancelAlarm()) return;
switch (clockMode)
{
case TIME_SET:
if (inSubMenu)
{
switch(timeSetMode)
{
case TIME_HOUR: newTime.Hour = (newTime.Hour + 24 - 1) % 24; break;
case TIME_MIN: newTime.Minute = (newTime.Minute + 60 - 1) % 60; break;
case TIME_FORMAT: EepromData.format12hr = !EepromData.format12hr; break;
}
showSetup(true);
}
break;
case DATE_SET:
if (inSubMenu)
{
switch(dateSetMode)
{
case DATE_YEAR: newTime.Year = ((newTime.Year - 30 + 100) - 1) % 100 + 30; break;
case DATE_MONTH: newTime.Month = ((newTime.Month - 1 + 12) - 1) % 12 + 1; break;
case DATE_DAY:
uint8_t md = daysInMonth(newTime.Year, newTime.Month);
newTime.Day = ((newTime.Day - 1 + md) - 1) % md + 1;
break;
}
showSetup(true);
}
break;
case ALARM_SET:
if (inSubMenu)
{
switch(alarmSetMode)
{
case ALARM_HOUR: EepromData.hours = (EepromData.hours + 24 - 1) % 24; break;
case ALARM_MIN: EepromData.minutes = (EepromData.minutes + 60 - 1) % 60; break;
case ALARM_STATE: EepromData.alarm = !EepromData.alarm; break;
}
showSetup(true);
}
break;
case TUNE_SET:
EepromData.tune = (EepromData.tune + NUM_OF_MELODIES - 1) % NUM_OF_MELODIES;
showSetup(true);
break;
case BRIGHT_SET:
EepromData.brightness = (EepromData.brightness + MAX_INTENSITY - 1) % MAX_INTENSITY;
mx.control(MD_MAX72XX::INTENSITY, EepromData.brightness); //0..MAX_INTENSITY
showSetup(true);
break;
case FORMAT_SET:
if (inSubMenu)
{
switch (formatSetMode)
{
case DAY_MONTH: EepromData.formatDmy = !EepromData.formatDmy; break;
case FONT_STYLE: EepromData.squareFont = !EepromData.squareFont; break;
}
}
showSetup(true);
break;
}
}
//---------------------------------------------------------------
//Handle UP btton
void upButtonPressed()
{
if (cancelAlarm()) return;
switch (clockMode)
{
case TIME_SET:
if (inSubMenu)
{
switch(timeSetMode)
{
case TIME_HOUR: newTime.Hour = (newTime.Hour + 1) % 24; break;
case TIME_MIN: newTime.Minute = (newTime.Minute + 1) % 60; break;
case TIME_FORMAT: EepromData.format12hr = !EepromData.format12hr; break;
}
showSetup(true);
}
break;
case DATE_SET:
if (inSubMenu)
{
switch(dateSetMode)
{
case DATE_YEAR: newTime.Year = ((newTime.Year - 30) + 1) % 100 + 30; break;
case DATE_MONTH: newTime.Month = ((newTime.Month - 1) + 1) % 12 + 1; break;
case DATE_DAY:
uint8_t md = daysInMonth(newTime.Year, newTime.Month);
newTime.Day = (newTime.Day % md) + 1;
break;
}
showSetup(true);
}
break;
case ALARM_SET:
if (inSubMenu)
{
switch(alarmSetMode)
{
case ALARM_HOUR: EepromData.hours = (EepromData.hours + 1) % 24; break;
case ALARM_MIN: EepromData.minutes = (EepromData.minutes + 1) % 60; break;
case ALARM_STATE: EepromData.alarm = !EepromData.alarm; break;
}
showSetup(true);
}
break;
case TUNE_SET:
EepromData.tune = (EepromData.tune + 1) % NUM_OF_MELODIES;
showSetup(true);
break;
case BRIGHT_SET:
EepromData.brightness = (EepromData.brightness + 1) % MAX_INTENSITY;
mx.control(MD_MAX72XX::INTENSITY, EepromData.brightness); //0..MAX_INTENSITY
showSetup(true);
break;
case FORMAT_SET:
if (inSubMenu)
{
switch (formatSetMode)
{
case DAY_MONTH: EepromData.formatDmy = !EepromData.formatDmy; break;
case FONT_STYLE: EepromData.squareFont = !EepromData.squareFont; break;
}
}
showSetup(true);
break;
}
}
//---------------------------------------------------------------
//Turn off the alarm if it is playing a tune
bool cancelAlarm()
{
if (musicPlaying)
{
musicPlaying = false;
alarmCancelled = alarmRinging;
return true;
}
yield();
return false;
}
//---------------------------------------------------------------
//Show the Setup menu and flash current item selected
void showSetup(bool force)
{
setupDisplayState = setupDisplayState | force;
force = force || (millis() > setupTimeout);
if (force)
{
setupTimeout = millis() + SETUP_FLASH_RATE;
bool on = setupDisplayState;
setupDisplayState = !setupDisplayState;
mx.clear();
if (on || !(clockMode == TIME_SET && !inSubMenu)) displayString(0,7,"TINE");
if (on || !(clockMode == TIME_SET && inSubMenu && timeSetMode == TIME_HOUR)) displayNumber(0,13,newTime.Hour,2,true);
if (on || !(clockMode == TIME_SET && inSubMenu && timeSetMode == TIME_MIN)) displayNumber(0,16,newTime.Minute,2,true);
if (on || !(clockMode == TIME_SET && inSubMenu && timeSetMode == TIME_FORMAT)) displayString(0,19,(EepromData.format12hr) ? "12HR" : "24HR");
if (on || !(clockMode == DATE_SET && !inSubMenu)) displayString(1,7,"DATE");
if (on || !(clockMode == DATE_SET && inSubMenu && dateSetMode == DATE_YEAR)) displayNumber(1,13,tmYearToCalendar(newTime.Year),4,true);
if (on || !(clockMode == DATE_SET && inSubMenu && dateSetMode == DATE_MONTH)) displayNumber(1,18,newTime.Month,2,true);
if (on || !(clockMode == DATE_SET && inSubMenu && dateSetMode == DATE_DAY)) displayNumber(1,21,newTime.Day,2,true);
if (on || !(clockMode == ALARM_SET && !inSubMenu)) displayString(2,6,"ALARN");
if (on || !(clockMode == ALARM_SET && inSubMenu && alarmSetMode == ALARM_HOUR)) displayNumber(2,13,EepromData.hours,2,true);
if (on || !(clockMode == ALARM_SET && inSubMenu && alarmSetMode == ALARM_MIN)) displayNumber(2,16,EepromData.minutes,2,true);
if (on || !(clockMode == ALARM_SET && inSubMenu && alarmSetMode == ALARM_STATE)) displayString(2,19,(EepromData.alarm) ? "ON" : "OFF");
if (on || !(clockMode == TUNE_SET && !inSubMenu)) displayString(3,7,"TUNE");
if (on || !(clockMode == TUNE_SET && inSubMenu))
{
switch(EepromData.tune)
{
case 0: displayString(3,13,"ELISE"); break;
case 1: displayString(3,13,"RANGE"); break;
case 2: displayString(3,13,"SUNSHINE"); break;
}
}
if (on || !(clockMode == BRIGHT_SET && !inSubMenu)) displayString(4,1,"BRIGHTNESS");
if (on || !(clockMode == BRIGHT_SET && inSubMenu)) displayNumber(4,13,EepromData.brightness,0,false);
if (on || !(clockMode == FORMAT_SET && !inSubMenu)) displayString(5,0,"DATE FORNAT");
if (on || !(clockMode == FORMAT_SET && inSubMenu && formatSetMode == DAY_MONTH)) displayString(5,13,(EepromData.formatDmy) ? "DD-NN" : "NN-DD");
if (on || !(clockMode == FORMAT_SET && inSubMenu && formatSetMode == FONT_STYLE)) displayString(5,19,(EepromData.squareFont) ? "FONT1" : "FONT2");
mx.update();
}
}
//---------------------------------------------------------------
//Display the current time on the display if changed
// force - always show time even if not changed
void showTime(bool force)
{
force = force || (lastSeconds != second());
if (force)
{
lastSeconds = second();
showTime(hour(), minute(), true, true, (second() & 0x01), (clockMode != CLOCK || !EepromData.format12hr));
}
}
//---------------------------------------------------------------
//Display the time on the display
// h - hour
// m - minute
// he - hour enable
// me - minute enable
// ce - colon enable
void showTime(int h, int m, bool he, bool me, bool ce, bool f24)
{
mx.clear();
if (he)
{
if (!f24 && h > 12)
{
h = h - 12;
}
if (h > 9 || f24)
{
displayLargeDigit(0, h / 10);
}
displayLargeDigit(5, h % 10);
}
if (me)
{
displayLargeDigit(13, m / 10);
displayLargeDigit(18, m % 10);
}
if (ce)
{
displayLargeDigit(11, 10);
}
mx.update();
}
//---------------------------------------------------------------
void showDate(int d, int m, int y)
{
#define XOFS 3
mx.clear();
if (EepromData.formatDmy)
{
displayDateDigit(XOFS+0, d / 10);
displayDateDigit(XOFS+4, d % 10);
displayDateDigit(XOFS+8, 10); //colon
displayDateDigit(XOFS+12, m / 10);
displayDateDigit(XOFS+16, m % 10);
}
else
{
displayDateDigit(XOFS+0, m / 10);
displayDateDigit(XOFS+4, m % 10);
displayDateDigit(XOFS+8, 10); //colon
displayDateDigit(XOFS+12, d / 10);
displayDateDigit(XOFS+16, d % 10);
}
displayNumber(5, 10, y, 0, false);
mx.update();
}
//---------------------------------------------------------------
// Write a square or small digit based of configuration setting
// x = 0 to 23
// v = large or square digit to display (0..10)
void displayDateDigit(uint8_t x, uint8_t v)
{
if (EepromData.squareFont)
{
displaySquareDigit(x, v);
}
else
{
displaySmallDigit(x, v);
}
}
//---------------------------------------------------------------
// Write a segment bit mask
// x = 0 to 23
// v = large digit to display (0..10)
void displayLargeDigit(uint8_t x, uint8_t v)
{
if (x < 24 && v < 11)
{
for (uint8_t row = 0; row < 6; row++)
{
for (uint8_t col = 0; col < 6; col++)
{
writePhysicalDigit(row, col + x, pgm_read_byte(&largeDigits[v][row][col]), false);
}
}
}
}
//---------------------------------------------------------------
// Write a segment bit mask
// x = 0 to 23
// v = large digit to display (0..10)
void displaySmallDigit(uint8_t x, uint8_t v)
{
if (x < 24 && v < 11)
{
for (uint8_t row = 0; row < 5; row++)
{
for (uint8_t col = 0; col < 3; col++)
{
writePhysicalDigit(row, col + x, pgm_read_byte(&smallDigits[v][row][col]), false);
}
}
}
}
//---------------------------------------------------------------
// Write a segment bit mask
// x = 0 to 23
// v = large digit to display (0..10)
void displaySquareDigit(uint8_t col, uint8_t v)
{
//Segment order d e f b c a _ g
uint8_t mask = ascii[v];
if (mask == B00000000)
{
//Hyphen
mask = B00000001;
}
if (mask & B00000100)
{
//seg A
writePhysicalDigit(0, col + 0, 0xff, false);
writePhysicalDigit(0, col + 1, 0xff, false);
writePhysicalDigit(0, col + 2, 0xff, false);
}
if (mask & B00010000)
{
//seg B
writePhysicalDigit(0, col + 2, 0xff, false);
writePhysicalDigit(1, col + 2, 0xff, false);
writePhysicalDigit(2, col + 2, 0xff, false);
}
if (mask & B00001000)
{
//seg C
writePhysicalDigit(2, col + 2, 0xff, false);
writePhysicalDigit(3, col + 2, 0xff, false);
writePhysicalDigit(4, col + 2, 0xff, false);
}
if (mask & B10000000)
{
//seg D
writePhysicalDigit(4, col + 0, 0xff, false);
writePhysicalDigit(4, col + 1, 0xff, false);
writePhysicalDigit(4, col + 2, 0xff, false);
}
if (mask & B01000000)
{
//seg C
writePhysicalDigit(2, col, 0xff, false);
writePhysicalDigit(3, col, 0xff, false);
writePhysicalDigit(4, col, 0xff, false);
}
if (mask & B00100000)
{
//seg E
writePhysicalDigit(0, col, 0xff, false);
writePhysicalDigit(1, col, 0xff, false);
writePhysicalDigit(2, col, 0xff, false);
}
if (mask & B00000001)
{
//seg D
writePhysicalDigit(2, col + 0, 0xff, false);
writePhysicalDigit(2, col + 1, 0xff, false);
writePhysicalDigit(2, col + 2, 0xff, false);
}
}
//---------------------------------------------------------------
// Write string of characters
uint8_t displayString(uint8_t row, uint8_t col, String s)
{
for (int i = 0; i < s.length(); i++)
{
byte c = (byte)s.charAt(i);
if (c == 0x2D)
{
c = 0x3D;
}
if (c < 0x30 || c > 0x5F)
{
c = 0x3F; //Used as a space character
}
c = c - 0x30;
writePhysicalDigit(row, col, ascii[c], true);
col = col + 1;
}
}
//---------------------------------------------------------------
// Write a number
uint8_t displayNumber(uint8_t row, uint8_t col, int number, int padding, bool leadingZeros)
{
if (padding == 0)
{
padding = (number > 0) ? floor(log10(number)) + 1 : 1;
}
col = col + padding;
bool first = true;
for (int i = 0; i < padding; i++)
{
col--;
if (number != 0 || first)
{
writePhysicalDigit(row, col, ascii[number % 10], true);
number = number / 10;
first = false;
}
else
{
writePhysicalDigit(row, col, ascii[(leadingZeros) ? 0 : 0x0F], true);
}
}
}
//---------------------------------------------------------------
// Write a segment bit mask
// row = 0 to 5
// col = 0 to 23
// v = segment mask
// erase = true will replace all old pattern, false will OR with existing
void writePhysicalDigit(uint8_t row, uint8_t col, uint8_t v, bool erase)
{
if (row < 6 && col < 24)
{
uint8_t dig = (col & 0x03) + ((row & 0x01) ? 4 : 0);
uint8_t dev = (col >> 2) * 3 + (row >> 1);
uint16_t c = ((uint16_t)dev << 3) | digitMap[dig];
if (!erase)
{
v = v | mx.getColumn(c);
}
mx.setColumn(c, v);
}
}
//---------------------------------------------------------------
//Return the days in a given year and month
//Feb has 28 unless leap year or the turn of a century
uint8_t daysInMonth(int y, int m)
{
return dom[m - 1] + ((m == 2 && (y % 4) == 0 && (y % 100) != 0) ? 1 : 0);
}
//---------------------------------------------------------------
//Write the EepromData structure to EEPROM
void writeEepromData()
{
//This function uses EEPROM.update() to perform the write, so does not rewrites the value if it didn't change.
EEPROM.put(EEPROM_ADDRESS,EepromData);
}
//---------------------------------------------------------------
//Read the EepromData structure from EEPROM, initialise if necessary
void readEepromData()
{
//Eprom
EEPROM.get(EEPROM_ADDRESS,EepromData);
//Serial.println("magic:" + String(EepromData.magic, 16) + ", alarm: " + String(EepromData.alarm) + ", time: " + String(EepromData.hours) + ":" + String(EepromData.minutes) + ", 12hr: " + String(EepromData.format12hr) + ", brightness: " + String(EepromData.brightness));
if (EepromData.magic != EEPROM_MAGIC)
{
Serial.println("Initialising EEPROM ...");
EepromData.magic = EEPROM_MAGIC;
EepromData.alarm = false;
EepromData.minutes = 30;
EepromData.hours = 5;
EepromData.format12hr = false;
EepromData.brightness = 8;
EepromData.tune = 0;
EepromData.formatDmy = false;
writeEepromData();
}
Serial.println("alarm: " + String(EepromData.alarm) + ", time: " + String(EepromData.hours) + ":" + String(EepromData.minutes) + ", 12hr: " + String(EepromData.format12hr) + ", brightness: " + String(EepromData.brightness));
}
//---------------------------------------------------------------
//Play a melody
int playSong(const uint16_t* melody)
{
//Play each note in the melody until the END_OF_TUNE note is encountered
musicPlaying = true;
int thisNote = 0;
uint16_t noteRaw = pgm_read_word(&melody[thisNote++]);
while (musicPlaying && noteRaw != END_OF_TUNE)
{
testButtons();
yield();
playNote(noteRaw);
noteRaw = pgm_read_word(&melody[thisNote++]);
} //while
delay(50);
}
//---------------------------------------------------------------
//Play a single note
void playNote(uint16_t noteRaw)
{
// to calculate the note duration, take one second divided by the note type.
// e.g. quarter note = 1000 / 4, eighth note = 1000/8, etc.
uint16_t frequency = noteRaw & 0x1FFF;
uint16_t duration = (noteRaw & 0xE000) >> 13;
if (duration == 7)
duration = 8;
uint16_t noteDuration = 1800 / duration;
if (frequency != REST)
{
tone(SPEAKER, frequency, noteDuration);
}
// to distinguish the notes, set a minimum time between them.
// the note's duration + 30% seems to work well:
uint16_t pauseBetweenNotes = (noteDuration * 13) / 10;
delay(pauseBetweenNotes);
if (frequency != REST)
{
// stop the tone playing:
noTone(SPEAKER);
}
}
/*---------------------------------------------------
* 7 Segment array clock
* define each digit in a 6x6 byte array
*--------------------------------------------------*/
#pragma once
//---------------------------------------------------
// Standard MAX7219 wiring
// Digit order 0,1,2,3,4,5
/*
// Segment order _ a b c d e f g
#define _______ B00000000
#define __cd___ B00011000
#define _bcde_g B00111101
#define abcdefg B01111111
#define __cdefg B00011111
#define __c____ B00010000
#define ab__efg B01100111
#define abcd_fg B01111011
#define abcde_g B01111101
#define ab_defg B01101111
#define _bc____ B00110000
#define a__defg B01001111
#define ____ef_ B00000110
#define ___defg B00001111
#define abc__fg B01110011
#define a___ef_ B01000110
#define _b_____ B00100000
#define ab___f_ B01100010
#define _bcd___ B00111000
#define a___ef_ B01000110
#define ab_____ B01100000
#define ____e__ B00000100
#define __cde_g B00011101
#define a____f_ B01000010
#define a_cdefg B01011111
#define ___de__ B00001100
#define _____f_ B00000010
#define ab___fg B01100011
#define a__def_ B01001110
#define __cde__ B00011100
#define a___efg B01000111
#define a__d___ B01001000
#define abc____ B01110000
#define _bc_ef_ B00110110
#define ___def_ B00001110
#define abc_ef_ B01110110
#define _bcdef_ B00111110
#define a__def_ B01001110
#define abcd___ B01111000
*/
//Segment order d e f b c a _ g
#define _______ B00000000
#define __cd___ B10001000
#define _bcde_g B11011001
#define abcdefg B11111101
#define __cdefg B11101001
#define __c____ B00001000
#define ab__efg B01110101
#define abcd_fg B10111101
#define abcde_g B11011101
#define ab_defg B11110101
#define _bc____ B00011000
#define a__defg B11100101
#define ____ef_ B01100000
#define ___defg B11100001
#define abc__fg B00111101
#define a___ef_ B01100100
#define _b_____ B00010000
#define ab___f_ B00110100
#define _bcd___ B10011000
#define a___ef_ B01100100
#define ab_____ B00010100
#define ____e__ B01000000
#define __cde_g B11001001
#define a____f_ B00100100
#define a_cdefg B11101101
#define ___de__ B11000000
#define _____f_ B00100000
#define ab___fg B00110101
#define a__def_ B11100100
#define __cde__ B11001000
#define a___efg B01100101
#define a__d___ B10000100
#define abc____ B00011100
#define _bc_ef_ B01111000
#define ___def_ B11100000
#define abc_ef_ B01111100
#define _bcdef_ B11111000
#define a__def_ B11100100
#define abcd___ B10011100
//Square Numbers
//ASCII Character Set
//Numbers 0 - 9
//Letters A - Z
//Segment order d e f b c a _ g
uint8_t ascii[] = {
B11111100, B00011000, B11010101, B10011101, B00111001, B10101101, B11101101, B00011100, B11111101, B10111101, B00000000, B00000000, B00000000, B00000001, B00000000, B00000000,
B00000000, B01111101, B11101001, B11100100, B11011001, B11100101, B01100101, B10111101, B01111001, B00011000, B11011000, B00000000, B11100000, B00000000, B01001001, B11001001,
B01110101, B00000000, B01000001, B10101101, B11100001, B11001000, B00000000, B00000000, B00000000, B10111001, B00000000, B11001100, B01010001, B10011100, B00000000, B10000000
};
//Digit sequence for each device (MAX7219)
uint8_t digitMap[] = {5, 2, 6, 4, 1, 7, 3, 0};
//------------------------------------------------------------
// Digits using logical coordinates
//------------------------------------------------------------
const int8_t largeDigits[11][6][6] PROGMEM =
{
{ //0
{ _______, _______, __cd___, _bcde_g, abcdefg, __cdefg },
{ _______, __c____, abcdefg, ab__efg, abcd_fg, abcdefg },
{ _______, abcde_g, ab_defg, _______, _bc____, a__defg },
{ _______, abcdefg, ____ef_, __c____, abcdefg, ____ef_ },
{ _______, abcdefg, __cdefg, abcdefg, a___efg, _______ },
{ _______, abc__fg, abcdefg, ab__efg, _______, _______ }
},
{ //1
{ _______, _______, _______, __c____, abcdefg, __cdefg },
{ _______, _______, _______, abcdefg, abcdefg, ____ef_ },
{ _______, _______, _bcde_g, abcdefg, a__defg, _______ },
{ _______, _bc____, abcdefg, abcdefg, ____ef_, _______ },
{ _______, abcdefg, abcdefg, a___ef_, _______, _______ },
{ _b_____, abcdefg, abcdefg, _______, _______, _______ }
},
{ //2
{ _______, _______, __cd___, _bcde_g, abcdefg, __cdefg },
{ _______, _bc____, abcdefg, ab___f_, abcd_fg, abcdefg },
{ _______, _______, _______, _bcd___, abcdefg, a___ef_ },
{ _______, _______, _bcde_g, abcdefg, a___ef_, _______ },
{ __c____, abcdefg, abcdefg, a___ef_, _______, _______ },
{ _b_____, abc__fg, abcdefg, abcdefg, ab__efg, _______ }
},
{ //3
{ _______, _______, __cd___, abcdefg, abcdefg, __cdefg },
{ _______, _b_____, abcdefg, ab___f_, abcd_fg, abcdefg },
{ _______, _______, __cd___, _bcde_g, abcdefg, a___ef_ },
{ _______, _______, ab_____, abc__fg, abcdefg, ____e__ },
{ _______, __cd___, __cde_g, _bcde_g, abcdefg, ____ef_ },
{ _b_____, abc__fg, abcdefg, abcdefg, a____f_, _______ }
},
{ //4
{ _______, _______, _bcd___, abcdefg, __c____, abcdefg },
{ _______, __c____, abcdefg, a___ef_, abcde_g, a__defg },
{ _______, abcde_g, abcdefg, _bcd___, abcdefg, ____ef_ },
{ __c____, abcdefg, abcdefg, abcdefg, abcdefg, _______ },
{ _______, _______, __c____, abcdefg, ____ef_, _______ },
{ _______, _______, abcdefg, ab__efg, _______, _______ }
},
{ //5
{ _______, _______, _bcde_g, abcdefg, abcdefg, abcdefg },
{ _______, _bc____, abcdefg, a_cdefg, ___de__, _______ },
{ _______, _______, abc__fg, abcdefg, abcdefg, ____ef_ },
{ _______, _______, _______, _bc____, abcdefg, ____ef_ },
{ _______, __cde_g, __cde_g, _bcde_g, abcdefg, _____f_ },
{ _b_____, abcdefg, abcdefg, ab__efg, a____f_, _______ }
},
{ //6
{ _______, _______, _______, __cd___, abcdefg, ____ef_ },
{ _______, _______, _bcde_g, abcdefg, a____f_, _______ },
{ _______, _bcd___, abcdefg, abcdefg, abcdefg, ____e__ },
{ __c____, abcdefg, a___ef_, ab_____, abcdefg, ____ef_ },
{ _bc____, abcdefg, __cdefg, _bcde_g, abcdefg, _______ },
{ _______, abc__fg, abcdefg, ab__efg, _______, _______ }
},
{ //7
{ _______, _bc____, abcdefg, abcdefg, abcdefg, __cdefg },
{ _______, _b_____, ab___fg, ab___fg, abcdefg, abcdefg },
{ _______, _______, _______, _bcde_g, abcdefg, a___ef_ },
{ _______, _______, _bcde_g, abcdefg, ab__efg, _______ },
{ _______, _bcde_g, abcdefg, a___ef_, _______, _______ },
{ _b_____, abcdefg, abcdefg, _______, _______, _______ }
},
{ //8
{ _______, _______, __cd___, abcdefg, abcdefg, __cdefg },
{ _______, _bc____, abcdefg, ab___f_, abcd_fg, abcdefg },
{ _______, _b_____, abcdefg, _bcde_g, abcdefg, a___ef_ },
{ _______, _bcde_g, ab__efg, abc__fg, abcdefg, ____e__ },
{ _bc____, abcdefg, __cde_g, _bcde_g, abcdefg, ____ef_ },
{ _______, abc__fg, abcdefg, abcdefg, a____f_, _______ }
},
{ //9
{ _______, _______, __cde_g, abcdefg, __cdefg, ___de__ },
{ _______, _bcd___, abcdefg, ab___fg, abcd_fg, abcdefg },
{ _______, abcdefg, a_cdefg, __cde__, abcdefg, a__defg },
{ _______, ab_____, abcd_fg, abcdefg, abcdefg, _____f_ },
{ _______, __cd___, abcdefg, ab__efg, _______, _______ },
{ _b_____, abcdefg, a___ef_, _______, _______, _______ }
},
{ //Colon
{ _______, _______, _______, _______, _______, _______ },
{ _______, __cde_g, _______, _______, _______, _______ },
{ _______, _______, _______, _______, _______, _______ },
{ _______, _______, _______, _______, _______, _______ },
{ __cde_g, _______, _______, _______, _______, _______ },
{ _______, _______, _______, _______, _______, _______ }
}
};
const int8_t smallDigits[11][5][3] PROGMEM =
{
{ //0
{ a___ef_, a__d___, abc____ },
{ _bc_ef_, _______, _bc_ef_ },
{ _bc_ef_, _______, _bc_ef_ },
{ _bc_ef_, _______, _bc_ef_ },
{ ___def_, a__d___, _bcd___ }
},
{ //1
{ _______, abc_ef_, _______ },
{ _______, _bc_ef_, _______ },
{ _______, _bc_ef_, _______ },
{ _______, _bc_ef_, _______ },
{ _______, _bcdef_, _______ }
},
{ //2
{ a__def_, a__d___, abc____ },
{ _______, _______, _bc_ef_ },
{ a___ef_, a__d___, _bcd___ },
{ _bc_ef_, _______, _______ },
{ ___def_, a__d___, abcd___ }
},
{ //3
{ a__def_, a__d___, abc____ },
{ _______, _______, _bc_ef_ },
{ _______, a__def_, _bc____ },
{ _______, _______, _bc_ef_ },
{ a__def_, a__d___, _bcd___ }
},
{ //4
{ abc_ef_, _______, abc_ef_ },
{ _bc_ef_, _______, _bc_ef_ },
{ ___def_, a__d___, _bc____ },
{ _______, _______, _bc_ef_ },
{ _______, _______, _bcdef_ }
},
{ //5
{ a___ef_, a__d___, abcd___ },
{ _bc_ef_, _______, _______ },
{ ___def_, a__d___, abc____ },
{ _______, _______, _bc_ef_ },
{ a__def_, a__d___, _bcd___ }
},
{ //6
{ a___ef_, a__d___, abcd___ },
{ _bc_ef_, _______, _______ },
{ ____ef_, a__d___, abc____ },
{ _bc_ef_, _______, _bc_ef_ },
{ ___def_, a__d___, _bcd___ }
},
{ //7
{ a__def_, a__d___, abc____ },
{ _______, _______, _bc_ef_ },
{ _______, _bc_ef_, _______ },
{ _______, _bc_ef_, _______ },
{ _______, _bcdef_, _______ },
},
{ //8
{ a___ef_, a__d___, abc____ },
{ _bc_ef_, _______, _bc_ef_ },
{ ____ef_, a__d___, _bc____ },
{ _bc_ef_, _______, _bc_ef_ },
{ ___def_, a__d___, _bcd___ }
},
{ //9
{ a___ef_, a__d___, abc____ },
{ _bc_ef_, _______, _bc_ef_ },
{ ___def_, a__d___, _bc____ },
{ _______, _______, _bc_ef_ },
{ a__def_, a__d___, _bcd___ }
},
{ //Hyphen
{ _______, _______, _______ },
{ _______, _______, _______ },
{ a__def_, a__d___, abcd___ },
{ _______, _______, _______ },
{ _______, _______, _______ }
}
};
/*
Class: Button
Author: John Bradnam (jbrad2089@gmail.com)
Purpose: Arduino library to handle buttons
*/
#pragma once
#include "Arduino.h"
#define DEBOUNCE_DELAY 10
//Repeat speed
#define REPEAT_START_SPEED 500
#define REPEAT_INCREASE_SPEED 50
#define REPEAT_MAX_SPEED 50
class Button
{
public:
//Simple constructor
Button(int pin);
Button(int name, int pin);
Button(int name, int pin, int analogLow, int analogHigh, bool activeLow = true);
//Background function called when in a wait or repeat loop
void Background(void (*pBackgroundFunction)());
//Repeat function called when button is pressed
void Repeat(void (*pRepeatFunction)());
//Test if button is pressed
bool IsDown(void);
//Test whether button is pressed and released
//Will call repeat function if one is provided
bool Pressed();
//Return button state (HIGH or LOW) - LOW = Pressed
int State();
//Return button name
int Name();
private:
int _name;
int _pin;
bool _range;
int _low;
int _high;
bool _activeLow;
void (*_repeatCallback)(void);
void (*_backgroundCallback)(void);
};
/*
Class: Button
Author: John Bradnam (jbrad2089@gmail.com)
Purpose: Arduino library to handle buttons
*/
#include "Button.h"
Button::Button(int pin)
{
_name = pin;
_pin = pin;
_range = false;
_low = 0;
_high = 0;
_backgroundCallback = NULL;
_repeatCallback = NULL;
pinMode(_pin, INPUT_PULLUP);
}
Button::Button(int name, int pin)
{
_name = name;
_pin = pin;
_range = false;
_low = 0;
_high = 0;
_backgroundCallback = NULL;
_repeatCallback = NULL;
pinMode(_pin, INPUT_PULLUP);
}
Button::Button(int name, int pin, int analogLow, int analogHigh, bool activeLow)
{
_name = name;
_pin = pin;
_range = true;
_low = analogLow;
_high = analogHigh;
_activeLow = activeLow;
_backgroundCallback = NULL;
_repeatCallback = NULL;
pinMode(_pin, INPUT);
}
//Set function to invoke in a delay or repeat loop
void Button::Background(void (*pBackgroundFunction)())
{
_backgroundCallback = pBackgroundFunction;
}
//Set function to invoke if repeat system required
void Button::Repeat(void (*pRepeatFunction)())
{
_repeatCallback = pRepeatFunction;
}
bool Button::IsDown()
{
if (_range)
{
int value = analogRead(_pin);
return (value >= _low && value < _high);
}
else
{
return (digitalRead(_pin) == LOW);
}
}
//Tests if a button is pressed and released
// returns true if the button was pressed and released
// if repeat callback supplied, the callback is called while the key is pressed
bool Button::Pressed()
{
bool pressed = false;
if (IsDown())
{
unsigned long wait = millis() + DEBOUNCE_DELAY;
while (millis() < wait)
{
if (_backgroundCallback != NULL)
{
_backgroundCallback();
}
}
if (IsDown())
{
//Set up for repeat loop
if (_repeatCallback != NULL)
{
_repeatCallback();
}
unsigned long speed = REPEAT_START_SPEED;
unsigned long time = millis() + speed;
while (IsDown())
{
if (_backgroundCallback != NULL)
{
_backgroundCallback();
}
if (_repeatCallback != NULL && millis() >= time)
{
_repeatCallback();
unsigned long faster = speed - REPEAT_INCREASE_SPEED;
if (faster >= REPEAT_MAX_SPEED)
{
speed = faster;
}
time = millis() + speed;
}
}
pressed = true;
}
}
return pressed;
}
//Return current button state
int Button::State()
{
if (_range)
{
int value = analogRead(_pin);
if (_activeLow)
{
return (value >= _low && value < _high) ? LOW : HIGH;
}
else
{
return (value >= _low && value < _high) ? HIGH : LOW;
}
}
else
{
return digitalRead(_pin);
}
}
//Return current button name
int Button::Name()
{
return _name;
}
Comments