John Bradnam
Published © GPL3+

Tiny Word Clock

A 3D printed tiny word clock build using a 8x8 LED matrix and ATtiny1614 microprocessor.

IntermediateFull instructions provided12 hours635
Tiny Word Clock

Things used in this project

Hardware components

Microchip ATtiny1614 microprocessor
×1
MAX7219/MAX7221 LED Display Drivers
Maxim Integrated MAX7219/MAX7221 LED Display Drivers
SOIC package
×1
DS1302 Real Time Clock
SOIC package
×1
32.768 kHz Crystal
32.768 kHz Crystal
×1
PTS 645 Series Switch
C&K Switches PTS 645 Series Switch
17mm shaft with button caps
×3
CR1220 Battery Holder
SMD variant
×1
LM1117-5 5V Regulator
SOT223 package
×1
DC power connector socket
Search for "DC Power Supply Jack Socket Female Panel Mount Connector 5.5 x 2.1mm"
×1
Passive components
3x 0.1uF capacitor 0805, 1 x 47uF 16V tantalum capacitor 3528, 2 x 39K 0805, 2x 22K 0805
×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 files for 3D printing

Schematics

Schematic

PCB

Eagle Files

Schematic and PCB in Eagle format

Code

MatrixWordClock.ino

C/C++
/**
 * ATtiny1614 Matrix Word Clock
 * John Bradnam (jbrad2089@gmail.com)
 * 
 * Based on "Tiny Word Clock" by gfwilliams
 * https://www.instructables.com/Tiny-Word-Clock/
 * 
 * 2021-05-09 - 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 <avr/sleep.h>
#include <LedControl.h>
#include <TimeLib.h>
#include <DS1302RTC.h>

//MAX7219
#define CLK 5          //PB2
#define LOAD 6         //PB1
#define DIN 7          //PB0

//DS1302
#define SCLK 2         //PA6
#define IO 3           //PA7
#define CE 4           //PB3

//Switches
#define SWITCHES 8     //PA1

enum SwitchEnum { NONE, SET, UP, DOWN };

enum WordEnum {
  TEN_M, HALF, QUARTER, TWENTY, 
  FIVE_M, TO, PAST, ONE, 
  TWO, THREE, FOUR, FIVE, 
  SIX, SEVEN, EIGHT, NINE, 
  TEN, ELEVEN, TWELVE
};
#define H 0
#define M 7
#define S 15
const byte letters[19][7] PROGMEM = { 
  { 1, 3, 4, 0, 0, 0, 0}, {20,21,22,23, 0, 0, 0}, { 8, 9,10,11,12,13,14}, { 1, 2, 3, 4, 5, 6, 0},
  {16,17,18,19, 0, 0, 0}, {28,29, 0, 0, 0, 0, 0}, {25,26,27,28, 0, 0, 0}, {57,62,63, 0, 0, 0, 0},
  {48,49,57, 0, 0, 0, 0}, {43,44,45,46,47, 0, 0}, {56,57,58,59, 0, 0, 0}, {32,33,34,35, 0, 0, 0},
  {40,41,42, 0, 0, 0, 0}, {40,52,53,54,55, 0, 0}, {35,36,37,38,39, 0, 0}, {60,61,62,63, 0, 0, 0},
  {39,47,55, 0, 0, 0, 0}, {50,51,52,53,54,55, 0}, {48,49,50,51,53,54, 0}
};

#define VSHIFT 2
#define SPACE 10
const byte numbers[11][5] PROGMEM = {
  {7,5,5,5,7}, {2,2,2,2,2}, {7,1,7,4,7}, {7,1,7,1,7}, {5,5,7,1,1},
  {7,4,7,1,7}, {7,4,7,5,7}, {7,1,2,2,2}, {7,5,7,5,7}, {7,5,7,1,1},
  {0,0,0,0,0}
};

// Because the MAX7219 digit and segment pins are not connected to the
// correct row/column pins on the display to simplify PCB routing, these
// tables map the logical row/columns to their physical locations.
uint8_t rowMap[8] = {5,4,1,0,3,6,7,2};
uint8_t colMap[8] = {6,7,5,3,1,2,4,0};

//Secondary menus
enum ClockEnum { WORD_TIME, SET_HOUR, SET_MINUTE };
ClockEnum clockMode = WORD_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

int lastHour;                   //Used to store current hour displayed
int lastMinute;                 //Used to store current minute displayed
int setH = 0;                   //Hour being set
int setM = 0;                   //Minute being set

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

LedControl lc = LedControl(DIN, CLK, LOAD, 1);
DS1302RTC rtc(CE, IO, CLK);

//----------------------------------------------------------------------
// Hardware Setup
void setup() 
{
  pinMode(SWITCHES,INPUT);
  
  lc.shutdown(0, false); //The MAX7219 is in power-saving mode on startup,
  lc.setIntensity(0, 15); //Set the brightness to a medium values 
  lc.clearDisplay(0);    //Clear the display 

  //Setup RTC pins
  //Check if RTC has a valid time/date, if not set it to 00:00:00 01/01/2018.
  //This will run only at first time or if the coin battery is low.
  //setSyncProvider() causes the Time library to synchronize with the
  //external RTC by calling RTC.get() every five minutes by default.
  setSyncProvider(rtc.get);
  if (timeStatus() != timeSet)
  {
    //Set RTC
    tmElements_t tm;
    tm.Year = CalendarYrToTm(2021);
    tm.Month = 05;
    tm.Day = 11;
    tm.Hour = 11;
    tm.Minute = 33;
    tm.Second = 0;
    time_t t = makeTime(tm);
    rtc.set(t); //use the time_t value to ensure correct weekday is set
    setTime(t);
  }

  //Show current time
  time_t t = now();
  showTimeInWords(hour(t), minute(t), second(t), true);
}

//----------------------------------------------------------------------
// Main program loop
void loop() 
{
  wordTimeMode();
  switch (clockMode)
  {
    case SET_HOUR: hourMode(); break;
    case SET_MINUTE: minuteMode(); break;
  }
}

//----------------------------------------------------------------------
// In word clock display mode
void wordTimeMode()
{
  bool updateTime = false;
  if (getButtonState() == SET)
  {
    delay(10);
    if (getButtonState() == SET)
    {
      time_t t = now();
      clockMode = (clockMode == SET_MINUTE) ? WORD_TIME : (ClockEnum)((int)clockMode + 1);
      switch (clockMode)
      {
        case SET_HOUR: 
          setH = hour(t);
          setM = minute(t); 
          flashTimeout = millis() + FLASH_TIME;
          flashOn = false;
          lc.clearDisplay(0);    //Clear the display 
          displayNumber(setH, true, flashOn);
          showLetter(H, flashOn);
          break;

        case SET_MINUTE:
          flashTimeout = millis() + FLASH_TIME;
          flashOn = false;
          lc.clearDisplay(0);    //Clear the display 
          displayNumber(setM, true, flashOn);
          showLetter(M, flashOn);
          break;

        case WORD_TIME:
          //Set RTC
          tmElements_t tm;
          tm.Year = CalendarYrToTm(2020);
          tm.Month = 1;
          tm.Day = 1;
          tm.Hour = setH;
          tm.Minute = setM;
          tm.Second = 0;
          time_t t = makeTime(tm); //use the time_t value to ensure correct weekday is set
          rtc.set(t);
          setTime(t);
          //force update
          updateTime = true;
          break;
      }
      //Wait until button is released
      while (getButtonState())
      {
        delay(10);
      }
    }
  }
  if (clockMode == WORD_TIME)
  {
    time_t t = now();
    showTimeInWords(hour(t), minute(t), second(t), updateTime);
  }
}

//----------------------------------------------------------------------
// In word clock 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 word clock 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;
    }
  }
}

//----------------------------------------------------------------------
// Displays the current time in words
//  hours = current hour (0 to 23) 
//  minutes = current minute (0 to 59)
//  second = current second (0 to 59)
void showTimeInWords(int hours, int minutes, int secs, bool forceShow)
{
  //Convert to all seconds
  unsigned int h = (hours % 12) * 3600;
  unsigned int hm = h + (minutes / 5) * 300;
  unsigned int hms = h + minutes * 60 + secs;
  
  //Since only 5 minute intervals are displayed, each position is +- 150 seconds (2.5min)
  if ((hms - hm) >= 150)
  {
    hm = hm + 300;    //Add 5 min to take it to the closest word
  }
  hours = hm / 3600;
  minutes = (hm / 60) % 60;
  
  //After half past the hour, other prefixes are TO the next hour
  if (minutes > 30)
  {
    hours++;
  }

  hours = hours % 12;
  minutes = minutes / 5;
  if (forceShow || hours != lastHour || minutes != lastMinute)
  {
    //Get the hour word index
    int hourWord;
    switch (hours)
    {
      case 0: hourWord = TWELVE; break;
      case 1: hourWord = ONE; break;
      case 2: hourWord = TWO; break;
      case 3: hourWord = THREE; break;
      case 4: hourWord = FOUR; break;
      case 5: hourWord = FIVE; break;
      case 6: hourWord = SIX; break;
      case 7: hourWord = SEVEN; break;
      case 8: hourWord = EIGHT; break;
      case 9: hourWord = NINE; break;
      case 10: hourWord = TEN; break;
      case 11: hourWord = ELEVEN; break;
    }
  
    //Show the words that make up the time
    switch (minutes)
    {
      case 0: showWords(1, hourWord); break;
      case 1: showWords(3, FIVE_M, PAST, hourWord); break;
      case 2: showWords(3, TEN_M, PAST, hourWord); break;
      case 3: showWords(3, QUARTER, PAST, hourWord); break;
      case 4: showWords(3, TWENTY, PAST, hourWord); break;
      case 5: showWords(4, TWENTY, FIVE_M, PAST, hourWord); break;
      case 6: showWords(3, HALF, PAST, hourWord); break;
      case 7: showWords(4, TWENTY, FIVE_M, TO, hourWord); break;
      case 8: showWords(3, TWENTY, TO, hourWord); break;
      case 9: showWords(3, QUARTER, TO, hourWord); break;
      case 10: showWords(3, TEN_M, TO, hourWord); break;
      case 11: showWords(3, FIVE_M, TO, hourWord); break;
    }

    lastHour = hours;
    lastMinute = minutes;
  }
}

//----------------------------------------------------------------------
// Show a variable number of words
//   words - the number of word indexes that follow in the parameter list
void showWords(int words, ...)
{
  int wordIndex;
  byte pixel;
  
  lc.clearDisplay(0);    //Clear the display 
  
  va_list valist;
  va_start(valist, words);
  for (int i = 0; i < words; i++) 
  {
    wordIndex = va_arg(valist, int);
    for (int letter = 0; letter < 7; letter++)
    {
      pixel = pgm_read_byte_near(&letters[wordIndex][letter]);
      if (pixel == 0)
      {
        break;
      }
      else
      {
        showLetter(pixel, true);
      }
    }
  }
  va_end(valist);
}

//-----------------------------------------------------------------------------------
// Display a character
//   letter - character index to show
//   flash - true to show digit, false to show blank
void showLetter(int letter, bool flash)
{
  lc.setLed(0, colMap[letter & 0x07], rowMap[letter >> 3], flash);
}

//-----------------------------------------------------------------------------------
// 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)
{
  num = max(min(num, 99), 0);
  for (int i = 0, shift = 0; i < 2; i++, shift+=4)
  {
    if (flash && (num > 0 || i == 0 || leadingZeros))
    {
      displayDigit(num % 10, shift, VSHIFT);
    }
    else
    {
      displayDigit(SPACE, shift, VSHIFT);
    }
    num = num / 10;
  }
}

//-----------------------------------------------------------------------------------
// Display a digit
//   num - 0 to 10
//   hshift - columns to shift left
//   vshift - rows to shift down
void displayDigit(int num, int hshift, int vshift)
{
  byte pixel;
  for (int row = 0; row < 5; row++)
  {
    pixel = pgm_read_byte_near(&numbers[num][row]);

    uint8_t mask = 0x01;
    for (int col = 0; col < 3; col++)
    {
      lc.setLed(0, colMap[7-(col + hshift)], rowMap[row + vshift], (pixel & mask));
      mask = mask << 1;
    }
  }
}

//-----------------------------------------------------------------------------------
// Return current button state
SwitchEnum getButtonState()
{
  //S4 (SET) - 0V 0
  //S3 (UP) - 2.5V 512
  //S3 (DOWN) - 3V 614
  SwitchEnum 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

141 projects • 165 followers
Thanks to gfwilliams.

Comments