The Matrixclock project

This little project was born to build a present for my son’s birthday, I had a couple of components laying around and I decided to make something new.

Goals

  • build an easy-to-use alarm clock with a personal touch
  • use only components I already had in stock
  • use as few components as possible
  • low power consumption
  • learn to solder some SOIC components

Design decisions

  • project a 3.3V circuit
  • use a 64 led matrix for the display…
  • a rotary encoder with integrated pushbutton for the controls
  • an RGB led to change the clock color while the time passes (60′ cicle)
  • sometimes print some random messages addressed to my son
  • play a small song once every hour at his birthday
  • play a custom alarm music
  • an LDR sensor to adjust display and RGB brightness in the dark
The project ended up using:
  • ATMEGA328P-PU for the MCU, clocked @ 8MHz using internal oscillator and running @ 3.3V
  • DS1388 SOIC for the RTC (unfortunately DS1307 doesn’t run @ 3.3V)
  • an 8OHM speaker for the sound
  • an AS1107  SOIC to drive the matrix (MAX7219 does not officialy run @ 3.3V even if it seemed ok to me when I tested it on the breadboard)
Here is the scheme: Matrixclock Scheme

And my beloved jungle-breadboard 🙂

20121215_002

Building the board

Placing components was the most difficult and tedious work, I had to keep the board small and I only had 100x70mm boards available. Matrixclock board This was my first project using soic components, initially I was a bit scared about that, but it wasn’t too hard after all. I used the toner-transfer technique to prepare the PCB: 20121220_001 … and HCL + H2O2 to etch the board: 20121220_005   And finally: some more photos on Flickr and a short and ugly video showing the clock running.  

Follow the source Luke! (The code)

The code is very patchy but Just Works (TM) , I used a couple of Arduino libraries and I merged the Pong game I’ve already written as a standalone sketch.  
/**
* Matrix Clock
*/

#define DEBUG_CLOCK 0
#define ENABLE_RGB  1 /* rgb led */


// Pins
#define ENCODER_PIN_1   2 // INT0
#define ENCODER_PIN_2   3 // INT1
#define SET_PIN         4 // digital pull up, connected to ENCODER push button
#define LC_PIN_1        8
#define LC_PIN_2        7
#define LC_PIN_3        6
#define ALM_SWITCH_PIN  A3
#define BUZZER_PIN      5 // digital (PWM?) -> BUZZER -> GND
#define RED_PIN         9
#define GREEN_PIN       10
#define BLUE_PIN        11
#define LDR_PIN         A2

// Includes
#include "LedControl.h"
#include "8x8Font.h"
#include "3x5Font.h"
#include <EEPROM.h>
#include "EEPROMAnything.h"
#include <Wire.h>
#include "RTClib.h"
#include "Timer.h"
#include <Encoder.h>
#include "note.h"
#include "messages.h"

#define NO_PORTB_PINCHANGES // to indicate that port b will not be used for pin change interrupts
#define NO_PORTC_PINCHANGES // to indicate that port c will not be used for pin change interrupts
// if there is only one PCInt vector in use the code can be inlined
// reducing latency and code size
// define DISABLE_PCINT_MULTI_SERVICE below to limit the handler to servicing a single interrupt per invocation.
#define       DISABLE_PCINT_MULTI_SERVICE
#include <PinChangeInt.h>

// FSM status
#define CLK_IDLE        0
#define CLK_PLAY        1
#define CLK_SET_ALM_H   2
#define CLK_SET_ALM_M   3
#define CLK_SET_TIM_H   4
#define CLK_SET_TIM_M   5
#define CLK_SET_TIM_S   6
#define CLK_SET_DAT_D   7
#define CLK_SET_DAT_M   8
#define CLK_SET_DAT_Y   9
#define CLK_SET_SPEED   10
#define CLK_SET_LAST    10 // Must be the last: check main code for status changes.

#define POS_CENTER      1
#define POS_LEFT        0
#define POS_RIGHT       2
#define SYM_ALM         0
#define SYM_DATE        1
#define SYM_TIME        2
#define SYM_SETUP       3

// Config
#define BIRTHDAY_M      4
#define BIRTHDAY_D      12
#define CHAR_SPEED      20

// Random message every
#define RANDOM_MSG_EVERY 6 * 60 * 1000 // 13 minutes
#define PRINT_DATE_EVERY_CICLES 5

// Alarm FSM
#define ALM_PLAY        1
#define ALM_STOP        0
#define ALM_CLEARED     2

// Pong
#define PADSIZE 3
#define BALL_DELAY 100
#define GAME_DELAY 9
#define BOUNCE_VERTICAL 1
#define BOUNCE_HORIZONTAL -1

#define HIT_NONE 0
#define HIT_CENTER 1
#define HIT_LEFT 2
#define HIT_RIGHT 3


// Timers
#define TIMEOUT         5000 // millis
#define DELAYED_TIMEOUT 20000 // millis
#define DEBOUNCE        200 // millis between pulses
#define ALM_RESET_AFTER_MINUTES     2 // timer for alarm reset


// Ugly globals...
uint8_t timeH;
uint8_t timeM;
volatile uint8_t timeS;
uint8_t dateD;
uint8_t dateM;
uint8_t dateY;
uint8_t dateW;
uint8_t almH;
uint8_t almM;
//int almDays;
uint8_t brightness = 8;
const uint8_t eeprom_id = 0x99;
DateTime now;
uint8_t print_date;
uint8_t char_speed = CHAR_SPEED;

// store clock status
int clockStatus = CLK_IDLE;


// Used in ISR, keep one-byte-long to avoid disabling INTs

byte alarmStatus = ALM_STOP;
volatile boolean setButtonPressed = false;
volatile unsigned long setButtonBounceTime=0; // variable to hold ms count to debounce a pressed switch

int oldStatus = CLK_IDLE;

// Timers
Timer timer;
volatile int idleTimer = 1000; // set to out of range value
volatile int alarmTimer = 1000;
volatile int storeConfigTimer = 1000;
volatile int ballTimer = 1000;

// Pong
byte direction; // Wind rose, 0 is north
int xball;
int yball;
int yball_prev;
int xpad;


// Sound
//char noteNames[] =     {'C','D','E','F','G','a','b'};
//unsigned int frequencies[] = {262,294,330,349,392,440,494};
//const byte noteCount = sizeof(noteNames); // the number of notes
                                          // (7 in this example)

//notes, a space represents a rest
// char song[] PROGMEM = "CCGGaaGFFEEDDC GGFFEEDGGFFEED CCGGaaGFFEEDDC ";
unsigned int alarm_song[] PROGMEM = {NOTE_C4, NOTE_C4, NOTE_G4, NOTE_G4, NOTE_A4, NOTE_A4, NOTE_G4, SILENCE, NOTE_F4, NOTE_F4, NOTE_E4, NOTE_E4, NOTE_D4, NOTE_D4, NOTE_C4, SILENCE, SILENCE, NOTE_G4, NOTE_G4, NOTE_F4, NOTE_F4, NOTE_E4, NOTE_E4,  NOTE_D4, SILENCE, NOTE_G4, NOTE_G4, NOTE_F4, NOTE_F4, NOTE_E4, NOTE_E4, NOTE_D4, SILENCE, SILENCE, NOTE_C4, NOTE_C4, NOTE_G4, NOTE_G4, NOTE_A4, NOTE_A4, NOTE_G4, SILENCE, NOTE_F4, NOTE_F4, NOTE_E4, NOTE_E4, NOTE_D4, NOTE_D4, NOTE_C4, SILENCE, END_SONG};

//do do re do fa mi   do do re do sol fa  do do do# la fa mi re   do do re do fa mi
// # G G A G C B | G G A G D C | G G + G E C B A | F F E C D C
unsigned int birthday_song[] PROGMEM = {NOTE_G4, NOTE_A4, NOTE_G4, NOTE_C5, NOTE_B4, SILENCE, NOTE_G4, NOTE_A4, NOTE_G4, NOTE_D5, NOTE_C5, SILENCE, NOTE_G4, NOTE_G5, NOTE_E5, NOTE_C5, NOTE_B4, NOTE_A4, SILENCE, NOTE_F4, NOTE_E4, NOTE_C4, NOTE_D4, NOTE_C4, SILENCE, END_SONG};

// Encoder
int encoderCount = 0;
Encoder myEnc(ENCODER_PIN_2, ENCODER_PIN_1);


#if DEBUG_CLOCK

RTC_DS1307 RTC;


// variables created by the build process when compiling the sketch
extern int __bss_end;
extern void *__brkval;
// function to return the amount of free RAM
int memoryFree()  {
    int freeValue;
    if((int)__brkval == 0)
        freeValue = ((int)&freeValue) - ((int)&__bss_end);
    else
        freeValue = ((int)&freeValue) - ((int)__brkval);
    return freeValue;
}
#else

RTC_DS1388 RTC;

#endif


LedControl lc = LedControl(LC_PIN_1, LC_PIN_2, LC_PIN_3,1);

char char_buffer[8];
char char_buffer2[8];
char buf[64];

#if ENABLE_RGB

void setColor(int red, int green, int blue)
{
    analogWrite(RED_PIN, 255-red);
    analogWrite(GREEN_PIN, 255-green);
    analogWrite(BLUE_PIN, 255-blue);
}

//Convert a given HSV (Hue Saturation Value) to RGB(Red Green Blue) and set the led to the color
//  h is hue value, integer between 0 and 360
//  s is saturation value, double between 0 and 1
//  v is value, double between 0 and 1
//http://splinter.com.au/blog/?p=29
void setColorHsv(int h, double s, double v)
{
    byte rgb[3];

    // Make sure our arguments stay in-range
    h = max(0, min(360, h));
    s = max(0, min(1.0, s));
    v = max(0, min(1.0, v));
    if(s == 0)
    {
        // Achromatic (grey)
        rgb[0] = rgb[1] = rgb[2] = round(v * 255);
    } else {
        double hs = h / 60.0; // sector 0 to 5
        int i = floor(hs);
        double f = hs - i; // factorial part of h
        double p = v * (1 - s);
        double q = v * (1 - s * f);
        double t = v * (1 - s * (1 - f));
        double r, g, b;
        switch(i)
        {
                case 0:
                        r = v;
                        g = t;
                        b = p;
                        break;
                case 1:
                        r = q;
                        g = v;
                        b = p;
                        break;
                case 2:
                        r = p;
                        g = v;
                        b = t;
                        break;
                case 3:
                        r = p;
                        g = q;
                        b = v;
                        break;
                case 4:
                        r = t;
                        g = p;
                        b = v;
                        break;
                default: // case 5:
                        r = v;
                        g = p;
                        b = q;
        }
        rgb[0] = round(r * 255.0);
        rgb[1] = round(g * 255.0);
        rgb[2] = round(b * 255.0);
    }
    setColor(rgb[0], rgb[1], rgb[2]);
}



void setColorHueValue(int hueValue, uint8_t brightness)
{
    setColorHsv(hueValue, 1, constrain((double)brightness/15, 0.1, 1));
}

#endif


/*
* Play a note for a given time
*
*
void playNote(char note, int duration)
{
  // play the tone corresponding to the note name
  for (int i = 0; i < noteCount; i++)
  {
    // try and find a match for the noteName to get the index to the note
    if (noteNames[i] == note) // find a matching note name in the array
      //  play the note using the frequency:
      tone(BUZZER_PIN, frequencies[i], duration);
  }
  // if there is no match then the note is a rest, so just do the delay
  delay(duration);
}
*/

/**
* Play a note for a given time
*
*/
void playFrequency(unsigned int note, int duration)
{
    if(note != SILENCE){
        tone(BUZZER_PIN, note, duration);
    }
    delay(duration);
}


/**
* Set IDLE status
*/
void setIdle()
{
    clockStatus = CLK_IDLE;
}


/**
 * Get encoder move
 */
void readEncoder(){
    encoderCount = myEnc.read()/4;
    if(encoderCount){
        myEnc.write(0);
    }
}


/**
 * String message handling
 */


char reverseByte(char b) {
   b = (b & 0xF0) >> 4 | (b & 0x0F) << 4;
   b = (b & 0xCC) >> 2 | (b & 0x33) << 2;
   b = (b & 0xAA) >> 1 | (b & 0x55) << 1;
   return b;
}

void setSprite(char* font){
    for(int r = 0; r < 8; r++){
        lc.setRow(0, r, reverseByte(*(font + r)));
    }
}

void load_char(char i, char* c){
    for(int r = 0; r < 8; r++){
        c[r] = pgm_read_byte((char*)font_data + (i * 8) + r);
    }
}

void merge_char(char* c1, char* c2, char pos){
    for(int r = 0; r < 8; r++){
        c1[r] = (c1[r] << 1) | (c2[r] >> (8 - pos)) ;
    }
}

void print_string(char* s){
    load_char(32, char_buffer);
    bool valid = 1;
    while (valid){
        setSprite(char_buffer);
        load_char(*s ? *s : 32, char_buffer2);
        for(int r = 1; r <= 8; r++){
            if(setButtonPressed){
                return;
            }
            merge_char(char_buffer, char_buffer2, r);
            setSprite(char_buffer);
            delay(char_speed);
        }
        valid = (char) *s;
        s++;
    }
}

void load_digits(char* c, uint8_t digits, uint8_t pos, uint8_t sym){
    for(uint8_t i=0; i<5; i++){
        c[i] = pgm_read_byte((char*)small_font_data + digits / 10 * 5 + i) << 4 | pgm_read_byte((char*)small_font_data + digits % 10 * 5 + i);
    }
    c[5] = 0;
    uint8_t shift = constrain(6 - 3 * pos, 0, 5);
    switch(sym){
        case SYM_ALM:
            c[6] = B00000010 << shift;
            c[7] = B00000101 << shift;
        break;
        case SYM_TIME:
            c[6] = B00000111 << shift;
            c[7] = B00000010 << shift;
        break;
        case SYM_DATE:
            c[6] = B00000111 << shift;
            c[7] = B00000101 << shift;
        break;
        case SYM_SETUP:
            c[6] = 0;
            c[7] = B00000111 << shift;
        break;
    }
}

/**
 * Pong game
 */
void newGame() {
    lc.clearDisplay(0);
    // initial position
    xball = random(1, 7);
    yball = 1;
    direction = random(3, 6); // Go south
    load_char(1, char_buffer);
    setSprite(char_buffer);
    delay(700);
    lc.clearDisplay(0);
}


void setPad() {
    readEncoder();
    xpad = constrain(xpad + encoderCount, 0, 8 - PADSIZE);
}


/**
* Checks if the ball is in contact with the walls
*/
int checkBounce() {
    if(!xball || !yball || xball == 7 || yball == 6){
        int bounce = (yball == 0 || yball == 6) ? BOUNCE_HORIZONTAL : BOUNCE_VERTICAL;
        return bounce;
    }
    return 0;
}

int getHit() {
    if(yball != 6 || xball < xpad || xball > xpad + PADSIZE){
        return HIT_NONE;
    }
    if(xball == xpad + PADSIZE / 2){
        return HIT_CENTER;
    }
    return xball < xpad + PADSIZE / 2 ? HIT_LEFT : HIT_RIGHT;
}

bool checkLoose() {
    return yball == 6 && getHit() == HIT_NONE;
}

void hitSound(){
    playFrequency(NOTE_B4, 2);
}

void moveBall() {
    int bounce = checkBounce();
    if(bounce) {
        switch(direction){
            case 0:
                direction = 4;
            break;
            case 1:
                direction = (bounce == BOUNCE_VERTICAL) ? 7 : 3;
            break;
            case 2:
                direction = 6;
            break;
            case 6:
                direction = 2;
            break;
            case 7:
                direction = (bounce == BOUNCE_VERTICAL) ? 1 : 5;
            break;
            case 5:
                direction = (bounce == BOUNCE_VERTICAL) ? 3 : 7;
            break;
            case 3:
                direction = (bounce == BOUNCE_VERTICAL) ? 5 : 1;
            break;
            case 4:
                direction = 0;
            break;
        }
    }

    // Check hit
    switch(getHit()){
        case HIT_LEFT:
            if(direction == 0){
                direction =  7;
            } else if (direction == 1){
                direction = 0;
            }
            hitSound();
        break;
        case HIT_RIGHT:
            if(direction == 0){
                direction = 1;
            } else if(direction == 7){
                direction = 0;
            }
            hitSound();
        break;
        case HIT_CENTER:
            hitSound();
        break;
    }

    // Check orthogonal directions and borders ...
    // should never happen
    if((direction == 0 && xball == 0) || (direction == 4 && xball == 7)){
        direction++;
    }
    if(direction == 0 && xball == 7){
        direction = 7;
    }
    if(direction == 4 && xball == 0){
        direction = 3;
    }

    if(direction == 2 && yball == 0){
        direction = 3;
    }
    if(direction == 2 && yball == 6){
        direction = 1;
    }
    if(direction == 6 && yball == 0){
        direction = 5;
    }
    if(direction == 6 && yball == 6){
        direction = 7;
    }

    // Corner case:
    if(xball == 0 && yball == 0){
        direction = 3;
    }
    if(xball == 0 && yball == 6){
        direction = 1;
    }
    if(xball == 7 && yball == 6){
        direction = 7;
    }
    if(xball == 7 && yball == 0){
        direction = 5;
    }


    yball_prev = yball;
    if(2 < direction && direction < 6) {
        yball++;
    } else if(direction != 6 && direction != 2) {
        yball--;
    }
    if(0 < direction && direction < 4) {
        xball++;
    } else if(direction != 0 && direction != 4) {
        xball--;
    }
    xball = max(0, min(7, xball));
    yball = max(0, min(6, yball));
}

void gameOver() {
    load_char(33, char_buffer);
    setSprite(char_buffer);
    delay(1500);
    lc.clearDisplay(0);
}


void drawGame() {
    if(yball_prev != yball){
        lc.setRow(0, yball_prev, 0);
    }
    lc.setRow(0, yball, byte(1 << (xball)));
    byte padmap = byte(0xFF >> (8 - PADSIZE) << xpad) ;
    lc.setRow(0, 7, padmap);
}

void initGame(){
  randomSeed(analogRead(0));
  newGame();
  ballTimer = timer.every(BALL_DELAY, moveBall);

}

void playGame(){
    while(!setButtonPressed){
        timer.update();
        setPad();
        drawGame();
        if(checkLoose()) {
            gameOver();
            newGame();
        }
        delay(GAME_DELAY);
    }
    timer.stop(ballTimer);
}




/**
* Wake up effect, this is not returning
* check for status from button ISR
*
*/
void wakeUp(const unsigned int* song)
{
    while (pgm_read_word(song) != END_SONG)
    {
        if(alarmStatus != ALM_PLAY || setButtonPressed){
            alarmStatus = ALM_CLEARED;
            setButtonPressed = false;
            return;
        }
        playFrequency(pgm_read_word(song++), 333); // play the note
    }
    delay(4000); // wait four seconds before repeating the song
}


/**
 * Birthday
 */
void check_birthday(){
    unsigned int *song;
    song = birthday_song;
    if(dateD == BIRTHDAY_D && dateM == BIRTHDAY_M && timeH > 9 && timeH < 21){
        load_char(3, char_buffer);
        setSprite(char_buffer);
        while (pgm_read_word(song) != END_SONG)
        {
            playFrequency(pgm_read_word(song++), 333); // play the note
        }
        delay(4000);
    }
}

/**
* Turn off alarm
*/
void stopAlarm(){
    alarmStatus = ALM_CLEARED;
    //digitalWrite(BUZZER_PIN, LOW);
    //digitalWrite(ALM_LED_PIN, LOW);
}


/**
* ISR on FALLING push button
*/
void setSetButtonPressed(){
    // this is the interrupt handler for button presses
    // it ignores presses that occur in intervals less then the bounce time
    if (abs(millis() - setButtonBounceTime) > DEBOUNCE)
    {
        setButtonPressed = true;
        setButtonBounceTime = millis();  // set whatever bounce time in ms is appropriate
    }
}


/**
* Changes clock status
*/
void changeStatus()
{
    clockStatus++;
    if(clockStatus > CLK_SET_LAST){
        setIdle();
    }
    // Start timer
    timer.stop(idleTimer);
    idleTimer = timer.after(TIMEOUT, setIdle);
    setButtonPressed = false;
}

/**
 * Store config to EEPROM
 */
void store_config(){
    // Read EEPROM
    int write = 0;
    write += EEPROM_writeAnything(write, eeprom_id);
    write += EEPROM_writeAnything(write, char_speed);
    write += EEPROM_writeAnything(write, almH);
    EEPROM_writeAnything(write, almM);
}

/**
 * Delayed store config
 */
void store_config_delayed(){
    timer.stop(storeConfigTimer);
    storeConfigTimer = timer.after(DELAYED_TIMEOUT, store_config);
}

/**
 * Adjust clock
 */
void storeDateTime(){
    RTC.adjust(DateTime(dateY, dateM, dateD, timeH, timeM, timeS));
}

/**
 * Adjust brightness
 */
void adjustBrightness(){
    brightness = map(analogRead(LDR_PIN), 0, 1023, 0, 15);
    lc.setIntensity(0, brightness);
#if ENABLE_RGB
    setColorHueValue((uint16_t)timeM * 6, brightness);
#endif

}

/**
 *  Random message
 */
void randomMessage(){
    if(clockStatus == CLK_IDLE){
        strcpy_P(buf, (char*)pgm_read_word(&(messages[random(0, sizeof(messages)/sizeof(char *))])));
        print_string(buf);
    }
}

/**
 * Setup
 */
void setup() {

    // Read Alm from EEPROM
    // Read EEPROM
    if(EEPROM.read(0) == eeprom_id){
        int read = 1;
        read += EEPROM_readAnything(read, char_speed);
        read += EEPROM_readAnything(read, almH);
        EEPROM_readAnything(read, almM);
    } else {
        store_config();
    }

    pinMode(LDR_PIN, INPUT);
    adjustBrightness();

    // LCD
    // The MAX72XX is in power-saving mode on startup,
    // we have to do a wakeup call
    lc.shutdown(0,false);
    /* Set the brightness to a medium values range (0-15) */
    lc.setIntensity(0, brightness);
    /* and clear the display */
    lc.clearDisplay(0);

    // RTC
    Wire.begin();
    RTC.begin();
    if (! RTC.isrunning()) {
        // following line sets the RTC to the date & time this sketch was compiled
        RTC.adjust(DateTime(__DATE__, __TIME__));
    }

    // Encoder & push
    pinMode(SET_PIN, INPUT);
    digitalWrite(SET_PIN, HIGH);
    PCintPort::attachInterrupt(SET_PIN, &setSetButtonPressed, FALLING);

    // Alm enable pin
    pinMode(ALM_SWITCH_PIN, INPUT);
    digitalWrite(ALM_SWITCH_PIN, HIGH); // pull up
    pinMode(BUZZER_PIN, OUTPUT);

    timer.every(1000, adjustBrightness);
    timer.every(RANDOM_MSG_EVERY, randomMessage);

    print_date = 0;
}


/**
 * Loop
 */
void loop() {
    if(setButtonPressed && alarmStatus != ALM_PLAY){
        changeStatus();
    }

    // Read encoder
    readEncoder();

    // Read clock
    now = RTC.now();
    timeH = now.hour();
    timeM = now.minute();
    timeS = now.second();
    dateD = now.day();
    dateM = now.month();
    dateY = now.yoff();

    boolean alarmSwitchClosed;

    alarmSwitchClosed = (digitalRead(ALM_SWITCH_PIN) == LOW); // pull-up, closed is low

    // Check alarm, buttons pressed stop alarm
    int minutes_after_alarm;
    minutes_after_alarm = ((int)timeH * 60 + (int)timeM) - ((int)almH * 60 + (int)almM);


    if(clockStatus == CLK_IDLE && alarmSwitchClosed && alarmStatus == ALM_STOP && minutes_after_alarm == 0){
        alarmStatus = ALM_PLAY;
    }

    if(alarmStatus == ALM_PLAY){
        if(clockStatus == CLK_IDLE && alarmSwitchClosed && !setButtonPressed){
            load_char(13, char_buffer);
            setSprite(char_buffer);
            wakeUp(alarm_song);
        } else {
            stopAlarm();
        }
    }

    if(clockStatus == CLK_PLAY){
        timer.stop(idleTimer);
        initGame();
        playGame();
        idleTimer = timer.after(TIMEOUT, setIdle);
    }

    if(clockStatus > CLK_PLAY){
        uint8_t digits;
        uint8_t pos;
        uint8_t sym;
        switch(clockStatus) {
            case CLK_SET_ALM_H:
                pos = POS_LEFT;
                sym = SYM_ALM;
                digits = almH;
            break;
            case CLK_SET_ALM_M:
                pos = POS_CENTER;
                sym = SYM_ALM;
                digits = almM;
            break;
            case CLK_SET_TIM_H:
                pos = POS_LEFT;
                sym = SYM_TIME;
                digits = timeH;
            break;
            case CLK_SET_TIM_M:
                pos = POS_CENTER;
                sym = SYM_TIME;
                digits = timeM;
            break;
            case CLK_SET_TIM_S:
                pos = POS_RIGHT;
                sym = SYM_TIME;
                digits = timeS;
            break;
            case CLK_SET_DAT_D:
                pos = POS_LEFT;
                sym = SYM_DATE;
                digits = dateD;
            break;
            case CLK_SET_DAT_M:
                pos = POS_CENTER;
                sym = SYM_DATE;
                digits = dateM;
            break;
            case CLK_SET_DAT_Y:
                pos = POS_RIGHT;
                sym = SYM_DATE;
                pos = 2;
                digits = dateY;
            break;
            case CLK_SET_SPEED:
                digits = char_speed;
                pos = POS_CENTER;
                sym = SYM_SETUP;
            break;
        }
        if(encoderCount){
            // reset timeout
            timer.stop(idleTimer);
            idleTimer = timer.after(TIMEOUT, setIdle);
            // Update...
            switch(clockStatus) {
                case CLK_SET_ALM_H:
                    almH += encoderCount;
                    if(almH > 23) almH = 0;
                    store_config_delayed();
                break;
                case CLK_SET_ALM_M:
                    almM += encoderCount;
                    if(almM > 59) almM = 0;
                    store_config_delayed();
                break;
                case CLK_SET_TIM_H:
                    timeH  += encoderCount;
                    if(timeH > 23) timeH = 0;
                    storeDateTime();
                break;
                case CLK_SET_TIM_M:
                    timeM += encoderCount;
                    if(timeM > 59) timeM = 0;
                    storeDateTime();
                break;
                case CLK_SET_TIM_S:
                    timeS += encoderCount;
                    if(timeS > 59) timeS = 0;
                    storeDateTime();
                break;
                case CLK_SET_DAT_D:
                    dateD += encoderCount;
                    if(dateD > 31)dateD = 1;
                    storeDateTime();
                break;
                case CLK_SET_DAT_M:
                    dateM += encoderCount;
                    if(dateM > 12) dateM= 1;
                    storeDateTime();
                break;
                case CLK_SET_DAT_Y:
                    dateY += encoderCount;
                    if(dateY > 99) dateY = 0;
                    storeDateTime();
                break;
                case CLK_SET_SPEED:
                    char_speed+= encoderCount;
                    if(char_speed > 30) char_speed = 10;
                    if(char_speed < 10) char_speed = 30;
                    store_config_delayed();
                break;
           }
        }
        load_digits(char_buffer, digits, pos, sym);
        setSprite(char_buffer);
    } else {
        //strcpy_P(buf, PSTR("Ciao Leo... sono le"));
        //print_string((char*) buf);
#if DEBUG_CLOCK
        //sprintf(buf, "E: %d S: %d", encoderCount, clockStatus);
        //print_string(buf);
        //sprintf_P(buf, PSTR("F:%d"), memoryFree());
        //print_string(buf);
        sprintf_P(buf, PSTR("B:%d"), brightness);
        print_string(buf);
#endif
        sprintf_P(buf, PSTR("%02d:%02d:%02d"), now.hour(), now.minute(), now.second());
        print_string(buf);

        if(print_date++ == PRINT_DATE_EVERY_CICLES){
            print_date = 0;
            sprintf_P(buf, PSTR("%02d-%02d-%d"), now.day(), now.month(), now.year());
            print_string(buf);
        }

        // Show alarm
        if(alarmSwitchClosed && alarmStatus == ALM_STOP){
            sprintf_P(buf, PSTR("%c %02d:%02d"), 13, almH, almM);
            print_string(buf);
        }
    }

    if(timeM == 0){
        check_birthday();
    }

    // Hand timer
    if(alarmStatus != ALM_STOP && minutes_after_alarm >= ALM_RESET_AFTER_MINUTES){
        alarmStatus = ALM_STOP;
    }

     timer.update();

}