John Bradnam
Published © GPL3+

LOL Clock

A 3D printed clock made up of lots of LEDs (LOL).

IntermediateFull instructions provided12 hours2,499

Things used in this project

Hardware components

LED (generic)
LED (generic)
5mm
×95
Arduino Mini 05
Arduino Mini 05
×1
DM13a 16bit Shift Register
×2
Real Time Clock (RTC)
Real Time Clock (RTC)
DS1307 Module with battery
×1
LM2596 DC-DC Step Down Converter Module
×1
PTS 645 Series Switch
C&K Switches PTS 645 Series Switch
17mm shaft with button tops
×3
AO3401 P-Channel MOSFET
×5

Software apps and online services

Arduino IDE
Arduino IDE

Hand tools and fabrication machines

3D Printer (generic)
3D Printer (generic)
Soldering iron (generic)
Soldering iron (generic)

Story

Read more

Custom parts and enclosures

STL Files

Schematics

Schematic

PCB

Eagle files

Schematic & PCB in Eagle format

Code

LolClockV1.ino

C/C++
/*
LOL Clock
19x5 LED matrix driven by two DM31A shift registers/LED drivers
DS1307 RTC
Three buttons on a single pin resistor network to set the time

Concept and Case: robaux (https://www.thingiverse.com/thing:2219658)

Schematic, Board design, Code: John Bradnam (jbrad2089@gmail.com)

*/

#include <SPI.h>// SPI Library used to clock data out to the shift registers
#include <Wire.h>
#include <RTClib.h>

#define LATCH_PIN 2    //can use any pin you want to latch the shift registers
#define BLANK_PIN 4    // same, can use any pin you want for this, just make sure you pull up via a 1k to 5V
#define DATA_PIN 11    // used by SPI, must be pin 11
#define CLOCK_PIN 13   // used by SPI, must be 13
#define ROW_1 5
#define ROW_2 6
#define ROW_3 7
#define ROW_4 8
#define ROW_5 9

//Buttons are on a resistor network
// A1 > 1000 (off)
// 850 <= A1 < 900 (MODE)
// 750 <= A1 < 800 (UP or NEXT)
// 0 <= A1 <= 50 (DOWN or PREV)
#define BUTTON_PIN A1 
enum ButtonEnum { NO_BUTTON, MODE_BTN, UP_BTN, DOWN_BTN };

#define ROWS 5            //Number of rows of LEDs
#define LEDS_PER_ROW 24		//Number of bits to shift
#define BYTES_PER_ROW 4   //Number of bytes required to hold one bit per LED in each row
#define ACTUAL_OFFSET 8   //Offset to first LED
#define ACTUAL_PER_ROW 19 //Number of leds on each row

byte ledStates[ROWS][BYTES_PER_ROW];  //Store state of each LED (either off or on)
int activeRow=0;                      //this increments through the anode levels

//define modes to set time
enum ModeEnum { NORMAL, SET_HOURS, SET_MINUTES };

//Font characters use 20 bits and are stored as 3 bytes
//03 02 01 00
//07 06 05 04
//11 10 09 08
//15 14 13 12
//19 18 17 16
const uint8_t numbers[] PROGMEM = {
    //Character 0
    //0, 1, 1, 0, | 6
    //1, 0, 0, 1, | 9
    //1, 0, 0, 1, | 9
    //1, 0, 0, 1, | 9
    //0, 1, 1, 0, | 6
    0x06, 0x99, 0x96,
    //Character 1
    //0, 0, 1, 0, | 2 
    //0, 1, 1, 0, | 6
    //0, 0, 1, 0, | 2
    //0, 0, 1, 0, | 2
    //0, 0, 1, 0  | 2
    0x02, 0x22, 0x62,
    //Character 2
    //1, 1, 1, 0, | E
    //0, 0, 0, 1, | 1
    //0, 1, 1, 0, | 6
    //1, 0, 0, 0, | 8
    //1, 1, 1, 1  | F
    0x0F, 0x86, 0x1E,
    //Character 3
    //1, 1, 1, 0, | E
    //0, 0, 0, 1, | 1
    //0, 1, 1, 0, | 6
    //0, 0, 0, 1, | 1
    //1, 1, 1, 0  | E
    0x0E, 0x16, 0x1E,
    //Character 4
    //1, 0, 0, 0, | 8 
    //1, 0, 1, 0, | A
    //1, 1, 1, 1, | F
    //0, 0, 1, 0, | 2
    //0, 0, 1, 0  | 2
    0x02, 0x2F, 0xA8,
    //Character 5
    //1, 1, 1, 1, | F
    //1, 0, 0, 0, | 8
    //1, 1, 1, 0, | E
    //0, 0, 0, 1, | 1
    //1, 1, 1, 0  | E
    0x0E, 0x1E, 0x8F,
    //Character 6
    //0, 1, 1, 0, | 6
    //1, 0, 0, 0, | 8
    //1, 1, 1, 0, | E
    //1, 0, 0, 1, | 9
    //0, 1, 1, 0, | 6
    0x06, 0x9E, 0x86,
    //Character 7
    //1, 1, 1, 1, | F 
    //0, 0, 0, 1, | 1
    //0, 0, 1, 0, | 2
    //0, 1, 0, 0, | 4
    //0, 1, 0, 0, | 4
    0x04, 0x42, 0x1F,
    //Character 8
    //0, 1, 1, 0, | 6
    //1, 0, 0, 1, | 9
    //0, 1, 1, 0, | 6
    //1, 0, 0, 1, | 9
    //0, 1, 1, 0, | 6
    0x06, 0x96, 0x96,
    //Character 9
    //0, 1, 1, 0, | 6 
    //1, 0, 0, 1, | 9
    //0, 1, 1, 1, | 7
    //0, 0, 0, 1, | 1
    //0, 1, 1, 0  | 6
    0x06, 0x17, 0x96,
    //Character Space
    0x00, 0x00, 0x00
};

#define SPACE_CHAR 10

RTC_DS1307 rtc;
char buf[8];          //Used to convert time to string
bool colon = false;   //State of colon
int lastSecond = 0;   //Used to test if clock has changed time

void setup()
{
  Serial.begin(9600);
  
  SPI.setBitOrder(MSBFIRST);//Most Significant Bit First
  SPI.setDataMode(SPI_MODE0);// Mode 0 Rising edge of data, keep clock low
  SPI.setClockDivider(SPI_CLOCK_DIV2);//Run the data in at 16MHz/2 - 8MHz

  noInterrupts();// kill interrupts until everybody is set up

  //Clear out ledStates array
  for (int r = 0; r < ROWS; r++)
  {
    for (int c = 0; c < BYTES_PER_ROW; c++)
    {
      ledStates[r][c] = 0;
    }
  }
  activeRow = 0;

  //We use Timer 1 to refresh the display
  TCCR1A = B00000000;//Register A all 0's since we're not toggling any pins
  TCCR1B = B00001011;//bit 3 set to place in CTC mode, will call an interrupt on a counter match
  //bits 0 and 1 are set to divide the clock by 64, so 16MHz/64=250kHz
  TIMSK1 = B00000010;//bit 1 set to call the interrupt on an OCR1A match
  OCR1A=120; // you can play with this, but I set it to 120, which means:
  //our clock runs at 250kHz, which is 1/250kHz = 4us
  //with OCR1A set to 120, this means the interrupt will be called every (120+1)x4us=484us, 
  //which gives a multiplex frequency of about 2kHz

  //finally set up the Outputs
  pinMode(LATCH_PIN, OUTPUT);//Latch
  pinMode(DATA_PIN, OUTPUT);//MOSI DATA
  pinMode(CLOCK_PIN, OUTPUT);//SPI Clock
  //Because of the P-Channel MOSFETs, rows are active LOW
  pinMode(ROW_1, OUTPUT);
  pinMode(ROW_2, OUTPUT);
  pinMode(ROW_3, OUTPUT);
  pinMode(ROW_4, OUTPUT);
  pinMode(ROW_5, OUTPUT);
  digitalWrite(ROW_1, HIGH);
  digitalWrite(ROW_2, HIGH);
  digitalWrite(ROW_3, HIGH);
  digitalWrite(ROW_4, HIGH);
  digitalWrite(ROW_5, HIGH);
  
  //pinMode(BLANK_PIN, OUTPUT);//Output Enable  important to do this last, so LEDs do not flash on boot up
  SPI.begin();//start up the SPI library
  interrupts();//let the show begin, this lets the multiplexing start

  if (!rtc.begin())
  {
    Serial.println("Cannot find RTC");
  }
  else if (!rtc.isrunning()) 
  {
    //Serial.println("RTC lost power, lets set the time!");
    rtc.adjust(DateTime(1900, 1, 1, 0, 0, 0));
    Serial.println("SetTime");
  }
  
}

void loop()
{
  if (ButtonPressed() == MODE_BTN)
  {
    SetClockTime();
  }

  DateTime now = rtc.now();  
  if (now.second() != lastSecond)
  {
    lastSecond = now.second();
    showTime(now.hour(), now.minute());
    colon = !colon;
    led(1, 9, colon);
    led(3, 9, colon);
  }
  delay(50);
}

//show the time
//h = hours (0..11)
//m = minutes (0..59)
void showTime(int h, int m)
{
  showTime(h, m, true, true);
}

//show the time
//h = hours (0..11)
//m = minutes (0..59)
//he = hours enable (true/false)
//me = minutes enable (true/false)
void showTime(int h, int m, bool he, bool me)
{
  if (h == 0)
  {
    h = 12;
  }
  sprintf(buf, "%02d%02d", h, m);
  digit(0, (he && buf[0] != '0') ? (int)buf[0] - 48 : SPACE_CHAR);
  digit(5, (he) ? (int)buf[1] - 48 : SPACE_CHAR);
  digit(10, (me) ? (int)buf[2] - 48 : SPACE_CHAR);
  digit(15, (me) ? (int)buf[3] - 48 : SPACE_CHAR);
}

//Displays a digit at a specific column offset
//column = left most column to start showing digit
//value = digit to display
void digit(int column, int value)
{
  column = constrain(column, 0, ACTUAL_PER_ROW - 1);
  value = constrain(value, 0, 10) * 3;
  int row = 0;
  for (int i = 2; i >=0; i--)
  {
    uint8_t b = pgm_read_byte(&numbers[value + i]);
    outRow(row, column, b);
    row++;
    outRow(row, column, b >> 4);
    row++;
  }
}

//Display a least significant 4 bits
//row = row to write to
//column = left most column to start showing the 4 bits
//b = nibble to display in least significant 4 bits (0 = off, 1 = on)
void outRow(int row, int column, byte b)
{
  int mask = 0x08;
  for (int i = 0; i < 4; i++)
  {
    if (column < ACTUAL_PER_ROW && row < ROWS)
    {
      led(row, column, ((b & mask) != 0) ? 1 : 0);
    }
    column++;
    mask = mask >> 1;
  }
}

//This turns on or off a LED in the matrix
//row => (0 <= row < ROWS)
//column => (0 <= column < ACTUAL_PER_ROW-1)
//on => true to turn on LED, false to turn off LED
void led(int row, int column, bool on)
{ 
  // First, check and make sure nothing went beyond the limits
  row = constrain(row, 0, ROWS - 1);
  column = constrain(column, 0, ACTUAL_PER_ROW - 1) + ACTUAL_OFFSET;

  //Divide the column by 8 to get the index into the array
  //Use the remainder to determine the bit to set/clear in the byte at that index
  int colIndex = column >> 3;  //Divide by 8 => Divide by 2^3 => shift right 3
  int bitIndex = column & 7;   //Get the bottom 3 bits which is the bit position of the bit we want
  int bitMask = 1 << bitIndex;
  if (on)
  {
    ledStates[row][BYTES_PER_ROW - colIndex] |= bitMask;
  }
  else
  {
    ledStates[row][BYTES_PER_ROW - colIndex] &= ~bitMask;
  }
}

//Called every 4uS to update the LEDs
ISR(TIMER1_COMPA_vect)
{

  //This routine is called in the background automatically at frequency set by OCR1A
  //In this code, I set OCR1A to 120, so this is called every 484us, giving each row in the cube 484us of ON time
  //There are 5 levels, the frequency of the multiplexing is then 484us*5=2.420ms, or about 400Hz

  PORTD |= 1 << BLANK_PIN;  //The first thing we do is turn all of the LEDs OFF, by writing a 1 to the blank pin

  //Put the column data out to the shift registers
  for (int shift_out = 0; shift_out < BYTES_PER_ROW; shift_out++)
  {
    SPI.transfer(ledStates[activeRow][shift_out]);
  }

  //Enable row that we just outputed the column data for (LOW means row is Active)
  digitalWrite(ROW_5, (activeRow == 0) ? LOW : HIGH);
  digitalWrite(ROW_4, (activeRow == 1) ? LOW : HIGH);
  digitalWrite(ROW_3, (activeRow == 2) ? LOW : HIGH);
  digitalWrite(ROW_2, (activeRow == 3) ? LOW : HIGH);
  digitalWrite(ROW_1, (activeRow == 4) ? LOW : HIGH);

  PORTD |= 1<<LATCH_PIN;//Latch pin HIGH
  PORTD &= ~(1<<LATCH_PIN);//Latch pin LOW
  PORTD &= ~(1<<BLANK_PIN);//Blank pin LOW to turn on the LEDs with the new data

  activeRow = (activeRow + 1) % ROWS;   //increment the active row

  pinMode(BLANK_PIN, OUTPUT);
}

//Sets the time on the clock
// Press MODE button to Set the time
// Use UP and DOWN buttons to set the hour
// Press MODE button to switch to minutes
// Use UP and DOWN buttons to set the minute
// Press MODE button to store the time and return to the normal clock mode
void SetClockTime()
{
  #define FLASH_TIME 200
  
  DateTime now = rtc.now();  
  int h = now.hour();
  int m = now.minute();

  //Switch to SET_HOURS MODE
  int mode = SET_HOURS;
  bool hourOn = false;
  bool minOn = true;
  showTime(now.hour(), now.minute(), hourOn, minOn);
  Serial.println("MODE: SET_HOURS");

  long flash = millis() + FLASH_TIME;
  while (mode != NORMAL)
  {
    if (millis() > flash)
    {
      flash = millis() + FLASH_TIME;
      switch (mode)
      {
        case SET_HOURS: hourOn = !hourOn; break;
        case SET_MINUTES: minOn = !minOn; break;
      }
      showTime(h, m, hourOn, minOn);
    }
    ButtonEnum button = ButtonPressed();
    switch (mode) 
    {
      case SET_HOURS:
        switch (button)
        {
          case MODE_BTN:
            mode = SET_MINUTES;
            hourOn = true;
            minOn = false;
            showTime(h, m, hourOn, minOn);
            Serial.println("MODE: SET_MINUTES");
            break;

          case DOWN_BTN:
            h = (h + 12 - 1) % 12;
            showTime(h, m, hourOn, minOn);
            break;

          case UP_BTN:
            h = (h + 1) % 12;
            showTime(h, m, hourOn, minOn);
            break;
        }
        break;

      case SET_MINUTES:
        switch (button)
        {
          case MODE_BTN:
            mode = NORMAL;
            //Set RTC
            now = rtc.now();
            rtc.adjust(DateTime (now.year(), now.month(), now.day(), h, m, now.second()));
            Serial.println("MODE: NORMAL");
            showTime(now.hour(), now.minute(), hourOn, minOn);
            break;

          case DOWN_BTN:
            m = (m + 60 - 1) % 60;
            showTime(h, m, hourOn, minOn);
            break;

          case UP_BTN:
            m = (m + 1) % 60;
            showTime(h, m, hourOn, minOn);
           break;
        }
        break;
    }
  }
}

//Tests if any of the buttons have been pressed and released
//  returns the button that was pressed
ButtonEnum ButtonPressed()
{
  bool pressed = false;
  ButtonEnum button = NO_BUTTON;
  int lowRange = 1000;
  int highRange = 1024;
  // A1 > 1000 (off)
  // 850 <= A1 < 900 (MODE)
  // 750 <= A1 < 800 (UP or NEXT)
  // 0 <= A1 <= 50 (DOWN or PREV)

  int value = analogRead(BUTTON_PIN);
  if (value < lowRange)
  {
    button = MODE_BTN;
    lowRange = 850; 
    highRange = 900;
    if (value < lowRange)
    {
      button = UP_BTN;
      lowRange = 750; 
      highRange = 800;
      if (value < lowRange)
      {
        button = DOWN_BTN;
        lowRange = 0; 
        highRange = 50;
      }
    }
    if (analogRead(BUTTON_PIN) >= lowRange && analogRead(BUTTON_PIN) < highRange)
    {
      _delay_ms(5);
      if (analogRead(BUTTON_PIN) >= lowRange && analogRead(BUTTON_PIN) < highRange)
      {
        while (analogRead(BUTTON_PIN) >= lowRange && analogRead(BUTTON_PIN) < highRange)
        {
        }
        pressed = true;
      }
    }
  }
  return (pressed) ? button : NO_BUTTON;
}

Credits

John Bradnam

John Bradnam

141 projects • 167 followers
Thanks to robaux.

Comments