Mirko Pavleski
Published © GPL3+

DIY 3-Display OLED Clock with Arduino and I2C Multiplexer

The three displays are with I2C communication protocol and are controlled by a single microcontroller

BeginnerFull instructions provided2 hours187
DIY 3-Display OLED Clock with Arduino and I2C Multiplexer

Things used in this project

Hardware components

Arduino Nano R3
Arduino Nano R3
×1
0.96" OLED 64x128 Display Module
ElectroPeak 0.96" OLED 64x128 Display Module
×3
DS3231M - ±5ppm, I2C Real-Time Clock
Maxim Integrated DS3231M - ±5ppm, I2C Real-Time Clock
×1
I2C Hub 1 to 6 Expansion TCA9548A Module
M5Stack I2C Hub 1 to 6 Expansion TCA9548A Module
×1

Software apps and online services

Arduino IDE
Arduino IDE

Hand tools and fabrication machines

Soldering iron (generic)
Soldering iron (generic)
Solder Wire, Lead Free
Solder Wire, Lead Free

Story

Read more

Schematics

Schematic

...

Code

Code

C/C++
..
#include <Wire.h>
#include <Adafruit_SSD1306.h>
#include <RTClib.h>

Adafruit_SSD1306 display(128, 64, &Wire, 4);
RTC_DS3231 rtc;

int hours, minutes, seconds;
#define YELLOW_ZONE 16

void TCA9548A(uint8_t bus)
{
  Wire.beginTransmission(0x70);
  Wire.write(1 << bus);
  Wire.endTransmission();
}

void setup()
{
  // Initialize RTC on channel 1
  TCA9548A(1);
  rtc.begin();
  
  if (rtc.lostPower()) {
    rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
  }

    // --- OPTIONAL MANUAL TIME SET (only enable when needed) ---
  // To set time manually, remove the comment marks from the lines below,
  // adjust the DateTime parameters (Year, Month, Day, Hour, Minute, Second),
  // upload once, then comment them out again to prevent overwriting the RTC each boot.
 
 // rtc.adjust(DateTime(2025, 10, 22, 12, 15, 00));  // YYYY, MM, DD, HH, MM, SS
 

  // Initialize displays with rotated orientation
  // Display 1: Seconds (leftmost) - Channel 2
  TCA9548A(2);
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  display.setRotation(1); // Rotate 90 degrees for portrait mode
  display.clearDisplay();
  display.display();

  // Display 2: Minutes (middle) - Channel 3
  TCA9548A(3);
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  display.setRotation(1); // Rotate 90 degrees for portrait mode
  display.clearDisplay();
  display.display();
  
  // Display 3: Hours (rightmost) - Channel 4
  TCA9548A(4);
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  display.setRotation(1); // Rotate 90 degrees for portrait mode
  display.clearDisplay();
  display.display();

   delay(500);
  showIntro();
  
 // delay(1000);
}

void showIntro() {
  int screenH = 128;
  const char* words[3] = {"CLOCK", "DISPL", "THREE"};

  // --- STEP 1: Loading animation + falling letters ---
  for (int h = 0; h <= 120; h += 6) {
    for (int idx = 0; idx < 3; idx++) {
      int ch = 2 + idx;
      TCA9548A(ch);
      display.clearDisplay();

      // --- Yellow frame (16x128, 2-px border) ---
      for (int i = 0; i < 2; i++) {
        display.drawRect(i, i, YELLOW_ZONE - 2 * i, screenH - 2 * i, WHITE);
      }

      // --- Filling bar ---
      int fillX = 4;
      int fillW = YELLOW_ZONE - 8;
      int fillY = 124 - h;
      display.fillRect(fillX, fillY, fillW, h, WHITE);

      // --- Falling letters ---
      display.setTextColor(WHITE);
      display.setTextSize(2);
      int textX = YELLOW_ZONE + 20;
      int textY = 12;
      const char* w = words[idx];

      // Determine how many letters to show depending on progress
      int totalLetters = strlen(w);
      int visibleLetters = map(h, 0, 120, 0, totalLetters);
      if (visibleLetters > totalLetters) visibleLetters = totalLetters;

      // Draw visible letters vertically, one by one
      for (int k = 0; k < visibleLetters; k++) {
        display.setCursor(textX, textY + k * 20);
        display.write(w[k]);
      }

      display.display();
    }
    delay(80);
  }

  delay(1000); // pause 1 s at full bar

  // --- STEP 2: Fade-out animation (bars emptying downward) ---
  for (int h = 120; h >= 0; h -= 6) {
    for (int idx = 0; idx < 3; idx++) {
      int ch = 2 + idx;
      TCA9548A(ch);
      display.clearDisplay();

      // --- Yellow frame ---
      for (int i = 0; i < 2; i++) {
        display.drawRect(i, i, YELLOW_ZONE - 2 * i, screenH - 2 * i, WHITE);
      }

      // --- Emptying bar ---
      int fillX = 4;
      int fillW = YELLOW_ZONE - 8;
      int fillY = 124 - h;
      display.fillRect(fillX, fillY, fillW, h, WHITE);

      // --- Keep all letters visible during fade ---
      display.setTextColor(WHITE);
      display.setTextSize(2);
      int textX = YELLOW_ZONE + 20;
      int textY = 12;
      const char* w = words[idx];
      for (int k = 0; w[k]; k++) {
        display.setCursor(textX, textY + k * 20);
        display.write(w[k]);
      }

      display.display();
    }
    delay(60);
  }

  // --- STEP 3: Clear all displays ---
  for (int ch = 2; ch <= 4; ch++) {
    TCA9548A(ch);
    display.clearDisplay();
    display.display();
  }
}

void loop()
{
  // Read time from RTC
  TCA9548A(1);
  DateTime now = rtc.now();
  hours = now.hour();
  minutes = now.minute();
  seconds = now.second();

  // Update Seconds Display (Left) - Channel 2
  TCA9548A(2);
  display.clearDisplay();
  drawDisplay(seconds, 'S', 60, seconds);
  display.display();

  // Update Minute Display (Middle) - Channel 3
  TCA9548A(3);
  display.clearDisplay();
  drawDisplay(minutes, 'M', 60, minutes);
  display.display();
  
  // Update Hours Display (Right) - Channel 4
  TCA9548A(4);
  display.clearDisplay();
  drawDisplay(hours, 'H', 24, hours);
  display.display();
  
  delay(200); // Update 5 times per second
}

void drawDisplay(int value, char unit, int maxValue, int barValue) {
  // NOTE: In portrait rotation, display.width() = 64, display.height() = 128
  int screenW = 64;
  int screenH = 128;

  // --- Bargraph container ---
  int barX = 0;
  int barW = YELLOW_ZONE;  // 16 px
  int barY = 0;
  int barH = screenH;

  // Outer rectangle (frame, 2-px thick)
  for (int i = 0; i < 2; i++) {
    display.drawRect(barX + i, barY + i, barW - 2 * i, barH - 2 * i, WHITE);
  }

  // --- Calculate bar height (usable area = 120 px: from y=4 to y=124) ---
  int innerTop = 4;
  int innerBottom = screenH - 4;
  int usableHeight = innerBottom - innerTop;  // 120 px

  int barHeight = map(barValue, 0, maxValue, 0, usableHeight);

  // --- Fill the bar inside the container ---
  int fillX = barX + 2 + 2;        // 2-px frame + 2-px gap
  int fillW = barW - 8;            // 2+2 margin each side → 8 total
  int fillY = innerBottom - barHeight;  // fill upward from bottom
  display.fillRect(fillX, fillY, fillW, barHeight, WHITE);

  // --- Blue field dimensions (right side) ---
  int frameX1 = YELLOW_ZONE;                
  int frameW  = screenW - YELLOW_ZONE;      
  int frameY1 = 0;
  int frameH  = screenH;

  // --- Outer frame (3 px) ---
  for (int i = 0; i < 3; i++) {
    display.drawRect(frameX1 + i, frameY1 + i, frameW - 2 * i, frameH - 2 * i, WHITE);
  }

  // --- Divider between letter and digits ---
  int dividerY = 40;  
  display.fillRect(frameX1 + 3, dividerY, frameW - 6, 3, WHITE);

  // --- Text ---
  display.setTextColor(WHITE);
  display.setTextSize(3);

  // Upper: unit letter (H / M / S)
  int unitX = frameX1 + 17;
  int unitY = 11;
  display.setCursor(unitX, unitY);
  display.print(unit);

  display.setTextSize(4);

  // Lower: digits
  int tens = value / 10;
  int units = value % 10;

  int tensX = frameX1 + 13;
  int tensY = 52;
  display.setCursor(tensX, tensY);
  display.print(tens);

  int unitsX = frameX1 + 13;
  int unitsY = 87;
  display.setCursor(unitsX, unitsY);
  display.print(units);

  // --- Inner rounded frames ---
  // Top rectangle (letters)
  int innerTopX = frameX1 + 4;
  int innerTopY = 4;
  int innerTopW = frameW - 8;
  int innerTopH = dividerY - innerTopY - 2; // 1px gap to divider
  for (int i = 0; i < 2; i++) {
    display.drawRoundRect(innerTopX + i, innerTopY + i, innerTopW - 2 * i, innerTopH - 2 * i, 4, WHITE);
  }

  // Bottom rectangle (digits)
  int innerBotX = frameX1 + 4;
  int innerBotY = dividerY + 4; // 1px gap below divider
  int innerBotW = frameW - 8;
  int innerBotH = frameH - innerBotY - 4;
  for (int i = 0; i < 2; i++) {
    display.drawRoundRect(innerBotX + i, innerBotY + i, innerBotW - 2 * i, innerBotH - 2 * i, 4, WHITE);
  }
}

Credits

Mirko Pavleski
209 projects • 1550 followers

Comments