Introduction: DIY Apple Clock

About: These instructables are mostly about electronics. I hope you find them helpful!

OK so there's no such thing as the Apple Clock.
However, you can make your own retro-style bubble display clock in an Apple earbuds case.

I'm going to show you how to do that just sit tight.

Step 1: Parts

You're going to need some parts for this project.

  • 1x PCB (not shown)
  • 1x Apple earbuds case (not shown)
  • 1x 74HC595 shift register
  • 1x ATTiny44 or ATTiny84
  • 1x 16MHz crystal oscillator
  • 1x 1N4001 or 4007 power diode
  • 1x USB B mini port
  • 1x Li-Ion rechargeable battery
  • 1x bubble display
  • These two bags of nuts and bolts I bought at RadioShack and I don't know what to call it but its in the pictures
  • 2x pushbutton switches
  • 3x 10 kohm resistors
  • 4x 330 ohm resistors

You'll of course need some tools like a soldering iron, home PCB manufacturing tools, saw or dremel, drill, screwdrivers, etc.

Step 2: Prototyping and Files!

If you just want to build the project this step isn't too necessary. I'm going to be talking about how the code and hardware work.

I'm going to go through the code line by line now :D

#include "avr/interrupt.h"  //We are using pin change interrupts

The ATtiny24, 44, 84 series only has 1 external interrupt (INT0) [Page 48 of the datasheet]. This is the interrupt that we are most familiar with using on the Arduino. You can simply use this interrupt by saying "attachInterrupt()"

While it only had 1 interrupt, I wanted to have two buttons on my clock: one button to increase the time and another to decrease the time. Looking back on the datasheet, the ATtiny has two Pin Change Interrupt ports: PCINT0 and PCINT1 [Page 48].

The difference between External Interrupts and Pin Change Interrupts is that an External Interrupt has its own ISR (interrupt service routine). PCINTerrupts share one ISR for all pins on one port. This means that with Pin Change Interrupts we can make any pin be an interrupt, but then we have to go through the trouble of determining which pin caused the interrupt on the port.

Here are two really good articles to read. This is how I learned:

volatile int dig1 = 0;    //Hours tens
volatile int dig2 = 0;    //Hours ones
volatile int dig3 = 0;    //Minutes tens
volatile int dig4 = 0;    //Minutes ones

Each of these variables determines what number is displayed on each of the four digits on the bubble display.
I declared them volatile because they will be changed in the interrupt routines.
Setting them to 0 means when I turn on my clock, the count will start at 00:00 (12:00 AM).

const int latchPin = A3;  //Pin connected to ST_CP of 74HC595
const int clockPin = A4;  //Pin connected to SH_CP of 74HC595
const int dataPin = A5;   //Pin connected to DS of 74HC595

Physical pins 8(A5), 9(A4), and 10(A3) will be used with the 74HC595 shift register.
The shift register will allow us to control segments a, b, c, d, e, f, g, and dp with pins Q0, Q1, Q2, Q3, Q4, Q5, Q6, and Q7 respectively.
To display each digit, we will be multiplexing.

const int dig1Pin = A0;  //Pin to multiplex and display digit1
const int dig2Pin = A1;  //Pin to multiplex and display digit2
const int dig3Pin = A2;  //Pin to multiplex and display digit3
const int dig4Pin = A6;  //Pin to multiplex and display digit4

The bubble display is common cathode. This means that when the digit is grounded or pulled to a digital LOW, the digit will light up. We will be using physical pins 13(A0), 12(A1), 11(A2), and 7(A6) to control the display of each digit on the bubble display.

//Two values below for debouncing.
volatile unsigned long xlastDebounceTime = 0;
volatile unsigned long ylastDebounceTime = 0;
long debounceDelay = 150; // the debounce time; increase if the output flickers

These are some values we will be using to debounce the two buttons when the interrupt is activated. Without debouncing, one button press could be registered as five, and using the clock would be annoying.
Check out Arduino Examples/Digital/Debounce if you want.

//We will be using this value to prevent rolloever for millis() that occurs every 49 days.
unsigned long currentMillis = 0;
volatile unsigned long lastMillis = 0;

Arduino can hold long variables. I will be using long variables currentMillis and lastMillis to read millis(); After 49 days, the value of millis() will get too big for the microcontroller and overflow back to 0.
This is a bad thing if I'm comparing currentMillis and lastMillis to debounce and the value of currentMillis becomes 0. I'll explain a little more later.

int secTime = 0;  //The seconds counter
byte tictoc = 0;  //Flashing dot

Initializing some stuff. secTime is the seconds counter and tictoc determines whether to show the dot or not each second.

void setup()
{
  pinMode(latchPin, OUTPUT);
  pinMode(clockPin, OUTPUT);
  pinMode(dataPin, OUTPUT);
  pinMode(dig1Pin, OUTPUT);
  pinMode(dig2Pin, OUTPUT);
  pinMode(dig3Pin, OUTPUT);
  pinMode(dig4Pin, OUTPUT);
  cli();  //turns interrupts off
  GIMSK |= 0b00110000;  //turn on pin change interrupts
  PCMSK0 |= 0b10000000;  //turn on interrupts on PCINT7(A7)
  PCMSK1 |= 0b00000100;  //turn on interrupts on PCINT10(8)
  sei();  //turns interrupts back on
}

In the setup function, I will be declaring the shift register pins and multiplexing pins as OUTPUTS

The following is how to initialize the Pin Change Interrupts.
cli(); is a function we need to call to turn interrupts off.

GIMSK is the General Interrupt Mask Register, a register on the ATtiny. Check out page 51 of the datasheet to look at what each bit of the GIMSK register does.
0b00110000 turns on the 4th and 5th bits on the register, which turn on PCIE0 and PCIE1. PCIE0 enables Pin Change Interrupts for the port that has pins 0 to 7. PCIE1 enables Pin Change Interrupts for the part that has pins 8 to 11.

PCMSK0 is the Pin Change Enable Mask for the pins 0 to 7. This is activated from PCIE0. I want to enable an interrupt on PCINT7, or A7 on the ATtiny44.
To do this I activate bit 7: 0b10000000 [Page 53]

PCMSK1 is the same as PCMSK0 except for pins 8 to 11. This is activated from PCIE1. I want to enable an interrupt on PCINT10, or pin 8.
To do this, I enable the 2nd bit: 0b00000100 [Page 52]

sei(); turns on the interrupts.

void loop()
{
  display(dig1,dig2,dig3,dig4);    //display all digits
  
  //////////////////////////////////////////////////////////////////////
  //////////////////////////////////////////////////////////////////////
  //This part toggles the dot every second
  currentMillis = millis();
  if((unsigned long)(currentMillis - lastMillis) >= 1000)
  {
    secTime++;      //Taking use of this if loop to increment the second
    if(tictoc == 1)
    {
      dig2 = dig2 - 10;
      tictoc = 0;
    }
    else
    {
      dig2 = dig2 + 10;
      tictoc = 1;
    }
    lastMillis = millis();
  }
  //////////////////////////////////////////////////////////////////////
  //////////////////////////////////////////////////////////////////////

display() is a function that I will explain in the next step. It does just what you think it does.

The bracket of code basically toggles the dot on display 2 ever second to simulate tics and tocs.
Basically, every 1000 milliseconds, the code checks for whether tictoc is 1 or 0. If it's 0, it turns the dot on and sets tictoc to 1. If it's 1, it turns the dot off and sets tictoc back to 0.
the +10 and -10 will make sense later
Included in this bracket is also an increment for secTime, which counts how many seconds have passed.

//////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////
  //This part does the counting on the clock.
  if(secTime == 60)
  {
    secTime = 0;
    dig4++;
    if(dig4 == 10)
    {
      dig4 = 0;
      dig3++;
      if(dig3 == 6)
      {
        dig3 = 0;
        dig2++;
        if(dig2 == 10)
        {
          dig2 = 0;
          dig1++;
          if(dig1 == 3)
          {
            dig1 = 0;
          }
        }
      }
    }
  }
  if(dig1 == 2 && dig2 == 4)
  {
    dig1 = 0;
    dig2 = 0;
  }
  //The if loops above just tell when the digits should reset or roll over
  //////////////////////////////////////////////////////////////////////
  //////////////////////////////////////////////////////////////////////
}

If secTime reaches 60, we increment dig4 (minutes)

The following if loops basically determine when the digit overflows and should be reset. For example, if we have 9 on dig4, the next should be 0 and dig2 should be incremented by 1.

If dig1 (hours tens) is 2 and dig2 (hours ones) is 4, we reset the clock to 00.

/***********************************************************************/
//Below are the pin change interrupt functions
//////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////
//Executes if A7 is pushed
ISR(PCINT0_vect)
{
  secTime = 0;      //Make seconds 0
  if(tictoc == 1)   //Set the dot to 0 so we aren't confused
  {
    dig2 = dig2 - 10;
    tictoc = 0;
  }
  currentMillis = millis();
  if ((unsigned long)(currentMillis - xlastDebounceTime) >= debounceDelay)
  {
    dig4--;
    if(dig4 == -1)
    {
      dig4 = 9;
      dig3--;
      if(dig3 == -1)
      {      
        dig3 = 5;
        dig2--;
        if(dig2 == -1)
        {
          dig2 = 9;
          dig1--;
          if(dig1 == -1)
          {
            dig1 = 2;
          }
        }
      }
    }
    //The above was, again, more if loops for digit overflow and stuff.
    
    //Below is handling the problem of going to 29 instead of 00 when backing up.
    if(dig1 == 2 && dig2 == 9)
    {
      dig2 = 3;
    }
    xlastDebounceTime = millis();
  }
}
//////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////

ISR(PCINT0_vect){} is the interrupt routine that is executed when PCINT0 is activated by pin A7.
This function basically sets secTime to 0, and decrements dig4 by 1.
The following if loops basically handle overflow.
The whole function is wrapped by a debouncing if loop.

//////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////
//Executes if 8 is pushed
ISR(PCINT1_vect)
{
  secTime = 0;      //Make seconds 0
  if(tictoc == 1)   //Set the dot to 0 so we aren't confused
  {
    dig2 = dig2 - 10;
    tictoc = 0;
  }
  currentMillis = millis();
  if ((unsigned long)(currentMillis - ylastDebounceTime) >= debounceDelay)
  {
    dig4++;
    if(dig4 == 10)
    {
      dig4 = 0;
      dig3++;
      if(dig3 == 6)
      {
        dig3 = 0;
        dig2++;
        if(dig2 == 10)
        {
          dig2 = 0;
          dig1++;
          if(dig1 == 3)
          {
            dig1 = 0;
          }
        }
      }
    }
    //The above was, again, more if loops for digit overflow and stuff. 
    
    //Below is handling the problem of going to 24 instead of 00 when going forwards.
    if(dig1 == 2 && dig2 == 4)
    {
      dig1 = 0;
      dig2 = 0;
    }
    ylastDebounceTime = millis();
  }
}

ISR(PCINT1_vect){} is the interrupt routine that is executed when PCINT1 is activated by pin 8.
This basically does the same thing as the other interrupt except increment instead of decrement.

This is whats going on in the WATCHG.ino sketch. In order to stay organized, I made another sketch ShiftDatas.ino to hold some variables and functions. We will go over that in the next step.

Step 3: Prototyping Continued

ShiftDatas.ino basically holds the array which has the bytes for each character/number that can be displayed.

It also has the display() function which is basically shifts out a byte for a character for a digit, turns it off, shifts out a different byte, and turns off another digit. This is multiplexing.

byte digits[21] =
{
  0xFC,  //0  case 0
  0x60,  //1  case 1
  0xDA,  //2  case 2
  0xF2,  //3  case 3
  0x66,  //4  case 4
  0xB6,  //5  case 5
  0xBE,  //6  case 6
  0xE0,  //7  case 7
  0xFE,  //8  case 8
  0xE6,  //9  case 9
  0xFD,  //0.  case 10
  0x61,  //1.  case 11
  0xDB,  //2.  case 12
  0xF3,  //3.  case 13
  0x67,  //4.  case 14
  0xB7,  //5.  case 15
  0xBF,  //6.  case 16
  0xE1,  //7.  case 17
  0xFF,  //8.  case 18
  0xE7,  //9.  case 19
  0x00,  //BLANK  case 20
};

This is the array of characters. You can call a certain byte 'n' by calling digits[n].
Like I said earlier, Q0, Q1, Q2, Q3, Q4, Q5, Q6, and Q7 each control a, b, c, d, e, f, g, and dp respectively.
If I wanted to display the number '0', I need to turn on segments a, b, c, d, e, and f. This means the shift register needs to shift out 0b11111100, which is a hexidecimal 0xFC.
That's how I did that.

//4 arguments for 4 displays.
//This part is to multiplex.
void display(int disp1,int disp2,int disp3,int disp4)
{
  //First digit (hours tens)
  digitalWrite(latchPin, LOW);
  shiftOut (dataPin, clockPin, LSBFIRST, digits[disp1]);
  digitalWrite(latchPin, HIGH);
  digitalWrite(dig1Pin, LOW);
  digitalWrite(dig2Pin, HIGH);
  digitalWrite(dig3Pin, HIGH);
  digitalWrite(dig4Pin, HIGH);
  
  //Second digit (hours ones)
  digitalWrite(latchPin, LOW);
  shiftOut (dataPin, clockPin, LSBFIRST, digits[disp2]);
  digitalWrite(latchPin, HIGH);
  digitalWrite(dig1Pin, HIGH);
  digitalWrite(dig2Pin, LOW);
  digitalWrite(dig3Pin, HIGH);
  digitalWrite(dig4Pin, HIGH);
  
  //Third digit (minutes tens)
  digitalWrite(latchPin, LOW);
  shiftOut (dataPin, clockPin, LSBFIRST, digits[disp3]);
  digitalWrite(latchPin, HIGH);
  digitalWrite(dig1Pin, HIGH);
  digitalWrite(dig2Pin, HIGH);
  digitalWrite(dig3Pin, LOW);
  digitalWrite(dig4Pin, HIGH);
  
  //Fourth digit (minutes ones)
  digitalWrite(latchPin, LOW);
  shiftOut (dataPin, clockPin, LSBFIRST, digits[disp4]);
  digitalWrite(latchPin, HIGH);
  digitalWrite(dig1Pin, HIGH);
  digitalWrite(dig2Pin, HIGH);
  digitalWrite(dig3Pin, HIGH);
  digitalWrite(dig4Pin, LOW);
}

Remember earlier how I wrote display(dig1, dig2, dig3, dig4) in the WATCHG code?
Each arguement is handled in this function.
Lets say I wanted to display 04:20. This means dig1 is 0, dig2 is 4, dig3 is 2, and dig1 is 0.

To display all of the numbers, I do this:

  1. shift out bits for 0, turn on dig4
  2. turn off dig4, shift out bits for 2, turn on dig3.
  3. turn off dig3, shift out bits for 4, turn on dig2.
  4. turn off dig2, shift out bits for 0, turn on dig1.

And then cycle forever. The microcontroller does this so fast the numbers appear to show at the same time, though each one is switching on and off really fast.

Step 4: Program Upload

Even though there are two Arduino codes, just open WATCHG.ino and upload that one. ShiftDatas will open as a tab along with it.

The microcontroller will be an ATtinyX4 running at 16MHz, so make sure you burn the bootloader to the ATtiny before uploading the code. After you burn the bootloader, you will need a 16MHz crystal in order to upload the program.

Step 5: PCB Mfg.

To make this thing all professional, I wanted to make a printed circuit board for it. I have two boards to keep the circuit small and compact. One is the actual brains of the circuit, and the other is for the battery, recharging, and step up circuit.

Attached are the RRB files for the circuit boards. I use a program called Copper Connection.
Here's the free program if you want to edit the files: http://www.robotroom.com/CopperConnection/

I also attached the PDF files for toner transfer or photosensitive transfer methods. You just need to print out the PDF as Actual Size.

I will be showing you how to use the photosensitive transfer method.

  1. Print out the PCB designs on overhead transparencies
  2. Measure out your circuit board and cut. The circuit board is 4.1mm by 4.3mm if you're wondering
    You want to be precise and clean when you measure and cut so the final product comes out nice. Everything is pretty tight in this project.

EDIT:

I've added a schematic from Fritzing for those who want it. It's in the zip folder. Sorry if it's messy, I used the auto-route function. If you have Fritzing that might make it easier to follow.

Step 6: PCB Mfg. Continued, WATCHG

Since there are two boards to make, we will do each individually.

The first board is the main computation board.

  1. Peel off the protective layer and lay the transparency in the right orientation on top of the board.
    You have to check for whether the transparency is upside down or not, and if you have the 4.1mm and 4.3mm sides matching properly
  2. Lay a transparent piece of material on top and put the flashlight lantern on top.
  3. Wait 7 to 8 minutes.
  4. Put the board in developer until the traces appear.
  5. Put the board in etchant.

Step 7: PCB Mfg. Continued, Powerboard

While the other board is etching, we can work on the second on.

We basically follow the same steps as the other previous board.

  1. After both boards are in the etchant you should go do something else for a while. Etching takes a while if you're watching it etch.
  2. After the boards are finished, wipe off the photosensitive coat with nail polish remover
  3. While etching, some scratching got rid of some traces on my board. If this happens to you, makes sure you remember to repair them when you're soldering
  4. After you take note of any broken traces, go on and drill the holes on the board.

Step 8: Soldering!

Now you're ready to solder the parts into the PCB. Refer to the PCB files in the previous steps to know what which component goes where

Some tips:

  • Solder low profile and small parts first.
  • Check the orientation of diodes and chips by comparing it to the PCB files.
  • Everything on the powerboard is SMD. Just bend the leads to fit.
  • To solder the powerboard on, add solder from the above pads and heat the pad. The solder will drip down and attach itself to the PCB.
  • Hot glue the battery to the board.

After both boards are soldered and any broken traces are fixed, solder the power from the powerboard to the mainboard. Check if it's working properly. If not, desolder power and debug.

After everything works and the power lines are soldered, add a blob of hot glue on each end of the power line to prevent it from wear.

Secure the first nut and bolt onto the powerboard, then stack the main board on top of it.

If you don't have an Apple earbuds case you can stop here! You have a good looking clock now.

Step 9: Modifying the Apple Earbuds Case

I decided that this clock would look really nice inside the Apple Earbuds case. There are a few things we need to do before the clock can fit in the box.

  1. Cut off the earbud holding part of the case. I recommend NOT using a dremel for this. A hacksaw or bandsaw works well.
  2. Tear off the remaining ridges and use a Dremel to cut a relief for the USB port. The ridges can be easily torn off using pliers.
  3. Mark and drill holes for the nuts and bolts to secure the board to the case.
  4. Put the power board on, then the main board. Using two nuts will keep the board pretty secure
  5. Cover the circuitry with masking tape and then cut off the excess from the bolt so it can fit under the case cover.
  6. Drill two holes in the cover so that buttons can be pushed with a pencil.

Step 10: Finished!

Now you're finished! This makes a cute wall clock or a desk clock.

To charge, simply plug it in to a USB B mini cable for about 10 minutes. It's a small battery and USB ports offer 1A of current.

The circuit isn't that energy efficient: at 16MHz, the clock can run for maybe 10 hours without needing to recharge. It's not that energy efficient, but it looks nice :P.

I hope you enjoyed!

@Epilog Contest, a laser cutter would be really useful to make neater cuts on the plastic casing.