Skip to main content

Build an Arduino Word Clock

5 min By Mike Gualtieri 953 words

A few months ago I came across an interesting project, an Arduino Word Clock. If you aren’t familiar with the term, it’s basically a clock that tells you the time as a sentence instead of with digits. So instead of 10:30pm, it would say, it is half past ten. The reported time accuracy is within 5 minutes, which is good enough for a general clock / interesting piece of art. I decided to build one as a Christmas present for my dad.

arduino word clock

Although I read a few different designs on the web, I decided to take my own path and design my own. I encourage you to do the same; sometimes it’s easier to do things the way you want to do them and look to others for inspiration, than follow instructions to the last detail.

The general steps are: 1) Create the clock face, 2) Wire the lights, 3) Build the driver for the lights, 4) Add the clock, and 5) Final assembly.

Creating the clock face

I started to create the clock face by planning out the word placement. I went with a layout like this, that has 23 words:

    IT ISATENHALF
    QUARTERTWENTY
    FIVE MINUTES
    PASTTO ONETWO
    THREEFOURFIVE
    SIXSEVENEIGHT
    NINETENELEVEN
    TWELVE OCLOCK

Since I decided on a proportional font, I had room for the extra word/letter A in the first row. I also added in some spaces. If you go with a fixed-width font, you would need to remove the A and all the spaces. I created this SVG to guide my stencil, printed it, and glued it to some thick cardboard. I then drilled holes into the center of each letter that will hold an LED for each letter. I also printed the stencil in reverse and used some rubber cement to temporarily glue the stencil to a thick piece of black oak tag paper, which will make the clock face. I used an x-acto knife to cut out each letter. This was a time consuming endeavor, to say the least. Be sure to save the middle cutouts for any letters that have one, so they can be glued back in during the final assembly.

wordclock LED backing

wordclock clock face stencil

Wiring the LEDs

This was also a time consuming process, mainly due to soldering a resistor to each of the 99 LEDs. Each word was soldered up in parallel, and connected to a pin on a shift register (more on that later). I went with individual LEDs for each letter, to ensure an even amount of light would be illuminated behind each letter. In retrospect, I wonder if using some LED strips would have looked good enough. It’s a good idea to label each group so you won’t make any mistakes while soldering. For ground, I used a strip of bare wire along each row. Since the shadow box that would be my final mount had a lot of room, I kept the positive lead on the LEDs sticking straight up.

LED circuit for word clock

Building the Arduino Driver

With the need to drive lights for 23 words, we will need to use shift registers, as the Arduino UNO doesn’t have 23 pin outs. If you don’t know what a shift register is, it’s basically a memory buffer that can be used to control a larger set of outputs from a smaller set. In our case, we will use 3 pins from the Arduino to drive three 8-bit 74HC595 shift registers. So, from three pins we can control up to 24 output states.

The nice part of the 74HC595, is that they can be chained together. Theoretically you could have infinite pins under your control.

Before I soldered things up, I created a small prototype to ensure I was able to properly control each LED group. The Arduino ShiftOut tutorial was quite helpful here.

shift register prototype

After everything was working well, I soldered it up on some perf board. Again, this was time consuming as there were many connections to make.

arduino word clock shift register driver

arduino word clock shift register driver

Adding the clock

A clock is only good if it can tell accurate time. And, it didn’t seem like a great option to build a bunch of controls to set the time each time it’s powered up. Instead I opted to add a battery backup RTC (Real Time Clock) to the device. Luckily Adafruit sells an Arduino RTC breakout board based on the popular DS1307 IC. Their tutorial was also quite helpful.

A small aside: I ran into trouble getting the breakout to work. The issue was a crossdev toolchain problem with the RTC library and my Gentoo Linux laptop. Details about the issue are on theAdafruit forums. Chances are very few people will run into this same issue.

Adafruit RTC breakout

Final assembly

If you are reading this deep in the article, you are probably wondering about the code used to power the clock. Before I wrote any code, I created these maps to show which register and pin should be lit for each word.

    reg pin
    0   0   IT IS
    0   1   A
    0   2   TEN
    0   3   HALF
    0   4   QUARTER
    0   5   TWENTY
    0   6   FIVE
    0   7   MINUTES
    1   0   PAST
    1   1   TO
    1   2   ONE
    1   3   TWO
    1   4   THREE
    1   5   FOUR
    1   6   FIVE
    1   7   SIX
    2   0   SEVEN
    2   1   EIGHT
    2   2   NINE
    2   3   TEN
    2   4   ELEVEN
    2   5   TWELVE
    2   6   OCLOCK
    
    For all: IT IS OCLOCK 0-0, 2-6
    
    1:00 IT IS ONE OCLOCK                               N
    1:05 IT IS FIVE MINUTES PAST ONE OCLOCK             0-6, 1-0, N
    1:10 IT IS TEN MINUTES PAST ONE OCLOCK              0-2, 0-7, 1-0, N
    1:15 IT IS A QUARTER PAST ONE OCLOCK                0-1, 0-4, 1-0, N
    1:20 IT IS TWENTY MINUTES PAST ONE OCLOCK           0-5, 0-7, 1-0, N
    1:25 IT IS TWENTY FIVE MINUTES PAST ONE OCLOCK      0-5, 0-6, 0-7, 1-0, N
    1:30 IT IS HALF PAST ONE OCLOCK                     0-3, 1-0, N
    1:35 IT IS TWENTY FIVE MINUTES TO TWO OCLOCK        0-5, 0-6, 0-7, 1-1, N+1
    1:40 IT IS TWENTY MINUTES TO TWO OCLOCK             0-5, 0-7, 1-1, N+1
    1:45 IT IS A QUARTER TO TWO OCLOCK                  0-1, 0-4, 1-1, N+1
    1:50 IT IS TEN MINUTES TO TWO OCLOCK                0-2, 0-7, 1-1, N+1
    1:55 IT IS FIVE MINUTES TO TWO OCLOCK               0-6, 0-7, 1-1, N+1
    2:00 IT IS TWO OCLOCK                               N
    

Here’s the code with comments to explain how things work. The code was heavily based on the ShiftOut and RTC examples, with some additional logic needed for this specific application.

    #include < wire.h >
    #include "RTClib.h"
    
    RTC_DS1307 RTC;
    
    // Pin connected to ST_CP of 74HC595
    int latchPin = 8;
    // Pin connected to SH_CP of 74HC595
    int clockPin = 12;
    // Pin connected to DS of 74HC595
    int dataPin = 11;
    
    
    // Holders for information you're going to pass to shifting function
    byte dataArray[10];
    byte dataToSend[3];
    
    
    
    
    void setup() {
      // Set pins to output because they are addressed in the main loop
      pinMode(latchPin, OUTPUT);
      Serial.begin(9600);
      Wire.begin();
      RTC.begin();
      
      // Set pins A2 and A3 so we can power the RTC breakout directly from each
      pinMode(A2, OUTPUT);
      digitalWrite(A2, LOW);
      pinMode(A3, OUTPUT);
      digitalWrite(A3, HIGH);
    
     
      if (! RTC.isrunning()) {
          Serial.println("RTC is NOT running!");
          // following line sets the RTC to the date & time this sketch was compiled
          RTC.adjust(DateTime(__DATE__, __TIME__));
      }   
    
      
      // These are the bitmasks used to set the shift registers  
      // states 0 - 7 set one of each of the pins to be on  
      // state 8 turns all the pins on, and state 9 sets them all off
      dataArray[0] = 0x01; //00000001
      dataArray[1] = 0x02; //00000010
      dataArray[2] = 0x04; //00000100
      dataArray[3] = 0x08; //00001000
      dataArray[4] = 0x10; //00010000
      dataArray[5] = 0x20; //00100000
      dataArray[6] = 0x40; //01000000
      dataArray[7] = 0x80; //10000000
      dataArray[8] = 0xFF; //11111111
      dataArray[9] = 0x00; //00000000
    
    
      // Function that blinks all the LEDs, feel free to eliminate...   
      // I got it directly from the Arduino ShiftOut tutorial and kept it in
      blinkAll_2Bytes(2,500);
      
      // Just runs through all the clock states each time its powered on...  
      // Feel free to eliminate  
      testAllStates(); 
    }
    
    
    
    
    void loop() 
    {
        // Get the current hour and minute  
        DateTime now = RTC.now();
    
        int hour = now.hour();
        int min  = now.minute();
    
        // Print statements for debugging
        /*
        Serial.print(now.hour(), DEC);
        Serial.print(':');
        Serial.print(now.minute(), DEC);
        Serial.println();
        */
          
        // We need to set the latch to 0 before each set of pin writes
        digitalWrite(latchPin, 0);  
    
    
        // IT IS and OCLOCK are lit in all states
        // Also initialize register 1 to 0's
        dataToSend[0] = dataArray[0];
        dataToSend[1] = dataArray[9];
        dataToSend[2] = dataArray[6];
    
    
        // Add bytes needed to light other pins
        if(min < 5)
        {
            //1:00 IT IS ONE OCLOCK
            // no need to light anything else
        }
        else if(min < 10)
        {
            //1:05 IT IS FIVE MINUTES PAST ONE OCLOCK
            dataToSend[0] += dataArray[6];
            dataToSend[1] += dataArray[0];
        }
        else if(min < 15)
        {
            //1:10 IT IS TEN MINUTES PAST ONE OCLOCK
            dataToSend[0] += dataArray[2] + dataArray[7];
            dataToSend[1] += dataArray[0];
        }
        else if(min < 20)
        {
            //1:15 IT IS A QUARTER PAST ONE OCLOCK
            dataToSend[0] += dataArray[1] + dataArray[4];
            dataToSend[1] += dataArray[0];        
        }
        else if(min < 25)
        {
            //1:20 IT IS TWENTY MINUTES PAST ONE OCLOCK 
            dataToSend[0] += dataArray[5] + dataArray[7];
            dataToSend[1] += dataArray[0];
        }
        else if(min < 30)
        {
            //1:25 IT IS TWENTY FIVE MINUTES PAST ONE OCLOCK
            dataToSend[0] += dataArray[5] + dataArray[6] + dataArray[7];
            dataToSend[1] += dataArray[0];
        }
        else if(min < 35)
        {
            //1:30 IT IS HALF PAST ONE OCLOCK 
            dataToSend[0] += dataArray[3];
            dataToSend[1] += dataArray[0];
        }
        else if(min < 40)
        {
            //1:35 IT IS TWENTY FIVE MINUTES TO TWO OCLOCK
            dataToSend[0] += dataArray[5] + dataArray[6] + dataArray[7];
            dataToSend[1] += dataArray[1];
        }
        else if(min < 45)
        {
            //1:40 IT IS TWENTY MINUTES TO TWO OCLOCK
            dataToSend[0] += dataArray[5] + dataArray[7];
            dataToSend[1] += dataArray[1];
        }
        else if(min < 50)
        {
            //1:45 IT IS A QUARTER TO TWO OCLOCK
            dataToSend[0] += dataArray[1] + dataArray[4];
            dataToSend[1] += dataArray[1];
        }
        else if(min < 55)
        {
            //1:50 IT IS TEN MINUTES TO TWO OCLOCK
            dataToSend[0] += dataArray[2] + dataArray[7];
            dataToSend[1] += dataArray[1];
        }
        else if(min < 60)
        {
            //1:55 IT IS FIVE MINUTES TO TWO OCLOCK
            dataToSend[0] += dataArray[6] + dataArray[7];
            dataToSend[1] += dataArray[1];
        }
    
    
        // Set hour register and pin
        if(min < 35)
        {
            int reg = getHourRegister(hour);
            int pin = getHourPin(hour);
            dataToSend[reg] += dataArray[pin];
        }
        else
        {
            int reg = getHourRegister(hour + 1);
            int pin = getHourPin(hour + 1);
            dataToSend[reg] += dataArray[pin];
        }
            
    
        shiftOut(dataPin, clockPin, dataToSend[0]);
        shiftOut(dataPin, clockPin, dataToSend[1]);  
        shiftOut(dataPin, clockPin, dataToSend[2]);  
        
        
        digitalWrite(latchPin, 1);
        delay(5000);
    }
    
    
    
    
    // the heart of the program
    void shiftOut(int myDataPin, int myClockPin, byte myDataOut) {
      // This shifts 8 bits out MSB first, 
      //on the rising edge of the clock,
      //clock idles low
    
      //internal function setup
      int i=0;
      int pinState;
      pinMode(myClockPin, OUTPUT);
      pinMode(myDataPin, OUTPUT);
    
    
      //clear everything out just in case to
      //prepare shift register for bit shifting
      digitalWrite(myDataPin, 0);
      digitalWrite(myClockPin, 0);
    
    
      //for each bit in the byte myDataOut
      //NOTICE THAT WE ARE COUNTING DOWN in our for loop
      //This means that %00000001 or "1" will go through such
      //that it will be pin Q0 that lights. 
      for (i=7; i>=0; i--)  {
        digitalWrite(myClockPin, 0);
    
    
        //if the value passed to myDataOut and a bitmask result 
        // true then... so if we are at i=6 and our value is
        // %11010100 it would the code compares it to %01000000 
        // and proceeds to set pinState to 1.
        if ( myDataOut & (1<<i) ) {
          pinState= 1;
        }
        else {    
          pinState= 0;
        }
    
    
        //Sets the pin to HIGH or LOW depending on pinState
        digitalWrite(myDataPin, pinState);
        //register shifts bits on upstroke of clock pin  
        digitalWrite(myClockPin, 1);
        //zero the data pin after shift to prevent bleed through
        digitalWrite(myDataPin, 0);
      }
    
    
      //stop shifting
      digitalWrite(myClockPin, 0);
    }
    
    
    
    //blinks the whole register based on the number of times you want to 
    //blink "n" and the pause between them "d"
    //starts with a moment of darkness to make sure the first blink
    //has its full visual effect.
    void blinkAll_2Bytes(int n, int d) 
    {
        digitalWrite(latchPin, 0);
        shiftOut(dataPin, clockPin, 0);
        shiftOut(dataPin, clockPin, 0);
        digitalWrite(latchPin, 1);
        
        delay(200);
      
        for (int x = 0; x < n; x++) 
        {
          digitalWrite(latchPin, 0);
          shiftOut(dataPin, clockPin, 255);
          shiftOut(dataPin, clockPin, 255);
          digitalWrite(latchPin, 1);
          delay(d);
          
          digitalWrite(latchPin, 0);
          shiftOut(dataPin, clockPin, 0);
          shiftOut(dataPin, clockPin, 0);
          digitalWrite(latchPin, 1);
          delay(d);
        }
    }
    
    
    
    // Returns the register used for the hour word
    int getHourRegister(int hour)
    {
      if(hour > 12)
      {
        hour-=12;
      }
      
      if(hour <= 6)
      {
        return 1;
      }
     
      return 2;
    }
    
    
    // Returns the pin output for the hour word
    int getHourPin(int hour)
    {
      if(hour > 12)
      {
        hour-=12;
      }
      
      if(hour == 1)
      {
        return 2;
      }
      else if(hour == 2)
      {
        return 3;
      }
      else if(hour == 3)
      {
        return 4;
      }
      else if(hour == 4)
      {
        return 5;
      }
      else if(hour == 5)
      {
        return 6;
      }
      else if(hour == 6)
      {
        return 7;
      }
      else if(hour == 7)
      {
        return 0;
      }
      else if(hour == 8)
      {
        return 1;
      }
      else if(hour == 9)
      {
        return 2;
      }
      else if(hour == 10)
      {
        return 3;
      }
      else if(hour == 11)
      {
        return 4;
      }
      else if(hour == 12)
      {
        return 5;
      }
      
    }
      
    
      
    // Mostly cut and paste from above
    int testAllStates()
    {
      int min  = 0;
      int hour = 12;
      while(min < 60)
      {
        digitalWrite(latchPin, 0);  
    
        dataToSend[0] = dataArray[0];
        dataToSend[1] = dataArray[9];
        dataToSend[2] = dataArray[6];
    
    
        // Add bytes needed to light other pins
        if(min < 5)
        {
            //1:00 IT IS ONE OCLOCK
            // no need to light anything else
        }
        else if(min < 10)
        {
            //1:05 IT IS FIVE MINUTES PAST ONE OCLOCK
            dataToSend[0] += dataArray[6];
            dataToSend[1] += dataArray[0];
        }
        else if(min < 15)
        {
            //1:10 IT IS TEN MINUTES PAST ONE OCLOCK
            dataToSend[0] += dataArray[2] + dataArray[7];
            dataToSend[1] += dataArray[0];
        }
        else if(min < 20)
        {
            //1:15 IT IS A QUARTER PAST ONE OCLOCK
            dataToSend[0] += dataArray[1] + dataArray[4];
            dataToSend[1] += dataArray[0];        
        }
        else if(min < 25)
        {
            //1:20 IT IS TWENTY MINUTES PAST ONE OCLOCK 
            dataToSend[0] += dataArray[5] + dataArray[7];
            dataToSend[1] += dataArray[0];
        }
        else if(min < 30)
        {
            //1:25 IT IS TWENTY FIVE MINUTES PAST ONE OCLOCK
            dataToSend[0] += dataArray[5] + dataArray[6] + dataArray[7];
            dataToSend[1] += dataArray[0];
        }
        else if(min < 35)
        {
            //1:30 IT IS HALF PAST ONE OCLOCK 
            dataToSend[0] += dataArray[3];
            dataToSend[1] += dataArray[0];
        }
        else if(min < 40)
        {
            //1:35 IT IS TWENTY FIVE MINUTES TO TWO OCLOCK
            dataToSend[0] += dataArray[5] + dataArray[6] + dataArray[7];
            dataToSend[1] += dataArray[1];
        }
        else if(min < 45)
        {
            //1:40 IT IS TWENTY MINUTES TO TWO OCLOCK
            dataToSend[0] += dataArray[5] + dataArray[7];
            dataToSend[1] += dataArray[1];
        }
        else if(min < 50)
        {
            //1:45 IT IS A QUARTER TO TWO OCLOCK
            dataToSend[0] += dataArray[1] + dataArray[4];
            dataToSend[1] += dataArray[1];
        }
        else if(min < 55)
        {
            //1:50 IT IS TEN MINUTES TO TWO OCLOCK
            dataToSend[0] += dataArray[2] + dataArray[7];
            dataToSend[1] += dataArray[1];
        }
        else if(min < 60)
        {
            //1:55 IT IS FIVE MINUTES TO TWO OCLOCK
            dataToSend[0] += dataArray[6] + dataArray[7];
            dataToSend[1] += dataArray[1];
        }
    
    
        // Set hour register and pin
        if(min < 35)
        {
            int reg = getHourRegister(hour);
            int pin = getHourPin(hour);
            dataToSend[reg] += dataArray[pin];
        }
        else
        {
            int reg = getHourRegister(hour + 1);
            int pin = getHourPin(hour + 1);
            dataToSend[reg] += dataArray[pin];
        }
            
    
        shiftOut(dataPin, clockPin, dataToSend[0]);
        shiftOut(dataPin, clockPin, dataToSend[1]);  
        shiftOut(dataPin, clockPin, dataToSend[2]);  
        
        
        digitalWrite(latchPin, 1);
        delay(200);
        min+=1;
      }
    }
    

Next the LEDs need to be hooked up to the Arduino driver board. Lots of wires, lots of fun!

leds soldered to driver board

On the front of the LED board, I blocked off each section by first cutting strips of oaktag paper to block off each row, and next block off each word with a small piece of cardboard. Each word is blocked out so words not lit appear dark. It will take some adjusting to get things looking proper during the final assembly. Behind the front face stencil, I included a piece of dark tissue paper to filter the light though and create a nice effect.

LED board words

Mount everything else inside your display case (I used a shadowbox purchased at a local craft store), and you are done!

arduino word clock

arduino word clock

Here is a schematic of the clock:

Arduino word clock schematic