spudnut1
Created June 14, 2022

TFT-Based "Analog" and Digital Clock

Taking advantage of the newer 3.5" displays, this clock displays an analog clock with h, m & s hand, plus time, date and day of week.

TFT-Based "Analog" and Digital Clock

Things used in this project

Hardware components

Arduino Nano R3
Arduino Nano R3
×1
Adafruit 3.5" TFT Display 480x320
×1
Adafruit Rotary Encoder with I2c chip ("extras") PIDs 377 and 4991
×1
5 volt adapter
×1
Adafruit DS3231 Real Time Clock (RTC)
×1
Battery for DS3231
×1
Shadowbox 5x5 inch
×1
5 x 5 1/8 inch plexiglass (To replace glass on front of shadowbox)
×1

Story

Read more

Custom parts and enclosures

Picture of completed clock

Shows front of clock with TFT attached to plexiglas

Schematics

Connections for Analog Clock

Simple list for clock connections

Code

Arduino IDE Code for Analog Clock

C/C++
Code for clock
/***************************************************
  Analog and Digital Clock displaying time, date, dow  480 X 320
  Uses Adafruit RTC 3231 real time clock, Adafruit 3.5" 320 x 480 TFT display in SPI mode HXD8357D PID: 2050 and 
  Rotary Encoder + Extras PID: 377 and Adafruit I2C QT Rotary Encoder with NeoPixel PID: 4991
  Processor is Arduino Nano 

  June 3 2022 - Add non-colliding second hand 
  June 6 2022 - Lengthen 'tick' marks
 ****************************************************/

#include "Adafruit_GFX.h"                                           // graphics libary to fonts, letters, shapes and lines
#include "Adafruit_HX8357.h"                                        // hardware specific TFT library
#include <TimeLib.h>                                                // date, time manipulation and structure  
#include <RTClib.h>                                                 // real-time clock library
#include "Adafruit_seesaw.h"                                        // used by rotary encoder i2c module

#define SS_SWITCH        24                                         // used to set up virtual interrupt pin - not a real GPIO

#define SEESAW_ADDR      0x36                                       // i2c address of encoder

Adafruit_seesaw ss;

int32_t encoder_position;
int encoder_delta = 0;
int encoder_base = 0;
int32_t new_position;


// These are 'flexible' lines that can be changed - Works on Nano
#define TFT_CS 10
#define TFT_DC 9
#define TFT_RST -1 // RST can be set to -1 if you tie it to Arduino's reset

// Use hardware SPI (on Uno, #13, #12, #11) and the above for CS/DC
Adafruit_HX8357 tft = Adafruit_HX8357(TFT_CS, TFT_DC, TFT_RST);

// Arduino Nano
//      SPI Mosi = 11, MISO = 12, Clock/SCK = 13
//     SDA = A4 and SCL = A5
 
int MyYear;                                                     // Usual suspects for date and time
int MyMonth;                                                    // 
int MyDay;
int MyHour;                                                     // 
int MyMinute;
int MySecond;
int MyWeekDay;

const String  DaysOfTheWeek[7]= {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"};             // text for days of the week
const int DOW_Pad[7] = {30,30,18,0,12,30,12};                   // not an elegant way to do this, but centers each dow on the display
const String  Months[12]  =   {"-Jan-", "-Feb-", "-Mar-", "-Apr-", "-May-", "-Jun-", "-Jul-", "-Aug-", "-Sep-", "-Oct-", "-Nov-", "-Dec-"};  // for dd-mon-yyyy display

#define sunday  0                                 // used for the daylight savings time routine
#define monday  1                                 // 
#define tuesday 2                                 //  
#define wednesday 3                               // 
#define thursday  4                               // and instead we use the RTC3231 time and date for the DST logic, and must use 0-6
#define friday 5                                  // to match its 0-6 week for returned day of week
#define saturday 6
#define january 1                                 // months and days are used by DST routine among others
#define february 2
#define march 3
#define april 4
#define may 5
#define june 6
#define july 7
#define august 8
#define september 9
#define october 10
#define november 11
#define december 12



#define Screen_V 480
#define Screen_H 320
#define Circle_R Screen_H/2
#define cradians  57.29577951                                     // constant conversion from degrees to radians
#define dpm = 6;                                                      // constant conversion from minutes to degrees angle (e.g., 6 degrees*60 minutes or seconds = 360 degrees)
#define second_length  140                                          // Circle_R - 20   Length of each hand (though seconds is a 'dot')
#define minute_length  125                                          // Circle_R - 40
#define hour_length  80                                             // Circle_R - 80
#define tick_length 8                                               // pixels in from circle for 5 minute tick marks
float Last_sdangle; float sdangle; float hdangle; float mdangle;    // hand 'angles' for second, hour and minute
int XS1; int YS1; int XM1;  int YM1;  int XH1;  int YH1;            // persistent second, minute and hour hand coordinates
int Persistent_Second = -1;                                             // persistent second, etc., to eliminate un-needed updates
int Persistent_Day= -1;
int Persistent_Minute = -1;
int Persistent_Hour  = -1; 
String Persistent_HM_Display =  "88:88";                                //  

RTC_DS3231 rtc;                                                                   // structure for rtc DS3231 clock 
unsigned long t_unix_date;                                                        // unix date for time adjustments
#define onehour 3600                                                              // number of seconds in one hour for DST adjustments
bool Need_Setup = false;                                                          // assume clock is set, set 'true' when battery dead/lost power

/* debugging tools named after Digital Equipment Corporation's DDT (dynamic debugging technique)
 where the mini-computer was invented and where I worked for several years
 commented out to save space on the Nano (its up to 86% full!)
 
void DDTv(String st, int vt) {              // Print descriptor and value
  Serial.print("  ");
  Serial.print(st);
  Serial.print("  ");
  Serial.print(vt);}
void DDTvl(String st, int vt) {               // Print descriptor and value and new line
  DDTv(st, vt);
  Serial.println(" ");}
void DDTs(String st) {                     // Print string
  Serial.print(st);}
void DDTsl(String st) {                     // Print string and new line
  Serial.println(st);}
void DDTss(String st, String vt) {               // Print descriptor and string value
  Serial.print(st);
  Serial.print(vt);}
void DDTssl(String st, String vt) {               // Print descriptor and string value and new line
  Serial.print(st);
  Serial.println(vt);}
void DDTfl(String st, float fi) {          // Print descriptor and floating point value and new line
  Serial.print("  ");
  Serial.print(st);
  Serial.print("  ");
  Serial.println(fi, 4);}
*/

void setup() {
  Serial.begin(9600);
  
  tft.begin();                                                          // start display
  tft.setRotation(0);
  tft.setCursor(0, 0);
  tft.setTextColor(HX8357_WHITE);
  tft.setTextSize(3);
  Serial.println("Starting Setup");


Serial.println("Looking for seesaw!");                                        // see-saw is adafruit library used for encoder
  
  if (! ss.begin(SEESAW_ADDR) ) {
    Serial.println("Couldn't find seesaw on default address");
    while(1) delay(10);
  }
  Serial.println("seesaw started");

  uint32_t version = ((ss.getVersion() >> 16) & 0xFFFF);
  if (version  != 4991){
    Serial.print("Wrong firmware loaded? ");
    Serial.println(version);
    while(1) delay(10);
  }
  Serial.println("Rotary Encoder Started");
  
  // use a pin for the virtual  encoder switch
  ss.pinMode(SS_SWITCH, INPUT_PULLUP);

  // get starting position
  encoder_position = ss.getEncoderPosition();                                       // initialize encoder

  Serial.println("Turning on interrupts");
  delay(10);
  ss.setGPIOInterrupts((uint32_t)1 << SS_SWITCH, 1);
  ss.enableEncoderInterrupt();
  tft.fillScreen(HX8357_BLACK);
  tft.println("Starting");
  tft.println("Clock");
  delay(1000);
  if (! rtc.begin()) {                                                   // check that clock is there
    Serial.println("No Clock");
    tft.println("No Clock");
    tft.println("Found??");
    while (true) {                                                                // halt if no clock
    delay(500);}
  } else Serial.println("RTC Started");

if (rtc.lostPower()) {                                                      // if battery dead, 
  tft.println(" ");
  tft.println("Clock");
  tft.println("Battery Dead");
  tft.println("& Lost Power");
  tft.println(" ");
  tft.println("Please Set");
  tft.println("Date & Time");
  Serial.println("RTC Battery Dead");
  Need_Setup = true;
  delay(5000); }                                                        // Then get time using rotary encoder
 } // end of setup

void loop(void) {

  if (!ss.digitalRead(SS_SWITCH) || Need_Setup) {Serial.println("Button Pushed");
    Set_DateTime();}                                 // If rotary encoder shaft (button) pressed, get set up

    Load_DateTime();                                                                // else, read the clock and
    if (MySecond != Persistent_Second){                                             // if its the same second, do nothing
      Set_Clock_Hands();                                                            // else, set clock hands
      Set_Digital_Display();                                                        // set digital display
      Persistent_Second = MySecond; }                                               // and update persistent second

} // end of "loop"

void Load_DateTime(){                                                                // routine to get time from the real time clock
   
   DateTime now = rtc.now();                                                         //do an initial load time from the RTC                                  
   MyYear= now.year();
   MyMonth = now.month();
   MyDay = now.day();
   MyHour = now.hour();
   MyMinute= now.minute();
   MySecond = now.second();
   MyWeekDay= now.dayOfTheWeek();                                                       // RTC3231 returns 0-6 for day of week, not like unixdate return
   if (DSTInEffect()){                                                                            // if we are in DST then
     DateTime unow = rtc.now();                                                                   // Reload from RTC
     t_unix_date = unow.unixtime()+onehour;                                                          // 
     MyYear = year(t_unix_date);                                                                                                           
     MyMonth = month(t_unix_date);                                                  // bump forward one hour
     MyDay = day(t_unix_date);                                                       
     MyHour = hour(t_unix_date);                                                       // really only want to bump hour plus one,
     MyMinute = minute(t_unix_date);
     MySecond = second(t_unix_date);                                                  // but that could trigger day, date cascade
     MyWeekDay= (weekday(t_unix_date))-1;}                                            // time.h library weekday returns a 1-7, not 0-6 so emulate RTC by subtracting one                                           
    /*
    Serial.print("Today's Date and Time: ");                                          // uncomment to display what you have
    Serial.print("  ");
    Serial.print(MyWeekDay);
    Serial.print("  ");
    Serial.print(MyMonth);
    Serial.print("/");
    Serial.print(MyDay);
    Serial.print("/");
    Serial.print(MyYear);
    Serial.print("  ");
    Serial.print(MyHour);
    Serial.print(":");
    if (MyMinute < 10) {Serial.print("0");}
    Serial.print(MyMinute);
    Serial.print(":");
    if (MySecond < 10) {Serial.print("0");}
    Serial.print(MySecond);
    Serial.print(" ");
    if (DSTInEffect()) {Serial.println("DST On");
               } else {Serial.println("DST Off");}                          
  */
  } // end of Load_DateTime

  void Get_DateTime(){                                                          // Gets the date and time using rotary encoder
                                                                                // stepping through each of year, month, day, hour and minute (seconds are set to zero)
      int MyDayUL = 31;                                                         // "usual" days in Month
      
      encoder_base = 0;
      ss.setEncoderPosition(0);                                                 // reset encoder position with button press
      tft.fillScreen(HX8357_BLACK);                                             // clear the screen
      tft.setTextColor(HX8357_GREEN);
      tft.setTextSize(4);
      tft.setCursor(50, 350);
      tft.println("Setup Mode");
      delay(1000);
      MyYear = 2022;                                                                          // current year
      MyYear = Get_Input("Year",MyYear,2022,2222);                                            // Input, label, starting point, lower and upper bound
      MyMonth = 1;                                                                          // January, 
      MyMonth = Get_Input("Month",MyMonth,1,12);
      MyDay = 1;
      if ( (MyMonth == april) || (MyMonth == june) || (MyMonth == september) || (MyMonth == september) || (MyMonth == november) ) MyDayUL = 31;
      if (MyMonth == february) MyDayUL = 28;                                                  // February but not Leap Year
      if ( (MyMonth == february) && (MyYear%4 == 0) ) MyDayUL = 29;                           // Leap Year, and yes this is wrong for century years 
      MyDay = Get_Input("Day",MyDay,1,MyDayUL);
      MyHour = 1;                                                                            // starting point
      MyHour = Get_Input("Hour",MyHour,0,23);
      MyMinute = 1;                                                                           // again start in the middle
      MyMinute = Get_Input("Minute",MyMinute,0,59);
      MySecond = 0;                                                                           // when minute set, set seconds to zero 
      tft.fillScreen(HX8357_BLACK);                                                               // clear the screen
      tft.setTextColor(HX8357_GREEN);
      tft.setTextSize(4);
      tft.setCursor(50, 350);
      Need_Setup = false;                                                                       // don't need set up any more
      tft.println("Done!"); 
            
  }  // end of Get_DateTime

  int Get_Input(String GLabel, int GStart, int GLower, int GUpper){                               // routine to get all data input
      Serial.println("Get Date Time Called");
      tft.fillScreen(HX8357_BLACK);                                                               // clear the screen
      tft.setTextColor(HX8357_GREEN);
      tft.setTextSize(5);
      tft.setCursor(50, 350);
      tft.println(GLabel);                                                                        // identify which item we are getting                                             
      tft.setTextColor(HX8357_YELLOW);
      tft.setTextSize(4);
      tft.setCursor(50, 400);
      tft.println(GStart);                                                                        // and initial value
      while (true){                                                                               // infinite loop - 'return' is the exit
      if (! ss.digitalRead(SS_SWITCH)) {return GStart; }                                          // return updated value when button pushed again
      new_position = (ss.getEncoderPosition()*-1);                                                // encoder is counter-intuitive in +/- so adjust
      if (encoder_position != new_position) {
      encoder_delta =  new_position - encoder_position;
      encoder_base = encoder_base - encoder_delta;
//      Serial.print("The change is ");
//      Serial.println(encoder_delta);
      tft.setTextColor(HX8357_BLACK);                                                             // black out (erase) old value
      tft.setCursor(50, 400);
      tft.println(GStart);                                                                        // apply rotary encoder delta (-1 or +1) to base
      GStart = GStart + encoder_delta;
      if (GStart < GLower) GStart = GUpper;                                                         // ensure its between bounds
      if (GStart > GUpper) GStart = GLower;
      tft.setCursor(50, 400);
      tft.setTextColor(HX8357_YELLOW);
      tft.println(GStart);                                                                          // and print new value
      encoder_position = new_position; }     // and save for next round
      }
  }  // end of Get_Input

  void Set_DateTime(){   
    Serial.println("Set Date Time Called");
    Get_DateTime();                                                                                 // either 
    rtc.adjust(DateTime(MyYear, MyMonth, MyDay, MyHour, MyMinute, MySecond));                   // and set RTC
    DateTime now = rtc.now();                                                                   //do an initial load time from the RTC for the DST checker                                 
      MyYear= now.year();
      MyMonth = now.month();
      MyDay = now.day();
      MyHour = now.hour();
      MyMinute= now.minute();
      MySecond = now.second();
      MyWeekDay= now.dayOfTheWeek();
    if (DSTInEffect()) {                                                        // we adjust +1 hour in display during DST, so
     DateTime unow = rtc.now();                                                 // Reload from RTC
     t_unix_date = unow.unixtime()-onehour;                                     // and adjust BACK to std time
     rtc.adjust(DateTime(t_unix_date));}                                        // and re-set the time/date      
     delay(2000);
     Setup_Clock();
   } // end of Set_DateTime

bool DSTInEffect(){                                                                // returns true if DST in effect
                                                                                  // by examining month,day and hour (first Sunday in March at 2am, 2nd Sunday in November at 2am)                                                                                                                                                                                    
 int hhmm = (100*MyHour)+MyMinute;                                           // quick way to check if its past 2:05 am on 'change day'
       
 switch (MyMonth){ 
  case april:                                                                  // April thru October, we are in DST if its used
  case may:
  case june:
  case july:
  case august:
  case september:
  case october:
    return true;
    break;
  case december:                                                                // December thru February always NOT in DST if used
  case january:
  case february:
    return false;
    break;
  case march:                                                                    // in March, we ARE in DST on 2nd Sunday
     if (MyDay < 8) {return false;}                                                   // days 1 thru 7 can't be 2nd sunday
     if (MyDay >14){return true;}                                                   // days 14 and beyond always WILL be DST
     if (MyWeekDay == sunday){if  (hhmm >= 205) {return true;} else return false; } else  //Sunday AND after 2:05am then adjust
          {if (MyDay >= (8+MyWeekDay)) {return true;} else {return false;}}
          break;
 case november:                                                                       // in November, switch off of DST on first Sunday 
     if (MyDay > 7) {return false;}                                                        // Days 8 through end of month have to be non-DST
     if (MyWeekDay == sunday){if (hhmm < 205) {return true;} else return false; } else  //Sunday AND after 2:05am then adjust
          {if (MyDay >= (1+MyWeekDay))  {return false;} else {return true;}}
          break;

default: break;
      return true; 
 } // end of case on month
      return true;      
 } // end of DSTInEffect

 void Setup_Clock(){                                                                  //set clock face (circle and tick marks)
    int X1;
    int Y1;
    int X2;
    int Y2;
    float tdangle;                                                         // angle for tick marks at 5 minute intervals
    
    XS1 = 0;                                                            // reset persistent second, minute and hour hand coordinates
    YS1 = 0;                                                            // so initial 'blanking' doesn't cause problems  
    XM1 = 0;
    YM1 = 0;
    XH1 = 0;
    YH1 = 0;
    Persistent_Second = -1;
    Persistent_Day= -1;
    Persistent_Minute = -1;
    Persistent_Hour  = -1; 
    Persistent_HM_Display =  "     ";  
    
    tft.fillScreen(HX8357_BLACK);
    tft.drawCircle(Circle_R, Circle_R, 159,HX8357_WHITE);
    tft.drawCircle(Circle_R, Circle_R, 160,HX8357_WHITE);
    for( int z=0; z < 360;z= z + 30 ){                                    //Begin at 0 and stop at 360, marking every 5 minutes (5 min x 6 degrees = +
    tdangle=(float(z)/cradians);                                             //Convert degrees to radians
    int x1=(Circle_R+(sin(tdangle)*Circle_R));
    int y1=(Circle_R-(cos(tdangle)*Circle_R));
    int x2=(Circle_R+(sin(tdangle)*(Circle_R-tick_length)));
    int y2=(Circle_R-(cos(tdangle)*(Circle_R-tick_length)));
    tft.drawLine(x1,y1,x2,y2,HX8357_WHITE);
  }

 }  // end of clock face setup
 void Set_Clock_Hands(){      
      int X1;                                                                                     // target coordinates
      int Y1;                                         

      Last_sdangle = sdangle;                                                                // angle of second hand we are about to erase
      sdangle = (MySecond*6)/cradians;                                                     // seconds display 
      tft.fillCircle(XS1,YS1, 4,HX8357_BLACK);                                            // erase the old 'dot'
      tft.drawLine(Circle_R,Circle_R, XS1,YS1,HX8357_BLACK);                              // and the line
      XS1 = (Circle_R+(sin(sdangle)*(second_length)));                                     // calculate the new position
      YS1 = (Circle_R-(cos(sdangle)*(second_length)));
      tft.drawLine(Circle_R,Circle_R, XS1,YS1,HX8357_RED);                                  // 
      tft.fillCircle(XS1,YS1, 4,HX8357_RED);                                                // and draw new second 'dot'                  
      
      if ( (MyMinute != Persistent_Minute) || (Last_sdangle == mdangle) || (sdangle == mdangle)  ){  // minute changed or hands collide?
        tft.drawLine(Circle_R,Circle_R, XM1,YM1,HX8357_BLACK);                              // clear old minute hand
        tft.fillCircle(XM1,YM1,4,HX8357_BLACK);                                                   // and its tip
        mdangle = (MyMinute*6)/cradians;                                                      // minute
        X1 = (Circle_R+(sin(mdangle)*(minute_length)));                                        // draw new minute hand and
        Y1 = (Circle_R-(cos(mdangle)*(minute_length)));
        tft.drawLine(Circle_R,Circle_R, X1,Y1,HX8357_GREEN);                                  
        tft.fillCircle(X1,Y1,4,HX8357_GREEN);                                                 //tip                
        XM1 = X1;                                                                          // store what you drew to erase next time                                                               
        YM1 = Y1; }                                                                        //   persistent minute will be updated by digital display - dont do it yet
        
      if ( (MyMinute != Persistent_Minute) || (Last_sdangle == hdangle) || (sdangle == hdangle) ){  // minute changed or second hand collided?
        tft.drawLine(Circle_R,Circle_R, XH1,YH1,HX8357_BLACK);                                     // Black blanks out old hour hand and tip                              
        tft.fillCircle(XH1,YH1, 4,HX8357_BLACK);    
        hdangle = (((MyHour%12)*30) + int((MyMinute/12)*6))/cradians;                                // hour jumps by 30 degrees + fractional minutes of the hour degrees
        X1 = (Circle_R+(sin(hdangle)*(hour_length)));
        Y1 = (Circle_R-(cos(hdangle)*(hour_length)));
        tft.drawLine(Circle_R,Circle_R, X1,Y1,HX8357_YELLOW);
        tft.fillCircle(X1,Y1, 4,HX8357_YELLOW);
        XH1 = X1;                                                                          // store for next time
        YH1 = Y1;}            

      tft.fillCircle(Circle_R,Circle_R,5,HX8357_BLUE);
 } // end Set_Clock_Hands

 void Set_Digital_Display(){
      
      int Display_P;     
      String Display_String;
      
      if (MyDay != Persistent_Day) {                                                      // if its a new day then
        Setup_Clock();                                                                // clear whole display and then display outline of clock
        Set_Clock_Hands();                                                             // and clock
        tft.setTextSize(3);
        tft.setTextColor(HX8357_YELLOW);
        if (MyDay < 10){tft.setCursor(77, Screen_H + 75);} else {tft.setCursor(65, Screen_H + 75);}      // Center and ensure spacing for two digits for hour
        Display_String = Display_String + MyDay;
        Display_String = Display_String + Months[MyMonth-january];
        Display_String = Display_String + MyYear;
        tft.print(Display_String);
        tft.setCursor(83+DOW_Pad[MyWeekDay-sunday], Screen_H + 115);
        tft.setTextColor(HX8357_MAGENTA);
        tft.print(DaysOfTheWeek[MyWeekDay-sunday]);
        Persistent_Day = MyDay;}
      
      if (MyMinute != Persistent_Minute) {                                                  // if minute has changed,
        tft.setTextSize(5);                                                                 // blank out HH:MM                                 
        tft.setCursor(89, Screen_H + 20);
        tft.setTextColor(HX8357_BLACK);
        tft.print(Persistent_HM_Display);
        tft.setCursor(89, Screen_H + 20);                                                   // and refresh it
        tft.setTextColor(HX8357_WHITE);
        Display_P = MyHour%12;                                                              // Convert to 12 hour format
        if (Display_P == 0) {Display_P = 12;}                                               // and make midnite 12 too
        if (Display_P < 10){Display_String = " ";} else {Display_String = "";}              // ensure spacing for two digits for hour
        Display_String = Display_String + Display_P;
        Display_String = Display_String + ":";
        if (MyMinute < 10){Display_String = Display_String + "0";}                          // always two digit minute
        Display_String = Display_String + MyMinute;
        //if (MyHour < 12) {Display_String = Display_String + " AM";} else {Display_String = Display_String + " PM";}
        tft.print(Display_String);
        Persistent_HM_Display = Display_String;
        Persistent_Minute = MyMinute;}

 }  // end of Set Digital Display

Credits

spudnut1
10 projects • 15 followers
Contact

Comments

Please log in or sign up to comment.