/**
* ATtiny1614 Micro 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 <LedControl.h>
#include <Wire.h>
#include <RTClib.h>
//MAX7219
#define CLK 1 //PA5
#define LOAD 2 //PA6
#define DIN 3 //PA7
//DS1307
#define _SCL 9 //PA2
#define _SDA 8 //PA1
//Switches
#define SWITCHES 0 //PA4
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
#define R 10
#define W 2
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}
};
//Scrolling text variables
#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 bool scrollFinished; //Set when scrolling is complete
byte displayBuffer[8]; //Double buffer for fast updates
char timeBuffer[16]; //Used to create time string
//Small font
#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}
};
// 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] = {0,1,2,3,4,5,6,7};
uint8_t colMap[8] = {7,6,5,4,3,2,1,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 = -1; //Used to store current hour displayed
int lastMinute = -1; //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);
RTC_DS1307 rtc;
//----------------------------------------------------------------------
// Hardware Setup
void setup()
{
//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
pinMode(SWITCHES,INPUT);
lc.shutdown(0, false); //The MAX7219 is in power-saving mode on startup,
lc.setIntensity(0, 4); //Set the brightness to a medium values
lc.clearDisplay(0); //Clear the display
if (!rtc.begin())
{
showLetter(R, true); //Cannot find RTC
delay(2000);
}
else if (!rtc.isrunning())
{
showLetter(W, true); //RTC lost power
rtc.adjust(DateTime(2022, 1, 4, 6, 30, 0));
delay(2000);
}
//Show current time
DateTime newTime = rtc.now();
showTimeInWords(newTime.hour(), newTime.minute(), newTime.second(), true);
//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;
scrollFinished = false;
scrollDelay = 0;
}
//-------------------------------------------------------------------------
//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;
scrollTextLeft();
}
}
//Clear interrupt flag
TCB1.INTFLAGS |= TCB_CAPT_bm; //clear the interrupt flag(to reset TCB1.CNT)
}
//----------------------------------------------------------------------
// 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 = scrollFinished;
scrollFinished = false;
if (isScrollComplete())
{
SwitchEnum s = getButtonState();
if (s != NONE)
{
delay(10);
if (getButtonState() == s)
{
if (s == SET)
{
DateTime currentTime = rtc.now();
clockMode = (clockMode == SET_MINUTE) ? WORD_TIME : (ClockEnum)((int)clockMode + 1);
switch (clockMode)
{
case SET_HOUR:
setH = currentTime.hour();
setM = currentTime.minute();
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
rtc.adjust(DateTime(currentTime.year(), currentTime.month(), currentTime.day(), setH, setM, 0));
//force update
updateTime = true;
break;
}
//Wait until button is released
while (getButtonState())
{
delay(10);
}
}
else if (s == DOWN && clockMode == WORD_TIME)
{
//Scroll current time
DateTime currentTime = rtc.now();
setH = currentTime.hour();
int hours = currentTime.hour();
int h = (setH == 0) ? 12 : (setH > 12) ? setH - 12 : setH;
char c = (setH < 12) ? 'A' : 'P';
sprintf(timeBuffer,"%d:%02d_%cM",h,currentTime.minute(),c);
drawString(String(timeBuffer));
}
}
}
if (clockMode == WORD_TIME && isScrollComplete());
{
DateTime newTime = rtc.now();
showTimeInWords(newTime.hour(), newTime.minute(), newTime.second(), 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++;
}
if (forceShow || hours != lastHour || minutes != lastMinute)
{
int hourWord;
switch (hours % 12)
{
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
int five = minutes / 5;
switch (five)
{
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 = DOWN;
}
else if (value < 550)
{
result = UP;
}
else if (value < 650)
{
result = SET;
}
return result;
}
//---------------------------------------------------------------
// 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)
{
while (!isScrollComplete())
{
}
s.toUpperCase();
scrollText = s;
scrollCharPos = 0;
scrollCharCol = 0;
scrollOffScreen = 0;
scrollDelay = SCROLL_SPEED; //Starts scrolling
scrollFinished = false;
}
//---------------------------------------------------------------
//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)));
}
}
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;
}
}
}
if (scrollOffScreen > 0)
{
scrollOffScreen--;
if (scrollOffScreen == 0)
{
//Stop scrolling
scrollDelay = 0;
scrollFinished = true;
}
}
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();
}
//---------------------------------------------------------------
//Transfers the display buffer to the matrix
void displayRefresh()
{
byte row = 0;
byte rowMask = 0;
byte colMask = 0;
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;
}
}
//---------------------------------------------------------------
//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);
}
Comments