Maker.io main logo

Custom 24-Hour Single Revolution Clock

1,298

2025-02-03 | By PTSolns

License: Attribution 3D Printing Drivers / Controllers EEPROM Microcontrollers Real Time Clocks (RTCs) Stepper Arduino

Have you ever wondered why the hour arm on a standard clock rotates twice per day? Why is that ... Why not make it so that the hour arm rotates once per day?... it is these questions that keep us up at night.

Well, no more! We've designed a clock whose hour arm rotates once per day. Although this is not a new concept, the approach we took somewhat is. Along with the development of this project, we encountered some interesting issues for which we found unique solutions. One of these issues was solved via software, and the other via hardware. These will be explained as they come up.

This project is presented as follows. First, all the main components are outlined, what they are, and what they do. Then an electrical schematic brings it all together. We will show the code we used (and how we got it!) and explain all the required details. We will not go into details regarding the 3D-printed case and hour arm, nor how to assemble the clock itself (insert brass nuts, soldering, etc.). This part is fairly straightforward, but a curious reader can always contact us for more information.

All files relating to this project, including the 3D printing files, are made freely available on our Documentation Subdomain docs.PTSolns.com.

Clock Main Picture

Components Explained

Nano Flip

The Nano Flip is the microcontroller development board of choice for this project. We like this dev board as it has a small footprint, has a ton of online community support and tutorials, and is more than capable running this simple project. Of particular interest, as we will find out, is the EEPROM available on the Nano Flip (err, we mean the ATmega328P IC). There are 1024 bytes of EEPROM, of which we only need a tiny amount to store an integer value ... more on this later.

Nano Flip

Real-Time Clock (RTC)

For simplicity, we've decided to use an off-the-shelf RTC module with the DS3231 IC. This module keeps fairly precise time over the course of the year, has a battery backup (CR2032), and communicates with the microcontroller via the I2C protocol. This is a generic module and is easily available on many platforms. The user is referred to the image below as a reference.

RTC DS3231 Module

Stepper Motor & Driver

We chose to use the 28BYJ-48 stepper motor with the ULN2003 driver. This stepper runs on 5V and has 4096 steps per revolution. This is pretty much what we need as the Nano Flip runs on 5V logic so we can power both with a common 5V wall adapter. Also, 4096 steps are plenty to accurately move the hour arm within the context of hours and a full day. Note that there is no minute arm, so for visually reading the clock, this level of precision is more than enough. This motor and driver are generic and also easily available on many platforms. The user is referred to the image below as a reference.

Stepper Motor and Driver

Prototyping Board

To bring the entire project together we used the Proto-Half prototyping board. This board worked quite well for the following reasons:

  • It has a female barrel jack connector footprint so the clock can easily be powered by a 5V wall adapter.

  • Configurable power and central rails

  • Fits nicely inside the main clock body

  • Plenty of prototyping space to add the connectors and some other components.

Proto-Half

Miscellaneous Parts

To put it all together we needed some other small components. Here is a list:

Component Modifications

After we assembled the first prototype of the clock we noticed that we didn't like the various LEDs on the modules to shine through the white PLA of the clock cover. This didn't look good, so we decided to remove all internal LEDs as follows.

  • On the Nano Flip, simply cut the PWR LED trace on the back of the board. This can be undone at any time by simply soldering the bridge closed.

  • On the RTC module remove the red power LED by unsoldering.

  • On the driver module remove the four red indicator LEDs by unsoldering.

Two Unforeseen Challenges

As we were assembling the first prototype, we came across two unforeseen challenges. The first one was an oversight on our part, and it stems from the fact that the stepper motor does not have any way to measure the rotation absolutely. Meaning that there is no absolute zero position and upon every power-up, the relative zero position would be different. We should have looked at the motor specs more closely before choosing this part. But in the end, the solution is quite neat so we're happy with our choice.

The second unforeseen challenge came from the driver's behavior upon power-up. When the entire board is powered, an inrush current drives the stepper motor a handful of positions. This happens before the microcontroller has even had a chance to control the driver and prevent it from doing this start-up movement. Both of these issues are explained below.

Challenge #1 - No Absolute Zero on Stepper, Solved Via Software

Every time you turn ON the stepper motor, whatever position the shaft is in is considered the zero position. This wouldn't be a problem if this position were to remain constant. However, if the motor rotates a bit, then powers down and back up, this new position would now be the zero position. We need an absolute zero that is kept constant even upon power cycles.

To do this we use a clever trick ... well, we think it is clever. We wrote a calibration sketch (more about that below in the coding section) that is first loaded. This sketch moves the hour arm a small amount. You can press the restart button to successively move the hour arm by this small amount. Do this until the hour arm points at the "24" position. When it's done, don't press the reset anymore as this now has been registered as the absolute zero position. On a side note, this sketch also sets the date and time on the RTC module, which only needs to be done at this stage, and never again until the backup battery runs out.

This sketch writes an integer value, stepsCounter, to the EEPROM available in the ATmega328P. This value counts the number of steps the motor has rotated away from the absolute zero position. Hence, at this stage of the calibration stepsCounter = 0.

Next, we have a second sketch. This is the main sketch that codes how the clock is to behave. More details on that below. But what's important here is that every time this sketch tells the motor to rotate by some number of steps, it updates stepsCounter and then saves the new value to the EEPROM. This way every single movement is recorded to the EEPROM. The main key here is that EEPROM keeps the value even when the microcontroller is not powered.

Upon a start-up, the first thing this sketch does is read what value is in the EEPROM. The first time running this sketch right after calibration, stepsCounter is still 0. However, at any other time, the stepsCounter could be any value between 0 and 4096. If the value is non-zero, the sketch tells the motor to rotate counter-clockwise by this number of steps it has moved from the absolute zero position. Therefore, every time upon power up, the hour arm first moves to the "24" position. After that, the sketch reads the time from the RTC (which was set in the calibration sketch) and moves the arm to the position corresponding to the current time. And of course, the sketch keeps track of all movement and always updates stepsCounter in the EEPROM.

Challenge #2 - Start-up Driver Behavior, Solved Via Hardware (and a bit of software)

When powering on the entire project, the driver moves the stepper by a small amount, some number of degrees. This happens before the microcontroller has had a chance to prevent this and control the driver. This of course messes up with the stepsCounter and causes an offset in the hour arm position and the actual time. It is enough that after two or three power cycles, the hour arm is sufficiently in the wrong position. The solution here is to power up the project in two stages. Stage 1 - power up everything except the driver, Stage 2 - power up the driver.

This gives us control over when the driver gets power, which is going to be after the microcontroller has had a chance to handle the driver. We achieve this by using a P-MOSFET that disables the power to the driver. The P-MOSFET's gate is connected to the Nano Flip via a digital pin. By default, the pin is pulled high by a 1k ohm resistor which causes the power to remain off. Only when the digital pin is pulled low does the FET allow the driver to be powered. With this simple FET switch, the driver no longer has this initial startup movement, and all is well!

Electrical Schematic

Now let's put it all together. Below is a Fritzing schematic with all of the above main parts connected. Note that we are not showing, for example, the screw terminals or the female headers. This schematic simply shows electrical connections.

NOTE 1: The Fritzing model for the Proto-Half and the Nano Flip can both be found on our Documentation Subdomain: docs.PTSolns.com

NOTE 2: The Fritzing models for the stepper and driver are not ours. We give credit to their author: https://github.com/mgesteiro/fritzing-parts/tree/main

NOTE 3: The arrangement of the components is NOT how it is in the real product. Wires are much messier, running under the Nano Flip, and connected with a bunch of screw terminals. The enthusiastic maker will arrange the exact positions of all components and wires on the Proto-Half as they best see fit. We've included an image of the inside of the clock to show how we decided to arrange the components. The reader can use this as a reference.

Fritzing SchematicInside the Clock

Code

We didn't write any code ... no, seriously. We used ChatGPT to write the entire sketch (both of them). We prompted ChatGPT what we wanted and when it told us the sketch, we tested it. It took several back and forths until we got the sketches to work as intended. This way saved a ton of time coding and debugging. The two sketches are simple enough that ChatGPT could handle them reasonably well. For more complicated projects one might want to actually code...

There are two sketches. The first one is a calibration sketch that A) moves the stepper motor position until the hour arm is pointed towards the "24" mark, and B) sets the RTC time and date. The second sketch moves the motor to the right time position and updates the hour arm every minute. It also keeps track of all motor movements and stores these values, the stepsCounter, to EEPROM memory.

Below we've included the code which can be uploaded using Arduino IDE. The two sketches are also available, along with all documentation for this tutorial on our Documentation Subdomain: docs.PTSolns.com

Copy Code
Calibration Sketch
// Offset Calculator
// Last Update: Jan 8, 2025

// DESCRIPTION
// Use this sketch to move the hour hand to 24hrs position.
// Increment the hour hand until it is at 24hrs. This is the zero position.
// This zero position is to act like an absolute zero, which the stepper inherently does not have. 
// The zero position is reset in the EEPROM.
// When running the full Clock sketch, each time the stepper is moved, the counter is updated in the EEPROM.

// In the event of a power outage, the stepper will be able to remember how many steps it was away 
// from the absolute zero (24hrs) and recalibrate itself to the proper position.



// User-specified time for calibration
int targetSteps = -5; // {0-2048} Change this value so that after several runs of this sketch the hour hand is exactly on 24hrs.
                       // Use the Nano Flip reset button to slowly increment the hour hand forward (clockwise) to the 24hrs position.
                       // If the hour hand is far away from the 24hrs position, then increase the targetSteps to a large value.

int stepsCounter = 0; // The goal of this sketch is to align stepsCounter = 0 to the hour hand to point at 24hrs.
                      // In the full Clock sketch the stepsCounter is incremented every time the stepper moves a step.


#include <AccelStepper.h>
#include <EEPROM.h>
#include <RTClib.h>

const int EEPROM_ADDRESS = 0; // Starting address to store the integer

RTC_DS3231 rtc;                             // RTC object
AccelStepper stepper(AccelStepper::FULL4WIRE, 8, 10, 9, 11); // IN1, IN3, IN2, IN4 on ULN2003


void setup() {
  pinMode(3, OUTPUT);    // Set pin D3 as an output
  digitalWrite(3, HIGH); // Set D3 to HIGH to disable power to the stepper driver

  // Set up the stepper motor
  stepper.setMaxSpeed(500.0); // Maximum speed in steps per second
  stepper.setAcceleration(100.0); // Acceleration in steps per second^2

  digitalWrite(3, LOW); // Set D3 to LOW to enable power to the stepper driver

  stepper.moveTo(targetSteps);
  stepper.runToPosition(); // This is a blocking function.

  Serial.begin(9600);

  // Save the integer to EEPROM
  saveIntToEEPROM(EEPROM_ADDRESS, stepsCounter);
  Serial.println("Absolute zero calibrated when stepsCounter = 0 and hour hand points to 24hrs.");

  // Initialize RTC
  if (!rtc.begin()) {
    Serial.println("Couldn't find RTC");
    while (1);
  }

  Serial.println("RTC Date and Time Set.");
  rtc.adjust(DateTime(F(__DATE__), F(__TIME__))); // Set RTC to compile time
}

void loop() {
  // Nothing needed in the loop
}

// Function to save an integer to EEPROM
void saveIntToEEPROM(int address, int value) {
  byte lowByte = value & 0xFF;        // Extract the lower 8 bits
  byte highByte = (value >> 8) & 0xFF; // Extract the upper 8 bits
  EEPROM.write(address, lowByte);    // Write lower byte
  EEPROM.write(address + 1, highByte); // Write upper byte
}

Copy Code
Main Sketch
// 24Hrs Clock
// Last Update: Jan 6, 2025

// DESCRIPTION
// First run the "offset_calc.ide" to calibrate the zero position.
// Upon powing up, the driver sends signals to stepper to move a few positions. This happens even when the stepper pins are all low.
// To disable this, a P-Channel MOSFET was added controlled by digital pin D3 to cut main power to driver upon startup. Power is manually turned ON when D3 is pulled LOW.
// 

#include <AccelStepper.h>
#include <Wire.h>
#include <RTClib.h>
#include <EEPROM.h>

// Constants
const int STEPS_PER_REV    = 2048;           // Steps for a full revolution (28BYJ-48)
const int EEPROM_ADDRESS   = 0;              // EEPROM address to store stepsCounter
const float HOURS_PER_REV  = 24.0;           // Full revolution = 24 hours
const float STEPS_PER_HOUR = STEPS_PER_REV / HOURS_PER_REV; // Steps per hour

// Global variables
int stepsCounter = 0;                       // Steps away from absolute zero

RTC_DS3231 rtc;                             // RTC object
AccelStepper stepper(AccelStepper::FULL4WIRE, 8, 10, 9, 11); // Stepper pins

void setup() {
  pinMode(3, OUTPUT);    // Set pin D3 as an output
  digitalWrite(3, HIGH); // Set D3 to HIGH to disable power to the stepper driver

  Serial.begin(9600);

  // Initialize RTC
  if (!rtc.begin()) {
    Serial.println("Couldn't find RTC");
    while (1);
  }

  // Initialize stepper motor
  stepper.setMaxSpeed(500.0);
  stepper.setAcceleration(100.0);

  // Retrieve stepsCounter from EEPROM
  stepsCounter = readIntFromEEPROM(EEPROM_ADDRESS);
  Serial.print("Steps from zero position on startup: ");
  Serial.println(stepsCounter);

  digitalWrite(3, LOW); // Set D3 to LOW to enable power to the stepper driver

  // Move stepper to 24Hrs zero position to check
  if (stepsCounter != 0){
    stepper.move(-stepsCounter);
    stepper.runToPosition();
  };

  delay(3000);

  stepsCounter = 0; // Reset, since now we are at the zero position

  // Move stepper to correct position based on RTC time
  moveToCurrentTimePosition();
}

void loop() {
  // Continuously run the stepper
  stepper.run();

  // Update the position at regular intervals
  static unsigned long lastUpdate = 0;
  if (millis() - lastUpdate > 60000) { // Update every minute
    lastUpdate = millis();
    moveToCurrentTimePosition();
  }
}

// Function to move the stepper to the correct position based on the RTC time
void moveToCurrentTimePosition() {
  DateTime now = rtc.now();

  // Calculate current hour fraction (e.g., 12:30 = 12.5)
  float hourFraction = now.hour() + now.minute() / 60.0 + now.second() / 3600.0;

  // Calculate target steps based on current time
  long targetSteps = hourFraction * STEPS_PER_HOUR;

  // Calculate the number of steps to move
  long stepsToMove = targetSteps - stepsCounter;
  //long stepsToMove = targetSteps;

  // Wrap stepsToMove within one revolution (optional, for safety)
  stepsToMove = stepsToMove % STEPS_PER_REV;
  if (stepsToMove < 0) stepsToMove += STEPS_PER_REV;

  // Move the stepper and update stepsCounter
  stepper.move(stepsToMove);
  stepper.runToPosition();
  stepsCounter = targetSteps;

  // Save updated stepsCounter to EEPROM
  saveIntToEEPROM(EEPROM_ADDRESS, stepsCounter);

  // Print debugging information
  Serial.print("Current Time: ");
  Serial.print(now.hour());
  Serial.print(":");
  Serial.print(now.minute());
  Serial.print(":");
  Serial.println(now.second());
  Serial.print("Steps moved to: ");
  Serial.println(stepsCounter);
}

// Function to save an integer to EEPROM
void saveIntToEEPROM(int address, int value) {
  byte lowByte = value & 0xFF;        // Extract the lower 8 bits
  byte highByte = (value >> 8) & 0xFF; // Extract the upper 8 bits
  EEPROM.write(address, lowByte);    // Write lower byte
  EEPROM.write(address + 1, highByte); // Write upper byte
}

// Function to read an integer from EEPROM
int readIntFromEEPROM(int address) {
  byte lowByte = EEPROM.read(address);      // Read lower byte
  byte highByte = EEPROM.read(address + 1); // Read upper byte
  return (highByte << 8) | lowByte;         // Combine bytes into an integer
}


Final Thoughts

This was a pretty fun project. The two challenges kept us on our toes. We are not 3D CAD model designers, so the 3D modeling probably took us the longest. We didn't cover this part in the tutorial, but we made our files available to download. The clock certainly isn't perfect, but as a novelty and unique item, we are happy with the outcome. For any questions or comments please contact us. All documentation and supporting materials are available on our Documentation Subdomain: docs.PTSolns.com

Resources

Datasheets & supporting material on PTSolns Documentation Repository: https://docs.ptsolns.com

制造商零件编号 PTS-00079-201
BREADBOARD GENERAL PURPOSE PTH
PTSolns
制造商零件编号 PTS-00196-201
NANO FLIP ATMEGA328P EVAL BRD
PTSolns
制造商零件编号 MIKROE-1530
STEPPER MOTOR PM GEARED UNI 5V
MikroElektronika
制造商零件编号 PTS-00021-211
BOB FOR ARDUINO NANO NTEA-LG KIT
PTSolns
Add all DigiKey Parts to Cart
Have questions or comments? Continue the conversation on TechForum, DigiKey's online community and technical resource.