Digital Clock on Arduino (Uno) with Nokia LCD Display

When I heard about Arduino a couple years ago (yes, I was a bit late to the party) I immediately fell in love with it. The main reason was that it allowed me to try things that otherwise would require a lot of time and effort from a total electronics noob like me. With Arduino, a breadboard, a few LEDs and a couple of cables I was ready to write my very first program – controlling the LED with Arduino. It was fun! But I wanted to do something more interesting. I figured that while flashing a LED is fun displaying something on a screen must be even more fun. Soon I found that it was possible to buy a cheap Nokia 5110 LCD displays on ebay. I had some doubts whether I would be able to make it work but figured out that even if I wouldn’t I could always use it as a key chain or a similar geeky gadget. Indeed, when the display arrived I did have some problems to make it work. First, I was not sure how to connect the display to Arduino. Even though I found a few websites that showed how to do that I had hard time to make it work. I also tried a couple of sample programs that in theory should display something on the display but in practice they did not. Eventually, I found that one of the cables were connected incorrectly and the sequence to initialize the display did not work (at least on my display). Seeing something on the screen was a huge step forward and I finally could think about using the display for my own purpose. I decided that I could do a digital clock as it seemed a reasonably sized project which would still require understanding how the display really works. First I wanted to “design” fonts for the clock. For this I just used graph paper and this reminded me designing sprites for my C-64 in the late eighties. After designing all digits I had to understand how to encode these so that they could be drawn easily on the screen. I found the datasheet for the PCD8544 which is a controller for the display. This was the first time in my life I had to read a datasheet for an electronic component but I was able to understand what the most important things and the datasheet turned out to be an interesting read. Soon, I found what I wanted – the display has 6 rows and 84 columns. Each column in a row has 8 pixels. To draw something on a screen first you need to set a pointer (row and column) and then write a byte (8 bits) to draw the column. When you draw a column the pointer automatically advances to the next column – this is handy since you don’t really have to set the pointer all the time if you draw something like a bitmap. The mechanics of drawing on the screen surprised me. Firstly, I found it kind of similar to advanced graphics modes on 8-bit computers. Secondly, it was interesting to see that the programmer’s convenience totally lost with the low level design of the controller (or may be I am just spoiled by higher level APIs which allow me just to pass x and y to draw a point). After figuring out how drawing looks like encoding my digits was relatively simple. Now that I was able to draw digits on the screen it was the time to implement the clock. Initially I wanted to use the loop function but I quickly realized that it would not really work for a clock. I did not know how long drawing would take and therefore the clock would not work correctly. I could calculate how much time drawing took and then cut the wait time accordingly but it didn’t feel like the right solution. Obviously, there was one more (and I believe the only correct for this kind of problem) solution – interrupts. The only problem was to set everything up correctly. I spent some time reading Arduino datasheet but things did not really want to work. Fortunately I found this page which helped tremendously. After setting up interrupts things went smoothly and soon I had my digital clock up and running. I actually ended up using both the loop function and the interrupt. The interrupt just increases the timer while all the drawing takes place in the loop function. This way I don’t have to worry that drawing takes too much time and when a new interrupt happens the previous one is still being handled. That’s pretty much it. Here is a short video showing the clock in action:

If you want to try my project here is how I connected the Nokia 5110 display to Arduino:

Arduino Uno               Nokia 5110 Display
3.3V   ------------------ 1-VCC
PIN #7 ------------------ 3-SCE
PIN #6 ------------------ 4-RST
PIN #5 ------------------ 5-D/C
PIN #4 ------------------ 6-DNK(MOSI) (SDIN)
PIN #3 ------------------ 7-SCLK

I also posted the sketch on my github.

One more thing I always wanted to do but never really have had time to do was to add some buttons to enable setting the clock – at the moment the clock always starts at 00:00. Maybe one day I will come back to this project again (like I did today just to publish it) and will add the buttons…

Pawel Kadluczka

17 thoughts on “Digital Clock on Arduino (Uno) with Nokia LCD Display

    1. From what I can see Nokia 3310 LCD uses the same driver (i.e. PCD8544) so it should work. I don’t have this display though, so I have not tried this and I don’t know how to connect it to Arduino (hopefully it is similar to 5110 display)…

      Like

        1. Trudno mi odpowiedzieć na Twoje pytanie ze względu na zbyt duże pole do interpretacji. Czy możesz rozwinąć termin “przyciski do regulacji”?

          Like

    1. Tak, kiedyś się nad tym zastanawiałem ale ostatecznie mi się nie udało a potem przeszedłem do innych projektów. Wydaje mi się, że dodanie samemu nie powinno być trudne – jest sporo tutoriali na youtube.

      Like

  1. hi, very good design with large digits, implemented the buttons of Accrued hour and minute with pull-up resistors, the only problem with it is to tell the button only once, this project taking advantage of the interruption, the buttons were fully functional, I’ll try to post the code, if it is useful, but very congratulations creation

    #
    #define PIN_SCE 7
    #define PIN_RESET 6
    #define PIN_DC 5
    #define PIN_SDIN 4
    #define PIN_SCLK 3
    #define LCD_CMD 0

    #define LCD_C LOW
    #define LCD_D HIGH

    #define LCD_X 84
    #define LCD_Y 48

    const int minuto = 11;
    const int hora = 12;

    static const byte Digits[][4][18] =
    {
    {
    { 0xE0, 0xF0, 0xF8, 0xF4, 0xEE, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0xEE, 0xF4, 0xF8, 0xF0, 0xE0 },
    { 0x1F, 0x3F, 0x7F, 0x3F, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x3F, 0x7F, 0x3F, 0x1F },
    { 0xFC, 0xFE, 0xFF, 0xFE, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0xFE, 0xFF, 0xFE, 0xFC },
    { 0x03, 0x07, 0x0F, 0x17, 0x3B, 0x7C, 0x7C, 0x7C, 0x7C, 0x7C, 0x7C, 0x7C, 0x7C, 0x3B, 0x17, 0x0F, 0x07, 0x03 },
    },
    {
    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xF0, 0xF8, 0xF0, 0xE0 },
    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x3F, 0x7F, 0x3F, 0x1F },
    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0xFE, 0xFF, 0xFE, 0xFC },
    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x07, 0x0F, 0x07, 0x03 },
    },
    {
    { 0x00, 0x00, 0x00, 0x04, 0x0E, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0xEE, 0xF4, 0xF8, 0xF0, 0xE0 },
    { 0x00, 0x00, 0x00, 0x80, 0xC0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xDF, 0xBF, 0x7F, 0x3F, 0x1F },
    { 0xFC, 0xFE, 0xFF, 0xFE, 0xFD, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x01, 0x00, 0x00, 0x00, 0x00 },
    { 0x03, 0x07, 0x0F, 0x17, 0x3B, 0x7C, 0x7C, 0x7C, 0x7C, 0x7C, 0x7C, 0x7C, 0x7C, 0x38, 0x10, 0x00, 0x00, 0x00 },
    },
    {
    { 0x00, 0x00, 0x00, 0x04, 0x0E, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0xEE, 0xF4, 0xF8, 0xF0, 0xE0 },
    { 0x00, 0x00, 0x00, 0x80, 0xC0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xDF, 0xBF, 0x7F, 0x3F, 0x1F },
    { 0x00, 0x00, 0x00, 0x00, 0x01, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0xFD, 0xFE, 0xFF, 0xFE, 0xFC },
    { 0x00, 0x00, 0x00, 0x10, 0x38, 0x7C, 0x7C, 0x7C, 0x7C, 0x7C, 0x7C, 0x7C, 0x7C, 0x3B, 0x17, 0x0F, 0x07, 0x03 },
    },
    {
    { 0xE0, 0xF0, 0xF8, 0xF0, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xF0, 0xF8, 0xF0, 0xE0 },
    { 0x1F, 0x3F, 0x7F, 0xBF, 0xDF, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xDF, 0xBF, 0x7F, 0x3F, 0x1F },
    { 0x00, 0x00, 0x00, 0x00, 0x01, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0xFD, 0xFE, 0xFF, 0xFE, 0xFC },
    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x07, 0x0F, 0x07, 0x03 },
    },
    {
    { 0xE0, 0xF0, 0xF8, 0xF4, 0xEE, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x0E, 0x04, 0x00, 0x00, 0x00 },
    { 0x1F, 0x3F, 0x7F, 0xBF, 0xDF, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xC0, 0x80, 0x00, 0x00, 0x00 },
    { 0x00, 0x00, 0x00, 0x00, 0x01, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0xFD, 0xFE, 0xFF, 0xFE, 0xFC },
    { 0x00, 0x00, 0x00, 0x10, 0x38, 0x7C, 0x7C, 0x7C, 0x7C, 0x7C, 0x7C, 0x7C, 0x7C, 0x3B, 0x17, 0x0F, 0x07, 0x03 },
    },
    {
    { 0xE0, 0xF0, 0xF8, 0xF4, 0xEE, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x0E, 0x04, 0x00, 0x00, 0x00 },
    { 0x1F, 0x3F, 0x7F, 0xBF, 0xDF, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xC0, 0x80, 0x00, 0x00, 0x00 },
    { 0xFC, 0xFE, 0xFF, 0xFE, 0xFD, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0xFD, 0xFE, 0xFF, 0xFE, 0xFC },
    { 0x03, 0x07, 0x0F, 0x17, 0x3B, 0x7C, 0x7C, 0x7C, 0x7C, 0x7C, 0x7C, 0x7C, 0x7C, 0x3B, 0x17, 0x0F, 0x07, 0x03 },
    },
    {
    { 0x00, 0x00, 0x00, 0x04, 0x0E, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0xEE, 0xF4, 0xF8, 0xF0, 0xE0 },
    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x3F, 0x7F, 0x3F, 0x1F },
    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0xFE, 0xFF, 0xFE, 0xFC },
    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x07, 0x0F, 0x07, 0x03 },
    },
    {
    { 0xE0, 0xF0, 0xF8, 0xF4, 0xEE, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0xEE, 0xF4, 0xF8, 0xF0, 0xE0 },
    { 0x1F, 0x3F, 0x7F, 0xBF, 0xDF, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xDF, 0xBF, 0x7F, 0x3F, 0x1F },
    { 0xFC, 0xFE, 0xFF, 0xFE, 0xFD, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0xFD, 0xFE, 0xFF, 0xFE, 0xFC },
    { 0x03, 0x07, 0x0F, 0x17, 0x3B, 0x7C, 0x7C, 0x7C, 0x7C, 0x7C, 0x7C, 0x7C, 0x7C, 0x3B, 0x17, 0x0F, 0x07, 0x03 },
    },
    {
    { 0xE0, 0xF0, 0xF8, 0xF4, 0xEE, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0xEE, 0xF4, 0xF8, 0xF0, 0xE0 },
    { 0x1F, 0x3F, 0x7F, 0xBF, 0xDF, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xDF, 0xBF, 0x7F, 0x3F, 0x1F },
    { 0x00, 0x00, 0x00, 0x00, 0x01, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0xFD, 0xFE, 0xFF, 0xFE, 0xFC },
    { 0x00, 0x00, 0x00, 0x10, 0x38, 0x7C, 0x7C, 0x7C, 0x7C, 0x7C, 0x7C, 0x7C, 0x7C, 0x3B, 0x17, 0x0F, 0x07, 0x03 },
    }
    };

    static const byte SecondIndicator[4] =
    {
    0x00, 0x07, 0x70, 0x00
    };

    void LcdInitialise(void)
    {
    pinMode(PIN_SCE, OUTPUT);
    pinMode(PIN_RESET, OUTPUT);
    pinMode(PIN_DC, OUTPUT);
    pinMode(PIN_SDIN, OUTPUT);
    pinMode(PIN_SCLK, OUTPUT);
    digitalWrite(PIN_RESET, LOW);
    digitalWrite(PIN_RESET, HIGH);

    LcdWrite( LCD_CMD, 0x21 ); // LCD Extended Commands.
    LcdWrite( LCD_CMD, 0xC8 ); // Set LCD Vop (Contrast)
    LcdWrite( LCD_CMD, 0x06 ); // Set Temp coefficent
    LcdWrite( LCD_CMD, 0x14 ); // LCD bias mode 1:48

    LcdWrite( LCD_CMD, 0x20 ); // LCD Standard Commands.
    LcdWrite( LCD_CMD, 0x0C ); // LCD in normal mode. 0x0d for inverse
    }

    void LcdWrite(byte dc, byte data)
    {
    digitalWrite(PIN_DC, dc);
    digitalWrite(PIN_SCE, LOW);
    shiftOut(PIN_SDIN, PIN_SCLK, MSBFIRST, data);
    digitalWrite(PIN_SCE, HIGH);
    }

    void LcdClear(void)
    {
    for (int index = 0; index < LCD_X * LCD_Y / 8; index++)
    {
    LcdWrite(LCD_D, 0x00);
    }
    }

    void Spacer()
    {
    LcdWrite(LCD_D, 0x00);
    LcdWrite(LCD_D, 0x00);
    }

    void DisplayTime(byte hour, byte minutes, byte seconds)
    {
    byte components[4] =
    {
    (byte)(hour / 10),
    (byte)(hour % 10),
    (byte)(minutes / 10),
    (byte)(minutes % 10)
    };

    for(byte row = 0; row < 4; row++)
    {
    LcdWrite(LCD_C, 0x80 | 0);
    LcdWrite(LCD_C, 0x40 | row);

    for(byte digit = 0; digit < 4; digit++)
    {
    for(byte col = 0; col < 18; col++)
    {
    LcdWrite(LCD_D, Digits[components[digit]][row][col]);
    }

    Spacer();

    // Display second indicator after the second digit
    if(digit == 1)
    {
    DisplaySecondIndicator(row, seconds & 0x01);
    }
    }
    }

    DrawSecondsBar(seconds);
    }

    void DisplaySecondIndicator(byte row, boolean show)
    {
    for(int secondIndicatorSegment = 0; secondIndicatorSegment < 3; secondIndicatorSegment++)
    {
    if(show)
    {
    LcdWrite(LCD_D, SecondIndicator[row]);
    }
    else // clear
    {
    LcdWrite(LCD_D, 0x00);
    }
    }

    Spacer();
    }

    void DrawSecondsBar(byte seconds)
    {
    // Position the pointer
    LcdWrite(LCD_C, 0x80 | 0x0b);
    LcdWrite(LCD_C, 0x44);

    // Draw the left side of the progress bar box
    LcdWrite(LCD_D, 0xF0);

    for(byte i = 0; i < 59; i++)
    {
    if(i < seconds)
    {
    LcdWrite(LCD_D, 0xF0);
    }
    else
    {
    LcdWrite(LCD_D, 0x90);
    }
    }

    // Draw the right side of the progress bar box
    LcdWrite(LCD_D, 0xF0);
    }

    byte tcnt2;
    unsigned long time = 0; // 86390000;
    int a =0, b=0,k=0;

    void setup(void)
    {
    SetupInterrupt();
    InitializeDisplay();
    pinMode(minuto, INPUT);
    pinMode(hora, INPUT);
    }

    // Credits for the interrupt setup routine:
    // http://popdevelop.com/2010/04/mastering-timer-interrupts-on-the-arduino/
    void SetupInterrupt()
    {
    /* First disable the timer overflow interrupt while we're configuring */
    TIMSK2 &= ~(1<<TOIE2);

    /* Configure timer2 in normal mode (pure counting, no PWM etc.) */
    TCCR2A &= ~((1<<WGM21) | (1<<WGM20));
    TCCR2B &= ~(1<<WGM22);

    /* Select clock source: internal I/O clock */
    ASSR &= ~(1<<AS2);

    /* Disable Compare Match A interrupt enable (only want overflow) */
    TIMSK2 &= ~(1<<OCIE2A);

    /* Now configure the prescaler to CPU clock divided by 128 */
    TCCR2B |= (1<<CS22) | (1<<CS20); // Set bits
    TCCR2B &= ~(1<<CS21); // Clear bit

    /* We need to calculate a proper value to load the timer counter.
    * The following loads the value 131 into the Timer 2 counter register
    * The math behind this is:
    * (CPU frequency) / (prescaler value) = 125000 Hz = 8us.
    * (desired period) / 8us = 125.
    * MAX(uint8) + 1 – 125 = 131;
    */
    /* Save value globally for later reload in ISR */
    tcnt2 = 131;

    /* Finally load end enable the timer */
    TCNT2 = tcnt2;
    TIMSK2 |= (1<<TOIE2);
    }

    void InitializeDisplay()
    {
    LcdInitialise();
    LcdClear();
    }

    /*
    * Install the Interrupt Service Routine (ISR) for Timer2 overflow.
    * This is normally done by writing the address of the ISR in the
    * interrupt vector table but conveniently done by using ISR() */
    ISR(TIMER2_OVF_vect) {
    /* Reload the timer */
    TCNT2 = tcnt2;

    time++;
    time = time % 86400000;

    a = digitalRead(minuto);
    b = digitalRead(hora);
    if(k==0){
    if(a == LOW) time = time + 60000;
    if(b == LOW) time = time + 3600000;
    }
    if((a==LOW)||(b==LOW)) k=1;
    else k=0;
    }

    void loop(void)
    {

    unsigned long t = (unsigned long)(time/1000);

    DisplayTime((byte)(t / 3600), (byte)((t / 60) % 60), (byte)(t % 60));

    }

    Like

    1. There is no magic way of doing this. The shapes of the digits are define in the Digits array. Think of them as of bitmaps. To increase the size of the fonts you will have to redefine them. Likely you will also have to change the logic to display the digits a little bit because this logic assumes that the current height and width of the digits.

      Like

Leave a comment