Clock on a LCD 3310

Share:

Motivation and overview

mini screenshotFor some time I wanted to build a clock using a Nokia 3310 LCD but never had the time to create the digital font big enough to be readable at a distance and nice looking. After finding the Arduino code published by Pawel Kadluczka, on his blog Code, the Universe and everything, I converted it to use on a PIC 16F88.

There's only two hardware requirements for my version of the code to run on a PIC:

All code is in a single file to make it easy to understand. 

 

Schematic

The circuit runs from 5V and has its own 3.3V regulator. You can remove the 3.3V regulator and power it directly from 3.3V but the back light will be dimmer.

The schematic shows a PIC 16F88 with 2 crystals X1 and X2 but only X2 is really required because this PIC has an internal oscillator that you can use instead of X1. You can safely remove X1, C7 and C8 as long as you change the FUSES line in the code to setup the internal RC oscillator.

schematic

PCB

None, although the prototype was tested in a PCB from another project.

Mount it on a breadboard.

Software

/*
* Clock for LCD 3310
* Adapted from arduino code by moozzyk
* Blog: http://blog.3d-logic.com/2012/08/26/digital-clock-on-arduino-uno-with-nokia-lcd-display/
* Source: https://github.com/moozzyk/ArduinoDigitalClock/blob/master/src/DigitalClock.pde
*
* Conversion made on 2015/06 Joao Figueiredo (http://www.enide.net)
*/

#include <htc.h>

// fuses for external crystal X1
__CONFIG(MCLREN & PWRTEN & BORDIS & LVPDIS & WDTDIS & HS);

// fuses for the internal oscillator, without X1, C7 and C8
//__CONFIG(MCLREN & PWRTEN & BOREN & LVPDIS & WDTDIS & INTIO);

#define LCDDATACOMM RB3
#define LCDSCE RB0
#define LCDRESET RB5
#define LCD_C 0
#define LCD_D 1

#define BACKLIGHT RA1
#define BACKLIGHTTRIS TRISA1

// --- Informative (not really used) -------------------------------
#define SDO RB2 // Serial Data Out
#define SDI RB1 // Serial Data In
#define SCK RB4 // Serial Clock



// ----------------------------------------------------------------------------
// SPI interface
// ----------------------------------------------------------------------------
void spiInit(void)
{
    TRISB2 = 0; // SDO
    TRISB1 = 1; // SDI
    TRISB4 = 0; // SCK
    SSPIE = 0; // Disable MSSP interrupt

    SSPSTAT = 0xC0; // 0b1100 0000 Sampled at end and data transmited at rising edge of clock
    SSPCON = 0x21; // 0b0010 0001 Enable CKP=0 SPI Master Fosc/16
}

void spiTxChar(unsigned char c)
{
    SSPBUF = c; // Set char in SSPBUF to start transmission
    while(!BF); // wait for data exchange to occur (polling)
    c=SSPBUF; // Removes the garbage char to complete the transmission
}
// ----------------------------------------------------------------------------


// ----------------------------------------------------------------------------
// Real time clock using a LP 32768Hz crystal
// Timer 1 @ 1Hz
// ----------------------------------------------------------------------------
static unsigned char hour = 0, mins = 0, secs = 0;
static bit time_changed;

void rtcInit(void)
{
    time_changed = 1; // compiler error: static initialization of bit types is illegal

    ANS5 = 0; // for pin 12 RB6/T1OSO
    ANS6 = 0; // for pin 13 RB7/T1OSI

    TMR1H = 0x80;
    TMR1L = 0;
    TMR1IF = 0;

    T1CON = 0x0F; // 16 bit timer with external clock
    TMR1IE = 1; // enable TMR1 interrupt to fetch a new sample in the pwm buffer
    PEIE = 1;
}

void rtcTick(void)
{
    // retrigger timer 1 (check datasheet example 7.3, page 77)
    TMR1H |= 0x80;
    TMR1IF = 0;

    ++secs;
    if (secs > 59)
    {
        secs = 0;
        ++mins;
        if (mins > 59)
        {
            mins = 0;
            ++hour;
            if (hour > 23)
            {
                hour = 0;
            }
        }
    }
    time_changed = 1;
}

bit rtcGetTime(unsigned char *h, unsigned char *m, unsigned char *s)
{
    *h = hour;
    *m = mins;
    *s = secs;

    if(time_changed)
    {
        time_changed = 0;
        return 1;
    }
    else
    {
        return 0;
    }
}
// ----------------------------------------------------------------------------


// ----------------------------------------------------------------------------
// Clock by moozzyk
// ----------------------------------------------------------------------------
const unsigned char 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 },
}
};

const unsigned char SecondIndicator[4] =
{
    0x00, 0x07, 0x70, 0x00
};

void LcdInitialise(void)
{
    PORTB = 0; // nah... just in case let's start with every pin at 0
    TRISB3 = 0; // D/_C
    TRISB0 = 0; // _SCE
    TRISB5 = 0; // _RES

    LCDRESET = 1; // RESET pin high
    asm("nop");
    asm("nop");
    asm("nop");
    asm("nop");
    asm("nop"); // 1us delay with a 20MHz XTAL
    LCDRESET = 0; // !!!!RESET!!!!

    LCDSCE = 1; // disable LCD CE
    LCDDATACOMM = 0;// change to command mode

    // minimum reset time is 100ns!

    LCDRESET = 1; // RESET over

    spiInit();

    // inititalization routine from the moozzyk (author of the clock)
    LCDSCE = 0;
    LCDDATACOMM = 0;

    spiTxChar( 0x21 ); // LCD Extended Commands.
    spiTxChar( 0x80 | 0x0A /*0xC8*/ ); // Set LCD Vop (Contrast)
    spiTxChar( 0x06 ); // Set Temp coefficent
    spiTxChar( 0x14 ); // LCD bias mode 1:48

    spiTxChar( 0x20 ); // LCD Standard Commands.
    spiTxChar( 0x0C ); // LCD in normal mode. 0x0d for inverse

    LCDSCE = 0;
}

void LcdWrite(unsigned char dc, unsigned char data)
{
    LCDSCE = 0; // enable LCD

    LCDDATACOMM = dc; // COMMAND/DATA mode depending on argument
    spiTxChar( data );

    LCDSCE = 1; // disable LCD
}

void LcdClear(void)
{
    unsigned int i;

    LCDSCE = 0; // enable LCD

    LCDDATACOMM = 0; // COMMAND mode and send GotoXY
    spiTxChar( 0x80 | 0x00 ); // x=0
    spiTxChar( 0x40 | 0x00 ); // y=0

    LCDDATACOMM = 1; // DATA mode
    for(i=0; i<504; i++)
        spiTxChar(0); // fill entire screen with 0

    LCDSCE = 1; // disable LCD
}

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

void DrawSecondsBar(unsigned char 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(unsigned char 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);
}

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

    Spacer();
}

void DisplayTime(unsigned char hour, unsigned char minutes, unsigned char seconds)
{
    unsigned char row;
    unsigned char col;
    unsigned char digit;
    unsigned char components[4];
    components[0] = (unsigned char)(hour / 10);
    components[1] = (unsigned char)(hour % 10);
    components[2] = (unsigned char)(minutes / 10);
    components[3] = (unsigned char)(minutes % 10);

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

        for(digit = 0; digit < 4; digit++)
        {
            for(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 InitializeDisplay()
{
    LcdInitialise();
    LcdClear();
}
// ----------------------------------------------------------------------------


void setup(void)
{
    BACKLIGHTTRIS = 0;
    BACKLIGHT = 1; // backlight on

    CMCON = 0x07; // disable the analog comparator
    CVRCON = 0x00; // disable the voltage reference module
    ANSEL = 0; // disable ADC

    rtcInit();

    InitializeDisplay();

    GIE = 1;
}

void interrupt isr(void)
{
    if (TMR1IF)
    {
        rtcTick();
    }
}

void loop(void)
{
    unsigned char h, m, s;
    rtcGetTime(&h, &m, &s);

    DisplayTime(h, m, s);

    asm("sleep");
}

void main(void)
{
    setup();
    while(1)
        loop();
}

Prototype

The prototype in the pictures below runs on the same PCB of Baby Night Light & LCD project because it was faster to test the code using that PCB instead of using a breadboard.

Pictures and Videos

First picture of the clock running after power on. The bar under the digits represents the seconds and fills until they reach 59.

clock screenshot

 

Demonstration video

Downloads

List of files from the project

 

References

  1. http://blog.3d-logic.com/2012/08/26/digital-clock-on-arduino-uno-with-nokia-lcd-display/ - Blog post with the original Arduino code
  2. Baby night light & LCD - PCB used to test the code

 

Published on Friday 2015/06/26, last modified on Monday 2015/07/13