Community project

Make Arduino Rotating Clock Using Led Line

Arduino
Photo of Make Arduino Rotating Clock Using Led Line
Generated with AI

Pruthivi Sampath

Published September 23, 2026

This project builds a mesmerizing analog clock that displays the time using a rotating arm of addressable LEDs. As the arm spins, the WS2812B LEDs light up in patterns that form digits, creating a persistence-of-vision display that shows hours and minutes. The rotation is driven by a geared DC motor and synchronized using a Hall effect sensor that detects a magnet on the spinning shaft, while a precision RTC module keeps accurate time.

Builders will receive a complete wiring diagram showing how to connect the motor, slip ring, LED strip, Hall sensor, and power supply to the Arduino Uno, along with a full parts list and firmware that handles the timing synchronization and LED animation. The assembly guide walks through constructing the rotating arm guard, mounting the motor and sensor, and calibrating the display so the time appears correctly as the arm sweeps around.

Wiring diagram

Wiring diagram for Make Arduino Rotating Clock Using Led Line

Gather all the parts

QtyComponent
1

WS2812B

8 pixels

Addressable RGB LED strip

1

Adafruit DS3231 Precision RTC Breakout

DS3231

DS3231 precision real-time clock breakout with integrated TCXO and crystal for accurate timekeeping over I2C.

1

M5Stack Hall Effect Unit (A3144E)

A3144

M5Stack Grove-connector unit that packs three A3144E unipolar Hall-effect switches behind 74HC08D logic gates into one digital sensor. It drives its output low (and lights an onboard LED) when a magnet's S pole approaches the front face or an N pole approaches the back, so it's a drop-in digital hall/magnetic-proximity sensor for door/lid detection, tachometers, and reed-switch-style proximity sensing — plug it into a Grove PORT and read one GPIO.

1

Neodymium disc magnet

10 x 3 mm

A small magnet glued to the rotating arm so the fixed Hall sensor can identify the start of each turn.

1

5V enclosed geared DC motor

5V, 600 RPM

A low-speed geared motor that spins the LED arm and must be fully covered by a rigid guard.

1

3-wire miniature slip ring

3-wire, low-current

A preassembled rotating connector that carries 5 volt power, ground, and LED data to the spinning arm.

1

Resistor

330 Ω

Through-hole resistor (current-limiting in series with an LED)

1

Regulated 5V 3A enclosed wall power supply

5V, 3A

An insulated low-voltage power supply that runs the Arduino, LED line, and motor without using mains wiring in the project.

1

1000 µF 10V electrolytic capacitor

1000 µF, 10V minimum

A polarized capacitor placed across the 5 volt LED supply to soften sudden LED current changes.

Assemble it in 5 steps

1. Make a guarded rotating arm

Fix the 8-pixel LED line (led_line) firmly to a light, balanced arm attached to the motor shaft. Put pixel 0 at the lower end and keep the LEDs facing outward. Glue the small magnet (magnet) to the arm so it passes the fixed Hall sensor once per revolution. Enclose the whole spinning arm in a clear guard before powering the motor.

  • Turn the shaft by hand first: the arm must not wobble or touch the guard.
  • Keep the magnet 2 to 5 mm from the sensor face; flip the magnet if the sensor’s small indicator light does not react.
  • An unguarded spinning arm can strike fingers, snag hair, or throw loose parts across the room.

2. Wire the fixed clock and rotation marker

With power unplugged, connect the RTC module (rtc): VIN → 5V (power), GND → GND (ground), SDA → Arduino A4 / GPIO18 (data), and SCL → Arduino A5 / GPIO19 (clock). Connect the Hall sensor (hall_sensor): 5V → 5V (power), GND → GND (ground), and OUT → Arduino D2 / GPIO2 (rotation signal).

  • Fit a CR1220 backup cell in the RTC holder if your module has one; it keeps the time while the main supply is unplugged.
  • All GND connections must join together so the Arduino can read the sensor reliably.
  • Make sure 5V and GND are not swapped — swapped power can damage the modules.

3. Wire the rotating LED arm

Connect Arduino D6 / GPIO6 to one end of the 330 Ω resistor (data_resistor); connect its other end to the fixed-side DATA wire on the slip ring (slip_ring). Connect the slip ring’s fixed-side 5V → 5V (power) and fixed-side GND → GND (ground). On the rotating side, connect 5V rotor → LED line VCC (power), GND rotor → LED line GND (ground), and DATA rotor → LED line DATA (signal). Put the capacitor (supply_capacitor) across the LED line: + → VCC (power) and the striped − lead → GND (ground).

  • Use flexible, strain-relieved wires on the rotating side so they cannot rub the shaft.
  • The resistor is installed in the data wire, not across the 5V and GND wires.
  • The capacitor’s striped negative lead must go to GND; reversing it can make it hot or burst.

4. Connect the motor and supply

Connect the regulated 5V supply (power_supply) 5V+ → the shared 5V rail (power) and GND → the shared GND rail (ground). Connect motor M+ → 5V (power) and M− → GND (ground). Power the Arduino from the same regulated 5V rail through its 5V pin, not through VIN.

  • Use a 5V supply rated for at least 3 A, and keep the motor leads twisted together where practical to reduce electrical noise.
  • Check the arm is inside its guard before applying power.
  • Do not connect a 5V supply to the Arduino VIN pin — VIN expects a higher input voltage and the board may behave unreliably. Never work on wiring while the wall supply is plugged in.

5. Set the time and check the image

After the design is deployed, the RTC will set itself once from the firmware build time if its backup cell was flat or missing. Watch the guarded arm from the side: the blue HH:MM time should appear still in the air. If the digits are upside down, reverse the LED strip physically or change the indicated pixel order in the firmware.

  • The motor should run near 600 RPM; if the image drifts or smears, adjust the magnet gap and make sure it gives exactly one clean sensor trigger per turn.
  • Stop power before repositioning the magnet, sensor, LED arm, or guard.

Review all connections

1. Connections between "power_supply" and "Arduino"

Functionpower_supplyArduino
power5V+5V
groundGNDGND

2. Connections between "rtc" and "Arduino"

FunctionrtcArduino
powerVIN5V
groundGNDGND
i2cSDAGPIO 18
i2cSCLGPIO 19

3. Connections between "hall_sensor" and "Arduino"

Functionhall_sensorArduino
power5V5V
groundGNDGND
digitalOUTGPIO 2

4. Connections between "data_resistor" and "Arduino"

Functiondata_resistorArduino
digitalP1GPIO 6
digitalP23-wire miniature slip ring DATA fixedEXT

5. Connections between "slip_ring" and "Arduino"

Functionslip_ringArduino
power5V fixed5V
groundGND fixedGND
power5V rotorWS2812B VCCEXT
groundGND rotorWS2812B GNDEXT
digitalDATA rotorWS2812B DATAEXT

6. Connections between "supply_capacitor" and "Arduino"

Functionsupply_capacitorArduino
power+WS2812B VCCEXT
ground-WS2812B GNDEXT

7. Connections between "motor" and "Arduino"

FunctionmotorArduino
powerM+5V
groundM-GND

Deploy the firmware

#include <Arduino.h>
#include <Wire.h>
#include <RTClib.h>
#include <Adafruit_NeoPixel.h>

// Forward declarations
void onMarker();
bool pixelForColumn(const uint8_t column, const uint8_t y, const uint8_t values[4]);
void showColumn(uint8_t phaseColumn);

constexpr uint8_t HALL_PIN = 2;
constexpr uint8_t LED_DATA_PIN = 6;
constexpr uint8_t LED_COUNT = 8; // Number of LEDs on the rotating arm, not a board pin.
constexpr uint8_t COLUMNS_PER_TURN = 120;
constexpr uint8_t GLYPH_HEIGHT = 5;
constexpr uint8_t DISPLAY_Y_OFFSET = 1;
constexpr uint8_t BRIGHTNESS = 32;

Adafruit_NeoPixel leds(LED_COUNT, LED_DATA_PIN, NEO_GRB + NEO_KHZ800);
RTC_DS3231 rtc;

volatile uint32_t markerMicros = 0;
volatile uint32_t previousMarkerMicros = 0;
volatile uint32_t revolutionMicros = 100000;
volatile bool markerSeen = false;

uint8_t lastColumn = 255;

const uint8_t digits[10][GLYPH_HEIGHT] = {
  {0b111, 0b101, 0b101, 0b101, 0b111},
  {0b010, 0b110, 0b010, 0b010, 0b111},
  {0b111, 0b001, 0b111, 0b100, 0b111},
  {0b111, 0b001, 0b111, 0b001, 0b111},
  {0b101, 0b101, 0b111, 0b001, 0b001},
  {0b111, 0b100, 0b111, 0b001, 0b111},
  {0b111, 0b100, 0b111, 0b101, 0b111},
  {0b111, 0b001, 0b010, 0b010, 0b010},
  {0b111, 0b101, 0b111, 0b101, 0b111},
  {0b111, 0b101, 0b111, 0b001, 0b111}
};

void onMarker() {
  const uint32_t now = micros();
  const uint32_t interval = now - markerMicros;
  // Ignore a second trigger from the same magnet pass and unreasonable speeds.
  if (interval > 20000UL && interval < 500000UL) {
    previousMarkerMicros = markerMicros;
    markerMicros = now;
    revolutionMicros = interval;
    markerSeen = true;
  } else if (markerMicros == 0) {
    markerMicros = now;
    markerSeen = true;
  }
}

bool pixelForColumn(const uint8_t column, const uint8_t y, const uint8_t values[4]) {
  // Each digit is 3 columns wide. The two separator dots occupy one column.
  uint8_t cursor = 0;
  for (uint8_t digitIndex = 0; digitIndex < 4; ++digitIndex) {
    if (digitIndex == 2) {
      if (column == cursor && (y == 1 || y == 3)) return true;
      ++cursor;
    }
    if (column >= cursor && column < cursor + 3) {
      const uint8_t bit = 2 - (column - cursor);
      return (digits[values[digitIndex]][y] & (1 << bit)) != 0;
    }
    cursor += 3;
    if (digitIndex != 3) ++cursor;
  }
  return false;
}

void showColumn(uint8_t phaseColumn) {
  // Put the 15-column HH:MM image in the middle of a 120-column turn.
  const int16_t imageColumn = static_cast<int16_t>(phaseColumn) - 52;
  leds.clear();

  if (imageColumn >= 0 && imageColumn < 15) {
    const DateTime now = rtc.now();
    const uint8_t values[4] = {
      static_cast<uint8_t>(now.hour() / 10),
      static_cast<uint8_t>(now.hour() % 10),
      static_cast<uint8_t>(now.minute() / 10),
      static_cast<uint8_t>(now.minute() % 10)
    };

    for (uint8_t y = 0; y < GLYPH_HEIGHT; ++y) {
      if (pixelForColumn(static_cast<uint8_t>(imageColumn), y, values)) {
        // Reverse if your physical strip is fitted with pixel 0 at the top.
        leds.setPixelColor(DISPLAY_Y_OFFSET + y, leds.Color(0, 0, BRIGHTNESS));
      }
    }
  }
  leds.show();
}

void setup() {
  pinMode(HALL_PIN, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(HALL_PIN), onMarker, FALLING);

  Wire.begin();
  rtc.begin();
  if (rtc.lostPower()) {
    // Sets the RTC once from the firmware build time after a flat or missing coin cell.
    rtc.adjust(DateTime(__DATE__, __TIME__));
  }

  leds.begin();
  leds.setBrightness(BRIGHTNESS);
  leds.clear();
  leds.show();
}

void loop() {
  uint32_t markerCopy;
  uint32_t revolutionCopy;
  bool seenCopy;
  noInterrupts();
  markerCopy = markerMicros;
  revolutionCopy = revolutionMicros;
  seenCopy = markerSeen;
  interrupts();

  if (!seenCopy || revolutionCopy == 0) {
    return;
  }

  const uint32_t elapsed = micros() - markerCopy;
  if (elapsed >= revolutionCopy) {
    return; // Wait for the next Hall marker rather than drawing with a guessed angle.
  }

  const uint8_t column = static_cast<uint8_t>((elapsed * COLUMNS_PER_TURN) / revolutionCopy);
  if (column != lastColumn) {
    lastColumn = column;
    showColumn(column);
  }
}

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik