Introduction: Kinetic & Digital Clock (Arduino + 3D Print)

About: Hello, I'm a Software Engineer!

This is a seven segment clock where instead of LEDs we have segments moving on the Z-axis of the viewing pane, the difference in depth by these segments will allow the viewer to see the time against the white on white segments. We use an Arduino microcontroller to first process the read the current time and then align the segments out accordingly through a motor controller.

This piece is a continuation of work I did previously out of paper. I bought a 3D printer to make this clock into a reality, as the precision and mechanical nature of this piece needed strong and reliable parts to create a clean finish and movement.

Supplies

  1. Arduino Mega - The brain
  2. Arduino Mega Sensor Shield - The I/O
  3. DS3231 or RTC clock breakout - To keep track of time
  4. 30 Servos - To move each segment
  5. 3D Printer & Filament

Step 1: Print Your Parts

I made this design with sliding and snap fit parts. As this is a large mechanical piece, designing smaller parts made it so that I could iterate quickly on individual components. Each digit is constructed out of 7 segments that are slotted into a panel. The body of the clock holds 4 of these digit panels. The back panel slides into the face plate, where the segments move on along the gear to create linear motion.

The STL's are provided for you to slice and print out with your printer of choice. The orientation of print for each piece was critical to this project. Ensure you orient the STLs in slicer in a way that suits your 3D printer when slicing. In my case I took care to ensure the faces of the clock are pieces that were printed directly on the Steel sheet, as this creates the cleanish finish on the Prusa. Finish is important to the aesthetic of this piece.

  1. back x 4
  2. back-2 x 1
  3. face-1 x 1
  4. face-2 x 2
  5. gear x 30
  6. segment-1 x 16
  7. segment-2 x 12
  8. segment-3 x 2

Step 2: Attach Servo to Bracket

Take a screwdriver and attach the servo to bracket. The screws should come bundled with your servos. The placement of the servo is from the bottom of the mount as pictured. You will need to repeat this step for all 30 of servos.

When you're done attaching it to the bracket, snip the servo head, and attach it to the gear. You may optionally want to glue this to keep things in place.

Step 3: Attach Servo Brackets to the Back Panel

Each panel is a a seven segment display, attach 7 servo & brackets to each panel. They are fitted with snap fit brackets. If you're doing some debugging and need to remove the bracket, take a pair of pliers to clamp the snap fit mechanism to release the bracket.

Step 4: Add Panel to Main Body

Slide the segment panels into the back of the body of the clock. There are four digit panels and one panel for the colon. The body includes alignment flags, that will hold the pieces in alignment for ease of assembly.

Step 5: Connect Servos to Microcontroller

Attach servos to microcontroller. Each segment of a digit is ordered from A to G. We will map these as 1 to 7 for connecting to our microcontroller. Simply attach the servos in order, starting from the following pins.

  1. The first digit will be at pin 2.
  2. The second digit starts at pin 9.
  3. The third digit starts at pin 22.
  4. The fourth digit starts at pin 29.

You can refer to the source-code section for the final pin-out.

Step 6: Insert Segments Into the Display

These hobby servos actually only have 180 degrees of motion. In order to calibrate and align the mechanisms, prep all the servos by moving them to the end of their motion (180 degree).

Slide the segment from the front of the display into each individual socket, you will feel the servos accept the segment there should be some light resistance and smooth actuation of the part as inserting.

Step 7: Programming

Our code is run in a loop, it checks the current time and moves the destination of each segment to the desired location. Using the loop as a ticker it makes small increments on the angle of the servo creating a smooth push and pull motion on the segment.

Since the servos used in this project are cheap and not the most accurate we use the segment intervals map, to store the internal offset of each servo. This is how we ensure that when we push or pull the servos they are all aligned. This is critical to the aesthetic of the display.

String input;#include <Servo.h>#include <DS3231.h>DS3231  rtc(SDA, SCL);const int DIGIT_TO_SEGMENT_MAPPING[10][7] = {  { 1, 1, 1, 1, 1, 1, 0 }, // 0  { 0, 1, 1, 0, 0, 0, 0 }, // 1  { 1, 1, 0, 1, 1, 0, 1 }, // 2  { 1, 1, 1, 1, 0, 0, 1 }, // 3  { 0, 1, 1, 0, 0, 1, 1 }, // 4  { 1, 0, 1, 1, 0, 1, 1 }, // 5  { 1, 0, 1, 1, 1, 1, 1 }, // 6  { 1, 1, 1, 0, 0, 0, 0 }, // 7  { 1, 1, 1, 1, 1, 1, 1 }, // 8  { 1, 1, 1, 1, 0, 1, 1 }  // 9};const int SEGMENT_INTERVALS[4][7][2] = {  {    {141, 54},    {155, 69},    {150, 73},    {151, 70},    {159, 75},    {159, 75},    {125, 40}  },  {    {164, 76},    {155, 76},    {138, 61},    {180, 87},    {151, 63},    {145, 57},    {165, 78}  },  {    {157, 73},    {156, 70},    {165, 85},    {137, 52},    {133, 50},    {133, 50},    {168, 73}  },  {    {131, 52},    {147, 61},    {131, 51},    {158, 69},    {155, 73},    {116, 28},    {137, 60}  }};const int COLON_INTERVAL[2][2] = {  {141, 62},  {137, 30},};const int DIGIT_STARTING_SEGMENT_INDEX[4] = {2, 9, 22, 29};const int COLON_STARTING_INDEX = 16;const int START_POS = 0;const int COLON = 2;const int DIGITS = 4;const int SEGMENTS_PER_DIGIT = 7;const int STEP_MS = 20;const int COUNT_MS = 2000;const int NUM_SERVOS = DIGITS * SEGMENTS_PER_DIGIT;int servoTargetDestination[DIGITS][NUM_SERVOS];int servoTargetDestinationColon[COLON];int count = 1200;int timeMS = 0;Servo servos[DIGITS][SEGMENTS_PER_DIGIT];Servo colonServos[COLON];void setup() {  rtc.begin();  for (int i = 0; i < DIGITS; i++) {    for (int j = 0; j < SEGMENTS_PER_DIGIT; j++) {      int offset = DIGIT_STARTING_SEGMENT_INDEX[i];      servoTargetDestination[i][j] = SEGMENT_INTERVALS[i][j][START_POS];      servos[i][j].attach(j + offset);      servos[i][j].write(servoTargetDestination[i][j]);    }  }  delay(500);  for (int i = 0; i < DIGITS; i++) {    for (int j = 0; j < SEGMENTS_PER_DIGIT; j++) {      servos[i][j].detach();    }  }  for (int i = 0; i < COLON; i++) {    colonServos[i].attach(i + COLON_STARTING_INDEX);    colonServos[i].write(COLON_INTERVAL[i][START_POS]);  }  delay(500);  for (int i = 0; i < COLON; i++) {    colonServos[i].detach();  }  for (int i = 0; i < COLON; i++) {    colonServos[i].attach(i + COLON_STARTING_INDEX);    colonServos[i].write(COLON_INTERVAL[i][1]);  }  delay(500);  for (int i = 0; i < COLON; i++) {    colonServos[i].detach();  }}void loop() {    // Retrieve and cleanup RTC string. "12:45" -> "1245"    String timeStr = rtc.getTimeStr();    String timeString = timeStr.substring(0, 2) + timeStr.substring(3, 5);    for (int activeDigit = 0; activeDigit < 4; activeDigit++) {            // Step 1: Set servoTargetDestination      String stringCount = String(count); // Swap to timeString to use the clock.      for (int i = 0; i < SEGMENTS_PER_DIGIT; i++)      {        int displayNumber = stringCount.charAt(timeString.length() - 1 - activeDigit) - '0';        int placement = (activeDigit == 3 && displayNumber == 0) ? 0 : DIGIT_TO_SEGMENT_MAPPING[displayNumber][i];        servoTargetDestination[activeDigit][i] =  SEGMENT_INTERVALS[activeDigit][i][placement];      }      // Step 2: Increment Segments      for (int i = 0; i < SEGMENTS_PER_DIGIT; i++)      {        Servo servo = servos[activeDigit][i];        int pos = servo.read();        int dest = servoTargetDestination[activeDigit][i];        if (pos != dest) {          if (pos < dest) {            pos++;          } else {            pos--;          }          if (!servo.attached()) {            int offset = DIGIT_STARTING_SEGMENT_INDEX[activeDigit];            servo.attach(i + offset);          }          servo.write(pos);        }      }    }      // Step 3: Wait    delay(STEP_MS);    timeMS = timeMS + STEP_MS;      // Step 4A: Countdown    if (timeMS >= COUNT_MS) {      timeMS = 0;      count = count + 1;    }      // Step 5: Detach anything that is at its destination    for (int i = 0; i < DIGITS; i++) {      for (int j = 0; j < NUM_SERVOS; j++) {        Servo servo = servos[i][j];        int pos = servo.read();        int destination = servoTargetDestination[i][j];        if (pos == destination && servo.attached()) {          servos[i][j].detach();        }      }    }}<br>

Step 8: Calibration & Setup

The previous step will actually require additional calibration because the servos that I have will have completely different parameters as yours. The following code is what I used to calibrate and find the intervals needed for each servo

/*  Controlling a servo position using a potentiometer (variable resistor)  by Michal Rinott < http://www.arduino.cc/en/Tutorial/Knob>  modified on 8 Nov 2013  by Scott Fitzgerald   http://www.arduino.cc/en/Tutorial/Knob*/#include <Servo.h>Servo myservo;  // create servo object to control a servoint potpin = 0;  // analog pin used to connect the potentiometerint val;    // variable to read the value from the analog pinconst int SEGMENT_INTERVALS[4][7][2] = {  {    {146, 57},    {147, 62},    {138, 51},    {160, 88},    {135, 64},    {149, 70},    {139, 58}  },  {    {131, 45},    {150, 53},    {138, 52},    {146, 61},    {151, 70},    {140, 57},    {137, 48}  },  {    {157, 73},    {156, 70},    {135, 50},    {137, 52},    {133, 50},    {133, 50},    {168, 73}  },  {    {131, 52},    {147, 61},    {131, 51},    {128, 43},    {125, 41},    {104, 24},    {137, 60}  }};const int COLON_INTERVAL[2][2] = {  {127, 45},  {156, 81},};const int DIGIT_STARTING_SEGMENT_INDEX[4] = {2, 9, 22, 29};const int COLON_STARTING_INDEX = 16;const int SEGMENTS_PER_DIGIT = 7;const int DIGITS = 4;const int START_POS = 1;const int COLON = 2;int segment = 12;void setup() {  Servo servos[DIGITS][SEGMENTS_PER_DIGIT];  Servo colonServos[COLON];  for (int i = 0; i < DIGITS; i++) {    for (int j = 0; j < SEGMENTS_PER_DIGIT; j++) {      int offset = DIGIT_STARTING_SEGMENT_INDEX[i];      servos[i][j].attach(j + offset);      servos[i][j].write(SEGMENT_INTERVALS[i][j][START_POS]);    }      delay(1000);    for (int j = 0; j < SEGMENTS_PER_DIGIT; j++) {      servos[i][j].detach();    }  }  for (int i = 0; i < COLON; i++) {    colonServos[i].attach(i + COLON_STARTING_INDEX);    colonServos[i].write(COLON_INTERVAL[i][START_POS]);  }  delay(1000);  for (int i = 0; i < COLON; i++) {    colonServos[i].detach();  }  myservo.attach(segment);  // attaches the servo on pin 9 to the servo object  Serial.begin(9600); // open the serial port at 9600 bps:}int inputParse(String input) {  // Clean number  int angle = input.toInt();  if (angle > 180 || angle < 0) {    angle = 0;  }  return angle;}void loop() {  if (Serial.available()) {    segment = inputParse(Serial.readString());    Serial.print("Attaching: " );    Serial.println(segment);    myservo.detach();    myservo.attach(segment);  }  if (myservo.attached()) {    val = analogRead(potpin);            // reads the value of the potentiometer (value between 0 and 1023)    val = map(val, 0, 1023, 0, 180);     // scale it to use it with the servo (value between 0 and 180)    myservo.write(val);                  // sets the servo position according to the scaled value    Serial.print("Seg: " );    Serial.print(segment);    Serial.print(" Pot: " );    Serial.println(val);    delay(15);                           // waits for the servo to get there  }}

In addition, we also need to set the time of our RTC! Here is the following code used to do that.

// DS3231_Serial_Easy// Copyright (C)2015 Rinky-Dink Electronics, Henning Karlsen. All right reserved// web: http://www.RinkyDinkElectronics.com///// A quick demo of how to use my DS3231-library to // quickly send time and date information over a serial link//// To use the hardware I2C (TWI) interface of the Arduino you must connect// the pins as follows://// Arduino Uno/2009:// ----------------------// DS3231:  SDA pin   -> Arduino Analog 4 or the dedicated SDA pin//          SCL pin   -> Arduino Analog 5 or the dedicated SCL pin//// Arduino Leonardo:// ----------------------// DS3231:  SDA pin   -> Arduino Digital 2 or the dedicated SDA pin//          SCL pin   -> Arduino Digital 3 or the dedicated SCL pin//// Arduino Mega:// ----------------------// DS3231:  SDA pin   -> Arduino Digital 20 (SDA) or the dedicated SDA pin//          SCL pin   -> Arduino Digital 21 (SCL) or the dedicated SCL pin//// Arduino Due:// ----------------------// DS3231:  SDA pin   -> Arduino Digital 20 (SDA) or the dedicated SDA1 (Digital 70) pin//          SCL pin   -> Arduino Digital 21 (SCL) or the dedicated SCL1 (Digital 71) pin//// The internal pull-up resistors will be activated when using the // hardware I2C interfaces.//// You can connect the DS3231 to any available pin but if you use any// other than what is described above the library will fall back to// a software-based, TWI-like protocol which will require exclusive access // to the pins used, and you will also have to use appropriate, external// pull-up resistors on the data and clock signals.//#include <DS3231.h>// Init the DS3231 using the hardware interfaceDS3231  rtc(SDA, SCL);void setup(){  // Setup Serial connection  Serial.begin(115200);  // Uncomment the next line if you are using an Arduino Leonardo  //while (!Serial) {}    // Initialize the rtc object  rtc.begin();    // The following lines can be uncommented to set the date and time  //  rtc.setDOW(FRIDAY);     // Set Day-of-Week to SUNDAY  rtc.setTime(23, 05, 0);     // Set the time to 12:00:00 (24hr format)  rtc.setDate(7, 19, 2021);   // Set the date to January 1st, 2014}void loop(){  // Send time  Serial.println(rtc.getTimeStr());    // Wait one second before repeating :)  delay (1000);}
Arduino Contest

Second Prize in the
Arduino Contest