Hackster is hosting Hackster Holidays, Ep. 6: Livestream & Giveaway Drawing. Watch previous episodes or stream live on Monday!Stream Hackster Holidays, Ep. 6 on Monday!
John Bradnam
Published © GPL3+

Cube Clock

A tiny 8x8 matrix clock that automatically rotates the display when the cube is rotated.

IntermediateFull instructions provided8 hours1,207

Things used in this project

Hardware components

Microchip ATtiny1614 Microprocessor
×1
Real Time Clock (RTC)
Real Time Clock (RTC)
DS1307 SOIC 8
×1
CR1220 Battery and SMD holder
×1
32.768 kHz Crystal
32.768 kHz Crystal
×1
Tactile Switch, Top Actuated
Tactile Switch, Top Actuated
17mm shaft with button tops
×1
Passive Components
0805 SMD resistors - 2 x 39K, 1 x 22K, 1 x 0 ohm; 1 x 0.1uF0805 SMD capacitor; 1 x 47uF/16V 3528 Tantalum capacitor
×1
Mercury switch
4mm diameter or smaller
×4
LED Dot Matrix Display, Red
LED Dot Matrix Display, Red
8x8 Red Matrix module that incorporates a MAX7219 driver IC. See description further down in text.
×1

Software apps and online services

Arduino IDE
Arduino IDE

Hand tools and fabrication machines

3D Printer (generic)
3D Printer (generic)
Soldering iron (generic)
Soldering iron (generic)

Story

Read more

Custom parts and enclosures

STL Files

STL for 3D printing

Schematics

Schematic

PCB

Eagle Files

Schematic and PCB in Eagle format

Code

CubeClockV1.ino

C/C++
/**
 * ATtiny1614 Cube Clock
 * John Bradnam (jbrad2089@gmail.com)
 * 
 * 2021-09-06 - Initial Code Base
 *
 * ---------------------------------------
 * ATtiny1614 Pins mapped to Ardunio Pins
 *
 *             +--------+
 *         VCC + 1   14 + GND
 * (SS)  0 PA4 + 2   13 + PA3 10 (SCK)
 *       1 PA5 + 3   12 + PA2 9  (MISO)
 * (DAC) 2 PA6 + 4   11 + PA1 8  (MOSI)
 *       3 PA7 + 5   10 + PA0 11 (UPDI)
 * (RXD) 4 PB3 + 6    9 + PB0 7  (SCL)
 * (TXD) 5 PB2 + 7    8 + PB1 6  (SDA)
 *             +--------+
 *             
 *             
 * BOARD: ATtiny1614/1604/814/804/414/404/214/204
 * Chip: ATtiny1614
 * Clock Speed: 20MHz
 * millis()/micros(): "TCD0 (1 series only, default there)"
 * Programmer: jtag2updi (megaTinyCore)
 * ----------------------------------------
 */

#include <Wire.h>
#include <RTClib.h>

//DS1307
#define _SCL 9         //PA2
#define _SDA 8         //PA1

#include "Memory.h"
#include "Display.h"
#include "Switches.h"

enum STATES { CLOCK_TIME, SET_DATE, SET_FORMAT, SET_FONT, SET_BRIGHTNESS, SET_HOUR, SET_MINUTE };
STATES clockState = CLOCK_TIME;

#define FLASH_TIME 200          //Time in mS to flash digit being set
#define STEP_TIME 350           //Time in mS for auto increment or decrement of time

long flashTimeout = 0;          //Flash timeout when setting clock or alarm
bool flashOn = false;           //Used to flash display when setting clock or alarm
long stepTimeout = 0;           //Set time speed for auto increment or decrement of time

int lastHours = -1;             //Used to test if hour changed
int lastMinutes = -1;           //Used to test if minute changed
int lastSecs = -1;              //Used to test if second changed
int setH = 0;                   //Hour being set
int setM = 0;                   //Minute being set
bool updateTime = true;         //Force display of time

char timeBuffer[16];            //Used to create time string
char dateBuffer[16];            //Used to create date string
 
//----------------------------------------------------------------------
// Hardware Setup

RTC_DS1307 rtc;

void setup() 
{
  //Eprom
  readEepromData();
  
  setupSwitches();
  setupDisplay();

  //Tell Wire handler that we are using the alternative SDA and SCL pins
  Wire.pins(PIN_PA1,PIN_PA2);       // SDA pin, SCL pin
  Wire.usePullups();                // Use pullup resistors on SDA and SCL pins
  if (!rtc.begin())
  {
    drawString("Cannot find RTC",FONT5X7);
  }
  else if (!rtc.isrunning()) 
  {
    drawString("RTC lost power",FONT5X7);
    rtc.adjust(DateTime(2021, 9, 5, 7, 55, 0));
    drawString("RTC reset",FONT5X7);
  }
  drawString("Cube Time",FONT5X7);
}

//----------------------------------------------------------------------
// Main program loop
void loop() 
{
  updateOrientation();      //Read the mecury switches to get cube orientation
  updateTime |= testSetupButton();
  switch (clockState)
  {
    case CLOCK_TIME: flipTimeMode(); break;
    case SET_DATE: dateMode(); break;
    case SET_FORMAT: formatMode(); break;
    case SET_FONT: fontMode(); break;
    case SET_BRIGHTNESS: brightnessMode(); break;
    case SET_HOUR: hourMode(); break;
    case SET_MINUTE: minuteMode(); break;
  }
}
  
//----------------------------------------------------------------------
// In time display mode
void flipTimeMode()
{
  
  if (isScrollComplete() && clockState == CLOCK_TIME)
  {
    DateTime newTime = rtc.now();
    int hours = newTime.hour();
    int minutes = newTime.minute();
    int secs = newTime.second() / ((EepromData.clockDates == TIME_ONLY) ? 3 : 5);
  
    if (updateTime || hours != lastHours || minutes != lastMinutes || secs != lastSecs)
    {
      updateTime = false;
      lastHours = hours;
      lastMinutes = minutes;
      lastSecs = secs;

      dateBuffer[0] = '\0';
      switch(EepromData.clockDates)
      {
        case TIME_DMY: sprintf(dateBuffer," %02d;%02d;%04d",newTime.day(),newTime.month(),newTime.year()); break;
        case TIME_MDY: sprintf(dateBuffer," %02d;%02d;%04d",newTime.month(),newTime.day(),newTime.year()); break;
      }
      char c;
      switch(EepromData.clockFormat)
      {
        case HOUR_24: 
          sprintf(timeBuffer,"%d:%02d",hours,minutes);
          break;
        
        case HOUR_12: 
          int h = (hours == 0) ? 12 : (hours > 12) ? hours - 12 : hours;
          if (EepromData.clockFont == FONT_HORZ_SMALL)
          {
            c = (hours < 12) ? 'A' : 'P';
            sprintf(timeBuffer,"%d:%02d_%cM",h,minutes,c);
          }
          else
          {
            c = (hours < 12) ? '[' : '\\';
            sprintf(timeBuffer,"%d:%02d%c",h,minutes,c);
          }
          break;
      }
      switch(EepromData.clockFont)
      {
        case FONT_HORZ_SMALL:
          drawString(String(timeBuffer) + String(dateBuffer),FONT3X5);
          break;

        case FONT_HORZ_LARGE:
        case FONT_VERT:
          drawString(String(timeBuffer) + String(dateBuffer),FONT5X7);
          break;
      }
    }
  }
}

//----------------------------------------------------------------------
// Test for setup button and handle the initial setup
bool testSetupButton()
{ 
  bool updateTime = false; 
  if (getButtonState() == SET)
  {
    delay(10);
    if (getButtonState() == SET)
    {
      scrollTerminate();
      displayClear();
      forceHorizontal = true;
      
      DateTime currentTime = rtc.now();
      clockState = (clockState == SET_MINUTE) ? CLOCK_TIME : (STATES)((int)clockState + 1);
      switch (clockState)
      {
        case SET_DATE:
          displayDate(EepromData.clockDates, true);
          break;
          
        case SET_FORMAT:
          displayFormat(EepromData.clockFormat, true);
          break;
        
        case SET_FONT:
          displayFont(EepromData.clockFont, true);
          break;
        
        case SET_BRIGHTNESS:
          displayBrightness(EepromData.brightness, true);
          break;
        
        case SET_HOUR: 
          setH = currentTime.hour();
          setM  = currentTime.minute();
          flashTimeout = millis() + FLASH_TIME;
          flashOn = false;
          displayNumber(setH, true, flashOn);
          break;

        case SET_MINUTE:
          flashTimeout = millis() + FLASH_TIME;
          flashOn = false;
          displayNumber(setM, true, flashOn);
          break;

        case CLOCK_TIME:
          //Set RTC
          rtc.adjust(DateTime(currentTime.year(), currentTime.month(), currentTime.day(), setH, setM, 0));
          writeEepromData();
          //force update
          updateTime = true;
          forceHorizontal = false;
          break;
      }
      //Wait until button is released
      while (getButtonState())
      {
        delay(10);
      }
    }
  }
  return updateTime;
}

//----------------------------------------------------------------------
// In set date  mode
void dateMode()
{
  if (millis() > flashTimeout)
  {
    flashTimeout = millis() + FLASH_TIME;
    flashOn = !flashOn;
    displayDate(EepromData.clockDates, flashOn);
  }
  if (millis() > stepTimeout)
  {
    if (getButtonState() == UP)
    {
      EepromData.clockDates = (EepromData.clockDates == TIME_MDY) ? TIME_ONLY : (DATES)((int)EepromData.clockDates + 1);
      displayDate(EepromData.clockDates, flashOn);
      stepTimeout = millis() + STEP_TIME;
    }
    else if (getButtonState() == DOWN)
    {
      EepromData.clockDates = (EepromData.clockDates == TIME_ONLY) ? TIME_MDY : (DATES)((int)EepromData.clockDates - 1);
      displayDate(EepromData.clockDates, flashOn);
      stepTimeout = millis() + STEP_TIME;
    }
  }
}

//----------------------------------------------------------------------
// In set format  mode
void formatMode()
{
  if (millis() > flashTimeout)
  {
    flashTimeout = millis() + FLASH_TIME;
    flashOn = !flashOn;
    displayFormat(EepromData.clockFormat, flashOn);
  }
  if (millis() > stepTimeout)
  {
    if (getButtonState() == UP)
    {
      EepromData.clockFormat = (EepromData.clockFormat == HOUR_12) ? HOUR_24 : (FORMATS)((int)EepromData.clockFormat + 1);
      displayFormat(EepromData.clockFormat, flashOn);
      stepTimeout = millis() + STEP_TIME;
    }
    else if (getButtonState() == DOWN)
    {
      EepromData.clockFormat = (EepromData.clockFormat == HOUR_24) ? HOUR_12 : (FORMATS)((int)EepromData.clockFormat - 1);
      displayFormat(EepromData.clockFormat, flashOn);
      stepTimeout = millis() + STEP_TIME;
    }
  }
}

//----------------------------------------------------------------------
// In set font  mode
void fontMode()
{
  if (millis() > flashTimeout)
  {
    flashTimeout = millis() + FLASH_TIME;
    flashOn = !flashOn;
    displayFont(EepromData.clockFont, flashOn);
  }
  if (millis() > stepTimeout)
  {
    if (getButtonState() == UP)
    {
      EepromData.clockFont = (EepromData.clockFont == FONT_VERT) ? FONT_HORZ_SMALL : (FONTS)((int)EepromData.clockFont + 1);
      displayFont(EepromData.clockFont, flashOn);
      stepTimeout = millis() + STEP_TIME;
    }
    else if (getButtonState() == DOWN)
    {
      EepromData.clockFont = (EepromData.clockFont == FONT_HORZ_SMALL) ? FONT_VERT : (FONTS)((int)EepromData.clockFont - 1);
      displayFont(EepromData.clockFont, flashOn);
      stepTimeout = millis() + STEP_TIME;
    }
  }
}

//----------------------------------------------------------------------
// In set hour  mode
void brightnessMode()
{
  if (millis() > flashTimeout)
  {
    flashTimeout = millis() + FLASH_TIME;
    flashOn = !flashOn;
    displayBrightness(EepromData.brightness, flashOn);
  }
  if (millis() > stepTimeout)
  {
    if (getButtonState() == UP)
    {
      EepromData.brightness = (EepromData.brightness + 1) % 8;
      EepromData.clockFont = (EepromData.brightness == FONT_VERT) ? FONT_HORZ_SMALL : (FONTS)((int)EepromData.clockFont + 1);
      displayBrightness(EepromData.brightness, flashOn);
      displayIntensity(EepromData.brightness);
      stepTimeout = millis() + STEP_TIME;
    }
    else if (getButtonState() == DOWN)
    {
      EepromData.brightness = (EepromData.brightness + 7) % 8;
      displayBrightness(EepromData.brightness, flashOn);
      displayIntensity(EepromData.brightness);
      stepTimeout = millis() + STEP_TIME;
    }
  }
}

//----------------------------------------------------------------------
// In set hour  mode
void hourMode()
{
  if (millis() > flashTimeout)
  {
    flashTimeout = millis() + FLASH_TIME;
    flashOn = !flashOn;
    displayNumber(setH, true, flashOn);
    //showLetter(H, flashOn);
  }
  if (millis() > stepTimeout)
  {
    if (getButtonState() == UP)
    {
      setH = (setH + 1) % 24;
      displayNumber(setH, true, flashOn);
      stepTimeout = millis() + STEP_TIME;
    }
    else if (getButtonState() == DOWN)
    {
      setH = (setH + 23) % 24;
      displayNumber(setH, true, flashOn);
      stepTimeout = millis() + STEP_TIME;
    }
  }
}

//----------------------------------------------------------------------
// In set minute  mode
void minuteMode()
{
  if (millis() > flashTimeout)
  {
    flashTimeout = millis() + FLASH_TIME;
    flashOn = !flashOn;
    displayNumber(setM, true, flashOn);
    //showLetter(M, flashOn);
  }
  if (millis() > stepTimeout)
  {
    if (getButtonState() == UP)
    {
      setM = (setM + 1) % 60;
      displayNumber(setM, true, flashOn);
      //showLetter(M, flashOn);
      stepTimeout = millis() + STEP_TIME;
    }
    else if (getButtonState() == DOWN)
    {
      setM = (setM + 59) % 60;
      displayNumber(setM, true, flashOn);
      //showLetter(M, flashOn);
      stepTimeout = millis() + STEP_TIME;
    }
  }
}

Display.h

C/C++
/**
 * ATtiny1614 Cube Clock
 * John Bradnam (jbrad2089@gmail.com)
 * 
 * 2021-09-06 - Create Matrix display functions for clock
 *
*/

#pragma once

#include <LedControl.h>
#include "Memory.h"
#include "Switches.h"

//MAX7219
#define CLK 1          //PA5
#define LOAD 2         //PA6
#define DIN 3          //PA7

enum FONTSIZE {FONT3X5,FONT5X7};

#define SCROLL_SPEED 40                //Speed at which text is scrolled
String scrollText;                     //Used to store scrolling text
volatile int8_t scrollDelay;           //Used to store scroll delay
volatile int8_t scrollCharPos;         //Current character position in text being scrolled
volatile int8_t scrollCharCol;         //Next column in character to display
volatile int8_t scrollOffScreen;       //Extra columns required to scroll last character off screen
volatile FONTSIZE scrollFont;          //Font used when scrolling text
volatile bool forceHorizontal;         //Set TRUE to override vertical mode during setup
byte displayBuffer[8];                 //Double buffer for fast updates

//----------------------------------------------------------------------
// Font definitions

#define ASCII_OFFSET 32
#define SPACE_5X7 64
#define BRIGHT_1 32
#define TM_5X7 40
#define DM_5X7 41
#define MD_5X7 42
#define T4_5X7 43
#define T2_5X7 44
#define FS_5X7 45
#define FL_5X7 46
#define FV_5X7 47
const uint8_t font5x7 [61][8] PROGMEM = {
  {0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, //   - Brightness Level 1
  {0x80, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // ! - Brightness Level 3
  {0x80, 0xC0, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00}, // " - Brightness Level 5
  {0x80, 0xC0, 0xE0, 0xF0, 0x00, 0x00, 0x00, 0x00}, // # - Brightness Level 7
  {0x80, 0xC0, 0xE0, 0xF0, 0xF8, 0x00, 0x00, 0x00}, // $ - Brightness Level 9
  {0x80, 0xC0, 0xE0, 0xF0, 0xF8, 0xFC, 0x00, 0x00}, // % - Brightness Level 11
  {0x80, 0xC0, 0xE0, 0xF0, 0xF8, 0xFC, 0xFE, 0x00}, // & - Brightness Level 13
  {0x80, 0xC0, 0xE0, 0xF0, 0xF8, 0xFC, 0xFE, 0xFF}, // ' - Brightness Level 15
  {0x01, 0x0F, 0x01, 0xE0, 0x10, 0x60, 0x10, 0xE0}, // ( - TM
  {0x1C, 0x14, 0x1F, 0xE0, 0x10, 0x60, 0x10, 0xE0}, // ) - DM
  {0x0E, 0x01, 0x06, 0x01, 0x0E, 0xE0, 0xA0, 0xF8}, // * - MD
  {0x12, 0x19, 0x15, 0x52, 0x60, 0x50, 0xF8, 0x40}, // + - 24
  {0x12, 0x1F, 0x10, 0x00, 0x90, 0xC8, 0xA8, 0x90}, // , - 12
  {0x1F, 0x05, 0x05, 0x01, 0x90, 0xA8, 0xA8, 0x48}, // - - FS
  {0x1F, 0x05, 0x05, 0x01, 0xF8, 0x80, 0x80, 0x80}, // . - FL
  {0x1F, 0x05, 0x05, 0x01, 0x78, 0x80, 0x80, 0x78}, // / - FV
  {0x3E, 0x51, 0x49, 0x45, 0x3E, 0x00, 0x00, 0x00}, // 0 
  {0x00, 0x42, 0x7F, 0x40, 0x00, 0x00, 0x00, 0x00}, // 1 
  {0x42, 0x61, 0x51, 0x49, 0x46, 0x00, 0x00, 0x00}, // 2 
  {0x21, 0x41, 0x45, 0x4B, 0x31, 0x00, 0x00, 0x00}, // 3 
  {0x18, 0x14, 0x12, 0x7F, 0x10, 0x00, 0x00, 0x00}, // 4 
  {0x27, 0x45, 0x45, 0x45, 0x39, 0x00, 0x00, 0x00}, // 5 
  {0x3C, 0x4A, 0x49, 0x49, 0x30, 0x00, 0x00, 0x00}, // 6 
  {0x01, 0x71, 0x09, 0x05, 0x03, 0x00, 0x00, 0x00}, // 7 
  {0x36, 0x49, 0x49, 0x49, 0x36, 0x00, 0x00, 0x00}, // 8 
  {0x06, 0x49, 0x49, 0x29, 0x1E, 0x00, 0x00, 0x00}, // 9 
  {0x00, 0x36, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00}, // : 
  {0x60, 0x10, 0x08, 0x04, 0x03, 0x00, 0x00, 0x00}, // ; - /
  {0x08, 0x14, 0x22, 0x41, 0x00, 0x00, 0x00, 0x00}, // <
  {0x14, 0x14, 0x14, 0x14, 0x14, 0x00, 0x00, 0x00}, // =
  {0x00, 0x41, 0x22, 0x14, 0x08, 0x00, 0x00, 0x00}, // >
  {0x02, 0x01, 0x51, 0x09, 0x06, 0x00, 0x00, 0x00}, // ? 
  {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // @ - Space 
  {0x7C, 0x12, 0x11, 0x12, 0x7C, 0x00, 0x00, 0x00}, // A 
  {0x7F, 0x49, 0x49, 0x49, 0x36, 0x00, 0x00, 0x00}, // B 
  {0x3E, 0x41, 0x41, 0x41, 0x22, 0x00, 0x00, 0x00}, // C 
  {0x7F, 0x41, 0x41, 0x22, 0x1C, 0x00, 0x00, 0x00}, // D 
  {0x7F, 0x49, 0x49, 0x49, 0x41, 0x00, 0x00, 0x00}, // E 
  {0x7F, 0x09, 0x09, 0x09, 0x01, 0x00, 0x00, 0x00}, // F 
  {0x3E, 0x41, 0x49, 0x49, 0x7A, 0x00, 0x00, 0x00}, // G 
  {0x7F, 0x08, 0x08, 0x08, 0x7F, 0x00, 0x00, 0x00}, // H 
  {0x00, 0x41, 0x7F, 0x41, 0x00, 0x00, 0x00, 0x00}, // I 
  {0x20, 0x40, 0x41, 0x3F, 0x01, 0x00, 0x00, 0x00}, // J 
  {0x7F, 0x08, 0x14, 0x22, 0x41, 0x00, 0x00, 0x00}, // K 
  {0x7F, 0x40, 0x40, 0x40, 0x40, 0x00, 0x00, 0x00}, // L 
  {0x7F, 0x02, 0x0C, 0x02, 0x7F, 0x00, 0x00, 0x00}, // M 
  {0x7F, 0x04, 0x08, 0x10, 0x7F, 0x00, 0x00, 0x00}, // N 
  {0x3E, 0x41, 0x41, 0x41, 0x3E, 0x00, 0x00, 0x00}, // O 
  {0x7F, 0x09, 0x09, 0x09, 0x06, 0x00, 0x00, 0x00}, // P 
  {0x3E, 0x41, 0x51, 0x21, 0x5E, 0x00, 0x00, 0x00}, // Q 
  {0x7F, 0x09, 0x19, 0x29, 0x46, 0x00, 0x00, 0x00}, // R 
  {0x46, 0x49, 0x49, 0x49, 0x31, 0x00, 0x00, 0x00}, // S 
  {0x01, 0x01, 0x7F, 0x01, 0x01, 0x00, 0x00, 0x00}, // T 
  {0x3F, 0x40, 0x40, 0x40, 0x3F, 0x00, 0x00, 0x00}, // U 
  {0x1F, 0x20, 0x40, 0x20, 0x1F, 0x00, 0x00, 0x00}, // V 
  {0x3F, 0x40, 0x38, 0x40, 0x3F, 0x00, 0x00, 0x00}, // W 
  {0x63, 0x14, 0x08, 0x14, 0x63, 0x00, 0x00, 0x00}, // X 
  {0x07, 0x08, 0x70, 0x08, 0x07, 0x00, 0x00, 0x00}, // Y 
  {0x61, 0x51, 0x49, 0x45, 0x43, 0x00, 0x00, 0x00}, // Z 
  {0x1E, 0x05, 0x1E, 0xE0, 0x10, 0x60, 0x10, 0xE0}, // [ - AM
  {0x1F, 0x05, 0x07, 0xE0, 0x10, 0x60, 0x10, 0xE0}  // \ - PM
};

//Custom characters are stored in PAM
#define CUSTOM_1 93 //]
#define CUSTOM_2 94 //^
uint8_t customChars[2][8];

#define COLON_3X5 10
#define SLASH_3X5 11
#define A_3X5     12
#define P_3X5     13
#define M_3X5     14
#define SPACE_3X5 15
#define VSHIFT 1    //Pixels up from bottom
const byte font3x5[16][3] PROGMEM = {
  {0x1F,0x11,0x1F}, {0x00,0x1F,0x00}, {0x1D,0x15,0x17}, {0x15,0x15,0x1F}, {0x07,0x04,0x1F},
  {0x17,0x15,0x1D}, {0x1F,0x15,0x1D}, {0x01,0x1D,0x03}, {0x1F,0x15,0x1F}, {0x07,0x05,0x1F},
  {0x00,0x0A,0x00}, {0x18,0x04,0x03}, {0x1E,0x05,0x1E}, {0x1F,0x05,0x07}, {0x1F,0x06,0x1F},
  {0x00,0x00,0x00} 
};

//-----------------------------------------------------------------------------------
// Forward references

void setupDisplay();
void displayDate(DATES f, bool on);
void displayFormat(FORMATS f, bool on);
void displayFont(FONTS f, bool on);
void displayBrightness(uint8_t b, bool on);
void displayNumber(long num, bool leadingZeros, bool flash);
void displayCharacter(char ch);
void createCustomNumber(int custom, long num, bool leadingZeros, bool flash);
void createCustomDigit(int custom, int num, int hshift, int vshift);
void clearCustomCharacter(int custom);
bool isScrollComplete();
void drawString(String s, FONTSIZE f);
void scrollTerminate();
void scrollTextLeft();
void scrollTextUp();
void displaySetPixel(int8_t r, int8_t c, bool on);
bool displayGetPixel(int8_t r, int8_t c);
void displayClear();
void displayIntensity(int brightness);
void displayRefresh();

//-----------------------------------------------------------------------------------
//Initialise matrix and setup timer for matrix scrolling

LedControl lc = LedControl(DIN, CLK, LOAD, 1);

void setupDisplay()
{
  forceHorizontal = false;
  
  lc.shutdown(0, false); //The MAX7219 is in power-saving mode on startup,
  displayIntensity(EepromData.brightness);
  lc.clearDisplay(0);    //Clear the display 

  //Set up display refresh timer
  //CLK_PER = 3.3MHz (303nS)
  TCB1.CCMP = 49152;   //Refresh value for display (67Hz)
  TCB1.INTCTRL = TCB_CAPT_bm;
  TCB1.CTRLA = TCB_ENABLE_bm;
}

//-------------------------------------------------------------------------
//Timer B Interrupt handler interrupt each mS - output segments
ISR(TCB1_INT_vect)
{
  //Handle scrolling of text
  if (scrollDelay != 0)
  {
    scrollDelay--;
    if (scrollDelay == 0)
    {
      scrollDelay = SCROLL_SPEED;
      if (!forceHorizontal && EepromData.clockFont == FONT_VERT)
      {
        scrollTextUp(); 
      }
      else
      {
        scrollTextLeft(); 
      }
    }
  }

  //Clear interrupt flag
  TCB1.INTFLAGS |= TCB_CAPT_bm; //clear the interrupt flag(to reset TCB1.CNT)
}

//-----------------------------------------------------------------------------------
// Display a date layout
//  f - TIME_ONLY or TIME_DMY or TIME_MDY
void displayDate(DATES f, bool on)
{
  if (!on)
  {
    displayCharacter(SPACE_5X7);
  }
  else
  {
    switch(f)
    {
      case TIME_ONLY: displayCharacter(TM_5X7); break; //TM
      case TIME_DMY: displayCharacter(DM_5X7); break; //DM
      case TIME_MDY: displayCharacter(MD_5X7); break; //MD
    }
  }
}

//-----------------------------------------------------------------------------------
// Display a format layout
//  f - HOUR_24 or HOUR_12
void displayFormat(FORMATS f, bool on)
{
  if (!on)
  {
    displayCharacter(SPACE_5X7);
  }
  else
  {
    switch(f)
    {
      case HOUR_24: displayCharacter(T4_5X7); break; //24
      case HOUR_12: displayCharacter(T2_5X7); break; //12
    }
  }
}

//-----------------------------------------------------------------------------------
// Display a font layout
//  f - FONT_HORZ_SMALL or FONT_HORZ_LARGE or FONT_VERT
void displayFont(FONTS f, bool on)
{
  if (!on)
  {
    displayCharacter(SPACE_5X7);
  }
  else
  {
    switch(f)
    {
      case FONT_HORZ_SMALL: displayCharacter(FS_5X7); break; //FS
      case FONT_HORZ_LARGE: displayCharacter(FL_5X7); break; //FL
      case FONT_VERT: displayCharacter(FV_5X7); break; //FV
    }
  }
}

//-----------------------------------------------------------------------------------
// Display the brightness level
//  b - brightness 0 to 7
void displayBrightness(uint8_t b, bool on)
{
  if (!on)
  {
    displayCharacter(SPACE_5X7);
  }
  else
  {
    displayCharacter(BRIGHT_1 + b);
  }
}

//-----------------------------------------------------------------------------------
// Display a number as two 7 segment digits
//   num - 0 to 99
//   leadingZeros - true to have leading zeros
//   flash - true to show digit, false to show blank
void displayNumber(long num, bool leadingZeros, bool flash)
{
  scrollDelay = 0;    //Switch off scrolling
  createCustomNumber(CUSTOM_1, num, leadingZeros, flash);
  displayCharacter(CUSTOM_1);
}

//-----------------------------------------------------------------------------------
// Display a custom character on the screen
void displayCharacter(char ch)
{
  uint8_t fromBits;
  uint8_t fromMask;
  uint8_t toBits;
  uint8_t toMask;
  fromMask = 0x01;
  for(int r = 0; r < 8; r++)
  {
    toBits = 0;
    toMask = 0x80;
    for (int c = 0; c < 8; c++)
    {
      if (ch >= CUSTOM_1)
      {
        fromBits = customChars[ch - CUSTOM_1][c];
      }
      else
      {
        fromBits = pgm_read_byte(&font5x7[ch - ASCII_OFFSET][c]);
      }
      if (fromBits & fromMask)
      {
        toBits = toBits | toMask;
      }
      toMask = toMask >> 1;
    }
    displayBuffer[r] = toBits;
    fromMask = fromMask << 1;
  }
  displayRefresh();
}

//-----------------------------------------------------------------------------------
// Display a number as two 7 segment digits
//   custom - (CUSTOM_1 to CUSTOM_2) One of the custom character slots
//   num - 0 to 99
//   leadingZeros - true to have leading zeros
//   flash - true to show digit, false to show blank
void createCustomNumber(int custom, long num, bool leadingZeros, bool flash)
{
  clearCustomCharacter(custom);
  num = max(min(num, 99), 0);
  for (int i = 0, shift = 4; i < 2; i++, shift-=4)
  {
    if (flash && (num > 0 || i == 0 || leadingZeros))
    {
      createCustomDigit(custom, num % 10, shift, VSHIFT+1);
    }
    else
    {
      createCustomDigit(custom, SPACE_3X5, shift, VSHIFT+1);
    }
    num = num / 10;
  }
}

//-----------------------------------------------------------------------------------
// Display a digit
//   custom - (CUSTOM_1 to CUSTOM_4) One of the custom characrer slots
//   num - 0 to 10
//   hshift - columns to shift left
//   vshift - rows to shift down
void createCustomDigit(int custom, int num, int hshift, int vshift)
{
  uint8_t numBits;
  uint8_t numMask;
  uint8_t cusMask;
  uint8_t cusBits;
  for (int col = 0; col < 3; col++)
  {
    numBits = pgm_read_byte_near(&font3x5[num][col]);

    numMask = 0x01;
    cusMask = 0x01 << vshift;
    cusBits = customChars[custom-CUSTOM_1][col+hshift];  //Read existing column
    for (int row = 0; row < 5; row++)
    {
      if (numBits & numMask)
      {
        cusBits = cusBits | cusMask;
      }
      else
      {
        cusBits = cusBits & ~cusMask;
      }
      numMask = numMask << 1;
      cusMask = cusMask << 1;
    }
    customChars[custom-CUSTOM_1][col+hshift] = cusBits;  //Update column
  }
}

//-----------------------------------------------------------------------------------
// Clear a customer character buffer
//   custom - (CUSTOM_1 to CUSTOM_2) One of the custom characrer slots
void clearCustomCharacter(int custom)
{
  for (int i = 0; i < 8; i++)
  {
    customChars[custom-CUSTOM_1][i] = 0;
  }
}

//---------------------------------------------------------------
// Test if text scroll has completed
bool isScrollComplete()
{
  return (scrollDelay == 0);
}

//---------------------------------------------------------------
//Draw string
// s = String to display
// f = font to use
void drawString(String s, FONTSIZE f)
{
  while (!isScrollComplete())
  {
    //Wait until last lot of text has scrolled off the screen
    updateOrientation();      //Read the mecury switches to get cube orientation
  }
  s.toUpperCase();
  scrollText = s;
  scrollFont = f;
  scrollCharPos = 0;
  scrollCharCol = 0;
  scrollOffScreen = 0;
  scrollDelay = SCROLL_SPEED;     //Starts scrolling
}

//---------------------------------------------------------------
// Terminate aby scrolling
void scrollTerminate()
{
  scrollDelay = 0;
}

//---------------------------------------------------------------
//Scroll text left
void scrollTextLeft()
{
  uint8_t bits;
  uint8_t mask;
  int8_t colMax;
  int8_t rowMax;
  char ch;
  
  //Scroll screen buffer left
  for (int8_t c = 0; c < 8; c++)
  {
    for (int8_t r = 0; r < 8; r++)
    {
      displaySetPixel(r, c, (c != 7 && displayGetPixel(r, c + 1)));
    }
  }

  switch(scrollFont)
  {
    case FONT3X5: 
      colMax = 3;
      rowMax = 5;
      //character column after last column is blank for letter spacing
      if (scrollOffScreen == 0 && scrollCharCol < colMax)
      {
        ch = scrollText[scrollCharPos];
        if (ch >= '0' && ch <= '9' || ch == ':' || ch == ';' || ch == 'A' || ch == 'P' || ch == 'M' || ch == ' ' || ch == '_')
        {
          uint8_t c = ch - 48;
          switch(ch)
          {
            case ' ': c = SPACE_3X5; break;
            case ':': c = COLON_3X5; break;
            case ';': c = SLASH_3X5; break;
            case 'A': c = A_3X5; break;
            case 'P': c = P_3X5; break;
            case 'M': c = M_3X5; break;
            case '_': c = SPACE_3X5; colMax = 0; break;
          }
          bits = pgm_read_byte(&font3x5[c][scrollCharCol]);
          mask = 0x10;
          //Get bits in the next column and output to buffer
          for(int8_t r = 0; r < rowMax; r++)
          {
            if (bits & mask)
            {
              displaySetPixel(7 - r - VSHIFT, 7, true);
            }
            mask = mask >> 1;
          }
        }
      }
      break;

    case FONT5X7:
      ch = scrollText[scrollCharPos];
      colMax = (ch > 90) ? 8 : 5;
      rowMax = (ch > 90) ? 8 : 7;
      mask = (ch > 90) ? 0x80 : 0x40;
      //character column after last column is blank for letter spacing
      if (scrollOffScreen == 0 && scrollCharCol < colMax)
      {
        if (ch >= 40 && ch <= 94)
        {
          if (ch == CUSTOM_1 || ch == CUSTOM_2)
          {
            bits = customChars[ch - CUSTOM_1][scrollCharCol];
          }
          else
          {
            bits = pgm_read_byte(&font5x7[ch - ASCII_OFFSET][scrollCharCol]);
          }
          //Get bits in the next column and output to buffer
          for(int8_t r = 0; r < rowMax; r++)
          {
            if (bits & mask)
            {
              displaySetPixel(7 - r, 7, true);
            }
            mask = mask >> 1;
          }
        }
      }
      break;
  }

  if (scrollOffScreen > 0)
  {
    scrollOffScreen--;
    if (scrollOffScreen == 0)
    {
      //Stop scrolling
      scrollDelay = 0;
    }
  }
  else
  {
    scrollCharCol++;
    if (scrollCharCol == (colMax+1))
    {
      scrollCharCol = 0;
      scrollCharPos++;
      if (scrollCharPos == scrollText.length())
      {
        //All text has been outputted, just wait until it is scrolled of the screen
        scrollOffScreen = 8;
      }
    }
  }
  displayRefresh();
}

//---------------------------------------------------------------
//Scroll text up
void scrollTextUp()
{
  uint8_t bits;
  uint8_t mask;
  int8_t colMax;
  int8_t rowMax;
  char ch;

  //Scroll screen buffer up
  for (int8_t r = 0; r < 8; r++)
  {
    for (int8_t c = 0; c < 8; c++)
    {
      displaySetPixel(r, c, (r != 7 && displayGetPixel(r + 1, c)));
    }
  }

  //Seventh character row is blank for letter spacing
  ch = scrollText[scrollCharPos];
  colMax = (ch > 90) ? 8 : 5;
  rowMax = (ch > 90) ? 8 : 7;

  if (scrollOffScreen == 0 && scrollCharCol < rowMax)
  {
    char ch = scrollText[scrollCharPos];
    if (ch >= 40 && ch <= 94)
    {
      mask = 1 << scrollCharCol;
      //Get bits in the next column and output to buffer
      for(int8_t c = 0; c < colMax; c++)
      {
        if (ch == CUSTOM_1 || ch == CUSTOM_2)
        {
          bits = customChars[ch - CUSTOM_1][c];
        }
        else
        {
          bits = pgm_read_byte(&font5x7[ch - ASCII_OFFSET][c]);
        }
        if (bits & mask)
        {
          displaySetPixel(7, c, true);
        }
      }
    }
  }

  if (scrollOffScreen > 0)
  {
    scrollOffScreen--;
    if (scrollOffScreen == 0)
    {
      //Stop scrolling
      scrollDelay = 0;
    }
  }
  else
  {
    scrollCharCol++;
    if (scrollCharCol == 8)
    {
      scrollCharCol = 0;
      scrollCharPos++;
      if (scrollCharPos == scrollText.length())
      {
        //All text has been outputted, just wait until it is scrolled of the screen
        scrollOffScreen = 8;
      }
    }
  }
  displayRefresh();
}

//---------------------------------------------------------------
//Sets the bit in the 8 byte array that corresponds to the physical column and row
// r = row (0 - top row to 7 - bottom row)
// c = column (0 to 7) (0 is far left)
// on = true to switch bit on, false to switch bit off
void displaySetPixel(int8_t r, int8_t c, bool on)
{
  byte mask = 1 << (7-c);
  if (on)
  {
    displayBuffer[r] = displayBuffer[r] | mask;
  }
  else
  {
    displayBuffer[r] = displayBuffer[r] & ~mask;
  }
}

//---------------------------------------------------------------
//Get the color of the bit that corresponds to the physical column and row
// r = row (0 - top row to 7 - bottom row)
// c = column (0 to 7)
// Returns true if on
bool displayGetPixel(int8_t r, int8_t c)
{
  byte mask = 1 << (7-c);
  return (displayBuffer[r] & mask);
}

//---------------------------------------------------------------
//Clear the display buffer
void displayClear()
{
  memset(&displayBuffer,0,8);  
}

//---------------------------------------------------------------
//Set the screen brightness
// b = 0 to 7
void displayIntensity(int b)
{
  lc.setIntensity(0, b * 2 + 1);
}

//---------------------------------------------------------------
//Transfers the display buffer to the matrix
void displayRefresh()
{
  byte row = 0;
  byte rowMask = 0;
  byte colMask = 0;
  switch (scrollOrient)
  {
    case LEFT_TO_RIGHT:
      for(int i = 0; i < 8; i++)
      {
        lc.setRow(0,i,displayBuffer[i]);
      }
      break;
      
    case TOP_TO_BOTTOM:
      colMask = 0x01;
      for(int c = 0; c < 8; c++)
      {
        row = 0;
        rowMask = 0x80;
        for (int r = 0; r < 8; r++)
        {
          if (displayBuffer[r] & colMask)
          {
            row = row | rowMask;
          }
          rowMask = rowMask >> 1;
        }
        lc.setRow(0,c,row);
        colMask = colMask << 1;
      }
      break;
    
      case RIGHT_TO_LEFT:
        for(int r = 0; r < 8; r++)
        {
          row = 0;
          rowMask = 0x01;
          colMask = 0x80;
          for(int c = 0; c < 8; c++)
          {
            if (displayBuffer[7-r] & colMask)
            {
              row = row | rowMask;
            }
            rowMask = rowMask << 1;
            colMask = colMask >> 1;
            lc.setRow(0,r,row);
          }
        }
        break;
        
      case BOTTOM_TO_TOP:
        colMask = 0x80;
        for(int c = 0; c < 8; c++)
        {
          row = 0;
          rowMask = 0x80;
          for (int r = 0; r < 8; r++)
          {
            if (displayBuffer[7-r] & colMask)
            {
              row = row | rowMask;
            }
            rowMask = rowMask >> 1;
          }
          lc.setRow(0,c,row);
          colMask = colMask >> 1;
        }
        break;
  }
}

Memory.h

C/C++
/**
 * ATtiny1614 Cube Clock
 * John Bradnam (jbrad2089@gmail.com)
 * 
 * 2021-09-06 - Create EEPROM functions for clock
 *
*/

#pragma once

//Uncomment to reset EEPROM data
//#define RESET_EEPROM

#ifdef __AVR_ATtiny1614__
#include <avr/eeprom.h>
#else
#include <EEPROM.h>
#endif

enum DATES { TIME_ONLY, TIME_DMY, TIME_MDY };
enum FORMATS { HOUR_24, HOUR_12 };
enum FONTS { FONT_HORZ_SMALL, FONT_HORZ_LARGE, FONT_VERT };

//EEPROM handling
#define EEPROM_ADDRESS 0
#define EEPROM_MAGIC 0x0DAD0BAD
typedef struct {
  uint32_t magic;
  DATES clockDates;
  FORMATS clockFormat;
  FONTS clockFont;
  uint8_t brightness; //Current brightness level
} EEPROM_DATA;

volatile EEPROM_DATA EepromData;       //Current EEPROM settings

//-----------------------------------------------------------------------------------
// Forward references

void writeEepromData();
void readEepromData();

//---------------------------------------------------------------
//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.
  #ifdef __AVR_ATtiny1614__
    eeprom_update_block (( void *) &EepromData , ( const void *) EEPROM_ADDRESS, sizeof(EepromData));  
  #else
    EEPROM.put(EEPROM_ADDRESS,EepromData);
  #endif
}

//---------------------------------------------------------------
//Read the EepromData structure from EEPROM, initialise if necessary
void readEepromData()
{
  //Eprom
  #ifdef __AVR_ATtiny1614__
    eeprom_read_block (( void *) &EepromData , ( const void *) EEPROM_ADDRESS, sizeof(EepromData));  
  #else
    EEPROM.get(EEPROM_ADDRESS,EepromData);
  #endif
  #ifndef RESET_EEPROM
  if (EepromData.magic != EEPROM_MAGIC)
  #endif
  {
    EepromData.magic = EEPROM_MAGIC;
    EepromData.clockDates = TIME_ONLY;
    EepromData.clockFormat = HOUR_24;
    EepromData.clockFont = FONT_HORZ_SMALL;
    EepromData.brightness = 0;
    writeEepromData();
  }
}

Switches.h

C/C++
/**
 * ATtiny1614 Cube Clock
 * John Bradnam (jbrad2089@gmail.com)
 * 
 * 2021-09-06 - Create Switch functions for clock
 *
*/

#pragma once

//Switches
#define SWITCHES 0     //PA4
#define NW 4           //PB3
#define NE 5           //PB2
#define SE 7           //PB0
#define SW 6           //PB1

enum BUTTONS { NONE, SET, UP, DOWN };
enum ORIENTATION {UNKNOWN,LEFT_TO_RIGHT,TOP_TO_BOTTOM,RIGHT_TO_LEFT,BOTTOM_TO_TOP};

volatile ORIENTATION scrollOrient = UNKNOWN; //Current cube orientation
uint8_t lastOrientation = 0;           //Used to detect change in cube orientation

//-----------------------------------------------------------------------------------
// Forward references

void setupSwitches();
void updateOrientation();
BUTTONS getButtonState();

//---------------------------------------------------------------
// Setup pins connected to switches
void setupSwitches()
{
  pinMode(SWITCHES,INPUT);
  pinMode(NW,INPUT_PULLUP);
  pinMode(NE,INPUT_PULLUP);
  pinMode(SE,INPUT_PULLUP);
  pinMode(SW,INPUT_PULLUP);
}

//---------------------------------------------------------------
// Read the mercury switches and update for changes
void updateOrientation()
{
  byte dir = B00000000;
  if (digitalRead(NW) == LOW)
  {
    dir = dir | B00000001;
  }
  if (digitalRead(NE) == LOW)
  {
    dir = dir | B00000010;
  }
  if (digitalRead(SW) == LOW)
  {
    dir = dir | B00000100;
  }
  if (digitalRead(SE) == LOW)
  {
    dir = dir | B00001000;
  }

  if (lastOrientation != dir || scrollOrient == UNKNOWN)
  {
    lastOrientation = dir;
    switch (dir)
    {
      case B00000000: scrollOrient = LEFT_TO_RIGHT; break;
      case B00000010: scrollOrient = TOP_TO_BOTTOM; break;
      case B00001011: scrollOrient = RIGHT_TO_LEFT; break;
      case B00001001: scrollOrient = BOTTOM_TO_TOP; break;
    }
  }
}

//-----------------------------------------------------------------------------------
// Return current button state
BUTTONS getButtonState()
{
  //S4 (SET) - 0V 0
  //S3 (UP) - 2.5V 512
  //S3 (DOWN) - 3V 614
  BUTTONS result = NONE;
  int value = analogRead(SWITCHES);
  if (value < 450)
  {
    result = SET;
  }
  else if (value < 550)
  {
    result = UP;
  }
  else if (value < 650)
  {
    result = DOWN;
  }
  return result;
}

Credits

John Bradnam

John Bradnam

147 projects • 181 followers

Comments