John Bradnam
Published © GPL3+

Folding Clock

An interesting 3D printed clock that folds out flat or back on top of itself.

AdvancedFull instructions provided16 hours585
Folding Clock

Things used in this project

Hardware components

Microchip ATtiny1614 Microprocessor
×1
Real Time Clock (RTC)
Real Time Clock (RTC)
SOIC package
×1
TM1650 4-Digit Display Drivers
SOIC 16 package
×2
4-Digit 7-Segment CC 0.8in display
×2
LM1117-5
SOT-223 5V regulator
×1
Passive Components
Resistors: 3 x 10K 0805, Capacitors: 2 x 0.1uF 0805, 1 x 47uF/16V 3528 Tantalum
×1
Tactile Switch, Top Actuated
Tactile Switch, Top Actuated
17mm shaft with button tops
×3

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

Files for your slicer software

Schematics

Schematic (bottom)

PCB (bottom)

Schematic (top)

PCB (top)

Eagle Files

Schematics and PCBs in Eagle format

Code

FoldingClockV1.ino

C/C++
/**
 * ATtiny1614 Folding Clock
 * John Bradnam (jbrad2089@gmail.com)
 * 
 * 2021-10-08 - 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(): "Enabled (default timer)"
 * Programmer: jtag2updi (megaTinyCore)
 * ----------------------------------------
 */

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

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

#define DATA      0   //PA4
#define CLK1      1   //PA5
#define CLK2      2   //PA6
#define MERCURY   3   //PA7
#define SWITCHES  8   //PA1

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

enum STATES { CLOCK_TIME, SET_HOUR, SET_MINUTE, SET_SECOND };
STATES clockState = CLOCK_TIME;

enum FORMATS { HOUR_24, HOUR_12 };
FORMATS clockFormat = HOUR_12;

#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
int setS = 0;                     //Second being set
bool updateTime = true;           //Force display of time

RTC_DS1307 rtc;

void setup() 
{

  setupDisplay();
  setupSwitches();
  
  Wire.usePullups();                // Use pullup resistors on SDA and SCL pins
  if (!rtc.begin())
  {
    //Cannot find RTC
    displayChar(0,CHAR_R);
    displayChar(1,CHAR_T);
    displayChar(2,CHAR_C);
    displayChar(3,SPACE);
    delay(1000);
  }
  else if (!rtc.isrunning()) 
  {
    //RTC lost power - RTC reset;
    rtc.adjust(DateTime(2021, 10, 1, 13, 45, 0));
    displayChar(0,CHAR_L);
    displayChar(1,CHAR_O);
    displayChar(2,5);
    displayChar(3,CHAR_T);
    delay(1000);
  }
}

//----------------------------------------------------------------------
// Main program loop
void loop() 
{
  updateTime |= testSetupButton();
  switch (clockState)
  {
    case CLOCK_TIME: timeMode(); break;
    case SET_HOUR: hourMode(); break;
    case SET_MINUTE: minuteMode(); break;
    case SET_SECOND: secondMode(); break;
  }
}
  
//----------------------------------------------------------------------
// In time display mode
void timeMode()
{
  
  if (clockState == CLOCK_TIME)
  {
    DateTime newTime = rtc.now();
    int hours = newTime.hour();
    int minutes = newTime.minute();
    int secs = newTime.second();
  
    if (updateTime || hours != lastHours || minutes != lastMinutes || secs != lastSecs)
    {
      updateTime = false;
      lastHours = hours;
      lastMinutes = minutes;
      lastSecs = secs;

      bool colon = secs & 0x01;

      switch(clockFormat)
      {
        case HOUR_24: 
          displayTime(HOURS,hours,false,true,colon);
          break;
        
        case HOUR_12: 
          int h = (hours == 0) ? 12 : (hours > 12) ? hours - 12 : hours;
          displayTime(HOURS,h,false,true,colon);
          displayChar(0,(hours < 12) ? CHAR_A : CHAR_P);
          displayChar(1,SPACE);
          break;
      }
      displayTime(MINUTES,minutes,true,true,colon);
      displayTime(SECONDS,secs,true,true,colon);
    }
  }
}

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

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

//----------------------------------------------------------------------
// Set second  mode
void secondMode()
{
  if (millis() > flashTimeout)
  {
    flashTimeout = millis() + FLASH_TIME;
    flashOn = !flashOn;
    displayTime(SECONDS, setS, true, flashOn, true);
  }
  if (millis() > stepTimeout)
  {
    if (getButtonState() == UP)
    {
      setS = (setS + 1) % 60;
      displayTime(SECONDS, setS, true, flashOn, true);
      stepTimeout = millis() + STEP_TIME;
    }
    else if (getButtonState() == DOWN)
    {
      setS = (setS + 59) % 60;
      displayTime(SECONDS, setS, true, flashOn, true);
      stepTimeout = millis() + STEP_TIME;
    }
  }
}

//----------------------------------------------------------------------
// Test for setup button and handle the initial setup
bool testSetupButton()
{ 
  bool updateTime = false; 
  if (getButtonState() == SET)
  {
    delay(10);
    if (getButtonState() == SET)
    {
      DateTime currentTime = rtc.now();
      clockState = (clockState == SET_SECOND) ? CLOCK_TIME : (STATES)((int)clockState + 1);
      switch (clockState)
      {
        case SET_HOUR:
          setH = currentTime.hour();
          setM = currentTime.minute();
          setS = currentTime.second();
          flashTimeout = millis() + FLASH_TIME;
          flashOn = false;
          displayTime(HOURS, setH, true, flashOn, true);
          break;
        
        case SET_MINUTE:
          displayTime(HOURS, setH, true, true, true);
          flashTimeout = millis() + FLASH_TIME;
          flashOn = false;
          displayTime(MINUTES, setM, true, flashOn, true);
          break;
        
        case SET_SECOND:
          displayTime(MINUTES, setM, true, true, true);
          flashTimeout = millis() + FLASH_TIME;
          flashOn = false;
          displayTime(SECONDS, setS, true, flashOn, true);
          break;
        
        case CLOCK_TIME:
          displayTime(SECONDS, setS, true, true, true);
          //Set RTC
          rtc.adjust(DateTime(currentTime.year(), currentTime.month(), currentTime.day(), setH, setM, setS));
          //force update
          updateTime = true;
          break;
      }
      //Wait until button is released
      while (getButtonState())
      {
        delay(10);
      }
    }
  }
  return updateTime;
}

Display.h

C/C++
/**
 * ATtiny1614 Folding Clock
 * John Bradnam (jbrad2089@gmail.com)
 * 
 * 2021-10-08 - Create display functions for clock
 *
*/

#pragma once

#include <TM1650.h>

#define DATA      0   //PA4
#define CLK1      1   //PA5
#define CLK2      2   //PA6
#define MERCURY   3   //PA7

#define BRIGHTNESS 2
enum SHOW {HOURS,MINUTES,SECONDS};

#define SPACE 10
#define CHAR_R 11
#define CHAR_T 12
#define CHAR_C 13
#define CHAR_L 14
#define CHAR_O 15
#define CHAR_A 16
#define CHAR_P 17
#define DP_SEGMENT 0b00000010
const PROGMEM byte NUMBER_FONT[] = {
//  edcgfbpa
  0b11101101, // 0
  0b00100100, // 1
  0b11010101, // 2
  0b01110101, // 3
  0b00111100, // 4
  0b01111001, // 5
  0b11111001, // 6
  0b00100101, // 7
  0b11111101, // 8
  0b01111101, // 9
  0b00000000,  // SPACE
  0b10010000, // r
  0b11011000, // t
  0b11010000, // c
  0b11001000, // L
  0b11110000, // o
  0b10111101, // A
  0b10011101  // P
};

//a<->d, f<->c, b<->e
const PROGMEM byte REVERSE_FONT[] = {
//  edcgfbpa
  0b11101101, // 0
  0b10001000, // 1
  0b11010101, // 2
  0b11011001, // 3
  0b10111000, // 4
  0b01111001, // 5
  0b01111101, // 6
  0b11001000, // 7
  0b11111101, // 8
  0b11111001, // 9
  0b00000000,  // SPACE
  0b00010100, // r
  0b00110101, // t
  0b00010101, // c
  0b00100101, // L
  0b00011101, // o
  0b11111100, // A
  0b11110100  // P
};

uint8_t digitsOpen[] = {7,6,5,4,3,2,1,0};
uint8_t digitsClosed[] = {3,2,1,0,4,5,6,7};

//----------------------------------------------------------------------
// TM1650 definitions

TM1650 display1(DATA,   //byte dataPin
                CLK1,   //byte clockPin
                4,      //byte number of digits
                true,   //boolean activeDisplay = true
                BRIGHTNESS       //byte intensity
);

TM1650 display2(DATA,   //byte dataPin
                CLK2,   //byte clockPin
                4,      //byte number of digits
                true,   //boolean activeDisplay = true
                BRIGHTNESS       //byte intensity
);

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

void setupDisplay();
void displayNumber(long num, bool leadingZeros, bool on);
void displayTime(SHOW s, int num, bool leadingZeros, bool on, bool c);
void displayChar(int digit, int chr);
void displayCharWithColon(int digit, int chr, bool c);

//---------------------------------------------------------------
// Setup pin connected to mercury switch
void setupDisplay()
{
  pinMode(MERCURY,INPUT_PULLUP);
}

//---------------------------------------------------------------------
//Write number to display
//  num - (0 to 9999) 
//  leadingZeros - true to have leading zeros
//  on - true to show digit, false to show blank
void displayNumber(long num, bool leadingZeros, bool on)
{
  num = max(min(num, 99999999), 0);
  for (int i = 0; i < 8; i++)
  {
    if (on && (num > 0 || i == 0 || leadingZeros))
    {
      displayChar(i, num % 10);
    }
    else
    {
      displayChar(i, SPACE);
    }
    num = num / 10;
  }
}

//---------------------------------------------------------------------
//Write time to display
//  s - SHOW constant
//  num - (0 to 99) 
//  leadingZeros - true to have leading zeros
//  on - true to show digit, false to show blank
//  c - true to show colon
void displayTime(SHOW s, int num, bool leadingZeros, bool on, bool c)
{
  num = max(min(num, 99), 0);
  int b = (s == SECONDS) ? 2 : (s == HOURS) ? 6 : 4;
  for (int i = 0; i < 2; i++)
  {
    if (on && (num > 0 || i == 0 || leadingZeros))
    {
      displayCharWithColon(b + i, num % 10, c);
    }
    else
    {
      displayChar(b + i, SPACE);
    }
    num = num / 10;
  }
}

//---------------------------------------------------------------------
//Write digit to display
//  digit - digit to write to (0 - left most to 7 - right most)
//  char - character to display
void displayChar(int digit, int chr)
{
  displayCharWithColon(digit, chr, false);
}

//---------------------------------------------------------------------
//Write digit to display
//  digit - digit to write to (0 - left most to 7 - right most)
//  char - character to display
//  c - true to show colon
void displayCharWithColon(int digit, int chr, bool c)
{
  bool open = (digitalRead(MERCURY) == LOW);

  int8_t physical = (open) ? digitsOpen[digit] : digitsClosed[digit];  
  int8_t cDigit = (open) ? 4 : 7;  
  byte segments = (open | (digit < 4)) ? pgm_read_byte(&NUMBER_FONT[chr]) : pgm_read_byte(&REVERSE_FONT[chr]);
  if (c && digit == cDigit)
  {
    segments = segments | DP_SEGMENT;
  }
  if (physical < 4)
  {
    display1.setSegments(segments, physical);
  }
  else
  {
    display2.setSegments(segments, physical - 4);
  }
}

Switches.h

C/C++
/**
 * ATtiny1614 Folding Clock
 * John Bradnam (jbrad2089@gmail.com)
 * 
 * 2021-10-08 - Create Switch functions for clock
 *
*/

#pragma once

//Switches
#define SWITCHES  8   //PA1

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

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

void setupSwitches();
BUTTONS getButtonState();

//---------------------------------------------------------------
// Setup pins connected to switches
void setupSwitches()
{
  pinMode(SWITCHES,INPUT);
}

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

Credits

John Bradnam

John Bradnam

145 projects • 177 followers

Comments