Introduction: Steampunk Analog Gauge Moonphase Clock

I made a steampunk clock using an old wooden telephone box, three analog gauges, switches, a viewport, and a wind-up mechanism. The clock displays the hours, minutes and seconds on the gauges and when you wind it up, it displays the month, date and day of the week as well as playing a video of the correct moon phase for the current date and time. The dials are electroluminescent and are activated and dimmed by touch. The clock plays audio including four selectable volume controllable clock sounds and random sound effects when the moon phase video is being played. The time is set using the chicken head knobs and the date is set via a usb port.
The code is written for the arduino platform (I'm using a mega) and the arduino IDE.

Check out the video,

Cheers!

And here's a link to the longer version of the video with closeups and more detail:

https://www.instructables.com/id/Toms-Clock/

Step 1: A Few Words for Newbs

I am a mechanical engineer by training and by practice. I like to hack around with electronics and know enough about them to be dangerous and to be able to build some fun stuff.   I am not a trained programmer but have written some pretty extensive programs over my career. Do not be intimidated by this project, please.
I firmly believe that anyone with gumption can take on some or even all of this project and be successful. Break it down into pieces. I'll try to give you as much info as possible to lead you in the right direction.

Step 2: What I Used


1) An early 1900s oak telephone box (ebay)
2) Three vintage round analog volt meters (ebay)
3) A fiveway switch (Sparkfun)
4) Arduino Mega (Sparkfun)
5) Arduino Mega prototyping shield
6) Chronodot (Dallas 3231 real time clock board with battery backup) (www.macetech.com)
7) Micro OLED 128 by 4D systems (Saelig)
8) SOMO-14D sound board and amp by 4D systems (Sparkfun)
9) Three rotary encoders with pushbutton  (Sparkfun)
10) Capacitive touch sensor board MPR121 (Sparkfun)
11) Three small FETs to drive the gauges
12) Three 10k trimpots to trim the PWM output to the gauges
13) Watch crystal for the viewport (www.esslinger.com) I used the 29mm magnifier
14) Clock wind up mechanism made from old clock parts
15) Small microswitch for wind up mechanism
16) Custom trim hardware
17) Electroluminescent sheet (  cA4-4S-BG ) (e-luminates.com)
18) Avery laser printer clear sticker sheets (15664)
19) 1.5 " diameter speaker (Sparkfun)
20) Various wires, cables, screws, grommets, caps and almost no tape or glue!
21) Arduino development environment (FREE!)

Step 3: The Gauges

The first thing I did was to get the Arduino to move the gauges. This is really straight forward once you figure out how many amps (milliamps) are required to move the needle to full scale.The gauges that I used had an internal shunt resistor that I found when I opened one of them up. I bypased it (shorted across it) and decided to use an external trimpot instead. In the case of my gauges, it took about about 1 milliamp to move the needle full scale (this is pretty typical), so if I apply 5V (the max from the arduino) through a 5kOhm resistor, I should get 1 mA. ( V=IR, I=V/R, I=5/5000 or 1/1000 A). I used 10k trimpots so that I could set them about mid travel (5 kOhm) and tweak them until the meter read exactly full scale at my max PWM (pulse width modulated) value.

The arduino library has a handy function called analogWrite that outputs a PWM square wave on any number of pins depending on which arduino you are using. The PWM is an 8 bit value so you have theoretically 256 values (0-255) to use. Since I was making a clock, I chose a convenient value of 240 for my PWM scale because it is evenly divisible by 60. So for my minutes and seconds gauges I have 60 divisions and a total of 240 PWM values, or 4 PWM values per minute or second. If I set the PWM output to 20 I would get 5 minutes or 5 seconds. Since there are 24 hours in the day, every 10 PWM counts is an hour.

I ran into a little snag that I still don't fully understand (told you I'm a hack!). For whatever reason when I connected all three gauges to PWMs on the arduino, the output voltage of the PWM dropped substantially. I just ended up using a small field effect transistor to drive each gauge. This worked very well.

ST Micro 2N7000  Datasheet:http://www.datasheetcatalog.org/datasheet2/c/0h0r7l0sywjlwayppi1u7lwyr33y.pdf

Connect the PWM line from the Arduino to the Gate of the MOSFET. Connect the negative terminal of the gauge to the Drain  of the MOSFET. Connect the Source of the MOSFET to the ground of the Arduino. Connect the positive terminal of the gauge to the +5V supply of the Arduino. (See the schematic below)

Here's a super simple Arduino program that will output PWM:


//PWM code snippet

#define pwmpin 3
#define pwmval 128  //change this value to change the PWM duty cycle


void setup(){

}

void loop(){

analogWrite(pwmpin, pwmval);

}

Step 4: Make It Tick

Now that the gauge is moving, let's make it tick in the very simplest of ways. The following piece of code just delays for a second and then calls a function that updates the gauge. The gauge update routine just increments a pwm value by 4 counts and then outputs it to the gauge. This repeats until the pwm value exceeds 240 at which point it is set to zero to return the gauge needle and the process repeats........

You'll see some extra wiring on the proto board. Ignore it for now. The only wiring that matters is the wiring shown in the previous step's schematic.


//Simplest tick

#define pwmpin 5

byte pwmval;


void setup()
{
 pwmval = 0;  //initialize
 Serial.begin(9600);      //enable serial output

}

void loop(){

  delay (1000);   //pause for 1000 msec (1 second)
  updateGauge();  //call the updategauge function
}


void updateGauge(){
   pwmval += 4;      //increment the pwm value by 4 counts
  if (pwmval > 240){     //set the pwmvalue back to zero when it exceeds 240
    pwmval = 0;
  }
  analogWrite(pwmpin, pwmval);    //output the pwmvalue to pin 5
}

Step 5: The Real Time Clock Chip

Now that the gauges are working you need some code to make a clock. I originally wrote a routine to keep time based on the internal oscillator of the arduino but was not satisfied with the accuracy. I researched real time clock chips and found the DS 3231. It is really an easy to use and amazingly accurate little chip. It also is battery backed up and keeps the month date day of the week and year. This was important to me because I wanted to add a routine to calculate and display the moonphase based on the current time and date. Another important feature is that it outputs a 1Hz square wave that can be used to trigger the updating of the gauges. Just the ticket! And all over I2C!
I found a really convenient little board called the chronodot that contains the DS3231 chip, battery slot, battery and circuitry in a dip format board. Very convenient. You don't have to use this, as a matter of fact, the first one I set up I used the DS3231 chip soldered to an SOIC to DIP adapter board and a separate battery holder. I like the chronodot much better because it is all integrated and ended up being much smaller too.

Here's a link to the chronodot manufacturer:

http://macetech.com/store/index.php?main_page=product_info&products_id=8

Step 6: Talk to the Real Time Clock (RTC)

What you want to do here is to connect the chronodot to a suitable power source and to the Arduino, upload a piece of code to the arduino and get it talking to the DS3231. There's a schematic below that should get you hooked up right away. You don't have to connect the square wave pin now for this code to work. We'll use it later to get the gauges ticking.
The circuit and the code below worked right away for me.

Here's the link to the DS3231 datasheet:
http://datasheets.maxim-ic.com/en/ds/DS3231.pdf


I found this little program (sketch in Arduinish) on thew web and have attached it for you. It reads the info from the RTC and spits it out to the serial monitor. (Much thanks to the author; gfbarros). Here's the original link:
http://code.google.com/p/gfb/source/browse/arduino/DS3231/DS3231.pde

If all is well, you should see output in the serial monitor like that in the photo below.

//DS3231 Code Snippet

#include <Wire.h>

#define DS3231_I2C_ADDRESS 104

byte seconds, minutes, hours, day, date, month, year;
char weekDay[4];

byte tMSB, tLSB;
float temp3231;

void setup()
{
  Wire.begin();
  Serial.begin(9600);
  //set control register to output square wave on pin 3 at 1Hz
  Wire.beginTransmission(DS3231_I2C_ADDRESS); // 104 is DS3231 device address
  Wire.send(0x0E); //
  Wire.send(B00000000);
  Wire.endTransmission();
}

void loop()
{

  watchConsole();


  get3231Date();

  Serial.print(weekDay); Serial.print(", "); Serial.print(month, DEC); Serial.print("/"); Serial.print(date, DEC); Serial.print("/"); Serial.print(year, DEC); Serial.print(" - ");
  Serial.print(hours, DEC); Serial.print(":"); Serial.print(minutes, DEC); Serial.print(":"); Serial.print(seconds, DEC);

  Serial.print("   Temperature: "); Serial.println(get3231Temp());

  delay(1000);
}

// Convert normal decimal numbers to binary coded decimal
byte decToBcd(byte val)
{
  return ( (val/10*16) + (val%10) );
}

void watchConsole()
{
  if (Serial.available()) {      // Look for char in serial queue and process if found
    if (Serial.read() == 84) {      //If command = "T" Set Date
      set3231Date();
      get3231Date();
      Serial.println(" ");
    }
  }
}

void set3231Date()
{
//T(sec)(min)(hour)(dayOfWeek)(dayOfMonth)(month)(year)
//T(00-59)(00-59)(00-23)(1-7)(01-31)(01-12)(00-99)
//Example: 02-Feb-09 @ 19:57:11 for the 3rd day of the week -> T1157193020209

  seconds = (byte) ((Serial.read() - 48) * 10 + (Serial.read() - 48)); // Use of (byte) type casting and ascii math to achieve result. 
  minutes = (byte) ((Serial.read() - 48) *10 +  (Serial.read() - 48));
  hours   = (byte) ((Serial.read() - 48) *10 +  (Serial.read() - 48));
  day     = (byte) (Serial.read() - 48);
  date    = (byte) ((Serial.read() - 48) *10 +  (Serial.read() - 48));
  month   = (byte) ((Serial.read() - 48) *10 +  (Serial.read() - 48));
  year    = (byte) ((Serial.read() - 48) *10 +  (Serial.read() - 48));
  Wire.beginTransmission(DS3231_I2C_ADDRESS);
  Wire.send(0x00);
  Wire.send(decToBcd(seconds));
  Wire.send(decToBcd(minutes));
  Wire.send(decToBcd(hours));
  Wire.send(decToBcd(day));
  Wire.send(decToBcd(date));
  Wire.send(decToBcd(month));
  Wire.send(decToBcd(year));
  Wire.endTransmission();
}


void get3231Date()
{
  // send request to receive data starting at register 0
  Wire.beginTransmission(DS3231_I2C_ADDRESS); // 104 is DS3231 device address
  Wire.send(0x00); // start at register 0
  Wire.endTransmission();
  Wire.requestFrom(DS3231_I2C_ADDRESS, 7); // request seven bytes

  if(Wire.available()) {
    seconds = Wire.receive(); // get seconds
    minutes = Wire.receive(); // get minutes
    hours   = Wire.receive();   // get hours
    day     = Wire.receive();
    date    = Wire.receive();
    month   = Wire.receive(); //temp month
    year    = Wire.receive();

    seconds = (((seconds & B11110000)>>4)*10 + (seconds & B00001111)); // convert BCD to decimal
    minutes = (((minutes & B11110000)>>4)*10 + (minutes & B00001111)); // convert BCD to decimal
    hours   = (((hours & B00110000)>>4)*10 + (hours & B00001111)); // convert BCD to decimal (assume 24 hour mode)
    day     = (day & B00000111); // 1-7
    date    = (((date & B00110000)>>4)*10 + (date & B00001111)); // 1-31
    month   = (((month & B00010000)>>4)*10 + (month & B00001111)); //msb7 is century overflow
    year    = (((year & B11110000)>>4)*10 + (year & B00001111));
  }
  else {
    //oh noes, no data!
  }

  switch (day) {
    case 1:
      strcpy(weekDay, "Sun");
      break;
    case 2:
      strcpy(weekDay, "Mon");
      break;
    case 3:
      strcpy(weekDay, "Tue");
      break;
    case 4:
      strcpy(weekDay, "Wed");
      break;
    case 5:
      strcpy(weekDay, "Thu");
      break;
    case 6:
      strcpy(weekDay, "Fri");
      break;
    case 7:
      strcpy(weekDay, "Sat");
      break;
  }
}

float get3231Temp()
{
  //temp registers (11h-12h) get updated automatically every 64s
  Wire.beginTransmission(DS3231_I2C_ADDRESS);
  Wire.send(0x11);
  Wire.endTransmission();
  Wire.requestFrom(DS3231_I2C_ADDRESS, 2);

  if(Wire.available()) {
    tMSB = Wire.receive(); //2's complement int portion
    tLSB = Wire.receive(); //fraction portion

    temp3231 = (tMSB & B01111111); //do 2's math on Tmsb
    temp3231 += ( (tLSB >> 6) * 0.25 ); //only care about bits 7 & 8
  }
  else {
    //oh noes, no data!
  }

  return temp3231;
}


Step 7: Another Step Closer to Putting It All Together

Now that we can read the time from the RTC and we can move the gauges with PWM, we need to convert the reading from the clock chip into pwm values so that the gauges read hours minutes and seconds. I decided that I would read the RTC and update the gauges every second. I used the 1Hz square wave output from the RTC and connected to it to an input pin on the arduino that allows the use of an interrupt. The idea is that whenever the 1Hz square wave increases from 0V to 5V (every second), the arduino is interrupted and a  function is called. The function will read the RTC and update the gauges; then the arduino will continue on with what it was doing just before it was interrupted. The arduino library has a command called attachInterrupt that works with a few specific pins on the arduino mega. Here's a link to the description of the attachInterrupt command:

http://www.arduino.cc/en/Reference/AttachInterrupt


Before we get into reading the RTC and parsing the data, let's just make the gauge tick again like in Step 4, but this time using the square wave from the RTC and an interrupt. You'll notice in the schematic that I show a pull up resistor on pin 2. This is required by the RTC to output the 1 Hz square wave (you read the datasheet right? :^)  but in the circuit in in the photo it is missing. This is because the input pins on the arduino have built in pull-ups, but you have to enable them. By writing a HIGH value to pin 2, the internal pull up is enabled for that pin and you don't have to use an external. Read the comments in the code to see the command that does this.


//Tick routine using an interrupt triggered by the square wave from the DS3231 RTC

#define pwmpin 5

byte pwmval;


void setup()
{
 pwmval = 0;  //initialize
 Serial.begin(9600);      //enable serial output
  digitalWrite(2,HIGH);   //enable internal pull up on pin 2. This obviates the need for the external pull up resistor shown in the schematic
  attachInterrupt(0,interrupted, RISING);  //attach interrupt 0 to the function 'interrupted' when pin two sees a rising voltage
}

void loop(){

  ; //add stuff here if you want

}

void interrupted(){    //this is the function called when pin 2 sees a rising voltage
  Serial.println("Interrupt detected");   //print to serial terminal
  pwmval += 4;      //increment the pwm value by 4 counts
  if (pwmval > 240){     //set the pwmvalue back to zero when it exceeds 240
    pwmval = 0;
  }
  updateGauge();   //call the update gauge function
}

void updateGauge(){
  analogWrite(pwmpin, pwmval);    //output the pwmvalue to pin 5
}


Step 8: Using the RTC to Drive the Gauges

Now we need to read the data from the RTC and convert it to numbers that can be sent to the gauges to display the time (and date!). This is actually really easy because of the great routine for the DS3231 from Step 6. The routine already converts the BCD data from the 3231 to decimal so all we need to do is multiply the hours by 10, and the minutes and seconds by 4 (in the case of my gauges, yours may vary) and send the new pwm values to the gauges.
We also need some code to advance the minutes and hours and to return the gauges to zero when they max out. This is really straight forward, but I ran into a little problem. When the needles return to zero from their full travel, they do so pretty forcefully. Enough to make a clicking noise and cause concern about the longevity of the d'Arsonval movements in the gauges. I wanted to implement a soft return routine in the software which at first blush seemed pretty easy to do.  I did eventually get it to work and I will show you how. Some of you electronics gurus might be able to figure out a hardware solution for this (cap charges and drains through a diode resistor arrangement when the PWM goes to zero or something). If so, please let me know. I took the software path, and haven't spent any more time thinking about it.

A little video of the code below running. The O'scope trace is the square wave from the RTC. Notice every time it rises, the seconds hand increments.


Here is a program that will read the RTC based on an interrupt generated by the RTC square wave attached to pin 2 of the arduino and output the seconds value on pin 5 (PWM).
This routine does not include the soft return for the needle.



#include <Wire.h>

#define DS3231_I2C_ADDRESS 104
#define int_pin 2
#define gauge_pin 5

byte seconds, minutes, hours, day, date, month, year;
byte secpos;
char weekDay[4];
boolean int_tick;
byte tMSB, tLSB;
float temp3231;

void setup()
{
  Wire.begin();
  Serial.begin(9600);
  Wire.beginTransmission(DS3231_I2C_ADDRESS); // 104 is DS3231 device address
  Wire.send(0x0E); //
  Wire.send(B00000000);
  Wire.endTransmission();
  pinMode(int_pin, INPUT);
  digitalWrite(2,HIGH); //turn on internal pull up of pin 2
  attachInterrupt(0, int0handler ,RISING); //attach interrupt zero to pin 2 and call the function int0handler whenever pin 2 sees a rising voltage
  secpos = 0;
}

void loop()
{

  if (int_tick){
    updategauge();
  }

  watchConsole();  //used to change the time and date




}

void int0handler(){
  int_tick = 1;
}

void updategauge(){
  Serial.println("INT");
  get3231Date();
  Serial.print(weekDay); Serial.print(", "); Serial.print(month, DEC); Serial.print("/"); Serial.print(date, DEC); Serial.print("/"); Serial.print(year, DEC); Serial.print(" - ");
  Serial.print(hours, DEC); Serial.print(":"); Serial.print(minutes, DEC); Serial.print(":"); Serial.print(seconds, DEC);

  Serial.print("   Temperature: "); Serial.println(get3231Temp());
  secpos = seconds * 4;
  if (secpos >= 240) secpos = 0; //this will return the second needle at end of travel
  analogWrite(gauge_pin, secpos);
  int_tick = 0;  //reset the int tick flag
}

// Convert normal decimal numbers to binary coded decimal
byte decToBcd(byte val)
{
  return ( (val/10*16) + (val%10) );
}

void watchConsole()
{
  if (Serial.available()) {      // Look for char in serial queue and process if found
    if (Serial.read() == 84) {      //If command = "T" Set Date
      set3231Date();
      get3231Date();
      Serial.println(" ");
    }
  }
}

void set3231Date()
{
//T(sec)(min)(hour)(dayOfWeek)(dayOfMonth)(month)(year)
//T(00-59)(00-59)(00-23)(1-7)(01-31)(01-12)(00-99)
//Example: 02-Feb-09 @ 19:57:11 for the 3rd day of the week -> T1157193020209

  seconds = (byte) ((Serial.read() - 48) * 10 + (Serial.read() - 48)); // Use of (byte) type casting and ascii math to achieve result. 
  minutes = (byte) ((Serial.read() - 48) *10 +  (Serial.read() - 48));
  hours   = (byte) ((Serial.read() - 48) *10 +  (Serial.read() - 48));
  day     = (byte) (Serial.read() - 48);
  date    = (byte) ((Serial.read() - 48) *10 +  (Serial.read() - 48));
  month   = (byte) ((Serial.read() - 48) *10 +  (Serial.read() - 48));
  year    = (byte) ((Serial.read() - 48) *10 +  (Serial.read() - 48));
  Wire.beginTransmission(DS3231_I2C_ADDRESS);
  Wire.send(0x00);
  Wire.send(decToBcd(seconds));
  Wire.send(decToBcd(minutes));
  Wire.send(decToBcd(hours));
  Wire.send(decToBcd(day));
  Wire.send(decToBcd(date));
  Wire.send(decToBcd(month));
  Wire.send(decToBcd(year));
  Wire.endTransmission();
}


void get3231Date()
{
  // send request to receive data starting at register 0
  Wire.beginTransmission(DS3231_I2C_ADDRESS); // 104 is DS3231 device address
  Wire.send(0x00); // start at register 0
  Wire.endTransmission();
  Wire.requestFrom(DS3231_I2C_ADDRESS, 7); // request seven bytes

  if(Wire.available()) {
    seconds = Wire.receive(); // get seconds
    minutes = Wire.receive(); // get minutes
    hours   = Wire.receive();   // get hours
    day     = Wire.receive();
    date    = Wire.receive();
    month   = Wire.receive(); //temp month
    year    = Wire.receive();

    seconds = (((seconds & B11110000)>>4)*10 + (seconds & B00001111)); // convert BCD to decimal
    minutes = (((minutes & B11110000)>>4)*10 + (minutes & B00001111)); // convert BCD to decimal
    hours   = (((hours & B00110000)>>4)*10 + (hours & B00001111)); // convert BCD to decimal (assume 24 hour mode)
    day     = (day & B00000111); // 1-7
    date    = (((date & B00110000)>>4)*10 + (date & B00001111)); // 1-31
    month   = (((month & B00010000)>>4)*10 + (month & B00001111)); //msb7 is century overflow
    year    = (((year & B11110000)>>4)*10 + (year & B00001111));
  }
  else {
    //oh noes, no data!
  }

  switch (day) {
    case 1:
      strcpy(weekDay, "Sun");
      break;
    case 2:
      strcpy(weekDay, "Mon");
      break;
    case 3:
      strcpy(weekDay, "Tue");
      break;
    case 4:
      strcpy(weekDay, "Wed");
      break;
    case 5:
      strcpy(weekDay, "Thu");
      break;
    case 6:
      strcpy(weekDay, "Fri");
      break;
    case 7:
      strcpy(weekDay, "Sat");
      break;
  }
}

float get3231Temp()
{
  //temp registers (11h-12h) get updated automatically every 64s
  Wire.beginTransmission(DS3231_I2C_ADDRESS);
  Wire.send(0x11);
  Wire.endTransmission();
  Wire.requestFrom(DS3231_I2C_ADDRESS, 2);

  if(Wire.available()) {
    tMSB = Wire.receive(); //2's complement int portion
    tLSB = Wire.receive(); //fraction portion

    temp3231 = (tMSB & B01111111); //do 2's math on Tmsb
    temp3231 += ( (tLSB >> 6) * 0.25 ); //only care about bits 7 & 8
  }
  else {
    //oh noes, no data!
  }

  return temp3231;
}

Step 9: The Rotary Encoders

The rotary encoders are used primarily to set the time, but are useful for other things as well. They have a momentary normally open pushbutton built into them too. With three of them, we can read combinations of button presses, rotary motion with and without the button pressed etc. Very cool little encoders. Get them at Sparkfun. There is great example code on the sparkfun site as well:

http://www.circuitsathome.com/mcu/reading-rotary-encoder-on-arduino

This is also good:

http://www.sparkfun.com/datasheets/Components/RotaryEncoder.pde

Step 10: Using the Rotary Encoders to Set the Hours


Step 11: Cad Models

I modeled the clock using SolidWorks to be sure all the machined components fit properly.

Clocks Challenge

First Prize in the
Clocks Challenge