Mirko Pavleski
Published © GPL3+

Single LED Matrix Arduino Flip Clock

A simple to build LED matrix flip clock.

BeginnerFull instructions provided2,402
Single LED Matrix Arduino Flip Clock

Things used in this project

Hardware components

Arduino Nano R3
Arduino Nano R3
×1
DS3231M - ±5ppm, I2C Real-Time Clock
Maxim Integrated DS3231M - ±5ppm, I2C Real-Time Clock
×1
8x8 Led matrix with MAX7219 driver
×1
RGB Diffused Common Cathode
RGB Diffused Common Cathode
×1
Resistor 220 ohm
Resistor 220 ohm
×2
Rotary Encoder with Push-Button
Rotary Encoder with Push-Button
×1

Software apps and online services

Arduino IDE
Arduino IDE

Hand tools and fabrication machines

Soldering iron (generic)
Soldering iron (generic)

Story

Read more

Schematics

schematic

Code

code

Arduino
//*****************************************************************************************//
//                                      MATRIX CLOCK
//                                 Adrian Jones, March 2014
//
//    - allows left-to-right and top-to-bottom scrolling
//
//*****************************************************************************************//

#include <Wire.h>             // I2C-WIRE library
#include <RTClib.h>           // RTC-Library

// define max7219 registers and control pins
#define max7219_reg_noop        0x00
#define max7219_reg_digit0      0x01
#define max7219_reg_digit1      0x02
#define max7219_reg_digit2      0x03
#define max7219_reg_digit3      0x04
#define max7219_reg_digit4      0x05
#define max7219_reg_digit5      0x06
#define max7219_reg_digit6      0x07
#define max7219_reg_digit7      0x08
#define max7219_reg_decodeMode  0x09
#define max7219_reg_intensity   0x0a
#define max7219_reg_scanLimit   0x0b
#define max7219_reg_shutdown    0x0c
#define max7219_reg_displayTest 0x0f
#define dataIn  12             // DIN
#define load    10             // CS 
#define clock   11             // CLK



char alphanum[][8] ={{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},    // blank  Hex 20  Dec 32
                     {0x10,0x10,0x10,0x10,0x10,0x10,0x00,0x10},    // ! 33
                     {0x00,0x28,0x28,0x28,0x00,0x00,0x00,0x00},    // " 34
                     {0x00,0x28,0x7C,0x28,0x7C,0x28,0x00,0x00},    // # 35
                     {0x10,0x38,0x50,0x38,0x14,0x54,0x38,0x10},    // $
                     {0x41,0xA2,0x44,0x08,0x10,0x22,0x45,0x82},    // %
                     {0x38,0x44,0x44,0x38,0x50,0x4A,0x44,0x3A},    // &
                     {0x08,0x08,0x00,0x00,0x00,0x00,0x00,0x00},    // '
                     {0x30,0x40,0x80,0x80,0x80,0x80,0x40,0x30},    // (  40
                     {0xC0,0x20,0x10,0x10,0x10,0x10,0x20,0xC0},    // )
                     {0x28,0x10,0xAA,0x54,0xAA,0x10,0x28,0x00},    // *
                     {0x00,0x10,0x10,0x10,0xFE,0x10,0x10,0x10},    // +
                     {0x00,0x00,0x00,0x00,0x00,0x08,0x08,0x10},    // ,
                     {0x00,0x00,0x00,0x00,0x7E,0x00,0x00,0x00},    // -
                     {0x00,0x00,0x00,0x00,0x00,0x00,0x18,0x18},    // .
                     {0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80},    // /
                     {0x7E,0xC1,0xA1,0x91,0x89,0x85,0x83,0x7E},    // 0
                     {0x10,0x30,0x10,0x10,0x10,0x10,0x10,0x7C},    // 1
                     {0x38,0x44,0x82,0x04,0x18,0x20,0x40,0xFE},    // 2   50
                     {0x7C,0x82,0x02,0x3C,0x02,0x02,0x82,0x7C},    // 3
                     {0x08,0x18,0x28,0x48,0xFE,0x08,0x08,0x08},    // 4
                     {0xFE,0x80,0xF8,0x04,0x02,0x82,0x44,0x38},    // 5
                     {0x38,0x44,0x80,0xB8,0xC4,0x82,0x44,0x38},    // 6
                     {0xFE,0x02,0x04,0x08,0x10,0x20,0x20,0x20},    // 7
                     {0x7C,0x82,0x82,0x7C,0x82,0x82,0x82,0x7C},    // 8
                     {0x7C,0x82,0x82,0x7E,0x02,0x82,0x44,0x38},    // 9
                     {0x00,0x00,0x18,0x18,0x00,0x18,0x18,0x00},    // :
                     {0x00,0x00,0x18,0x18,0x00,0x18,0x18,0x30},    // ;
                     {0x00,0x10,0x20,0x40,0x80,0x40,0x20,0x10},    // <  60
                     {0x00,0x00,0x00,0x7E,0x00,0x7E,0x00,0x00},    // =
                     {0x00,0x80,0x40,0x20,0x10,0x20,0x40,0x80},    // >
                     {0x70,0x88,0x88,0x10,0x20,0x20,0x00,0x20},    // ?
                     {0x7E,0x81,0x99,0xA1,0xA1,0x9E,0x80,0x7E},    // @
                     {0x3C,0x42,0x81,0x81,0xFF,0x81,0x81,0x81},    // A
                     {0xFC,0x82,0x81,0xFE,0x81,0x81,0x82,0xFC},    // B
                     {0x3C,0x42,0x81,0x80,0x80,0x81,0x42,0x3C},    // C
                     {0xFC,0x82,0x81,0x81,0x81,0x81,0x82,0xFC},    // D
                     {0xFE,0x80,0x80,0xFC,0x80,0x80,0x80,0xFE},    // E
                     {0xFE,0x80,0x80,0xFC,0x80,0x80,0x80,0x80},    // F  70
                     {0x3C,0x42,0x81,0x80,0x87,0x81,0x42,0x3C},    // G
                     {0x81,0x81,0x81,0xFF,0x81,0x81,0x81,0x81},    // H
                     {0xFE,0x10,0x10,0x10,0x10,0x10,0x10,0xFE},    // I
                     {0xFF,0x08,0x08,0x08,0x08,0x88,0x88,0x70},    // J
                     {0x88,0x90,0xA0,0xC0,0xA0,0x90,0x88,0x84},    // K
                     {0x80,0x80,0x80,0x80,0x80,0x80,0x80,0xFE},    // L
                     {0x81,0xC3,0xA5,0x99,0x81,0x81,0x81,0x81},    // M
                     {0x81,0xC1,0xA1,0x91,0x89,0x85,0x83,0x81},    // N
                     {0x3C,0x42,0x81,0x81,0x81,0x81,0x42,0x3C},    // O
                     {0xFC,0x82,0x81,0x82,0xFC,0x80,0x80,0x80},    // P  80
                     {0x3C,0x42,0x81,0x81,0x81,0x85,0x42,0x3D},    // Q
                     {0xFC,0x82,0x81,0x82,0xFC,0x84,0x82,0x81},    // R
                     {0x3C,0x42,0x81,0x40,0x3E,0x81,0x42,0x3C},    // S
                     {0xFE,0x10,0x10,0x10,0x10,0x10,0x10,0x10},    // T
                     {0x82,0x82,0x82,0x82,0x82,0x82,0x44,0x38},    // U
                     {0x82,0x82,0x82,0x82,0x82,0x44,0x28,0x10},    // V
                     {0x81,0x81,0x81,0x81,0x99,0xA5,0xC3,0x81},    // W
                     {0x81,0x42,0x24,0x18,0x18,0x24,0x42,0x81},    // X
                     {0x82,0x44,0x28,0x10,0x10,0x10,0x10,0x10},    // Y
                     {0xFF,0x02,0x04,0x08,0x10,0x20,0x40,0xFF},    // Z  90
                     {0xE0,0x80,0x80,0x80,0x80,0x80,0x80,0xE0},    // [ 
                     {0x80,0x40,0x20,0x10,0x08,0x04,0x02,0x01},    // 
                     {0x07,0x01,0x01,0x01,0x01,0x01,0x01,0x07},    // ]
                     {0xE0,0xA0,0xE0,0xA0,0xAA,0x15,0x15,0x11},    // am (coded as '^'
                     {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x7E},    // _
                     {0x10,0x08,0x00,0x00,0x00,0x00,0x00,0x00},    // '
                     {0x00,0x00,0x38,0x04,0x3C,0x44,0x48,0x34},    // a
                     {0x00,0x40,0x40,0x40,0x78,0x44,0x44,0x38},    // b
                     {0x00,0x00,0x18,0x24,0x40,0x40,0x24,0x18},    // c
                     {0x00,0x04,0x04,0x04,0x3C,0x44,0x44,0x38},    // d  100
                     {0x00,0x00,0x38,0x44,0x7C,0x40,0x44,0x38},    // e
                     {0x00,0x18,0x20,0x20,0x78,0x20,0x20,0x20},    // f
                     {0x00,0x38,0x44,0x44,0x38,0x04,0x44,0x38},    // g
                     {0x00,0x40,0x40,0x40,0x78,0x44,0x44,0x44},    // h
                     {0x00,0x00,0x40,0x00,0x40,0x40,0x40,0x40},    // i
                     {0x00,0x08,0x00,0x08,0x08,0x08,0x48,0x30},    // j
                     {0x00,0x40,0x40,0x48,0x50,0x60,0x50,0x48},    // k 
                     {0x00,0x40,0x40,0x40,0x40,0x40,0x40,0x20},    // l
                     {0x00,0x00,0x00,0x28,0x54,0x44,0x44,0x44},    // m
                     {0x00,0x00,0x00,0x38,0x44,0x44,0x44,0x44},    // n  110
                     {0x00,0x00,0x00,0x38,0x44,0x44,0x44,0x38},    // o 
                     {0x00,0x00,0x70,0x48,0x48,0x70,0x40,0x40},    // p
                     {0x00,0x00,0x30,0x48,0x48,0x38,0x08,0x08},    // q
                     {0x00,0x00,0x00,0x30,0x48,0x40,0x40,0x40},    // r
                     {0x00,0x30,0x48,0x40,0x30,0x08,0x48,0x30},    // s
                     {0x00,0x20,0x70,0x20,0x20,0x20,0x28,0x10},    // t
                     {0x00,0x00,0x44,0x44,0x44,0x44,0x44,0x38},    // u
                     {0x00,0x00,0x44,0x44,0x44,0x44,0x28,0x10},    // v
                     {0x00,0x00,0x82,0x82,0x82,0x92,0x54,0x28},    // w
                     {0x00,0x00,0x84,0x48,0x30,0x30,0x48,0x84},    // x  120
                     {0x00,0x48,0x48,0x48,0x38,0x08,0x48,0x30},    // y 
                     {0x00,0x00,0x00,0x7C,0x08,0x10,0x20,0x7C},    // z
                     {0x00,0x30,0x40,0x40,0x80,0x40,0x40,0x30},    // {
                     {0x00,0x20,0x20,0x20,0x20,0x20,0x20,0x20},    // |
                     {0x00,0x60,0x10,0x10,0x08,0x10,0x10,0x60},    // }
                     {0xE0,0xA0,0xE0,0x80,0x8A,0x15,0x15,0x11}     // pm codes as '~'  Hex 7E, Dec 126
                   };




// define RTC operation
RTC_DS1307 RTC;               // Tiny RTC (DS1307) module (SDA - A4, SCL - A5)

// rotary encoder, switch and LED control
#define enc_PinA   2          // encoder A to pin 2  (interrupt 0)
#define enc_PinB   4          // encoder B to pin 4
#define enc_Switch 3          // encoder switch to pin 3 (interrupt 1)
#define mode_Pin   8          // mode LED pin
#define min_Pin    9          // minute LED pin

unsigned char enc_A, enc_B, enc_A_prev=0;

static boolean rotating  = false;
static boolean clockwise = false;
static boolean updateFlag= false;
static int mode = 0;          // 0 - nothing, 1 - hour set, 2 - min set

// define display strings
#define max_array_size 100
char ac[max_array_size] = {};
byte rc[8] = {};
String display_message = "";
int arraylen;


// operational parameters
#define delay_line 75       // ms between line shifts
#define delay_char 400       // ms between characters
#define delay_mess 500       // ms between messages
#define cblanks 1            // number of blank lines between characters
#define eblanks 0            // number of additional blank lines (above 8) at the end of the message

// display features
static boolean top2bottom  = false;      // display direction (top to bottom, or right to left
static boolean hour24      = false;      // 24 hour display?
static boolean charHI      = true;       // highlight whole character

static boolean doSerial = true;           // serial output?


//*****************************************************************************************//
//                                      Initial Setup
//*****************************************************************************************//

void setup () {
  Wire.begin();
  
  Serial.begin(57600);
  if(doSerial) Serial.print("MATRIX Clock - Adrian Jones, Mar. 2014");
  
  // 8x8 LED matrix control pins
  pinMode(dataIn, OUTPUT);
  pinMode(clock,  OUTPUT);
  pinMode(load,   OUTPUT);
  initMatrix();                            // initialize LED matrix

  // LED pins
  pinMode(mode_Pin, OUTPUT);               // mode pin
  digitalWrite(mode_Pin, 1);
  pinMode(min_Pin, OUTPUT);                // minute pin
  digitalWrite(min_Pin, 1);
  
  // encoder control
  pinMode(enc_PinA, INPUT_PULLUP);  digitalWrite(enc_PinA,   HIGH);      // rotary encoder pin A
  pinMode(enc_PinB, INPUT_PULLUP);  digitalWrite(enc_PinB,   HIGH);      // rotary encoder pin B
  pinMode(enc_Switch, INPUT_PULLUP);digitalWrite(enc_Switch, HIGH);      // encoder switch
  
  attachInterrupt(0, rotEncoder, CHANGE);    // time setting
  attachInterrupt(1, swEncoder,  CHANGE);    // mins / hours

  // RTC  
  RTC.begin();
  if (! RTC.isrunning()) {   
    RTC.adjust(DateTime(__DATE__, __TIME__));
    if(doSerial) Serial.println("  (RTC reset)");
  } else {
    if(doSerial) Serial.println("  (RTC running)");
  }
  
}  

//*****************************************************************************************//
//                                      Main Loop
//*****************************************************************************************//

void loop () {

    DateTime now = RTC.now();   // 
    show_time_and_date(now);                    // display time
    display_message = createMessage(now);
    arraylen = initDisplayString(display_message);
    
    if(updateFlag) {
      show_time_and_date(now);
      updateFlag = false;  
    }

    while(rotating) {
      delay(1);                                   // debounce
      adjTime(now, clockwise); 
      show_time_and_date( RTC.now() );
      display_message = createMessage(now);
      arraylen = initDisplayString(display_message);
      delay(1);
      rotating = false;       // Reset the interrupt flag back to false
    }
    delay(5);

    for (int i = 0; i < (arraylen-7); i++) {                       // loops through message array, advancing one byte at a time
      for (int j = 1; j < 9; j++) { maxSingle(j,ac[i+8-j]);  }     // row 1 gets ac[i+8], row 2 gets ac[i+7] etc... row 8 gets ac[i+0]
      if(i%(8+cblanks) == 0) {                                     // when there is a complete character on the display...
          if(charHI) maxSingle(max7219_reg_intensity, 0x01);       // ... increase brightness and temporary halt
          newDelay(delay_char);
      } else {                                                     // normal brightness
          maxSingle(max7219_reg_intensity, 0x00);
          newDelay(delay_line);
      }
    }
    if(mode == 0) newDelay(delay_mess);
}

// ********************************************************************************** //
//                                      INTERRUPT ROUTINES
// ********************************************************************************** //

// function rotEncoder(): ISR called when encoder rotated
void rotEncoder(){
  delay(1);
  enc_A = digitalRead(enc_PinA); 
  enc_B = digitalRead(enc_PinB);
  if(!enc_A && enc_A_prev){      // change of state
    clockwise = (!enc_A && enc_B)? true : false;
    if(mode != 0) rotating = true;
  } 
  enc_A_prev = enc_A;     // Store value of A for next time    
}

// function swEncoder(): ISR called when encoder button pushed
void swEncoder(){
  delay(1);
  if(digitalRead (enc_Switch) != LOW) return;  // if switch depressed
  delay(1);                                    // debounce  
  if(digitalRead (enc_Switch) != LOW) return;  // if switch still depressed
  mode++; mode = mode % 3;                     // increment mode
  digitalWrite(mode_Pin, !(mode == 1));        // hour adjust LED
  digitalWrite(min_Pin,  !(mode == 2));        // minute adjust LED
  updateFlag = true;
}

// ********************************************************************************** //
//                                      OPERATION ROUTINES
// ********************************************************************************** //

// function newDelay
void newDelay (int dly) {
  for (int z=1; z< dly; z++) {
    delay(1);
    if(rotating || updateFlag) break;
  }
}



// function initMatrix()  :   initialization of the MAX7219 registers
void initMatrix() {
      maxSingle(max7219_reg_scanLimit, 0x07);       // all 8 columns being used
      maxSingle(max7219_reg_decodeMode, 0x00);      // set to LED matrix (not 7 seg. digit)
      maxSingle(max7219_reg_shutdown, 0x01);        // not in shutdown mode
      maxSingle(max7219_reg_displayTest, 0x00);     // not in display test
      for (int e=1; e<=8; e++) {maxSingle(e,0); }   // clear LED registers (turn all LEDs off)
      maxSingle(max7219_reg_intensity, 0x00);       // set intensity. Range: 0x00 to 0x0f 
}

// function adjTime(): increments/decrements (based encoder direction) hours/mins (depending on mode)
void adjTime(DateTime now, boolean dir) {
  if(mode == 1) {    // adjust hours
    int adj_hrs = now.hour();
    if(dir) {        // increment
       if(++adj_hrs >= 25) adj_hrs = 1;
    } else {         // decrement
      if(adj_hrs == 0) adj_hrs = 24;
      if(--adj_hrs <= 0) adj_hrs = 24;
    }
    RTC.adjust(DateTime(now.year(), now.month(), now.day(), adj_hrs, now.minute(), now.second() ));
  } 
  
  if(mode == 2) {    // adjust minutes
    int adj_mins = now.minute();
    if(dir) {   
      if(++adj_mins >= 60) adj_mins = 0; 
    } else {
      if(--adj_mins < 0) adj_mins = 59; 
    }
    RTC.adjust(DateTime(now.year(), now.month(), now.day(), now.hour(), adj_mins, now.second() ));
  }
}


// function rotChar(char): for character char, transposes bits 90 deg. (top - bottom ==> left - right)
// and stores results in rc[0] - rc[7].
byte rotChar(char inLetter) {
   int ind = int(inLetter) - 0x20;
   for (int col = 0; col < 8; col++) {
     byte mask = 0x01 << (7-col);
     for (int row = 0; row < 8 ; row++) { bitWrite(rc[col], 7-row,(alphanum[ind][row] & mask)); }
   }
}  


// function show_time_and_date:  print out time string & bytes
void show_time_and_date(DateTime datetime){
  if(doSerial) {
    int minutes = datetime.minute();
    int hours   = datetime.hour(); if(hours==0) hours=24;
    int seconds = datetime.second();
    char delim = '/'; char dend = ' ';
    String te = "Current date/time: ";
    te = te + datetime.year() + delim + datetime.month() + delim + datetime.day() + dend;

    Serial.print(te);
    if(hours<10) Serial.print(0); Serial.print(hours,DEC);
    Serial.print(":");
    if(minutes<10) Serial.print(0); Serial.print(minutes,DEC);
    Serial.print(":");
    if(seconds < 10) Serial.print(0); Serial.print(seconds,DEC);
    Serial.println("");
  } 
}


String createMessage(DateTime datetime) {
  String new_mess = " ";
  int hr = datetime.hour()%24;    if(hr == 0) hr = 24;
  int mn = datetime.minute();
  if(mode == 0) {                  // Normal mode
    if(hour24) { 
      new_mess += hr;
    } else {
      new_mess += (hr > 12)? hr - 12 : hr;
    }
    new_mess += ':';
    if(mn < 10) new_mess += '0'; new_mess += mn;
    if(!hour24) new_mess += (hr > 12)? "~" : "^";
  }
  
  if(mode == 1) {                  // Adjusting hours
    new_mess += hr;
  }  
  
  if(mode == 2) {                 // Adjusting minutes
    if(mn < 10) new_mess += '0'; new_mess += mn;
  }
  return new_mess;
}


// function initDisplayString()  :   creates array of message string with blanks between characters and at end
int initDisplayString(String message) {
  int x = 0;
  for (int y = 0; y < message.length(); y++ ){
    char thisCh = message.charAt(y);
    int ind = int(thisCh) - 0x20;
    if(!top2bottom) rotChar(thisCh);
    for (int row = 0; row < 8 + cblanks; row++) {
      if (row <= 7) {
        ac[x] = (top2bottom)? alphanum[ind][7-row] : rc[row];
      } else {
         ac[x] = 0;
      }
      x++;
    }
  }

  for(int y = 0; y < 7+eblanks; y++) {ac[x] = 0; x++; }    // end blanks
  return x;
}


// function maxSingle()  :   loads data into register 
void maxSingle(byte reg, byte col) {      
  digitalWrite(load, LOW);       // begin    
  putByte(reg);                    // specify register
  putByte(col);                    // ((data & 0x01) * 256) + data >> 1); // put data  
  digitalWrite(load,HIGH);
}


// function putByte()  :   loads data to matrix, MSB to LSB 
void putByte(byte data) {
  byte i = 8;
  byte mask;
  while(i > 0) {                 // MSB to LSB
    mask = 0x01 << (i - 1);      // create bitmask
    digitalWrite(clock, LOW);   // tick
    if (data & mask){            // choose bit
      digitalWrite(dataIn, HIGH);// send 1
    } else {
      digitalWrite(dataIn, LOW); // send 0
    }
    digitalWrite(clock, HIGH);   // tock
    --i;                         // move to lesser bit
   }
}

Credits

Mirko Pavleski

Mirko Pavleski

4 projects • 125 followers

Comments