/**
* 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;
}
Comments